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,599 @@
1
+ """`TbStaticCheck`: a cocotb test read without a simulator and without the RTL (M2-06).
2
+
3
+ The check the `dv/tb_module` rule runs on the testbench Author's test file. It is
4
+ deterministic and never runs a simulator (unless the profile asks for one, see `sim`
5
+ below); it reads only the test file, the Design Model and, when the model lacks a
6
+ module's ports, that module's declaration (`chipgraph.packs.dv.interface`, the same
7
+ interface the Author's context shows). Issues are on the test file, as `file:line`:
8
+
9
+ - `tb_static.missing`: the test file does not exist;
10
+ - `tb_static.syntax`: it does not compile (`ast.parse`);
11
+ - `tb_static.no_cocotb`: it does not import `cocotb`;
12
+ - `tb_static.no_test`: no `@cocotb.test()` coroutine (`tb_static.not_async`: a
13
+ `@cocotb.test()` on a plain `def`);
14
+ - `tb_static.unknown_port`: a `dut.<name>` access (or `getattr(dut, "<name>")`) whose
15
+ name is not a port of the interface nor an allowed handle attribute (`_log`, `_name`,
16
+ `_path`, and the `allow` arg's names, for a documented clock/reset convention);
17
+ `tb_static.dynamic_access`: `getattr(dut, <not a literal>)`, `dut._id(...)` and other
18
+ lookups by a computed name. When no interface is known the port check is skipped
19
+ with one `warning`;
20
+ - `tb_static.forbidden`: anything that could read the RTL or run a program: `open()`,
21
+ `exec`/`eval`/`compile`/`__import__`, `os.system`/`os.popen`/`os.exec*`/`os.spawn*`/
22
+ `os.listdir`/`os.scandir`/`os.walk`, `.read_text()`/`.read_bytes()`/`.open()`,
23
+ importing `subprocess`, `glob`, `pathlib`, `shutil`, `io`, `fileinput`, `linecache`,
24
+ `importlib`, `codecs`, `builtins`, `pty`, and string literals naming an RTL file
25
+ (`.sv`, `.svh`, `.v`, `.vh`) or an `rtl/` path;
26
+ - requirements (from the Design Model, the block's): every id must appear in a
27
+ `# verifies: <ID>[, <ID>...]` comment or in a `@cocotb.test()` docstring at least once
28
+ (`tb_static.req_missing`, a `warning`); an id cited there that is not one of the
29
+ model's requirements is an `error` (`tb_static.req_unknown`).
30
+
31
+ A `fail` has at least one `error` issue; warnings alone pass.
32
+
33
+ Profile example (`.chipgraph.yml`)::
34
+
35
+ adapters:
36
+ tb_static:
37
+ use: tb_static
38
+ test: "dv/{block}/test_{block}.py" # the default
39
+ top: "tiny_{block}" # the module, when the model has none
40
+ filelist: "filelists/{block}.f" # where its declaration is (default: layout)
41
+ allow: [clk_i] # extra dut names (clock/reset conventions)
42
+ sim: # optional: run the test on the RTL too
43
+ use: edalize
44
+ simulator: verilator
45
+ top: "tiny_{block}"
46
+ filelist: "filelists/{block}.f"
47
+ test_module: "dv/{block}/test_{block}.py"
48
+
49
+ `sim` (optional, per profile): an inline adapter config. Only when every static rule
50
+ passed, the named tool adapter (`edalize`, or any check/tool adapter) runs with these
51
+ args and its result becomes this check's: its status, and its issues after the static
52
+ ones. The engine shows a role that must not see RTL only what it may of that result
53
+ (`FeedbackFilter`).
54
+ """
55
+
56
+ from __future__ import annotations
57
+
58
+ import ast
59
+ import io
60
+ import re
61
+ import time
62
+ import tokenize
63
+ from collections.abc import Iterator, Mapping
64
+ from pathlib import Path
65
+ from typing import Any
66
+
67
+ from chipgraph.checks._common import (
68
+ ArgError,
69
+ compute_idempotency_key,
70
+ error_result,
71
+ optional_list_of_str,
72
+ optional_str,
73
+ sha256_file,
74
+ )
75
+ from chipgraph.checks._model import ModelUnavailable, load_model
76
+ from chipgraph.core.contracts import CheckResult, CheckSpec, Issue
77
+ from chipgraph.core.contracts.types import CheckStatus, Severity
78
+ from chipgraph.core.model.model import DesignModel
79
+ from chipgraph.core.plugin_api.registry import PluginError, Registry
80
+ from chipgraph.core.plugin_api.types import ToolContext
81
+ from chipgraph.packs.dv.interface import Interface, block_requirements, project_interface
82
+
83
+ DEFAULT_TEST = "dv/{block}/test_{block}.py"
84
+ """Where `dv/tb_module` writes a block's test."""
85
+
86
+ HANDLE_ATTRS = frozenset({"_log", "_name", "_path"})
87
+ """Attributes of a cocotb handle a test may use besides the ports."""
88
+
89
+ FORBIDDEN_MODULES = frozenset(
90
+ {
91
+ "subprocess",
92
+ "glob",
93
+ "pathlib",
94
+ "shutil",
95
+ "io",
96
+ "fileinput",
97
+ "linecache",
98
+ "importlib",
99
+ "codecs",
100
+ "builtins",
101
+ "pty",
102
+ }
103
+ )
104
+ FORBIDDEN_CALLS = frozenset({"open", "exec", "eval", "compile", "__import__"})
105
+ FORBIDDEN_OS = frozenset({"system", "popen", "listdir", "scandir", "walk", "startfile"})
106
+ FORBIDDEN_OS_PREFIXES = ("exec", "spawn", "posix_spawn")
107
+ FORBIDDEN_METHODS = frozenset({"read_text", "read_bytes", "open", "iterdir", "glob", "rglob"})
108
+ _RTL_LITERAL = re.compile(r"(?:\.(?:sv|svh|v|vh)\b|(?:^|[/\\])rtl[/\\])", re.IGNORECASE)
109
+ _VERIFIES = re.compile(r"#\s*verifies\s*:\s*(?P<ids>.+)$", re.IGNORECASE)
110
+ _ID_TOKEN = re.compile(r"[A-Za-z][A-Za-z0-9_]*(?:-[A-Za-z0-9_]+)+")
111
+ _ID_LIKE = re.compile(r"^[A-Z][A-Z0-9]*(?:[-_][A-Z0-9]+)*-\d+$")
112
+
113
+
114
+ def _fill(template: str, params: Mapping[str, str]) -> str:
115
+ out = template
116
+ for key, value in params.items():
117
+ out = out.replace("{" + key + "}", value).replace("{" + key.upper() + "}", value.upper())
118
+ return out
119
+
120
+
121
+ def _issue(file: str, line: int | None, rule: str, msg: str, severity: Severity = "error") -> Issue:
122
+ return Issue(file=file, line=line, rule=rule, severity=severity, msg=msg)
123
+
124
+
125
+ # --- what the test file does -------------------------------------------------------------
126
+
127
+
128
+ def _is_cocotb_test(decorator: ast.expr) -> bool:
129
+ target = decorator.func if isinstance(decorator, ast.Call) else decorator
130
+ return (
131
+ isinstance(target, ast.Attribute)
132
+ and target.attr == "test"
133
+ and isinstance(target.value, ast.Name)
134
+ and target.value.id == "cocotb"
135
+ )
136
+
137
+
138
+ def _tests(tree: ast.Module) -> Iterator[ast.FunctionDef | ast.AsyncFunctionDef]:
139
+ for node in ast.walk(tree):
140
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and any(
141
+ _is_cocotb_test(d) for d in node.decorator_list
142
+ ):
143
+ yield node
144
+
145
+
146
+ def _imports_cocotb(tree: ast.Module) -> bool:
147
+ for node in ast.walk(tree):
148
+ if isinstance(node, ast.Import) and any(
149
+ a.name == "cocotb" or a.name.startswith("cocotb.") for a in node.names
150
+ ):
151
+ return True
152
+ if isinstance(node, ast.ImportFrom) and (node.module or "").split(".")[0] == "cocotb":
153
+ return True
154
+ return False
155
+
156
+
157
+ def _dotted(node: ast.expr) -> str | None:
158
+ if isinstance(node, ast.Name):
159
+ return node.id
160
+ if isinstance(node, ast.Attribute):
161
+ base = _dotted(node.value)
162
+ return f"{base}.{node.attr}" if base is not None else None
163
+ return None
164
+
165
+
166
+ def _forbidden(tree: ast.Module, rel: str) -> list[Issue]:
167
+ issues: list[Issue] = []
168
+ os_names = {"os"}
169
+ for node in ast.walk(tree):
170
+ if isinstance(node, ast.Import):
171
+ for alias in node.names:
172
+ top = alias.name.split(".")[0]
173
+ if top in FORBIDDEN_MODULES:
174
+ issues.append(
175
+ _issue(rel, node.lineno, "tb_static.forbidden", f"import of {alias.name!r}")
176
+ )
177
+ if top == "os" and alias.asname:
178
+ os_names.add(alias.asname)
179
+ elif isinstance(node, ast.ImportFrom):
180
+ module = node.module or ""
181
+ top = module.split(".")[0]
182
+ if top in FORBIDDEN_MODULES:
183
+ issues.append(
184
+ _issue(rel, node.lineno, "tb_static.forbidden", f"import from {module!r}")
185
+ )
186
+ if top == "os":
187
+ for alias in node.names:
188
+ if alias.name in FORBIDDEN_OS or alias.name.startswith(FORBIDDEN_OS_PREFIXES):
189
+ issues.append(
190
+ _issue(
191
+ rel,
192
+ node.lineno,
193
+ "tb_static.forbidden",
194
+ f"import of os.{alias.name}",
195
+ )
196
+ )
197
+ elif isinstance(node, ast.Call):
198
+ func = node.func
199
+ name = _dotted(func)
200
+ if isinstance(func, ast.Name) and func.id in FORBIDDEN_CALLS:
201
+ issues.append(
202
+ _issue(rel, node.lineno, "tb_static.forbidden", f"call of {func.id}()")
203
+ )
204
+ elif isinstance(func, ast.Attribute):
205
+ base = _dotted(func.value)
206
+ if base in os_names and (
207
+ func.attr in FORBIDDEN_OS or func.attr.startswith(FORBIDDEN_OS_PREFIXES)
208
+ ):
209
+ issues.append(
210
+ _issue(rel, node.lineno, "tb_static.forbidden", f"call of {name}()")
211
+ )
212
+ elif func.attr in FORBIDDEN_METHODS:
213
+ issues.append(
214
+ _issue(
215
+ rel,
216
+ node.lineno,
217
+ "tb_static.forbidden",
218
+ f"call of .{func.attr}(): a test reads no file",
219
+ )
220
+ )
221
+ elif (
222
+ isinstance(node, ast.Constant)
223
+ and isinstance(node.value, str)
224
+ and _RTL_LITERAL.search(node.value)
225
+ ):
226
+ issues.append(
227
+ _issue(
228
+ rel,
229
+ node.lineno,
230
+ "tb_static.forbidden",
231
+ "a string naming an RTL file or an rtl/ path: a test never reads the RTL",
232
+ )
233
+ )
234
+ return issues
235
+
236
+
237
+ def _dut_names(tree: ast.Module) -> set[str]:
238
+ """The names a test calls the DUT handle: `dut`, and the first argument of each
239
+ `@cocotb.test()` coroutine."""
240
+ names = {"dut"}
241
+ for node in _tests(tree):
242
+ if node.args.args:
243
+ names.add(node.args.args[0].arg)
244
+ return names
245
+
246
+
247
+ def _port_issues(tree: ast.Module, rel: str, allowed: frozenset[str]) -> list[Issue]:
248
+ issues: list[Issue] = []
249
+ duts = _dut_names(tree)
250
+ for node in ast.walk(tree):
251
+ if (
252
+ isinstance(node, ast.Attribute)
253
+ and isinstance(node.value, ast.Name)
254
+ and node.value.id in duts
255
+ ):
256
+ name = node.attr
257
+ if name in allowed or name in HANDLE_ATTRS:
258
+ continue
259
+ if name.startswith("_"):
260
+ issues.append(
261
+ _issue(
262
+ rel,
263
+ node.lineno,
264
+ "tb_static.dynamic_access",
265
+ f"{node.value.id}.{name}: a lookup into the design's internals; use "
266
+ "only the interface's ports",
267
+ )
268
+ )
269
+ continue
270
+ issues.append(
271
+ _issue(
272
+ rel,
273
+ node.lineno,
274
+ "tb_static.unknown_port",
275
+ f"{node.value.id}.{name} is not a port of the interface "
276
+ f"(ports: {', '.join(sorted(allowed)) or 'none'}); a test uses only the "
277
+ "interface, never the design's internals",
278
+ )
279
+ )
280
+ elif (
281
+ isinstance(node, ast.Call)
282
+ and isinstance(node.func, ast.Name)
283
+ and node.func.id in ("getattr", "hasattr", "setattr")
284
+ and node.args
285
+ and isinstance(node.args[0], ast.Name)
286
+ and node.args[0].id in duts
287
+ ):
288
+ key = node.args[1] if len(node.args) > 1 else None
289
+ if isinstance(key, ast.Constant) and isinstance(key.value, str):
290
+ if key.value not in allowed and key.value not in HANDLE_ATTRS:
291
+ issues.append(
292
+ _issue(
293
+ rel,
294
+ node.lineno,
295
+ "tb_static.unknown_port",
296
+ f"{node.func.id}(dut, {key.value!r}): not a port of the interface",
297
+ )
298
+ )
299
+ else:
300
+ issues.append(
301
+ _issue(
302
+ rel,
303
+ node.lineno,
304
+ "tb_static.dynamic_access",
305
+ f"{node.func.id}(dut, <computed name>): name the interface's ports "
306
+ "directly",
307
+ )
308
+ )
309
+ return issues
310
+
311
+
312
+ # --- cocotb 2.x types ------------------------------------------------------------------------
313
+
314
+ _ARRAY_ONLY = frozenset(
315
+ {"to_unsigned", "to_signed", "to_bytes", "binstr", "integer", "signed_integer"}
316
+ )
317
+ """Methods of `LogicArray` that a one-bit signal's value (a `Logic`) does not have."""
318
+
319
+
320
+ def _logic_issues(
321
+ tree: ast.Module, rel: str, widths: Mapping[str, int | str | None]
322
+ ) -> list[Issue]:
323
+ """`dut.<port>.value.to_unsigned()` on a one-bit port: its value is a `Logic`, which has no
324
+ such method (cocotb 2.x), so the test would die with an AttributeError in the simulator."""
325
+ issues: list[Issue] = []
326
+ duts = _dut_names(tree)
327
+ for node in ast.walk(tree):
328
+ if not (isinstance(node, ast.Attribute) and node.attr in _ARRAY_ONLY):
329
+ continue
330
+ value = node.value
331
+ if not (isinstance(value, ast.Attribute) and value.attr == "value"):
332
+ continue
333
+ port = value.value
334
+ if (
335
+ isinstance(port, ast.Attribute)
336
+ and isinstance(port.value, ast.Name)
337
+ and port.value.id in duts
338
+ and widths.get(port.attr) == 1
339
+ ):
340
+ issues.append(
341
+ _issue(
342
+ rel,
343
+ node.lineno,
344
+ "tb_static.logic_method",
345
+ f"{port.value.id}.{port.attr}.value.{node.attr}: {port.attr} is one bit wide, "
346
+ f"so its value is a `Logic`, which has no `{node.attr}`; use "
347
+ f"`int({port.value.id}.{port.attr}.value)`",
348
+ )
349
+ )
350
+ return issues
351
+
352
+
353
+ # --- requirements --------------------------------------------------------------------------
354
+
355
+
356
+ def _citations(text: str, tree: ast.Module) -> list[tuple[int, str]]:
357
+ """Every (line, id) a test cites: in `# verifies:` comments, and id-like tokens in the
358
+ docstrings of `@cocotb.test()` coroutines."""
359
+ found: list[tuple[int, str]] = []
360
+ try:
361
+ for tok in tokenize.generate_tokens(io.StringIO(text).readline):
362
+ if tok.type == tokenize.COMMENT:
363
+ match = _VERIFIES.search(tok.string)
364
+ if match:
365
+ for id_ in re.split(r"[\s,;]+", match.group("ids").strip()):
366
+ if id_:
367
+ found.append((tok.start[0], id_.strip(".")))
368
+ except (tokenize.TokenError, SyntaxError):
369
+ pass
370
+ for node in _tests(tree):
371
+ doc = ast.get_docstring(node, clean=False)
372
+ if not doc or not node.body:
373
+ continue
374
+ line = node.body[0].lineno
375
+ for match in _ID_TOKEN.finditer(doc):
376
+ if _ID_LIKE.match(match.group(0)):
377
+ found.append((line, match.group(0)))
378
+ return found
379
+
380
+
381
+ def _requirement_issues(
382
+ rel: str, text: str, tree: ast.Module, model: DesignModel | None, block: str | None
383
+ ) -> list[Issue]:
384
+ if model is None or block is None:
385
+ return [
386
+ _issue(
387
+ rel,
388
+ None,
389
+ "tb_static.req_unchecked",
390
+ "no Design Model (run `chipgraph ingest`): requirement citations not checked",
391
+ "warning",
392
+ )
393
+ ]
394
+ ids = [r["id"] for r in block_requirements(model, block)]
395
+ known = {
396
+ r.name for r in model.by_kind("requirement")
397
+ } # another block's id is not unknown, only stale ones are
398
+ cited = _citations(text, tree)
399
+ cited_ids = {c for _, c in cited}
400
+ issues = [
401
+ _issue(
402
+ rel,
403
+ line,
404
+ "tb_static.req_unknown",
405
+ f"{id_} is cited but is not a requirement of the Design Model",
406
+ )
407
+ for line, id_ in cited
408
+ if id_ not in known
409
+ ]
410
+ issues.extend(
411
+ _issue(
412
+ rel,
413
+ None,
414
+ "tb_static.req_missing",
415
+ f"requirement {id_} of block {block!r} is cited by no `# verifies:` comment or "
416
+ "`@cocotb.test()` docstring",
417
+ "warning",
418
+ )
419
+ for id_ in ids
420
+ if id_ not in cited_ids
421
+ )
422
+ return issues
423
+
424
+
425
+ # --- the check -------------------------------------------------------------------------------
426
+
427
+
428
+ def _project_parts(root: Path, block: str | None) -> dict[str, Any]:
429
+ """The profile's layout for `block` ({} when there is no readable profile)."""
430
+ from chipgraph.core.config.errors import ConfigError
431
+ from chipgraph.core.config.loader import load
432
+
433
+ try:
434
+ resolved = load(root)
435
+ except (ConfigError, OSError, ValueError):
436
+ return {}
437
+ if resolved is None:
438
+ return {}
439
+ profile = resolved.for_block(block) if block is not None else resolved.profile
440
+ return dict(profile.layout)
441
+
442
+
443
+ def static_issues(
444
+ root: Path,
445
+ rel: str,
446
+ *,
447
+ interface: Interface | None,
448
+ model: DesignModel | None,
449
+ block: str | None,
450
+ allow: frozenset[str] = frozenset(),
451
+ ) -> list[Issue]:
452
+ """Every static issue of the test file `rel` (see the module docstring)."""
453
+ path = root / rel
454
+ if not path.is_file():
455
+ return [_issue(rel, None, "tb_static.missing", f"test file {rel} does not exist")]
456
+ text = path.read_text(encoding="utf-8", errors="replace")
457
+ try:
458
+ tree = ast.parse(text, filename=rel)
459
+ except SyntaxError as exc:
460
+ return [_issue(rel, exc.lineno, "tb_static.syntax", f"does not compile: {exc.msg}")]
461
+ issues: list[Issue] = []
462
+ if not _imports_cocotb(tree):
463
+ issues.append(_issue(rel, None, "tb_static.no_cocotb", "the test does not import cocotb"))
464
+ tests = list(_tests(tree))
465
+ if not tests:
466
+ issues.append(
467
+ _issue(rel, None, "tb_static.no_test", "no `@cocotb.test()` coroutine in the file")
468
+ )
469
+ issues.extend(
470
+ _issue(
471
+ rel,
472
+ t.lineno,
473
+ "tb_static.not_async",
474
+ f"{t.name}: a `@cocotb.test()` must be an `async def`",
475
+ )
476
+ for t in tests
477
+ if not isinstance(t, ast.AsyncFunctionDef)
478
+ )
479
+ issues.extend(_forbidden(tree, rel))
480
+ if interface is None or interface.source is None:
481
+ issues.append(
482
+ _issue(
483
+ rel,
484
+ None,
485
+ "tb_static.no_interface",
486
+ "no interface known for the module: port use not checked",
487
+ "warning",
488
+ )
489
+ )
490
+ else:
491
+ names = {p.name for p in interface.ports} | {
492
+ n for n in (interface.clock, interface.reset) if n
493
+ }
494
+ issues.extend(_port_issues(tree, rel, frozenset(names) | allow))
495
+ issues.extend(_logic_issues(tree, rel, {p.name: p.width for p in interface.ports}))
496
+ issues.extend(_requirement_issues(rel, text, tree, model, block))
497
+ return sorted(issues, key=lambda i: (i.line or 0, i.rule, i.msg))
498
+
499
+
500
+ class TbStaticCheck:
501
+ """Reads a cocotb test file without a simulator and without the RTL."""
502
+
503
+ id = "tb_static"
504
+ name = "Testbench static check"
505
+
506
+ async def run(self, spec: CheckSpec, ctx: ToolContext) -> CheckResult:
507
+ start = time.monotonic()
508
+ args = spec.args
509
+ try:
510
+ test_template = optional_str(args, "test", DEFAULT_TEST)
511
+ allow = frozenset(optional_list_of_str(args, "allow"))
512
+ sim = args.get("sim")
513
+ if sim is not None and not (
514
+ isinstance(sim, Mapping) and isinstance(sim.get("use"), str)
515
+ ):
516
+ raise ArgError("'sim' must be an adapter config with a 'use' key")
517
+ except ArgError as exc:
518
+ return error_result(spec, str(exc), start)
519
+
520
+ params = dict(ctx.params)
521
+ block = params.get("block") or None
522
+ rel = _fill(test_template, params)
523
+ if "{" in rel:
524
+ return error_result(spec, f"test path {rel!r} has an unfilled placeholder", start)
525
+ model: DesignModel | None = None
526
+ model_hash = ""
527
+ try:
528
+ loaded = load_model(ctx.repo_root)
529
+ model, model_hash = loaded.model, loaded.build_inputs_hash or ""
530
+ except ModelUnavailable:
531
+ model = None
532
+ interface = project_interface(
533
+ ctx.repo_root,
534
+ model=model,
535
+ block=block,
536
+ params=params,
537
+ args=args,
538
+ layout=_project_parts(ctx.repo_root, block),
539
+ )
540
+ issues = static_issues(
541
+ ctx.repo_root, rel, interface=interface, model=model, block=block, allow=allow
542
+ )
543
+ status: CheckStatus = "fail" if any(i.severity == "error" for i in issues) else "pass"
544
+ log_tail = (
545
+ f"tb_static: {rel}: {sum(i.severity == 'error' for i in issues)} error(s), "
546
+ f"{sum(i.severity == 'warning' for i in issues)} warning(s); interface "
547
+ f"{interface.module or '?'} from {interface.source or 'nowhere'}"
548
+ )
549
+ if status == "pass" and sim is not None:
550
+ sim_result = await _run_sim(dict(sim), ctx, spec.id)
551
+ status = sim_result.status
552
+ issues = [*issues, *sim_result.issues]
553
+ log_tail = f"{log_tail}\n{sim_result.log_tail}".strip()
554
+ path = ctx.repo_root / rel
555
+ files = [(rel, sha256_file(path))] if path.is_file() else []
556
+ files.append(("<model>", model_hash))
557
+ return CheckResult(
558
+ check_id=spec.id,
559
+ status=status,
560
+ issues=tuple(issues),
561
+ log_tail=log_tail[-4000:],
562
+ duration_s=time.monotonic() - start,
563
+ idempotency_key=compute_idempotency_key(spec.id, args, files),
564
+ )
565
+
566
+
567
+ async def _run_sim(cfg: dict[str, Any], ctx: ToolContext, check_id: str) -> CheckResult:
568
+ """Run the inline `sim` adapter config with this check's context."""
569
+ use = str(cfg.pop("use"))
570
+ registry = Registry()
571
+ try:
572
+ registry.discover()
573
+ except PluginError as exc:
574
+ return _sim_error(check_id, f"cannot load the adapters: {exc}")
575
+ plugin = None
576
+ for kind in ("check", "tool"):
577
+ try:
578
+ plugin = registry.get(kind, use)
579
+ break
580
+ except PluginError:
581
+ continue
582
+ if plugin is None:
583
+ return _sim_error(check_id, f"no check or tool adapter named {use!r} (tb_static.sim)")
584
+ sim_spec = CheckSpec(id=f"{check_id}.sim", capability="sim", adapter=use, args=cfg)
585
+ result: CheckResult = await plugin.run(sim_spec, ctx)
586
+ return result
587
+
588
+
589
+ def _sim_error(check_id: str, msg: str) -> CheckResult:
590
+ return CheckResult(
591
+ check_id=f"{check_id}.sim",
592
+ status="error",
593
+ issues=(Issue(rule="tb_static.sim", msg=msg),),
594
+ duration_s=0.0,
595
+ idempotency_key=compute_idempotency_key(check_id, {"sim": msg}, ()),
596
+ )
597
+
598
+
599
+ __all__ = ["DEFAULT_TEST", "TbStaticCheck", "static_issues"]