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,455 @@
1
+ """Elaborate a block's RTL with pyslang and expose the facts the CDC check needs.
2
+
3
+ The Design Model does not hold nets or connections (see `adapters/tool/pyslang.py`), so
4
+ the structural CDC check must look at the RTL itself. This module runs a real pyslang
5
+ *elaboration* (unlike `naming`/`connect`, which parse a single file's syntax tree) so
6
+ that named references resolve to symbols, instance port connections are known, and one
7
+ level of hierarchy can be followed.
8
+
9
+ Everything here is pure and deterministic and never raises on malformed input: parse or
10
+ elaboration errors come back as `Diagnostic`s alongside a best-effort model, never thrown.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from collections.abc import Iterable
16
+ from contextlib import suppress
17
+ from dataclasses import dataclass, field
18
+ from pathlib import Path
19
+ from typing import Any
20
+
21
+ from pyslang import Bag
22
+ from pyslang.ast import (
23
+ Compilation,
24
+ CompilationFlags,
25
+ CompilationOptions,
26
+ EdgeKind,
27
+ ExpressionKind,
28
+ StatementKind,
29
+ SymbolKind,
30
+ TimingControlKind,
31
+ VisitAction,
32
+ )
33
+ from pyslang.syntax import SyntaxTree
34
+
35
+
36
+ @dataclass(frozen=True, slots=True)
37
+ class Diagnostic:
38
+ """A parse/elaboration error, with a repo-relative file and 1-based line when known."""
39
+
40
+ file: str | None
41
+ line: int | None
42
+ message: str
43
+
44
+
45
+ @dataclass(frozen=True, slots=True)
46
+ class Loc:
47
+ """A source location: repo-relative file and 1-based line."""
48
+
49
+ file: str | None
50
+ line: int | None
51
+
52
+
53
+ @dataclass(slots=True)
54
+ class Register:
55
+ """A variable assigned by an `always_ff`, with the clock root it is driven by."""
56
+
57
+ path: str
58
+ """The elaborated hierarchical path, a stable identity (e.g. `top.a_q`)."""
59
+
60
+ name: str
61
+ clock_root: str | None
62
+ """The module input clock port name the driving `always_ff` samples, or None."""
63
+
64
+ loc: Loc
65
+ reads: frozenset[str] = frozenset()
66
+ """Hierarchical paths this register's next-state expression reads (before comb expansion)."""
67
+
68
+
69
+ @dataclass(slots=True)
70
+ class CombNode:
71
+ """A combinationally-driven signal (an `assign` LHS or `always_comb` target)."""
72
+
73
+ path: str
74
+ reads: set[str] = field(default_factory=set)
75
+
76
+
77
+ @dataclass(slots=True)
78
+ class SubInstance:
79
+ """A child module instance and its named port connections (one hierarchy level)."""
80
+
81
+ name: str
82
+ module: str
83
+ loc: Loc
84
+ is_sync: bool
85
+ inputs: dict[str, frozenset[str]] = field(default_factory=dict)
86
+ """Input port name -> hierarchical paths the connected expression reads (parent scope)."""
87
+
88
+ outputs: dict[str, str] = field(default_factory=dict)
89
+ """Output port name -> the single parent hierarchical path it drives, when it is one."""
90
+
91
+ clock_inputs: dict[str, str | None] = field(default_factory=dict)
92
+ """Clock-like input port name -> parent path it is connected to, when resolvable."""
93
+
94
+
95
+ @dataclass(slots=True)
96
+ class ModuleFacts:
97
+ """The CDC-relevant facts of one elaborated module instance."""
98
+
99
+ name: str
100
+ registers: dict[str, Register] = field(default_factory=dict)
101
+ comb: dict[str, CombNode] = field(default_factory=dict)
102
+ sub_instances: list[SubInstance] = field(default_factory=list)
103
+ clock_roots: set[str] = field(default_factory=set)
104
+ """Input port names used as a clock by some `always_ff` in this module."""
105
+
106
+
107
+ @dataclass(slots=True)
108
+ class Elaboration:
109
+ """The result of elaborating a block: per-top-module facts and any diagnostics."""
110
+
111
+ modules: dict[str, ModuleFacts] = field(default_factory=dict)
112
+ diagnostics: tuple[Diagnostic, ...] = ()
113
+
114
+
115
+ def elaborate(
116
+ sources: Iterable[Path],
117
+ *,
118
+ sync_cells: frozenset[str],
119
+ clock_matchers: Iterable[Any],
120
+ repo_root: Path | None = None,
121
+ tops: Iterable[str] | None = None,
122
+ ) -> Elaboration:
123
+ """Elaborate `sources` together and extract CDC facts for each top module.
124
+
125
+ `sync_cells` are the module names to treat as synchronisers; `clock_matchers` are
126
+ compiled regexes that recognise a clock-like port by name. `tops`, when given, limits
127
+ which modules become analysis roots; otherwise every uninstantiated module is a top.
128
+ """
129
+ comp = Compilation(_ignore_unknown_modules())
130
+ diags: list[Diagnostic] = []
131
+ for src in sorted(set(sources)):
132
+ try:
133
+ text = src.read_text(errors="ignore")
134
+ except OSError as exc:
135
+ diags.append(Diagnostic(file=_rel(src, repo_root), line=None, message=str(exc)))
136
+ continue
137
+ comp.addSyntaxTree(SyntaxTree.fromText(text, src.as_posix()))
138
+
139
+ sm = comp.sourceManager
140
+ diags.extend(_diagnostics(comp, sm, repo_root))
141
+
142
+ root = comp.getRoot()
143
+ top_names = set(tops) if tops is not None else None
144
+ matchers = list(clock_matchers)
145
+ modules: dict[str, ModuleFacts] = {}
146
+ for top in getattr(root, "topInstances", ()):
147
+ def_name = _definition_name(top)
148
+ if top_names is not None and def_name not in top_names:
149
+ continue
150
+ facts = _extract_module(top, sm, sync_cells, matchers, repo_root)
151
+ modules.setdefault(facts.name, facts)
152
+ return Elaboration(modules=modules, diagnostics=tuple(diags))
153
+
154
+
155
+ # --------------------------------------------------------------------------------------
156
+ # per-module extraction
157
+ # --------------------------------------------------------------------------------------
158
+
159
+
160
+ def _ignore_unknown_modules() -> Bag:
161
+ """Compilation options that treat a module instantiated but not in scope as a blackbox.
162
+
163
+ A per-block scope holds only the block's own files; the child IP a wrapper
164
+ instantiates lives in another block. Ignoring unknown modules (as the pyslang
165
+ extractor does) lets the block elaborate, with the missing child as an unanalysed
166
+ blackbox (its output resolves to `not_analysed`, an `info`), instead of an error.
167
+ """
168
+ opts = CompilationOptions()
169
+ opts.flags = CompilationFlags.IgnoreUnknownModules
170
+ return Bag([opts])
171
+
172
+
173
+ def _extract_module(
174
+ inst: Any,
175
+ sm: Any,
176
+ sync_cells: frozenset[str],
177
+ clock_matchers: list[Any],
178
+ repo_root: Path | None,
179
+ ) -> ModuleFacts:
180
+ facts = ModuleFacts(name=_definition_name(inst))
181
+ for member in inst.body:
182
+ kind = getattr(member, "kind", None)
183
+ if kind == SymbolKind.ProceduralBlock:
184
+ _extract_procedural(member, sm, facts, repo_root)
185
+ elif kind == SymbolKind.ContinuousAssign:
186
+ _extract_continuous_assign(member, facts)
187
+ elif kind == SymbolKind.Instance:
188
+ sub = _extract_instance(member, sm, sync_cells, clock_matchers, repo_root)
189
+ if sub is not None:
190
+ facts.sub_instances.append(sub)
191
+ return facts
192
+
193
+
194
+ def _extract_procedural(block: Any, sm: Any, facts: ModuleFacts, repo_root: Path | None) -> None:
195
+ """Extract registers (always_ff) or comb nodes (always_comb/always) from a block."""
196
+ proc_kind = str(getattr(block, "procedureKind", ""))
197
+ body = getattr(block, "body", None)
198
+ if body is None:
199
+ return
200
+
201
+ is_ff = proc_kind.endswith("AlwaysFF")
202
+ clock_root: str | None = None
203
+ inner = body
204
+ if getattr(body, "kind", None) == StatementKind.Timed:
205
+ if is_ff:
206
+ clock_root = _clock_root_of(getattr(body, "timing", None))
207
+ if clock_root is not None:
208
+ facts.clock_roots.add(clock_root)
209
+ inner = getattr(body, "stmt", body)
210
+
211
+ for lhs, reads, loc in _assignments(inner, sm, repo_root):
212
+ if is_ff:
213
+ existing = facts.registers.get(lhs.path)
214
+ merged_reads = (existing.reads if existing else frozenset()) | reads
215
+ facts.registers[lhs.path] = Register(
216
+ path=lhs.path,
217
+ name=lhs.name,
218
+ clock_root=(
219
+ clock_root if existing is None else (existing.clock_root or clock_root)
220
+ ),
221
+ loc=existing.loc if existing is not None else loc,
222
+ reads=merged_reads,
223
+ )
224
+ else:
225
+ node = facts.comb.setdefault(lhs.path, CombNode(path=lhs.path))
226
+ node.reads |= reads
227
+
228
+
229
+ def _extract_continuous_assign(member: Any, facts: ModuleFacts) -> None:
230
+ assignment = getattr(member, "assignment", None)
231
+ if assignment is None or getattr(assignment, "kind", None) != ExpressionKind.Assignment:
232
+ return
233
+ lhs = _lvalue_target(getattr(assignment, "left", None))
234
+ if lhs is None:
235
+ return
236
+ reads = _named_reads(getattr(assignment, "right", None))
237
+ node = facts.comb.setdefault(lhs.path, CombNode(path=lhs.path))
238
+ node.reads |= reads
239
+
240
+
241
+ def _extract_instance(
242
+ inst: Any,
243
+ sm: Any,
244
+ sync_cells: frozenset[str],
245
+ clock_matchers: list[Any],
246
+ repo_root: Path | None,
247
+ ) -> SubInstance | None:
248
+ module = _definition_name(inst)
249
+ if not module:
250
+ return None
251
+ sub = SubInstance(
252
+ name=getattr(inst, "name", "") or "",
253
+ module=module,
254
+ loc=_loc(inst, sm, repo_root),
255
+ is_sync=module in sync_cells,
256
+ )
257
+ body = getattr(inst, "body", None)
258
+ port_list = getattr(body, "portList", ()) if body is not None else ()
259
+ for port in port_list:
260
+ try:
261
+ conn = inst.getPortConnection(port)
262
+ except Exception: # pragma: no cover - defensive
263
+ conn = None
264
+ if conn is None:
265
+ continue
266
+ expr = getattr(conn, "expression", None)
267
+ direction = str(getattr(port, "direction", "")).split(".")[-1]
268
+ pname = getattr(port, "name", "") or ""
269
+ if direction == "In":
270
+ reads = _named_reads(expr)
271
+ sub.inputs[pname] = reads
272
+ if any(m.match(pname) for m in clock_matchers):
273
+ sub.clock_inputs[pname] = next(iter(sorted(reads)), None)
274
+ elif direction == "Out":
275
+ target = _lvalue_target(expr)
276
+ if target is not None:
277
+ sub.outputs[pname] = target.path
278
+ return sub
279
+
280
+
281
+ # --------------------------------------------------------------------------------------
282
+ # expression / statement walking
283
+ # --------------------------------------------------------------------------------------
284
+
285
+
286
+ @dataclass(frozen=True, slots=True)
287
+ class _Target:
288
+ path: str
289
+ name: str
290
+
291
+
292
+ def _assignments(
293
+ stmt: Any, sm: Any, repo_root: Path | None
294
+ ) -> list[tuple[_Target, frozenset[str], Loc]]:
295
+ """Every assignment reachable from `stmt`: its LHS target, RHS reads and location."""
296
+ out: list[tuple[_Target, frozenset[str], Loc]] = []
297
+
298
+ def visit(node: Any) -> Any:
299
+ if getattr(node, "kind", None) == ExpressionKind.Assignment:
300
+ target = _lvalue_target(getattr(node, "left", None))
301
+ if target is not None:
302
+ reads = _named_reads(getattr(node, "right", None))
303
+ out.append((target, reads, _loc(node, sm, repo_root)))
304
+ return VisitAction.Advance
305
+
306
+ _safe_visit(stmt, visit)
307
+ return out
308
+
309
+
310
+ def _lvalue_target(expr: Any) -> _Target | None:
311
+ """The single named variable an l-value writes (peeling selects to one name)."""
312
+ names = _named_symbols(expr)
313
+ if len(names) != 1:
314
+ return None
315
+ path, name = names[0]
316
+ return _Target(path=path, name=name)
317
+
318
+
319
+ def _named_reads(expr: Any) -> frozenset[str]:
320
+ """The hierarchical paths of every `NamedValue` read in `expr`."""
321
+ return frozenset(path for path, _ in _named_symbols(expr))
322
+
323
+
324
+ def _named_symbols(expr: Any) -> list[tuple[str, str]]:
325
+ """Every `(hierarchical_path, name)` referenced by a `NamedValue` in `expr`."""
326
+ out: list[tuple[str, str]] = []
327
+ seen: set[str] = set()
328
+
329
+ def visit(node: Any) -> Any:
330
+ if getattr(node, "kind", None) == ExpressionKind.NamedValue:
331
+ sym = getattr(node, "symbol", None)
332
+ path = getattr(sym, "hierarchicalPath", None)
333
+ if isinstance(path, str) and path not in seen:
334
+ seen.add(path)
335
+ out.append((path, getattr(sym, "name", "") or ""))
336
+ return VisitAction.Advance
337
+
338
+ _safe_visit(expr, visit)
339
+ return out
340
+
341
+
342
+ def _clock_root_of(timing: Any) -> str | None:
343
+ """The clock a posedge/negedge event samples: the signal name of its first edge event.
344
+
345
+ A reset event in the same list (`negedge rst_n`) is left for the caller's domain
346
+ logic to ignore -- a reset name never becomes a register's domain root because it is
347
+ not itself a clock any `always_ff` is keyed on for its data.
348
+ """
349
+ if timing is None:
350
+ return None
351
+ kind = getattr(timing, "kind", None)
352
+ events: list[Any] = []
353
+ if kind == TimingControlKind.SignalEvent:
354
+ events = [timing]
355
+ elif kind == TimingControlKind.EventList:
356
+ events = list(getattr(timing, "events", ()))
357
+ for ev in events:
358
+ edge = getattr(ev, "edge", None)
359
+ if edge not in (EdgeKind.PosEdge, EdgeKind.NegEdge):
360
+ continue
361
+ expr = getattr(ev, "expr", None)
362
+ if getattr(expr, "kind", None) == ExpressionKind.NamedValue:
363
+ sym = getattr(expr, "symbol", None)
364
+ name = getattr(sym, "name", None)
365
+ if isinstance(name, str) and name:
366
+ return name
367
+ return None
368
+
369
+
370
+ def _safe_visit(node: Any, visit: Any) -> None:
371
+ if node is None:
372
+ return
373
+ with suppress(Exception): # pragma: no cover - defensive against odd nodes
374
+ node.visit(visit)
375
+
376
+
377
+ def _definition_name(inst: Any) -> str:
378
+ body = getattr(inst, "body", None)
379
+ definition = getattr(body, "definition", None)
380
+ name = getattr(definition, "name", None)
381
+ return name if isinstance(name, str) else ""
382
+
383
+
384
+ def _loc(node: Any, sm: Any, repo_root: Path | None) -> Loc:
385
+ loc = getattr(node, "location", None)
386
+ if not loc:
387
+ source_range = getattr(node, "sourceRange", None)
388
+ loc = getattr(source_range, "start", None) if source_range is not None else None
389
+ if not loc:
390
+ return Loc(file=None, line=None)
391
+ try:
392
+ raw = sm.getFileName(loc)
393
+ line = sm.getLineNumber(loc)
394
+ except Exception: # pragma: no cover - defensive
395
+ return Loc(file=None, line=None)
396
+ return Loc(file=_rel_str(raw, repo_root), line=int(line) if line else None)
397
+
398
+
399
+ def _diagnostics(comp: Any, sm: Any, repo_root: Path | None) -> list[Diagnostic]:
400
+ """Genuine syntax errors of the scope, as `Diagnostic`s (never raised).
401
+
402
+ Only `Parser`/`Lexer` subsystem errors count -- those are malformed SystemVerilog.
403
+ A partial per-block scope is elaborated without every package or child module it
404
+ needs, so `Lookup`/`Elaboration` diagnostics (an unknown package, an unresolved name
405
+ from a file not in scope) are expected and ignored, exactly as `naming`'s single-file
406
+ parse ignores `Preprocessor` diagnostics. Unknown *modules* are already tolerated by
407
+ the `IgnoreUnknownModules` flag.
408
+ """
409
+ out: list[Diagnostic] = []
410
+ try:
411
+ all_diags = comp.getAllDiagnostics()
412
+ except Exception: # pragma: no cover - defensive
413
+ return out
414
+ for diag in all_diags:
415
+ if not diag.isError():
416
+ continue
417
+ try:
418
+ subsystem = str(diag.code.getSubsystem()).rsplit(".", 1)[-1]
419
+ except Exception: # pragma: no cover - defensive
420
+ subsystem = ""
421
+ if subsystem not in ("Parser", "Lexer"):
422
+ continue
423
+ loc = getattr(diag, "location", None)
424
+ file = _rel_str(sm.getFileName(loc), repo_root) if loc else None
425
+ line = sm.getLineNumber(loc) if loc else None
426
+ out.append(Diagnostic(file=file, line=int(line) if line else None, message=str(diag.code)))
427
+ return out
428
+
429
+
430
+ def _rel(path: Path, repo_root: Path | None) -> str:
431
+ result = _rel_str(path.as_posix(), repo_root)
432
+ return result if result is not None else path.as_posix()
433
+
434
+
435
+ def _rel_str(raw: str | None, repo_root: Path | None) -> str | None:
436
+ if raw is None:
437
+ return None
438
+ if repo_root is None:
439
+ return raw
440
+ try:
441
+ return Path(raw).resolve().relative_to(repo_root.resolve()).as_posix()
442
+ except ValueError:
443
+ return raw
444
+
445
+
446
+ __all__ = [
447
+ "CombNode",
448
+ "Diagnostic",
449
+ "Elaboration",
450
+ "Loc",
451
+ "ModuleFacts",
452
+ "Register",
453
+ "SubInstance",
454
+ "elaborate",
455
+ ]