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,773 @@
|
|
|
1
|
+
"""`EdalizeTool`: cocotb simulation on Verilator or Icarus, configured by Edalize.
|
|
2
|
+
|
|
3
|
+
Shape (docs/spikes/S3.md): Edalize 0.6's `sim` flow is used **in process, to configure
|
|
4
|
+
only** (it writes the Makefile and the tool files into a work root and starts no
|
|
5
|
+
subprocess for these two simulators). The build (`make`) and the run (the model, or
|
|
6
|
+
``make run`` for Icarus) then go through `ctx.runner`, with a cocotb 2.x environment
|
|
7
|
+
chipgraph computes itself (`GPI_USERS`, `COCOTB_TEST_MODULES`, `COCOTB_TOPLEVEL`,
|
|
8
|
+
`COCOTB_RESULTS_FILE`, the virtualenv's `bin` first on `PATH`, ...). Edalize's own
|
|
9
|
+
`build()`/`run()` are never called: they call `subprocess` directly and, with cocotb 2.1,
|
|
10
|
+
exit 0 without running a test. The result comes from cocotb's `results.xml` only
|
|
11
|
+
(`CocotbParser`): failing tests leave every exit code at 0, and **no `results.xml` is an
|
|
12
|
+
`error`, never a `pass`**.
|
|
13
|
+
|
|
14
|
+
Edalize and cocotb are the optional `sim` extra (``uv sync --extra sim``) and are imported
|
|
15
|
+
lazily: without them the adapter still loads, and a run returns an `error` saying how to
|
|
16
|
+
install them. A missing simulator, `make` or C++ compiler is an `error` that names it.
|
|
17
|
+
|
|
18
|
+
Profile example (`.chipgraph.yml`)::
|
|
19
|
+
|
|
20
|
+
adapters:
|
|
21
|
+
sim:
|
|
22
|
+
use: edalize
|
|
23
|
+
simulator: verilator # or icarus; `tool:` is accepted too
|
|
24
|
+
top: "tiny_{block}"
|
|
25
|
+
filelist: "filelists/{block}.f"
|
|
26
|
+
test_module: "dv/{block}_tb.py" # a file, or dotted names found in `test_dir`
|
|
27
|
+
waves: false
|
|
28
|
+
timeout_s: 600
|
|
29
|
+
|
|
30
|
+
`spec.args` (every string may use `ctx.params` placeholders such as ``{block}``):
|
|
31
|
+
|
|
32
|
+
- ``top`` (required): the HDL toplevel, also `COCOTB_TOPLEVEL`.
|
|
33
|
+
- ``filelist`` and/or ``files``: a `.f` filelist (read by
|
|
34
|
+
`chipgraph.adapters.tool.filelist.read_filelist`: sources, ``+incdir+``,
|
|
35
|
+
``+define+``) and/or a list of source files, repo-relative. ``filelist_relative_to``:
|
|
36
|
+
``auto`` (default: filelist-relative, else repo-root-relative, whichever resolves),
|
|
37
|
+
``filelist`` or ``cwd`` (the repo root).
|
|
38
|
+
- ``parameters`` (HDL parameters), ``defines``, ``plusargs``: mappings.
|
|
39
|
+
- ``simulator`` (or ``tool``): ``verilator`` (default) or ``icarus``.
|
|
40
|
+
- ``test_module`` (or ``test_modules``): one or more cocotb test modules, each a ``.py``
|
|
41
|
+
path (its directory goes on `PYTHONPATH`) or a dotted module name found in
|
|
42
|
+
``test_dir`` (default: the repo root).
|
|
43
|
+
- ``testcase``: a test name or a list of names to run (default: all).
|
|
44
|
+
- ``waves`` (bool): write ``dump.fst`` in the work root.
|
|
45
|
+
- ``seed`` (int, default 0; ``null`` for cocotb's random seed): `COCOTB_RANDOM_SEED`.
|
|
46
|
+
- ``timeout_s``: wall-time budget for the build and the run together.
|
|
47
|
+
- ``work_root``: ``state`` (default, ``.chipgraph/state/sim/<check>/<params>/<simulator>``,
|
|
48
|
+
machine-local and not tracked), ``temp`` (a new temporary directory) or a path outside
|
|
49
|
+
the project's source tree.
|
|
50
|
+
- ``env``: extra environment variables for the build and the run (the cocotb variables
|
|
51
|
+
above win over them).
|
|
52
|
+
- ``tool_options``: extra Edalize tool options (e.g. ``verilator_options``,
|
|
53
|
+
``iverilog_options``, ``run_options``, ``timescale``), merged into the defaults.
|
|
54
|
+
- ``jobs``: parallel jobs for the Verilator model's `make` (default: CPU count, max 8).
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
from __future__ import annotations
|
|
58
|
+
|
|
59
|
+
import hashlib
|
|
60
|
+
import importlib
|
|
61
|
+
import importlib.metadata
|
|
62
|
+
import json
|
|
63
|
+
import os
|
|
64
|
+
import re
|
|
65
|
+
import shutil
|
|
66
|
+
import sys
|
|
67
|
+
import tempfile
|
|
68
|
+
import time
|
|
69
|
+
from collections.abc import Callable, Mapping
|
|
70
|
+
from dataclasses import dataclass
|
|
71
|
+
from pathlib import Path
|
|
72
|
+
from typing import Any, cast
|
|
73
|
+
|
|
74
|
+
from chipgraph.adapters.parser.cocotb import CocotbOutcome, CocotbParser, Simulator
|
|
75
|
+
from chipgraph.adapters.tool.filelist import Filelist, FilelistError, read_filelist
|
|
76
|
+
from chipgraph.core.contracts import CheckResult, CheckSpec, Issue
|
|
77
|
+
from chipgraph.core.plugin_api.types import RunResult, ToolContext
|
|
78
|
+
from chipgraph.core.state.layout import StateLayout
|
|
79
|
+
|
|
80
|
+
__all__ = ["EdalizeTool", "SimArgs", "SimModules", "cocotb_env", "load_sim_modules"]
|
|
81
|
+
|
|
82
|
+
SIM_EXTRA_HINT = "uv sync --extra sim"
|
|
83
|
+
_SIMULATORS: tuple[Simulator, ...] = ("verilator", "icarus")
|
|
84
|
+
_EDAM_NAME = "chipgraph_sim"
|
|
85
|
+
_DUMP_TOP = "chipgraph_dump"
|
|
86
|
+
_RESULTS_FILE = "results.xml"
|
|
87
|
+
_WAVES_FILE = "dump.fst"
|
|
88
|
+
_DEFAULT_TIMESCALE = "1ns/1ps"
|
|
89
|
+
_CXX_CANDIDATES = ("g++", "c++", "clang++")
|
|
90
|
+
_LOG_TAIL_MAX_CHARS = 4000
|
|
91
|
+
_SLUG_RE = re.compile(r"[^A-Za-z0-9_.=-]+")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class _ArgError(Exception):
|
|
95
|
+
"""A malformed `spec.args`, turned into an `error` `CheckResult`."""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class _SetupError(Exception):
|
|
99
|
+
"""The environment cannot run the simulation (missing extra, tool, libpython ...)."""
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(frozen=True, slots=True)
|
|
103
|
+
class SimModules:
|
|
104
|
+
"""What the adapter needs from the `sim` extra, loaded lazily (see `load_sim_modules`)."""
|
|
105
|
+
|
|
106
|
+
sim_flow: Callable[[dict[str, Any], Path], Any]
|
|
107
|
+
"""`edalize.flows.sim.Sim`: ``Sim(edam, work_root)``."""
|
|
108
|
+
pygpi_entry_point: Callable[[], str]
|
|
109
|
+
"""`cocotb_tools.config.pygpi_entry_point`: the ``lib,func`` cocotb's GPI loads."""
|
|
110
|
+
find_libpython: Callable[[], str | None]
|
|
111
|
+
"""`find_libpython.find_libpython`: the shared libpython of this interpreter."""
|
|
112
|
+
versions: Mapping[str, str]
|
|
113
|
+
"""Installed versions of edalize and cocotb (part of the idempotency key)."""
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def load_sim_modules() -> SimModules:
|
|
117
|
+
"""Import Edalize and cocotb (the `sim` extra); `_SetupError` if they are missing."""
|
|
118
|
+
try:
|
|
119
|
+
sim = importlib.import_module("edalize.flows.sim")
|
|
120
|
+
cocotb_config = importlib.import_module("cocotb_tools.config")
|
|
121
|
+
find_libpython = importlib.import_module("find_libpython")
|
|
122
|
+
except ImportError as exc:
|
|
123
|
+
raise _SetupError(
|
|
124
|
+
f"the `sim` extra is not installed ({exc.name or exc}): run `{SIM_EXTRA_HINT}` "
|
|
125
|
+
"(or `pip install 'chipgraph[sim]'`) to install edalize and cocotb"
|
|
126
|
+
) from exc
|
|
127
|
+
return SimModules(
|
|
128
|
+
sim_flow=sim.Sim,
|
|
129
|
+
pygpi_entry_point=cocotb_config.pygpi_entry_point,
|
|
130
|
+
find_libpython=find_libpython.find_libpython,
|
|
131
|
+
versions=_package_versions(),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass(frozen=True, slots=True)
|
|
136
|
+
class SimArgs:
|
|
137
|
+
"""`spec.args`, validated and resolved against the repo root and `ctx.params`."""
|
|
138
|
+
|
|
139
|
+
simulator: Simulator
|
|
140
|
+
top: str
|
|
141
|
+
sources: tuple[Path, ...]
|
|
142
|
+
incdirs: tuple[Path, ...]
|
|
143
|
+
defines: dict[str, Any]
|
|
144
|
+
parameters: dict[str, Any]
|
|
145
|
+
plusargs: dict[str, Any]
|
|
146
|
+
test_modules: tuple[str, ...]
|
|
147
|
+
test_dirs: tuple[Path, ...]
|
|
148
|
+
testcase: tuple[str, ...]
|
|
149
|
+
waves: bool
|
|
150
|
+
seed: int | None
|
|
151
|
+
timeout_s: float | None
|
|
152
|
+
work_root: str
|
|
153
|
+
env: dict[str, str]
|
|
154
|
+
tool_options: dict[str, Any]
|
|
155
|
+
jobs: int
|
|
156
|
+
warnings: tuple[str, ...] = ()
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def from_spec(cls, spec: CheckSpec, ctx: ToolContext) -> SimArgs:
|
|
160
|
+
"""Validate `spec.args`; raises `_ArgError` with a message naming the bad key."""
|
|
161
|
+
args = spec.args
|
|
162
|
+
root = ctx.repo_root.resolve()
|
|
163
|
+
|
|
164
|
+
def fmt(value: object, key: str) -> str:
|
|
165
|
+
if not isinstance(value, str):
|
|
166
|
+
raise _ArgError(f"args[{key!r}] must be a string, got {value!r}")
|
|
167
|
+
try:
|
|
168
|
+
return value.format(**ctx.params)
|
|
169
|
+
except (KeyError, IndexError, ValueError) as exc:
|
|
170
|
+
raise _ArgError(f"args[{key!r}] = {value!r}: cannot substitute {exc}") from exc
|
|
171
|
+
|
|
172
|
+
def str_list(key: str) -> list[str]:
|
|
173
|
+
raw = args.get(key)
|
|
174
|
+
if raw is None:
|
|
175
|
+
return []
|
|
176
|
+
items = [raw] if isinstance(raw, str) else raw
|
|
177
|
+
if not isinstance(items, list):
|
|
178
|
+
raise _ArgError(f"args[{key!r}] must be a string or a list of strings")
|
|
179
|
+
return [fmt(item, key) for item in items]
|
|
180
|
+
|
|
181
|
+
def mapping(key: str) -> dict[str, Any]:
|
|
182
|
+
raw = args.get(key) or {}
|
|
183
|
+
if not isinstance(raw, dict):
|
|
184
|
+
raise _ArgError(f"args[{key!r}] must be a mapping")
|
|
185
|
+
return {str(k): v for k, v in raw.items()}
|
|
186
|
+
|
|
187
|
+
simulator = args.get("simulator", args.get("tool", "verilator"))
|
|
188
|
+
if simulator not in _SIMULATORS:
|
|
189
|
+
raise _ArgError(f"args['simulator'] must be one of {list(_SIMULATORS)}: {simulator!r}")
|
|
190
|
+
if "top" not in args:
|
|
191
|
+
raise _ArgError("args['top'] (the HDL toplevel) is required")
|
|
192
|
+
top = fmt(args["top"], "top")
|
|
193
|
+
|
|
194
|
+
sources: list[Path] = []
|
|
195
|
+
incdirs: list[Path] = []
|
|
196
|
+
defines: dict[str, Any] = {}
|
|
197
|
+
warnings: list[str] = []
|
|
198
|
+
if args.get("filelist") is not None:
|
|
199
|
+
style = args.get("filelist_relative_to", "auto")
|
|
200
|
+
if style not in ("auto", "filelist", "cwd"):
|
|
201
|
+
raise _ArgError("args['filelist_relative_to'] must be auto, filelist or cwd")
|
|
202
|
+
filelist = _read_filelist(root / fmt(args["filelist"], "filelist"), root, style)
|
|
203
|
+
sources += filelist.sources
|
|
204
|
+
incdirs += filelist.incdirs
|
|
205
|
+
defines.update({k: "" if v is None else v for k, v in filelist.defines.items()})
|
|
206
|
+
warnings += filelist.warnings
|
|
207
|
+
sources += [(root / f).resolve() for f in str_list("files")]
|
|
208
|
+
if not sources:
|
|
209
|
+
raise _ArgError("no HDL source: set args['filelist'] and/or args['files']")
|
|
210
|
+
missing = [str(p) for p in sources if not p.is_file()]
|
|
211
|
+
if missing:
|
|
212
|
+
raise _ArgError(f"HDL source(s) not found: {', '.join(missing)}")
|
|
213
|
+
defines.update(mapping("defines"))
|
|
214
|
+
|
|
215
|
+
modules: list[str] = []
|
|
216
|
+
test_dirs: list[Path] = []
|
|
217
|
+
default_dir = (root / fmt(args.get("test_dir", "."), "test_dir")).resolve()
|
|
218
|
+
for raw in str_list("test_module") + str_list("test_modules"):
|
|
219
|
+
name, directory = _resolve_test_module(raw, root, default_dir)
|
|
220
|
+
modules.append(name)
|
|
221
|
+
if directory not in test_dirs:
|
|
222
|
+
test_dirs.append(directory)
|
|
223
|
+
if not modules:
|
|
224
|
+
raise _ArgError("args['test_module'] (the cocotb test module) is required")
|
|
225
|
+
|
|
226
|
+
seed = args.get("seed", 0)
|
|
227
|
+
if seed is not None and (isinstance(seed, bool) or not isinstance(seed, int)):
|
|
228
|
+
raise _ArgError("args['seed'] must be an integer or null")
|
|
229
|
+
timeout_s = args.get("timeout_s")
|
|
230
|
+
if timeout_s is not None and (
|
|
231
|
+
isinstance(timeout_s, bool) or not isinstance(timeout_s, int | float) or timeout_s <= 0
|
|
232
|
+
):
|
|
233
|
+
raise _ArgError("args['timeout_s'] must be a positive number")
|
|
234
|
+
jobs = args.get("jobs", min(os.cpu_count() or 1, 8))
|
|
235
|
+
if isinstance(jobs, bool) or not isinstance(jobs, int) or jobs < 1:
|
|
236
|
+
raise _ArgError("args['jobs'] must be a positive integer")
|
|
237
|
+
env = {str(k): str(v) for k, v in mapping("env").items()}
|
|
238
|
+
|
|
239
|
+
return cls(
|
|
240
|
+
simulator=cast(Simulator, simulator),
|
|
241
|
+
top=top,
|
|
242
|
+
sources=tuple(dict.fromkeys(sources)),
|
|
243
|
+
incdirs=tuple(dict.fromkeys(incdirs)),
|
|
244
|
+
defines=defines,
|
|
245
|
+
parameters=mapping("parameters"),
|
|
246
|
+
plusargs=mapping("plusargs"),
|
|
247
|
+
test_modules=tuple(dict.fromkeys(modules)),
|
|
248
|
+
test_dirs=tuple(test_dirs),
|
|
249
|
+
testcase=tuple(str_list("testcase")),
|
|
250
|
+
waves=bool(args.get("waves", False)),
|
|
251
|
+
seed=seed,
|
|
252
|
+
timeout_s=float(timeout_s) if timeout_s is not None else None,
|
|
253
|
+
work_root=fmt(args.get("work_root", "state"), "work_root"),
|
|
254
|
+
env=env,
|
|
255
|
+
tool_options=mapping("tool_options"),
|
|
256
|
+
jobs=jobs,
|
|
257
|
+
warnings=tuple(warnings),
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def cocotb_env(
|
|
262
|
+
*,
|
|
263
|
+
simulator: Simulator,
|
|
264
|
+
top: str,
|
|
265
|
+
test_modules: tuple[str, ...],
|
|
266
|
+
test_dirs: tuple[Path, ...],
|
|
267
|
+
results_file: Path,
|
|
268
|
+
python: str,
|
|
269
|
+
libpython: str,
|
|
270
|
+
pygpi_entry_point: str,
|
|
271
|
+
path: str,
|
|
272
|
+
pythonpath: str = "",
|
|
273
|
+
seed: int | None = 0,
|
|
274
|
+
testcase: tuple[str, ...] = (),
|
|
275
|
+
) -> dict[str, str]:
|
|
276
|
+
"""The environment cocotb 2.x needs at build and run time, computed in process.
|
|
277
|
+
|
|
278
|
+
It mirrors what `cocotb_tools.runner` sets (S3, pitfall 1): `GPI_USERS` is the one
|
|
279
|
+
cocotb's GPI reads (without it the model prints "No GPI_USERS specified" and exits 0);
|
|
280
|
+
`PYGPI_PYTHON_BIN` is this interpreter; the test directories, then `pythonpath`, then
|
|
281
|
+
this interpreter's `sys.path` make up `PYTHONPATH`. `PATH` gets the interpreter's
|
|
282
|
+
`bin` first, because Edalize's Makefile calls `cocotb-config` through the shell and
|
|
283
|
+
another one (OSS CAD Suite has its own) must not shadow it.
|
|
284
|
+
"""
|
|
285
|
+
py_path = [str(d) for d in test_dirs]
|
|
286
|
+
py_path += [p for p in pythonpath.split(os.pathsep) if p]
|
|
287
|
+
py_path += [p for p in sys.path[1:] if p]
|
|
288
|
+
env = {
|
|
289
|
+
"PATH": os.pathsep.join([str(Path(python).parent), path]),
|
|
290
|
+
"PYTHONPATH": os.pathsep.join(dict.fromkeys(py_path)),
|
|
291
|
+
"PYGPI_PYTHON_BIN": python,
|
|
292
|
+
"GPI_USERS": f"{libpython};{pygpi_entry_point}",
|
|
293
|
+
"COCOTB_TEST_MODULES": ",".join(test_modules),
|
|
294
|
+
"COCOTB_TOPLEVEL": top,
|
|
295
|
+
"TOPLEVEL_LANG": "verilog",
|
|
296
|
+
"COCOTB_RESULTS_FILE": str(results_file),
|
|
297
|
+
}
|
|
298
|
+
if seed is not None:
|
|
299
|
+
env["COCOTB_RANDOM_SEED"] = str(seed)
|
|
300
|
+
if testcase:
|
|
301
|
+
names = "|".join(re.escape(name) for name in testcase)
|
|
302
|
+
env["COCOTB_TEST_FILTER"] = rf"(^|\.)({names})$"
|
|
303
|
+
if simulator == "verilator":
|
|
304
|
+
env["COCOTB_TRUST_INERTIAL_WRITES"] = "1" # what cocotb's own Verilator runner sets
|
|
305
|
+
return env
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class EdalizeTool:
|
|
309
|
+
"""Runs a cocotb testbench on Verilator or Icarus (`capability="sim"`)."""
|
|
310
|
+
|
|
311
|
+
name = "edalize"
|
|
312
|
+
capability = "sim"
|
|
313
|
+
|
|
314
|
+
def __init__(self, *, modules: Callable[[], SimModules] | None = None) -> None:
|
|
315
|
+
self._load_modules = modules or load_sim_modules
|
|
316
|
+
self._parser = CocotbParser()
|
|
317
|
+
|
|
318
|
+
async def run(self, spec: CheckSpec, ctx: ToolContext) -> CheckResult:
|
|
319
|
+
start = time.monotonic()
|
|
320
|
+
key = self.key_for(spec, ctx)
|
|
321
|
+
|
|
322
|
+
def error(msg: str, rule: str, log: str = "") -> CheckResult:
|
|
323
|
+
return CheckResult(
|
|
324
|
+
check_id=spec.id,
|
|
325
|
+
status="error",
|
|
326
|
+
issues=(Issue(msg=msg, rule=rule, severity="error"),),
|
|
327
|
+
log_tail=log[-_LOG_TAIL_MAX_CHARS:],
|
|
328
|
+
duration_s=time.monotonic() - start,
|
|
329
|
+
idempotency_key=key,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
try:
|
|
333
|
+
args = SimArgs.from_spec(spec, ctx)
|
|
334
|
+
except _ArgError as exc:
|
|
335
|
+
return error(f"check {spec.id!r}: {exc}", "sim/args")
|
|
336
|
+
try:
|
|
337
|
+
modules = self._load_modules()
|
|
338
|
+
except _SetupError as exc:
|
|
339
|
+
return error(str(exc), "sim/setup")
|
|
340
|
+
|
|
341
|
+
base_env = {**os.environ, **ctx.env, **args.env}
|
|
342
|
+
path = base_env.get("PATH", "")
|
|
343
|
+
python = sys.executable
|
|
344
|
+
tools_path = os.pathsep.join([str(Path(python).parent), path])
|
|
345
|
+
try:
|
|
346
|
+
cxx = _check_tools(args.simulator, tools_path, base_env)
|
|
347
|
+
libpython = modules.find_libpython()
|
|
348
|
+
if not libpython:
|
|
349
|
+
raise _SetupError(
|
|
350
|
+
f"no shared libpython found for {python} (find_libpython): cocotb "
|
|
351
|
+
"embeds Python in the simulator and needs a Python built with a "
|
|
352
|
+
"shared library"
|
|
353
|
+
)
|
|
354
|
+
work_root = _work_root(args, spec, ctx)
|
|
355
|
+
except _SetupError as exc:
|
|
356
|
+
return error(str(exc), "sim/setup")
|
|
357
|
+
except _ArgError as exc:
|
|
358
|
+
return error(f"check {spec.id!r}: {exc}", "sim/args")
|
|
359
|
+
|
|
360
|
+
results = work_root / _RESULTS_FILE
|
|
361
|
+
env = {
|
|
362
|
+
**ctx.env,
|
|
363
|
+
**args.env,
|
|
364
|
+
**cocotb_env(
|
|
365
|
+
simulator=args.simulator,
|
|
366
|
+
top=args.top,
|
|
367
|
+
test_modules=args.test_modules,
|
|
368
|
+
test_dirs=args.test_dirs,
|
|
369
|
+
results_file=results,
|
|
370
|
+
python=python,
|
|
371
|
+
libpython=libpython,
|
|
372
|
+
pygpi_entry_point=modules.pygpi_entry_point(),
|
|
373
|
+
path=path,
|
|
374
|
+
pythonpath=base_env.get("PYTHONPATH", ""),
|
|
375
|
+
seed=args.seed,
|
|
376
|
+
testcase=args.testcase,
|
|
377
|
+
),
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
deadline = start + args.timeout_s if args.timeout_s is not None else None
|
|
381
|
+
|
|
382
|
+
def remaining() -> float | None:
|
|
383
|
+
return None if deadline is None else max(deadline - time.monotonic(), 1.0)
|
|
384
|
+
|
|
385
|
+
options = await _tool_options(args, ctx, cxx, work_root, env)
|
|
386
|
+
edam = _edam(args, work_root, options)
|
|
387
|
+
_reset_work_root(work_root, edam)
|
|
388
|
+
try:
|
|
389
|
+
flow = modules.sim_flow(edam, work_root)
|
|
390
|
+
flow.configure()
|
|
391
|
+
build_cmd, build_args = flow.build_runner.get_build_command()
|
|
392
|
+
run_cmd, run_args, run_cwd = flow.flow.get_node(args.simulator).inst.run()
|
|
393
|
+
except Exception as exc: # Edalize raises plain RuntimeError/ValueError/KeyError ...
|
|
394
|
+
return error(
|
|
395
|
+
f"Edalize could not configure the {args.simulator} flow: {exc}", "sim/setup"
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
build = await ctx.runner.run(
|
|
399
|
+
[build_cmd, *build_args], cwd=work_root, env=env, timeout_s=remaining()
|
|
400
|
+
)
|
|
401
|
+
build_log = build.stdout + build.stderr
|
|
402
|
+
_write(work_root / "build.log", build_log)
|
|
403
|
+
|
|
404
|
+
run: RunResult | None = None
|
|
405
|
+
run_log = ""
|
|
406
|
+
if build.returncode == 0 and not build.timed_out:
|
|
407
|
+
run_cwd = Path(run_cwd)
|
|
408
|
+
run_exe = str(run_cwd / run_cmd) if str(run_cmd).startswith("./") else str(run_cmd)
|
|
409
|
+
run = await ctx.runner.run(
|
|
410
|
+
[run_exe, *run_args], cwd=run_cwd, env=env, timeout_s=remaining()
|
|
411
|
+
)
|
|
412
|
+
run_log = run.stdout + run.stderr
|
|
413
|
+
_write(work_root / "run.log", run_log)
|
|
414
|
+
|
|
415
|
+
outcome = self._parser.parse_run(
|
|
416
|
+
results,
|
|
417
|
+
simulator=args.simulator,
|
|
418
|
+
build_rc=build.returncode,
|
|
419
|
+
build_log=build_log,
|
|
420
|
+
build_timed_out=build.timed_out,
|
|
421
|
+
run_rc=run.returncode if run is not None else None,
|
|
422
|
+
run_log=run_log,
|
|
423
|
+
run_timed_out=run.timed_out if run is not None else False,
|
|
424
|
+
)
|
|
425
|
+
warnings = tuple(
|
|
426
|
+
Issue(msg=w, rule="sim/filelist", severity="warning") for w in args.warnings
|
|
427
|
+
)
|
|
428
|
+
return CheckResult(
|
|
429
|
+
check_id=spec.id,
|
|
430
|
+
status=outcome.status,
|
|
431
|
+
issues=(*(_in_repo(i, ctx.repo_root) for i in outcome.issues), *warnings),
|
|
432
|
+
log_tail=_log_tail(build_log if run is None else run_log, outcome, args, work_root),
|
|
433
|
+
duration_s=time.monotonic() - start,
|
|
434
|
+
idempotency_key=key,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
@staticmethod
|
|
438
|
+
def key_for(spec: CheckSpec, ctx: ToolContext) -> str:
|
|
439
|
+
"""A stable idempotency key: the inputs that decide what the simulation does.
|
|
440
|
+
|
|
441
|
+
Hashes the check id, the resolved arguments, the sha256 of every HDL source and of
|
|
442
|
+
every `.py` file in the test directories, `ctx.env`/`ctx.params`, the Python
|
|
443
|
+
version, the installed edalize and cocotb versions, and the simulator's
|
|
444
|
+
executables (resolved path, size, mtime). Not the work root, the timeout or any
|
|
445
|
+
output. Never raises: with invalid args, it keys on the raw `spec.args`.
|
|
446
|
+
"""
|
|
447
|
+
try:
|
|
448
|
+
args = SimArgs.from_spec(spec, ctx)
|
|
449
|
+
except _ArgError:
|
|
450
|
+
raw = json.dumps(spec.args, sort_keys=True, default=str)
|
|
451
|
+
return _sha256_text(repr((spec.id, "invalid-args", raw)))
|
|
452
|
+
root = ctx.repo_root.resolve()
|
|
453
|
+
test_files = sorted(
|
|
454
|
+
{p.resolve() for d in args.test_dirs if d.is_dir() for p in d.glob("*.py")}
|
|
455
|
+
)
|
|
456
|
+
path = {**os.environ, **ctx.env, **args.env}.get("PATH", "")
|
|
457
|
+
tools = _REQUIRED_TOOLS[args.simulator]
|
|
458
|
+
payload = {
|
|
459
|
+
"check": spec.id,
|
|
460
|
+
"simulator": args.simulator,
|
|
461
|
+
"top": args.top,
|
|
462
|
+
"sources": [[_relative(str(p), root), _sha256_file(p)] for p in args.sources],
|
|
463
|
+
"tests": [[_relative(str(p), root), _sha256_file(p)] for p in test_files],
|
|
464
|
+
"incdirs": [_relative(str(p), root) for p in args.incdirs],
|
|
465
|
+
"test_modules": list(args.test_modules),
|
|
466
|
+
"testcase": list(args.testcase),
|
|
467
|
+
"defines": args.defines,
|
|
468
|
+
"parameters": args.parameters,
|
|
469
|
+
"plusargs": args.plusargs,
|
|
470
|
+
"waves": args.waves,
|
|
471
|
+
"seed": args.seed,
|
|
472
|
+
"env": args.env,
|
|
473
|
+
"tool_options": args.tool_options,
|
|
474
|
+
"ctx_env": ctx.env,
|
|
475
|
+
"ctx_params": ctx.params,
|
|
476
|
+
"python": list(sys.version_info[:3]),
|
|
477
|
+
"packages": dict(_package_versions()),
|
|
478
|
+
"executables": {t: _fingerprint(t, path) for t in tools},
|
|
479
|
+
}
|
|
480
|
+
return _sha256_text(json.dumps(payload, sort_keys=True, default=str))
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
_REQUIRED_TOOLS: dict[Simulator, tuple[str, ...]] = {
|
|
484
|
+
"verilator": ("verilator", "make"),
|
|
485
|
+
"icarus": ("iverilog", "vvp", "make"),
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _check_tools(simulator: Simulator, path: str, env: Mapping[str, str]) -> str | None:
|
|
490
|
+
"""Raise `_SetupError` naming what is missing; return the C++ compiler (Verilator)."""
|
|
491
|
+
missing = [t for t in _REQUIRED_TOOLS[simulator] if shutil.which(t, path=path) is None]
|
|
492
|
+
cxx: str | None = None
|
|
493
|
+
if simulator == "verilator":
|
|
494
|
+
wanted = env.get("CXX")
|
|
495
|
+
candidates = (wanted,) if wanted else _CXX_CANDIDATES
|
|
496
|
+
cxx = next((c for c in candidates if shutil.which(c, path=path)), None)
|
|
497
|
+
if cxx is None:
|
|
498
|
+
missing.append(f"a C++ compiler ({wanted or 'g++'})")
|
|
499
|
+
if missing:
|
|
500
|
+
hint = (
|
|
501
|
+
"Verilator compiles the model with a C++ compiler (e.g. `apt-get install g++`)"
|
|
502
|
+
if simulator == "verilator"
|
|
503
|
+
else "install Icarus Verilog from the distribution (e.g. `apt-get install iverilog`)"
|
|
504
|
+
)
|
|
505
|
+
raise _SetupError(f"{simulator} simulation needs {', '.join(missing)} on PATH: {hint}")
|
|
506
|
+
return cxx
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
async def _tool_options(
|
|
510
|
+
args: SimArgs, ctx: ToolContext, cxx: str | None, work_root: Path, env: Mapping[str, str]
|
|
511
|
+
) -> dict[str, Any]:
|
|
512
|
+
"""Per-simulator Edalize options, with the S3 fixes as detected on this machine."""
|
|
513
|
+
opts: dict[str, Any]
|
|
514
|
+
if args.simulator == "verilator":
|
|
515
|
+
verilator_options = await _cxx_std_args(ctx, cxx, work_root, env)
|
|
516
|
+
verilator_options += [f"+incdir+{d}" for d in args.incdirs]
|
|
517
|
+
run_options: list[str] = []
|
|
518
|
+
if args.waves:
|
|
519
|
+
verilator_options += ["--trace-fst", *_fst_lib_args()]
|
|
520
|
+
run_options.append("--trace")
|
|
521
|
+
opts = {
|
|
522
|
+
"verilator_options": verilator_options,
|
|
523
|
+
"run_options": run_options,
|
|
524
|
+
"make_options": [f"-j{args.jobs}"],
|
|
525
|
+
}
|
|
526
|
+
else:
|
|
527
|
+
# tinysoc-style SystemVerilog (logic, always_ff) needs -g2012, and a timescale so
|
|
528
|
+
# a 10 ns cocotb clock is representable (Icarus defaults to 1 s): S3, pitfall 8.
|
|
529
|
+
opts = {
|
|
530
|
+
"iverilog_options": ["-g2012", *(f"-I{d}" for d in args.incdirs)],
|
|
531
|
+
"timescale": _DEFAULT_TIMESCALE,
|
|
532
|
+
}
|
|
533
|
+
for key, value in args.tool_options.items():
|
|
534
|
+
if isinstance(value, list) and isinstance(opts.get(key), list):
|
|
535
|
+
opts[key] = [*opts[key], *(str(v) for v in value)]
|
|
536
|
+
else:
|
|
537
|
+
opts[key] = value
|
|
538
|
+
return opts
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
async def _cxx_std_args(
|
|
542
|
+
ctx: ToolContext, cxx: str | None, work_root: Path, env: Mapping[str, str]
|
|
543
|
+
) -> list[str]:
|
|
544
|
+
"""``-CFLAGS -std=gnu++17`` when the C++ compiler defaults below C++14 (S3, pitfall 6).
|
|
545
|
+
|
|
546
|
+
Verilator 5.05x needs C++14 and adds a ``-std=`` flag only if the compiler it was
|
|
547
|
+
built with needed one; old Apple Command Line Tools default to C++98.
|
|
548
|
+
"""
|
|
549
|
+
if cxx is None:
|
|
550
|
+
return []
|
|
551
|
+
probe = await ctx.runner.run(
|
|
552
|
+
[cxx, "-dM", "-E", "-x", "c++", os.devnull], cwd=work_root, env=dict(env), timeout_s=30
|
|
553
|
+
)
|
|
554
|
+
for line in probe.stdout.splitlines():
|
|
555
|
+
if line.startswith("#define __cplusplus "):
|
|
556
|
+
try:
|
|
557
|
+
value = int(line.split()[2].rstrip("L"))
|
|
558
|
+
except (IndexError, ValueError):
|
|
559
|
+
return []
|
|
560
|
+
return ["-CFLAGS", "-std=gnu++17"] if value < 201402 else []
|
|
561
|
+
return []
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def _fst_lib_args() -> list[str]:
|
|
565
|
+
"""Where Verilator's FST writer finds lz4/zlib on macOS with Homebrew (S3, pitfall 7).
|
|
566
|
+
|
|
567
|
+
On Linux the headers come from the distribution (`liblz4-dev`, `zlib1g-dev`) on the
|
|
568
|
+
default paths; Apple clang does not search the Homebrew prefix.
|
|
569
|
+
"""
|
|
570
|
+
if sys.platform != "darwin":
|
|
571
|
+
return []
|
|
572
|
+
for prefix in (os.environ.get("HOMEBREW_PREFIX", ""), "/opt/homebrew", "/usr/local"):
|
|
573
|
+
if prefix and (Path(prefix) / "include" / "lz4.h").is_file():
|
|
574
|
+
return ["-CFLAGS", f"-I{prefix}/include", "-LDFLAGS", f"-L{prefix}/lib"]
|
|
575
|
+
return []
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _edam(args: SimArgs, work_root: Path, options: Mapping[str, Any]) -> dict[str, Any]:
|
|
579
|
+
"""The EDAM for Edalize's `sim` flow; `cocotb_module` turns its cocotb glue on."""
|
|
580
|
+
files = [{"name": str(p), "file_type": _file_type(p)} for p in args.sources]
|
|
581
|
+
toplevel = args.top
|
|
582
|
+
if args.simulator == "icarus" and args.waves:
|
|
583
|
+
# Edalize already runs `vvp ... -fst`; a second top with $dumpvars does the rest.
|
|
584
|
+
dump = work_root / f"{_DUMP_TOP}.v"
|
|
585
|
+
timescale = str(options.get("timescale", _DEFAULT_TIMESCALE))
|
|
586
|
+
_write(
|
|
587
|
+
dump,
|
|
588
|
+
f"`timescale {timescale.replace('/', ' / ')}\nmodule {_DUMP_TOP};\n"
|
|
589
|
+
f' initial begin\n $dumpfile("{_WAVES_FILE}");\n'
|
|
590
|
+
f" $dumpvars(0, {args.top});\n end\nendmodule\n",
|
|
591
|
+
)
|
|
592
|
+
files.append({"name": str(dump), "file_type": "verilogSource"})
|
|
593
|
+
toplevel = f"{args.top} {_DUMP_TOP}"
|
|
594
|
+
parameters: dict[str, Any] = {}
|
|
595
|
+
for kind, values in (
|
|
596
|
+
("vlogparam", args.parameters),
|
|
597
|
+
("vlogdefine", args.defines),
|
|
598
|
+
("plusarg", args.plusargs),
|
|
599
|
+
):
|
|
600
|
+
for name, value in values.items():
|
|
601
|
+
parameters[name] = {"datatype": _datatype(value), "paramtype": kind, "default": value}
|
|
602
|
+
return {
|
|
603
|
+
"name": _EDAM_NAME,
|
|
604
|
+
"toplevel": toplevel,
|
|
605
|
+
"files": files,
|
|
606
|
+
"parameters": parameters,
|
|
607
|
+
"flow_options": {
|
|
608
|
+
"tool": args.simulator,
|
|
609
|
+
"cocotb_module": args.test_modules[0],
|
|
610
|
+
**options,
|
|
611
|
+
},
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def _work_root(args: SimArgs, spec: CheckSpec, ctx: ToolContext) -> Path:
|
|
616
|
+
"""The work root: under the run state, a new temp dir, or a path outside the sources."""
|
|
617
|
+
repo = ctx.repo_root.resolve()
|
|
618
|
+
state_dir = StateLayout(repo).state_dir
|
|
619
|
+
if args.work_root == "state":
|
|
620
|
+
params = ",".join(f"{k}={v}" for k, v in sorted(ctx.params.items())) or "_"
|
|
621
|
+
path = state_dir / "sim" / _slug(spec.id) / _slug(params) / args.simulator
|
|
622
|
+
elif args.work_root == "temp":
|
|
623
|
+
path = Path(tempfile.mkdtemp(prefix=f"chipgraph-sim-{_slug(spec.id)}-"))
|
|
624
|
+
else:
|
|
625
|
+
path = Path(args.work_root)
|
|
626
|
+
path = (path if path.is_absolute() else repo / path).resolve()
|
|
627
|
+
if path.is_relative_to(repo) and not path.is_relative_to(state_dir.resolve()):
|
|
628
|
+
raise _ArgError(
|
|
629
|
+
f"args['work_root'] {args.work_root!r} is inside the project's source tree; "
|
|
630
|
+
"use `state`, `temp` or a directory outside the repo"
|
|
631
|
+
)
|
|
632
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
633
|
+
return path.resolve()
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def _reset_work_root(work_root: Path, edam: Mapping[str, Any]) -> None:
|
|
637
|
+
"""Remove stale outputs; wipe the build when the EDAM changed since the last run.
|
|
638
|
+
|
|
639
|
+
`results.xml` and `dump.fst` are always removed first, so an old result can never be
|
|
640
|
+
read as this run's.
|
|
641
|
+
"""
|
|
642
|
+
for name in (_RESULTS_FILE, _WAVES_FILE, "build.log", "run.log"):
|
|
643
|
+
(work_root / name).unlink(missing_ok=True)
|
|
644
|
+
stamp = work_root / "edam.json"
|
|
645
|
+
text = json.dumps(edam, indent=2, sort_keys=True, default=str) + "\n"
|
|
646
|
+
old = stamp.read_text(encoding="utf-8") if stamp.is_file() else None
|
|
647
|
+
if old is not None and old != text:
|
|
648
|
+
for child in work_root.iterdir():
|
|
649
|
+
if child.is_dir() and not child.is_symlink():
|
|
650
|
+
shutil.rmtree(child)
|
|
651
|
+
elif child.name != f"{_DUMP_TOP}.v":
|
|
652
|
+
child.unlink()
|
|
653
|
+
_write(stamp, text)
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def _read_filelist(path: Path, root: Path, style: str) -> Filelist:
|
|
657
|
+
try:
|
|
658
|
+
if style in ("filelist", "cwd"):
|
|
659
|
+
return read_filelist(path, relative_to=cast(Any, style), root=root)
|
|
660
|
+
first = read_filelist(path, relative_to="filelist", root=root)
|
|
661
|
+
if first.sources and all(p.is_file() for p in first.sources):
|
|
662
|
+
return first
|
|
663
|
+
alt = read_filelist(path, relative_to="cwd", root=root)
|
|
664
|
+
if sum(p.is_file() for p in alt.sources) > sum(p.is_file() for p in first.sources):
|
|
665
|
+
return alt
|
|
666
|
+
return first
|
|
667
|
+
except FilelistError as exc:
|
|
668
|
+
raise _ArgError(str(exc)) from exc
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def _resolve_test_module(raw: str, root: Path, default_dir: Path) -> tuple[str, Path]:
|
|
672
|
+
"""(module name, directory for PYTHONPATH) of a `.py` path or a dotted module name."""
|
|
673
|
+
if raw.endswith(".py") or "/" in raw:
|
|
674
|
+
file = (root / raw).resolve()
|
|
675
|
+
if not file.is_file():
|
|
676
|
+
raise _ArgError(f"cocotb test module not found: {raw}")
|
|
677
|
+
return file.stem, file.parent
|
|
678
|
+
if not re.fullmatch(r"[A-Za-z_]\w*(\.[A-Za-z_]\w*)*", raw):
|
|
679
|
+
raise _ArgError(f"invalid cocotb test module name: {raw!r}")
|
|
680
|
+
rel = Path(*raw.split("."))
|
|
681
|
+
if not ((default_dir / rel).with_suffix(".py").is_file() or (default_dir / rel).is_dir()):
|
|
682
|
+
raise _ArgError(f"cocotb test module {raw!r} not found in {default_dir}")
|
|
683
|
+
return raw, default_dir
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def _log_tail(log: str, outcome: CocotbOutcome, args: SimArgs, work_root: Path) -> str:
|
|
687
|
+
footer = (
|
|
688
|
+
f"\n--- chipgraph sim ({args.simulator}, top {args.top}): {outcome.status}, "
|
|
689
|
+
f"{outcome.summary}; work root {work_root}\n"
|
|
690
|
+
)
|
|
691
|
+
return log[-(_LOG_TAIL_MAX_CHARS - len(footer)) :] + footer
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def _in_repo(issue: Issue, root: Path) -> Issue:
|
|
695
|
+
"""`issue` with a repo-relative `file`; a file outside the repo moves into the message.
|
|
696
|
+
|
|
697
|
+
Findings (and their evidence) only take repo-relative paths, and a test module or a
|
|
698
|
+
source may live outside the project (e.g. a shared testbench).
|
|
699
|
+
"""
|
|
700
|
+
if issue.file is None:
|
|
701
|
+
return issue
|
|
702
|
+
rel = _relative(issue.file, root)
|
|
703
|
+
if not Path(rel).is_absolute():
|
|
704
|
+
return issue.model_copy(update={"file": rel})
|
|
705
|
+
where = f"{issue.file}:{issue.line}" if issue.line is not None else issue.file
|
|
706
|
+
return issue.model_copy(update={"file": None, "line": None, "msg": f"{issue.msg} (at {where})"})
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def _relative(path: str, root: Path) -> str:
|
|
710
|
+
"""`path` relative to `root` (a relative `path` is taken from `root`); else absolute."""
|
|
711
|
+
full = Path(path) if Path(path).is_absolute() else root / path
|
|
712
|
+
try:
|
|
713
|
+
return full.resolve().relative_to(root.resolve()).as_posix()
|
|
714
|
+
except ValueError:
|
|
715
|
+
return str(full.resolve())
|
|
716
|
+
except OSError:
|
|
717
|
+
return str(full)
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def _file_type(path: Path) -> str:
|
|
721
|
+
return "verilogSource" if path.suffix in (".v", ".vh") else "systemVerilogSource"
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def _datatype(value: object) -> str:
|
|
725
|
+
if isinstance(value, bool):
|
|
726
|
+
return "bool"
|
|
727
|
+
if isinstance(value, int):
|
|
728
|
+
return "int"
|
|
729
|
+
if isinstance(value, float):
|
|
730
|
+
return "real"
|
|
731
|
+
return "str"
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def _slug(text: str) -> str:
|
|
735
|
+
return _SLUG_RE.sub("_", text).strip("_") or "_"
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def _write(path: Path, text: str) -> None:
|
|
739
|
+
path.write_text(text, encoding="utf-8")
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def _sha256_text(text: str) -> str:
|
|
743
|
+
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def _sha256_file(path: Path) -> str:
|
|
747
|
+
try:
|
|
748
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
749
|
+
except OSError:
|
|
750
|
+
return "unreadable"
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def _fingerprint(tool: str, path: str) -> list[object]:
|
|
754
|
+
"""(resolved path, size, mtime_ns) of `tool` on `path`: changes when it is upgraded."""
|
|
755
|
+
found = shutil.which(tool, path=path)
|
|
756
|
+
if found is None:
|
|
757
|
+
return ["missing"]
|
|
758
|
+
real = Path(found).resolve()
|
|
759
|
+
try:
|
|
760
|
+
st = real.stat()
|
|
761
|
+
except OSError:
|
|
762
|
+
return [str(real)]
|
|
763
|
+
return [str(real), st.st_size, st.st_mtime_ns]
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def _package_versions() -> dict[str, str]:
|
|
767
|
+
versions: dict[str, str] = {}
|
|
768
|
+
for dist in ("edalize", "cocotb"):
|
|
769
|
+
try:
|
|
770
|
+
versions[dist] = importlib.metadata.version(dist)
|
|
771
|
+
except importlib.metadata.PackageNotFoundError:
|
|
772
|
+
versions[dist] = "missing"
|
|
773
|
+
return versions
|