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,127 @@
1
+ """The `naming` check's rules, as a versioned pydantic data model.
2
+
3
+ A naming rule file (e.g. `src/chipgraph/orgs/qnsc/naming-v1.yml`) declares, as data:
4
+
5
+ - per **object kind** (`module`, `port`, `parameter`, `localparam`, `enum_value`,
6
+ `instance`, `signal`, `memory`, `genvar`, ...) an allowed `pattern`, a `rule` id and a
7
+ `message`;
8
+ - **lexical** rules that apply to every identifier declared in a file (a forbidden
9
+ regex with an optional allow-if escape hatch);
10
+ - a **vocabulary** map of forbidden word -> project term;
11
+ - the `ignore_comment` that exempts a declaration line.
12
+
13
+ The check (`chipgraph.checks.naming`) reads this model; it does not hard-code any
14
+ pattern. Rule ids and messages follow the source document's numbering ("2.1 module",
15
+ "1.5 vocabulary", ...) so a reviewer can compare the file with the document line by line.
16
+
17
+ The JSON Schema of this model is committed at `schemas/formats/naming-rules.schema.json`
18
+ (regenerate with `python -m chipgraph.checks.naming`).
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import re
25
+ from typing import Literal
26
+
27
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
28
+
29
+ # The object kinds a rule file may constrain. These are the classifications the check
30
+ # derives from the pyslang syntax tree, not free-form strings.
31
+ ObjectKind = Literal[
32
+ "module",
33
+ "port",
34
+ "parameter",
35
+ "localparam",
36
+ "enum_value",
37
+ "instance",
38
+ "signal",
39
+ "memory",
40
+ "genvar",
41
+ ]
42
+
43
+
44
+ def _valid_regex(pattern: str) -> str:
45
+ try:
46
+ re.compile(pattern)
47
+ except re.error as exc: # pragma: no cover - exercised via model validation
48
+ raise ValueError(f"invalid regex {pattern!r}: {exc}") from exc
49
+ return pattern
50
+
51
+
52
+ class _Model(BaseModel):
53
+ model_config = ConfigDict(extra="forbid", frozen=True)
54
+
55
+
56
+ class KindRule(_Model):
57
+ """The allowed-name rule for one object kind: a pattern the whole name must match."""
58
+
59
+ rule: str = Field(description="The rule id, e.g. '2.1 module'.")
60
+ pattern: str = Field(description="A regex the identifier must fully match to pass.")
61
+ message: str = Field(description="Human-readable message shown when the name is rejected.")
62
+
63
+ @field_validator("pattern")
64
+ @classmethod
65
+ def _check_pattern(cls, value: str) -> str:
66
+ return _valid_regex(value)
67
+
68
+
69
+ class LexicalRule(_Model):
70
+ """A lexical rule: a forbidden pattern with an optional allow-if escape hatch."""
71
+
72
+ rule: str = Field(description="The rule id, e.g. '1.1 case'.")
73
+ forbid_pattern: str = Field(description="A regex that, if found in a name, is a violation.")
74
+ allow_if_matches: str | None = Field(
75
+ default=None,
76
+ description="If set, a name fully matching this regex is exempt from this rule.",
77
+ )
78
+ message: str = Field(description="Human-readable message shown when the rule fires.")
79
+
80
+ @field_validator("forbid_pattern", "allow_if_matches")
81
+ @classmethod
82
+ def _check_pattern(cls, value: str | None) -> str | None:
83
+ return _valid_regex(value) if value is not None else None
84
+
85
+
86
+ class VocabularyRule(_Model):
87
+ """The mandatory-vocabulary rule: each forbidden word maps to the project term."""
88
+
89
+ rule: str = Field(description="The rule id, e.g. '1.5 vocabulary'.")
90
+ replace: dict[str, str] = Field(description="Map of forbidden word -> preferred project term.")
91
+ message: str = Field(description="Human-readable message prefix shown when a word is used.")
92
+
93
+
94
+ class NamingRules(_Model):
95
+ """A naming rule file: per-kind patterns, lexical rules, vocabulary and metadata."""
96
+
97
+ schema_version: Literal[1] = Field(default=1, description="Schema version of this model.")
98
+ document: str = Field(description="Name of the source naming-rule document.")
99
+ version: str = Field(description="Version of the source document these rules enforce.")
100
+ ignore_comment: str = Field(
101
+ default="naming-check: ignore",
102
+ description="A trailing line comment matching this exempts the declaration line.",
103
+ )
104
+ identifiers: dict[ObjectKind, KindRule] = Field(
105
+ description="Allowed-name rule per object kind (module, port, signal, ...)."
106
+ )
107
+ lexical: tuple[LexicalRule, ...] = Field(
108
+ default=(), description="Lexical rules applied to every identifier declared in a file."
109
+ )
110
+ vocabulary: VocabularyRule | None = Field(
111
+ default=None, description="The mandatory-vocabulary rule, if any."
112
+ )
113
+
114
+
115
+ def schema_json() -> str:
116
+ """The JSON Schema of a naming rule file, as committed under `schemas/formats/`."""
117
+ return json.dumps(NamingRules.model_json_schema(), indent=2, sort_keys=True) + "\n"
118
+
119
+
120
+ __all__ = [
121
+ "KindRule",
122
+ "LexicalRule",
123
+ "NamingRules",
124
+ "ObjectKind",
125
+ "VocabularyRule",
126
+ "schema_json",
127
+ ]
@@ -0,0 +1,260 @@
1
+ """Syntax-tree helpers shared by the `connect` and `hardcode` cross checks.
2
+
3
+ Both checks look at RTL *structure*, not the elaborated model: `connect` needs the
4
+ hierarchy instances of the top module and their named port connections; `hardcode` needs
5
+ every integer literal a file types, with its decoded value and line, and the comment on
6
+ that line (so a `// contract:` tag can exempt it). Neither elaborates: a single file with
7
+ missing packages still parses (matching `naming`'s single-file, syntax-only parse).
8
+
9
+ Everything here is pure and deterministic and never raises on malformed input: a parse
10
+ error is returned as a `ParseFailure` (via `chipgraph.checks._naming_pyslang.parse_errors`),
11
+ never thrown.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import dataclass, field
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ from pyslang.syntax import SyntaxKind, SyntaxNode, SyntaxTree
21
+
22
+ from chipgraph.checks._naming_pyslang import ParseFailure, parse_errors, parse_file
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class PortConn:
27
+ """One named port connection `.port(expr)` of an instance."""
28
+
29
+ port: str
30
+ expr: str
31
+ line: int
32
+
33
+
34
+ @dataclass(frozen=True, slots=True)
35
+ class Instance:
36
+ """One hierarchy instance in a module: `module_name inst_name (.p(e), ...)`."""
37
+
38
+ module: str
39
+ name: str
40
+ line: int
41
+ connections: tuple[PortConn, ...] = ()
42
+
43
+ def connection(self, port: str) -> PortConn | None:
44
+ """The named connection to `port`, or None if it is not connected by name."""
45
+ for conn in self.connections:
46
+ if conn.port == port:
47
+ return conn
48
+ return None
49
+
50
+
51
+ @dataclass(frozen=True, slots=True)
52
+ class IntLiteral:
53
+ """One integer literal token in RTL: its decoded value, line and raw text."""
54
+
55
+ value: int
56
+ line: int
57
+ raw: str
58
+
59
+
60
+ @dataclass(slots=True)
61
+ class ParsedRtl:
62
+ """The syntax facts one RTL file yields, plus any parse errors."""
63
+
64
+ instances: list[Instance] = field(default_factory=list)
65
+ literals: list[IntLiteral] = field(default_factory=list)
66
+ parse_errors: tuple[ParseFailure, ...] = ()
67
+ line_comments: dict[int, str] = field(default_factory=dict)
68
+
69
+
70
+ def parse_rtl(path: Path) -> ParsedRtl:
71
+ """Parse the RTL at `path`, returning its instances, literals and parse errors."""
72
+ tree = parse_file(path)
73
+ text = path.read_text(errors="ignore")
74
+ return ParsedRtl(
75
+ instances=_instances(tree),
76
+ literals=_literals(tree),
77
+ parse_errors=tuple(parse_errors(tree)),
78
+ line_comments=_line_comments(text),
79
+ )
80
+
81
+
82
+ def parse_rtl_text(text: str, name: str = "source") -> ParsedRtl:
83
+ """Parse RTL `text` (for tests), returning its instances, literals and parse errors."""
84
+ tree = SyntaxTree.fromText(text, name)
85
+ return ParsedRtl(
86
+ instances=_instances(tree),
87
+ literals=_literals(tree),
88
+ parse_errors=tuple(parse_errors(tree)),
89
+ line_comments=_line_comments(text),
90
+ )
91
+
92
+
93
+ # --------------------------------------------------------------------------------------
94
+ # instances and their named port connections
95
+ # --------------------------------------------------------------------------------------
96
+
97
+
98
+ def _instances(tree: Any) -> list[Instance]:
99
+ sm = tree.sourceManager
100
+
101
+ def line_of(token: Any) -> int:
102
+ return int(sm.getLineNumber(token.location))
103
+
104
+ out: list[Instance] = []
105
+ for hi in _find(tree.root, SyntaxKind.HierarchyInstantiation):
106
+ module = _text(getattr(hi, "type", None))
107
+ if not module:
108
+ continue
109
+ for inst in _find(hi, SyntaxKind.HierarchicalInstance):
110
+ decl = getattr(inst, "decl", None)
111
+ name = _text(getattr(decl, "name", None)) if decl is not None else ""
112
+ conns: list[PortConn] = []
113
+ for npc in _find(inst, SyntaxKind.NamedPortConnection):
114
+ port = _text(getattr(npc, "name", None))
115
+ if not port:
116
+ continue
117
+ expr = _text(getattr(npc, "expr", None))
118
+ conns.append(PortConn(port=port, expr=expr, line=line_of(npc.getFirstToken())))
119
+ out.append(
120
+ Instance(
121
+ module=module,
122
+ name=name,
123
+ line=line_of(inst.getFirstToken()),
124
+ connections=tuple(conns),
125
+ )
126
+ )
127
+ return out
128
+
129
+
130
+ # --------------------------------------------------------------------------------------
131
+ # integer literals (tokens, so comments and strings are never scanned)
132
+ # --------------------------------------------------------------------------------------
133
+
134
+ _RADIX = {"b": 2, "o": 8, "d": 10, "h": 16}
135
+
136
+
137
+ def _literals(tree: Any) -> list[IntLiteral]:
138
+ """Every integer literal in `tree`, decoded, in source order.
139
+
140
+ Works on the token stream, so numbers inside comments or strings are never seen. A
141
+ sized literal (`32'h8000_4000`) is three tokens -- the size `IntegerLiteral`, the
142
+ `IntegerBase` (`'h`) and the digits `IntegerLiteral` -- so the size token is skipped
143
+ and the digits are decoded with the base's radix. A bare `IntegerLiteral` with no
144
+ base before or after it is a plain decimal.
145
+ """
146
+ sm = tree.sourceManager
147
+ tokens = _tokens(tree.root)
148
+ out: list[IntLiteral] = []
149
+ for i, tok in enumerate(tokens):
150
+ kind = str(getattr(tok, "kind", ""))
151
+ if not kind.endswith("IntegerLiteral"):
152
+ continue
153
+ prev = str(getattr(tokens[i - 1], "kind", "")) if i > 0 else ""
154
+ nxt = str(getattr(tokens[i + 1], "kind", "")) if i + 1 < len(tokens) else ""
155
+ raw = str(getattr(tok, "rawText", "") or "")
156
+ line = int(sm.getLineNumber(tok.location))
157
+ if prev.endswith("IntegerBase"):
158
+ base = str(getattr(tokens[i - 1], "rawText", "") or "").lstrip("'").lower()
159
+ value = _decode(raw, _RADIX.get(base[:1], 10))
160
+ elif nxt.endswith("IntegerBase"):
161
+ # A size prefix (`32` in `32'h...`), not a value of its own.
162
+ continue
163
+ else:
164
+ value = _decode(raw, 10)
165
+ if value is not None:
166
+ out.append(IntLiteral(value=value, line=line, raw=raw))
167
+ return out
168
+
169
+
170
+ def _decode(digits: str, radix: int) -> int | None:
171
+ """Decode `digits` (with `_` separators) in `radix`; None if it is not a plain number.
172
+
173
+ A literal with `x`/`z`/`?` bits (`8'bxxxx_xxxx`) has no single value, so it is
174
+ dropped rather than guessed at.
175
+ """
176
+ cleaned = digits.replace("_", "")
177
+ if not cleaned:
178
+ return None
179
+ try:
180
+ return int(cleaned, radix)
181
+ except ValueError:
182
+ return None
183
+
184
+
185
+ # --------------------------------------------------------------------------------------
186
+ # shared token/node walking
187
+ # --------------------------------------------------------------------------------------
188
+
189
+
190
+ def _find(node: Any, kind: SyntaxKind) -> list[Any]:
191
+ out: list[Any] = []
192
+
193
+ def walk(n: Any) -> None:
194
+ if n is None:
195
+ return
196
+ if getattr(n, "kind", None) == kind:
197
+ out.append(n)
198
+ if isinstance(n, SyntaxNode):
199
+ for i in range(len(n)):
200
+ try:
201
+ child = n[i]
202
+ except Exception: # pragma: no cover - defensive against odd child slots
203
+ child = None
204
+ walk(child)
205
+
206
+ walk(node)
207
+ return out
208
+
209
+
210
+ def _tokens(node: Any) -> list[Any]:
211
+ out: list[Any] = []
212
+
213
+ def walk(n: Any) -> None:
214
+ if n is None:
215
+ return
216
+ if isinstance(n, SyntaxNode):
217
+ for i in range(len(n)):
218
+ try:
219
+ child = n[i]
220
+ except Exception: # pragma: no cover - defensive against odd child slots
221
+ child = None
222
+ walk(child)
223
+ else:
224
+ out.append(n)
225
+
226
+ walk(node)
227
+ return out
228
+
229
+
230
+ def _text(node: Any) -> str:
231
+ """The trimmed source text of `node` (or a token), or '' when it is None/missing."""
232
+ if node is None:
233
+ return ""
234
+ if getattr(node, "isMissing", False):
235
+ return ""
236
+ return str(node).strip()
237
+
238
+
239
+ def _line_comments(text: str) -> dict[int, str]:
240
+ """Map each 1-based line number to its `//` comment text (without the `//`), if any.
241
+
242
+ A best-effort text scan used only to read a `// contract:`-style exemption marker;
243
+ the literals themselves come from the token stream, never from this.
244
+ """
245
+ out: dict[int, str] = {}
246
+ for i, line in enumerate(text.splitlines(), start=1):
247
+ idx = line.find("//")
248
+ if idx >= 0:
249
+ out[i] = line[idx + 2 :]
250
+ return out
251
+
252
+
253
+ __all__ = [
254
+ "Instance",
255
+ "IntLiteral",
256
+ "ParsedRtl",
257
+ "PortConn",
258
+ "parse_rtl",
259
+ "parse_rtl_text",
260
+ ]
@@ -0,0 +1,235 @@
1
+ """`CdcStructCheck`: structural clock-domain-crossing check on the Design Model (layer 1).
2
+
3
+ A layer-1 (structure) check (DESIGN.md 4.8): a signal that crosses from one clock domain
4
+ to another must pass through a synchroniser cell declared in the profile (e.g.
5
+ `qnsc_sync`). A crossing that goes straight into a flop of another domain -- directly or
6
+ through plain combinational logic -- is an error.
7
+
8
+ The Design Model does not hold nets or connections (`adapters/tool/pyslang.py`), so this
9
+ check elaborates the block's RTL itself (`_cdc_elaborate`) and analyses clock domains and
10
+ their crossings structurally (`_cdc_analyze`). It is a per-block check: it runs on the RTL
11
+ of the block being checked. Issues cite the destination flop's file and line.
12
+
13
+ Rules:
14
+
15
+ * `cdc.unsynchronised` (error): a register samples another domain with no synchroniser.
16
+ * `cdc.unknown_domain` (info): a register samples an input whose clock domain is unknown.
17
+ * `cdc.not_analysed` (info): a path leaves what this structural check resolves.
18
+
19
+ Args:
20
+
21
+ * `sync_cells` (list of str, required): module names that are synchronisers.
22
+ * `scope` (list of glob str, optional): RTL files to analyse; `{block}` is substituted
23
+ from `ctx.params`. Defaults to the files of the RTL modules the model attributes to the
24
+ block, plus the files of every module they instantiate (so a synchroniser or child IP
25
+ the block wires up is elaborated and recognised, not treated as an unknown blackbox).
26
+ * `clock_patterns` (list of str, optional): regexes that recognise a clock port by name
27
+ (default `^i_clk`, `^clk`).
28
+ * `ignore` (list of glob str, optional): files to drop (default: any `vendor` path).
29
+
30
+ Example profile usage (`.chipgraph.yml`):
31
+
32
+ ```yaml
33
+ adapters:
34
+ cdc_struct:
35
+ use: cdc_struct
36
+ sync_cells: [qnsc_sync]
37
+ ```
38
+ """
39
+
40
+ from __future__ import annotations
41
+
42
+ import re
43
+ import time
44
+ from pathlib import Path
45
+
46
+ from chipgraph.checks._cdc_analyze import Crossing, Kind, analyse
47
+ from chipgraph.checks._cdc_elaborate import elaborate
48
+ from chipgraph.checks._common import (
49
+ ArgError,
50
+ compute_idempotency_key,
51
+ error_result,
52
+ glob_to_regex,
53
+ iter_repo_files,
54
+ optional_list_of_str,
55
+ require_list_of_str,
56
+ sha256_file,
57
+ )
58
+ from chipgraph.checks._model import ModelUnavailable, block_param, load_model
59
+ from chipgraph.core.contracts import CheckResult, CheckSpec, Issue
60
+ from chipgraph.core.contracts.types import CheckStatus, Severity
61
+ from chipgraph.core.model.entities import ModuleEntity
62
+ from chipgraph.core.model.model import DesignModel
63
+ from chipgraph.core.plugin_api.types import ToolContext
64
+
65
+ _DEFAULT_CLOCK_PATTERNS = (r"^i_clk", r"^clk")
66
+ _DEFAULT_IGNORE = ("**/vendor/**", "vendor/**")
67
+
68
+ _RULE_BY_KIND: dict[Kind, tuple[str, Severity]] = {
69
+ Kind.UNSYNCHRONISED: ("cdc.unsynchronised", "error"),
70
+ Kind.UNKNOWN_DOMAIN: ("cdc.unknown_domain", "info"),
71
+ Kind.NOT_ANALYSED: ("cdc.not_analysed", "info"),
72
+ }
73
+
74
+
75
+ class CdcStructCheck:
76
+ """Checks that every clock-domain crossing goes through a declared synchroniser."""
77
+
78
+ id = "cdc_struct"
79
+ name = "CDC (structural)"
80
+ per_block = True # a block's RTL is analysed at a time
81
+
82
+ async def run(self, spec: CheckSpec, ctx: ToolContext) -> CheckResult:
83
+ start = time.monotonic()
84
+ args = spec.args
85
+ block = block_param(ctx)
86
+ try:
87
+ sync_cells = frozenset(require_list_of_str(args, "sync_cells"))
88
+ scope = [_substitute(g, block) for g in optional_list_of_str(args, "scope")]
89
+ ignore = optional_list_of_str(args, "ignore") or list(_DEFAULT_IGNORE)
90
+ clock_patterns = optional_list_of_str(args, "clock_patterns") or list(
91
+ _DEFAULT_CLOCK_PATTERNS
92
+ )
93
+ clock_matchers = [re.compile(p) for p in clock_patterns]
94
+ except (ArgError, re.error) as exc:
95
+ return error_result(spec, str(exc), start)
96
+
97
+ try:
98
+ loaded = load_model(ctx.repo_root)
99
+ except ModelUnavailable as exc:
100
+ return error_result(spec, str(exc), start, rule="model")
101
+
102
+ block_modules = _block_modules(loaded.model, block)
103
+
104
+ scope_files = _scope_files(ctx.repo_root, scope, ignore, loaded.model, block_modules)
105
+ if not scope_files:
106
+ # Nothing to analyse (a block with no RTL yet): a clean pass, not an error.
107
+ return _result(spec, [], scope_files, ctx.repo_root, loaded.build_inputs_hash, start)
108
+
109
+ tops = {m.name for m in block_modules} or None
110
+ elaboration = elaborate(
111
+ (ctx.repo_root / f for f in scope_files),
112
+ sync_cells=sync_cells,
113
+ clock_matchers=clock_matchers,
114
+ repo_root=ctx.repo_root,
115
+ tops=tops,
116
+ )
117
+
118
+ issues: list[Issue] = [
119
+ Issue(
120
+ file=diag.file,
121
+ line=diag.line,
122
+ rule="parse",
123
+ severity="error",
124
+ msg=f"elaboration error: {diag.message}",
125
+ )
126
+ for diag in elaboration.diagnostics
127
+ ]
128
+ for module in sorted(elaboration.modules):
129
+ for crossing in analyse(elaboration.modules[module]):
130
+ issues.append(_issue(crossing))
131
+
132
+ return _result(spec, issues, scope_files, ctx.repo_root, loaded.build_inputs_hash, start)
133
+
134
+
135
+ def _issue(crossing: Crossing) -> Issue:
136
+ rule, severity = _RULE_BY_KIND[crossing.kind]
137
+ return Issue(
138
+ file=crossing.loc.file,
139
+ line=crossing.loc.line,
140
+ rule=rule,
141
+ severity=severity,
142
+ msg=crossing.detail,
143
+ )
144
+
145
+
146
+ def _block_modules(model: DesignModel, block: str | None) -> list[ModuleEntity]:
147
+ """The RTL modules the model attributes to `block` (all modules when no block)."""
148
+ block_key = f"block:{block}" if block else None
149
+ out: list[ModuleEntity] = []
150
+ for entity in model.by_kind("module"):
151
+ assert isinstance(entity, ModuleEntity)
152
+ if block_key is None or entity.block == block_key:
153
+ out.append(entity)
154
+ return out
155
+
156
+
157
+ def _scope_files(
158
+ repo_root: Path,
159
+ scope: list[str],
160
+ ignore: list[str],
161
+ model: DesignModel,
162
+ block_modules: list[ModuleEntity],
163
+ ) -> list[str]:
164
+ """The repo-relative RTL files to analyse, deterministically ordered.
165
+
166
+ An explicit `scope` (globs, `{block}` already substituted) wins. Otherwise the files
167
+ of the block's modules, *plus* the files of every module they instantiate (following
168
+ `instantiates` transitively): so a synchroniser or child IP the block wires up is
169
+ elaborated too and can be recognised, instead of being an unknown blackbox. `ignore`
170
+ globs (vendor by default) are dropped either way, so a vendored cell is not analysed
171
+ as a top of its own.
172
+ """
173
+ ignore_regexes = [glob_to_regex(p) for p in ignore]
174
+
175
+ def kept(rel: str) -> bool:
176
+ return not any(r.fullmatch(rel) for r in ignore_regexes)
177
+
178
+ if scope:
179
+ regexes = [glob_to_regex(g) for g in scope]
180
+ return sorted(
181
+ f
182
+ for f in iter_repo_files(repo_root)
183
+ if any(r.fullmatch(f) for r in regexes) and kept(f)
184
+ )
185
+
186
+ files = {m.file for m in _with_instantiated(model, block_modules) if m.file}
187
+ return sorted(f for f in files if kept(f))
188
+
189
+
190
+ def _with_instantiated(model: DesignModel, block_modules: list[ModuleEntity]) -> list[ModuleEntity]:
191
+ """`block_modules` plus every module reachable through `instantiates`, deduped."""
192
+ by_key = {m.key: m for m in model.by_kind("module") if isinstance(m, ModuleEntity)}
193
+ seen: dict[str, ModuleEntity] = {m.key: m for m in block_modules}
194
+ frontier = list(seen)
195
+ while frontier:
196
+ src = frontier.pop()
197
+ for rel in model.get_relations(src=src, kind="instantiates"):
198
+ child = by_key.get(rel.dst)
199
+ if child is not None and child.key not in seen:
200
+ seen[child.key] = child
201
+ frontier.append(child.key)
202
+ return list(seen.values())
203
+
204
+
205
+ def _substitute(glob: str, block: str | None) -> str:
206
+ if "{block}" not in glob:
207
+ return glob
208
+ if block is None:
209
+ raise ArgError(f"scope glob {glob!r} uses {{block}} but no block is set in ctx.params")
210
+ return glob.replace("{block}", block)
211
+
212
+
213
+ def _result(
214
+ spec: CheckSpec,
215
+ issues: list[Issue],
216
+ scope_files: list[str],
217
+ repo_root: Path,
218
+ build_inputs_hash: str | None,
219
+ start: float,
220
+ ) -> CheckResult:
221
+ ordered = sorted(issues, key=lambda i: (i.file or "", i.line or 0, i.rule, i.msg))
222
+ file_hashes = [(f, sha256_file(repo_root / f)) for f in scope_files]
223
+ file_hashes.append(("<model>", build_inputs_hash or "none"))
224
+ key = compute_idempotency_key(spec.id, spec.args, file_hashes)
225
+ status: CheckStatus = "fail" if any(i.severity == "error" for i in ordered) else "pass"
226
+ return CheckResult(
227
+ check_id=spec.id,
228
+ status=status,
229
+ issues=tuple(ordered),
230
+ duration_s=time.monotonic() - start,
231
+ idempotency_key=key,
232
+ )
233
+
234
+
235
+ __all__ = ["CdcStructCheck"]