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.
- chipgraph/__init__.py +7 -0
- chipgraph/adapters/__init__.py +1 -0
- chipgraph/adapters/format/__init__.py +6 -0
- chipgraph/adapters/format/__main__.py +16 -0
- chipgraph/adapters/format/_yaml_lines.py +54 -0
- chipgraph/adapters/format/chip_yaml.py +323 -0
- chipgraph/adapters/format/qsoc_contract.py +246 -0
- chipgraph/adapters/llm/__init__.py +58 -0
- chipgraph/adapters/llm/_budget.py +173 -0
- chipgraph/adapters/llm/_errors.py +50 -0
- chipgraph/adapters/llm/_nda.py +30 -0
- chipgraph/adapters/llm/_retry.py +85 -0
- chipgraph/adapters/llm/anthropic.py +247 -0
- chipgraph/adapters/llm/decide_backend.py +179 -0
- chipgraph/adapters/llm/fake.py +87 -0
- chipgraph/adapters/llm/guard.py +68 -0
- chipgraph/adapters/parser/__init__.py +22 -0
- chipgraph/adapters/parser/cocotb.py +304 -0
- chipgraph/adapters/parser/generic_regex.py +75 -0
- chipgraph/adapters/parser/verible.py +55 -0
- chipgraph/adapters/parser/verilator.py +46 -0
- chipgraph/adapters/review/__init__.py +7 -0
- chipgraph/adapters/review/file.py +123 -0
- chipgraph/adapters/review/github.py +886 -0
- chipgraph/adapters/runner/__init__.py +7 -0
- chipgraph/adapters/runner/local.py +122 -0
- chipgraph/adapters/runtime/__init__.py +5 -0
- chipgraph/adapters/runtime/claude_code/__init__.py +24 -0
- chipgraph/adapters/runtime/claude_code/agents.py +158 -0
- chipgraph/adapters/runtime/claude_code/decisions.py +280 -0
- chipgraph/adapters/runtime/claude_code/guarded.py +310 -0
- chipgraph/adapters/runtime/claude_code/isolation.py +482 -0
- chipgraph/adapters/runtime/claude_code/loop.py +342 -0
- chipgraph/adapters/runtime/claude_code/previous.py +419 -0
- chipgraph/adapters/runtime/claude_code/review.py +492 -0
- chipgraph/adapters/runtime/claude_code/runtime.py +192 -0
- chipgraph/adapters/runtime/claude_code/service.py +2285 -0
- chipgraph/adapters/tool/__init__.py +7 -0
- chipgraph/adapters/tool/cmd.py +229 -0
- chipgraph/adapters/tool/edalize.py +773 -0
- chipgraph/adapters/tool/filelist.py +259 -0
- chipgraph/adapters/tool/pyslang.py +485 -0
- chipgraph/adapters/vcs/__init__.py +1 -0
- chipgraph/adapters/vcs/git.py +521 -0
- chipgraph/app/__init__.py +10 -0
- chipgraph/app/baseline.py +339 -0
- chipgraph/app/build.py +332 -0
- chipgraph/app/checks.py +202 -0
- chipgraph/app/context.py +191 -0
- chipgraph/app/errors.py +11 -0
- chipgraph/app/executors.py +79 -0
- chipgraph/app/findings.py +203 -0
- chipgraph/app/ingest.py +523 -0
- chipgraph/checks/__init__.py +52 -0
- chipgraph/checks/_cdc_analyze.py +272 -0
- chipgraph/checks/_cdc_elaborate.py +455 -0
- chipgraph/checks/_common.py +268 -0
- chipgraph/checks/_cross.py +96 -0
- chipgraph/checks/_model.py +64 -0
- chipgraph/checks/_naming_pyslang.py +235 -0
- chipgraph/checks/_naming_rules.py +127 -0
- chipgraph/checks/_xref_syntax.py +260 -0
- chipgraph/checks/cdc_struct.py +235 -0
- chipgraph/checks/connect.py +353 -0
- chipgraph/checks/cross_chip.py +454 -0
- chipgraph/checks/duplicate.py +153 -0
- chipgraph/checks/filelist.py +323 -0
- chipgraph/checks/format.py +268 -0
- chipgraph/checks/generated.py +151 -0
- chipgraph/checks/hardcode.py +233 -0
- chipgraph/checks/layout.py +155 -0
- chipgraph/checks/naming.py +311 -0
- chipgraph/checks/plan_check.py +901 -0
- chipgraph/checks/ports_diff.py +203 -0
- chipgraph/checks/spec_schema.py +277 -0
- chipgraph/checks/style.py +910 -0
- chipgraph/checks/tb_static.py +599 -0
- chipgraph/checks/trace.py +224 -0
- chipgraph/checks/vendor_guard.py +369 -0
- chipgraph/checks/wrap_check.py +486 -0
- chipgraph/cli/__init__.py +1688 -0
- chipgraph/cli/next_steps.py +137 -0
- chipgraph/core/__init__.py +1 -0
- chipgraph/core/config/__init__.py +93 -0
- chipgraph/core/config/defaults.py +14 -0
- chipgraph/core/config/errors.py +32 -0
- chipgraph/core/config/loader.py +766 -0
- chipgraph/core/config/models.py +845 -0
- chipgraph/core/config/templates.py +340 -0
- chipgraph/core/contracts/__init__.py +75 -0
- chipgraph/core/contracts/_paths.py +26 -0
- chipgraph/core/contracts/agent.py +49 -0
- chipgraph/core/contracts/artifact.py +68 -0
- chipgraph/core/contracts/check.py +76 -0
- chipgraph/core/contracts/event.py +70 -0
- chipgraph/core/contracts/export.py +128 -0
- chipgraph/core/contracts/finding.py +171 -0
- chipgraph/core/contracts/gate.py +50 -0
- chipgraph/core/contracts/rule.py +177 -0
- chipgraph/core/contracts/types.py +45 -0
- chipgraph/core/engine/__init__.py +70 -0
- chipgraph/core/engine/agent_rule.py +1362 -0
- chipgraph/core/engine/baseline.py +283 -0
- chipgraph/core/engine/decide.py +462 -0
- chipgraph/core/engine/fanout.py +1358 -0
- chipgraph/core/engine/gate.py +170 -0
- chipgraph/core/engine/graph.py +647 -0
- chipgraph/core/engine/records.py +60 -0
- chipgraph/core/engine/rules.py +92 -0
- chipgraph/core/engine/scheduler.py +637 -0
- chipgraph/core/engine/select.py +1239 -0
- chipgraph/core/engine/targets.py +342 -0
- chipgraph/core/model/__init__.py +98 -0
- chipgraph/core/model/entities.py +244 -0
- chipgraph/core/model/export.py +16 -0
- chipgraph/core/model/ingest.py +693 -0
- chipgraph/core/model/json_value.py +4 -0
- chipgraph/core/model/keys.py +95 -0
- chipgraph/core/model/model.py +153 -0
- chipgraph/core/model/provenance.py +27 -0
- chipgraph/core/model/query.py +583 -0
- chipgraph/core/model/registry.py +80 -0
- chipgraph/core/model/relations.py +61 -0
- chipgraph/core/model/store.py +279 -0
- chipgraph/core/plugin_api/__init__.py +81 -0
- chipgraph/core/plugin_api/local.py +167 -0
- chipgraph/core/plugin_api/pack.py +159 -0
- chipgraph/core/plugin_api/protocols.py +177 -0
- chipgraph/core/plugin_api/registry.py +111 -0
- chipgraph/core/plugin_api/types.py +94 -0
- chipgraph/core/runtime/__init__.py +41 -0
- chipgraph/core/runtime/executor.py +79 -0
- chipgraph/core/runtime/queue.py +565 -0
- chipgraph/core/runtime/roles/__init__.py +91 -0
- chipgraph/core/runtime/roles/data/author.md +41 -0
- chipgraph/core/runtime/roles/data/critic.md +56 -0
- chipgraph/core/runtime/roles/data/planner.md +38 -0
- chipgraph/core/runtime/roles/data/researcher.md +37 -0
- chipgraph/core/runtime/roles/data/rtl-author.md +64 -0
- chipgraph/core/runtime/roles/data/tb-author.md +48 -0
- chipgraph/core/runtime/roles/data/triage.md +34 -0
- chipgraph/core/runtime/roles/policy.py +299 -0
- chipgraph/core/runtime/roles/registry.py +52 -0
- chipgraph/core/runtime/roles/review.py +275 -0
- chipgraph/core/runtime/roles/schema.py +82 -0
- chipgraph/core/runtime/roles/skills.py +134 -0
- chipgraph/core/runtime/roles/spec.py +232 -0
- chipgraph/core/state/__init__.py +1 -0
- chipgraph/core/state/artifacts.py +156 -0
- chipgraph/core/state/backend.py +117 -0
- chipgraph/core/state/findings.py +151 -0
- chipgraph/core/state/handoff.py +411 -0
- chipgraph/core/state/idempotency.py +59 -0
- chipgraph/core/state/journal.py +238 -0
- chipgraph/core/state/layout.py +82 -0
- chipgraph/core/state/lock.py +195 -0
- chipgraph/core/state/trace.py +225 -0
- chipgraph/learn/__init__.py +96 -0
- chipgraph/learn/draft.py +187 -0
- chipgraph/learn/infer.py +580 -0
- chipgraph/learn/models.py +145 -0
- chipgraph/learn/try_run.py +228 -0
- chipgraph/mcp/__init__.py +12 -0
- chipgraph/mcp/model_tools.py +167 -0
- chipgraph/mcp/server.py +787 -0
- chipgraph/orgs/qnsc/naming-v1.yml +113 -0
- chipgraph/orgs/qnsc/profile.yml +5 -0
- chipgraph/packs/__init__.py +8 -0
- chipgraph/packs/assist/__init__.py +16 -0
- chipgraph/packs/assist/ask/__init__.py +58 -0
- chipgraph/packs/assist/ask/_db.py +57 -0
- chipgraph/packs/assist/ask/_project.py +230 -0
- chipgraph/packs/assist/ask/answer.py +266 -0
- chipgraph/packs/assist/ask/check.py +182 -0
- chipgraph/packs/assist/ask/contract.py +115 -0
- chipgraph/packs/assist/ask/documents.py +157 -0
- chipgraph/packs/assist/ask/fts.py +80 -0
- chipgraph/packs/assist/ask/retrieve.py +331 -0
- chipgraph/packs/assist/audit/__init__.py +383 -0
- chipgraph/packs/assist/pack.yml +13 -0
- chipgraph/packs/assist/triage/__init__.py +64 -0
- chipgraph/packs/assist/triage/contract.py +94 -0
- chipgraph/packs/assist/triage/parse.py +371 -0
- chipgraph/packs/assist/triage/question.py +384 -0
- chipgraph/packs/assist/triage/report.py +160 -0
- chipgraph/packs/assist/triage/rules.py +447 -0
- chipgraph/packs/assist/triage/run.py +224 -0
- chipgraph/packs/digital_rtl/__init__.py +19 -0
- chipgraph/packs/digital_rtl/layout.py +144 -0
- chipgraph/packs/digital_rtl/pack.yml +25 -0
- chipgraph/packs/digital_rtl/plan/__init__.py +34 -0
- chipgraph/packs/digital_rtl/plan/__main__.py +16 -0
- chipgraph/packs/digital_rtl/plan/context.py +82 -0
- chipgraph/packs/digital_rtl/plan/expand.py +94 -0
- chipgraph/packs/digital_rtl/plan/model.py +150 -0
- chipgraph/packs/digital_rtl/plan/resolver.py +400 -0
- chipgraph/packs/digital_rtl/plan/show.py +176 -0
- chipgraph/packs/digital_rtl/pr/__init__.py +11 -0
- chipgraph/packs/digital_rtl/pr/body.py +165 -0
- chipgraph/packs/digital_rtl/pr/gate.py +110 -0
- chipgraph/packs/digital_rtl/pr/open.py +397 -0
- chipgraph/packs/digital_rtl/rtl/__init__.py +9 -0
- chipgraph/packs/digital_rtl/rtl/context.py +272 -0
- chipgraph/packs/digital_rtl/rules/open_pr.yml +30 -0
- chipgraph/packs/digital_rtl/rules/plan.yml +28 -0
- chipgraph/packs/digital_rtl/rules/plan_expand.yml +29 -0
- chipgraph/packs/digital_rtl/rules/pr_gate.yml +27 -0
- chipgraph/packs/digital_rtl/rules/review.yml +31 -0
- chipgraph/packs/digital_rtl/rules/rtl_module.yml +53 -0
- chipgraph/packs/digital_rtl/skills/lang-sv-rtl.md +58 -0
- chipgraph/packs/digital_rtl/skills/org-naming.md +29 -0
- chipgraph/packs/digital_rtl/skills/plan-modules.md +63 -0
- chipgraph/packs/digital_rtl/skills/review-diff.md +41 -0
- chipgraph/packs/digital_rtl/targets/plan.yml +11 -0
- chipgraph/packs/digital_rtl/targets/rtl.yml +20 -0
- chipgraph/packs/digital_rtl/wrapper/__init__.py +24 -0
- chipgraph/packs/digital_rtl/wrapper/common.py +171 -0
- chipgraph/packs/digital_rtl/wrapper/context.py +222 -0
- chipgraph/packs/digital_rtl/wrapper/emacs.py +626 -0
- chipgraph/packs/digital_rtl/wrapper/expansion.py +304 -0
- chipgraph/packs/digital_rtl/wrapper/lisp.py +504 -0
- chipgraph/packs/digital_rtl/wrapper/resolver.py +60 -0
- chipgraph/packs/digital_rtl/wrapper/rules/wrapper.yml +41 -0
- chipgraph/packs/digital_rtl/wrapper/rules/wrapper_gen.yml +35 -0
- chipgraph/packs/digital_rtl/wrapper/skills/wrapper-auto.md +120 -0
- chipgraph/packs/dv/__init__.py +15 -0
- chipgraph/packs/dv/declaration.py +392 -0
- chipgraph/packs/dv/interface.py +596 -0
- chipgraph/packs/dv/pack.yml +14 -0
- chipgraph/packs/dv/rules/tb_module.yml +42 -0
- chipgraph/packs/dv/skills/dv/cocotb.md +72 -0
- chipgraph/packs/spec_core/__init__.py +9 -0
- chipgraph/packs/spec_core/extract/__init__.py +12 -0
- chipgraph/packs/spec_core/extract/_markdown.py +371 -0
- chipgraph/packs/spec_core/extract/_text.py +116 -0
- chipgraph/packs/spec_core/extract/mas.py +975 -0
- chipgraph/packs/spec_core/gen/__init__.py +11 -0
- chipgraph/packs/spec_core/gen/agents_md/__init__.py +148 -0
- chipgraph/packs/spec_core/gen/agents_md/_context.py +239 -0
- chipgraph/packs/spec_core/gen/agents_md/_markers.py +131 -0
- chipgraph/packs/spec_core/gen/agents_md/agents_md.md.j2 +178 -0
- chipgraph/packs/spec_core/gen/diagram/__init__.py +110 -0
- chipgraph/packs/spec_core/gen/diagram/__main__.py +44 -0
- chipgraph/packs/spec_core/gen/diagram/_diagrams.py +403 -0
- chipgraph/packs/spec_core/gen/diagram/_scene.py +163 -0
- chipgraph/packs/spec_core/pack.yml +6 -0
- chipgraph/py.typed +0 -0
- chipgraph-0.1.0.dist-info/METADATA +53 -0
- chipgraph-0.1.0.dist-info/RECORD +252 -0
- chipgraph-0.1.0.dist-info/WHEEL +4 -0
- chipgraph-0.1.0.dist-info/entry_points.txt +61 -0
- chipgraph-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""`GeneratedCheck`: generated files are not edited by hand.
|
|
2
|
+
|
|
3
|
+
Also exports `stamp`/`verify`, the two functions generators use to mark a file as
|
|
4
|
+
generated and later confirm it has not been hand-edited. See DESIGN.md 7.3.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import re
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
from chipgraph.checks._common import (
|
|
14
|
+
ArgError,
|
|
15
|
+
compute_idempotency_key,
|
|
16
|
+
error_result,
|
|
17
|
+
glob_to_regex,
|
|
18
|
+
iter_repo_files,
|
|
19
|
+
optional_bool,
|
|
20
|
+
require_list_of_str,
|
|
21
|
+
sha256_file,
|
|
22
|
+
)
|
|
23
|
+
from chipgraph.core.contracts import CheckResult, CheckSpec, Issue
|
|
24
|
+
from chipgraph.core.contracts.types import CheckStatus
|
|
25
|
+
from chipgraph.core.plugin_api.types import ToolContext
|
|
26
|
+
|
|
27
|
+
_MARKER_SEARCH_LINES = 20
|
|
28
|
+
_COMMENT_CLOSERS = {"<!--": " -->"}
|
|
29
|
+
|
|
30
|
+
# A marker line: an optional comment prefix, then `chipgraph:generated sha256=<64 hex>`,
|
|
31
|
+
# then an optional closer (for `<!--`-style comments).
|
|
32
|
+
_MARKER_RE = re.compile(
|
|
33
|
+
r"^\s*(?://|#|--|;|<!--)?\s*chipgraph:generated sha256=([0-9a-f]{64})\s*(?:-->)?\s*$"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def stamp(text: str, comment_prefix: str = "//") -> str:
|
|
38
|
+
"""Insert or replace the generation marker as the first line of `text`.
|
|
39
|
+
|
|
40
|
+
The marker's hash covers `text` with any existing marker's first line removed, so
|
|
41
|
+
calling `stamp` again on already-stamped, unedited text is a no-op (byte for byte).
|
|
42
|
+
"""
|
|
43
|
+
lines = text.splitlines(keepends=True)
|
|
44
|
+
has_marker = bool(lines) and _MARKER_RE.match(_strip_eol(lines[0])) is not None
|
|
45
|
+
base_text = "".join(lines[1:]) if has_marker else text
|
|
46
|
+
|
|
47
|
+
digest = hashlib.sha256(base_text.encode("utf-8")).hexdigest()
|
|
48
|
+
closer = _COMMENT_CLOSERS.get(comment_prefix, "")
|
|
49
|
+
eol = _eol_of(lines[0]) if lines else "\n"
|
|
50
|
+
marker_line = f"{comment_prefix} chipgraph:generated sha256={digest}{closer}{eol}"
|
|
51
|
+
return marker_line + base_text
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def verify(text: str) -> bool | None:
|
|
55
|
+
"""Check `text`'s generation marker against its content.
|
|
56
|
+
|
|
57
|
+
Returns `True` if the marker matches, `False` if it does not (hand-edited), or
|
|
58
|
+
`None` if no marker is found in the first 20 lines.
|
|
59
|
+
"""
|
|
60
|
+
lines = text.splitlines(keepends=True)
|
|
61
|
+
limit = min(len(lines), _MARKER_SEARCH_LINES)
|
|
62
|
+
for i in range(limit):
|
|
63
|
+
m = _MARKER_RE.match(_strip_eol(lines[i]))
|
|
64
|
+
if m is None:
|
|
65
|
+
continue
|
|
66
|
+
stored = m.group(1)
|
|
67
|
+
remainder = "".join(lines[:i] + lines[i + 1 :])
|
|
68
|
+
actual = hashlib.sha256(remainder.encode("utf-8")).hexdigest()
|
|
69
|
+
return actual == stored
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _strip_eol(line: str) -> str:
|
|
74
|
+
return line.rstrip("\n").rstrip("\r")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _eol_of(line: str) -> str:
|
|
78
|
+
if line.endswith("\r\n"):
|
|
79
|
+
return "\r\n"
|
|
80
|
+
if line.endswith("\n"):
|
|
81
|
+
return "\n"
|
|
82
|
+
return "\n"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class GeneratedCheck:
|
|
86
|
+
"""Checks that generated files still match the marker hash they were stamped with."""
|
|
87
|
+
|
|
88
|
+
id = "generated"
|
|
89
|
+
name = "Generated"
|
|
90
|
+
|
|
91
|
+
async def run(self, spec: CheckSpec, ctx: ToolContext) -> CheckResult:
|
|
92
|
+
start = time.monotonic()
|
|
93
|
+
args = spec.args
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
globs = require_list_of_str(args, "files")
|
|
97
|
+
require_marker = optional_bool(args, "require_marker", False)
|
|
98
|
+
regexes = [glob_to_regex(g) for g in globs]
|
|
99
|
+
except ArgError as exc:
|
|
100
|
+
return error_result(spec, str(exc), start)
|
|
101
|
+
|
|
102
|
+
all_files = iter_repo_files(ctx.repo_root)
|
|
103
|
+
matched = [f for f in all_files if any(r.fullmatch(f) for r in regexes)]
|
|
104
|
+
|
|
105
|
+
issues: list[Issue] = []
|
|
106
|
+
read_files: dict[str, str] = {}
|
|
107
|
+
for f in matched:
|
|
108
|
+
full_path = ctx.repo_root / f
|
|
109
|
+
read_files[f] = sha256_file(full_path)
|
|
110
|
+
try:
|
|
111
|
+
text = full_path.read_text()
|
|
112
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
113
|
+
issues.append(
|
|
114
|
+
Issue(
|
|
115
|
+
file=f,
|
|
116
|
+
rule="generated",
|
|
117
|
+
severity="error",
|
|
118
|
+
msg=f"could not read `{f}`: {exc}",
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
result = verify(text)
|
|
124
|
+
if result is False:
|
|
125
|
+
issues.append(
|
|
126
|
+
Issue(
|
|
127
|
+
file=f,
|
|
128
|
+
rule="generated",
|
|
129
|
+
severity="error",
|
|
130
|
+
msg=f"`{f}` was edited by hand after it was generated",
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
elif result is None:
|
|
134
|
+
issues.append(
|
|
135
|
+
Issue(
|
|
136
|
+
file=f,
|
|
137
|
+
rule="generated",
|
|
138
|
+
severity="error" if require_marker else "warning",
|
|
139
|
+
msg=f"`{f}` has no generation marker",
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
key = compute_idempotency_key(spec.id, args, tuple(read_files.items()))
|
|
144
|
+
status: CheckStatus = "fail" if any(i.severity == "error" for i in issues) else "pass"
|
|
145
|
+
return CheckResult(
|
|
146
|
+
check_id=spec.id,
|
|
147
|
+
status=status,
|
|
148
|
+
issues=tuple(issues),
|
|
149
|
+
duration_s=time.monotonic() - start,
|
|
150
|
+
idempotency_key=key,
|
|
151
|
+
)
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""`HardcodeCheck`: numbers the contract already names must not be typed by hand in RTL.
|
|
2
|
+
|
|
3
|
+
A layer-1 (structure) cross check (DESIGN.md 7.3). It reads the Design Model
|
|
4
|
+
`chipgraph ingest` wrote for the shared numbers a design must not duplicate, scans the
|
|
5
|
+
integer literals of the RTL files in scope (via the pyslang token stream, so numbers
|
|
6
|
+
inside comments and strings are never seen; sized literals like `32'h8000_4000` are
|
|
7
|
+
decoded), and reports each literal that equals a shared number.
|
|
8
|
+
|
|
9
|
+
Shared numbers come from the model:
|
|
10
|
+
|
|
11
|
+
* every memory region's base, size and end address (`base + size`) whose value is at
|
|
12
|
+
least arg `min_value` (default `0x100`); and
|
|
13
|
+
* every interrupt line number, but only counted when the literal is used as an index on
|
|
14
|
+
the arg `irq_vector` net (small line numbers are otherwise indistinguishable from
|
|
15
|
+
ordinary constants, so they are not flagged elsewhere).
|
|
16
|
+
|
|
17
|
+
A `hardcode.literal` issue is *not* raised when the literal's line carries the arg
|
|
18
|
+
`allow_comment` marker (default `contract:`, matching QSoC's `// contract: <key>` tag) or
|
|
19
|
+
when the file is one of the generated packages listed in arg `generated`.
|
|
20
|
+
|
|
21
|
+
A missing model is a whole-check `error` (run `chipgraph ingest` first).
|
|
22
|
+
|
|
23
|
+
Example profile usage (`.chipgraph.yml`):
|
|
24
|
+
|
|
25
|
+
```yaml
|
|
26
|
+
adapters:
|
|
27
|
+
hardcode:
|
|
28
|
+
use: hardcode
|
|
29
|
+
scope: ["design/**/rtl/**/*.sv"]
|
|
30
|
+
min_value: 0x100
|
|
31
|
+
generated: ["design/common/rtl/qnsc_pkg.sv"]
|
|
32
|
+
```
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import re
|
|
38
|
+
import time
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
|
|
41
|
+
from chipgraph.checks._common import (
|
|
42
|
+
ArgError,
|
|
43
|
+
compute_idempotency_key,
|
|
44
|
+
error_result,
|
|
45
|
+
glob_to_regex,
|
|
46
|
+
iter_repo_files,
|
|
47
|
+
optional_list_of_str,
|
|
48
|
+
optional_str,
|
|
49
|
+
require_list_of_str,
|
|
50
|
+
sha256_file,
|
|
51
|
+
)
|
|
52
|
+
from chipgraph.checks._model import ModelUnavailable, load_model
|
|
53
|
+
from chipgraph.checks._xref_syntax import parse_rtl
|
|
54
|
+
from chipgraph.core.contracts import CheckResult, CheckSpec, Issue
|
|
55
|
+
from chipgraph.core.contracts.types import CheckStatus
|
|
56
|
+
from chipgraph.core.model.model import DesignModel
|
|
57
|
+
from chipgraph.core.plugin_api.types import ToolContext
|
|
58
|
+
|
|
59
|
+
_DEFAULT_MIN_VALUE = 0x100
|
|
60
|
+
_DEFAULT_ALLOW_COMMENT = "contract:"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class HardcodeCheck:
|
|
64
|
+
"""Checks that no RTL literal duplicates a memory-map or interrupt number."""
|
|
65
|
+
|
|
66
|
+
id = "hardcode"
|
|
67
|
+
name = "Hardcode"
|
|
68
|
+
per_block = False # chip-wide: `chipgraph check` runs it once, not per block
|
|
69
|
+
|
|
70
|
+
async def run(self, spec: CheckSpec, ctx: ToolContext) -> CheckResult:
|
|
71
|
+
start = time.monotonic()
|
|
72
|
+
args = spec.args
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
scope = require_list_of_str(args, "scope")
|
|
76
|
+
if not scope:
|
|
77
|
+
raise ArgError("args['scope'] must list at least one glob")
|
|
78
|
+
min_value = _int_arg(args, "min_value", _DEFAULT_MIN_VALUE)
|
|
79
|
+
allow_comment = optional_str(args, "allow_comment", _DEFAULT_ALLOW_COMMENT)
|
|
80
|
+
generated = set(optional_list_of_str(args, "generated"))
|
|
81
|
+
ignore = optional_list_of_str(args, "ignore")
|
|
82
|
+
irq_vector = optional_str(args, "irq_vector", "")
|
|
83
|
+
except ArgError as exc:
|
|
84
|
+
return error_result(spec, str(exc), start)
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
loaded = load_model(ctx.repo_root)
|
|
88
|
+
except ModelUnavailable as exc:
|
|
89
|
+
return error_result(spec, str(exc), start, rule="model")
|
|
90
|
+
|
|
91
|
+
shared = _shared_numbers(loaded.model, min_value)
|
|
92
|
+
irq_lines = _interrupt_lines(loaded.model)
|
|
93
|
+
|
|
94
|
+
scope_regexes = [glob_to_regex(p) for p in scope]
|
|
95
|
+
ignore_regexes = [glob_to_regex(p) for p in ignore]
|
|
96
|
+
in_scope = [
|
|
97
|
+
f
|
|
98
|
+
for f in iter_repo_files(ctx.repo_root)
|
|
99
|
+
if any(r.fullmatch(f) for r in scope_regexes)
|
|
100
|
+
and not any(r.fullmatch(f) for r in ignore_regexes)
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
issues: list[Issue] = []
|
|
104
|
+
for rel in in_scope:
|
|
105
|
+
if rel in generated:
|
|
106
|
+
continue
|
|
107
|
+
issues.extend(
|
|
108
|
+
_file_issues(ctx.repo_root / rel, rel, shared, irq_lines, allow_comment, irq_vector)
|
|
109
|
+
)
|
|
110
|
+
issues.sort(key=lambda i: (i.file or "", i.line or 0, i.rule, i.msg))
|
|
111
|
+
|
|
112
|
+
file_hashes = [(f, sha256_file(ctx.repo_root / f)) for f in in_scope]
|
|
113
|
+
file_hashes.append(("<model>", loaded.build_inputs_hash or "none"))
|
|
114
|
+
key = compute_idempotency_key(spec.id, args, file_hashes)
|
|
115
|
+
status: CheckStatus = "fail" if any(i.severity == "error" for i in issues) else "pass"
|
|
116
|
+
return CheckResult(
|
|
117
|
+
check_id=spec.id,
|
|
118
|
+
status=status,
|
|
119
|
+
issues=tuple(issues),
|
|
120
|
+
duration_s=time.monotonic() - start,
|
|
121
|
+
idempotency_key=key,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _int_arg(args: object, key: str, default: int) -> int:
|
|
126
|
+
from collections.abc import Mapping
|
|
127
|
+
|
|
128
|
+
if not isinstance(args, Mapping) or key not in args:
|
|
129
|
+
return default
|
|
130
|
+
val = args[key]
|
|
131
|
+
if isinstance(val, bool) or not isinstance(val, int):
|
|
132
|
+
raise ArgError(f"args[{key!r}] must be an integer")
|
|
133
|
+
return val
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _shared_numbers(model: DesignModel, min_value: int) -> dict[int, list[str]]:
|
|
137
|
+
"""Map each shared memory-map number (>= `min_value`) to the model keys naming it."""
|
|
138
|
+
out: dict[int, list[str]] = {}
|
|
139
|
+
|
|
140
|
+
def add(value: object, label: str) -> None:
|
|
141
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
142
|
+
return
|
|
143
|
+
if value < min_value:
|
|
144
|
+
return
|
|
145
|
+
out.setdefault(value, [])
|
|
146
|
+
if label not in out[value]:
|
|
147
|
+
out[value].append(label)
|
|
148
|
+
|
|
149
|
+
for region in model.by_kind("memory_region"):
|
|
150
|
+
base = getattr(region, "base", None)
|
|
151
|
+
size = getattr(region, "size", None)
|
|
152
|
+
add(base, f"{region.key}.base")
|
|
153
|
+
add(size, f"{region.key}.size")
|
|
154
|
+
if isinstance(base, int) and isinstance(size, int):
|
|
155
|
+
add(base + size, f"{region.key}.end")
|
|
156
|
+
return out
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _interrupt_lines(model: DesignModel) -> dict[int, list[str]]:
|
|
160
|
+
"""Map each interrupt line number to the interrupt keys that use it."""
|
|
161
|
+
out: dict[int, list[str]] = {}
|
|
162
|
+
for irq in model.by_kind("interrupt"):
|
|
163
|
+
line = getattr(irq, "line", None)
|
|
164
|
+
if isinstance(line, int) and not isinstance(line, bool):
|
|
165
|
+
out.setdefault(line, [])
|
|
166
|
+
if irq.key not in out[line]:
|
|
167
|
+
out[line].append(irq.key)
|
|
168
|
+
return out
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _file_issues(
|
|
172
|
+
path: Path,
|
|
173
|
+
rel: str,
|
|
174
|
+
shared: dict[int, list[str]],
|
|
175
|
+
irq_lines: dict[int, list[str]],
|
|
176
|
+
allow_comment: str,
|
|
177
|
+
irq_vector: str,
|
|
178
|
+
) -> list[Issue]:
|
|
179
|
+
parsed = parse_rtl(path)
|
|
180
|
+
issues: list[Issue] = [
|
|
181
|
+
Issue(
|
|
182
|
+
file=rel, line=pf.line, rule="parse", severity="error", msg=f"parse error: {pf.message}"
|
|
183
|
+
)
|
|
184
|
+
for pf in parsed.parse_errors
|
|
185
|
+
]
|
|
186
|
+
text_lines = path.read_text(errors="ignore").splitlines()
|
|
187
|
+
index_re = re.compile(rf"{re.escape(irq_vector)}\s*\[\s*(\d+)\s*\]") if irq_vector else None
|
|
188
|
+
|
|
189
|
+
for lit in parsed.literals:
|
|
190
|
+
if _has_marker(parsed.line_comments.get(lit.line, ""), allow_comment):
|
|
191
|
+
continue
|
|
192
|
+
keys = shared.get(lit.value)
|
|
193
|
+
if keys:
|
|
194
|
+
issues.append(
|
|
195
|
+
Issue(
|
|
196
|
+
file=rel,
|
|
197
|
+
line=lit.line,
|
|
198
|
+
rule="hardcode.literal",
|
|
199
|
+
severity="error",
|
|
200
|
+
msg=(
|
|
201
|
+
f"literal {lit.raw!r} (= {lit.value}) duplicates the contract number "
|
|
202
|
+
f"{', '.join(sorted(keys))}; take it from the contract, "
|
|
203
|
+
f"or tag the line `// {allow_comment} <key>`"
|
|
204
|
+
),
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
continue
|
|
208
|
+
# An interrupt line number only counts as a shared number when used as an index
|
|
209
|
+
# on the interrupt vector net.
|
|
210
|
+
if index_re is not None and lit.value in irq_lines:
|
|
211
|
+
line_text = text_lines[lit.line - 1] if 0 < lit.line <= len(text_lines) else ""
|
|
212
|
+
if any(int(m.group(1)) == lit.value for m in index_re.finditer(line_text)):
|
|
213
|
+
keys = irq_lines[lit.value]
|
|
214
|
+
issues.append(
|
|
215
|
+
Issue(
|
|
216
|
+
file=rel,
|
|
217
|
+
line=lit.line,
|
|
218
|
+
rule="hardcode.literal",
|
|
219
|
+
severity="error",
|
|
220
|
+
msg=(
|
|
221
|
+
f"literal {lit.raw!r} indexes {irq_vector!r} at bit {lit.value}, the "
|
|
222
|
+
f"contract line of {', '.join(sorted(keys))}; take it from the contract"
|
|
223
|
+
),
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
return issues
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _has_marker(comment: str, allow_comment: str) -> bool:
|
|
230
|
+
return allow_comment in comment
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
__all__ = ["HardcodeCheck"]
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""`LayoutCheck`: every file in scope lives where the project's layout templates say.
|
|
2
|
+
|
|
3
|
+
See DESIGN.md 7.3 for the `layout` check and its `.chipgraph.yml` example.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from chipgraph.checks._common import (
|
|
12
|
+
ArgError,
|
|
13
|
+
CompiledTemplate,
|
|
14
|
+
compile_template,
|
|
15
|
+
compute_idempotency_key,
|
|
16
|
+
error_result,
|
|
17
|
+
glob_to_regex,
|
|
18
|
+
iter_repo_files,
|
|
19
|
+
require_dict,
|
|
20
|
+
require_list_of_str,
|
|
21
|
+
sha256_file,
|
|
22
|
+
str_or_list_of_str,
|
|
23
|
+
)
|
|
24
|
+
from chipgraph.core.contracts import CheckResult, CheckSpec, Issue
|
|
25
|
+
from chipgraph.core.contracts.types import CheckStatus
|
|
26
|
+
from chipgraph.core.plugin_api.types import ToolContext
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class LayoutCheck:
|
|
30
|
+
"""Checks that every in-scope file matches at least one declared layout template."""
|
|
31
|
+
|
|
32
|
+
id = "layout"
|
|
33
|
+
name = "Layout"
|
|
34
|
+
|
|
35
|
+
async def run(self, spec: CheckSpec, ctx: ToolContext) -> CheckResult:
|
|
36
|
+
start = time.monotonic()
|
|
37
|
+
args = spec.args
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
templates_by_kind = _flatten_templates(require_dict(args, "templates"))
|
|
41
|
+
scope = require_list_of_str(args, "scope")
|
|
42
|
+
ignore = args.get("ignore", [])
|
|
43
|
+
if not isinstance(ignore, list) or not all(isinstance(v, str) for v in ignore):
|
|
44
|
+
raise ArgError("args['ignore'] must be a list of strings")
|
|
45
|
+
values = _parse_values(args.get("values", {}))
|
|
46
|
+
_check_no_duplicate_templates(templates_by_kind)
|
|
47
|
+
compiled = [(kind, tmpl, compile_template(tmpl)) for kind, tmpl in templates_by_kind]
|
|
48
|
+
except ArgError as exc:
|
|
49
|
+
return error_result(spec, str(exc), start)
|
|
50
|
+
|
|
51
|
+
scope_regexes = [glob_to_regex(p) for p in scope]
|
|
52
|
+
ignore_regexes = [glob_to_regex(p) for p in ignore]
|
|
53
|
+
|
|
54
|
+
all_files = iter_repo_files(ctx.repo_root)
|
|
55
|
+
in_scope = [
|
|
56
|
+
f
|
|
57
|
+
for f in all_files
|
|
58
|
+
if any(r.fullmatch(f) for r in scope_regexes)
|
|
59
|
+
and not any(r.fullmatch(f) for r in ignore_regexes)
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
issues: list[Issue] = []
|
|
63
|
+
for path in in_scope:
|
|
64
|
+
issues.extend(_check_file(path, compiled, values))
|
|
65
|
+
|
|
66
|
+
file_hashes = tuple((f, sha256_file(ctx.repo_root / f)) for f in in_scope)
|
|
67
|
+
key = compute_idempotency_key(spec.id, args, file_hashes)
|
|
68
|
+
status: CheckStatus = "fail" if any(i.severity == "error" for i in issues) else "pass"
|
|
69
|
+
return CheckResult(
|
|
70
|
+
check_id=spec.id,
|
|
71
|
+
status=status,
|
|
72
|
+
issues=tuple(issues),
|
|
73
|
+
duration_s=time.monotonic() - start,
|
|
74
|
+
idempotency_key=key,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _flatten_templates(templates: dict[str, Any]) -> list[tuple[str, str]]:
|
|
79
|
+
out: list[tuple[str, str]] = []
|
|
80
|
+
for kind, val in templates.items():
|
|
81
|
+
for tmpl in str_or_list_of_str(val, f"args['templates'][{kind!r}]"):
|
|
82
|
+
out.append((kind, tmpl))
|
|
83
|
+
if not out:
|
|
84
|
+
raise ArgError("args['templates'] must declare at least one template")
|
|
85
|
+
return out
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _check_no_duplicate_templates(templates_by_kind: list[tuple[str, str]]) -> None:
|
|
89
|
+
seen: dict[str, str] = {}
|
|
90
|
+
for kind, tmpl in templates_by_kind:
|
|
91
|
+
if tmpl in seen:
|
|
92
|
+
raise ArgError(f"templates {seen[tmpl]!r} and {kind!r} are identical: {tmpl!r}")
|
|
93
|
+
seen[tmpl] = kind
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _parse_values(raw: Any) -> dict[str, list[str]]:
|
|
97
|
+
if not isinstance(raw, dict):
|
|
98
|
+
raise ArgError("args['values'] must be a mapping")
|
|
99
|
+
out: dict[str, list[str]] = {}
|
|
100
|
+
for name, allowed in raw.items():
|
|
101
|
+
out[name] = str_or_list_of_str(allowed, f"args['values'][{name!r}]")
|
|
102
|
+
return out
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _check_file(
|
|
106
|
+
path: str,
|
|
107
|
+
compiled: list[tuple[str, str, CompiledTemplate]],
|
|
108
|
+
values: dict[str, list[str]],
|
|
109
|
+
) -> list[Issue]:
|
|
110
|
+
matches: list[tuple[str, dict[str, str]]] = []
|
|
111
|
+
for kind, _tmpl, compiled_tmpl in compiled:
|
|
112
|
+
groups = compiled_tmpl.match(path)
|
|
113
|
+
if groups is not None:
|
|
114
|
+
matches.append((kind, groups))
|
|
115
|
+
|
|
116
|
+
if not matches:
|
|
117
|
+
return [
|
|
118
|
+
Issue(
|
|
119
|
+
file=path,
|
|
120
|
+
rule="layout",
|
|
121
|
+
severity="error",
|
|
122
|
+
msg=f"`{path}` is not in any layout template",
|
|
123
|
+
)
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
# A file may syntactically match more than one template (e.g. an overly broad and a
|
|
127
|
+
# more specific one). It passes if at least one match has only allowed placeholder
|
|
128
|
+
# values; otherwise we report the value violations of the first match.
|
|
129
|
+
first_violations: list[Issue] | None = None
|
|
130
|
+
for _kind, groups in matches:
|
|
131
|
+
violations = _value_violations(path, groups, values)
|
|
132
|
+
if not violations:
|
|
133
|
+
return []
|
|
134
|
+
if first_violations is None:
|
|
135
|
+
first_violations = violations
|
|
136
|
+
assert first_violations is not None
|
|
137
|
+
return first_violations
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _value_violations(
|
|
141
|
+
path: str, groups: dict[str, str], values: dict[str, list[str]]
|
|
142
|
+
) -> list[Issue]:
|
|
143
|
+
issues: list[Issue] = []
|
|
144
|
+
for name, val in groups.items():
|
|
145
|
+
allowed = values.get(name)
|
|
146
|
+
if allowed is not None and val not in allowed:
|
|
147
|
+
issues.append(
|
|
148
|
+
Issue(
|
|
149
|
+
file=path,
|
|
150
|
+
rule="layout",
|
|
151
|
+
severity="error",
|
|
152
|
+
msg=f"{name} `{val}` is not one of: {', '.join(allowed)}",
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
return issues
|