rocket-engine 0.0.2b1__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.
- rocket_engine/__init__.py +8 -0
- rocket_engine/__main__.py +12 -0
- rocket_engine/adapters/README.md +54 -0
- rocket_engine/adapters/__init__.py +7 -0
- rocket_engine/adapters/api.py +39 -0
- rocket_engine/adapters/base.py +46 -0
- rocket_engine/adapters/cace/__init__.py +163 -0
- rocket_engine/adapters/circulax/README.md +10 -0
- rocket_engine/adapters/circulax/__init__.py +112 -0
- rocket_engine/adapters/circulax/_driver.py +190 -0
- rocket_engine/adapters/circulax/adapter.py +538 -0
- rocket_engine/adapters/circulax/results.py +291 -0
- rocket_engine/adapters/cli/README.md +34 -0
- rocket_engine/adapters/cli/__init__.py +305 -0
- rocket_engine/adapters/connectivity/__init__.py +196 -0
- rocket_engine/adapters/connectivity/results.py +214 -0
- rocket_engine/adapters/cosim/README.md +9 -0
- rocket_engine/adapters/cosim/__init__.py +1390 -0
- rocket_engine/adapters/cosim/results.py +214 -0
- rocket_engine/adapters/devsim/__init__.py +171 -0
- rocket_engine/adapters/digital_flow/README.md +9 -0
- rocket_engine/adapters/digital_flow/__init__.py +1121 -0
- rocket_engine/adapters/digital_flow/results.py +227 -0
- rocket_engine/adapters/digital_flow/state.py +462 -0
- rocket_engine/adapters/drc/README.klayout.md +7 -0
- rocket_engine/adapters/drc/README.md +7 -0
- rocket_engine/adapters/drc/__init__.py +1 -0
- rocket_engine/adapters/drc/klayout.py +952 -0
- rocket_engine/adapters/drc/magic.py +287 -0
- rocket_engine/adapters/drc/results.py +184 -0
- rocket_engine/adapters/drc/results_kl.py +188 -0
- rocket_engine/adapters/elmer/__init__.py +163 -0
- rocket_engine/adapters/envelope.py +120 -0
- rocket_engine/adapters/equivalence/__init__.py +156 -0
- rocket_engine/adapters/errors.py +181 -0
- rocket_engine/adapters/extract_magic/README.md +15 -0
- rocket_engine/adapters/extract_magic/__init__.py +425 -0
- rocket_engine/adapters/extract_magic/results.py +209 -0
- rocket_engine/adapters/formal_sby/README.md +7 -0
- rocket_engine/adapters/formal_sby/__init__.py +773 -0
- rocket_engine/adapters/formal_sby/formal_edge.py +195 -0
- rocket_engine/adapters/formal_sby/results.py +185 -0
- rocket_engine/adapters/gdsfactory/__init__.py +194 -0
- rocket_engine/adapters/gdsfactory/_driver.py +459 -0
- rocket_engine/adapters/gdsfactory/adapter.py +609 -0
- rocket_engine/adapters/gdsfactory/results.py +181 -0
- rocket_engine/adapters/hdl_sim/README.md +7 -0
- rocket_engine/adapters/hdl_sim/__init__.py +369 -0
- rocket_engine/adapters/hdl_sim/ghdl.py +155 -0
- rocket_engine/adapters/hdl_sim/results.py +178 -0
- rocket_engine/adapters/hdl_sim/verilator.py +167 -0
- rocket_engine/adapters/http/README.md +15 -0
- rocket_engine/adapters/http/__init__.py +458 -0
- rocket_engine/adapters/http/events.py +63 -0
- rocket_engine/adapters/ipc/README.md +9 -0
- rocket_engine/adapters/ipc/__init__.py +111 -0
- rocket_engine/adapters/lvs_netgen/README.md +20 -0
- rocket_engine/adapters/lvs_netgen/__init__.py +889 -0
- rocket_engine/adapters/lvs_netgen/results.py +168 -0
- rocket_engine/adapters/mcp/README.md +19 -0
- rocket_engine/adapters/mcp/__init__.py +85 -0
- rocket_engine/adapters/mcp/api-schema.json +6107 -0
- rocket_engine/adapters/mcp/pull.py +246 -0
- rocket_engine/adapters/mcp/server.py +794 -0
- rocket_engine/adapters/meep/__init__.py +167 -0
- rocket_engine/adapters/meow/README.md +7 -0
- rocket_engine/adapters/meow/__init__.py +190 -0
- rocket_engine/adapters/meow/_driver.py +227 -0
- rocket_engine/adapters/meow/_envelope.py +108 -0
- rocket_engine/adapters/meow/adapter.py +491 -0
- rocket_engine/adapters/meow/results.py +218 -0
- rocket_engine/adapters/mutation/__init__.py +192 -0
- rocket_engine/adapters/ngspice/README.md +28 -0
- rocket_engine/adapters/ngspice/__init__.py +627 -0
- rocket_engine/adapters/ngspice/results.py +215 -0
- rocket_engine/adapters/postlayout/README.md +28 -0
- rocket_engine/adapters/postlayout/__init__.py +17 -0
- rocket_engine/adapters/postlayout/adapter.py +1062 -0
- rocket_engine/adapters/postlayout/results.py +330 -0
- rocket_engine/adapters/postlayout/translate.py +71 -0
- rocket_engine/adapters/profile.py +220 -0
- rocket_engine/adapters/sax/README.md +23 -0
- rocket_engine/adapters/sax/__init__.py +516 -0
- rocket_engine/adapters/sax/_driver.py +148 -0
- rocket_engine/adapters/sax/results.py +291 -0
- rocket_engine/adapters/stdio_console.py +1151 -0
- rocket_engine/adapters/syntax/README.md +5 -0
- rocket_engine/adapters/syntax/__init__.py +366 -0
- rocket_engine/adapters/syntax/results.py +181 -0
- rocket_engine/adapters/synth_yosys/README.md +7 -0
- rocket_engine/adapters/synth_yosys/__init__.py +655 -0
- rocket_engine/adapters/synth_yosys/amaranth.py +120 -0
- rocket_engine/adapters/synth_yosys/ghdl.py +128 -0
- rocket_engine/adapters/synth_yosys/results.py +173 -0
- rocket_engine/adapters/timing_sta/README.md +9 -0
- rocket_engine/adapters/timing_sta/__init__.py +726 -0
- rocket_engine/adapters/view_sync/__init__.py +185 -0
- rocket_engine/adapters/view_sync/results.py +189 -0
- rocket_engine/adapters/vlsir/__init__.py +142 -0
- rocket_engine/adapters/xschem/__init__.py +440 -0
- rocket_engine/adapters/xschem/deck.py +266 -0
- rocket_engine/clients/__init__.py +5 -0
- rocket_engine/clients/runtime.py +255 -0
- rocket_engine/composition.py +2279 -0
- rocket_engine/execution/README.md +30 -0
- rocket_engine/execution/__init__.py +150 -0
- rocket_engine/execution/apig/__init__.py +1 -0
- rocket_engine/execution/apig/pool.py +463 -0
- rocket_engine/execution/apig/submit.py +124 -0
- rocket_engine/execution/block_runs_scope.py +479 -0
- rocket_engine/execution/coalesce.py +414 -0
- rocket_engine/execution/console/__init__.py +1 -0
- rocket_engine/execution/console/apply_session.py +318 -0
- rocket_engine/execution/console/lifecycle.py +265 -0
- rocket_engine/execution/dispatch.py +205 -0
- rocket_engine/execution/errors.py +31 -0
- rocket_engine/execution/ports.py +797 -0
- rocket_engine/execution/pyexec/__init__.py +1 -0
- rocket_engine/execution/pyexec/apply_context.py +547 -0
- rocket_engine/execution/pyexec/approvals.py +173 -0
- rocket_engine/execution/pyexec/runner.py +404 -0
- rocket_engine/execution/pyexec/support.py +464 -0
- rocket_engine/execution/python_exec.py +897 -0
- rocket_engine/execution/queue.py +85 -0
- rocket_engine/execution/replay_safe.py +601 -0
- rocket_engine/execution/routing/__init__.py +1 -0
- rocket_engine/execution/routing/calls.py +364 -0
- rocket_engine/execution/routing/namespace.py +785 -0
- rocket_engine/execution/routing/ports.py +102 -0
- rocket_engine/execution/routing/tools.py +346 -0
- rocket_engine/execution/runner.py +311 -0
- rocket_engine/execution/runtime.py +1149 -0
- rocket_engine/execution/service.py +279 -0
- rocket_engine/execution/worker.py +464 -0
- rocket_engine/interfaces/README.md +26 -0
- rocket_engine/interfaces/__init__.py +58 -0
- rocket_engine/interfaces/apig_assets.py +407 -0
- rocket_engine/interfaces/celllink.py +461 -0
- rocket_engine/interfaces/documents.py +92 -0
- rocket_engine/interfaces/envelopes.py +131 -0
- rocket_engine/interfaces/errors.py +16 -0
- rocket_engine/interfaces/layout.py +410 -0
- rocket_engine/interfaces/layout_schema.yaml +278 -0
- rocket_engine/interfaces/paths.py +223 -0
- rocket_engine/interfaces/pdks.py +550 -0
- rocket_engine/interfaces/ports.py +72 -0
- rocket_engine/interfaces/references.py +115 -0
- rocket_engine/interfaces/refs.py +85 -0
- rocket_engine/interfaces/runparams.py +304 -0
- rocket_engine/interfaces/setup.py +552 -0
- rocket_engine/interfaces/setup_catalogue.yaml +243 -0
- rocket_engine/interfaces/setup_probes.py +66 -0
- rocket_engine/interfaces/vocabulary.py +58 -0
- rocket_engine/interfaces/yaml_dump.py +31 -0
- rocket_engine/library/README.md +22 -0
- rocket_engine/library/__init__.py +13 -0
- rocket_engine/library/apig_catalog.py +371 -0
- rocket_engine/library/catalog.py +185 -0
- rocket_engine/library/errors.py +16 -0
- rocket_engine/mutate/__init__.py +776 -0
- rocket_engine/mutate/operators.py +700 -0
- rocket_engine/ops/__init__.py +1 -0
- rocket_engine/ops/deferred.py +279 -0
- rocket_engine/ops/editors.py +404 -0
- rocket_engine/ops/evidence.py +194 -0
- rocket_engine/ops/gates.py +262 -0
- rocket_engine/ops/ports.py +76 -0
- rocket_engine/ops/queries.py +417 -0
- rocket_engine/ops/runs.py +553 -0
- rocket_engine/ops/submit.py +614 -0
- rocket_engine/project/README.md +27 -0
- rocket_engine/project/__init__.py +6 -0
- rocket_engine/project/canvas/__init__.py +55 -0
- rocket_engine/project/canvas/compile.py +109 -0
- rocket_engine/project/canvas/integrate.py +48 -0
- rocket_engine/project/canvas/schema.py +241 -0
- rocket_engine/project/canvas/templates.py +46 -0
- rocket_engine/project/canvas/validate.py +603 -0
- rocket_engine/project/edit/__init__.py +5 -0
- rocket_engine/project/edit/cas.py +87 -0
- rocket_engine/project/edit/transactions.py +282 -0
- rocket_engine/project/errors.py +16 -0
- rocket_engine/project/frontend13.py +685 -0
- rocket_engine/project/geometry/__init__.py +23 -0
- rocket_engine/project/geometry/ops.py +451 -0
- rocket_engine/project/geometry/snap.py +103 -0
- rocket_engine/project/ip/__init__.py +1 -0
- rocket_engine/project/ip/blocks.py +195 -0
- rocket_engine/project/ip/entry.schema.json +78 -0
- rocket_engine/project/ip/lifecycle.py +402 -0
- rocket_engine/project/ip/verbs.py +123 -0
- rocket_engine/project/layers/__init__.py +6 -0
- rocket_engine/project/layers/bindings.py +115 -0
- rocket_engine/project/layers/catalog.py +228 -0
- rocket_engine/project/layers/presets.py +43 -0
- rocket_engine/project/ledger/__init__.py +1 -0
- rocket_engine/project/ledger/docs.py +167 -0
- rocket_engine/project/ledger/entities.py +176 -0
- rocket_engine/project/ledger/events.py +29 -0
- rocket_engine/project/ledger/runs.py +257 -0
- rocket_engine/project/ledger/shared.py +174 -0
- rocket_engine/project/ledger/snapshots.py +385 -0
- rocket_engine/project/ledger/wires.py +552 -0
- rocket_engine/project/process/__init__.py +14 -0
- rocket_engine/project/process/model.py +193 -0
- rocket_engine/project/process/recipes.py +121 -0
- rocket_engine/project/schematic/__init__.py +1 -0
- rocket_engine/project/schematic/hierarchy.py +312 -0
- rocket_engine/project/schematic/instances.py +393 -0
- rocket_engine/project/schematic/sheets.py +198 -0
- rocket_engine/project/service.py +2941 -0
- rocket_engine/project/symbols/__init__.py +1 -0
- rocket_engine/project/symbols/cells.py +324 -0
- rocket_engine/project/symbols/linkage.py +356 -0
- rocket_engine/project/symbols/symbols.py +352 -0
- rocket_engine/project/templates/__init__.py +303 -0
- rocket_engine/project/templates/errors.py +25 -0
- rocket_engine/project/testbench/__init__.py +49 -0
- rocket_engine/project/testbench/blocks.py +619 -0
- rocket_engine/project/testbench/templates.py +134 -0
- rocket_engine/stores/README.md +12 -0
- rocket_engine/stores/__init__.py +6 -0
- rocket_engine/stores/ecs.py +1508 -0
- rocket_engine/stores/layers.py +1023 -0
- rocket_engine/stores/settings.py +1188 -0
- rocket_engine/stores/wireplace.py +1324 -0
- rocket_engine/workflows/README.md +18 -0
- rocket_engine/workflows/__init__.py +53 -0
- rocket_engine/workflows/capability_plan.py +102 -0
- rocket_engine/workflows/completion.py +228 -0
- rocket_engine/workflows/connectgraph.py +734 -0
- rocket_engine/workflows/connectgraph_emit.py +336 -0
- rocket_engine/workflows/gates.py +144 -0
- rocket_engine/workflows/photonic_wire.py +76 -0
- rocket_engine/workflows/stages.py +324 -0
- rocket_engine/workflows/viewsync.py +399 -0
- rocket_engine-0.0.2b1.dist-info/METADATA +200 -0
- rocket_engine-0.0.2b1.dist-info/RECORD +241 -0
- rocket_engine-0.0.2b1.dist-info/WHEEL +4 -0
- rocket_engine-0.0.2b1.dist-info/entry_points.txt +2 -0
- rocket_engine-0.0.2b1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Scoped testbench blocks with editable templates."""
|
|
2
|
+
|
|
3
|
+
from .blocks import (
|
|
4
|
+
SOURCE_PREFIX,
|
|
5
|
+
TESTBENCH_COMPONENT,
|
|
6
|
+
assert_runnable,
|
|
7
|
+
check_fresh,
|
|
8
|
+
create_block,
|
|
9
|
+
describe_block,
|
|
10
|
+
design_net_pins,
|
|
11
|
+
dut_digest,
|
|
12
|
+
excluded_harness_instances,
|
|
13
|
+
harness_instances,
|
|
14
|
+
materialize_interface,
|
|
15
|
+
normalize_probes,
|
|
16
|
+
normalize_targets,
|
|
17
|
+
probe_simulator,
|
|
18
|
+
regenerate_block,
|
|
19
|
+
)
|
|
20
|
+
from .templates import (
|
|
21
|
+
TEMPLATE_EXTENSIONS,
|
|
22
|
+
TEMPLATE_KINDS,
|
|
23
|
+
TEMPLATE_VERSION,
|
|
24
|
+
describe_template,
|
|
25
|
+
render,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"SOURCE_PREFIX",
|
|
30
|
+
"TESTBENCH_COMPONENT",
|
|
31
|
+
"TEMPLATE_EXTENSIONS",
|
|
32
|
+
"TEMPLATE_KINDS",
|
|
33
|
+
"TEMPLATE_VERSION",
|
|
34
|
+
"assert_runnable",
|
|
35
|
+
"check_fresh",
|
|
36
|
+
"create_block",
|
|
37
|
+
"describe_block",
|
|
38
|
+
"describe_template",
|
|
39
|
+
"design_net_pins",
|
|
40
|
+
"dut_digest",
|
|
41
|
+
"excluded_harness_instances",
|
|
42
|
+
"harness_instances",
|
|
43
|
+
"materialize_interface",
|
|
44
|
+
"normalize_probes",
|
|
45
|
+
"normalize_targets",
|
|
46
|
+
"probe_simulator",
|
|
47
|
+
"regenerate_block",
|
|
48
|
+
"render",
|
|
49
|
+
]
|
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
"""Scoped testbench blocks over the ENGINE-ECS entity seam.
|
|
2
|
+
|
|
3
|
+
One block names a device under test (a primary circuit-instance
|
|
4
|
+
occurrence plus explicit nonoverlapping additional targets),
|
|
5
|
+
probes inside the DUT closure, an explicit harness binding, and
|
|
6
|
+
an editable source rendered from the materialized interface. At
|
|
7
|
+
occurrence ``P+[bench]`` the legal DUTs are sibling circuit
|
|
8
|
+
instances plus descendants: ancestors, the bench scope itself,
|
|
9
|
+
unrelated scopes, overlapping targets, and out-of-closure probes
|
|
10
|
+
are refused. Stable entity ids disambiguate reused definitions —
|
|
11
|
+
never names or positions.
|
|
12
|
+
|
|
13
|
+
Renames preserve bindings; hierarchy moves, deletes, interface
|
|
14
|
+
changes, and template upgrades mark the link stale and block the
|
|
15
|
+
run. Regeneration renders a candidate the client adopts
|
|
16
|
+
explicitly and never overwrites an edited source. Harness
|
|
17
|
+
components stay out of the production netlist unless the design
|
|
18
|
+
adopts them. Simulator runs stay binary-gated: absent tools
|
|
19
|
+
report BLOCKED-needs-host, never a faked green.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import importlib.util
|
|
23
|
+
import shutil
|
|
24
|
+
|
|
25
|
+
from ...interfaces.errors import ContractError
|
|
26
|
+
from ...interfaces.refs import digest_mapping
|
|
27
|
+
from ..edit.cas import read_entity_revision
|
|
28
|
+
from .templates import (
|
|
29
|
+
TEMPLATE_EXTENSIONS,
|
|
30
|
+
TEMPLATE_VERSION,
|
|
31
|
+
describe_template,
|
|
32
|
+
render,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
#: Entity component carrying the testbench record.
|
|
36
|
+
TESTBENCH_COMPONENT = "Testbench"
|
|
37
|
+
|
|
38
|
+
#: Saved source document prefix per block name.
|
|
39
|
+
SOURCE_PREFIX = "testbench/"
|
|
40
|
+
|
|
41
|
+
#: Simulator binary probed per testbench kind.
|
|
42
|
+
SIMULATOR_TOOLS = {"spice": "ngspice", "hdl": "icarus", "cocotb": "cocotb"}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _instance_of(service, entity):
|
|
46
|
+
"""Read one instance payload, or None when not an instance.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
service: ProjectService handle to read through.
|
|
50
|
+
entity: Entity id to inspect.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Instance payload mapping, or None for a missing entity
|
|
54
|
+
or a non-instance holder.
|
|
55
|
+
"""
|
|
56
|
+
from ..errors import ProjectError as _ProjectError
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
data = service.get_component(entity, "Instance")["data"]
|
|
60
|
+
except _ProjectError:
|
|
61
|
+
return None
|
|
62
|
+
except LookupError:
|
|
63
|
+
return None
|
|
64
|
+
return data if isinstance(data, dict) else None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _bindings(service):
|
|
68
|
+
"""List every hierarchy binding payload, id-sorted.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
service: ProjectService handle to read through.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Binding mappings with id, parent_sheet, child_sheet,
|
|
75
|
+
and instance.
|
|
76
|
+
"""
|
|
77
|
+
found = []
|
|
78
|
+
for entity in service.entities_with("Hierarchy")["entities"]:
|
|
79
|
+
try:
|
|
80
|
+
raw = service.get_component(entity, "Hierarchy")["data"]
|
|
81
|
+
except Exception: # noqa: BLE001 — skip unreadable rows
|
|
82
|
+
continue
|
|
83
|
+
if isinstance(raw, dict):
|
|
84
|
+
item = dict(raw)
|
|
85
|
+
item["id"] = entity
|
|
86
|
+
found.append(item)
|
|
87
|
+
found.sort(key=lambda item: item["id"])
|
|
88
|
+
return found
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _descendant_sheets(service, root):
|
|
92
|
+
"""Collect a sheet plus every sheet bound beneath it.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
service: ProjectService handle to read through.
|
|
96
|
+
root: Root sheet entity id.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Sheet ids in the root closure (root first).
|
|
100
|
+
"""
|
|
101
|
+
seen = [root]
|
|
102
|
+
changed = True
|
|
103
|
+
bindings = _bindings(service)
|
|
104
|
+
while changed:
|
|
105
|
+
changed = False
|
|
106
|
+
for binding in bindings:
|
|
107
|
+
if (
|
|
108
|
+
binding.get("parent_sheet") in seen
|
|
109
|
+
and binding.get("child_sheet") not in seen
|
|
110
|
+
):
|
|
111
|
+
seen.append(binding.get("child_sheet"))
|
|
112
|
+
changed = True
|
|
113
|
+
return seen
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _ancestor_sheets(service, sheet):
|
|
117
|
+
"""Collect sheets above one sheet through hierarchy bindings.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
service: ProjectService handle to read through.
|
|
121
|
+
sheet: Sheet entity id to walk up from.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Ancestor sheet ids, nearest first.
|
|
125
|
+
"""
|
|
126
|
+
parents = {
|
|
127
|
+
binding.get("child_sheet"): binding.get("parent_sheet")
|
|
128
|
+
for binding in _bindings(service)
|
|
129
|
+
}
|
|
130
|
+
seen = []
|
|
131
|
+
current = parents.get(sheet)
|
|
132
|
+
while current is not None and current not in seen:
|
|
133
|
+
seen.append(current)
|
|
134
|
+
current = parents.get(current)
|
|
135
|
+
return seen
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _closure_instances(service, targets):
|
|
139
|
+
"""Collect DUT targets plus instances on descendant sheets.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
service: ProjectService handle to read through.
|
|
143
|
+
targets: Normalized targets with primary and
|
|
144
|
+
additional entity ids.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Entity ids in the DUT closure (targets plus
|
|
148
|
+
descendant-scope instances).
|
|
149
|
+
"""
|
|
150
|
+
sheets = set()
|
|
151
|
+
for entity in [targets["primary"], *targets["additional"]]:
|
|
152
|
+
payload = _instance_of(service, entity) or {}
|
|
153
|
+
if payload.get("sheet"):
|
|
154
|
+
sheets.update(
|
|
155
|
+
_descendant_sheets(service, payload["sheet"]))
|
|
156
|
+
found = {targets["primary"], *targets["additional"]}
|
|
157
|
+
for entity in service.entities_with("Instance")["entities"]:
|
|
158
|
+
payload = _instance_of(service, entity) or {}
|
|
159
|
+
if payload.get("sheet") in sheets:
|
|
160
|
+
found.add(entity)
|
|
161
|
+
return found
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def normalize_targets(service, targets):
|
|
165
|
+
"""Validate and normalize one DUT target set.
|
|
166
|
+
|
|
167
|
+
The primary must be a circuit-instance occurrence;
|
|
168
|
+
additional targets must be nonoverlapping sibling or
|
|
169
|
+
descendant instances — never ancestors, the scope itself,
|
|
170
|
+
or unrelated scopes.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
service: ProjectService handle to read through.
|
|
174
|
+
targets: Mapping with primary plus additional ids.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Normalized mapping with primary and additional ids.
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
ContractError: invalid_target for a missing primary or
|
|
181
|
+
a non-instance target, overlapping_targets for a
|
|
182
|
+
repeated DUT, illegal_scope for an ancestor or an
|
|
183
|
+
unrelated scope.
|
|
184
|
+
"""
|
|
185
|
+
if not isinstance(targets, dict) or not targets.get("primary"):
|
|
186
|
+
raise ContractError(
|
|
187
|
+
"invalid_target", "Testbench targets need a primary.")
|
|
188
|
+
primary = targets["primary"]
|
|
189
|
+
primary_payload = _instance_of(service, primary)
|
|
190
|
+
if primary_payload is None:
|
|
191
|
+
raise ContractError(
|
|
192
|
+
"invalid_target",
|
|
193
|
+
f"Primary DUT is not a circuit instance: {primary!r}.")
|
|
194
|
+
additional = list(targets.get("additional") or [])
|
|
195
|
+
if len(set(additional)) != len(additional) or primary in additional:
|
|
196
|
+
raise ContractError(
|
|
197
|
+
"overlapping_targets",
|
|
198
|
+
"Additional DUT targets must not overlap the primary.")
|
|
199
|
+
home = primary_payload.get("sheet")
|
|
200
|
+
allowed = set(_descendant_sheets(service, home)) if home else set()
|
|
201
|
+
ancestors = set(_ancestor_sheets(service, home)) if home else set()
|
|
202
|
+
for entity in additional:
|
|
203
|
+
payload = _instance_of(service, entity)
|
|
204
|
+
if payload is None:
|
|
205
|
+
raise ContractError(
|
|
206
|
+
"invalid_target",
|
|
207
|
+
f"Additional DUT is not an instance: {entity!r}.")
|
|
208
|
+
sheet = payload.get("sheet")
|
|
209
|
+
if sheet in ancestors:
|
|
210
|
+
raise ContractError(
|
|
211
|
+
"illegal_scope",
|
|
212
|
+
f"DUT {entity!r} is an ancestor of the primary.")
|
|
213
|
+
if sheet not in allowed:
|
|
214
|
+
raise ContractError(
|
|
215
|
+
"illegal_scope",
|
|
216
|
+
f"DUT {entity!r} is outside the primary scope.")
|
|
217
|
+
return {"primary": primary, "additional": additional}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def normalize_probes(service, targets, probes):
|
|
221
|
+
"""Validate probes against the DUT closure.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
service: ProjectService handle to read through.
|
|
225
|
+
targets: Normalized DUT targets.
|
|
226
|
+
probes: Probe mappings with an entity each.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Normalized probe list.
|
|
230
|
+
|
|
231
|
+
Raises:
|
|
232
|
+
ContractError: illegal_scope for a probe outside the
|
|
233
|
+
DUT closure.
|
|
234
|
+
"""
|
|
235
|
+
closure = _closure_instances(service, targets)
|
|
236
|
+
kept = []
|
|
237
|
+
for probe in probes or []:
|
|
238
|
+
entity = (probe or {}).get("entity")
|
|
239
|
+
if entity not in closure:
|
|
240
|
+
raise ContractError(
|
|
241
|
+
"illegal_scope",
|
|
242
|
+
f"Probe {entity!r} is outside the DUT closure.")
|
|
243
|
+
kept.append({"entity": entity})
|
|
244
|
+
return kept
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def materialize_interface(service, targets):
|
|
248
|
+
"""Materialize the target interface from symbol definitions.
|
|
249
|
+
|
|
250
|
+
Only symbol-defined pins appear with their occurrence; symbols
|
|
251
|
+
that do not resolve stay pin-less instead of invented.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
service: ProjectService handle to read through.
|
|
255
|
+
targets: Normalized DUT targets.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Interface with pins ([{occurrence, pin}]) and digest.
|
|
259
|
+
"""
|
|
260
|
+
symbols = {}
|
|
261
|
+
for entity in service.entities_with("Symbol")["entities"]:
|
|
262
|
+
try:
|
|
263
|
+
raw = service.get_component(entity, "Symbol")["data"]
|
|
264
|
+
except Exception: # noqa: BLE001 — skip unreadable rows
|
|
265
|
+
continue
|
|
266
|
+
if isinstance(raw, dict) and raw.get("name"):
|
|
267
|
+
symbols[raw["name"]] = raw.get("pins") or []
|
|
268
|
+
pins = []
|
|
269
|
+
for entity in [targets["primary"], *targets["additional"]]:
|
|
270
|
+
payload = _instance_of(service, entity) or {}
|
|
271
|
+
for pin in symbols.get(payload.get("symbol", ""), []):
|
|
272
|
+
if isinstance(pin, dict) and pin.get("name"):
|
|
273
|
+
pins.append(
|
|
274
|
+
{"occurrence": entity, "pin": pin["name"]})
|
|
275
|
+
interface = {"pins": pins}
|
|
276
|
+
interface["digest"] = digest_mapping(interface)
|
|
277
|
+
return interface
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def dut_digest(service, targets):
|
|
281
|
+
"""Digest the DUT occurrence set plus its live revisions.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
service: ProjectService handle to read through.
|
|
285
|
+
targets: Normalized DUT targets.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Digest pinning occurrences, payloads, and revisions.
|
|
289
|
+
"""
|
|
290
|
+
members = [targets["primary"], *targets["additional"]]
|
|
291
|
+
payloads = {
|
|
292
|
+
entity: (_instance_of(service, entity) or {}) for entity in members
|
|
293
|
+
}
|
|
294
|
+
revisions = {
|
|
295
|
+
entity: read_entity_revision(service, entity)
|
|
296
|
+
for entity in members
|
|
297
|
+
}
|
|
298
|
+
return digest_mapping(
|
|
299
|
+
{"payloads": payloads, "revisions": revisions})
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _record(service, bench):
|
|
303
|
+
"""Read one testbench record payload.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
service: ProjectService handle to read through.
|
|
307
|
+
bench: Testbench entity id.
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
Testbench record mapping.
|
|
311
|
+
"""
|
|
312
|
+
return service.get_component(bench, TESTBENCH_COMPONENT)["data"]
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def check_fresh(service, bench):
|
|
316
|
+
"""Check whether one testbench link is still fresh.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
service: ProjectService handle to read through.
|
|
320
|
+
bench: Testbench entity id.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
(fresh, reasons) with stale reason strings (dut,
|
|
324
|
+
template, hierarchy) when the link drifted.
|
|
325
|
+
"""
|
|
326
|
+
record = _record(service, bench)
|
|
327
|
+
reasons = []
|
|
328
|
+
targets = record.get("targets") or {}
|
|
329
|
+
try:
|
|
330
|
+
current = dut_digest(service, targets)
|
|
331
|
+
except Exception: # noqa: BLE001 — unreadable DUT reads stale
|
|
332
|
+
return False, ["dut"]
|
|
333
|
+
if current != record.get("dut_digest"):
|
|
334
|
+
reasons.append("dut")
|
|
335
|
+
members = [targets.get("primary"), *(targets.get("additional") or [])]
|
|
336
|
+
for entity in members:
|
|
337
|
+
try:
|
|
338
|
+
live = read_entity_revision(service, entity)
|
|
339
|
+
except Exception: # noqa: BLE001 — a lost DUT reads stale
|
|
340
|
+
reasons.append("hierarchy")
|
|
341
|
+
break
|
|
342
|
+
pinned = (record.get("dut_revisions") or {}).get(entity)
|
|
343
|
+
if pinned is not None and pinned != live:
|
|
344
|
+
reasons.append("hierarchy")
|
|
345
|
+
break
|
|
346
|
+
if (record.get("template") or {}).get(
|
|
347
|
+
"template_version") != TEMPLATE_VERSION:
|
|
348
|
+
reasons.append("template")
|
|
349
|
+
return (not reasons), sorted(set(reasons))
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def assert_runnable(service, bench):
|
|
353
|
+
"""Refuse to run a stale testbench link.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
service: ProjectService handle to read through.
|
|
357
|
+
bench: Testbench entity id.
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
True when the link is fresh.
|
|
361
|
+
|
|
362
|
+
Raises:
|
|
363
|
+
ContractError: stale_link naming the drift reasons.
|
|
364
|
+
"""
|
|
365
|
+
fresh, reasons = check_fresh(service, bench)
|
|
366
|
+
if not fresh:
|
|
367
|
+
raise ContractError(
|
|
368
|
+
"stale_link",
|
|
369
|
+
f"Testbench {bench!r} drifted ({', '.join(reasons)}); "
|
|
370
|
+
"regenerate before running.")
|
|
371
|
+
return True
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def create_block(service, request_id, command_text, name, kind,
|
|
375
|
+
targets, probes, harness, template=None,
|
|
376
|
+
expected_revision=None):
|
|
377
|
+
"""Create one scoped testbench block with editable source.
|
|
378
|
+
|
|
379
|
+
Validates the DUT scope, materializes the interface, renders
|
|
380
|
+
the template source, stores the record, and saves the source
|
|
381
|
+
as a project document. The harness binding rides verbatim —
|
|
382
|
+
never ambient parent inclusion.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
service: ProjectService handle to write through.
|
|
386
|
+
request_id: Inner client envelope id.
|
|
387
|
+
command_text: Inner command text for identity comparison.
|
|
388
|
+
name: Block name (source saved as testbench/<name>.<ext>).
|
|
389
|
+
kind: Template kind (spice, hdl, or cocotb).
|
|
390
|
+
targets: DUT target set (primary plus additional).
|
|
391
|
+
probes: Probe mappings inside the DUT closure.
|
|
392
|
+
harness: Explicit harness binding mapping.
|
|
393
|
+
template: Reserved template override (unused; the kind
|
|
394
|
+
selects the template).
|
|
395
|
+
expected_revision: Revision the writer read.
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
Receipt with testbench id, name, kind, source path,
|
|
399
|
+
DUT digest, and freshness.
|
|
400
|
+
|
|
401
|
+
Raises:
|
|
402
|
+
ContractError: invalid_kind, invalid_target,
|
|
403
|
+
overlapping_targets, illegal_scope, or whatever the
|
|
404
|
+
service raises.
|
|
405
|
+
"""
|
|
406
|
+
del template
|
|
407
|
+
describe_template(kind)
|
|
408
|
+
normalized = normalize_targets(service, targets)
|
|
409
|
+
kept_probes = normalize_probes(service, normalized, probes)
|
|
410
|
+
digest = dut_digest(service, normalized)
|
|
411
|
+
interface = materialize_interface(service, normalized)
|
|
412
|
+
rendered = render(kind, interface, dict(harness or {}))
|
|
413
|
+
path = f"{SOURCE_PREFIX}{name}.{TEMPLATE_EXTENSIONS[kind]}"
|
|
414
|
+
members = [normalized["primary"], *normalized["additional"]]
|
|
415
|
+
revisions = {
|
|
416
|
+
entity: read_entity_revision(service, entity)
|
|
417
|
+
for entity in members
|
|
418
|
+
}
|
|
419
|
+
created = service.create_entity(
|
|
420
|
+
request_id, command_text, kind="testbench",
|
|
421
|
+
components={
|
|
422
|
+
TESTBENCH_COMPONENT: {
|
|
423
|
+
"name": name, "kind": kind,
|
|
424
|
+
"targets": normalized, "probes": kept_probes,
|
|
425
|
+
"harness": dict(harness or {}),
|
|
426
|
+
"interface": interface, "dut_digest": digest,
|
|
427
|
+
"dut_revisions": revisions,
|
|
428
|
+
"template": rendered["provenance"],
|
|
429
|
+
"source": rendered["source"],
|
|
430
|
+
"source_path": path,
|
|
431
|
+
"candidate_source": None,
|
|
432
|
+
"candidate_digest": None,
|
|
433
|
+
"adopted": False, "fresh": True,
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
expected_revision=expected_revision,
|
|
437
|
+
)
|
|
438
|
+
service.write_doc(
|
|
439
|
+
path, rendered["source"],
|
|
440
|
+
f"{request_id}:source", f"{command_text}\n#inner source",
|
|
441
|
+
expected_revision=created["current_revision"],
|
|
442
|
+
)
|
|
443
|
+
return {
|
|
444
|
+
"testbench": created["entity"], "name": name, "kind": kind,
|
|
445
|
+
"source_path": path, "dut_digest": digest,
|
|
446
|
+
"current_revision": created["current_revision"],
|
|
447
|
+
"fresh": True,
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def regenerate_block(service, request_id, command_text, testbench,
|
|
452
|
+
expected_revision=None):
|
|
453
|
+
"""Regenerate one block to a candidate without overwriting edits.
|
|
454
|
+
|
|
455
|
+
A fresh link returns a noop receipt; a drifted link renders
|
|
456
|
+
the candidate from the current interface and stores it
|
|
457
|
+
beside the edited live source for explicit adoption.
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
service: ProjectService handle to write through.
|
|
461
|
+
request_id: Inner client envelope id.
|
|
462
|
+
command_text: Inner command text for identity comparison.
|
|
463
|
+
testbench: Testbench entity id.
|
|
464
|
+
expected_revision: Revision the writer read.
|
|
465
|
+
|
|
466
|
+
Returns:
|
|
467
|
+
Noop receipt when fresh, else the candidate source,
|
|
468
|
+
digest, drift reasons, and live-source diff flag.
|
|
469
|
+
|
|
470
|
+
Raises:
|
|
471
|
+
Exception: Whatever the service raises.
|
|
472
|
+
"""
|
|
473
|
+
record = _record(service, testbench)
|
|
474
|
+
fresh, reasons = check_fresh(service, testbench)
|
|
475
|
+
normalized = normalize_targets(service, record.get("targets"))
|
|
476
|
+
if fresh:
|
|
477
|
+
return {"testbench": testbench, "noop": True, "fresh": True}
|
|
478
|
+
interface = materialize_interface(service, normalized)
|
|
479
|
+
rendered = render(
|
|
480
|
+
record.get("kind"), interface, record.get("harness") or {})
|
|
481
|
+
digest = dut_digest(service, normalized)
|
|
482
|
+
service.set_component(
|
|
483
|
+
testbench, TESTBENCH_COMPONENT,
|
|
484
|
+
{**record, "candidate_source": rendered["source"],
|
|
485
|
+
"candidate_digest": rendered["digest"],
|
|
486
|
+
"dut_digest": digest, "fresh": False},
|
|
487
|
+
request_id, command_text,
|
|
488
|
+
expected_revision=expected_revision,
|
|
489
|
+
)
|
|
490
|
+
return {
|
|
491
|
+
"testbench": testbench, "fresh": False,
|
|
492
|
+
"stale_reasons": reasons,
|
|
493
|
+
"candidate_source": rendered["source"],
|
|
494
|
+
"candidate_digest": rendered["digest"],
|
|
495
|
+
"differs_from_live":
|
|
496
|
+
rendered["source"] != record.get("source"),
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def describe_block(service, bench):
|
|
501
|
+
"""Describe one block with freshness and closure pins.
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
service: ProjectService handle to read through.
|
|
505
|
+
bench: Testbench entity id.
|
|
506
|
+
|
|
507
|
+
Returns:
|
|
508
|
+
Record plus freshness, stale reasons, port bindings,
|
|
509
|
+
interface digest, template provenance, and the I28
|
|
510
|
+
closure pin.
|
|
511
|
+
"""
|
|
512
|
+
record = dict(_record(service, bench))
|
|
513
|
+
fresh, reasons = check_fresh(service, bench)
|
|
514
|
+
record["fresh"] = fresh
|
|
515
|
+
record["stale_reasons"] = reasons
|
|
516
|
+
record["port_bindings"] = [
|
|
517
|
+
{"occurrence": pin.get("occurrence"), "pin": pin.get("pin")}
|
|
518
|
+
for pin in (record.get("interface") or {}).get("pins", [])
|
|
519
|
+
]
|
|
520
|
+
record["interface_digest"] = (
|
|
521
|
+
record.get("interface") or {}).get("digest")
|
|
522
|
+
record["i28"] = {
|
|
523
|
+
"revision": service.snapshot()["current_revision"],
|
|
524
|
+
"dut_revisions": record.get("dut_revisions"),
|
|
525
|
+
"template": record.get("template"),
|
|
526
|
+
"digest": digest_mapping({
|
|
527
|
+
"dut": record.get("dut_digest"),
|
|
528
|
+
"template": record.get("template"),
|
|
529
|
+
"source": record.get("source"),
|
|
530
|
+
}),
|
|
531
|
+
}
|
|
532
|
+
return record
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def harness_instances(record):
|
|
536
|
+
"""List harness-owned instances excluded from production output.
|
|
537
|
+
|
|
538
|
+
Args:
|
|
539
|
+
record: Testbench record mapping.
|
|
540
|
+
|
|
541
|
+
Returns:
|
|
542
|
+
Harness instance ids (explicit binding only).
|
|
543
|
+
"""
|
|
544
|
+
harness = record.get("harness") or {}
|
|
545
|
+
return list(harness.get("instances") or [])
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def design_net_pins(record, net_pins):
|
|
549
|
+
"""Filter harness instances out of production net pins.
|
|
550
|
+
|
|
551
|
+
Args:
|
|
552
|
+
record: Testbench record mapping.
|
|
553
|
+
net_pins: Materialized [{instance, pin, net}] rows.
|
|
554
|
+
|
|
555
|
+
Returns:
|
|
556
|
+
Rows excluding harness-owned instances (adopted blocks
|
|
557
|
+
return every row unchanged).
|
|
558
|
+
"""
|
|
559
|
+
if record.get("adopted"):
|
|
560
|
+
return list(net_pins or [])
|
|
561
|
+
excluded = set(harness_instances(record))
|
|
562
|
+
return [
|
|
563
|
+
row for row in net_pins or []
|
|
564
|
+
if row.get("instance") not in excluded
|
|
565
|
+
]
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def excluded_harness_instances(service):
|
|
569
|
+
"""Collect harness-owned instances across all testbenches.
|
|
570
|
+
|
|
571
|
+
Args:
|
|
572
|
+
service: ProjectService handle to read through.
|
|
573
|
+
|
|
574
|
+
Returns:
|
|
575
|
+
Harness instance ids excluded from production output.
|
|
576
|
+
"""
|
|
577
|
+
from ..errors import ProjectError as _ProjectError
|
|
578
|
+
|
|
579
|
+
excluded = set()
|
|
580
|
+
for bench in service.entities_with(
|
|
581
|
+
TESTBENCH_COMPONENT)["entities"]:
|
|
582
|
+
try:
|
|
583
|
+
record = service.get_component(
|
|
584
|
+
bench, TESTBENCH_COMPONENT)["data"]
|
|
585
|
+
except _ProjectError:
|
|
586
|
+
continue
|
|
587
|
+
excluded.update(harness_instances(record))
|
|
588
|
+
return excluded
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def probe_simulator(kind):
|
|
592
|
+
"""Probe the simulator binary for one testbench kind.
|
|
593
|
+
|
|
594
|
+
Absent simulators report BLOCKED-needs-host with a remedy —
|
|
595
|
+
never a faked PASS.
|
|
596
|
+
|
|
597
|
+
Args:
|
|
598
|
+
kind: Testbench kind (spice, hdl, or cocotb).
|
|
599
|
+
|
|
600
|
+
Returns:
|
|
601
|
+
Probe with kind, tool, available flag, and verdict
|
|
602
|
+
(ready or BLOCKED-needs-host) plus remedy text.
|
|
603
|
+
"""
|
|
604
|
+
tool = SIMULATOR_TOOLS.get(kind, kind)
|
|
605
|
+
if tool == "cocotb":
|
|
606
|
+
available = importlib.util.find_spec("cocotb") is not None
|
|
607
|
+
else:
|
|
608
|
+
available = shutil.which(tool) is not None
|
|
609
|
+
if available:
|
|
610
|
+
return {"kind": kind, "tool": tool, "available": True,
|
|
611
|
+
"verdict": "ready"}
|
|
612
|
+
return {
|
|
613
|
+
"kind": kind, "tool": tool, "available": False,
|
|
614
|
+
"verdict": "BLOCKED-needs-host",
|
|
615
|
+
"remedy": (
|
|
616
|
+
f"Install {tool} on PATH (or provision the sim profile) "
|
|
617
|
+
"and rerun; testbench simulation stays gated until then."
|
|
618
|
+
),
|
|
619
|
+
}
|