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
chipgraph/app/ingest.py
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
"""Wire a project's profile to the extractors and run the core ingest (DESIGN.md 4.1-4.5).
|
|
2
|
+
|
|
3
|
+
`run_ingest` reads the resolved profile, builds a `SourcePart` for every input it can
|
|
4
|
+
(the chip-level spec, each block's RTL via the pyslang extractor, each block's MAS via the
|
|
5
|
+
`mas-markdown` extractor), calls `chipgraph.core.model.ingest.ingest` to merge them
|
|
6
|
+
deterministically, and writes the result to the project's `ModelStore` (rebuilt from
|
|
7
|
+
scratch, under `.chipgraph/state/cache/`, so the repo tree stays clean).
|
|
8
|
+
|
|
9
|
+
This layer may import adapters, packs and extractors; the merge logic itself is in
|
|
10
|
+
`core.model.ingest`, which knows nothing about them.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import hashlib
|
|
16
|
+
import json
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import TYPE_CHECKING
|
|
20
|
+
|
|
21
|
+
from chipgraph.adapters.tool.filelist import Filelist, FilelistError, read_filelist
|
|
22
|
+
from chipgraph.adapters.tool.pyslang import ParseDiagnostic, PyslangExtractor
|
|
23
|
+
from chipgraph.app.context import AppContext
|
|
24
|
+
from chipgraph.core.config.loader import ResolvedProfile
|
|
25
|
+
from chipgraph.core.config.models import Profile
|
|
26
|
+
from chipgraph.core.model.ingest import (
|
|
27
|
+
IngestIssue,
|
|
28
|
+
IngestResult,
|
|
29
|
+
InputFile,
|
|
30
|
+
SourcePart,
|
|
31
|
+
ingest,
|
|
32
|
+
)
|
|
33
|
+
from chipgraph.core.model.model import DesignModel
|
|
34
|
+
from chipgraph.core.model.store import ModelStore, default_model_db_path
|
|
35
|
+
from chipgraph.core.plugin_api.registry import PluginError, Registry
|
|
36
|
+
from chipgraph.packs.spec_core.extract.mas import MasDiagnostic, MasExtractor
|
|
37
|
+
|
|
38
|
+
if TYPE_CHECKING:
|
|
39
|
+
from chipgraph.packs.assist.ask.documents import DocumentIndexReport
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True, slots=True)
|
|
43
|
+
class IngestReport:
|
|
44
|
+
"""The result of `run_ingest`: what was written, plus the model, stats and issues."""
|
|
45
|
+
|
|
46
|
+
model: DesignModel
|
|
47
|
+
result: IngestResult
|
|
48
|
+
db_path: Path
|
|
49
|
+
documents: DocumentIndexReport | None = None
|
|
50
|
+
"""The documents indexed for `/ask` full-text search (and the ones skipped)."""
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def issues(self) -> tuple[IngestIssue, ...]:
|
|
54
|
+
return self.result.issues
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def has_error(self) -> bool:
|
|
58
|
+
return any(issue.severity == "error" for issue in self.result.issues)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def run_ingest(ctx: AppContext) -> IngestReport:
|
|
62
|
+
"""Run every extractor for the project, merge, and write the Design Model store.
|
|
63
|
+
|
|
64
|
+
Raises `AppError` if no profile is loaded. A missing file or an extractor exception
|
|
65
|
+
for one block becomes an `error` `IngestIssue` for that block; the rest of ingest
|
|
66
|
+
still runs.
|
|
67
|
+
"""
|
|
68
|
+
resolved = ctx.require_profile()
|
|
69
|
+
root = ctx.root
|
|
70
|
+
|
|
71
|
+
builder = _PartBuilder(root, resolved, ctx.registry)
|
|
72
|
+
builder.build_chip_spec()
|
|
73
|
+
builder.build_blocks()
|
|
74
|
+
|
|
75
|
+
model, result = ingest(
|
|
76
|
+
builder.parts,
|
|
77
|
+
input_files=builder.input_files,
|
|
78
|
+
profile_digest=_profile_digest(resolved.profile),
|
|
79
|
+
ip_blocks={name: override.instances for name, override in resolved.profile.blocks.items()},
|
|
80
|
+
)
|
|
81
|
+
# Fold the builder's own file-level issues (missing files, adapter exceptions) in.
|
|
82
|
+
result = _with_extra_issues(result, builder.issues)
|
|
83
|
+
|
|
84
|
+
db_path = default_model_db_path(root)
|
|
85
|
+
store = ModelStore(db_path)
|
|
86
|
+
store.write(model, build_inputs_hash=result.build_inputs_hash)
|
|
87
|
+
# `write()` rebuilds the database and drops every document: re-index the `/ask`
|
|
88
|
+
# documents (inputs read above plus the project's Markdown docs) right after it.
|
|
89
|
+
# Imported here: the `assist` pack imports this module (its `/audit` runs ingest).
|
|
90
|
+
from chipgraph.packs.assist.ask.documents import index_documents
|
|
91
|
+
|
|
92
|
+
documents = index_documents(
|
|
93
|
+
root, store, [f.rel_path for f in builder.input_files], ctx.store.labels
|
|
94
|
+
)
|
|
95
|
+
result = _with_extra_issues(result, _document_issues(documents))
|
|
96
|
+
return IngestReport(model=model, result=result, db_path=db_path, documents=documents)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _document_issues(documents: DocumentIndexReport) -> list[IngestIssue]:
|
|
100
|
+
"""One `info` issue per `nda` file that was not indexed for `/ask`."""
|
|
101
|
+
return [
|
|
102
|
+
IngestIssue(
|
|
103
|
+
severity="info",
|
|
104
|
+
code="ask.nda_not_indexed",
|
|
105
|
+
message=f"{path} is labelled 'nda': not indexed for /ask (it may use a cloud model)",
|
|
106
|
+
file=path,
|
|
107
|
+
)
|
|
108
|
+
for path in documents.nda_skipped
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass(slots=True)
|
|
113
|
+
class _PartBuilder:
|
|
114
|
+
"""Accumulates `SourcePart`s, the files they were read from, and file-level issues."""
|
|
115
|
+
|
|
116
|
+
root: Path
|
|
117
|
+
resolved: ResolvedProfile
|
|
118
|
+
registry: Registry
|
|
119
|
+
parts: list[SourcePart] = None # type: ignore[assignment]
|
|
120
|
+
input_files: list[InputFile] = None # type: ignore[assignment]
|
|
121
|
+
issues: list[IngestIssue] = None # type: ignore[assignment]
|
|
122
|
+
|
|
123
|
+
def __post_init__(self) -> None:
|
|
124
|
+
self.parts = []
|
|
125
|
+
self.input_files = []
|
|
126
|
+
self.issues = []
|
|
127
|
+
|
|
128
|
+
# -- chip-level spec ------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
def build_chip_spec(self) -> None:
|
|
131
|
+
chip = self.resolved.profile.spec.chip
|
|
132
|
+
if chip is None:
|
|
133
|
+
return
|
|
134
|
+
path = (self.root / chip.path).resolve()
|
|
135
|
+
# Any `format` adapter in the registry (built-in, entry point or local plugin) that
|
|
136
|
+
# offers `load_model(path, root)` can build the chip-level part.
|
|
137
|
+
try:
|
|
138
|
+
adapter = self.registry.get("format", chip.format)
|
|
139
|
+
except PluginError as exc:
|
|
140
|
+
self.issues.append(
|
|
141
|
+
IngestIssue(
|
|
142
|
+
severity="error",
|
|
143
|
+
code="chip_spec_format",
|
|
144
|
+
message=f"unknown chip spec format {chip.format!r}: {exc}",
|
|
145
|
+
file=chip.path,
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
return
|
|
149
|
+
load_model = getattr(adapter, "load_model", None)
|
|
150
|
+
if not callable(load_model):
|
|
151
|
+
self.issues.append(
|
|
152
|
+
IngestIssue(
|
|
153
|
+
severity="error",
|
|
154
|
+
code="chip_spec_format",
|
|
155
|
+
message=f"format adapter {chip.format!r} has no load_model(path, root)",
|
|
156
|
+
file=chip.path,
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
return
|
|
160
|
+
if not path.is_file():
|
|
161
|
+
self.issues.append(
|
|
162
|
+
IngestIssue(
|
|
163
|
+
severity="error",
|
|
164
|
+
code="missing_file",
|
|
165
|
+
message=f"chip spec not found: {chip.path}",
|
|
166
|
+
file=chip.path,
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
return
|
|
170
|
+
try:
|
|
171
|
+
model, warnings = load_model(path, root=self.root)
|
|
172
|
+
except Exception as exc: # extractor must not crash ingest
|
|
173
|
+
self.issues.append(
|
|
174
|
+
IngestIssue(
|
|
175
|
+
severity="error",
|
|
176
|
+
code="chip_spec_error",
|
|
177
|
+
message=f"failed to read chip spec {chip.path}: {exc}",
|
|
178
|
+
file=chip.path,
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
return
|
|
182
|
+
self._record_file(path)
|
|
183
|
+
diagnostics = tuple(
|
|
184
|
+
IngestIssue(severity="warning", code="chip_spec_warning", message=w) for w in warnings
|
|
185
|
+
)
|
|
186
|
+
self.parts.append(
|
|
187
|
+
SourcePart(source="chip", block=None, model=model, diagnostics=diagnostics)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# -- per-block RTL and MAS ------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
def build_blocks(self) -> None:
|
|
193
|
+
for block in sorted(self.resolved.profile.blocks):
|
|
194
|
+
block_profile = self.resolved.for_block(block)
|
|
195
|
+
self._build_block_rtl(block, block_profile)
|
|
196
|
+
for template in _layout_templates(block_profile, "spec"):
|
|
197
|
+
self._build_block_mas(block, _fill(template, block))
|
|
198
|
+
self._report_unread_specs()
|
|
199
|
+
|
|
200
|
+
def _report_unread_specs(self) -> None:
|
|
201
|
+
"""Warn about spec files that match the project's spec template but no block read.
|
|
202
|
+
|
|
203
|
+
A spec named after a block the profile does not list (or lists under another name)
|
|
204
|
+
would otherwise be skipped silently. Map it with a per-block `layout.spec`, add the
|
|
205
|
+
block, or exempt the file with `paths: {<file>: {checks: {ingest: off}, reason: ...}}`.
|
|
206
|
+
"""
|
|
207
|
+
read = {f.rel_path for f in self.input_files}
|
|
208
|
+
patterns: set[str] = set()
|
|
209
|
+
for template in _layout_templates(self.resolved.profile, "spec"):
|
|
210
|
+
if "{block}" in template or "{BLOCK}" in template:
|
|
211
|
+
patterns.add(template.replace("{block}", "*").replace("{BLOCK}", "*"))
|
|
212
|
+
seen: set[str] = set()
|
|
213
|
+
for pattern in sorted(patterns):
|
|
214
|
+
for path in sorted(self.root.glob(pattern)):
|
|
215
|
+
rel = _rel(path, self.root)
|
|
216
|
+
if rel in read or rel in seen or not path.is_file():
|
|
217
|
+
continue
|
|
218
|
+
seen.add(rel)
|
|
219
|
+
if self.resolved.for_path(rel).checks.get("ingest") == "off":
|
|
220
|
+
continue
|
|
221
|
+
self.issues.append(
|
|
222
|
+
IngestIssue(
|
|
223
|
+
severity="warning",
|
|
224
|
+
code="unread_spec",
|
|
225
|
+
message=(
|
|
226
|
+
f"{rel} matches the spec template {pattern!r} but no block reads "
|
|
227
|
+
"it; map it in blocks.<name>.layout.spec, or exempt it in paths:"
|
|
228
|
+
),
|
|
229
|
+
file=rel,
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def _build_block_rtl(self, block: str, block_profile: Profile) -> None:
|
|
234
|
+
templates = _layout_templates(block_profile, "filelist")
|
|
235
|
+
if not templates:
|
|
236
|
+
return # no filelist template, or an explicit `filelist: []` for this block
|
|
237
|
+
filelist_path = (self.root / _fill(templates[0], block)).resolve()
|
|
238
|
+
rel = _rel(filelist_path, self.root)
|
|
239
|
+
if not filelist_path.is_file():
|
|
240
|
+
self.issues.append(
|
|
241
|
+
IngestIssue(
|
|
242
|
+
severity="warning",
|
|
243
|
+
code="missing_filelist",
|
|
244
|
+
message=(
|
|
245
|
+
f"block {block!r}: no filelist at {rel}; its RTL is not in the model "
|
|
246
|
+
"(set blocks.<name>.layout.filelist, or [] if it has none)"
|
|
247
|
+
),
|
|
248
|
+
block=_block_key(block),
|
|
249
|
+
file=rel,
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
return
|
|
253
|
+
try:
|
|
254
|
+
filelist = _read_filelist_resolving(filelist_path, self.root)
|
|
255
|
+
except FilelistError as exc:
|
|
256
|
+
self.issues.append(
|
|
257
|
+
IngestIssue(
|
|
258
|
+
severity="error",
|
|
259
|
+
code="filelist_error",
|
|
260
|
+
message=f"cannot read filelist {rel}: {exc}",
|
|
261
|
+
block=_block_key(block),
|
|
262
|
+
file=rel,
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
return
|
|
266
|
+
if not filelist.sources:
|
|
267
|
+
self.issues.append(
|
|
268
|
+
IngestIssue(
|
|
269
|
+
severity="info",
|
|
270
|
+
code="empty_filelist",
|
|
271
|
+
message=f"filelist for block {block!r} lists no sources",
|
|
272
|
+
block=_block_key(block),
|
|
273
|
+
file=rel,
|
|
274
|
+
)
|
|
275
|
+
)
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
parse = _parse_options(block_profile)
|
|
279
|
+
try:
|
|
280
|
+
model, diags = PyslangExtractor.extract_model(
|
|
281
|
+
filelist,
|
|
282
|
+
block=None,
|
|
283
|
+
root=self.root,
|
|
284
|
+
tops=parse.tops,
|
|
285
|
+
defines=parse.defines,
|
|
286
|
+
clock_patterns=parse.clock_patterns,
|
|
287
|
+
reset_patterns=parse.reset_patterns,
|
|
288
|
+
)
|
|
289
|
+
except Exception as exc:
|
|
290
|
+
self.issues.append(
|
|
291
|
+
IngestIssue(
|
|
292
|
+
severity="error",
|
|
293
|
+
code="rtl_error",
|
|
294
|
+
message=f"RTL extraction failed for block {block!r}: {exc}",
|
|
295
|
+
block=_block_key(block),
|
|
296
|
+
file=rel,
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
self._record_file(filelist_path)
|
|
302
|
+
for source in filelist.sources:
|
|
303
|
+
self._record_file(source)
|
|
304
|
+
diagnostics = tuple(_parse_diag_to_issue(d, block, self.root) for d in diags)
|
|
305
|
+
self.parts.append(
|
|
306
|
+
SourcePart(source="rtl", block=_block_key(block), model=model, diagnostics=diagnostics)
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
def _build_block_mas(self, block: str, rel_path: str) -> None:
|
|
310
|
+
block_profile = self.resolved.for_block(block)
|
|
311
|
+
mas_path = (self.root / rel_path).resolve()
|
|
312
|
+
rel = _rel(mas_path, self.root)
|
|
313
|
+
if not mas_path.is_file():
|
|
314
|
+
self.issues.append(
|
|
315
|
+
IngestIssue(
|
|
316
|
+
severity="warning",
|
|
317
|
+
code="missing_spec",
|
|
318
|
+
message=(
|
|
319
|
+
f"block {block!r}: no spec at {rel}; its requirements are not in the "
|
|
320
|
+
"model (set blocks.<name>.layout.spec, or [] if it has none)"
|
|
321
|
+
),
|
|
322
|
+
block=_block_key(block),
|
|
323
|
+
file=rel,
|
|
324
|
+
)
|
|
325
|
+
)
|
|
326
|
+
return
|
|
327
|
+
requirements = block_profile.spec.requirements
|
|
328
|
+
try:
|
|
329
|
+
model, diags = MasExtractor.extract_model(
|
|
330
|
+
mas_path,
|
|
331
|
+
block=block,
|
|
332
|
+
requirements=requirements,
|
|
333
|
+
root=self.root,
|
|
334
|
+
access_modes=block_profile.spec.register_access,
|
|
335
|
+
)
|
|
336
|
+
except Exception as exc:
|
|
337
|
+
self.issues.append(
|
|
338
|
+
IngestIssue(
|
|
339
|
+
severity="error",
|
|
340
|
+
code="mas_error",
|
|
341
|
+
message=f"MAS extraction failed for block {block!r}: {exc}",
|
|
342
|
+
block=_block_key(block),
|
|
343
|
+
file=rel,
|
|
344
|
+
)
|
|
345
|
+
)
|
|
346
|
+
return
|
|
347
|
+
|
|
348
|
+
self._record_file(mas_path)
|
|
349
|
+
diagnostics = tuple(_mas_diag_to_issue(d, block) for d in diags)
|
|
350
|
+
self.parts.append(
|
|
351
|
+
SourcePart(source="mas", block=_block_key(block), model=model, diagnostics=diagnostics)
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
# -- helpers --------------------------------------------------------------------
|
|
355
|
+
|
|
356
|
+
def _record_file(self, path: Path) -> None:
|
|
357
|
+
rel = _rel(path, self.root)
|
|
358
|
+
try:
|
|
359
|
+
digest = hashlib.sha256(path.read_bytes()).hexdigest()
|
|
360
|
+
except OSError:
|
|
361
|
+
return
|
|
362
|
+
self.input_files.append(InputFile(rel_path=rel, sha256=digest))
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _read_filelist_resolving(filelist_path: Path, root: Path) -> Filelist:
|
|
366
|
+
"""Read a filelist, choosing the path style that actually resolves to real files.
|
|
367
|
+
|
|
368
|
+
QSoC-style filelists resolve source paths relative to the filelist's own directory;
|
|
369
|
+
tinysoc-style filelists resolve them relative to the project root (how its Makefile
|
|
370
|
+
runs Verilator). Try filelist-relative first, and fall back to root-relative when that
|
|
371
|
+
leaves listed sources unresolved, so both conventions ingest without a profile flag.
|
|
372
|
+
"""
|
|
373
|
+
filelist = read_filelist(filelist_path, relative_to="filelist", root=root)
|
|
374
|
+
if filelist.sources and all(src.is_file() for src in filelist.sources):
|
|
375
|
+
return filelist
|
|
376
|
+
alt = read_filelist(filelist_path, relative_to="cwd", root=root)
|
|
377
|
+
if alt.sources and sum(src.is_file() for src in alt.sources) > sum(
|
|
378
|
+
src.is_file() for src in filelist.sources
|
|
379
|
+
):
|
|
380
|
+
return alt
|
|
381
|
+
return filelist
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
@dataclass(frozen=True, slots=True)
|
|
385
|
+
class _ParseOptions:
|
|
386
|
+
"""Typed pyslang options, with the extractor's own defaults when unset."""
|
|
387
|
+
|
|
388
|
+
clock_patterns: tuple[str, ...] = (r"^i_clk", r"^clk")
|
|
389
|
+
reset_patterns: tuple[str, ...] = (r"^i_rst", r"^rst")
|
|
390
|
+
tops: tuple[str, ...] | None = None
|
|
391
|
+
defines: dict[str, str | None] | None = None
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _parse_options(profile: Profile) -> _ParseOptions:
|
|
395
|
+
"""The pyslang extractor options from `adapters.parse` (all optional)."""
|
|
396
|
+
cfg = profile.adapters.get("parse")
|
|
397
|
+
if cfg is None:
|
|
398
|
+
return _ParseOptions()
|
|
399
|
+
extra = cfg.model_dump()
|
|
400
|
+
updates: dict[str, object] = {}
|
|
401
|
+
clock = extra.get("clock_patterns")
|
|
402
|
+
reset = extra.get("reset_patterns")
|
|
403
|
+
tops = extra.get("tops")
|
|
404
|
+
defines = extra.get("defines")
|
|
405
|
+
if isinstance(clock, list | tuple):
|
|
406
|
+
updates["clock_patterns"] = tuple(str(p) for p in clock)
|
|
407
|
+
if isinstance(reset, list | tuple):
|
|
408
|
+
updates["reset_patterns"] = tuple(str(p) for p in reset)
|
|
409
|
+
if isinstance(tops, list | tuple):
|
|
410
|
+
updates["tops"] = tuple(str(t) for t in tops)
|
|
411
|
+
if isinstance(defines, dict):
|
|
412
|
+
updates["defines"] = {str(k): (None if v is None else str(v)) for k, v in defines.items()}
|
|
413
|
+
return _ParseOptions(**updates) # type: ignore[arg-type]
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _layout_templates(profile: Profile, key: str) -> tuple[str, ...]:
|
|
417
|
+
"""The layout template(s) for `key`: one string, a list of several, or `()` for none.
|
|
418
|
+
|
|
419
|
+
An explicit empty list (`spec: []`) says the block has no such file, so nothing is
|
|
420
|
+
reported missing for it.
|
|
421
|
+
"""
|
|
422
|
+
value = profile.layout.get(key)
|
|
423
|
+
if isinstance(value, str):
|
|
424
|
+
return (value,)
|
|
425
|
+
if isinstance(value, tuple):
|
|
426
|
+
return tuple(v for v in value if isinstance(v, str))
|
|
427
|
+
return ()
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _fill(template: str, block: str) -> str:
|
|
431
|
+
return template.replace("{block}", block).replace("{BLOCK}", block.upper())
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _block_key(block: str) -> str:
|
|
435
|
+
return f"block:{block}"
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _rel(path: Path, root: Path) -> str:
|
|
439
|
+
try:
|
|
440
|
+
return path.resolve().relative_to(root.resolve()).as_posix()
|
|
441
|
+
except ValueError:
|
|
442
|
+
return path.as_posix()
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _parse_diag_to_issue(diag: ParseDiagnostic, block: str, root: Path) -> IngestIssue:
|
|
446
|
+
# slang's warnings are lint findings (the `lint` check reports those); for ingest only
|
|
447
|
+
# a parse or elaboration error matters, so warnings are kept as info.
|
|
448
|
+
severity = "error" if diag.severity == "error" else "info"
|
|
449
|
+
file = _rel(Path(diag.file), root) if diag.file else None
|
|
450
|
+
return IngestIssue(
|
|
451
|
+
severity=severity, # type: ignore[arg-type]
|
|
452
|
+
code="rtl_diagnostic",
|
|
453
|
+
message=diag.message,
|
|
454
|
+
block=_block_key(block),
|
|
455
|
+
file=file,
|
|
456
|
+
line=diag.line,
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _mas_diag_to_issue(diag: MasDiagnostic, block: str) -> IngestIssue:
|
|
461
|
+
return IngestIssue(
|
|
462
|
+
severity=diag.severity,
|
|
463
|
+
code=f"mas.{diag.code}",
|
|
464
|
+
message=diag.message,
|
|
465
|
+
block=_block_key(block),
|
|
466
|
+
file=diag.file,
|
|
467
|
+
line=diag.line,
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _with_extra_issues(result: IngestResult, extra: list[IngestIssue]) -> IngestResult:
|
|
472
|
+
if not extra:
|
|
473
|
+
return result
|
|
474
|
+
from chipgraph.core.model.ingest import IngestStats
|
|
475
|
+
|
|
476
|
+
issues = _sort_issues(tuple(result.issues) + tuple(extra))
|
|
477
|
+
by_severity: dict[str, int] = {}
|
|
478
|
+
for issue in issues:
|
|
479
|
+
by_severity[issue.severity] = by_severity.get(issue.severity, 0) + 1
|
|
480
|
+
stats = result.stats.model_copy(
|
|
481
|
+
update={"diagnostics_by_severity": dict(sorted(by_severity.items()))}
|
|
482
|
+
)
|
|
483
|
+
assert isinstance(stats, IngestStats)
|
|
484
|
+
return result.model_copy(update={"issues": issues, "stats": stats})
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _sort_issues(issues: tuple[IngestIssue, ...]) -> tuple[IngestIssue, ...]:
|
|
488
|
+
order = {"error": 0, "warning": 1, "info": 2}
|
|
489
|
+
return tuple(
|
|
490
|
+
sorted(
|
|
491
|
+
issues,
|
|
492
|
+
key=lambda i: (
|
|
493
|
+
order.get(i.severity, 3),
|
|
494
|
+
i.code,
|
|
495
|
+
i.key or "",
|
|
496
|
+
i.block or "",
|
|
497
|
+
i.file or "",
|
|
498
|
+
i.line or 0,
|
|
499
|
+
i.message,
|
|
500
|
+
),
|
|
501
|
+
)
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _profile_digest(profile: Profile) -> str:
|
|
506
|
+
"""A stable digest of the ingest-relevant profile sections.
|
|
507
|
+
|
|
508
|
+
Changing any of these (spec source, requirement config, layout templates, blocks or
|
|
509
|
+
the parse adapter) changes the build-inputs hash, so a stale cache is detected.
|
|
510
|
+
"""
|
|
511
|
+
relevant = {
|
|
512
|
+
"spec": profile.spec.model_dump(mode="json"),
|
|
513
|
+
"layout": {k: v for k, v in sorted(profile.layout.items())},
|
|
514
|
+
"blocks": {k: v.model_dump(mode="json") for k, v in sorted(profile.blocks.items())},
|
|
515
|
+
"parse": profile.adapters["parse"].model_dump(mode="json")
|
|
516
|
+
if "parse" in profile.adapters
|
|
517
|
+
else None,
|
|
518
|
+
}
|
|
519
|
+
canonical = json.dumps(relevant, sort_keys=True, separators=(",", ":"), default=str)
|
|
520
|
+
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
__all__ = ["IngestReport", "run_ingest"]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Built-in checks: deterministic, no-external-tool verification of project conventions.
|
|
2
|
+
|
|
3
|
+
See DESIGN.md 7.3. Each check is registered under the `chipgraph.adapters.check` entry
|
|
4
|
+
point group (see `pyproject.toml`) and satisfies `chipgraph.core.plugin_api.Check`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from chipgraph.checks.cdc_struct import CdcStructCheck
|
|
8
|
+
from chipgraph.checks.connect import ConnectCheck
|
|
9
|
+
from chipgraph.checks.cross_chip import CrossChipCheck
|
|
10
|
+
from chipgraph.checks.duplicate import DuplicateCheck
|
|
11
|
+
from chipgraph.checks.filelist import FilelistCheck
|
|
12
|
+
from chipgraph.checks.generated import GeneratedCheck, stamp, verify
|
|
13
|
+
from chipgraph.checks.hardcode import HardcodeCheck
|
|
14
|
+
from chipgraph.checks.layout import LayoutCheck
|
|
15
|
+
from chipgraph.checks.naming import NamingCheck
|
|
16
|
+
from chipgraph.checks.ports_diff import PortsDiffCheck
|
|
17
|
+
from chipgraph.checks.spec_schema import SpecSchemaCheck
|
|
18
|
+
from chipgraph.checks.trace import TraceCheck
|
|
19
|
+
|
|
20
|
+
BUILTIN_CHECKS: dict[str, type] = {
|
|
21
|
+
"layout": LayoutCheck,
|
|
22
|
+
"filelist": FilelistCheck,
|
|
23
|
+
"generated": GeneratedCheck,
|
|
24
|
+
"naming": NamingCheck,
|
|
25
|
+
"spec_schema": SpecSchemaCheck,
|
|
26
|
+
"cross_chip": CrossChipCheck,
|
|
27
|
+
"ports_diff": PortsDiffCheck,
|
|
28
|
+
"duplicate": DuplicateCheck,
|
|
29
|
+
"trace": TraceCheck,
|
|
30
|
+
"connect": ConnectCheck,
|
|
31
|
+
"hardcode": HardcodeCheck,
|
|
32
|
+
"cdc_struct": CdcStructCheck,
|
|
33
|
+
}
|
|
34
|
+
"""Maps a check's registered name to its class, for entry-point-free wiring in tests."""
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"BUILTIN_CHECKS",
|
|
38
|
+
"CdcStructCheck",
|
|
39
|
+
"ConnectCheck",
|
|
40
|
+
"CrossChipCheck",
|
|
41
|
+
"DuplicateCheck",
|
|
42
|
+
"FilelistCheck",
|
|
43
|
+
"GeneratedCheck",
|
|
44
|
+
"HardcodeCheck",
|
|
45
|
+
"LayoutCheck",
|
|
46
|
+
"NamingCheck",
|
|
47
|
+
"PortsDiffCheck",
|
|
48
|
+
"SpecSchemaCheck",
|
|
49
|
+
"TraceCheck",
|
|
50
|
+
"stamp",
|
|
51
|
+
"verify",
|
|
52
|
+
]
|