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.
Files changed (174) hide show
  1. chipgraph/__init__.py +7 -0
  2. chipgraph/adapters/__init__.py +1 -0
  3. chipgraph/adapters/format/__init__.py +6 -0
  4. chipgraph/adapters/format/__main__.py +16 -0
  5. chipgraph/adapters/format/_yaml_lines.py +54 -0
  6. chipgraph/adapters/format/chip_yaml.py +323 -0
  7. chipgraph/adapters/format/qsoc_contract.py +246 -0
  8. chipgraph/adapters/llm/__init__.py +58 -0
  9. chipgraph/adapters/llm/_budget.py +168 -0
  10. chipgraph/adapters/llm/_errors.py +50 -0
  11. chipgraph/adapters/llm/_nda.py +30 -0
  12. chipgraph/adapters/llm/_retry.py +85 -0
  13. chipgraph/adapters/llm/anthropic.py +240 -0
  14. chipgraph/adapters/llm/decide_backend.py +179 -0
  15. chipgraph/adapters/llm/fake.py +87 -0
  16. chipgraph/adapters/llm/guard.py +68 -0
  17. chipgraph/adapters/parser/__init__.py +22 -0
  18. chipgraph/adapters/parser/generic_regex.py +75 -0
  19. chipgraph/adapters/parser/verible.py +55 -0
  20. chipgraph/adapters/parser/verilator.py +46 -0
  21. chipgraph/adapters/review/__init__.py +7 -0
  22. chipgraph/adapters/review/file.py +123 -0
  23. chipgraph/adapters/runner/__init__.py +7 -0
  24. chipgraph/adapters/runner/local.py +89 -0
  25. chipgraph/adapters/runtime/__init__.py +5 -0
  26. chipgraph/adapters/runtime/claude_code/__init__.py +18 -0
  27. chipgraph/adapters/runtime/claude_code/decisions.py +280 -0
  28. chipgraph/adapters/runtime/claude_code/runtime.py +102 -0
  29. chipgraph/adapters/runtime/claude_code/service.py +590 -0
  30. chipgraph/adapters/tool/__init__.py +7 -0
  31. chipgraph/adapters/tool/cmd.py +229 -0
  32. chipgraph/adapters/tool/filelist.py +259 -0
  33. chipgraph/adapters/tool/pyslang.py +485 -0
  34. chipgraph/adapters/vcs/__init__.py +1 -0
  35. chipgraph/adapters/vcs/git.py +107 -0
  36. chipgraph/app/__init__.py +10 -0
  37. chipgraph/app/baseline.py +339 -0
  38. chipgraph/app/build.py +139 -0
  39. chipgraph/app/checks.py +193 -0
  40. chipgraph/app/context.py +178 -0
  41. chipgraph/app/errors.py +11 -0
  42. chipgraph/app/executors.py +79 -0
  43. chipgraph/app/findings.py +203 -0
  44. chipgraph/app/ingest.py +523 -0
  45. chipgraph/checks/__init__.py +52 -0
  46. chipgraph/checks/_cdc_analyze.py +272 -0
  47. chipgraph/checks/_cdc_elaborate.py +455 -0
  48. chipgraph/checks/_common.py +268 -0
  49. chipgraph/checks/_cross.py +96 -0
  50. chipgraph/checks/_model.py +64 -0
  51. chipgraph/checks/_naming_pyslang.py +235 -0
  52. chipgraph/checks/_naming_rules.py +127 -0
  53. chipgraph/checks/_xref_syntax.py +260 -0
  54. chipgraph/checks/cdc_struct.py +235 -0
  55. chipgraph/checks/connect.py +353 -0
  56. chipgraph/checks/cross_chip.py +454 -0
  57. chipgraph/checks/duplicate.py +151 -0
  58. chipgraph/checks/filelist.py +323 -0
  59. chipgraph/checks/generated.py +151 -0
  60. chipgraph/checks/hardcode.py +233 -0
  61. chipgraph/checks/layout.py +155 -0
  62. chipgraph/checks/naming.py +311 -0
  63. chipgraph/checks/ports_diff.py +203 -0
  64. chipgraph/checks/spec_schema.py +257 -0
  65. chipgraph/checks/trace.py +207 -0
  66. chipgraph/cli/__init__.py +1500 -0
  67. chipgraph/core/__init__.py +1 -0
  68. chipgraph/core/config/__init__.py +81 -0
  69. chipgraph/core/config/defaults.py +14 -0
  70. chipgraph/core/config/errors.py +32 -0
  71. chipgraph/core/config/loader.py +598 -0
  72. chipgraph/core/config/models.py +612 -0
  73. chipgraph/core/config/templates.py +340 -0
  74. chipgraph/core/contracts/__init__.py +75 -0
  75. chipgraph/core/contracts/_paths.py +26 -0
  76. chipgraph/core/contracts/agent.py +49 -0
  77. chipgraph/core/contracts/artifact.py +68 -0
  78. chipgraph/core/contracts/check.py +76 -0
  79. chipgraph/core/contracts/event.py +70 -0
  80. chipgraph/core/contracts/export.py +128 -0
  81. chipgraph/core/contracts/finding.py +171 -0
  82. chipgraph/core/contracts/gate.py +50 -0
  83. chipgraph/core/contracts/rule.py +177 -0
  84. chipgraph/core/contracts/types.py +45 -0
  85. chipgraph/core/engine/__init__.py +70 -0
  86. chipgraph/core/engine/baseline.py +283 -0
  87. chipgraph/core/engine/decide.py +462 -0
  88. chipgraph/core/engine/gate.py +170 -0
  89. chipgraph/core/engine/graph.py +533 -0
  90. chipgraph/core/engine/records.py +60 -0
  91. chipgraph/core/engine/rules.py +92 -0
  92. chipgraph/core/engine/scheduler.py +540 -0
  93. chipgraph/core/model/__init__.py +98 -0
  94. chipgraph/core/model/entities.py +244 -0
  95. chipgraph/core/model/export.py +16 -0
  96. chipgraph/core/model/ingest.py +693 -0
  97. chipgraph/core/model/json_value.py +4 -0
  98. chipgraph/core/model/keys.py +95 -0
  99. chipgraph/core/model/model.py +153 -0
  100. chipgraph/core/model/provenance.py +27 -0
  101. chipgraph/core/model/query.py +583 -0
  102. chipgraph/core/model/registry.py +80 -0
  103. chipgraph/core/model/relations.py +61 -0
  104. chipgraph/core/model/store.py +279 -0
  105. chipgraph/core/plugin_api/__init__.py +81 -0
  106. chipgraph/core/plugin_api/local.py +167 -0
  107. chipgraph/core/plugin_api/pack.py +159 -0
  108. chipgraph/core/plugin_api/protocols.py +177 -0
  109. chipgraph/core/plugin_api/registry.py +111 -0
  110. chipgraph/core/plugin_api/types.py +94 -0
  111. chipgraph/core/runtime/__init__.py +40 -0
  112. chipgraph/core/runtime/executor.py +79 -0
  113. chipgraph/core/runtime/queue.py +380 -0
  114. chipgraph/core/state/__init__.py +1 -0
  115. chipgraph/core/state/artifacts.py +156 -0
  116. chipgraph/core/state/backend.py +117 -0
  117. chipgraph/core/state/findings.py +151 -0
  118. chipgraph/core/state/handoff.py +321 -0
  119. chipgraph/core/state/idempotency.py +59 -0
  120. chipgraph/core/state/journal.py +238 -0
  121. chipgraph/core/state/layout.py +73 -0
  122. chipgraph/core/state/lock.py +195 -0
  123. chipgraph/core/state/trace.py +225 -0
  124. chipgraph/learn/__init__.py +96 -0
  125. chipgraph/learn/draft.py +187 -0
  126. chipgraph/learn/infer.py +580 -0
  127. chipgraph/learn/models.py +145 -0
  128. chipgraph/learn/try_run.py +228 -0
  129. chipgraph/mcp/__init__.py +12 -0
  130. chipgraph/mcp/model_tools.py +167 -0
  131. chipgraph/mcp/server.py +605 -0
  132. chipgraph/orgs/qnsc/naming-v1.yml +113 -0
  133. chipgraph/orgs/qnsc/profile.yml +5 -0
  134. chipgraph/packs/__init__.py +8 -0
  135. chipgraph/packs/assist/__init__.py +16 -0
  136. chipgraph/packs/assist/ask/__init__.py +58 -0
  137. chipgraph/packs/assist/ask/_db.py +57 -0
  138. chipgraph/packs/assist/ask/_project.py +230 -0
  139. chipgraph/packs/assist/ask/answer.py +266 -0
  140. chipgraph/packs/assist/ask/check.py +182 -0
  141. chipgraph/packs/assist/ask/contract.py +115 -0
  142. chipgraph/packs/assist/ask/documents.py +157 -0
  143. chipgraph/packs/assist/ask/fts.py +80 -0
  144. chipgraph/packs/assist/ask/retrieve.py +331 -0
  145. chipgraph/packs/assist/audit/__init__.py +383 -0
  146. chipgraph/packs/assist/pack.yml +13 -0
  147. chipgraph/packs/assist/triage/__init__.py +64 -0
  148. chipgraph/packs/assist/triage/contract.py +94 -0
  149. chipgraph/packs/assist/triage/parse.py +371 -0
  150. chipgraph/packs/assist/triage/question.py +384 -0
  151. chipgraph/packs/assist/triage/report.py +160 -0
  152. chipgraph/packs/assist/triage/rules.py +447 -0
  153. chipgraph/packs/assist/triage/run.py +224 -0
  154. chipgraph/packs/spec_core/__init__.py +9 -0
  155. chipgraph/packs/spec_core/extract/__init__.py +12 -0
  156. chipgraph/packs/spec_core/extract/_markdown.py +371 -0
  157. chipgraph/packs/spec_core/extract/_text.py +116 -0
  158. chipgraph/packs/spec_core/extract/mas.py +895 -0
  159. chipgraph/packs/spec_core/gen/__init__.py +11 -0
  160. chipgraph/packs/spec_core/gen/agents_md/__init__.py +148 -0
  161. chipgraph/packs/spec_core/gen/agents_md/_context.py +239 -0
  162. chipgraph/packs/spec_core/gen/agents_md/_markers.py +131 -0
  163. chipgraph/packs/spec_core/gen/agents_md/agents_md.md.j2 +178 -0
  164. chipgraph/packs/spec_core/gen/diagram/__init__.py +110 -0
  165. chipgraph/packs/spec_core/gen/diagram/__main__.py +44 -0
  166. chipgraph/packs/spec_core/gen/diagram/_diagrams.py +403 -0
  167. chipgraph/packs/spec_core/gen/diagram/_scene.py +163 -0
  168. chipgraph/packs/spec_core/pack.yml +6 -0
  169. chipgraph/py.typed +0 -0
  170. chipgraph-0.1.0rc1.dist-info/METADATA +50 -0
  171. chipgraph-0.1.0rc1.dist-info/RECORD +174 -0
  172. chipgraph-0.1.0rc1.dist-info/WHEEL +4 -0
  173. chipgraph-0.1.0rc1.dist-info/entry_points.txt +48 -0
  174. chipgraph-0.1.0rc1.dist-info/licenses/LICENSE +202 -0
chipgraph/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """chipgraph: AI agent system for chip (IC) design."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ __version__ = version("chipgraph")
6
+
7
+ __all__ = ["__version__"]
@@ -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"]