Skip to main content

The SHDL Golden Test Suite

The complete verification program for the SHDL project: what must be tested, how it is verified, what is already covered, and what remains. Companion to SHDL_Project.md, shdl.md, base_shdl.md, and shdlc_goals.md.


0. Purpose​

SHDL's project principles demand correctness before performance and a test corpus mature enough that the flattener and compiler can be refactored or reimplemented with confidence. This document defines the golden test suite: the closed list of obligations that, when all green, justify the claim "SHDL contains no known bugs and no untested normative behavior."

"Extremely certain" is operationalized as five concrete conditions:

  1. Spec closure — every normative statement in shdl.md, base_shdl.md, and shdlc_goals.md maps to at least one test (this document is that mapping).
  2. Diagnostic closure — every diagnostic raise site (not just every code) is exercised by at least one test asserting code, message substance, and position.
  3. Oracle closure — every executable artifact is validated bit-exactly, cycle-by-cycle, against an independent reference, on fixed corpora and randomized inputs.
  4. Crash closure — no user input, however malformed, produces an uncaught Python exception, a C compiler error, or undefined behavior; failures are always structured diagnostics.
  5. Determinism closure — byte-identical Base SHDL, byte-identical generated C, identical traces: across runs, hash seeds, optimization levels, build modes, and platforms.

Each domain below lists its obligations with stable IDs. Status legend: ✅ covered (exists today) · 🟡 partial (exists, with named holes) · ❌ missing · ⚪ blocked (component not yet built; obligation defined now, activated at build time).


1. Verification Architecture​

1.1 The oracle chain​

The suite's trust argument rests on independent implementations checking each other:

shdl.md + base_shdl.md (specs; human-verified examples base_shdl §3.7/§4.4)
│ pinned literally by test_spec_examples
▼
HighEval ── high-level interpreter (runs phase-3 output: pre-expander SHDL)
│ trace equality on random + exhaustive stimulus (test_equivalence)
▼
BaseEval ── reference interpreter for Base SHDL (THE ground truth, §7.1 shdlc_goals)
│ lockstep differential, every port, every cycle (DualSim)
▼
Compiled C library (release; later: debug build, optimization tiers, backends)
│ replayed bit-exactly against (traces sourced from BaseEval, never from C)
▼
Conformance corpus (v1.1.0 — 38 cases / 40 traces, provenance: never from C)

Independence rules (already in force, must never be weakened):

  • Conformance traces are generated by OracleSim (BaseEval) only — never by the C implementation under test.
  • expected.base.shdl files are frozen flattener output with pinned timestamps; regeneration is a deliberate, reviewed, manifest-versioned act, never a side effect of a failing test.
  • Every future backend/tier validates against both the corpus and live BaseEval lockstep.

1.2 Test tiers​

TierNameContentsWhen it runs
T0Unitlexer/parser/phases/codegen unit tests, diagnostics matrixevery commit, seconds
T1Propertyinvariants over generated inputs (order independence, idempotence, no-crash)every commit
T2DifferentialDualSim fixtures, fuzz circuits, opt-level/build-mode equivalenceevery commit (small N), nightly (large N)
T3Conformancefrozen corpus, Tier A byte-equality + Tier B/C trace replayevery commit
T4AcceptanceSR16 CPU lockstep vs golden model, real programsevery commit (cached builds)
T5Soak / fuzz campaignenv-scaled long runs (SHDLC_FUZZ=500, SHDLC_DIFF_CYCLES=5000), sanitizer buildsnightly / pre-release

1.3 Standing enforcement mechanisms (keep forever)​

  • Diagnostics completeness assertion (test_diagnostics_matrix): the set of tested codes must equal the ErrorCode enum — a new code cannot land untested. Extend (see DIA-2) to per-raise-site coverage.
  • STRICT_CFLAGS (-std=c11 -Wall -Wextra -Werror -pedantic -O2 -fPIC -shared -fvisibility=hidden): every harness-driven generated-C build in the suite is a zero-warning proof. (The CLI subprocess tests build with release DEFAULT_CFLAGS, which carry no warning flags — the CCT-3 hole.)
  • Corpus integrity test: manifest↔disk bijection, feature-coverage check, minimum case count guard (≥38 Tier A, ≥40 traces) plus per-golden content hashes (CNF-9 drift fence) so silent corpus loss or coordinated regen fails loudly.
  • Seed-stable randomness: all randomized tests derive seeds from fixture names (zlib.crc32), and failures embed the full netlist/action log for replay.

2. Coverage Domains​

Domain A — Lexical analysis (LEX)​

IDObligationStatus
LEX-1All token kinds: 29 token kinds (28 + EOF), all one- and two-char operators, keywords vs identifiers✅
LEX-2Literal forms: decimal, hex 0x, binary 0b, value correctness✅
LEX-3Uppercase prefixes 0X / 0B accepted per spec §2.3 — implemented (lexer.py handles both cases); pure test gap✅
LEX-4All three comment forms (#, "…", """…""") stripped; doc comment captured only when leading✅
LEX-51-based line/column tracking with file attribution✅
LEX-6E0101 per raise site (6 sites in lexer.py): unexpected char, unterminated """, unterminated "…", hex with no digits, binary with no digits, ident char trailing a literal (12ab, 0xFG)✅ (all 6 sites — test_lexer.py LEX-6 block)
LEX-7Boundary inputs: empty file, comment-only file, literal/comment at EOF, tabs, CRLF line endings, BOM✅ (comment-only, tabs, CRLF, BOM-as-E0101 all pinned)
LEX-8Huge literals (e.g. 10^100): lexed as exact ints; downstream width-inference behavior pinned (see AMB-11)✅ (decimal/hex 10**100 exact-int)
LEX-9Character-set policy: ASCII-only [A-Za-z_]/[0-9] pinned (AMB-14) — é/٣/²/BOM all fall through to E0101; the ²→int() raw ValueError crash is closed✅ (lexer pinned ASCII; crash regression-tested)
LEX-10Source decoding: a .shdl file with invalid UTF-8 bytes → structured diagnostic at the read boundary, never a raw UnicodeDecodeError (loader decodes in a try → positioned E0101)✅ (flattener loader side; shdlc-CLI mirror is FUZ-4's open xfail)
LEX-11Doc-comment capture edge rules: only the first leading """…""" becomes meta.doc; preceding #/"…" comments do not disqualify capture; a second leading docstring is ignored✅

Domain B — Grammar & parser (PAR)​

IDObligationStatus
PAR-1Positive coverage of every EBNF production in shdl.md §13 — components (with/without top, params, empty port lists), instances (positional/named/mixed args), constants, init blocks, connect, generators, conditionals, all Signal/IndexExpr/NameTemplate forms✅ (empty-output -> (), declaration-context when, parenthesized ( BoolExpr ), and all RelOps != < <= >= + || now positively parsed)
PAR-2Slice forms [a:b], [a:], [:b], single [k], and [{expr}] index✅
PAR-3Concat/replication disambiguation: 8{sign}, N{x.O, y}, template fa{i}, bare {A} one-element concat✅
PAR-4Nested replication (Replication contains ConcatItem which may be Replication, e.g. 2{2{s}}) — grammar permits it; behavior pinned✅
PAR-5NameTemplate interleaving: cell{i}_{j}, mem{row*4+col}, substitution followed by literal ident✅
PAR-6E0201 per raise site (parser.py) — each triggered by a dedicated minimal program✅ (all 12 parser raise sites individually triggered via the DIA-2 site matrix)
PAR-7Context legality — corrected: declarations inside connect are rejected via E0201 (they parse as connections and die at the -> expect), not E0604; the connection-context E0604 site fires only for non-IDENT/non-LBRACE tokens (e.g. 5 -> Y;)✅ (both directions pinned + the conn-context E0604 site triggered)
PAR-8Deeply nested input (1000-deep braces/parens): structured diagnostic or documented cap — never a raw RecursionError (live-verified crash at 400-deep nesting)✅ (MAX_NESTING_DEPTH=200 cap → E0201; 201-deep probe pinned, crash closed)
PAR-9</> contextual disambiguation: param list vs generator intro vs relational inside when — adversarial mixes (a<b: T<N> style)✅ (three-role angle-bracket mix pinned)
PAR-10Premature EOF inside component/connect/generator/init bodies: pinned code + position — never a crash or hang✅ (all four body kinds pinned)
PAR-11Body-order laxity: declarations/init/connect accepted in any order (§13's EBNF fixes the order, §4.2's prose allows interleaving — AMB-16); pinned✅
PAR-12Reserved keyword in identifier position (a port/instance/constant/component named when, connect, …) → E0201 with position✅

Domain C — Names, structure & validation (VAL)​

IDObligationStatus
VAL-1Reserved names: leading __ (E0303), bus pattern X_<digits>_ (E0304) — plus boundary non-matches that must be accepted (X_2, X_2_3, _x)✅ (rejections + all three accept-side boundaries)
VAL-2Primitive shadowing: redefinition of each of the six (AND,OR,NOT,XOR,__VCC__,__GND__) → E0305✅ (all six; __VCC__/__GND__ E0305-over-E0303 precedence pinned — AMB-23)
VAL-3Structure: exactly one connect, at most one init, at most one top (E0309/E0310)✅
VAL-4Duplicates: component, port, instance, port/instance collision, duplicate inside generator body (E0301)✅
VAL-5Case sensitivity: MyGate ≠ mygate coexist legally (spec §2.2)✅
VAL-6Unknown component type E0306 (2 sites: validate.py + a monomorphize discovery duplicate — neither in expand.py); unknown signal/instance/port reference E0307 (all 8 raise sites in expand.py)✅ (E0306 validate site tested + monomorphize duplicate proven-unreachable via the unused-component test; E0307 all 8 sites)
VAL-7Zero/negative width literals → E0403 (3 sites: monomorphize port-width — the E0403/E0906 dual-code site, see AMB-31 — validate constant-literal, expand derived-constant)✅ (zero + negative port widths; both constant-width sites; AMB-31 E0403-vs-E0906 branch pinned)
VAL-8Reserved-name checks at the remaining declare sites: generator variable (>__i[2] → E0303, >i_1_[…] → E0304) and constant name (__K = 1 → E0303)✅
VAL-9Expansion-time name checks on generated names: template duplicate (>i[1:2, 1:2]{ b{i}: OR; } → E0301), generated name colliding with a port (E0301), generated name matching a reserved pattern (x_{i}_ → E0304) — the expand-phase declare sites, distinct from validate-phase checks on plain names✅

Domain D — Parameters & monomorphization (MON)​

IDObligationStatus
MON-1Binding: positional, named, defaults, dedup of identical bindings (P<2> ≡ P<N=2>), distinct specializations per tuple✅
MON-2Full E09xx matrix: unknown named (E0901), unbound (E0902), positional-after-named & arity (E0903), double-bind (E0904), negative arg (E0905), non-positive derived width (E0906)✅ (six scenarios + E0901 primitive-with-params, both the validate site and the generated-name path)
MON-3Top component requires all defaults (E0902 path)✅ (top-marked validate path + both selection-path sites: sole-component and --top without marker)
MON-4Recursion: direct and mutual instantiation → E0311✅
MON-5Dependent parameters: later port widths/ranges/when conditions using earlier params (Sum[N+1], >i[N/2])✅ (widths + range-over-param + when-condition-over-param)
MON-6Parameter scope: param referenced outside its component → E0603/E0307; generator var shadowing a param (pinned: shadowing wins); generator range expressions evaluate in the enclosing scope (AMB-22)✅ (shadowing-wins, out-of-scope param ref → E0603, range-scope-under-shadowing all pinned)
MON-7Specialization scale: e.g. 100 distinct Adder<k> instantiations — output correct, no superlinear blowup (ties into SCL)✅ (100 distinct Rep<k> specs; also SCL-4)
MON-8Binding canonicalization across named-arg order: P<N = 2, M = 1> ≡ P<M = 1, N = 2> dedup to one specialization (the declaration-order normalization exists precisely for this; its purpose is unasserted)✅

Domain E — Generators, conditionals & compile-time expressions (GEN)​

IDObligationStatus
GEN-1Range forms: [N], [a:b], [a:], [:b], compound [a:b, c, d:e]✅ ([:b] RangeTo now exercised end-to-end through parser + expansion)
GEN-2Open-range resolution from governing signal; ambiguity → E0602; declaration-context open range → E0602✅
GEN-3{expr} substitution with + - * /, nesting; {i-1} / {i+1} / {2*i-1} patterns✅ (/-in-substitution + the §7.2 {i+1} / {2*i-1} patterns pinned end-to-end)
GEN-4Nested generators (2 levels) with computed indices✅
GEN-5Nesting ≥3 levels deep; template names mixing several loop vars✅ (dedicated 3-level-nesting unit fixture with a multi-var template name)
GEN-6Empty/ill-ordered ranges → E0601 (all 8 raise sites)✅ (all 8 sites triggered by dedicated minimal cases — test_gen6_all_e0601_sites)
GEN-7when/else: structural selection, boundary-folding idiom (i == 1 / i > 1 carry chain)✅ (the dual-when i > 1 §7.7.1 carry chain now elaborated to a real ripple)
GEN-8Conditional operator matrix: each of == != < <= > >= and &&, ||, parenthesized BoolExpr — each proven to select/deselect structure✅ (every RelOp + BoolAnd/BoolOr + parenthesized BoolExpr proven to select structure in an elaborated when)
GEN-9Expression semantics pinned: precedence, integer division truncation direction (C-style toward zero, incl. negative operands), division by zero → E0605, unbound ident → E0603✅
GEN-10Compound-range duplicate index ([1:4, 2:6]) → behavior decided & pinned (expected: duplicate instance names → E0301) — see AMB-4✅ (verbatim iteration; overlap collides → E0301)
GEN-11Open range with expression bound (>i[N/2:]) — see AMB-5✅
GEN-12Declaration-context when/else (spec §7.7.2): conditional instance/constant creation gated on a parameter (when {WITH_CARRY == 1} { c: Carry; }) — elaboration and deselected-branch absence pinned (every when in the entire corpus lives inside connect)✅ (parameter-gated instance + conditional constant both pinned)
GEN-13Range value domain: lower bound 0 accepted today ([0:b] iterates from 0), negative → E0601 — accept/reject boundary pinned (see AMB-19)✅
GEN-14Open-range governing-signal scan semantics: several same-width signals dedup to one bound; no-signal-in-body E0602 site; both when branches scanned pre-evaluation; inner-loop-var shadowing; silent skip of unevaluable templates — each pinned (see AMB-20/AMB-21)✅

Domain F — Expanders: slices, concat, replication (EXP)​

IDObligationStatus
EXP-1Concat MSB-first lowering (spec §6.5.2) — exact per-bit connection list pinned✅
EXP-2Whole-vector connection bit pairing; slice pairing lowest-to-lowest✅
EXP-3Replication expansion (3{s}); sign-extension idiom {8{sign}, Value}✅
EXP-4Width mismatch E0401; index out of range E0402; ill-ordered/zero-based slice E0404✅
EXP-5Degenerate slice [k:k] (one bit) accepted✅
EXP-6Replication count 0 or negative — implemented as E0601 (count must be >= 1); spec amendment + pinning test outstanding (see AMB-6)✅
EXP-7Role enforcement (E0505, all 6 raise sites): input as destination, output as source, instance-port directions, constant as destination, replication as destination✅
EXP-8Multiple drivers E0501, floating input E0502, floating output E0503 — messages name the offending signal✅ (expander-path E0501 message names the wire; flatten-path E0501 proven-unreachable from per-component-valid input)
EXP-9Self-connection E0504 — policy (flag-gated?) decided and both modes tested✅ (role check fires first → E0505; E0504 structurally shadowed, pinned as the observable contract)
EXP-10Concat on both sides of one connection ({a,b} -> {x,y})✅
EXP-11Multi-item replication group N{a, b} lowering: N copies of the MSB-first group — exact per-bit connection order pinned (parse-only today; every flattened replication in the suite is single-item)✅
EXP-12Open-ended slice [a:] in a connection: upper bound resolved from the signal's width at resolve time✅ (hi=None resolution branch pinned end-to-end)
EXP-13Slice and bit-index on instance ports (ir.Q[1:8], sel.O[1], spec §5.1): pairing pinned with a targeted test; E0402/E0404 on an instance port asserted✅ (dedicated pairing assert + E0402/E0404 on an instance port)

Domain G — Constants (CON)​

IDObligationStatus
CON-1Referenced-bits-only materialization to __VCC__/__GND__; unreferenced constants produce nothing✅
CON-2Spec §8.1 truth table: unbounded-width semantics — referencing far beyond significant bits yields __GND__ (e.g. Hundred[64])✅ (§8.1 table pinned verbatim)
CON-3Declared width = overflow assertion only (E0801); width inference for bare constants✅
CON-4ZERO = 0 edge: inferred width of value 0 — decided & pinned (see AMB-12)✅ (direct unit assert of the width-1 inference + spec amended — AMB-12)
CON-5Constants are sources only (E0505); per-bit indexing in generators✅
CON-6Name collision with materialized X_bitN gates → E0308; hierarchy-prefixed constant metadata✅
CON-7Parameter-derived constant widths (K[N] = v): re-checked post-monomorphization — E0403/E0801 at the expand-phase sites (validate pre-checks literal widths only; both expand sites are dead in the suite; grammar question: AMB-17)✅ (both expand-phase sites triggered: non-positive width → E0403, overflow → E0801)
CON-8Constant referenced via slice (K[a:b] -> X, §6.1 'optionally indexed/sliced') — per-bit lowering identical to any sliced source, incl. a slice past significant bits (GND fill)✅

Domain H — Hierarchy flattening & init (FLT)​

IDObligationStatus
FLT-1Deep prefixing (3+ levels), inline-at-declaration ordering, canonical wires for hierarchy ports✅
FLT-2Pass-through/wire-through alias chains resolve fully; pure alias cycle → E0506✅
FLT-3Generated-name collision (fa_x1 vs hierarchical fa containing x1) → E0308✅
FLT-4Hierarchy depth ≥10 — explicit deep-nest fixture (the CPU does not hit this: measured max nesting is 6 — SR16 > RegFile > RegE > MSFFE > DLatch > SRLatch; deepest dedicated fixture is 3)✅ (dedicated 10-level chain)
FLT-5Init: canonical-key extraction, propagation through hierarchy, multi-bit LSB-first spread✅
FLT-6Init rules: non-drivable target E0A01 (all 4 raise sites), double-seed E0A02, overflow E0A03✅ (E0A02/E0A03 + all 4 E0A01 sites: input-port, instance-input-pin, constant, alias-resolves-to-input)
FLT-7Init on a combinational (non-state-holding) wire: allowed, then overwritten by first cycle — verified end-to-end in simulation (spec §11.4)✅ (named end-to-end case: seed visible at cycle 0, overwritten on first tick)
FLT-8Whole-loop seeding lesson (tasks/lessons): init values that don't form a fixed point of the seeded feedback loop — document & test expected (possibly oscillating) behavior deterministically✅ (minimal pinned non-fixed-point oscillation case)
FLT-9E0308 second flatten site: flattened gate name colliding with a top-level port wire (FLT-3 covers gate-vs-gate only; the gate-vs-port site is unexecuted by any test)✅
FLT-10Indexed/sliced init targets (init { Q[3] = 1; }, init { Q[4:2] = 5; }): grammar and primary_bits support them — per-bit seeding semantics plus E0402/E0404 on bad init indexes pinned✅
FLT-11Init targets resolve with an empty environment: a {…} template in an init target → E0603 by construction — pinned✅

Domain I — Modules, imports & loader (IMP)​

IDObligationStatus
IMP-1use mod::{A, B} resolution; search order (file's directory shadows -I dirs; spec wording divergence: AMB-24)✅
IMP-2E07xx: missing module (E0701), circular import with cycle chain in message (E0702), missing name (E0703)✅
IMP-3Imported/local name collision → E0301✅
IMP-4Diamond imports (A→B→D, A→C→D): legal, D loaded once, not flagged circular✅ (minimal A→B→D fixture asserts D loaded once + a diamond conformance case — CNF-4)
IMP-5Self-import; importing a primitive name (impossible per implementation — contradicts spec §9.2, see AMB-25); use after a component definition (implemented as E0201, untested)✅ (self-import → E0702, primitive import → E0703, trailing use → E0201)
IMP-6Module name = filename semantics; main file missing → E0701✅
IMP-7No transitive re-export: use b::{X} where b merely imports X → E0703 (the visibility-origin check; see AMB-26)✅
IMP-8Import-import collisions: use a::{X}; use b::{X}; → E0301; duplicate name within one list use lib::{A, A} → E0301 (the collision site covers all three shapes; only local-vs-import is tested)✅ (cross-module + within-list shapes)
IMP-9-I precedence among multiple include dirs: earlier dir wins (ordered search) — asserted the way IMP-1 asserts file-dir shadowing✅
IMP-10Top-selection precedence (§3): explicit --top overrides a top-marked component; marker > sole-component rule✅ (override-a-marker now asserted as a contract)
IMP-11Module-name case vs case-insensitive filesystems: use Add2 resolving add2.shdl on APFS but not Linux; two spellings loading one file as two modules — platform-independent behavior pinned (see AMB-28)✅ (case mismatch → E0701 platform-independently; exact case still loads)

Domain J — Metadata contract (MET)​

IDObligationStatus
MET-1Block order & presence: version, ports, hierarchy, source_map, constants, timing, monitors, stats, doc, init✅
MET-2ports: LSB-first lists; single-bit ports as one-element lists; inputs/outputs split✅
MET-3hierarchy: instance tree, types, prefixes, per-instance port→wire maps, bound params recorded✅
MET-4source_map: gate→loc and the lines inverse are mutually consistent (property test over all fixtures, not just spot checks)✅ (property test over all fixtures)
MET-5timing: max_depth, output_depths, is_combinational, has_feedback, critical_path validity✅
MET-6constants / init / stats / doc field-level correctness✅
MET-7Cross-consistency invariants (every name in meta exists in the structural core) — today internal asserts in metadata.py; promote to a public validator with structured errors + direct tests✅ (public validate_meta raising tagged MetaValidationError; survives python -O; per-invariant tests)
MET-8Forward compatibility: consumers (shdlc, BaseEval) ignore unknown metadata keys; version mismatch behavior pinned✅ (shdlc + BaseEval unknown-key tolerance + version-mismatch all pinned; producer always writes version)
MET-9monitors block: emitted (even if empty), consumed by future SHDB — schema pinned now (the flattener hardcodes {} and no population path exists — see AMB-30)✅ (always-present {} across all fixtures — AMB-30)
MET-10source_map column exactness: at least one gate and one instantiation site asserted against a hand-counted column (lines are pinned exactly today; columns only >= 1 — a column-attribution regression would pass the whole suite)✅ (hand-counted columns for direct gates + an instantiation site)
MET-11No absolute paths in emitted bytes: doc.source and source_map keys are basenames — asserted directly (checkout-location independence)✅ (direct basename assert + the absolute load dir never appears in bytes)

Domain K — Emission, serialization & determinism (DET)​

IDObligationStatus
DET-1Exact emitted layout (spacing, blank lines, trailing newline); JSON round-trip; every fixture re-parses faithfully✅
DET-2Byte-identical output across runs; with explicit --top; SOURCE_DATE_EPOCH pinning✅
DET-3Flattener byte-determinism across fresh processes with different PYTHONHASHSEED (compiler side has this; flattener side runs in-process only)✅ (python -m flattener over distinct hash seeds, byte-compared)
DET-4Idempotence property: re-flattening a flattened file yields byte-identical structure (modulo timestamp) — AMB-29 landed a trailing-meta-block production, so emitted Base SHDL re-flattens🟡 (structural-core idempotence pinned on plain-identifier fixtures; bus-port fixtures emit A_1_ which re-flattening rightly rejects via E0304, so they cannot fully round-trip by design — init/meta intentionally outside the structural core)
DET-5Generated C byte-identical across runs and hash seeds✅
DET-6Non-ASCII metadata emission: doc text emitted as raw UTF-8 (ensure_ascii=False), round-trips emit → parse_base, and the bare-stdout path never raises UnicodeEncodeError under a non-UTF-8 locale (-o writes UTF-8 explicitly; stdout writes UTF-8 via sys.stdout.buffer)✅
DET-7Cross-stage acceptance: every fixture's emitted Base SHDL parses via shdlc parse_base and validates via build_circuit✅ (all 16 fixtures incl. stdgates components cross both parse_base and build_circuit)

Domain L — Timing & depth analysis (TIM)​

IDObligationStatus
TIM-1Exact depths on known circuits (add2: Sum_1_=2, Sum_2_=4, Cout=5; adder8 ripple = 3+2/stage)✅
TIM-2Pass-through outputs depth 0; power-pin-fed depth; feedback detection sets has_feedback / clears is_combinational✅
TIM-3Under feedback: max_depth = feedback-free shell only; settle() must be refused — enforced at the oracle (BaseEval) level and, since 2026-06-12, at the driver level (PySHDL settle() raises SettleRefusedError under has_feedback — V.2, test_pyshdl_settle.py)✅
TIM-4Critical path is a real connected path ending at an output (structural property test)✅
TIM-5Settle sufficiency property: for every combinational fixture, poke → step(max_depth) → outputs equal BaseEval fixpoint; step(max_depth−1) may differ (proves bound is tight on at least one input)✅ (tightness witnessed on add2's carry path)
TIM-6Feedback-circuit timing content: per-output output_depths for SCC-driven outputs; critical_path that starts at a feedback gate or power pin rather than an input — exact values pinned on minimal fixtures (srlatch Q/Qn, ring)✅ (srlatch path ['n1_n1','Q'] starts at the feedback NOT; ring path pinned)
TIM-7Degenerate timing blocks: zero-output top → max_depth 0, empty output_depths, empty critical_path; output wired directly from a power pin (ONE[1] -> Y) → depth 1, path [pin_gate, Y] (both live-verified to flatten with exactly these values; nothing pins them)✅

Domain M — Diagnostics system & reporting (DIA)​

IDObligationStatus
DIA-1Every ErrorCode has ≥1 minimal trigger, with valid position (line ≥1, col ≥1) — enforced by completeness assertion✅ (41/41)
DIA-2Per-raise-site coverage: 107 raise err( sites across the pipeline; each site triggered by a dedicated case (a code reached via one path can silently break on another)✅ (mechanical inventory re-derives all 107 sites each run; test_sites_complete enforces registry==inventory, every trigger proves it reaches the exact file:line)
DIA-3CLI surfacing: file:line:col: error[CODE]: message format, exit code 1, OSError path✅ (the complete five-field line pinned exactly + field-by-field partition)
DIA-4Policy decision — multi-error collection: pipeline currently stops at first error. Legacy suite collected several errors per pass. Decide: implement collection (then test multi-error files, counts, ordering) or pin fail-fast as the contract.✅ (pinned fail-fast in shdl.md §14.1)
DIA-5Policy decision — warnings (legacy W01xx: unused input port, unused constant, unconnected instance output). No warning machinery exists. Decide scope; if adopted, extend the completeness matrix to W-codes.✅ (pinned out of scope for V1 — no W-codes, shdl.md §14.1)
DIA-6Policy decision — "did you mean" suggestions (legacy had similar-name hints for E03xx). None exist. Decide; if adopted, test suggestion quality and absence of false suggestions.✅ (pinned out of scope for V1 — no suggestions, shdl.md §14.1)
DIA-7Message quality assertions: offending name/value appears in message (spot-tested today for six codes: E0501/E0502/E0503/E0702/E0703/E0801) — extend to all codes✅ (33 codes mapped to verbatim name/value substrings + 6 name-free structural codes; the two sets partition the full enum — test_dia7_partition_is_exhaustive)
DIA-8Position accuracy: each diagnostic's line:col equals the offending token's actual coordinates, asserted per trigger (today only validity line≥1/col≥1 is checked; a raise site could drift to a wrong-but-valid position undetected)✅ (the DIA-2 matrix asserts exact line:col per site; mutation-checked to catch a 1-column drift)
DIA-9CLI usage-error contract: argparse failures (missing FILE, unknown flag) exit 2 with usage on stderr — distinct from the documented exit-1 diagnostic path; python -m flattener entry point executed by a test✅

Domain N — Simulation model semantics (SIM)​

IDObligationStatus
SIM-1Truth tables for all six primitives, compiled and oracle, all input rows✅
SIM-2Cycle-0 all-zeros; __VCC__ reads 0 at cycle 0 and 1 from cycle 1 (anti-preload pin)✅
SIM-3Unit delay observable: NOT-chains, adder8 carry ripple per-cycle trace (Cout flips exactly at cycle 16), no early visibility✅
SIM-4Fixpoint stability: extra steps after max_depth change nothing (combinational)✅
SIM-5Feedback: SR latch scripted sequence incl. forbidden-state transient (Q==Qn==1 observable — fidelity, not a bug); ring oscillator with exact period N✅
SIM-6Compute-commit order independence: permute instance/connection declaration order of a netlist → identical traces (direct property test; today only implied)✅ (direct property test at the oracle level + scrambled-feedback lockstep lib-vs-oracle)
SIM-7Reset semantics: idempotent, clears inputs+state+dirty, re-applies init seeds, constants take effect on first evaluation after reset (shdlc_goals' 'all-zero' wording contradicts the implemented & tested seed re-application — AMB-39)✅
SIM-8Init-at-load: fresh library has seeds applied without a tick✅
SIM-9Exhaustive small-circuit grids (add2: all input combos × cycles 0..6)✅
SIM-102-state guarantee: no value other than 0/1 ever observable per single-bit wire (guaranteed by the poke per-bit &1u scatter plus Boolean closure of the gate ops — not by the uint8 casts; assert in debug-build tests later)✅ (single-bit wires asserted always two-state at the port boundary; full-gate-state closure staged for the debug build under V.1)
SIM-11Input persistence (§11.1): poked component inputs hold their values across cycles until changed — direct assertion (poke once, step N, input peek unchanged, dependent outputs consistent at intermediate cycles)✅ (named contract: held inputs keep outputs pinned across cycles; persistence ends only on an explicit re-poke)

Domain O — Compiled ABI (release) (ABI)​

The release library exports six functions: reset, poke, peek, step, plus the later-landed step_settle and run_batch (ABI-11/ABI-12; shdlc_goals still documents four — AMB-38).

IDObligationStatus
ABI-1poke: user-facing names, LSB packing, masking to port width (2/8/64-bit), arms dirty flag✅
ABI-2peek: lazy exactly-one-tick on dirty output read; input peek never ticks nor clears dirty✅
ABI-3step(0) no-op preserving dirty; step(n) advances exactly n✅
ABI-4step with negative argument: pinned as no-op✅ (step(1) discriminator distinguishes a true no-op from one-tick-plus-dirty-clear)
ABI-5Unknown signal poke/peek: stderr message, no-op / returns 0, does not arm or consume dirty✅
ABI-6Signal-name case sensitivity pinned (see AMB-1)✅
ABI-7peek return zero-extension above port width pinned (see AMB-13)✅ (dedicated contract test + spec amended — AMB-13)
ABI-8Ports wider than 64 bits: uint64_t cannot carry them — shdlc must reject at compile time with a structured error; pinned by test✅ (end-to-end .shdl >64-bit port: flattens fine, shdlc rejects with a structured error)
ABI-9Multiple libraries in one process: distinct circuits, and two copies of the same circuit (dlopen cache-busting, symbol isolation via -fvisibility=hidden)✅
ABI-10Zero-input, zero-output, zero-gate circuits load and obey the ABI✅
ABI-11step_settle(n): observably ≡ step(n) with fixed-point early exit; returns ticks actually run (1 at an existing fixed point); n ≤ 0 no-op preserving dirty; oscillators never early-exit and run the full budget✅
ABI-12run_batch(in, out, count, cycles, settle): per-frame scatter/advance/gather ≡ the same poke/step/peek sequence (both settle modes, mid-settle budgets); cycles ≤ 0 takes the lazy-peek path; count 0 no-op; zero-input-port frames legal✅ (zero-output-port through run_batch + dirty-stays-set when nop==0 ∧ cycles≤0 now pinned)
ABI-13Export-set hygiene: the visible symbol set is exactly the six ABI functions — nothing else (no get_cycle in release, no leaked statics)🟡 (asserted via nm on macOS; source-level SHDLC_API/static scan exists; the Linux ELF export-set variant remains macOS-gated, deferred to a Linux CI pass — ties CCT-6)
ABI-14poke on an output port name resolves to the unknown-signal path (stderr names it, complete no-op, dirty untouched) — tested with a real output name; decide whether it deserves a distinct diagnostic✅
ABI-15Empty-string signal name takes the unknown-signal path even on zero-port circuits (the dummy { "", 0, 0 } table entries must never match — only the NUM_*_PORTS loop bounds protect them)✅
ABI-16NULL signal name to poke/peek: currently undefined behavior (unguarded strcmp) — decide guard-vs-precondition and pin (see AMB-32)✅ (generated C guards NULL → unknown-signal path, never UB; ctypes-tested — AMB-32)
ABI-17Double poke of the same port within one dirty batch (poke A=x; poke A=y; peek): last value wins, still exactly one lazy tick (re-poke is tested only across peek batches; the fuzzer samples distinct ports, so it never hits this)✅
ABI-18Load-time constructor is __GNUC__-gated: on non-GNUC builds no constructor runs, so init-at-load (SIM-8) does not hold and an explicit reset() before first use is the contract — pin when the Windows toolchain lands (ties CCT-6)⚪
ABI-19Fully-empty circuit (0 inputs, 0 outputs, 0 gates) loads and obeys the ABI (ABI-10 tests each degenerate axis separately, never all at once)✅

Domain P — Codegen & C toolchain (CCT)​

IDObligationStatus
CCT-1Structural codegen unit tests (wire table ordering, array bounds, two-buffer swap, chunked tick)✅
CCT-2CC discovery (CC env, PATH, precedence), per-platform flags/suffix✅
CCT-3Zero-warning generation under STRICT_CFLAGS for every test build, including >1024-gate chunked tick✅ (CLI builds via --cc "cc -Wall -Wextra -Werror -pedantic" — a strict build exiting 0 IS the proof, incl. the chunked tick)
CCT-4-O0 ≡ -O3 trace equality✅
CCT-5Sanitizer builds: run the differential suite against -fsanitize=address,undefined builds of generated C (UB in generated code is otherwise invisible)✅ (generated C + a driver main() built into a standalone ASAN+UBSAN executable, run as a subprocess and checked vs a BaseEval trace; self-gates on a probe)
CCT-6Platform matrix: today macOS/clang only. CI must add Linux/gcc + Linux/clang; Windows/MSVC best-effort later (shdlc_goals §8)🟡 (ci.yml matrix {ubuntu/gcc, ubuntu/clang, macos/clang} + nightly.yml authored and YAML-validated, $CC honoring verified; the Linux/gcc + Linux/clang generated-C builds are UNPROVEN — need one real CI pass; Windows/MSVC still later)
CCT-7Frontend robustness: malformed Base SHDL / metadata into shdlc → structured errors (model invariants, ~40 negative cases)✅ (malformed meta JSON now wrapped in BaseParseError — the raw json.JSONDecodeError crash closed; all 7 BaseParseError sites triggered — CCT-9)
CCT-8Model validation completeness: every ModelError path exercised (mirror of DIA-2 for the compiler side)✅ (5 remaining sites closed: model-level unknown-primitive reached directly, non-object ports, non-array wires, unknown input-port wire, non-object meta.init)
CCT-9Base-reader diagnostics per raise site (7 BaseParseError sites: scanner EOF, expect mismatch, non-identifier, unknown primitive, duplicate instance, one-component-per-file/trailing token, trailing content after meta) — each via a minimal artifact asserting the message; malformed meta JSON wrapped in BaseParseError, never a raw JSONDecodeError✅
CCT-10shdlc CLI contract: exit 0/1/2; default artifact naming (input stem + platform suffix, in CWD); --emit-c alongside build; --no-build with its default .c path; --base/--shdl overrides; --base+--shdl exclusion pinned at exit 2; --top/-I rejection for Base inputs; flattener-SHDLError surfacing branch; --cc plumb-through✅ (--shdl flag, --no-build default .c path, SHDLError-surfacing branch, exclusion pinned at exit 2 exactly, --cc at CLI level all closed)
CCT-11build_library temp hygiene: the mkstemp'd .c is unlinked on success and on CCError; with --emit-c the C file persists; no stray temp files after a failed build✅
CCT-12find_cc whitespace-only/empty value (explicit arg or $CC that shlex-splits to nothing) → CCError, no fallback✅
CCT-13Minimal-consumer guarantee end-to-end (base_shdl §2): a Base SHDL file with no meta section compiles to a working library; identity single-bit ports synthesized from the header; sim parity vs oracle✅ (a meta-less artifact built and run with synthesized identity ports, sim parity vs oracle)
CCT-14C-compile failure surfacing: CCError carries the exact argv and the compiler's stderr verbatim; str() names the exit code; CLI prints a single clean diagnostic, rc 1✅
CCT-15in_wires_* initializer wrapping past the 80-col limit: 8 names per line, comma discipline, closing brace — structurally pinned✅ (the wrapped text format structurally asserted: 8 names/line with comma discipline)
CCT-16Hot-path purity of generated C (shdlc_goals §3.2/§5.3): step() free of strcmp/fprintf/malloc/branches; tick() additionally loop-free; strcmp confined to the poke/peek scans✅
CCT-17The n-only tick_chunk_k signature variant (chunk with no cur[] reads) actually compiles under STRICT_CFLAGS and lockstep-matches BaseEval✅ (a circuit whose chunks read no gate outputs emits and builds the n-only signature, lockstep-matches BaseEval)
CCT-18Base-level naming-reservation asymmetry: shdlc accepts __x / Sum_2_ gate names unchecked while fully re-validating §3.6 connection rules — pin the trust as the contract with an acceptance test, or add rejection (see AMB-36)✅ (acceptance test pins the trust; §3.6 wiring rules still re-validated — AMB-36)
CCT-19Self-containedness (shdlc_goals §8): the built library's undefined-symbol set is a subset of libc — no other runtime dependencies (nm -u / platform equivalent)✅ (nm -u subset-of-libc; macOS-gated, ties CCT-6 for the Linux equivalent)

Domain Q — Differential & fuzz testing (FUZ)​

IDObligationStatus
FUZ-1Fixture differential: 10 fixtures + 10 handwritten minimal netlists, 150 cycles random poke/step/reset, all ports compared every cycle✅
FUZ-2Netlist fuzzer: random valid Base SHDL (5–80 gates, optional feedback), self-checked against BaseEval, then lib-vs-oracle lockstep; env-scalable✅
FUZ-3SHDL-source fuzzer: generate random high-level programs (params, generators, conditionals, slices, concat, constants, init, hierarchy) → flatten → HighEval vs BaseEval vs C. The flattener is the most complex component and currently has no randomized adversary.✅ (valid-by-construction generator + flatten → HighEval/BaseEval lockstep, env-gated compiled-C tier; campaigns to 1000/1000 found no flattener/oracle divergence)
FUZ-4No-crash fuzzing of the frontend: mutated/truncated/garbage .shdl text → always SHDLError-or-success, never a raw traceback (targets the 22 asserts, 7 raw exceptions, 25 raise AssertionError guards, and the implicit decode/JSON/env crash paths — ROB-5/ROB-6)✅ (SHDLC_NOCRASH=2000 + 66k out-of-band inputs, 0 breaches; the shdlc-CLI invalid-UTF-8 path now diagnosed via BaseParseError)
FUZ-5Action-sequence adversary: randomized interleavings emphasizing reset-after-poke, peek storms, step(0) runs (the dirty-flag state machine — mutation-killing cases exist; generalize)✅ (full-ABI adversary modeling the C per-frame dirty state machine in lockstep + a double-poke mutation killer; verified across 1000 seed-sequences)
FUZ-6Soak tier: nightly large-N campaign with frozen failure artifacts✅ (nightly.yml soak job — cron + all campaign knobs cranked across ubuntu+macos; CI execution unproven, ties CCT-6)
FUZ-7(Optional, post-V1) Mutation testing over flattener+shdlc to measure suite strength❌
FUZ-8DualSim observability boundary: port-only comparison is blind to internal divergences that cancel at the ports for the driven stimulus — pin the limitation in the harness docstring now; close via V.1 full-gate-state lockstep (peek_gate at every compared cycle)✅ (limitation pinned in the DualSim docstring; full closure staged under V.1)

Domain R — Conformance corpus (CNF)​

IDObligationStatus
CNF-1Corpus: primitives, derived gates, arithmetic, state/feedback, language features, degenerate cases, ABI tiers (v1.0.0 baseline 32/34, grown to v1.1.0 — 38 cases / 40 traces via CNF-4)✅
CNF-2Tier A byte-equality on expected.base.shdl; Tier B/C trace replay through oracle and C lib with lockstep divergence check✅
CNF-3Integrity & provenance policing (manifest bijection, never-from-C rule, regen policy)✅ (bijection/coverage/counts + the CNF-6 corrupted-corpus negatives, CNF-7 regen tests, and the CNF-9 mechanical drift fence now close all the holes)
CNF-4Corpus additions required by this document✅ (six cases added — nested_repl, diamond, degen_slice, deep_hier, init_comb, cond_matrix; suite v1.1.0, 38 cases / 40 traces)
CNF-5Every future component (PySHDL, debug build, backends, optimization tiers) consumes the corpus as its acceptance gate — wiring obligation, restated per component in Domain V🟡 (PySHDL wired 2026-06-12: the full corpus replays through the public Circuit API — from_base build once per case, from_library per trace, strict=False — bit-exact via conformance.runner.executor, tests/pyshdl/test_pyshdl_conformance.py; the debug build / backends / optimization tiers remain ⚪ until built — Domain V)
CNF-6Integrity-checker self-test: a corrupted-corpus matrix (missing/stray artifact, de-listed case, unsorted lists, wrong _format, ops[0] not reset, bad op keys, out-of-range values, unknown feature, empty provenance, unknown-signal trace via check_trace_signals) — each → ConformanceError naming the artifact (today only the healthy corpus is proven to pass; a silently broken checker keeps passing)✅ (full corrupted-corpus matrix over a sandboxed copy)
CNF-7Regen tooling contract: dry run writes nothing and prints the full diff; --write applies exactly the shown diff and touches only golden bytes/expect values (stimulus, metadata, descriptions frozen); unknown case → error listing names — pinned over a sandboxed corpus copy (gen.py is the only sanctioned golden writer and is completely untested)✅
CNF-8Report determinism: two consecutive runner invocations emit byte-identical reports; CLI surface (exit codes, --tier/--filter, list) tested✅
CNF-9Drift enforcement: a changed golden byte without a major suite_version bump + changelog entry fails mechanically (per-golden content hashes in MANIFEST.json or a CI diff against the last release) — closes the coordinated-regen loophole that live checks cannot see✅ (per-golden sha256 golden_hashes map in MANIFEST.json, bijective with the on-disk golden set; refreshed only on the sanctioned regen --write)

Domain S — Scale & stress (SCL)​

IDObligationStatus
SCL-1>1024-gate chunked tick correctness (differential, warning-free)✅
SCL-2CPU-scale build (~4×10⁴ gates — 42,530 measured) exercised continuously via T4✅
SCL-310⁵-gate synthetic: flatten + compile + 100-cycle differential smoke (known clang misched risk on giant tick — see perf memory; this is the regression guard)✅ (commit-time chunking-engages assert + env-gated STRICT build & 100-cycle differential)
SCL-4Generator emitting 10⁴ instances; 100-specialization monomorphization (MON-7); 1 MB source file parse✅
SCL-5Hierarchy 50 levels deep; generator nesting depth cap✅ (50-level instantiation chain + 200-vs-201 parser nesting-cap probe)
SCL-6Documented limits: max wire index (65535) and max port width (64) — both already have cap and cap+1 boundary tests; holes: the limits appear only in code comments, not in base_shdl.md/shdlc_goals.md; a max-gate-count limit is neither defined nor tested✅ (limits documented in shdl.md §14.2, base_shdl.md §6, shdlc_goals.md §3.2; max-gate-count decided as deliberately uncapped)
SCL-7Pathological single ranges ([1:10⁹], huge replication counts via params): documented instance-count cap with a structured diagnostic — never an unbounded list materialization / OOM hang (range_values materializes Python lists unguarded at four sites; mirror of the compiler-side wire cap)✅ (the four range_values sites AND the RRepl replication-count site are guarded by MAX_RANGE_VALUES=1_000_000 → E0601; tested at cap, [1:10⁹], param-driven, open-range, and literal-count replication {10⁹{Sign}})

Domain T — Robustness: the no-crash guarantee (ROB)​

The re-derived inventory: 7 raw builtin-exception raise sites (KeyError in loader.py; ValueErrors in high_eval.py, codegen.py, and base_eval.py ×4 — count and locations verified), 22 bare assert statements (9 of them verify_meta's — MET-7/ROB-3), 25 raise AssertionError invariant guards (a third class the original inventory missed — ROB-5), and four implicit raw-exception paths reachable from user input, each reproduced as a live crash: invalid UTF-8 through both CLIs, malformed meta JSON through shdlc, garbage SOURCE_DATE_EPOCH through the flattener (ROB-6) — plus the lexer's ² → int() ValueError (LEX-9). Policy:

IDObligationStatus
ROB-1Each raw-exception path: either (a) prove unreachable from user input and convert to an internal-invariant assert with a comment, or (b) convert to a structured diagnostic — then test it. Notably: codegen.py unknown gate type, loader.py cache KeyError.✅ (loader KeyError and codegen.py unknown-gate-type → commented internal-invariant AssertionErrors (route a); base_eval init-key + poke-overflow paths pinned; BaseEval settle-refusal pinned)
ROB-2Oracle API errors are intentional (BaseEval.poke overflow raises ValueError — it's a test harness, stricter than the C ABI's masking). Pin this asymmetry explicitly in a test + docstring so it's never "fixed" into agreement by accident.✅ (pytest.raises pins on both BaseEval and HighEval poke overflow + the stricter-than-ABI intent in both docstrings)
ROB-3metadata.py asserts → public validator (MET-7)✅ (validate_meta + MetaValidationError)
ROB-4FUZ-4 (frontend no-crash fuzzing) is the enforcement mechanism for this domain✅ (see FUZ-4)
ROB-5The 25 raise AssertionError invariant guards (monomorphize 7, expander 6, expr 4, expand 4, flatten 3, high_eval 1): classify each unreachable-from-user-input (comment) or convert to a structured diagnostic, per the ROB-1 policy; FUZ-4 targets these too✅ (each match-exhaustiveness guard carries a # pragma: no cover + unreachable-by-construction justification; FUZ-4 campaigns hit none)
ROB-6Implicit raw-exception paths (all reproduced): invalid-UTF-8 source → UnicodeDecodeError escapes both CLIs; non-JSON meta block → json.JSONDecodeError escapes shdlc (not a BaseParseError); malformed SOURCE_DATE_EPOCH → ValueError/OverflowError escapes the flattener CLI — catch-or-convert at each boundary, then test (LEX-10, CCT-9, DET-2's env var)✅ (flattener-side UTF-8 → positioned E0101, meta JSON → BaseParseError, SOURCE_DATE_EPOCH → structured E0001, shdlc-CLI invalid UTF-8 → BaseParseError naming the byte offset — all closed and tested)

Domain U — Spec ambiguities: decide, amend, pin (AMB)​

Each item needs: a decision, a spec amendment, a unit test, and (where observable) a conformance case. AMB-1..12 came from the original spec-extraction pass; AMB-13..41 from the 2026-06 full audit.

Resolution state (2026-06-12): all 41 AMBs are decided, spec-amended, and pinned. WP-C applied every Domain-U amendment to shdl.md / base_shdl.md / shdlc_goals.md as native prose (no AMB-NN literals in the docs); the behavior is implemented in the flattener/shdlc and exercised by a named unit test (the cross-references in the resolution column point at the owning obligation row). No new ErrorCode was minted: every resolution reuses an existing code — AMB-27 (a re-used module name resolving to a different file) lands as E0701 in the loader (test_imp*_… / the AMB-27 loader test), not the speculative new E07xx the original proposal floated. Observable cases carry conformance coverage (CNF-4: diamond for AMB-27-adjacent import identity, init_comb for AMB-3/AMB-39 power-on, abi_poke_masking for AMB-2, cond_matrix for the operator matrix). The table below is the decision register; the Resolution column states what was pinned.

IDAmbiguityResolution (decided · amended · pinned)
AMB-1poke/peek signal-name case sensitivitycase-sensitive (matches language)
AMB-2poke value larger than widthmasked modulo 2^width (already implemented & tested — amend spec to say so)
AMB-3peek before any step on fresh loadinit-seeded state, lazy tick applies (implemented — amend spec)
AMB-4Overlapping compound ranges [1:4, 2:6]iterate verbatim; duplicate emissions collide naturally (E0301/E0501)
AMB-5Open range with expression bound >i[N/2:]allowed; governing-signal rule applies after evaluation
AMB-6Replication count 0already implemented: E0601 'count must be ≥ 1' covers 0 and negative — amend spec + pin (EXP-6)
AMB-7init value overflow on multi-bit porterror E0A03 (implemented — amend spec)
AMB-8Does step(0) / lazy tick increment get_cycle?step(0): no; lazy peek tick: yes by 1 (pin when debug build lands)
AMB-9peek_gate on power pinsobservable like any gate (pin when debug build lands)
AMB-10get_gate_info enumeration order stabilitystable = state-layout order, invariant across reset (pin when debug build lands)
AMB-11Huge literals (≫64 bits) in constantslegal in flattener (unbounded ints); >64-bit port connection rejected (ABI-8)
AMB-12ZERO = 0 inferred widthwidth 1 (a single GND bit) — implemented & byte-frozen in the corpus; amend spec + direct unit pin (CON-4)
AMB-13peek return bits above port widthalways zero-extended to uint64 (bits ≥ width are 0, on every read, incl. after over-wide poke and after reset) — symmetric with AMB-2; dedicated test + amend shdlc_goals §3.1 (ABI-7)
AMB-14Identifier/digit character set: Unicode-wide today (é → IDENT, ٣ → NUMBER, ² → raw ValueError crash); spec's LETTER/DIGIT undefinedpin ASCII-only [A-Za-z]/[0-9]; any other non-whitespace, non-comment char → E0101 — fixes the ² crash class and makes LEX-7 implementable (LEX-9)
AMB-15Source encoding: undecodable UTF-8 dies at the read boundary (loader, byte offset only) before any lexing; decodable non-ASCII reaches the lexerpin UTF-8 source; undecodable input → positioned diagnostic raised at the read boundary, mirrored in shdlc for Base inputs (LEX-10, ROB-6)
AMB-16Component-body block order: §13's EBNF fixes decl→init→connect, §4.2's prose allows interleaving, the parser accepts any orderpin any-order; the count rules (E0309/E0310) remain the only structural constraints; amend the §13 note (PAR-11)
AMB-17ConstantDecl width grammar: §8's EBNF says literal NUMBER, §13 says ArithExpr; parser implements ArithExpr and validate defers param-dependent widths§13 wins; amend §8; param-dependent constant widths are overflow-checked per instantiation (CON-7)
AMB-18N{x} replication-vs-template tie-break: an IDENT count over a brace group holding a single arithmetic expression parses as a template, contra §13's notepin the parser's rule in the spec: replication requires a literal count or signal-only group syntax; parse tests both ways
AMB-19Generator range value domain: 0 accepted, negative → E0601; §7.1's table silent; E0601's enum text ('empty or ill-ordered') doesn't match its 'is negative' sitespin bounds ≥ 0 legal (0 iterates/emits index 0), negative → E0601; widen E0601's description; accept- and reject-side tests (GEN-13)
AMB-20Open-range governing signal: §7.1 says 'exactly one multi-bit signal' but the implementation collects a width set — several same-width signals are legal (GEN-2's own test relies on this)amend spec to the implemented rule: all loop-var-indexed signals must agree on one width; differing widths → E0602 (GEN-14)
AMB-21Governing-signal scan vs conditional branches: both when/else bodies are scanned before any condition is evaluated, so a never-emitted branch can ambiguate resolutionpin the conservative implemented behavior; amend §7.1; test an else-branch width forcing E0602 (GEN-14)
AMB-22Generator range scope under shadowing: >N[N]'s range evaluates in the enclosing scope (shadowed param visible), body sees the loop varpin current behavior; amend §7; unit test >N[N] with param default (MON-6)
AMB-23Diagnostic precedence when a user component shadows a dunder primitive (component __VCC__ violates both E0305 and E0303)pin E0305-wins (the more specific code); assert in VAL-2's completion tests
AMB-24Import search order: §9.1 says 'current directory first'; the loader (deliberately, per its docstring) searches the importing file's directory first, and IMP-1's test pins thatamend §9.1 to 'the importing file's directory, then -I dirs in order' (relocatable, deterministic; already tested)
AMB-25§9.2 claims importing primitives from stdgates is 'harmless but unnecessary' — impossible: stdgates cannot define primitive names (E0305) and importing an undefined name is E0703amend §9.2: primitive names are built in, cannot be defined or imported (E0703 if requested); negative test (IMP-5)
AMB-26Re-export of imported names: the loader requires a name to be defined in the target module (origin check) — no re-exports; unspecifiedpin no-re-export (E0703); amend §9.1; three-module test (IMP-7)
AMB-27Module identity is keyed by bare name, not resolved path: two different lib.shdl files reachable from different importers silently bind to whichever loaded firstpinned: module names are program-global; a resolved-path mismatch against the loaded module → E0701 ('resolves to two different files', naming both) — no new code minted
AMB-28Module-name case vs case-insensitive filesystems: use Add2 resolves add2.shdl on APFS, fails on Linux; two spellings can load one file as two modulesverify the real filename case; mismatch → E0701 (platform-independent) (IMP-11)
AMB-29Is the meta { … } block valid SHDL? The grammar has no production; re-flattening emitted output fails E0201 at meta, so DET-4 is undefinedflattener accepts and ignores a trailing meta block (one small production); DET-4 idempotence defined on the structural core (DET-4)
AMB-30Who populates monitors? base_shdl §4.8 says 'flattener or user annotations'; no annotation syntax or population path exists — always {}pin V1: always {}; schema {group: [gate_or_wire,…]} reserved; amend §4.8; stage population under FUT (MET-9)
AMB-31E0403 vs E0906 attribution is path-dependent: A[0] inside a parameterized component raises E0906 even with a literal width; identical A[0] elsewhere raises E0403pin: inside a specialization, any non-positive port width is E0906; elsewhere E0403; amend §14; test both branches of the shared site (VAL-7)
AMB-32NULL signal name to poke/peek: unguarded strcmp — UB/segfault, at odds with crash closureguard in generated C: NULL takes the unknown-signal path (stderr, no-op/0, dirty untouched); test via ctypes (ABI-16)
AMB-33meta.ports covering only a subset of header wires: uncovered inputs are stuck at 0 and ABI-unreachable, uncovered outputs unobservable — silentlypin legal-but-defined (matches the referenced-bits philosophy) and amend base_shdl §4.2 — or require total coverage with a ModelError; then test
AMB-34Duplicate wire references in meta.ports (within one port or across ports of one direction): silent last/highest-bit-wins aliasingreject duplicate use of an input wire (ModelError); allow output-wire reuse (harmless fan-out); negative cases both shapes
AMB-35meta.ports present but a direction key absent: zero ports of that direction with no diagnostic (identity synthesis fires only when ports is wholly absent)require both inputs and outputs keys whenever ports is present (ModelError naming the missing key)
AMB-36Base SHDL naming reservations (§3.5 __, §6 …_<digits>_): producer-side obligation or consumer-enforced? shdlc accepts both silently while §3.6 connection rules are fully re-validatedpin producer-side: consumers MAY accept violating names without semantic interpretation; state in base_shdl §6 + shdlc acceptance test (CCT-18) — or add rejection
AMB-37base_shdl §3.6 omits two rules shdlc enforces and tests: input wires cannot be connection destinations; only pin O is readableamend §3.6 with both rules (implementation and tests already conform)
AMB-38Release ABI surface drift: step_settle/run_batch are exported, tested, and symbol-asserted but absent from shdlc_goals §3.1/§3.2/§10 (their pointer params sit outside §3.2's type list)amend goals to document both with the observable-equivalence contract; extend §3.2 to 'primitive C types and pointers to them'; clarify run_batch borrows, never owns (ABI-11/12)
AMB-39Reset semantics contradiction: shdlc_goals says 'all-zero' in §2.4/§3.1/§7.1; base_shdl §4.11 + the implemented, tested behavior re-apply init seedsamend shdlc_goals: reset() returns to the power-on state — all-zero except meta.init seeds (SIM-7)
AMB-40Debug-build name lookup namespace: a port and a gate may legally share a name; V.1's peek_gate needs a rule before it landspin separate namespaces: poke/peek address ports exclusively, peek_gate gates exclusively; SHDB qualifies on collision; record in shdlc_goals
AMB-41shdl.md §11.4's SRLatch example does not compile as printed (uses NOR without use stdgates::{NOR})fix the spec example; the fixture already pins the corrected form

Domain V — Future components: obligations defined now (FUT) ⚪​

These activate when each Layer-1+ component is built. The legacy suite's debugger catalog maps here — none of it is lost, it is staged.

V.1 SHDLC debug build — release ≡ debug differential on the full fixture+fuzz corpus; peek_gate / peek_gate_prev (current vs previous buffer — the two-buffer model makes "prev" nearly free and the test must prove it's really the previous cycle); get_cycle semantics (AMB-8); gate enumeration (count, names, type codes 0–5, chunk/lane), O(1)-ish lookup scaling test at >1000 gates; .shdb = embedded meta + state_region, byte-equal JSON both places; upgrade the differential harness to full-gate-state lockstep (closes FUZ-8); name-namespace rule per AMB-40.

V.2 PySHDL — multi-bit poke/peek via ports; settle() = step(max_depth), refused when has_feedback; power-on reset honoring init; context manager & dict-style access; boundary re-runs of the legacy "comprehensive" catalog (pass-through 0–255, 0xFFFF patterns, bitwise ops, nibble extraction) — mostly subsumed by conformance traces but cheap to mirror at the Python API. EXECUTED 2026-06-12 — PySHDL is built (SHDL/ package); the full V.2 obligation set is green (tests/pyshdl/, 130 tests over 10 modules + the session-scoped ExampleCache fixture in conftest.py). Per-obligation closure:

  • multi-bit poke/peek via ports, dict-style access, strict/non-strict value policy, port/circuit introspection — ✅ test_pyshdl_ports.py (test_multibit_poke_peek, test_dict_access_is_poke_peek, test_strict_accepts_the_full_unsigned_and_twos_complement_range, test_nonstrict_masks_modulo_width, test_peek_is_zero_extended, test_unknown_poke_lists_inputs, test_port_info/test_circuit_info)
  • settle() ≡ step(max_depth), refused under has_feedback, step_settle early-exit/oscillator-never-exits — ✅ test_pyshdl_settle.py (test_settle_equals_explicit_step_max_depth, test_settle_refused_under_feedback over srLatch/dLatch/ringClock, test_step_settle_early_exit_is_observably_identical, test_step_settle_never_exits_on_an_oscillator)
  • power-on reset honoring init (power-on state, idempotence, reset ≡ fresh load, init metadata) — ✅ test_pyshdl_reset.py (test_power_on_state_is_init_seeded, test_reset_restores_the_power_on_state, test_reset_is_idempotent, test_reset_equals_a_fresh_load, test_init_metadata_is_exposed)
  • context manager, all four constructors, two-instance independence (same source AND same library), close/del safety, artifact lifecycle — ✅ test_pyshdl_construct.py (test_context_manager_closes, test_two_instances_over_one_source_are_independent, test_two_instances_over_one_library_file_are_independent, test_use_after_close_raises, test_del_without_close_is_safe, test_artifacts_cleaned_by_default/test_keep_artifacts_retains_managed_dir/test_explicit_build_dir_is_retained)
  • legacy "comprehensive" boundary catalog at the Python API (pass-through 0–255, SignExtend 0xFFFF, ALU bitwise/add boundaries, nibble split/swap) — ✅ test_pyshdl_boundary.py
  • run_batch ≡ poke/step loop (both settle modes, 50 random frames, hold semantics, lazy cycles=0, validation) — ✅ test_pyshdl_batch.py (test_batch_equals_the_explicit_loop, test_batch_holds_omitted_inputs, test_batch_with_lazy_evaluation_cycles_zero, test_batch_validates_names_and_values)
  • structured error surface (FlattenError/CompileError/BuildError, typed dispatch, bare-library degradation matrix → MetadataUnavailableError, exception hierarchy) — ✅ test_pyshdl_errors.py
  • OracleSim-vs-Circuit random-stimulus lockstep (adder8/mux2/srLatch/dLatch, 150 actions, all ports every action) — ✅ test_pyshdl_lockstep.py (test_random_stimulus_lockstep)
  • conformance corpus replayed through the public Circuit API (CNF-5) — ✅ test_pyshdl_conformance.py (test_corpus_trace_replays_bit_exactly)
  • SR16 CPU smoke test through the public API — ✅ test_pyshdl_cpu.py (test_sr16_runs_a_program)

V.3 SHDB debugger — port the legacy catalog wholesale: DebugInfo loading & JSON fidelity; SymbolTable resolution + completion; controller poke/peek/step/StopInfo; internal-gate inspection mid-propagation; breakpoints (value-change, conditional, temporary, hit counts, enable/disable/clear); watchpoints; run-until-breakpoint with max_cycles; bulk inspection; robustness (peek before step, 5 resets, 1000 steps, context-manager cleanup); SourceMap tolerance of missing locations.

V.4 Testbench runner (.shtb) — format round-trip; conformance traces expressible as .shtb and replayable bit-exactly (the corpus becomes the runner's own acceptance test).

V.5 Backends — Verilog: cross-validate against Verilator on the conformance corpus (independent implementation of the unit-delay model — the strongest external check available). BLIF: ABC formal equivalence between flattener output and known-equivalent circuits (CLA vs ripple adder). Both: corpus Tier A inputs → backend → external tool agrees with golden traces.

V.6 State Region — header magic/version, layout documented in .shdb matches reality (offset-poke equals ABI-poke), identical layout regardless of allocation, shared-memory read-only observer sees live values.

V.7 Optimization tiers (Layer 7) — every tier: full conformance + differential + fuzz at parity with V1, bit-exact, per the "validated bit-exactly against V1" mandate. The suite as specified here is the gate; no new obligations, just re-execution per tier.

Domain W — CPU acceptance tier (CPU)​

IDObligationStatus
CPU-1Every ISA instruction lockstep vs golden model (PC, R0–R7, flags, halt, cycle cost after every instruction)✅
CPU-2ALU: 13 ops × corner + random vectors × carry-in✅
CPU-3Power-on fixed-point regression: SRLatch — oracle full-gate-state fixed point (ticks 1–8); DLatch/MSFFE/RegE plus RegFile, RAM256 (incl. write-isolation) and the SR16 top's visible state held across the 15-offset settle sweep (the consecutive 1–8 prefix covers every phase of period ≤ 8)✅
CPU-4Settle-margin invariance (doubled timing budgets → identical architectural trace)✅
CPU-5Real programs: fib, gcd, memcpy, recursive sum, array wraparound — lockstep plus hand-derived headline constants (so the golden model cannot common-mode vouch); RAM/regfile/seq element units✅
CPU-6Reserved encodings via .word (shift functs 6/7 behave as NOT under lockstep; MISC/branch reserved words; canary register checks)✅
CPU-7Combinational datapath part units: Mux8N/Mux16N selection, Dec2/Dec3/Dec4 exhaustive one-hot, Inc16 corners incl. 0xFFFF wrap, Or16 reduce✅
CPU-8DMA port (ISA.md §6): full 256-word image load + bit-exact readback through LdEn/LdWe/LdAddr/LdData; LdEn freeze of a running CPU asserted directly✅ (freeze of a mid-execution CPU asserted directly: zero drift + RAM untouched + clean resumption)
CPU-9Memory-model aliasing (ISA.md §1): data addresses alias mod 2^A (incl. negative-EA wrap mod 2^16), instruction fetch aliases mod 2^A, PC wraps 0xFFFF→0✅
CPU-10HALTED is terminal (ISA.md §4): extra clocks after HALT change no visible architectural state✅
CPU-11ALU Op[4] codes 13–15: the reference models them as adder pass-through but no test drives them — pin at unit level or prove control never emits them✅ (unit-pinned 13/14/15 + decode analysis showing control never emits them)
CPU-12Shipped example programs (examples/CPU/programs: mul.s, popcount.s, maxarray.s) run under lockstep vs golden (today only the profiling scripts touch them)✅ (all three under lockstep + hand-derived headline constants)
CPU-13Lockstep input-space corners: an ISA-level write to R0 (no program targets rd=0 — a writeback special-case bug would be invisible at system level); shift-group V-preservation witnessed with V=1 pre-set (today only ever V=0)✅ (R0 writeback across all RegWDataSel sources + shift-group V-preservation with V pre-set to 1)

3. Comparison with the legacy SHDL test suite​

Direct answer to: does the new suite cover the old catalog, and does it cover more?

Legacy area (~280 tests)New suiteVerdict
Lexer: tokens, identifiers, dec/hex/bin, keywords, comments, line numberstest_lexer + diagnostics matrix; adds column tracking, doc-comment extractionCovered+ (LEX-3 uppercase prefixes and all 6 LEX-6 literal-error triggers — 0xGHI, bare 0b, 12ab — now closed)
Parser: components, widths, imports, constants, generatorstest_parser + 4 more phase suites; adds params, conditionals, init, replicationCovered+
Flattener: generator expansion, constants→power pins, hierarchy prefixing, valid re-emittest_expand/expander/constants/flatten/emit; adds monomorphization, alias-cycle detection, byte-determinismCovered+
Functional: primitive/derived truth tables, propagation timing, adders to 8-bit, multi-bit patterns, mux/decoder, reset, boundary valuescompiler suite + equivalence + conformance: truth tables on compiled C and two oracles, per-cycle ripple traces, exhaustive add2 grid, masking/64-bitCovered+ (mux/decoder live in CPU parts tests — CPU-7; legacy 16-bit patterns subsumed by SR16 + WIDE64)
Diagnostics: per-code coverage, line/col, integration multi-error files41 codes, 100% enum-enforced, per-raise-site matrix (107 sites, DIA-2), exact line:col (DIA-8), full five-field CLI format (DIA-3)Covered+ for codes and sites; the legacy multi-error/W-codes/"did you mean" features are now pinned as deliberate V1 non-goals (DIA-4/5/6) rather than gaps
Debugger (.shdb, breakpoints, watchpoints, symbol table…)Not built yet (Layer 1)Staged, fully specified in FUT V.3
Legacy's own admitted gaps: sequential elements, >16-bit, error recoverySR latch/D latch/MSFFE/ring oscillator/power-on metastability; 64-bit ports, 16-bit CPU, >1024-gate chunkingNew suite closes the legacy gaps; parser error recovery is now a pinned non-goal (fail-fast, DIA-4)
(no legacy equivalent)Differential oracle lockstep, netlist fuzzing, frozen conformance corpus, determinism across hash seeds/opt levels, strict-CFLAGS proofs, ABI dirty-flag state machine, CPU golden-model lockstepNet-new, strictly more

Bottom line: the new suite (1905 collected tests at PySHDL 1.1.0 — 1644 toolchain + 130 PySHDL driver-level + 131 shdl CLI (tests/shdl_cli/) — ~15k test lines — ~16k including the conformance runner — plus 38 conformance cases / 40 frozen traces) covers essentially everything the legacy suite covered for components that exist, and is categorically stronger on simulation semantics, verification methodology, and regression protection. The three deliberate-feature questions the legacy suite was "ahead" on (warnings, suggestions, multi-error reporting) are now pinned as explicit V1 non-goals (DIA-4/5/6, shdl.md §14.1); only the debugger remains, staged under FUT V.3.


4. Gap register (the work, prioritized)​

P0 — correctness/blind-spot risk (do before building on top of the flattener)​

  • Crash-closure violations — fixed + regression-tested on both sides: invalid UTF-8 (flattener loader AND shdlc CLI, both → structured diagnostics), garbage SOURCE_DATE_EPOCH, lexer ² ValueError, 400-deep RecursionError (200-cap), malformed meta JSON (shdlc) → ROB-6, LEX-9/LEX-10, PAR-8, CCT-7/CCT-9
  • FUZ-3 SHDL-source-level fuzzer (valid-by-construction; campaigns to 1000/1000, 0 divergence)
  • FUZ-4 / ROB-1..6 frontend no-crash fuzzing + raw-exception/assert/AssertionError policy
  • DIA-2 per-raise-site diagnostic coverage (all 107 sites; mechanical inventory == registry; exact line:col per site)
  • SIM-6 explicit order-independence property test (oracle level + scrambled-feedback lockstep)
  • ABI-8 the remaining end-to-end >64-bit-port case
  • AMB-1..41 resolved, specs amended, pinned with tests (AMB-27 via E0701 reuse, no new code)
  • LEX-6/LEX-3 malformed-literal and uppercase-prefix tests (legacy regression closed)

P1 — hardening (before SHDLC is declared done / PySHDL starts)​

  • DIA-4/5/6 decisions pinned as V1 non-goals (shdl.md §14.1); DIA-3/7/8/9 format, message, position-accuracy, usage-error coverage all landed
  • DET-3 flattener hash-seed child-process test; DET-7 full-fixture cross-stage acceptance; DET-4 structural-core idempotence (AMB-29 production landed); DET-6 non-ASCII emission incl. bare-stdout-under-C-locale (UTF-8 via sys.stdout.buffer)
  • MET-4/7/8/9/10/11 public metadata validator + BaseEval unknown-key/version-mismatch + column exactness + basename checks all landed
  • CCT-5 ASAN/UBSAN differential; CCT-3/10/11/12/13/15/17/18/19 all closed. Residual: CCT-6 Linux gcc+clang CI matrix is authored (ci.yml/nightly.yml) but UNPROVEN — needs one real Linux CI pass (also gates the ABI-13 ELF export-set variant)
  • Per-domain rows closed: LEX-7/8/11, PAR-1/4/7/9/10/11/12, VAL-1/2/5/8/9, MON-2/3/5/6/7/8, GEN-1/3/5/7/8/10/11/12/13/14, EXP-5/6/8/9/10/11/12/13, CON-2/4/7/8, FLT-4/6/7/8/9/10/11, IMP-4/5/7/8/9/10/11, TIM-5/6/7, SIM-10/11, ABI-4/6/7/12/14/15/16/17/19, FUZ-5/8, CPU-8/11/12/13 (ABI-13 stays 🟡 — Linux ELF variant)
  • CNF-4 corpus additions (6 cases, v1.1.0); CNF-3/6/7/8/9 integrity-checker negatives, regen-tool tests, report determinism, mechanical drift fence all landed

P2 — scale & process​

  • SCL-3/4/5 stress fixtures; SCL-6 limits documented in all three specs (max-gates decided as deliberately uncapped); SCL-7 range-bomb guard on all four range_values sites plus the RRepl literal-count replication site (E0601)
  • FUZ-6 nightly soak job authored (nightly.yml, all knobs); FUZ-7 mutation-testing audit still optional/open
  • [~] Coverage gate: a report-only coverage job is in nightly.yml (no fail-under yet); the ratchet to ≥95% on flattener phases is not yet enforced — UNPROVEN until the first CI run

Staged (activate with each component)​

  • FUT V.1–V.7 as each Layer-1+ component lands — no component is "done" until its FUT section is green against the conformance corpus
  • ABI-18 non-GNUC constructor contract (with CCT-6's Windows toolchain)

5. Suite policy​

  1. Tests are the spec's enforcement arm. When a test pins previously ambiguous behavior, the spec is amended in the same change. No "tested but undocumented" semantics.
  2. Regeneration discipline. Conformance artifacts regenerate only via the dedicated script, with a manifest version bump and changelog entry; CI rejects drift (mechanism: CNF-9 — today this policy has no enforcement hook).
  3. The completeness assertions are sacred (diagnostics enum match, corpus minimum counts, STRICT_CFLAGS). They are the mechanisms that keep this document true over time; extending them (per-site matrix, W-codes) is preferred over adding unenforced conventions.
  4. Randomized ≠ flaky. All fuzz/differential tests are seed-stable per fixture and embed replay artifacts in failure output. A fuzz failure is a permanent regression test candidate: minimize and freeze it into the corpus.
  5. Oracles stay strict. BaseEval may be stricter than the C ABI (e.g., raising on overflow where C masks). Harnesses bridge deliberately (DualSim masks for the oracle); never relax the oracle to match the implementation.
  6. One lesson, one test. Every bug found in the wild (e.g., the SRLatch power-on fixed-point incident) lands a minimal pinned regression in the most upstream suite that can express it, plus a conformance case if observable in traces.

6. Exit criteria​

The golden suite is complete when: every row in Domains A–T and W is ✅ (the per-component rows — CNF-5 (PySHDL wired 2026-06-12; debug build / backends / tiers still open), ABI-18 — convert and close per component under Domain V); every AMB row has decision+amendment+test; P0 and P1 registers are empty; CI runs T0–T4 on Linux+macOS per commit and T5 nightly; and the coverage gate holds. At that point — and only then — the project's "correct first, fast last" mandate permits Layer-7 optimization work, with this suite re-run bit-exactly per tier as the unchanging gate.