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,486 @@
|
|
|
1
|
+
"""`WrapCheck` (`wrap_check`): a wrapper's generated file is what its source expands to.
|
|
2
|
+
|
|
3
|
+
The check of the `digital-rtl/wrapper` rules (task M2-12), and the `chipgraph check`
|
|
4
|
+
equivalent of QSoC's `make wrap-check`. For each wrapper (the task's, when the params
|
|
5
|
+
name one; else every wrapper of the params' block, or of the project) it
|
|
6
|
+
|
|
7
|
+
1. refuses Lisp the source may not carry (`wrapper.lisp`), and reads the IP module's
|
|
8
|
+
port declaration from the wrapper's `vendor_files` (pyslang, header only);
|
|
9
|
+
2. regenerates the expansion with emacs, in a scratch copy (`wrapper.emacs.expand`);
|
|
10
|
+
3. checks the expanded instance against the declaration (`wrapper.expansion.validate`):
|
|
11
|
+
an `/*AUTOINST*/` that expanded to nothing, an IP port no template line maps (left
|
|
12
|
+
unconnected to any project signal), an open input, a port the IP does not declare,
|
|
13
|
+
an IP port not connected. These issues point at the source, and quote the expanded
|
|
14
|
+
connection: the author cannot run emacs, so this text is its redo;
|
|
15
|
+
4. compares the regeneration with the committed generated file: each differing hunk is
|
|
16
|
+
an issue at the committed file's line, with both texts and "run the generator".
|
|
17
|
+
A difference in whitespace only is a warning (verilog-mode's column alignment can
|
|
18
|
+
differ between emacs versions; QSoC's check ignores whitespace too).
|
|
19
|
+
|
|
20
|
+
When it compares (arg `compare`): `always`; `never`; `auto` (default): over a block or
|
|
21
|
+
the project, always; for a task (a `wrapper` param), only when the generated file was
|
|
22
|
+
generated from the current source (its header's source sha256): the author's check runs
|
|
23
|
+
before the generator, so a missing or older file is not the author's fault.
|
|
24
|
+
|
|
25
|
+
Without emacs (or without verilog-mode) the check cannot regenerate. With arg
|
|
26
|
+
`require_emacs: true` that is an `error`; `require_emacs` defaults to true when the
|
|
27
|
+
environment variable `CI` is set (to anything but empty, `0`, `false` or `no`), so a
|
|
28
|
+
CI run never passes on the weaker fallback unless the profile says
|
|
29
|
+
`require_emacs: false`. Otherwise (outside CI, so a machine without emacs can still run
|
|
30
|
+
`chipgraph check`), when the committed file's `chipgraph:generated` marker matches its
|
|
31
|
+
content and its header names the current source's sha256, the committed expansion is
|
|
32
|
+
validated as in 3 and the check passes with a warning that the regeneration was not
|
|
33
|
+
verified; anything else is an `error` with the install hint.
|
|
34
|
+
|
|
35
|
+
The fallback is weaker than a regeneration, by design: the marker is a plain sha256 of
|
|
36
|
+
the file's content, not a MAC or a signature, so anyone who edits the generated body and
|
|
37
|
+
re-renders the marker and header passes it (with the `wrap.not_regenerated` warning).
|
|
38
|
+
Only a regeneration with emacs catches that edit (`wrap.differs`).
|
|
39
|
+
|
|
40
|
+
Args (all optional): `src`, `out` (path templates with `{block}`, `{wrapper}`; default
|
|
41
|
+
`rtl/emacs/{wrapper}.src.sv`, `rtl/{wrapper}.sv`, as the rules; refused when they
|
|
42
|
+
resolve outside the repository, or under `.git/` or `.chipgraph/`), `compare`,
|
|
43
|
+
`require_emacs`, `emacs` (the executable: a trusted profile value, since the check runs
|
|
44
|
+
it; a bare name is looked up on `PATH`, a value with a path separator must be an
|
|
45
|
+
absolute path to an executable file), `timeout_s` (a number of seconds,
|
|
46
|
+
`0 < timeout_s <= 600`; default 30). A source or generated file that cannot be read,
|
|
47
|
+
or is not UTF-8, is a `wrap.read` issue.
|
|
48
|
+
|
|
49
|
+
Profile example (`.chipgraph.yml`)::
|
|
50
|
+
|
|
51
|
+
adapters:
|
|
52
|
+
wrap_check: { use: wrap_check }
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
from __future__ import annotations
|
|
56
|
+
|
|
57
|
+
import difflib
|
|
58
|
+
import math
|
|
59
|
+
import os
|
|
60
|
+
import re
|
|
61
|
+
import time
|
|
62
|
+
from collections.abc import Mapping, Sequence
|
|
63
|
+
from dataclasses import dataclass, field
|
|
64
|
+
from pathlib import Path
|
|
65
|
+
from typing import Any
|
|
66
|
+
|
|
67
|
+
from chipgraph.checks._common import compute_idempotency_key, sha256_file
|
|
68
|
+
from chipgraph.checks.generated import verify
|
|
69
|
+
from chipgraph.core.config.errors import ConfigError
|
|
70
|
+
from chipgraph.core.config.loader import load as load_profile
|
|
71
|
+
from chipgraph.core.contracts import CheckResult, CheckSpec, Issue
|
|
72
|
+
from chipgraph.core.contracts.types import CheckStatus
|
|
73
|
+
from chipgraph.core.plugin_api.types import ToolContext
|
|
74
|
+
from chipgraph.packs.digital_rtl.wrapper import emacs as gen
|
|
75
|
+
from chipgraph.packs.digital_rtl.wrapper import lisp
|
|
76
|
+
from chipgraph.packs.digital_rtl.wrapper.common import (
|
|
77
|
+
GEN_RULE,
|
|
78
|
+
Wrapper,
|
|
79
|
+
WrapperError,
|
|
80
|
+
all_wrappers,
|
|
81
|
+
find_wrapper,
|
|
82
|
+
inside,
|
|
83
|
+
instance_id,
|
|
84
|
+
paths,
|
|
85
|
+
)
|
|
86
|
+
from chipgraph.packs.digital_rtl.wrapper.expansion import instances, validate
|
|
87
|
+
from chipgraph.packs.dv.declaration import extract_declaration
|
|
88
|
+
|
|
89
|
+
COMPARE_MODES = ("auto", "always", "never")
|
|
90
|
+
MAX_HUNKS = 20
|
|
91
|
+
"""Most differing hunks reported per file."""
|
|
92
|
+
MAX_HUNK_LINES = 12
|
|
93
|
+
"""Most lines of each side shown per hunk."""
|
|
94
|
+
_MARKER_RE = re.compile(r"^\s*//\s*chipgraph:generated sha256=[0-9a-f]{64}\s*$")
|
|
95
|
+
_NO_EMACS = ("wrap.emacs_missing", "wrap.verilog_mode_missing")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class _Outcome:
|
|
100
|
+
issues: list[Issue] = field(default_factory=list)
|
|
101
|
+
files: list[tuple[str, str]] = field(default_factory=list)
|
|
102
|
+
infra: bool = False
|
|
103
|
+
log: str = ""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _without_marker(text: str) -> list[str]:
|
|
107
|
+
lines = text.splitlines()
|
|
108
|
+
return lines[1:] if lines and _MARKER_RE.match(lines[0]) else lines
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _squash(lines: Sequence[str]) -> str:
|
|
112
|
+
return re.sub(r"\s+", "", "\n".join(lines))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _shown(lines: Sequence[str], sign: str) -> str:
|
|
116
|
+
head = [f"{sign} {line}" for line in lines[:MAX_HUNK_LINES]]
|
|
117
|
+
if len(lines) > MAX_HUNK_LINES:
|
|
118
|
+
head.append(f"{sign} ... ({len(lines) - MAX_HUNK_LINES} more lines)")
|
|
119
|
+
return "\n".join(head)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def compare(committed: str, generated: str, out_rel: str, rerun: str) -> list[Issue]:
|
|
123
|
+
"""The hunks where `committed` differs from `generated` (see the module docstring)."""
|
|
124
|
+
old, new = _without_marker(committed), _without_marker(generated)
|
|
125
|
+
if old == new:
|
|
126
|
+
return []
|
|
127
|
+
if _squash(old) == _squash(new):
|
|
128
|
+
return [
|
|
129
|
+
Issue(
|
|
130
|
+
file=out_rel,
|
|
131
|
+
severity="warning",
|
|
132
|
+
rule="wrap.whitespace",
|
|
133
|
+
msg=(
|
|
134
|
+
f"{out_rel} differs from the generator's output in whitespace only "
|
|
135
|
+
f"(another emacs or verilog-mode version?); {rerun} to refresh it"
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
]
|
|
139
|
+
edited = verify(committed) is False
|
|
140
|
+
note = (
|
|
141
|
+
" The file was edited after it was generated (its chipgraph:generated marker no "
|
|
142
|
+
"longer matches): never edit a generated wrapper."
|
|
143
|
+
if edited
|
|
144
|
+
else ""
|
|
145
|
+
)
|
|
146
|
+
key_old = [" ".join(line.split()) for line in old]
|
|
147
|
+
key_new = [" ".join(line.split()) for line in new]
|
|
148
|
+
offset = len(committed.splitlines()) - len(old) # the marker line, when there is one
|
|
149
|
+
issues: list[Issue] = []
|
|
150
|
+
matcher = difflib.SequenceMatcher(a=key_old, b=key_new, autojunk=False)
|
|
151
|
+
hunks = [op for op in matcher.get_opcodes() if op[0] != "equal"]
|
|
152
|
+
for tag, i1, i2, j1, j2 in hunks[:MAX_HUNKS]:
|
|
153
|
+
parts = []
|
|
154
|
+
if i2 > i1:
|
|
155
|
+
parts.append(_shown(old[i1:i2], "-"))
|
|
156
|
+
if j2 > j1:
|
|
157
|
+
parts.append(_shown(new[j1:j2], "+"))
|
|
158
|
+
what = {
|
|
159
|
+
"replace": "differs from",
|
|
160
|
+
"delete": "has lines that are not in",
|
|
161
|
+
"insert": "is missing lines of",
|
|
162
|
+
}[tag]
|
|
163
|
+
issues.append(
|
|
164
|
+
Issue(
|
|
165
|
+
file=out_rel,
|
|
166
|
+
line=min(i1, max(len(old) - 1, 0)) + 1 + offset,
|
|
167
|
+
rule="wrap.differs",
|
|
168
|
+
msg=(
|
|
169
|
+
f"{out_rel} {what} the generator's output here ('-' committed, '+' "
|
|
170
|
+
f"generated); {rerun}.{note}\n" + "\n".join(parts)
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
note = ""
|
|
175
|
+
if len(hunks) > MAX_HUNKS:
|
|
176
|
+
issues.append(
|
|
177
|
+
Issue(
|
|
178
|
+
file=out_rel,
|
|
179
|
+
rule="wrap.differs",
|
|
180
|
+
msg=f"{len(hunks) - MAX_HUNKS} more differing hunks in {out_rel}; {rerun}",
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
return issues
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _mode(args: Mapping[str, Any]) -> str:
|
|
187
|
+
mode = args.get("compare", "auto")
|
|
188
|
+
if mode not in COMPARE_MODES:
|
|
189
|
+
raise WrapperError(f"args['compare'] must be one of {', '.join(COMPARE_MODES)}")
|
|
190
|
+
return str(mode)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _opt_str(args: Mapping[str, Any], key: str) -> str | None:
|
|
194
|
+
value = args.get(key)
|
|
195
|
+
if value is not None and not isinstance(value, str):
|
|
196
|
+
raise WrapperError(f"args[{key!r}] must be a string")
|
|
197
|
+
return value
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
MAX_TIMEOUT_S = 600.0
|
|
201
|
+
"""The longest `timeout_s` the check accepts."""
|
|
202
|
+
_PROTECTED_DIRS = (".git", ".chipgraph")
|
|
203
|
+
_CI_OFF = ("", "0", "false", "no")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _timeout(args: Mapping[str, Any]) -> float:
|
|
207
|
+
"""`timeout_s`: a finite number, `0 < t <= MAX_TIMEOUT_S` (never a bool or a string)."""
|
|
208
|
+
value = args.get("timeout_s", gen.TIMEOUT_S)
|
|
209
|
+
if (
|
|
210
|
+
isinstance(value, bool)
|
|
211
|
+
or not isinstance(value, int | float)
|
|
212
|
+
or not math.isfinite(value)
|
|
213
|
+
or not 0 < value <= MAX_TIMEOUT_S
|
|
214
|
+
):
|
|
215
|
+
raise WrapperError(
|
|
216
|
+
f"args['timeout_s'] must be a number of seconds, 0 < timeout_s <= {MAX_TIMEOUT_S:g}"
|
|
217
|
+
)
|
|
218
|
+
return float(value)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _in_ci() -> bool:
|
|
222
|
+
return os.environ.get("CI", "").strip().lower() not in _CI_OFF
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _require(args: Mapping[str, Any]) -> bool:
|
|
226
|
+
"""`require_emacs`: as given, else whether this runs in CI (see the module)."""
|
|
227
|
+
value = args.get("require_emacs")
|
|
228
|
+
if value is None:
|
|
229
|
+
return _in_ci()
|
|
230
|
+
if not isinstance(value, bool):
|
|
231
|
+
raise WrapperError("args['require_emacs'] must be true or false")
|
|
232
|
+
return value
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _emacs(args: Mapping[str, Any]) -> str:
|
|
236
|
+
"""`emacs`: a bare name (looked up on PATH) or an absolute path to an executable."""
|
|
237
|
+
value = _opt_str(args, "emacs") or gen.EMACS
|
|
238
|
+
if "/" in value or "\\" in value or os.sep in value:
|
|
239
|
+
path = Path(value)
|
|
240
|
+
if not path.is_absolute() or not path.is_file() or not os.access(path, os.X_OK):
|
|
241
|
+
raise WrapperError(
|
|
242
|
+
"args['emacs'] must be a command name or an absolute path to an executable"
|
|
243
|
+
)
|
|
244
|
+
return value
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _guarded(root: Path, rel: str, key: str) -> Path:
|
|
248
|
+
"""The repo file `rel` (`inside`), refused under `.git/` or `.chipgraph/`, as written
|
|
249
|
+
or as resolved."""
|
|
250
|
+
path = inside(root, rel)
|
|
251
|
+
real = Path(os.path.realpath(path)).relative_to(Path(os.path.realpath(root)))
|
|
252
|
+
for parts in (Path(rel).parts, real.parts):
|
|
253
|
+
folded = [p.casefold() for p in parts]
|
|
254
|
+
if ".git" in folded or (folded and folded[0] in _PROTECTED_DIRS):
|
|
255
|
+
raise WrapperError(f"args[{key!r}] may not name a file under .git/ or .chipgraph/")
|
|
256
|
+
return path
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _read_issue(rel: str, exc: OSError | UnicodeDecodeError) -> Issue:
|
|
260
|
+
why = "not UTF-8" if isinstance(exc, UnicodeDecodeError) else exc.strerror or str(exc)
|
|
261
|
+
return Issue(file=rel, rule="wrap.read", msg=f"cannot read {rel}: {why}")
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class WrapCheck:
|
|
265
|
+
"""Regenerates each wrapper and compares it with the committed file (see the module)."""
|
|
266
|
+
|
|
267
|
+
id = "wrap_check"
|
|
268
|
+
name = "Wrap check"
|
|
269
|
+
|
|
270
|
+
async def run(self, spec: CheckSpec, ctx: ToolContext) -> CheckResult:
|
|
271
|
+
start = time.monotonic()
|
|
272
|
+
args = spec.args
|
|
273
|
+
try:
|
|
274
|
+
mode = _mode(args)
|
|
275
|
+
require = _require(args)
|
|
276
|
+
emacs = _emacs(args)
|
|
277
|
+
timeout = _timeout(args)
|
|
278
|
+
resolved = load_profile(ctx.repo_root)
|
|
279
|
+
if resolved is None:
|
|
280
|
+
raise WrapperError("no .chipgraph.yml: the wrappers are listed in the profile")
|
|
281
|
+
profile = resolved.profile
|
|
282
|
+
if ctx.params.get("wrapper"):
|
|
283
|
+
targets = [find_wrapper(profile, ctx.params)]
|
|
284
|
+
else:
|
|
285
|
+
targets = all_wrappers(profile, ctx.params.get("block"))
|
|
286
|
+
except (WrapperError, ConfigError, TypeError, ValueError) as exc:
|
|
287
|
+
return self._result(spec, "error", [Issue(rule="wrap.args", msg=str(exc))], start)
|
|
288
|
+
if not targets:
|
|
289
|
+
note = Issue(severity="info", rule="wrap.none", msg="no wrapper to check here")
|
|
290
|
+
return self._result(spec, "skipped", [note], start)
|
|
291
|
+
|
|
292
|
+
task = bool(ctx.params.get("wrapper"))
|
|
293
|
+
outcomes = [
|
|
294
|
+
await self._one(
|
|
295
|
+
ctx, w, args, mode=mode, task=task, require=require, emacs=emacs, timeout=timeout
|
|
296
|
+
)
|
|
297
|
+
for w in targets
|
|
298
|
+
]
|
|
299
|
+
issues = [i for o in outcomes for i in o.issues]
|
|
300
|
+
files = [f for o in outcomes for f in o.files]
|
|
301
|
+
log = "\n".join(o.log for o in outcomes if o.log)
|
|
302
|
+
if any(o.infra for o in outcomes):
|
|
303
|
+
status: CheckStatus = "error"
|
|
304
|
+
elif any(i.severity == "error" for i in issues):
|
|
305
|
+
status = "fail"
|
|
306
|
+
else:
|
|
307
|
+
status = "pass"
|
|
308
|
+
return self._result(spec, status, issues, start, files, log)
|
|
309
|
+
|
|
310
|
+
@staticmethod
|
|
311
|
+
def _result(
|
|
312
|
+
spec: CheckSpec,
|
|
313
|
+
status: CheckStatus,
|
|
314
|
+
issues: Sequence[Issue],
|
|
315
|
+
start: float,
|
|
316
|
+
files: Sequence[tuple[str, str]] = (),
|
|
317
|
+
log: str = "",
|
|
318
|
+
) -> CheckResult:
|
|
319
|
+
return CheckResult(
|
|
320
|
+
check_id=spec.id,
|
|
321
|
+
status=status,
|
|
322
|
+
issues=tuple(issues),
|
|
323
|
+
log_tail=log[-4000:],
|
|
324
|
+
duration_s=time.monotonic() - start,
|
|
325
|
+
idempotency_key=compute_idempotency_key(spec.id, spec.args, files),
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
async def _one(
|
|
329
|
+
self,
|
|
330
|
+
ctx: ToolContext,
|
|
331
|
+
wrapper: Wrapper,
|
|
332
|
+
args: Mapping[str, Any],
|
|
333
|
+
*,
|
|
334
|
+
mode: str,
|
|
335
|
+
task: bool,
|
|
336
|
+
require: bool,
|
|
337
|
+
emacs: str,
|
|
338
|
+
timeout: float,
|
|
339
|
+
) -> _Outcome:
|
|
340
|
+
root = ctx.repo_root
|
|
341
|
+
out = _Outcome()
|
|
342
|
+
try:
|
|
343
|
+
src_rel, out_rel = paths(wrapper, _opt_str(args, "src"), _opt_str(args, "out"))
|
|
344
|
+
src_path, out_path = _guarded(root, src_rel, "src"), _guarded(root, out_rel, "out")
|
|
345
|
+
vendor = [inside(root, rel) for rel in wrapper.cfg.vendor_files]
|
|
346
|
+
except WrapperError as exc:
|
|
347
|
+
out.issues.append(Issue(rule="wrap.args", msg=str(exc)))
|
|
348
|
+
out.infra = True
|
|
349
|
+
return out
|
|
350
|
+
rerun = f"run the generator (`chipgraph build '{instance_id(GEN_RULE, wrapper)}'`)"
|
|
351
|
+
if not src_path.is_file():
|
|
352
|
+
out.issues.append(
|
|
353
|
+
Issue(file=src_rel, rule="wrap.src_missing", msg=f"no wrapper source at {src_rel}")
|
|
354
|
+
)
|
|
355
|
+
return out
|
|
356
|
+
reading = src_rel
|
|
357
|
+
try:
|
|
358
|
+
source = src_path.read_text(encoding="utf-8")
|
|
359
|
+
out.files.append((src_rel, sha256_file(src_path)))
|
|
360
|
+
for path, rel in zip(vendor, wrapper.cfg.vendor_files, strict=True):
|
|
361
|
+
reading = rel
|
|
362
|
+
if path.is_file():
|
|
363
|
+
out.files.append((rel, sha256_file(path)))
|
|
364
|
+
reading = out_rel
|
|
365
|
+
committed = out_path.read_text(encoding="utf-8") if out_path.is_file() else None
|
|
366
|
+
if committed is not None:
|
|
367
|
+
out.files.append((out_rel, sha256_file(out_path)))
|
|
368
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
369
|
+
out.issues.append(_read_issue(reading, exc))
|
|
370
|
+
out.infra = isinstance(exc, OSError)
|
|
371
|
+
return out
|
|
372
|
+
for note in lisp.scan(source):
|
|
373
|
+
if note.note:
|
|
374
|
+
out.issues.append(
|
|
375
|
+
Issue(
|
|
376
|
+
file=src_rel,
|
|
377
|
+
line=note.line,
|
|
378
|
+
severity="info",
|
|
379
|
+
rule="wrap.lisp",
|
|
380
|
+
msg=note.message,
|
|
381
|
+
)
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
declaration = extract_declaration(vendor, wrapper.cfg.vendor_module)
|
|
385
|
+
if declaration is None:
|
|
386
|
+
out.infra = True
|
|
387
|
+
out.issues.append(
|
|
388
|
+
Issue(
|
|
389
|
+
rule="wrap.vendor_declaration",
|
|
390
|
+
msg=(
|
|
391
|
+
f"no declaration of module `{wrapper.cfg.vendor_module}` in "
|
|
392
|
+
f"{', '.join(wrapper.cfg.vendor_files)} (the wrapper's vendor_files)"
|
|
393
|
+
),
|
|
394
|
+
)
|
|
395
|
+
)
|
|
396
|
+
return out
|
|
397
|
+
|
|
398
|
+
current = gen.source_sha(source)
|
|
399
|
+
origin = gen.generated_from(committed) if committed is not None else None
|
|
400
|
+
fresh = committed is not None and origin is not None and origin[1] == current
|
|
401
|
+
expansion = await gen.expand(
|
|
402
|
+
ctx.runner,
|
|
403
|
+
root,
|
|
404
|
+
src_rel,
|
|
405
|
+
wrapper.cfg.vendor_files,
|
|
406
|
+
name=wrapper.name,
|
|
407
|
+
emacs=emacs,
|
|
408
|
+
timeout_s=timeout,
|
|
409
|
+
text=source,
|
|
410
|
+
)
|
|
411
|
+
out.log = expansion.log
|
|
412
|
+
regenerated: str | None = None
|
|
413
|
+
if expansion.ok:
|
|
414
|
+
expanded = expansion.text
|
|
415
|
+
regenerated = gen.render(expanded, src_rel, source)
|
|
416
|
+
elif expansion.infra and {i.rule for i in expansion.issues} <= set(_NO_EMACS):
|
|
417
|
+
marked = committed is not None and verify(committed) is True
|
|
418
|
+
if require or not marked or not fresh:
|
|
419
|
+
if require and "require_emacs" not in args:
|
|
420
|
+
why = (
|
|
421
|
+
"the CI environment variable is set, so emacs is required (the "
|
|
422
|
+
"marker fallback is not a proof; `require_emacs: false` accepts it)"
|
|
423
|
+
)
|
|
424
|
+
elif require:
|
|
425
|
+
why = "the profile requires emacs (require_emacs)"
|
|
426
|
+
elif committed is None:
|
|
427
|
+
why = f"{out_rel} does not exist"
|
|
428
|
+
elif not marked:
|
|
429
|
+
why = f"{out_rel} has no valid generated marker (edited by hand?)"
|
|
430
|
+
else:
|
|
431
|
+
why = f"{out_rel} was not generated from the current source"
|
|
432
|
+
out.infra = True
|
|
433
|
+
out.issues.extend(
|
|
434
|
+
i.model_copy(update={"msg": f"{i.msg}; cannot check {src_rel}: {why}"})
|
|
435
|
+
for i in expansion.issues
|
|
436
|
+
)
|
|
437
|
+
return out
|
|
438
|
+
assert committed is not None
|
|
439
|
+
expanded = gen.body_of(committed)
|
|
440
|
+
out.issues.append(
|
|
441
|
+
Issue(
|
|
442
|
+
file=out_rel,
|
|
443
|
+
severity="warning",
|
|
444
|
+
rule="wrap.not_regenerated",
|
|
445
|
+
msg=(
|
|
446
|
+
f"emacs is not available, so {src_rel} was not regenerated: {out_rel} "
|
|
447
|
+
"is checked by its generated marker and its source's sha256 instead. "
|
|
448
|
+
"The marker is a plain sha256, not a signature: an edit of the body "
|
|
449
|
+
"with a re-rendered header passes here, and only a regeneration "
|
|
450
|
+
f"catches it ({gen.INSTALL_HINT})"
|
|
451
|
+
),
|
|
452
|
+
)
|
|
453
|
+
)
|
|
454
|
+
else:
|
|
455
|
+
out.infra = expansion.infra
|
|
456
|
+
out.issues.extend(expansion.issues)
|
|
457
|
+
return out
|
|
458
|
+
|
|
459
|
+
src_lines = {i.name: i.line for i in instances(source, declaration.module)}
|
|
460
|
+
out.issues.extend(
|
|
461
|
+
validate(
|
|
462
|
+
expanded,
|
|
463
|
+
declaration,
|
|
464
|
+
file=src_rel,
|
|
465
|
+
line_for=lambda inst: src_lines.get(inst.name),
|
|
466
|
+
wrapper=wrapper.name,
|
|
467
|
+
)
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
wanted = mode == "always" or (mode == "auto" and (not task or fresh))
|
|
471
|
+
if not wanted or regenerated is None:
|
|
472
|
+
return out
|
|
473
|
+
if committed is None:
|
|
474
|
+
out.issues.append(
|
|
475
|
+
Issue(
|
|
476
|
+
file=out_rel,
|
|
477
|
+
rule="wrap.not_generated",
|
|
478
|
+
msg=f"{out_rel} does not exist; {rerun} and commit the result",
|
|
479
|
+
)
|
|
480
|
+
)
|
|
481
|
+
return out
|
|
482
|
+
out.issues.extend(compare(committed, regenerated, out_rel, rerun))
|
|
483
|
+
return out
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
__all__ = ["COMPARE_MODES", "MAX_TIMEOUT_S", "WrapCheck", "compare"]
|