chipgraph 0.1.0__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 (252) 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 +173 -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 +247 -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/cocotb.py +304 -0
  19. chipgraph/adapters/parser/generic_regex.py +75 -0
  20. chipgraph/adapters/parser/verible.py +55 -0
  21. chipgraph/adapters/parser/verilator.py +46 -0
  22. chipgraph/adapters/review/__init__.py +7 -0
  23. chipgraph/adapters/review/file.py +123 -0
  24. chipgraph/adapters/review/github.py +886 -0
  25. chipgraph/adapters/runner/__init__.py +7 -0
  26. chipgraph/adapters/runner/local.py +122 -0
  27. chipgraph/adapters/runtime/__init__.py +5 -0
  28. chipgraph/adapters/runtime/claude_code/__init__.py +24 -0
  29. chipgraph/adapters/runtime/claude_code/agents.py +158 -0
  30. chipgraph/adapters/runtime/claude_code/decisions.py +280 -0
  31. chipgraph/adapters/runtime/claude_code/guarded.py +310 -0
  32. chipgraph/adapters/runtime/claude_code/isolation.py +482 -0
  33. chipgraph/adapters/runtime/claude_code/loop.py +342 -0
  34. chipgraph/adapters/runtime/claude_code/previous.py +419 -0
  35. chipgraph/adapters/runtime/claude_code/review.py +492 -0
  36. chipgraph/adapters/runtime/claude_code/runtime.py +192 -0
  37. chipgraph/adapters/runtime/claude_code/service.py +2285 -0
  38. chipgraph/adapters/tool/__init__.py +7 -0
  39. chipgraph/adapters/tool/cmd.py +229 -0
  40. chipgraph/adapters/tool/edalize.py +773 -0
  41. chipgraph/adapters/tool/filelist.py +259 -0
  42. chipgraph/adapters/tool/pyslang.py +485 -0
  43. chipgraph/adapters/vcs/__init__.py +1 -0
  44. chipgraph/adapters/vcs/git.py +521 -0
  45. chipgraph/app/__init__.py +10 -0
  46. chipgraph/app/baseline.py +339 -0
  47. chipgraph/app/build.py +332 -0
  48. chipgraph/app/checks.py +202 -0
  49. chipgraph/app/context.py +191 -0
  50. chipgraph/app/errors.py +11 -0
  51. chipgraph/app/executors.py +79 -0
  52. chipgraph/app/findings.py +203 -0
  53. chipgraph/app/ingest.py +523 -0
  54. chipgraph/checks/__init__.py +52 -0
  55. chipgraph/checks/_cdc_analyze.py +272 -0
  56. chipgraph/checks/_cdc_elaborate.py +455 -0
  57. chipgraph/checks/_common.py +268 -0
  58. chipgraph/checks/_cross.py +96 -0
  59. chipgraph/checks/_model.py +64 -0
  60. chipgraph/checks/_naming_pyslang.py +235 -0
  61. chipgraph/checks/_naming_rules.py +127 -0
  62. chipgraph/checks/_xref_syntax.py +260 -0
  63. chipgraph/checks/cdc_struct.py +235 -0
  64. chipgraph/checks/connect.py +353 -0
  65. chipgraph/checks/cross_chip.py +454 -0
  66. chipgraph/checks/duplicate.py +153 -0
  67. chipgraph/checks/filelist.py +323 -0
  68. chipgraph/checks/format.py +268 -0
  69. chipgraph/checks/generated.py +151 -0
  70. chipgraph/checks/hardcode.py +233 -0
  71. chipgraph/checks/layout.py +155 -0
  72. chipgraph/checks/naming.py +311 -0
  73. chipgraph/checks/plan_check.py +901 -0
  74. chipgraph/checks/ports_diff.py +203 -0
  75. chipgraph/checks/spec_schema.py +277 -0
  76. chipgraph/checks/style.py +910 -0
  77. chipgraph/checks/tb_static.py +599 -0
  78. chipgraph/checks/trace.py +224 -0
  79. chipgraph/checks/vendor_guard.py +369 -0
  80. chipgraph/checks/wrap_check.py +486 -0
  81. chipgraph/cli/__init__.py +1688 -0
  82. chipgraph/cli/next_steps.py +137 -0
  83. chipgraph/core/__init__.py +1 -0
  84. chipgraph/core/config/__init__.py +93 -0
  85. chipgraph/core/config/defaults.py +14 -0
  86. chipgraph/core/config/errors.py +32 -0
  87. chipgraph/core/config/loader.py +766 -0
  88. chipgraph/core/config/models.py +845 -0
  89. chipgraph/core/config/templates.py +340 -0
  90. chipgraph/core/contracts/__init__.py +75 -0
  91. chipgraph/core/contracts/_paths.py +26 -0
  92. chipgraph/core/contracts/agent.py +49 -0
  93. chipgraph/core/contracts/artifact.py +68 -0
  94. chipgraph/core/contracts/check.py +76 -0
  95. chipgraph/core/contracts/event.py +70 -0
  96. chipgraph/core/contracts/export.py +128 -0
  97. chipgraph/core/contracts/finding.py +171 -0
  98. chipgraph/core/contracts/gate.py +50 -0
  99. chipgraph/core/contracts/rule.py +177 -0
  100. chipgraph/core/contracts/types.py +45 -0
  101. chipgraph/core/engine/__init__.py +70 -0
  102. chipgraph/core/engine/agent_rule.py +1362 -0
  103. chipgraph/core/engine/baseline.py +283 -0
  104. chipgraph/core/engine/decide.py +462 -0
  105. chipgraph/core/engine/fanout.py +1358 -0
  106. chipgraph/core/engine/gate.py +170 -0
  107. chipgraph/core/engine/graph.py +647 -0
  108. chipgraph/core/engine/records.py +60 -0
  109. chipgraph/core/engine/rules.py +92 -0
  110. chipgraph/core/engine/scheduler.py +637 -0
  111. chipgraph/core/engine/select.py +1239 -0
  112. chipgraph/core/engine/targets.py +342 -0
  113. chipgraph/core/model/__init__.py +98 -0
  114. chipgraph/core/model/entities.py +244 -0
  115. chipgraph/core/model/export.py +16 -0
  116. chipgraph/core/model/ingest.py +693 -0
  117. chipgraph/core/model/json_value.py +4 -0
  118. chipgraph/core/model/keys.py +95 -0
  119. chipgraph/core/model/model.py +153 -0
  120. chipgraph/core/model/provenance.py +27 -0
  121. chipgraph/core/model/query.py +583 -0
  122. chipgraph/core/model/registry.py +80 -0
  123. chipgraph/core/model/relations.py +61 -0
  124. chipgraph/core/model/store.py +279 -0
  125. chipgraph/core/plugin_api/__init__.py +81 -0
  126. chipgraph/core/plugin_api/local.py +167 -0
  127. chipgraph/core/plugin_api/pack.py +159 -0
  128. chipgraph/core/plugin_api/protocols.py +177 -0
  129. chipgraph/core/plugin_api/registry.py +111 -0
  130. chipgraph/core/plugin_api/types.py +94 -0
  131. chipgraph/core/runtime/__init__.py +41 -0
  132. chipgraph/core/runtime/executor.py +79 -0
  133. chipgraph/core/runtime/queue.py +565 -0
  134. chipgraph/core/runtime/roles/__init__.py +91 -0
  135. chipgraph/core/runtime/roles/data/author.md +41 -0
  136. chipgraph/core/runtime/roles/data/critic.md +56 -0
  137. chipgraph/core/runtime/roles/data/planner.md +38 -0
  138. chipgraph/core/runtime/roles/data/researcher.md +37 -0
  139. chipgraph/core/runtime/roles/data/rtl-author.md +64 -0
  140. chipgraph/core/runtime/roles/data/tb-author.md +48 -0
  141. chipgraph/core/runtime/roles/data/triage.md +34 -0
  142. chipgraph/core/runtime/roles/policy.py +299 -0
  143. chipgraph/core/runtime/roles/registry.py +52 -0
  144. chipgraph/core/runtime/roles/review.py +275 -0
  145. chipgraph/core/runtime/roles/schema.py +82 -0
  146. chipgraph/core/runtime/roles/skills.py +134 -0
  147. chipgraph/core/runtime/roles/spec.py +232 -0
  148. chipgraph/core/state/__init__.py +1 -0
  149. chipgraph/core/state/artifacts.py +156 -0
  150. chipgraph/core/state/backend.py +117 -0
  151. chipgraph/core/state/findings.py +151 -0
  152. chipgraph/core/state/handoff.py +411 -0
  153. chipgraph/core/state/idempotency.py +59 -0
  154. chipgraph/core/state/journal.py +238 -0
  155. chipgraph/core/state/layout.py +82 -0
  156. chipgraph/core/state/lock.py +195 -0
  157. chipgraph/core/state/trace.py +225 -0
  158. chipgraph/learn/__init__.py +96 -0
  159. chipgraph/learn/draft.py +187 -0
  160. chipgraph/learn/infer.py +580 -0
  161. chipgraph/learn/models.py +145 -0
  162. chipgraph/learn/try_run.py +228 -0
  163. chipgraph/mcp/__init__.py +12 -0
  164. chipgraph/mcp/model_tools.py +167 -0
  165. chipgraph/mcp/server.py +787 -0
  166. chipgraph/orgs/qnsc/naming-v1.yml +113 -0
  167. chipgraph/orgs/qnsc/profile.yml +5 -0
  168. chipgraph/packs/__init__.py +8 -0
  169. chipgraph/packs/assist/__init__.py +16 -0
  170. chipgraph/packs/assist/ask/__init__.py +58 -0
  171. chipgraph/packs/assist/ask/_db.py +57 -0
  172. chipgraph/packs/assist/ask/_project.py +230 -0
  173. chipgraph/packs/assist/ask/answer.py +266 -0
  174. chipgraph/packs/assist/ask/check.py +182 -0
  175. chipgraph/packs/assist/ask/contract.py +115 -0
  176. chipgraph/packs/assist/ask/documents.py +157 -0
  177. chipgraph/packs/assist/ask/fts.py +80 -0
  178. chipgraph/packs/assist/ask/retrieve.py +331 -0
  179. chipgraph/packs/assist/audit/__init__.py +383 -0
  180. chipgraph/packs/assist/pack.yml +13 -0
  181. chipgraph/packs/assist/triage/__init__.py +64 -0
  182. chipgraph/packs/assist/triage/contract.py +94 -0
  183. chipgraph/packs/assist/triage/parse.py +371 -0
  184. chipgraph/packs/assist/triage/question.py +384 -0
  185. chipgraph/packs/assist/triage/report.py +160 -0
  186. chipgraph/packs/assist/triage/rules.py +447 -0
  187. chipgraph/packs/assist/triage/run.py +224 -0
  188. chipgraph/packs/digital_rtl/__init__.py +19 -0
  189. chipgraph/packs/digital_rtl/layout.py +144 -0
  190. chipgraph/packs/digital_rtl/pack.yml +25 -0
  191. chipgraph/packs/digital_rtl/plan/__init__.py +34 -0
  192. chipgraph/packs/digital_rtl/plan/__main__.py +16 -0
  193. chipgraph/packs/digital_rtl/plan/context.py +82 -0
  194. chipgraph/packs/digital_rtl/plan/expand.py +94 -0
  195. chipgraph/packs/digital_rtl/plan/model.py +150 -0
  196. chipgraph/packs/digital_rtl/plan/resolver.py +400 -0
  197. chipgraph/packs/digital_rtl/plan/show.py +176 -0
  198. chipgraph/packs/digital_rtl/pr/__init__.py +11 -0
  199. chipgraph/packs/digital_rtl/pr/body.py +165 -0
  200. chipgraph/packs/digital_rtl/pr/gate.py +110 -0
  201. chipgraph/packs/digital_rtl/pr/open.py +397 -0
  202. chipgraph/packs/digital_rtl/rtl/__init__.py +9 -0
  203. chipgraph/packs/digital_rtl/rtl/context.py +272 -0
  204. chipgraph/packs/digital_rtl/rules/open_pr.yml +30 -0
  205. chipgraph/packs/digital_rtl/rules/plan.yml +28 -0
  206. chipgraph/packs/digital_rtl/rules/plan_expand.yml +29 -0
  207. chipgraph/packs/digital_rtl/rules/pr_gate.yml +27 -0
  208. chipgraph/packs/digital_rtl/rules/review.yml +31 -0
  209. chipgraph/packs/digital_rtl/rules/rtl_module.yml +53 -0
  210. chipgraph/packs/digital_rtl/skills/lang-sv-rtl.md +58 -0
  211. chipgraph/packs/digital_rtl/skills/org-naming.md +29 -0
  212. chipgraph/packs/digital_rtl/skills/plan-modules.md +63 -0
  213. chipgraph/packs/digital_rtl/skills/review-diff.md +41 -0
  214. chipgraph/packs/digital_rtl/targets/plan.yml +11 -0
  215. chipgraph/packs/digital_rtl/targets/rtl.yml +20 -0
  216. chipgraph/packs/digital_rtl/wrapper/__init__.py +24 -0
  217. chipgraph/packs/digital_rtl/wrapper/common.py +171 -0
  218. chipgraph/packs/digital_rtl/wrapper/context.py +222 -0
  219. chipgraph/packs/digital_rtl/wrapper/emacs.py +626 -0
  220. chipgraph/packs/digital_rtl/wrapper/expansion.py +304 -0
  221. chipgraph/packs/digital_rtl/wrapper/lisp.py +504 -0
  222. chipgraph/packs/digital_rtl/wrapper/resolver.py +60 -0
  223. chipgraph/packs/digital_rtl/wrapper/rules/wrapper.yml +41 -0
  224. chipgraph/packs/digital_rtl/wrapper/rules/wrapper_gen.yml +35 -0
  225. chipgraph/packs/digital_rtl/wrapper/skills/wrapper-auto.md +120 -0
  226. chipgraph/packs/dv/__init__.py +15 -0
  227. chipgraph/packs/dv/declaration.py +392 -0
  228. chipgraph/packs/dv/interface.py +596 -0
  229. chipgraph/packs/dv/pack.yml +14 -0
  230. chipgraph/packs/dv/rules/tb_module.yml +42 -0
  231. chipgraph/packs/dv/skills/dv/cocotb.md +72 -0
  232. chipgraph/packs/spec_core/__init__.py +9 -0
  233. chipgraph/packs/spec_core/extract/__init__.py +12 -0
  234. chipgraph/packs/spec_core/extract/_markdown.py +371 -0
  235. chipgraph/packs/spec_core/extract/_text.py +116 -0
  236. chipgraph/packs/spec_core/extract/mas.py +975 -0
  237. chipgraph/packs/spec_core/gen/__init__.py +11 -0
  238. chipgraph/packs/spec_core/gen/agents_md/__init__.py +148 -0
  239. chipgraph/packs/spec_core/gen/agents_md/_context.py +239 -0
  240. chipgraph/packs/spec_core/gen/agents_md/_markers.py +131 -0
  241. chipgraph/packs/spec_core/gen/agents_md/agents_md.md.j2 +178 -0
  242. chipgraph/packs/spec_core/gen/diagram/__init__.py +110 -0
  243. chipgraph/packs/spec_core/gen/diagram/__main__.py +44 -0
  244. chipgraph/packs/spec_core/gen/diagram/_diagrams.py +403 -0
  245. chipgraph/packs/spec_core/gen/diagram/_scene.py +163 -0
  246. chipgraph/packs/spec_core/pack.yml +6 -0
  247. chipgraph/py.typed +0 -0
  248. chipgraph-0.1.0.dist-info/METADATA +53 -0
  249. chipgraph-0.1.0.dist-info/RECORD +252 -0
  250. chipgraph-0.1.0.dist-info/WHEEL +4 -0
  251. chipgraph-0.1.0.dist-info/entry_points.txt +61 -0
  252. chipgraph-0.1.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,596 @@
1
+ """The interface a testbench is written against, without the RTL (DESIGN.md 5.3 F1, 4.4).
2
+
3
+ `interface_for(module, ...)` is pure: it reads the Design Model it is given and, only
4
+ when it needs one, asks `declaration` for the module's declaration. The port list comes
5
+ from the first of these that has it:
6
+
7
+ 1. ``spec``: the block's spec ports in the Design Model (`port:spec.<block>.<name>`);
8
+ 2. ``model_rtl``: the module's RTL port entities already in the Design Model (they came
9
+ from declarations, by `chipgraph ingest`);
10
+ 3. ``rtl_declaration``: only when neither exists (a new module, a model not ingested, a
11
+ port list missing), the module's declaration read from the RTL with pyslang
12
+ (`chipgraph.packs.dv.declaration`: the header only, never the body).
13
+
14
+ Every port carries its `source`. When the spec ports are used and RTL ports are known
15
+ (2 or 3), each spec port that the RTL declares differently gets the note `rtl differs`,
16
+ with the RTL's direction and width only when they came from the model (2), never when
17
+ they came from reading the RTL (3); ports only the RTL has are counted in `notes`. The
18
+ difference itself is the `ports_diff` check's to report: nothing is resolved here.
19
+
20
+ `spec_slice(model, block)` is the block's spec side of the Design Model for a role that
21
+ must not see RTL: requirements (id, text, where), registers and fields, interrupts,
22
+ memory regions, clock and reset. No module, RTL port or RTL file is in it.
23
+
24
+ `project_interface(...)` wires `interface_for` to a project: the module (instance param
25
+ `module`, else the block's top module in the model, else the `top` template of the
26
+ profile's `tb_static` adapter) and the RTL files a declaration may come from (that
27
+ module's file in the model, the `tb_static` adapter's `files`/`filelist`, else the
28
+ profile layout's `filelist` and `rtl` entries for the block). The engine calls it for a
29
+ testbench task's context and `tb_static` calls it for its port check, so both see the
30
+ same interface.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import re
36
+ from collections.abc import Callable, Iterable, Mapping
37
+ from pathlib import Path
38
+ from typing import Any, Literal
39
+
40
+ from pydantic import BaseModel, ConfigDict, Field
41
+
42
+ from chipgraph.adapters.tool.filelist import FilelistError, read_filelist
43
+ from chipgraph.core.model.entities import (
44
+ BlockEntity,
45
+ FieldEntity,
46
+ InterruptEntity,
47
+ MemoryRegionEntity,
48
+ ModuleEntity,
49
+ PortEntity,
50
+ RegisterEntity,
51
+ RequirementEntity,
52
+ ResetEntity,
53
+ )
54
+ from chipgraph.core.model.model import DesignModel
55
+ from chipgraph.packs.dv.declaration import ModuleDeclaration, extract_declaration
56
+
57
+ PortSource = Literal["spec", "model_rtl", "rtl_declaration"]
58
+
59
+ RTL_DIFFERS = "rtl differs"
60
+ """The note on a spec port the RTL declares differently (see `ports_diff`)."""
61
+ MISSING_IN_RTL = "missing in rtl"
62
+ """The note on a spec port the RTL does not declare."""
63
+
64
+ CLOCK_PATTERNS = (r"^(?:i_)?clk", r"_clk(?:_i)?$", r"^clock")
65
+ """Port names taken as a clock when the model names none."""
66
+ RESET_PATTERNS = (r"^(?:i_)?rst", r"_rst(?:_n|_ni)?$", r"^reset")
67
+ """Port names taken as a reset when the model names none."""
68
+
69
+ LayoutValue = str | tuple[str, ...] | list[str]
70
+
71
+
72
+ class InterfacePort(BaseModel):
73
+ """One port of the interface, with where it was learnt."""
74
+
75
+ model_config = ConfigDict(extra="forbid", frozen=True)
76
+
77
+ schema_version: Literal[1] = Field(default=1, description="Schema version of this model.")
78
+ name: str = Field(description="The port name: `dut.<name>` in a cocotb test.")
79
+ direction: str | None = Field(default=None, description="input, output, inout (or ref).")
80
+ width: int | str | None = Field(default=None, description="Bits, or an expression.")
81
+ type: str = Field(default="", description="Declared type, from a declaration only.")
82
+ packed: str = Field(default="", description="Packed dimensions, from a declaration only.")
83
+ unpacked: str = Field(default="", description="Unpacked dimensions, from a declaration.")
84
+ description: str = Field(default="", description="The spec's description of the port.")
85
+ source: PortSource = Field(description="Where the port was learnt.")
86
+ note: str | None = Field(default=None, description="`rtl differs` / `missing in rtl`.")
87
+ rtl: dict[str, Any] | None = Field(
88
+ default=None,
89
+ description="The RTL's direction and width when they differ (model RTL ports only).",
90
+ )
91
+
92
+
93
+ class InterfaceParameter(BaseModel):
94
+ """A parameter of the module's header (from a declaration only)."""
95
+
96
+ model_config = ConfigDict(extra="forbid", frozen=True)
97
+
98
+ schema_version: Literal[1] = Field(default=1, description="Schema version of this model.")
99
+ name: str = Field(description="The parameter name.")
100
+ kind: str = Field(description="'parameter', 'localparam' or 'type'.")
101
+ type: str = Field(default="", description="Its declared type.")
102
+ default: str | None = Field(default=None, description="Its default expression.")
103
+ source: PortSource = Field(default="rtl_declaration", description="Where it was learnt.")
104
+
105
+
106
+ class Interface(BaseModel):
107
+ """The ports (and header parameters) a testbench may drive and sample."""
108
+
109
+ model_config = ConfigDict(extra="forbid", frozen=True)
110
+
111
+ schema_version: Literal[1] = Field(default=1, description="Schema version of this model.")
112
+ module: str | None = Field(default=None, description="The module under test (toplevel).")
113
+ block: str | None = Field(default=None, description="Its block.")
114
+ source: PortSource | None = Field(
115
+ default=None, description="Where the port list came from; None when none was found."
116
+ )
117
+ ports: tuple[InterfacePort, ...] = Field(default=(), description="The ports, in order.")
118
+ parameters: tuple[InterfaceParameter, ...] = Field(
119
+ default=(), description="The module header's parameters (from a declaration)."
120
+ )
121
+ clock: str | None = Field(default=None, description="The clock port, if known.")
122
+ reset: str | None = Field(default=None, description="The reset port, if known.")
123
+ reset_active_low: bool | None = Field(default=None, description="Whether reset is low.")
124
+ notes: tuple[str, ...] = Field(default=(), description="What the reader should know.")
125
+ truncated: bool = Field(default=False, description="Whether the declaration was cut.")
126
+
127
+ @property
128
+ def port_names(self) -> frozenset[str]:
129
+ """Every port name."""
130
+ return frozenset(p.name for p in self.ports)
131
+
132
+ def view(self) -> dict[str, Any]:
133
+ """The interface as a task's context shows it: unset port fields left out."""
134
+ out = self.model_dump(mode="json", exclude={"schema_version", "ports", "parameters"})
135
+ out["ports"] = [
136
+ p.model_dump(mode="json", exclude_defaults=True, exclude={"schema_version"})
137
+ | {"source": p.source}
138
+ for p in self.ports
139
+ ]
140
+ out["parameters"] = [
141
+ p.model_dump(mode="json", exclude_defaults=True, exclude={"schema_version"})
142
+ | {"source": p.source}
143
+ for p in self.parameters
144
+ ]
145
+ return out
146
+
147
+
148
+ DeclarationLoader = Callable[[str | None], ModuleDeclaration | None]
149
+ """Reads the declaration of a module (None: the only one) from the RTL, on demand."""
150
+
151
+
152
+ # --- the model ---------------------------------------------------------------------------
153
+
154
+
155
+ def _spec_ports(model: DesignModel, block: str) -> list[PortEntity]:
156
+ prefix = f"port:spec.{block}."
157
+ ports = [p for p in model.by_kind("port") if isinstance(p, PortEntity)]
158
+ found = [p for p in ports if p.key.startswith(prefix)]
159
+ return sorted(found, key=lambda p: (p.source.line or 0, p.key))
160
+
161
+
162
+ def _module(model: DesignModel, name: str) -> ModuleEntity | None:
163
+ for entity in model.by_kind("module"):
164
+ if isinstance(entity, ModuleEntity) and entity.name == name:
165
+ return entity
166
+ return None
167
+
168
+
169
+ def _rtl_ports(model: DesignModel, module: str) -> list[PortEntity]:
170
+ key = f"module:{module}"
171
+ ports = [p for p in model.by_kind("port") if isinstance(p, PortEntity) and p.module == key]
172
+ return sorted(ports, key=lambda p: (p.source.line or 0, p.key))
173
+
174
+
175
+ def top_module(model: DesignModel, block: str) -> str | None:
176
+ """The block's top RTL module in the model: the one module it owns that no other
177
+ module it owns instantiates (None when there is not exactly one)."""
178
+ key = f"block:{block}"
179
+ owned = [m for m in model.by_kind("module") if isinstance(m, ModuleEntity) and m.block == key]
180
+ inner = {r.dst for m in owned for r in model.get_relations(src=m.key, kind="instantiates")}
181
+ roots = [m for m in owned if m.key not in inner]
182
+ return roots[0].name if len(roots) == 1 else None
183
+
184
+
185
+ def _block_of(model: DesignModel, module: str) -> str | None:
186
+ entity = _module(model, module)
187
+ if entity is None or entity.block is None:
188
+ return None
189
+ return entity.block.partition(":")[2] or None
190
+
191
+
192
+ def _block_entity(model: DesignModel, block: str) -> BlockEntity | None:
193
+ entity = model.get(f"block:{block}")
194
+ return entity if isinstance(entity, BlockEntity) else None
195
+
196
+
197
+ # --- clock and reset -----------------------------------------------------------------------
198
+
199
+
200
+ def _matches(name: str, patterns: Iterable[str]) -> bool:
201
+ return any(re.search(p, name, re.IGNORECASE) for p in patterns)
202
+
203
+
204
+ def _clock_reset(
205
+ model: DesignModel | None, block: str | None, names: list[str]
206
+ ) -> tuple[str | None, str | None, bool | None]:
207
+ clock = reset = None
208
+ active_low: bool | None = None
209
+ if model is not None and block is not None:
210
+ entity = _block_entity(model, block)
211
+ if entity is not None:
212
+ c, r = entity.attrs.get("clock"), entity.attrs.get("reset")
213
+ clock = c if isinstance(c, str) and c in names else None
214
+ reset = r if isinstance(r, str) and r in names else None
215
+ if reset is not None:
216
+ found = model.get(f"reset:{reset}")
217
+ if isinstance(found, ResetEntity):
218
+ active_low = found.active_low
219
+ if clock is None:
220
+ clock = next((n for n in names if _matches(n, CLOCK_PATTERNS)), None)
221
+ if reset is None:
222
+ reset = next((n for n in names if n != clock and _matches(n, RESET_PATTERNS)), None)
223
+ if reset is not None and active_low is None:
224
+ active_low = re.search(r"(?:_n|_ni|_b|n)$", reset.lower()) is not None
225
+ return clock, reset, active_low
226
+
227
+
228
+ # --- the provider --------------------------------------------------------------------------
229
+
230
+
231
+ def _from_declaration(decl: ModuleDeclaration) -> list[InterfacePort]:
232
+ return [
233
+ InterfacePort(
234
+ name=p.name,
235
+ direction=p.direction,
236
+ width=p.width,
237
+ type=p.type,
238
+ packed=p.packed,
239
+ unpacked=p.unpacked,
240
+ source="rtl_declaration",
241
+ )
242
+ for p in decl.ports
243
+ ]
244
+
245
+
246
+ def _parameters(decl: ModuleDeclaration | None) -> tuple[InterfaceParameter, ...]:
247
+ if decl is None:
248
+ return ()
249
+ return tuple(
250
+ InterfaceParameter(name=p.name, kind=p.kind, type=p.type, default=p.default)
251
+ for p in decl.parameters
252
+ )
253
+
254
+
255
+ def _differs(spec: PortEntity, direction: str | None, width: int | str | None) -> bool:
256
+ if spec.direction is not None and direction is not None and spec.direction != direction:
257
+ return True
258
+ return isinstance(spec.width, int) and isinstance(width, int) and spec.width != width
259
+
260
+
261
+ def _spec_interface(
262
+ spec_ports: list[PortEntity],
263
+ rtl: list[PortEntity] | None,
264
+ decl: ModuleDeclaration | None,
265
+ ) -> tuple[list[InterfacePort], list[str]]:
266
+ """The spec ports, each compared with the RTL's when the RTL's are known."""
267
+ known: dict[str, tuple[str | None, int | str | None]] | None = None
268
+ from_model = rtl is not None
269
+ if rtl is not None:
270
+ known = {p.name: (p.direction, p.width) for p in rtl}
271
+ elif decl is not None:
272
+ known = {p.name: (p.direction, p.width) for p in decl.ports}
273
+ ports: list[InterfacePort] = []
274
+ for spec in spec_ports:
275
+ desc = spec.attrs.get("description")
276
+ note: str | None = None
277
+ rtl_value: dict[str, Any] | None = None
278
+ if known is not None:
279
+ if spec.name not in known:
280
+ note = MISSING_IN_RTL
281
+ elif _differs(spec, *known[spec.name]):
282
+ note = RTL_DIFFERS
283
+ if from_model:
284
+ rtl_value = {"direction": known[spec.name][0], "width": known[spec.name][1]}
285
+ ports.append(
286
+ InterfacePort(
287
+ name=spec.name,
288
+ direction=spec.direction,
289
+ width=spec.width,
290
+ description=desc if isinstance(desc, str) else "",
291
+ source="spec",
292
+ note=note,
293
+ rtl=rtl_value,
294
+ )
295
+ )
296
+ notes: list[str] = []
297
+ if known is not None:
298
+ extra = sorted(set(known) - {p.name for p in spec_ports})
299
+ if extra and from_model:
300
+ notes.append(
301
+ "the RTL has ports the spec does not list (not part of this interface; "
302
+ f"ports_diff reports them): {', '.join(extra)}"
303
+ )
304
+ elif extra:
305
+ notes.append(
306
+ f"the RTL declares {len(extra)} port(s) the spec does not list (not part of "
307
+ "this interface; ports_diff reports them)"
308
+ )
309
+ if any(p.note for p in ports):
310
+ notes.append(
311
+ f"ports marked '{RTL_DIFFERS}' or '{MISSING_IN_RTL}' disagree with the RTL; the "
312
+ "spec is what the test checks, ports_diff reports the difference"
313
+ )
314
+ return ports, notes
315
+
316
+
317
+ def interface_for(
318
+ module: str | None,
319
+ *,
320
+ model: DesignModel | None = None,
321
+ block: str | None = None,
322
+ declaration: DeclarationLoader | ModuleDeclaration | None = None,
323
+ ) -> Interface:
324
+ """The interface of `module` (or of `block`'s top module), learnt without the RTL body.
325
+
326
+ `declaration` is called (with the module name, or None for "the only module") only
327
+ when the model has no RTL ports of the module: for the port list when the spec has
328
+ none either, else to compare the spec ports with.
329
+ """
330
+ if model is not None and block is None and module is not None:
331
+ block = _block_of(model, module)
332
+ if model is not None and module is None and block is not None:
333
+ module = top_module(model, block)
334
+
335
+ def load() -> ModuleDeclaration | None:
336
+ if declaration is None or isinstance(declaration, ModuleDeclaration):
337
+ return declaration
338
+ return declaration(module)
339
+
340
+ spec_ports = _spec_ports(model, block) if model is not None and block is not None else []
341
+ rtl_ports = _rtl_ports(model, module) if model is not None and module is not None else []
342
+ decl: ModuleDeclaration | None = None
343
+ notes: list[str] = []
344
+ source: PortSource | None
345
+ if spec_ports:
346
+ source = "spec"
347
+ if not rtl_ports:
348
+ decl = load()
349
+ ports, notes = _spec_interface(spec_ports, rtl_ports or None, decl)
350
+ elif rtl_ports:
351
+ source = "model_rtl"
352
+ ports = [
353
+ InterfacePort(name=p.name, direction=p.direction, width=p.width, source="model_rtl")
354
+ for p in rtl_ports
355
+ ]
356
+ notes.append("the spec lists no ports for this block: the RTL's declared ports are used")
357
+ else:
358
+ decl = load()
359
+ if decl is not None:
360
+ source = "rtl_declaration"
361
+ ports = _from_declaration(decl)
362
+ notes.append(
363
+ "neither the spec nor the Design Model lists this module's ports: they were "
364
+ "read from its declaration (header only)"
365
+ )
366
+ else:
367
+ source = None
368
+ ports = []
369
+ notes.append(
370
+ "no interface found: the spec lists no ports, the Design Model has none "
371
+ "(run `chipgraph ingest`), and no RTL declaration of the module was found; "
372
+ "report needs_human"
373
+ )
374
+ if decl is not None and module is None:
375
+ module = decl.module
376
+ clock, reset, active_low = _clock_reset(model, block, [p.name for p in ports])
377
+ return Interface(
378
+ module=module,
379
+ block=block,
380
+ source=source,
381
+ ports=tuple(ports),
382
+ parameters=_parameters(decl),
383
+ clock=clock,
384
+ reset=reset,
385
+ reset_active_low=active_low,
386
+ notes=tuple(notes),
387
+ truncated=decl.truncated if decl is not None else False,
388
+ )
389
+
390
+
391
+ # --- the spec slice ------------------------------------------------------------------------
392
+
393
+
394
+ def _where(entity: Any) -> str | None:
395
+ src = entity.source
396
+ if src.file is None:
397
+ return None
398
+ return f"{src.file}:{src.line}" if src.line else src.file
399
+
400
+
401
+ def _requirement_keys(model: DesignModel, block: str) -> list[str]:
402
+ key = f"block:{block}"
403
+ keys = {
404
+ r.key
405
+ for r in model.by_kind("requirement")
406
+ if isinstance(r, RequirementEntity) and r.attrs.get("block") == key
407
+ }
408
+ return sorted(keys)
409
+
410
+
411
+ def block_requirements(model: DesignModel, block: str) -> list[dict[str, Any]]:
412
+ """The block's requirements: id, text and where the spec states them."""
413
+ found: list[dict[str, Any]] = []
414
+ for key in _requirement_keys(model, block):
415
+ req = model.get(key)
416
+ if not isinstance(req, RequirementEntity):
417
+ continue
418
+ found.append(
419
+ {
420
+ "id": req.name,
421
+ "text": req.text or "",
422
+ "where": _where(req),
423
+ "id_source": req.attrs.get("id_source"),
424
+ }
425
+ )
426
+ return found
427
+
428
+
429
+ def spec_slice(model: DesignModel, block: str) -> dict[str, Any]:
430
+ """The block's spec side of the Design Model (see the module docstring)."""
431
+ key = f"block:{block}"
432
+ entity = _block_entity(model, block)
433
+ registers: list[dict[str, Any]] = []
434
+ for reg in sorted(
435
+ (r for r in model.by_kind("register") if isinstance(r, RegisterEntity) and r.block == key),
436
+ key=lambda r: (str(r.offset), r.name),
437
+ ):
438
+ fields = [
439
+ {
440
+ "name": f.name,
441
+ "msb": f.msb,
442
+ "lsb": f.lsb,
443
+ "access": f.access,
444
+ "reset": f.reset_value,
445
+ }
446
+ for f in sorted(
447
+ (
448
+ f
449
+ for f in model.by_kind("field")
450
+ if isinstance(f, FieldEntity) and f.register == reg.key
451
+ ),
452
+ key=lambda f: (f.lsb or 0, f.name),
453
+ )
454
+ ]
455
+ registers.append(
456
+ {
457
+ "name": reg.name,
458
+ "offset": reg.offset,
459
+ "access": reg.access,
460
+ "reset": reg.reset_value,
461
+ "width": reg.width,
462
+ "fields": fields,
463
+ "where": _where(reg),
464
+ }
465
+ )
466
+ interrupts = [
467
+ {"name": i.name, "line": i.line}
468
+ for i in model.by_kind("interrupt")
469
+ if isinstance(i, InterruptEntity) and i.block == key
470
+ ]
471
+ regions = [
472
+ {"name": m.name, "base": m.base, "size": m.size}
473
+ for m in model.by_kind("memory_region")
474
+ if isinstance(m, MemoryRegionEntity) and m.block == key
475
+ ]
476
+ attrs = entity.attrs if entity is not None else {}
477
+ return {
478
+ "block": block,
479
+ "bus": attrs.get("bus"),
480
+ "clock": attrs.get("clock"),
481
+ "reset": attrs.get("reset"),
482
+ "requirements": block_requirements(model, block),
483
+ "registers": registers,
484
+ "interrupts": interrupts,
485
+ "memory_regions": regions,
486
+ }
487
+
488
+
489
+ # --- a project's sources -------------------------------------------------------------------
490
+
491
+
492
+ def _fill(template: str, block: str | None) -> str:
493
+ if block is None:
494
+ return template
495
+ return template.replace("{block}", block).replace("{BLOCK}", block.upper())
496
+
497
+
498
+ def _templates(value: Any) -> list[str]:
499
+ if isinstance(value, str):
500
+ return [value] if value else []
501
+ if isinstance(value, (list, tuple)):
502
+ return [v for v in value if isinstance(v, str) and v]
503
+ return []
504
+
505
+
506
+ def _filelist_sources(root: Path, rel: str) -> list[Path]:
507
+ path = root / rel
508
+ if not path.is_file():
509
+ return []
510
+ found: list[Path] = []
511
+ styles: tuple[Literal["filelist", "cwd"], ...] = ("filelist", "cwd")
512
+ for style in styles:
513
+ try:
514
+ listed = read_filelist(path, relative_to=style, root=root)
515
+ except FilelistError:
516
+ continue
517
+ found.extend(p for p in listed.sources if p.is_file() and p not in found)
518
+ return found
519
+
520
+
521
+ def rtl_files(
522
+ root: Path,
523
+ *,
524
+ model: DesignModel | None,
525
+ module: str | None,
526
+ block: str | None,
527
+ args: Mapping[str, Any],
528
+ layout: Mapping[str, Any],
529
+ ) -> list[Path]:
530
+ """The RTL files a declaration of `module` may be read from, in order."""
531
+ files: list[Path] = []
532
+
533
+ def add(path: Path) -> None:
534
+ if path.is_file() and path not in files:
535
+ files.append(path)
536
+
537
+ if model is not None and module is not None:
538
+ entity = _module(model, module)
539
+ if entity is not None and entity.file:
540
+ add(root / entity.file)
541
+ for template in _templates(args.get("files")):
542
+ for path in sorted(root.glob(_fill(template, block))):
543
+ add(path)
544
+ filelists = _templates(args.get("filelist")) or _templates(layout.get("filelist"))
545
+ for template in filelists:
546
+ for path in _filelist_sources(root, _fill(template, block)):
547
+ add(path)
548
+ for template in _templates(layout.get("rtl")):
549
+ for path in sorted(root.glob(_fill(template, block))):
550
+ add(path)
551
+ return files
552
+
553
+
554
+ def project_interface(
555
+ root: Path,
556
+ *,
557
+ model: DesignModel | None,
558
+ block: str | None,
559
+ params: Mapping[str, str],
560
+ args: Mapping[str, Any],
561
+ layout: Mapping[str, Any],
562
+ ) -> Interface:
563
+ """The interface of a testbench task in a project (see the module docstring).
564
+
565
+ `args` are the profile's `tb_static` adapter args (`top`, `files`, `filelist`),
566
+ `layout` the profile's layout for the block, `params` the rule instance's.
567
+ """
568
+ module = params.get("module") or None
569
+ if module is None and model is not None and block is not None:
570
+ module = top_module(model, block)
571
+ if module is None:
572
+ top = args.get("top")
573
+ module = _fill(top, block) if isinstance(top, str) and top else None
574
+
575
+ def load(name: str | None) -> ModuleDeclaration | None:
576
+ files = rtl_files(root, model=model, module=name, block=block, args=args, layout=layout)
577
+ return extract_declaration(files, name)
578
+
579
+ return interface_for(module, model=model, block=block, declaration=load)
580
+
581
+
582
+ __all__ = [
583
+ "MISSING_IN_RTL",
584
+ "RTL_DIFFERS",
585
+ "DeclarationLoader",
586
+ "Interface",
587
+ "InterfaceParameter",
588
+ "InterfacePort",
589
+ "PortSource",
590
+ "block_requirements",
591
+ "interface_for",
592
+ "project_interface",
593
+ "rtl_files",
594
+ "spec_slice",
595
+ "top_module",
596
+ ]
@@ -0,0 +1,14 @@
1
+ schema_version: 1
2
+ name: dv
3
+ version: 0.1.0
4
+ description: >-
5
+ Design verification (DESIGN.md 8.2): the rule `dv/tb_module`, a cocotb test for each
6
+ block written by the testbench Author from the spec and the interface only (never the
7
+ RTL), and its skill `dv/cocotb`. The interface comes from the Design Model, or from the
8
+ module's declaration only (`interface.py`, `declaration.py`); the `tb_static` check
9
+ reads the written test without a simulator.
10
+ provides:
11
+ rules:
12
+ - rules
13
+ skills:
14
+ - skills/dv
@@ -0,0 +1,42 @@
1
+ # The testbench of one block, written without seeing its RTL (DESIGN.md 5.1, 5.3 F1;
2
+ # task M2-06).
3
+ #
4
+ # For each block a `tb-author` task writes a cocotb 2.1 test module from the block's spec
5
+ # side of the Design Model (`block/{block}`: requirements with ids, registers and fields,
6
+ # interrupts, clock and reset), the module's interface (`interface/{block}`: the spec's
7
+ # ports, else the model's RTL ports, else the module's declaration only, read by the
8
+ # engine with pyslang) and the block's spec documents (the profile layout's `spec`). It
9
+ # never gets an rtl-kind input: the role's read policy denies `rtl`, the rule validator
10
+ # refuses such an input, `get_context` refuses to serve one, and the guard refuses reads
11
+ # of the RTL paths. What `submit` tells it of a failed check is filtered the same way.
12
+ #
13
+ # Params: `block` (from `foreach: blocks`); `module`, optional: the module under test.
14
+ # Without it the interface is the block's top module (in the Design Model, else the
15
+ # `tb_static` adapter's `top` template). The Planner's per-module expansion (M2-03, wired
16
+ # in M2-11) will set `module` and write one test per module; until then the test file is
17
+ # named after the block.
18
+ #
19
+ # The output is in the block's testbench directory, `{layout.tb}`: the directory part of
20
+ # the first `layout.tb` template of the block (QSoC's `dv/{block}/**` gives `dv/pwm`),
21
+ # `dv/{block}` when the profile has none (`chipgraph.packs.digital_rtl.layout`).
22
+ #
23
+ # Checks: `tb_static` (no simulator, no RTL: the file compiles, imports cocotb, has a
24
+ # `@cocotb.test()` coroutine, uses only interface ports, reads no file, cites every
25
+ # requirement id). A profile that sets `adapters.tb_static.sim` also runs the test on the
26
+ # RTL with that sim adapter once the static checks pass (see `checks/tb_static.py`).
27
+ rule: tb_module
28
+ kind: agent
29
+ role: tb-author
30
+ description: >-
31
+ Write a cocotb test module for the block from its spec, requirements and interface
32
+ only: one test per requirement group, each citing its requirement ids.
33
+ foreach: blocks
34
+ inputs:
35
+ - model: "block/{block}"
36
+ - model: "interface/{block}"
37
+ outputs:
38
+ - "{layout.tb}/test_{block}.py"
39
+ checks: [tb_static]
40
+ skills: [dv/cocotb]
41
+ budget:
42
+ tries: 3