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,272 @@
1
+ """Turn one module's elaborated facts into clock-domain crossings.
2
+
3
+ Conservative, structural analysis (no timing, no value analysis), per DESIGN.md 4.8
4
+ layer 1:
5
+
6
+ 1. Each `always_ff` has a clock *root* -- the module input clock its event samples. Every
7
+ register (a variable it assigns) belongs to that root's domain.
8
+ 2. A register's next-state expression reads other signals; each read is expanded through
9
+ combinational logic (`assign`, `always_comb`) to the set of *source registers* it
10
+ ultimately depends on.
11
+ 3. A source register in domain A feeding a register in domain B != A is a crossing.
12
+ 4. The crossing is *synchronised* when the path goes through a synchroniser cell: a sync
13
+ instance's output is a fresh signal that carries no source domain, so expansion stops
14
+ there (its source set is empty). A register reading a sync output is therefore never a
15
+ crossing, while the same value taken raw or through plain combinational logic still is.
16
+ 5. When the source is a module input with no known domain, or the path leaves the analysed
17
+ level (deeper hierarchy), the result is `info` ("unknown"/"not analysed"), never an
18
+ error.
19
+
20
+ One level of hierarchy is followed. A non-sync child instance with a resolvable clock
21
+ input is modelled as a set of *pseudo-registers*: each of its outputs is a flop in the
22
+ domain its clock port maps to (the parent clock root the port connects to), sampling the
23
+ child's data inputs. That makes a crossing wired between two sibling instances through the
24
+ parent visible, and lets the child's output feed a further crossing. A child with no
25
+ resolvable clock is treated as combinational passthrough; a sync child breaks the path.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ from dataclasses import dataclass, field
31
+ from enum import Enum
32
+
33
+ from chipgraph.checks._cdc_elaborate import Loc, ModuleFacts, SubInstance
34
+
35
+
36
+ class Kind(Enum):
37
+ """What a resolved crossing is: a real one, or one we could not decide."""
38
+
39
+ UNSYNCHRONISED = "unsynchronised"
40
+ UNKNOWN_DOMAIN = "unknown_domain"
41
+ NOT_ANALYSED = "not_analysed"
42
+
43
+
44
+ @dataclass(frozen=True, slots=True)
45
+ class Crossing:
46
+ """One resolved clock-domain crossing at a destination register."""
47
+
48
+ kind: Kind
49
+ dst_name: str
50
+ dst_domain: str
51
+ loc: Loc
52
+ detail: str
53
+
54
+
55
+ @dataclass(frozen=True, slots=True)
56
+ class _Dst:
57
+ """A crossing destination: a real register or a child instance's pseudo-register."""
58
+
59
+ name: str
60
+ domain: str | None
61
+ loc: Loc
62
+ reads: frozenset[str]
63
+
64
+
65
+ def analyse(facts: ModuleFacts) -> list[Crossing]:
66
+ """Every crossing at a destination of `facts`, deterministically ordered.
67
+
68
+ A destination in domain B whose fan-in reaches a source register in domain A != B is a
69
+ crossing (unsynchronised), unless every A-domain path is broken by a synchroniser
70
+ (which yields an empty source set for its output).
71
+ """
72
+ domains, dsts = _domains_and_destinations(facts)
73
+ resolver = _Resolver(facts, domains)
74
+
75
+ crossings: list[Crossing] = []
76
+ for dst in sorted(dsts, key=lambda d: (d.loc.line or 0, d.name)):
77
+ dst_domain = dst.domain
78
+ if dst_domain is None:
79
+ # The destination itself has no known clock: cannot judge crossings into it.
80
+ continue
81
+ sources = _SourceSet()
82
+ for read in dst.reads:
83
+ sources.merge(resolver.sources_of(read))
84
+
85
+ bad_domains = {d for d in sources.domains if d is not None and d != dst_domain}
86
+ if bad_domains:
87
+ crossings.append(
88
+ Crossing(
89
+ kind=Kind.UNSYNCHRONISED,
90
+ dst_name=dst.name,
91
+ dst_domain=dst_domain,
92
+ loc=dst.loc,
93
+ detail=(
94
+ f"{dst.name!r} in clock domain {dst_domain!r} samples a register in "
95
+ f"domain {', '.join(sorted(bad_domains))} without a synchroniser "
96
+ "between them"
97
+ ),
98
+ )
99
+ )
100
+ continue
101
+ # No hard crossing, but an unresolved source that *might* be another domain.
102
+ if sources.unknown_input is not None:
103
+ crossings.append(
104
+ Crossing(
105
+ kind=Kind.UNKNOWN_DOMAIN,
106
+ dst_name=dst.name,
107
+ dst_domain=dst_domain,
108
+ loc=dst.loc,
109
+ detail=(
110
+ f"{dst.name!r} in clock domain {dst_domain!r} samples "
111
+ f"{sources.unknown_input!r}, an input with no known clock domain"
112
+ ),
113
+ )
114
+ )
115
+ elif sources.unresolved is not None:
116
+ crossings.append(
117
+ Crossing(
118
+ kind=Kind.NOT_ANALYSED,
119
+ dst_name=dst.name,
120
+ dst_domain=dst_domain,
121
+ loc=dst.loc,
122
+ detail=(
123
+ f"{dst.name!r} in clock domain {dst_domain!r} reads "
124
+ f"{sources.unresolved!r}, which this check does not analyse deeper"
125
+ ),
126
+ )
127
+ )
128
+ return crossings
129
+
130
+
131
+ def _domains_and_destinations(
132
+ facts: ModuleFacts,
133
+ ) -> tuple[dict[str, str | None], list[_Dst]]:
134
+ """The domain of every register/pseudo-register, and the list of crossing destinations.
135
+
136
+ A real register maps its hierarchical path to its clock root. A non-sync child with a
137
+ resolvable clock also contributes: each output is a pseudo-register in the child's
138
+ mapped domain, reading the child's data inputs -- so the child's output can be both a
139
+ crossing destination and a further source.
140
+ """
141
+ domains: dict[str, str | None] = {}
142
+ dsts: list[_Dst] = []
143
+ for reg in facts.registers.values():
144
+ domains[reg.path] = reg.clock_root
145
+ dsts.append(_Dst(name=reg.name, domain=reg.clock_root, loc=reg.loc, reads=reg.reads))
146
+
147
+ for sub in facts.sub_instances:
148
+ if sub.is_sync:
149
+ continue
150
+ child_domain = _child_domain(sub)
151
+ if child_domain is None:
152
+ continue # combinational passthrough; handled by the resolver, not a flop here
153
+ data_reads = _child_data_reads(sub)
154
+ for port, out_path in sorted(sub.outputs.items()):
155
+ domains[out_path] = child_domain
156
+ dsts.append(
157
+ _Dst(
158
+ name=f"{sub.name}.{port}",
159
+ domain=child_domain,
160
+ loc=sub.loc,
161
+ reads=data_reads,
162
+ )
163
+ )
164
+ return domains, dsts
165
+
166
+
167
+ def _child_domain(sub: SubInstance) -> str | None:
168
+ """The parent clock root a child's single clock input connects to, or None.
169
+
170
+ The connected parent expression is a path like `top.clk_b`; its domain root is the
171
+ last segment (`clk_b`). More than one clock input, or an unresolvable connection, is
172
+ left to the resolver as passthrough rather than guessed at.
173
+ """
174
+ roots = {parent for parent in sub.clock_inputs.values() if parent is not None}
175
+ if len(roots) != 1:
176
+ return None
177
+ (parent,) = roots
178
+ return parent.rsplit(".", 1)[-1]
179
+
180
+
181
+ def _child_data_reads(sub: SubInstance) -> frozenset[str]:
182
+ """The parent paths a child's non-clock inputs read (its data inputs)."""
183
+ reads: set[str] = set()
184
+ for port, in_reads in sub.inputs.items():
185
+ if port in sub.clock_inputs:
186
+ continue
187
+ reads |= in_reads
188
+ return frozenset(reads)
189
+
190
+
191
+ @dataclass(slots=True)
192
+ class _SourceSet:
193
+ """The domains a signal's fan-in reaches, plus any unresolved source it hit."""
194
+
195
+ domains: set[str | None] = field(default_factory=set)
196
+ unknown_input: str | None = None
197
+ unresolved: str | None = None
198
+
199
+ def merge(self, other: _SourceSet) -> None:
200
+ self.domains |= other.domains
201
+ self.unknown_input = self.unknown_input or other.unknown_input
202
+ self.unresolved = self.unresolved or other.unresolved
203
+
204
+
205
+ class _Resolver:
206
+ """Expands a read to the source-register domains it depends on, with memoisation."""
207
+
208
+ def __init__(self, facts: ModuleFacts, domains: dict[str, str | None]) -> None:
209
+ self._domains = domains
210
+ self._comb = facts.comb
211
+ self._cache: dict[str, _SourceSet] = {}
212
+ self._sync_outputs: set[str] = set()
213
+ self._sub_output_reads: dict[str, frozenset[str]] = {}
214
+ for sub in facts.sub_instances:
215
+ for out_path in sub.outputs.values():
216
+ if sub.is_sync:
217
+ self._sync_outputs.add(out_path)
218
+ elif out_path not in domains:
219
+ # A combinational (no-clock) child: its output passes its inputs
220
+ # through. A clocked child's output is already a domain source above.
221
+ reads: set[str] = set()
222
+ for in_reads in sub.inputs.values():
223
+ reads |= in_reads
224
+ self._sub_output_reads[out_path] = frozenset(reads)
225
+
226
+ def sources_of(self, path: str) -> _SourceSet:
227
+ return self._resolve(path, frozenset())
228
+
229
+ def _resolve(self, path: str, stack: frozenset[str]) -> _SourceSet:
230
+ cached = self._cache.get(path)
231
+ if cached is not None:
232
+ return cached
233
+ result = _SourceSet()
234
+ if path in stack:
235
+ return result # break a combinational loop conservatively (do not cache)
236
+ stack = stack | {path}
237
+
238
+ if path in self._sync_outputs:
239
+ # A synchroniser output is a fresh signal in the reader's domain: it carries
240
+ # no source domain, so the source set is empty and the crossing is clean.
241
+ self._cache[path] = result
242
+ return result
243
+
244
+ if path in self._domains:
245
+ result.domains.add(self._domains[path])
246
+ self._cache[path] = result
247
+ return result
248
+
249
+ if path in self._comb:
250
+ for read in self._comb[path].reads:
251
+ result.merge(self._resolve(read, stack))
252
+ self._cache[path] = result
253
+ return result
254
+
255
+ sub_reads = self._sub_output_reads.get(path)
256
+ if sub_reads is not None:
257
+ for read in sub_reads:
258
+ result.merge(self._resolve(read, stack))
259
+ self._cache[path] = result
260
+ return result
261
+
262
+ # Neither register, comb node, nor child output. A plain `top.name` is a module
263
+ # input with no known domain (info); a deeper path is something not analysed.
264
+ if path.count(".") == 1:
265
+ result.unknown_input = path
266
+ else:
267
+ result.unresolved = path
268
+ self._cache[path] = result
269
+ return result
270
+
271
+
272
+ __all__ = ["Crossing", "Kind", "analyse"]