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,353 @@
1
+ """`ConnectCheck`: the top's RTL wiring against the chip contract (layer 1).
2
+
3
+ A layer-1 (structure) cross check (DESIGN.md 4.8). It reads the Design Model
4
+ `chipgraph ingest` wrote and parses the top module's RTL file with the pyslang syntax
5
+ tree (no elaboration), collecting the top's hierarchy instances and their named port
6
+ connections, then compares them to the contract:
7
+
8
+ * `instance.missing` -- a contract block that should appear under the top (or a D38
9
+ `instance_of` instance) has no RTL instance of a module owned by its IP.
10
+ * `interrupt.wiring` -- a contract interrupt's output on its instance is not wired. The
11
+ interrupt output is the instance port whose *spec* name matches arg `irq_port`
12
+ (default `^o_int_|irq`). When the design declares an interrupt vector net (arg
13
+ `irq_vector`), the output must connect to bit `line` of that vector; when it does not
14
+ (as in the tinysoc example, whose single interrupt is a plain `timer_irq` output), the
15
+ rule is only that the interrupt output is connected to *something*.
16
+
17
+ Wiring the check cannot resolve (an expression it does not understand) is reported as
18
+ `info`, never guessed at. A missing model is a whole-check `error`; a top file that
19
+ cannot be found or parsed is an `error` too.
20
+
21
+ The top module is the RTL module of the `top` block (arg `top_file` overrides the file).
22
+
23
+ Example profile usage (`.chipgraph.yml`):
24
+
25
+ ```yaml
26
+ adapters:
27
+ connect: { use: connect, irq_port: "^o_int_|irq", irq_vector: "o_int_vec" }
28
+ ```
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import re
34
+ import time
35
+
36
+ from chipgraph.checks._common import (
37
+ ArgError,
38
+ compute_idempotency_key,
39
+ error_result,
40
+ optional_str,
41
+ sha256_file,
42
+ )
43
+ from chipgraph.checks._model import ModelUnavailable, load_model
44
+ from chipgraph.checks._xref_syntax import Instance, parse_rtl
45
+ from chipgraph.core.contracts import CheckResult, CheckSpec, Issue
46
+ from chipgraph.core.contracts.types import CheckStatus
47
+ from chipgraph.core.model.entities import ModuleEntity
48
+ from chipgraph.core.model.model import DesignModel
49
+ from chipgraph.core.plugin_api.types import ToolContext
50
+
51
+ _DEFAULT_IRQ_PORT = r"^o_int_|irq"
52
+ _TOP_BLOCK = "top"
53
+
54
+
55
+ class ConnectCheck:
56
+ """Checks the top module's RTL instances and interrupt wiring against the contract."""
57
+
58
+ id = "connect"
59
+ name = "Connect"
60
+ per_block = False # chip-wide: `chipgraph check` runs it once, not per block
61
+
62
+ async def run(self, spec: CheckSpec, ctx: ToolContext) -> CheckResult:
63
+ start = time.monotonic()
64
+ args = spec.args
65
+
66
+ try:
67
+ irq_port_pat = optional_str(args, "irq_port", _DEFAULT_IRQ_PORT)
68
+ irq_vector = optional_str(args, "irq_vector", "")
69
+ top_file_arg = optional_str(args, "top_file", "")
70
+ top_block = optional_str(args, "top_block", _TOP_BLOCK)
71
+ irq_port_re = re.compile(irq_port_pat)
72
+ except (ArgError, re.error) as exc:
73
+ return error_result(spec, str(exc), start)
74
+
75
+ try:
76
+ loaded = load_model(ctx.repo_root)
77
+ except ModelUnavailable as exc:
78
+ return error_result(spec, str(exc), start, rule="model")
79
+
80
+ model = loaded.model
81
+ top_rel = _top_file(model, top_block, top_file_arg)
82
+ if top_rel is None:
83
+ return error_result(
84
+ spec,
85
+ f"no top module file found for block {top_block!r}; set args['top_file']",
86
+ start,
87
+ rule="top",
88
+ )
89
+ top_path = ctx.repo_root / top_rel
90
+ if not top_path.is_file():
91
+ return error_result(spec, f"top file {top_rel!r} does not exist", start, rule="top")
92
+
93
+ parsed = parse_rtl(top_path)
94
+ issues: list[Issue] = [
95
+ Issue(
96
+ file=top_rel,
97
+ line=pf.line,
98
+ rule="parse",
99
+ severity="error",
100
+ msg=f"parse error: {pf.message}",
101
+ )
102
+ for pf in parsed.parse_errors
103
+ ]
104
+
105
+ issues.extend(
106
+ _connect_issues(model, parsed.instances, top_rel, top_block, irq_port_re, irq_vector)
107
+ )
108
+ issues.sort(key=lambda i: (i.file or "", i.line or 0, i.rule, i.msg))
109
+
110
+ file_hashes = [
111
+ (top_rel, sha256_file(top_path)),
112
+ ("<model>", loaded.build_inputs_hash or "none"),
113
+ ]
114
+ key = compute_idempotency_key(spec.id, args, file_hashes)
115
+ status: CheckStatus = "fail" if any(i.severity == "error" for i in issues) else "pass"
116
+ return CheckResult(
117
+ check_id=spec.id,
118
+ status=status,
119
+ issues=tuple(issues),
120
+ duration_s=time.monotonic() - start,
121
+ idempotency_key=key,
122
+ )
123
+
124
+
125
+ def _top_file(model: DesignModel, top_block: str, top_file_arg: str) -> str | None:
126
+ """The repo-relative top RTL file: `top_file` arg wins, else the top block's module."""
127
+ if top_file_arg:
128
+ return top_file_arg
129
+ top_key = f"block:{top_block}"
130
+ modules = [
131
+ e
132
+ for e in model.by_kind("module")
133
+ if isinstance(e, ModuleEntity) and e.block == top_key and e.file
134
+ ]
135
+ if not modules:
136
+ return None
137
+
138
+ # The module of the top block with the most instances is the integration top.
139
+ def instance_count(module_key: str) -> int:
140
+ return len(model.get_relations(src=module_key, kind="instantiates"))
141
+
142
+ modules.sort(key=lambda m: (-instance_count(m.key), m.key))
143
+ return modules[0].file
144
+
145
+
146
+ def _connect_issues(
147
+ model: DesignModel,
148
+ instances: list[Instance],
149
+ top_rel: str,
150
+ top_block: str,
151
+ irq_port_re: re.Pattern[str],
152
+ irq_vector: str,
153
+ ) -> list[Issue]:
154
+ issues: list[Issue] = []
155
+ instantiated_modules = {inst.module for inst in instances}
156
+
157
+ # instance.missing: every non-top contract block (and D38 instance) needs an RTL
158
+ # instance of a module its IP owns.
159
+ for block in sorted(model.by_kind("block"), key=lambda b: b.key):
160
+ name = block.name
161
+ if name == top_block:
162
+ continue
163
+ ip_key = _ip_key(model, block.key)
164
+ owned = _owned_module_names(model, ip_key)
165
+ if not owned:
166
+ # No RTL module is attributed to this IP yet; cannot check structurally.
167
+ issues.append(
168
+ Issue(
169
+ file=top_rel,
170
+ rule="instance.info",
171
+ severity="info",
172
+ msg=f"block {name!r} owns no RTL module in the model; instance not checked",
173
+ )
174
+ )
175
+ continue
176
+ if owned.isdisjoint(instantiated_modules):
177
+ issues.append(
178
+ Issue(
179
+ file=top_rel,
180
+ rule="instance.missing",
181
+ severity="error",
182
+ msg=(
183
+ f"contract block {name!r} has no RTL instance in the top of a module "
184
+ f"it owns (expected one of {sorted(owned)})"
185
+ ),
186
+ )
187
+ )
188
+
189
+ # interrupt.wiring: each contract interrupt output on its instance must be wired.
190
+ issues.extend(_interrupt_issues(model, instances, top_rel, irq_port_re, irq_vector))
191
+ return issues
192
+
193
+
194
+ def _ip_key(model: DesignModel, block_key: str) -> str:
195
+ """The IP block key a block-as-instance points at via `instance_of`, else itself (D38)."""
196
+ rels = model.get_relations(src=block_key, kind="instance_of")
197
+ return rels[0].dst if rels else block_key
198
+
199
+
200
+ def _owned_module_names(model: DesignModel, block_key: str) -> set[str]:
201
+ """The RTL module names owned by `block_key` (its `block` field)."""
202
+ return {
203
+ e.name
204
+ for e in model.by_kind("module")
205
+ if isinstance(e, ModuleEntity) and e.block == block_key
206
+ }
207
+
208
+
209
+ def _interrupt_issues(
210
+ model: DesignModel,
211
+ instances: list[Instance],
212
+ top_rel: str,
213
+ irq_port_re: re.Pattern[str],
214
+ irq_vector: str,
215
+ ) -> list[Issue]:
216
+ issues: list[Issue] = []
217
+ for irq in sorted(model.by_kind("interrupt"), key=lambda e: e.key):
218
+ block_key = getattr(irq, "block", None)
219
+ if not isinstance(block_key, str):
220
+ continue
221
+ ip_key = block_key # interrupts live on the IP block itself
222
+ owned = _owned_module_names(model, ip_key)
223
+ irq_ports = _spec_irq_ports(model, block_key, irq_port_re)
224
+ if not irq_ports:
225
+ issues.append(
226
+ Issue(
227
+ file=top_rel,
228
+ rule="interrupt.info",
229
+ severity="info",
230
+ msg=(
231
+ f"interrupt {irq.name!r} of block {ip_key!r} has no spec port matching "
232
+ "the interrupt-port pattern; cannot check its wiring"
233
+ ),
234
+ )
235
+ )
236
+ continue
237
+ inst = _instance_of(instances, owned)
238
+ if inst is None:
239
+ # instance.missing already covers a wholly absent instance.
240
+ continue
241
+ conn = None
242
+ matched_port = None
243
+ for port in irq_ports:
244
+ conn = inst.connection(port)
245
+ if conn is not None:
246
+ matched_port = port
247
+ break
248
+ if conn is None:
249
+ issues.append(
250
+ Issue(
251
+ file=top_rel,
252
+ line=inst.line,
253
+ rule="interrupt.wiring",
254
+ severity="error",
255
+ msg=(
256
+ f"interrupt output {sorted(irq_ports)} of instance {inst.name!r} "
257
+ f"(interrupt {irq.name!r}) is not connected by name in the top"
258
+ ),
259
+ )
260
+ )
261
+ continue
262
+ expr = conn.expr.strip()
263
+ if not expr:
264
+ issues.append(
265
+ Issue(
266
+ file=top_rel,
267
+ line=conn.line,
268
+ rule="interrupt.wiring",
269
+ severity="error",
270
+ msg=(
271
+ f"interrupt output {matched_port!r} of instance {inst.name!r} "
272
+ f"(interrupt {irq.name!r}) is connected to nothing"
273
+ ),
274
+ )
275
+ )
276
+ continue
277
+ if irq_vector:
278
+ issues.extend(
279
+ _vector_bit_issue(irq, inst, matched_port or "", conn, top_rel, irq_vector)
280
+ )
281
+ return issues
282
+
283
+
284
+ def _vector_bit_issue(
285
+ irq: object, inst: Instance, port: str, conn: object, top_rel: str, irq_vector: str
286
+ ) -> list[Issue]:
287
+ """Check the interrupt output connects to `irq_vector[line]`; `info` when unresolved."""
288
+ line_no = getattr(irq, "line", None)
289
+ expr = getattr(conn, "expr", "").strip()
290
+ conn_line = getattr(conn, "line", None)
291
+ if not isinstance(line_no, int):
292
+ return [
293
+ Issue(
294
+ file=top_rel,
295
+ line=conn_line,
296
+ rule="interrupt.info",
297
+ severity="info",
298
+ msg=f"interrupt {getattr(irq, 'name', '?')!r} has no line number; not checked",
299
+ )
300
+ ]
301
+ m = re.fullmatch(rf"{re.escape(irq_vector)}\s*\[\s*(\d+)\s*\]", expr)
302
+ if m is None:
303
+ return [
304
+ Issue(
305
+ file=top_rel,
306
+ line=conn_line,
307
+ rule="interrupt.info",
308
+ severity="info",
309
+ msg=(
310
+ f"interrupt output {port!r} of {inst.name!r} connects to {expr!r}, "
311
+ f"which is not a resolvable bit of {irq_vector!r}; not checked further"
312
+ ),
313
+ )
314
+ ]
315
+ if int(m.group(1)) != line_no:
316
+ return [
317
+ Issue(
318
+ file=top_rel,
319
+ line=conn_line,
320
+ rule="interrupt.wiring",
321
+ severity="error",
322
+ msg=(
323
+ f"interrupt {getattr(irq, 'name', '?')!r} is line {line_no} in the contract "
324
+ f"but wired to {irq_vector}[{m.group(1)}]"
325
+ ),
326
+ )
327
+ ]
328
+ return []
329
+
330
+
331
+ def _spec_irq_ports(model: DesignModel, block_key: str, irq_port_re: re.Pattern[str]) -> set[str]:
332
+ """Spec output ports of `block_key` whose name matches the interrupt-port pattern."""
333
+ out: set[str] = set()
334
+ for port in model.by_kind("port"):
335
+ if port.attrs.get("block") != block_key:
336
+ continue
337
+ if port.attrs.get("origin") != "spec":
338
+ continue
339
+ if getattr(port, "direction", None) != "output":
340
+ continue
341
+ if irq_port_re.search(port.name):
342
+ out.add(port.name)
343
+ return out
344
+
345
+
346
+ def _instance_of(instances: list[Instance], module_names: set[str]) -> Instance | None:
347
+ for inst in instances:
348
+ if inst.module in module_names:
349
+ return inst
350
+ return None
351
+
352
+
353
+ __all__ = ["ConnectCheck"]