chipgraph 0.1.0rc1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chipgraph/__init__.py +7 -0
- chipgraph/adapters/__init__.py +1 -0
- chipgraph/adapters/format/__init__.py +6 -0
- chipgraph/adapters/format/__main__.py +16 -0
- chipgraph/adapters/format/_yaml_lines.py +54 -0
- chipgraph/adapters/format/chip_yaml.py +323 -0
- chipgraph/adapters/format/qsoc_contract.py +246 -0
- chipgraph/adapters/llm/__init__.py +58 -0
- chipgraph/adapters/llm/_budget.py +168 -0
- chipgraph/adapters/llm/_errors.py +50 -0
- chipgraph/adapters/llm/_nda.py +30 -0
- chipgraph/adapters/llm/_retry.py +85 -0
- chipgraph/adapters/llm/anthropic.py +240 -0
- chipgraph/adapters/llm/decide_backend.py +179 -0
- chipgraph/adapters/llm/fake.py +87 -0
- chipgraph/adapters/llm/guard.py +68 -0
- chipgraph/adapters/parser/__init__.py +22 -0
- chipgraph/adapters/parser/generic_regex.py +75 -0
- chipgraph/adapters/parser/verible.py +55 -0
- chipgraph/adapters/parser/verilator.py +46 -0
- chipgraph/adapters/review/__init__.py +7 -0
- chipgraph/adapters/review/file.py +123 -0
- chipgraph/adapters/runner/__init__.py +7 -0
- chipgraph/adapters/runner/local.py +89 -0
- chipgraph/adapters/runtime/__init__.py +5 -0
- chipgraph/adapters/runtime/claude_code/__init__.py +18 -0
- chipgraph/adapters/runtime/claude_code/decisions.py +280 -0
- chipgraph/adapters/runtime/claude_code/runtime.py +102 -0
- chipgraph/adapters/runtime/claude_code/service.py +590 -0
- chipgraph/adapters/tool/__init__.py +7 -0
- chipgraph/adapters/tool/cmd.py +229 -0
- chipgraph/adapters/tool/filelist.py +259 -0
- chipgraph/adapters/tool/pyslang.py +485 -0
- chipgraph/adapters/vcs/__init__.py +1 -0
- chipgraph/adapters/vcs/git.py +107 -0
- chipgraph/app/__init__.py +10 -0
- chipgraph/app/baseline.py +339 -0
- chipgraph/app/build.py +139 -0
- chipgraph/app/checks.py +193 -0
- chipgraph/app/context.py +178 -0
- chipgraph/app/errors.py +11 -0
- chipgraph/app/executors.py +79 -0
- chipgraph/app/findings.py +203 -0
- chipgraph/app/ingest.py +523 -0
- chipgraph/checks/__init__.py +52 -0
- chipgraph/checks/_cdc_analyze.py +272 -0
- chipgraph/checks/_cdc_elaborate.py +455 -0
- chipgraph/checks/_common.py +268 -0
- chipgraph/checks/_cross.py +96 -0
- chipgraph/checks/_model.py +64 -0
- chipgraph/checks/_naming_pyslang.py +235 -0
- chipgraph/checks/_naming_rules.py +127 -0
- chipgraph/checks/_xref_syntax.py +260 -0
- chipgraph/checks/cdc_struct.py +235 -0
- chipgraph/checks/connect.py +353 -0
- chipgraph/checks/cross_chip.py +454 -0
- chipgraph/checks/duplicate.py +151 -0
- chipgraph/checks/filelist.py +323 -0
- chipgraph/checks/generated.py +151 -0
- chipgraph/checks/hardcode.py +233 -0
- chipgraph/checks/layout.py +155 -0
- chipgraph/checks/naming.py +311 -0
- chipgraph/checks/ports_diff.py +203 -0
- chipgraph/checks/spec_schema.py +257 -0
- chipgraph/checks/trace.py +207 -0
- chipgraph/cli/__init__.py +1500 -0
- chipgraph/core/__init__.py +1 -0
- chipgraph/core/config/__init__.py +81 -0
- chipgraph/core/config/defaults.py +14 -0
- chipgraph/core/config/errors.py +32 -0
- chipgraph/core/config/loader.py +598 -0
- chipgraph/core/config/models.py +612 -0
- chipgraph/core/config/templates.py +340 -0
- chipgraph/core/contracts/__init__.py +75 -0
- chipgraph/core/contracts/_paths.py +26 -0
- chipgraph/core/contracts/agent.py +49 -0
- chipgraph/core/contracts/artifact.py +68 -0
- chipgraph/core/contracts/check.py +76 -0
- chipgraph/core/contracts/event.py +70 -0
- chipgraph/core/contracts/export.py +128 -0
- chipgraph/core/contracts/finding.py +171 -0
- chipgraph/core/contracts/gate.py +50 -0
- chipgraph/core/contracts/rule.py +177 -0
- chipgraph/core/contracts/types.py +45 -0
- chipgraph/core/engine/__init__.py +70 -0
- chipgraph/core/engine/baseline.py +283 -0
- chipgraph/core/engine/decide.py +462 -0
- chipgraph/core/engine/gate.py +170 -0
- chipgraph/core/engine/graph.py +533 -0
- chipgraph/core/engine/records.py +60 -0
- chipgraph/core/engine/rules.py +92 -0
- chipgraph/core/engine/scheduler.py +540 -0
- chipgraph/core/model/__init__.py +98 -0
- chipgraph/core/model/entities.py +244 -0
- chipgraph/core/model/export.py +16 -0
- chipgraph/core/model/ingest.py +693 -0
- chipgraph/core/model/json_value.py +4 -0
- chipgraph/core/model/keys.py +95 -0
- chipgraph/core/model/model.py +153 -0
- chipgraph/core/model/provenance.py +27 -0
- chipgraph/core/model/query.py +583 -0
- chipgraph/core/model/registry.py +80 -0
- chipgraph/core/model/relations.py +61 -0
- chipgraph/core/model/store.py +279 -0
- chipgraph/core/plugin_api/__init__.py +81 -0
- chipgraph/core/plugin_api/local.py +167 -0
- chipgraph/core/plugin_api/pack.py +159 -0
- chipgraph/core/plugin_api/protocols.py +177 -0
- chipgraph/core/plugin_api/registry.py +111 -0
- chipgraph/core/plugin_api/types.py +94 -0
- chipgraph/core/runtime/__init__.py +40 -0
- chipgraph/core/runtime/executor.py +79 -0
- chipgraph/core/runtime/queue.py +380 -0
- chipgraph/core/state/__init__.py +1 -0
- chipgraph/core/state/artifacts.py +156 -0
- chipgraph/core/state/backend.py +117 -0
- chipgraph/core/state/findings.py +151 -0
- chipgraph/core/state/handoff.py +321 -0
- chipgraph/core/state/idempotency.py +59 -0
- chipgraph/core/state/journal.py +238 -0
- chipgraph/core/state/layout.py +73 -0
- chipgraph/core/state/lock.py +195 -0
- chipgraph/core/state/trace.py +225 -0
- chipgraph/learn/__init__.py +96 -0
- chipgraph/learn/draft.py +187 -0
- chipgraph/learn/infer.py +580 -0
- chipgraph/learn/models.py +145 -0
- chipgraph/learn/try_run.py +228 -0
- chipgraph/mcp/__init__.py +12 -0
- chipgraph/mcp/model_tools.py +167 -0
- chipgraph/mcp/server.py +605 -0
- chipgraph/orgs/qnsc/naming-v1.yml +113 -0
- chipgraph/orgs/qnsc/profile.yml +5 -0
- chipgraph/packs/__init__.py +8 -0
- chipgraph/packs/assist/__init__.py +16 -0
- chipgraph/packs/assist/ask/__init__.py +58 -0
- chipgraph/packs/assist/ask/_db.py +57 -0
- chipgraph/packs/assist/ask/_project.py +230 -0
- chipgraph/packs/assist/ask/answer.py +266 -0
- chipgraph/packs/assist/ask/check.py +182 -0
- chipgraph/packs/assist/ask/contract.py +115 -0
- chipgraph/packs/assist/ask/documents.py +157 -0
- chipgraph/packs/assist/ask/fts.py +80 -0
- chipgraph/packs/assist/ask/retrieve.py +331 -0
- chipgraph/packs/assist/audit/__init__.py +383 -0
- chipgraph/packs/assist/pack.yml +13 -0
- chipgraph/packs/assist/triage/__init__.py +64 -0
- chipgraph/packs/assist/triage/contract.py +94 -0
- chipgraph/packs/assist/triage/parse.py +371 -0
- chipgraph/packs/assist/triage/question.py +384 -0
- chipgraph/packs/assist/triage/report.py +160 -0
- chipgraph/packs/assist/triage/rules.py +447 -0
- chipgraph/packs/assist/triage/run.py +224 -0
- chipgraph/packs/spec_core/__init__.py +9 -0
- chipgraph/packs/spec_core/extract/__init__.py +12 -0
- chipgraph/packs/spec_core/extract/_markdown.py +371 -0
- chipgraph/packs/spec_core/extract/_text.py +116 -0
- chipgraph/packs/spec_core/extract/mas.py +895 -0
- chipgraph/packs/spec_core/gen/__init__.py +11 -0
- chipgraph/packs/spec_core/gen/agents_md/__init__.py +148 -0
- chipgraph/packs/spec_core/gen/agents_md/_context.py +239 -0
- chipgraph/packs/spec_core/gen/agents_md/_markers.py +131 -0
- chipgraph/packs/spec_core/gen/agents_md/agents_md.md.j2 +178 -0
- chipgraph/packs/spec_core/gen/diagram/__init__.py +110 -0
- chipgraph/packs/spec_core/gen/diagram/__main__.py +44 -0
- chipgraph/packs/spec_core/gen/diagram/_diagrams.py +403 -0
- chipgraph/packs/spec_core/gen/diagram/_scene.py +163 -0
- chipgraph/packs/spec_core/pack.yml +6 -0
- chipgraph/py.typed +0 -0
- chipgraph-0.1.0rc1.dist-info/METADATA +50 -0
- chipgraph-0.1.0rc1.dist-info/RECORD +174 -0
- chipgraph-0.1.0rc1.dist-info/WHEEL +4 -0
- chipgraph-0.1.0rc1.dist-info/entry_points.txt +48 -0
- chipgraph-0.1.0rc1.dist-info/licenses/LICENSE +202 -0
chipgraph/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Adapters: tools, formats, LLMs, runners, VCS, review. Core never imports this package."""
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Format adapters: load facts from domain-specific file formats into the Design Model."""
|
|
2
|
+
|
|
3
|
+
from chipgraph.adapters.format.chip_yaml import ChipYamlAdapter
|
|
4
|
+
from chipgraph.adapters.format.qsoc_contract import QSocContractAdapter
|
|
5
|
+
|
|
6
|
+
__all__ = ["ChipYamlAdapter", "QSocContractAdapter"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""`python -m chipgraph.adapters.format [PATH]`: write the chip.yml JSON Schema."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from chipgraph.adapters.format.chip_yaml import schema_json
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main() -> None:
|
|
10
|
+
out = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("schemas/formats/chip.schema.json")
|
|
11
|
+
out.write_text(schema_json(), encoding="utf-8")
|
|
12
|
+
print(f"wrote {out}")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if __name__ == "__main__":
|
|
16
|
+
main()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""YAML loading that remembers the source line of every mapping key and list item."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
type YamlPath = tuple[str | int, ...]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LinedYaml:
|
|
15
|
+
"""A parsed YAML document plus the 1-based line of every node, by path.
|
|
16
|
+
|
|
17
|
+
`data` is what `yaml.safe_load` returns. `line(("memory_map", 3, "base"))` is the line
|
|
18
|
+
of that node; a path that is not found falls back to its longest known prefix, so an
|
|
19
|
+
error about a missing field still points at the item that lacks it.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, path: Path) -> None:
|
|
23
|
+
self.path = path
|
|
24
|
+
text = path.read_text(encoding="utf-8")
|
|
25
|
+
self.data: Any = yaml.safe_load(text)
|
|
26
|
+
self._lines: dict[YamlPath, int] = {}
|
|
27
|
+
root = yaml.compose(text, Loader=yaml.SafeLoader)
|
|
28
|
+
if root is not None:
|
|
29
|
+
self._index(root, ())
|
|
30
|
+
|
|
31
|
+
def _index(self, node: yaml.Node, at: YamlPath) -> None:
|
|
32
|
+
self._lines[at] = node.start_mark.line + 1
|
|
33
|
+
if isinstance(node, yaml.MappingNode):
|
|
34
|
+
for key_node, value_node in node.value:
|
|
35
|
+
key = str(key_node.value)
|
|
36
|
+
self._lines[(*at, key)] = key_node.start_mark.line + 1
|
|
37
|
+
self._index(value_node, (*at, key))
|
|
38
|
+
# A key's own line, not its value's (which may start on the next line).
|
|
39
|
+
self._lines[(*at, key)] = key_node.start_mark.line + 1
|
|
40
|
+
elif isinstance(node, yaml.SequenceNode):
|
|
41
|
+
for i, item in enumerate(node.value):
|
|
42
|
+
self._index(item, (*at, i))
|
|
43
|
+
|
|
44
|
+
def line(self, path: Sequence[str | int]) -> int:
|
|
45
|
+
"""The line of `path`, or of its longest known prefix (1 for the document)."""
|
|
46
|
+
at = tuple(path)
|
|
47
|
+
while at:
|
|
48
|
+
if at in self._lines:
|
|
49
|
+
return self._lines[at]
|
|
50
|
+
at = at[:-1]
|
|
51
|
+
return self._lines.get((), 1)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
__all__ = ["LinedYaml", "YamlPath"]
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Format adapter `chip-yaml`: chipgraph's own generic chip description (`chip.yml`).
|
|
2
|
+
|
|
3
|
+
`chip.yml` names a chip's buses, blocks, clocks and resets. Numbers that code can work
|
|
4
|
+
out may be left as `auto` (DESIGN.md 4.3, "code computes"):
|
|
5
|
+
|
|
6
|
+
- **base address** `auto`: blocks are placed in file order at the lowest free address
|
|
7
|
+
that is aligned to the block's size (a power of two) and overlaps no fixed or already
|
|
8
|
+
placed block. Needs `size`.
|
|
9
|
+
- **interrupt line** `auto`: interrupts get the lowest line number not used by a fixed
|
|
10
|
+
line or an earlier `auto`, in file order.
|
|
11
|
+
|
|
12
|
+
The model: a `project`; per block a `block` (ip, bus in `attrs`), a `memory_region` and
|
|
13
|
+
its `interrupt`s; per bus an `interface` (protocol as data); `clock`s and `reset`s.
|
|
14
|
+
Relations: `contains` project→block, block→region and block→interrupt, and `connects`
|
|
15
|
+
bus→block for each bus slave. A validation error names the file and the line of the item.
|
|
16
|
+
`load_model` also warns about overlapping regions and duplicate interrupt lines.
|
|
17
|
+
|
|
18
|
+
Regenerate `schemas/formats/chip.schema.json` with `python -m chipgraph.adapters.format`.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
from collections.abc import Iterable, Mapping
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Literal
|
|
27
|
+
|
|
28
|
+
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
|
29
|
+
|
|
30
|
+
from chipgraph.adapters.format._yaml_lines import LinedYaml
|
|
31
|
+
from chipgraph.adapters.format.qsoc_contract import (
|
|
32
|
+
Region,
|
|
33
|
+
_display_path,
|
|
34
|
+
contains,
|
|
35
|
+
duplicate_line_warnings,
|
|
36
|
+
model_facts,
|
|
37
|
+
overlap_warnings,
|
|
38
|
+
)
|
|
39
|
+
from chipgraph.core.model import (
|
|
40
|
+
BlockEntity,
|
|
41
|
+
ClockEntity,
|
|
42
|
+
DesignModel,
|
|
43
|
+
EntityBase,
|
|
44
|
+
InterfaceEntity,
|
|
45
|
+
InterruptEntity,
|
|
46
|
+
MemoryRegionEntity,
|
|
47
|
+
ProjectEntity,
|
|
48
|
+
Provenance,
|
|
49
|
+
Relation,
|
|
50
|
+
ResetEntity,
|
|
51
|
+
make_key,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
_EXTRACTOR = "chip-yaml"
|
|
55
|
+
|
|
56
|
+
type Auto = Literal["auto"]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class _Spec(BaseModel):
|
|
60
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ClockSpec(_Spec):
|
|
64
|
+
"""A clock."""
|
|
65
|
+
|
|
66
|
+
name: str
|
|
67
|
+
frequency_hz: float | None = Field(default=None, ge=0)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ResetSpec(_Spec):
|
|
71
|
+
"""A reset."""
|
|
72
|
+
|
|
73
|
+
name: str
|
|
74
|
+
active_low: bool | None = None
|
|
75
|
+
sync: bool | None = None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class InterruptSpec(_Spec):
|
|
79
|
+
"""One interrupt output of a block."""
|
|
80
|
+
|
|
81
|
+
name: str
|
|
82
|
+
line: int | Auto = Field(default="auto", description="Interrupt line, or 'auto'.")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class BlockSpec(_Spec):
|
|
86
|
+
"""A block: an IP instance on a bus."""
|
|
87
|
+
|
|
88
|
+
name: str
|
|
89
|
+
ip: str | None = Field(default=None, description="The IP it instantiates.")
|
|
90
|
+
bus: str | None = Field(default=None, description="The bus it is a slave of.")
|
|
91
|
+
base: int | Auto | None = Field(default=None, description="Base address, or 'auto'.")
|
|
92
|
+
size: int | None = Field(default=None, gt=0, description="Size of its window in bytes.")
|
|
93
|
+
interrupts: tuple[InterruptSpec, ...] = ()
|
|
94
|
+
clock: str | None = None
|
|
95
|
+
reset: str | None = None
|
|
96
|
+
owner: str | None = None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class BusSpec(_Spec):
|
|
100
|
+
"""A bus; the protocol is data (for example 'APB', 'AXI4')."""
|
|
101
|
+
|
|
102
|
+
name: str
|
|
103
|
+
protocol: str
|
|
104
|
+
masters: tuple[str, ...] = ()
|
|
105
|
+
slaves: tuple[str, ...] = ()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ChipYaml(_Spec):
|
|
109
|
+
"""The `chip.yml` schema."""
|
|
110
|
+
|
|
111
|
+
project: str
|
|
112
|
+
data_width: int = Field(ge=1)
|
|
113
|
+
addr_width: int = Field(ge=1)
|
|
114
|
+
buses: tuple[BusSpec, ...] = ()
|
|
115
|
+
blocks: tuple[BlockSpec, ...] = ()
|
|
116
|
+
clocks: tuple[ClockSpec, ...] = ()
|
|
117
|
+
resets: tuple[ResetSpec, ...] = ()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class ChipYamlAdapter:
|
|
121
|
+
"""`FormatAdapter` for `chip.yml`."""
|
|
122
|
+
|
|
123
|
+
name = "chip-yaml"
|
|
124
|
+
|
|
125
|
+
def load(self, path: Path) -> Iterable[Mapping[str, object]]:
|
|
126
|
+
"""The `FormatAdapter` protocol: every entity and relation as a plain mapping."""
|
|
127
|
+
model, _ = self.load_model(path)
|
|
128
|
+
return model_facts(model)
|
|
129
|
+
|
|
130
|
+
def load_model(self, path: Path, root: Path | None = None) -> tuple[DesignModel, list[str]]:
|
|
131
|
+
"""Load `path` into a `DesignModel`; raise `ValueError` naming file:line if invalid.
|
|
132
|
+
|
|
133
|
+
When `root` is given and `path` is under it, provenance `file` is written relative
|
|
134
|
+
to `root` (POSIX); otherwise it is `path.as_posix()` as before.
|
|
135
|
+
"""
|
|
136
|
+
doc = LinedYaml(path)
|
|
137
|
+
file = _display_path(path, root)
|
|
138
|
+
try:
|
|
139
|
+
chip = ChipYaml.model_validate(doc.data or {})
|
|
140
|
+
except ValidationError as exc:
|
|
141
|
+
problems = "; ".join(
|
|
142
|
+
f"{file}:{doc.line(err['loc'])}: {'.'.join(map(str, err['loc']))}: {err['msg']}"
|
|
143
|
+
for err in exc.errors()
|
|
144
|
+
)
|
|
145
|
+
raise ValueError(f"Invalid chip.yml {file}: {problems}") from exc
|
|
146
|
+
|
|
147
|
+
def src(*at: str | int) -> Provenance:
|
|
148
|
+
return Provenance(file=file, line=doc.line(at), extractor=_EXTRACTOR)
|
|
149
|
+
|
|
150
|
+
entities: list[EntityBase] = []
|
|
151
|
+
relations: list[Relation] = []
|
|
152
|
+
project_key = make_key("project", chip.project)
|
|
153
|
+
entities.append(
|
|
154
|
+
ProjectEntity(
|
|
155
|
+
key=project_key,
|
|
156
|
+
name=chip.project,
|
|
157
|
+
source=src("project"),
|
|
158
|
+
attrs={"data_width": chip.data_width, "addr_width": chip.addr_width},
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
bases = resolve_bases(chip.blocks)
|
|
163
|
+
lines = resolve_lines(chip.blocks)
|
|
164
|
+
regions: list[Region] = []
|
|
165
|
+
numbered: list[tuple[str, int, int]] = []
|
|
166
|
+
for i, block in enumerate(chip.blocks):
|
|
167
|
+
block_key = make_key("block", block.name)
|
|
168
|
+
entities.append(
|
|
169
|
+
BlockEntity(
|
|
170
|
+
key=block_key,
|
|
171
|
+
name=block.name,
|
|
172
|
+
owner=block.owner,
|
|
173
|
+
source=src("blocks", i),
|
|
174
|
+
attrs={
|
|
175
|
+
"ip": block.ip,
|
|
176
|
+
"bus": block.bus,
|
|
177
|
+
"clock": block.clock,
|
|
178
|
+
"reset": block.reset,
|
|
179
|
+
},
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
relations.append(contains(project_key, block_key, src("blocks", i)))
|
|
183
|
+
if block.size is not None:
|
|
184
|
+
region_key = make_key("memory_region", block.name)
|
|
185
|
+
base = bases[i]
|
|
186
|
+
entities.append(
|
|
187
|
+
MemoryRegionEntity(
|
|
188
|
+
key=region_key,
|
|
189
|
+
name=block.name,
|
|
190
|
+
base=base if base is not None else "auto",
|
|
191
|
+
size=block.size,
|
|
192
|
+
block=block_key,
|
|
193
|
+
source=src("blocks", i, "base"),
|
|
194
|
+
attrs={"auto": block.base == "auto"},
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
relations.append(contains(block_key, region_key, src("blocks", i)))
|
|
198
|
+
if base is not None:
|
|
199
|
+
regions.append(Region(block.name, base, block.size, doc.line(("blocks", i))))
|
|
200
|
+
for j, irq in enumerate(block.interrupts):
|
|
201
|
+
irq_key = make_key("interrupt", block.name, irq.name)
|
|
202
|
+
number = lines[(i, j)]
|
|
203
|
+
entities.append(
|
|
204
|
+
InterruptEntity(
|
|
205
|
+
key=irq_key,
|
|
206
|
+
name=irq.name,
|
|
207
|
+
line=number,
|
|
208
|
+
block=block_key,
|
|
209
|
+
source=src("blocks", i, "interrupts", j),
|
|
210
|
+
attrs={"auto": irq.line == "auto"},
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
relations.append(contains(block_key, irq_key, src("blocks", i, "interrupts", j)))
|
|
214
|
+
numbered.append(
|
|
215
|
+
(f"{block.name}.{irq.name}", number, doc.line(("blocks", i, "interrupts", j)))
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
block_keys = {b.name: make_key("block", b.name) for b in chip.blocks}
|
|
219
|
+
for i, bus in enumerate(chip.buses):
|
|
220
|
+
bus_key = make_key("interface", bus.name)
|
|
221
|
+
entities.append(
|
|
222
|
+
InterfaceEntity(
|
|
223
|
+
key=bus_key,
|
|
224
|
+
name=bus.name,
|
|
225
|
+
protocol=bus.protocol,
|
|
226
|
+
source=src("buses", i),
|
|
227
|
+
attrs={"masters": list(bus.masters), "slaves": list(bus.slaves)},
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
for slave in bus.slaves:
|
|
231
|
+
if slave in block_keys:
|
|
232
|
+
relations.append(
|
|
233
|
+
Relation(
|
|
234
|
+
kind="connects",
|
|
235
|
+
src=bus_key,
|
|
236
|
+
dst=block_keys[slave],
|
|
237
|
+
source=src("buses", i, "slaves"),
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
for i, clock in enumerate(chip.clocks):
|
|
242
|
+
entities.append(
|
|
243
|
+
ClockEntity(
|
|
244
|
+
key=make_key("clock", clock.name),
|
|
245
|
+
name=clock.name,
|
|
246
|
+
frequency_hz=clock.frequency_hz,
|
|
247
|
+
source=src("clocks", i),
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
for i, reset in enumerate(chip.resets):
|
|
251
|
+
entities.append(
|
|
252
|
+
ResetEntity(
|
|
253
|
+
key=make_key("reset", reset.name),
|
|
254
|
+
name=reset.name,
|
|
255
|
+
active_low=reset.active_low,
|
|
256
|
+
sync=reset.sync,
|
|
257
|
+
source=src("resets", i),
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
warnings = overlap_warnings(regions, file) + duplicate_line_warnings(numbered, file)
|
|
262
|
+
return DesignModel.build(entities, relations), warnings
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def resolve_bases(blocks: tuple[BlockSpec, ...]) -> list[int | None]:
|
|
266
|
+
"""Base address per block: fixed ones as given, `auto` ones by the module rule."""
|
|
267
|
+
placed = [(b.base, b.size) for b in blocks if isinstance(b.base, int) and b.size]
|
|
268
|
+
result: list[int | None] = []
|
|
269
|
+
for block in blocks:
|
|
270
|
+
if isinstance(block.base, int):
|
|
271
|
+
result.append(block.base)
|
|
272
|
+
continue
|
|
273
|
+
if block.base != "auto" or not block.size:
|
|
274
|
+
result.append(None)
|
|
275
|
+
continue
|
|
276
|
+
addr = 0
|
|
277
|
+
while clash := next(
|
|
278
|
+
((b, s) for b, s in placed if addr < b + s and b < addr + block.size), None
|
|
279
|
+
):
|
|
280
|
+
addr = _align_up(clash[0] + clash[1], block.size)
|
|
281
|
+
placed.append((addr, block.size))
|
|
282
|
+
result.append(addr)
|
|
283
|
+
return result
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def resolve_lines(blocks: tuple[BlockSpec, ...]) -> dict[tuple[int, int], int]:
|
|
287
|
+
"""Interrupt line per (block index, interrupt index), `auto` ones by the module rule."""
|
|
288
|
+
used = {irq.line for b in blocks for irq in b.interrupts if isinstance(irq.line, int)}
|
|
289
|
+
result: dict[tuple[int, int], int] = {}
|
|
290
|
+
next_free = 0
|
|
291
|
+
for i, block in enumerate(blocks):
|
|
292
|
+
for j, irq in enumerate(block.interrupts):
|
|
293
|
+
if isinstance(irq.line, int):
|
|
294
|
+
result[(i, j)] = irq.line
|
|
295
|
+
continue
|
|
296
|
+
while next_free in used:
|
|
297
|
+
next_free += 1
|
|
298
|
+
result[(i, j)] = next_free
|
|
299
|
+
used.add(next_free)
|
|
300
|
+
return result
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _align_up(addr: int, size: int) -> int:
|
|
304
|
+
return -(-addr // size) * size
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def schema_json() -> str:
|
|
308
|
+
"""The JSON Schema of `chip.yml`, as committed in `schemas/formats/chip.schema.json`."""
|
|
309
|
+
return json.dumps(ChipYaml.model_json_schema(), indent=2, sort_keys=True) + "\n"
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
__all__ = [
|
|
313
|
+
"BlockSpec",
|
|
314
|
+
"BusSpec",
|
|
315
|
+
"ChipYaml",
|
|
316
|
+
"ChipYamlAdapter",
|
|
317
|
+
"ClockSpec",
|
|
318
|
+
"InterruptSpec",
|
|
319
|
+
"ResetSpec",
|
|
320
|
+
"resolve_bases",
|
|
321
|
+
"resolve_lines",
|
|
322
|
+
"schema_json",
|
|
323
|
+
]
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Format adapter `qsoc-contract`: a QSoC-style inter-block contract into the Design Model.
|
|
2
|
+
|
|
3
|
+
The contract (`util/qsoc_contract.yml` in QSoC) is the single source of the numbers that
|
|
4
|
+
more than one block shares. It maps to the model as follows:
|
|
5
|
+
|
|
6
|
+
| Contract | Model |
|
|
7
|
+
|---|---|
|
|
8
|
+
| `meta` | one `project` (widths, clock, chip id in `attrs`) |
|
|
9
|
+
| each `memory_map` row | a `block` and its `memory_region` (base, size; port, kind in `attrs`) |
|
|
10
|
+
| each `interrupts.lines` item | an `interrupt` (line; peripheral, ports in `attrs`) |
|
|
11
|
+
| each `clock_domains.clusters` item | a `clock` (the chip frequency from `meta.clock_mhz`) |
|
|
12
|
+
| each `reset_sources` item | a `reset` |
|
|
13
|
+
|
|
14
|
+
An interrupt's `block` is the memory-map row of the same name, when there is one (the
|
|
15
|
+
contract names interrupts by peripheral, not always by row). Relations: `contains` from
|
|
16
|
+
the project to each block, and from a block to its region and
|
|
17
|
+
interrupts. Values the contract marks `tbd: true` are kept in `attrs` as they are and never
|
|
18
|
+
filled in. Unknown top-level keys are reported as warnings. `load_model` also warns about
|
|
19
|
+
overlapping memory regions and duplicate interrupt lines.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from collections.abc import Iterable, Mapping
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
from chipgraph.adapters.format._yaml_lines import LinedYaml
|
|
29
|
+
from chipgraph.core.model import (
|
|
30
|
+
BlockEntity,
|
|
31
|
+
ClockEntity,
|
|
32
|
+
DesignModel,
|
|
33
|
+
EntityBase,
|
|
34
|
+
InterruptEntity,
|
|
35
|
+
JSONValue,
|
|
36
|
+
MemoryRegionEntity,
|
|
37
|
+
ProjectEntity,
|
|
38
|
+
Provenance,
|
|
39
|
+
Relation,
|
|
40
|
+
ResetEntity,
|
|
41
|
+
make_key,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
_EXTRACTOR = "qsoc-contract"
|
|
45
|
+
_KNOWN_TOP = {"meta", "memory_map", "interrupts", "clock_domains", "reset_sources", "tbd"}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class QSocContractAdapter:
|
|
49
|
+
"""`FormatAdapter` for a QSoC-style `qsoc_contract.yml`."""
|
|
50
|
+
|
|
51
|
+
name = "qsoc-contract"
|
|
52
|
+
|
|
53
|
+
def load(self, path: Path) -> Iterable[Mapping[str, object]]:
|
|
54
|
+
"""The `FormatAdapter` protocol: every entity and relation as a plain mapping."""
|
|
55
|
+
model, _ = self.load_model(path)
|
|
56
|
+
return model_facts(model)
|
|
57
|
+
|
|
58
|
+
def load_model(self, path: Path, root: Path | None = None) -> tuple[DesignModel, list[str]]:
|
|
59
|
+
"""Load `path` into a `DesignModel`, with warnings for anything suspicious.
|
|
60
|
+
|
|
61
|
+
When `root` is given and `path` is under it, provenance `file` is written relative
|
|
62
|
+
to `root` (POSIX); otherwise it is `path.as_posix()` as before.
|
|
63
|
+
"""
|
|
64
|
+
doc = LinedYaml(path)
|
|
65
|
+
data: dict[str, Any] = doc.data if isinstance(doc.data, dict) else {}
|
|
66
|
+
file = _display_path(path, root)
|
|
67
|
+
warnings: list[str] = []
|
|
68
|
+
entities: list[EntityBase] = []
|
|
69
|
+
relations: list[Relation] = []
|
|
70
|
+
|
|
71
|
+
def src(*at: str | int) -> Provenance:
|
|
72
|
+
return Provenance(file=file, line=doc.line(at), extractor=_EXTRACTOR)
|
|
73
|
+
|
|
74
|
+
for key in sorted(set(data) - _KNOWN_TOP):
|
|
75
|
+
warnings.append(f"{file}:{doc.line((key,))}: unknown top-level key {key!r} ignored")
|
|
76
|
+
|
|
77
|
+
meta: dict[str, Any] = data.get("meta") or {}
|
|
78
|
+
project_name = str(meta.get("project", path.stem))
|
|
79
|
+
project_key = make_key("project", project_name)
|
|
80
|
+
entities.append(
|
|
81
|
+
ProjectEntity(
|
|
82
|
+
key=project_key,
|
|
83
|
+
name=project_name,
|
|
84
|
+
source=src("meta", "project"),
|
|
85
|
+
attrs={k: to_json(v) for k, v in meta.items() if k != "project"},
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
block_keys: dict[str, str] = {}
|
|
90
|
+
regions: list[Region] = []
|
|
91
|
+
for i, row in enumerate(data.get("memory_map") or []):
|
|
92
|
+
name = str(row["name"])
|
|
93
|
+
block_key = make_key("block", name)
|
|
94
|
+
region_key = make_key("memory_region", name)
|
|
95
|
+
block_keys[name] = block_key
|
|
96
|
+
extra = {k: to_json(v) for k, v in row.items() if k not in ("name", "base", "size")}
|
|
97
|
+
entities.append(BlockEntity(key=block_key, name=name, source=src("memory_map", i)))
|
|
98
|
+
entities.append(
|
|
99
|
+
MemoryRegionEntity(
|
|
100
|
+
key=region_key,
|
|
101
|
+
name=name,
|
|
102
|
+
base=int_or_str(row.get("base")),
|
|
103
|
+
size=int_or_str(row.get("size")),
|
|
104
|
+
block=block_key,
|
|
105
|
+
source=src("memory_map", i),
|
|
106
|
+
attrs=extra,
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
relations.append(contains(project_key, block_key, src("memory_map", i)))
|
|
110
|
+
relations.append(contains(block_key, region_key, src("memory_map", i)))
|
|
111
|
+
base, size = row.get("base"), row.get("size")
|
|
112
|
+
if isinstance(base, int) and isinstance(size, int):
|
|
113
|
+
regions.append(Region(name, base, size, doc.line(("memory_map", i))))
|
|
114
|
+
|
|
115
|
+
interrupts: dict[str, Any] = data.get("interrupts") or {}
|
|
116
|
+
numbered: list[tuple[str, int, int]] = [] # name, line number, source line
|
|
117
|
+
for i, item in enumerate(interrupts.get("lines") or []):
|
|
118
|
+
peripheral = str(item.get("peripheral", f"line{i}"))
|
|
119
|
+
number = item.get("line")
|
|
120
|
+
key = make_key("interrupt", peripheral)
|
|
121
|
+
owner = block_keys.get(peripheral)
|
|
122
|
+
entities.append(
|
|
123
|
+
InterruptEntity(
|
|
124
|
+
key=key,
|
|
125
|
+
name=peripheral,
|
|
126
|
+
line=number if isinstance(number, int) else None,
|
|
127
|
+
block=owner,
|
|
128
|
+
source=src("interrupts", "lines", i),
|
|
129
|
+
attrs={k: to_json(v) for k, v in item.items() if k != "line"},
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
if owner is not None:
|
|
133
|
+
relations.append(contains(owner, key, src("interrupts", "lines", i)))
|
|
134
|
+
if isinstance(number, int):
|
|
135
|
+
numbered.append((peripheral, number, doc.line(("interrupts", "lines", i))))
|
|
136
|
+
|
|
137
|
+
frequency = meta.get("clock_mhz")
|
|
138
|
+
clocks: dict[str, Any] = data.get("clock_domains") or {}
|
|
139
|
+
for i, cluster in enumerate(clocks.get("clusters") or []):
|
|
140
|
+
name = str(cluster["name"])
|
|
141
|
+
entities.append(
|
|
142
|
+
ClockEntity(
|
|
143
|
+
key=make_key("clock", name),
|
|
144
|
+
name=name,
|
|
145
|
+
frequency_hz=float(frequency) * 1e6 if isinstance(frequency, int) else None,
|
|
146
|
+
source=src("clock_domains", "clusters", i),
|
|
147
|
+
attrs={k: to_json(v) for k, v in cluster.items() if k != "name"},
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
for i, name in enumerate(data.get("reset_sources") or []):
|
|
152
|
+
entities.append(
|
|
153
|
+
ResetEntity(
|
|
154
|
+
key=make_key("reset", str(name)),
|
|
155
|
+
name=str(name),
|
|
156
|
+
source=src("reset_sources", i),
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
warnings.extend(overlap_warnings(regions, file))
|
|
161
|
+
warnings.extend(duplicate_line_warnings(numbered, file))
|
|
162
|
+
return DesignModel.build(entities, relations), warnings
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class Region:
|
|
166
|
+
"""A memory region with a known base and size, for overlap checks."""
|
|
167
|
+
|
|
168
|
+
__slots__ = ("base", "line", "name", "size")
|
|
169
|
+
|
|
170
|
+
def __init__(self, name: str, base: int, size: int, line: int) -> None:
|
|
171
|
+
self.name, self.base, self.size, self.line = name, base, size, line
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def overlap_warnings(regions: list[Region], file: str) -> list[str]:
|
|
175
|
+
"""One warning for every pair of memory regions that share an address."""
|
|
176
|
+
found: list[str] = []
|
|
177
|
+
ordered = sorted(regions, key=lambda r: (r.base, r.name))
|
|
178
|
+
for i, a in enumerate(ordered):
|
|
179
|
+
for b in ordered[i + 1 :]:
|
|
180
|
+
if b.base >= a.base + a.size:
|
|
181
|
+
break
|
|
182
|
+
found.append(
|
|
183
|
+
f"{file}:{b.line}: memory regions {a.name!r} "
|
|
184
|
+
f"[{a.base:#x}, {a.base + a.size:#x}) (line {a.line}) and {b.name!r} "
|
|
185
|
+
f"[{b.base:#x}, {b.base + b.size:#x}) are overlapping"
|
|
186
|
+
)
|
|
187
|
+
return found
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def duplicate_line_warnings(numbered: list[tuple[str, int, int]], file: str) -> list[str]:
|
|
191
|
+
"""One warning for every interrupt line number used more than once."""
|
|
192
|
+
found: list[str] = []
|
|
193
|
+
first: dict[int, tuple[str, int]] = {}
|
|
194
|
+
for name, number, line in numbered:
|
|
195
|
+
if number in first:
|
|
196
|
+
other, other_line = first[number]
|
|
197
|
+
found.append(
|
|
198
|
+
f"{file}:{line}: duplicate interrupt line {number}: {name!r} and "
|
|
199
|
+
f"{other!r} (line {other_line})"
|
|
200
|
+
)
|
|
201
|
+
else:
|
|
202
|
+
first[number] = (name, line)
|
|
203
|
+
return found
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def model_facts(model: DesignModel) -> list[Mapping[str, object]]:
|
|
207
|
+
"""Every entity (sorted by key) and relation of `model` as plain mappings."""
|
|
208
|
+
facts: list[Mapping[str, object]] = [
|
|
209
|
+
model.entities[key].model_dump(mode="json") for key in sorted(model.entities)
|
|
210
|
+
]
|
|
211
|
+
facts.extend(r.model_dump(mode="json") for r in model.relations)
|
|
212
|
+
return facts
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def contains(src: str, dst: str, source: Provenance) -> Relation:
|
|
216
|
+
return Relation(kind="contains", src=src, dst=dst, source=source)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _display_path(path: Path, root: Path | None) -> str:
|
|
220
|
+
"""`path` relative to `root` (POSIX) when it is under it, else its POSIX form."""
|
|
221
|
+
if root is not None:
|
|
222
|
+
try:
|
|
223
|
+
return path.resolve().relative_to(root.resolve()).as_posix()
|
|
224
|
+
except ValueError:
|
|
225
|
+
pass
|
|
226
|
+
return path.as_posix()
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def int_or_str(value: object) -> int | str | None:
|
|
230
|
+
if value is None or isinstance(value, int):
|
|
231
|
+
return value
|
|
232
|
+
return str(value)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def to_json(value: object) -> JSONValue:
|
|
236
|
+
"""A YAML value as JSON: containers kept, anything else not JSON as a string."""
|
|
237
|
+
if value is None or isinstance(value, bool | int | float | str):
|
|
238
|
+
return value
|
|
239
|
+
if isinstance(value, list | tuple):
|
|
240
|
+
return [to_json(v) for v in value]
|
|
241
|
+
if isinstance(value, dict):
|
|
242
|
+
return {str(k): to_json(v) for k, v in value.items()}
|
|
243
|
+
return str(value)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
__all__ = ["QSocContractAdapter"]
|