ADR 0016 — Selective X-propagation
Status: Accepted (2026-05; amended 2026-06-25). Extension to cosim proposed 2026-06-03 — see Amendment.
Amendment (2026-06-25): Stage-count correction. The body says "Stages 1–6 are implemented; Stage 7 (dynamic X narrowing) is a future enhancement." Per
docs/selective-x-propagation.md, all seven stages are implemented — Stage 7 is the Criterion benchmarks (benches/xprop.rs), not dynamic narrowing. Dynamic X narrowing (periodic X-mask scan, partition-kernel hot-swap) is a separate, still unbuilt enhancement.
Context
Jacquard's default two-state (0/1) simulation silently resolves uninitialised DFF and SRAM outputs to zero. This masks initialisation bugs that real hardware would expose as unknown (X) values, and creates false mismatches when comparing against four-state RTL simulators.
Naively upgrading the entire simulator to four-state logic would double storage and roughly halve throughput. In a well-designed SoC after reset, typically less than 5% of signals are genuinely X-capable.
Decision
Implement selective X-propagation controlled by the --xprop
CLI flag. Static analysis at compile time identifies X-source
signals (uninitialised DFFs, SRAM read ports); forward-cone
computation classifies each partition as X-capable or X-free. Only
X-capable partitions run an X-aware kernel variant; the rest
continue with the fast two-state path.
The full seven-phase design, implementation details, and design
rationale are in
docs/selective-x-propagation.md.
Stages 1–6 are implemented; Stage 7 (dynamic X narrowing) is a
future enhancement.
Key design choices (summary)
- Partition-level granularity — entire partition runs X-aware or not. ~95% of partitions are typically X-free after reset.
- Conservative SRAM X — all reads return X until any write. Per-address tracking deferred.
- No reset-aware analysis — all DFFs start as X; the fixpoint iteration naturally resolves reset-connected DFFs.
- State buffer doubling — X-mask words occupy
[reg_io_state_size .. 2*reg_io_state_size)when enabled. X-free partitions ignore the mask entirely. - Runtime flag, not compile-time —
--xproponjacquard sim; no new Cargo features needed.
Consequences
- X-capable partitions pay ~2× storage and ALU cost; X-free partitions (the vast majority) pay nothing.
- VCD output includes
xvalues when--xpropis enabled, compatible with standard four-state VCD tools. - The
--check-with-cpureference path includes an X-aware CPU kernel for validation. - Benchmarks (
benches/xprop.rs) track the overhead.
Amendment 2026-06-03: cosim and IO X-sources
The original decision wired --xprop into the sim (static-input)
path only. The reactive cosim path is two-state, so JTAG-replay /
peripheral runs silently zero-init uninitialised state
(#95). This amendment
extends selective X-propagation to cosim.
Two points the original design did not address, because the static
sim path never had to:
- Undriven input pads are X. In a reactive run, peripheral models
drive only some input bits each edge (clock, reset, JTAG/UART
pins, configured constants). Every primary-input bit not in that
driven set is unconnected and must be X, not
0. - Bidir pad reads. A
bi_24tpad's core-read was originally modelledY = PAD(tristate not modelled); sincePADis an undriven primary input, bidir reads fell out of rule (1) as X — safe (false-X, never false-0) but pessimistic for theOE=1loopback. The combinationally-correct readY = OE ? A : externalis now modelled as a mux in the AIG (#96, implemented — see the dated subsection below). (An earlier draft of this amendment proposed a per-edge OE→input feedback with one-edge latency — that was wrong; the correct read is combinational.)
So the X-source taxonomy is now three-way: uninitialised DFF,
uninitialised SRAM (both as before), and undriven input pads (which
subsumes bidir reads under the current Y = PAD model). The first two
are sequential power-up X; the third is the reactive IO X-source
specific to cosim.
The Metal kernel is already X-capable, so this is host-side reactive
plumbing (state-buffer expansion, per-edge X-mask maintenance, and an
observe-kernel output-offset fix for the doubled layout). Phasing and
risks are in ../plans/cosim-xprop.md.
Seed-template correction (2026-06-03)
Implementing the cosim extension surfaced a latent bug in the shipped
sim path too: the power-up X-mask seed
(expand_states_for_xprop) was built as "all-X, then clear every
input_map position." But input_map contains the DFF-Q
combinational-read positions, not just primary input ports — so
uninitialised DFFs were read as known 0 and X never originated. --xprop
was therefore silently two-state for any sequential design (the
gate-level X math was unit-tested, but no end-to-end test asserted X
surfacing from an uninitialised DFF).
The seed is now built by vcd_io::xprop_xmask_template: all-known, set
X only at genuine X-source positions (uninitialised DFF Q reads + SRAM
reads), excluding primary inputs (nets present in input_layout) and
constant-pinned DFFs (const_zero_pos = input_layout.len()). The cosim
path additionally seeds the output slot's X-mask, since its per-edge
state_prep copies output→input before the first simulate. This
corrects design choice #3 above ("all DFFs start as X"): the intent was
always X at DFF positions; the implementation had inverted it for
DFF-feedback reads.
Undriven input X-source (cosim, implemented 2026-06-03)
The "undriven input pad → X" rule from this amendment is now implemented
for cosim. compute_x_capable_pins(treat_inputs_as_x_sources) (gated by
DesignArgs::xprop_undriven_inputs, set only by the cosim path) marks
input cones X-capable; vcd_io::xprop_xmask_template_cosim seeds every
primary input as X; and the GPU kernels clear the X-mask of each bit they
drive each edge — state_prep for the build_edge_ops driven set
(clock/reset/constants/model pins) and gpu_apply_flash_din for the SPI
MISO bits it writes directly (they bypass state_prep). The complement —
genuinely undriven inputs — stays X. sim keeps inputs known (driven from
the VCD) and pays no extra X-aware cost. End-to-end guards covering the
DFF and undriven-input X-sources, in both sim and cosim, live in
tests/xprop_cosim/ (CI, fatal).
Bidir tristate read-back mux (implemented 2026-06-04)
Point #2 above is now implemented (#96). AIG::from_netlistdb's bi_24t
branch builds Y = OE ? A : external combinationally in the AIG —
OR(AND(OE, A), AND(!OE, PAD)) via the De Morgan idiom already used by
wire_dff_reset_set_overlay — instead of the conservative Y = PAD. The
external arm is the same undriven PAD primary input (X under rule (1) until
a peripheral model drives it); the OE=1 arm reads the core's own drive
A, so the loopback is X-exact (known whenever A is) and the
two-state read returns A, not the external stim. This removes bidir reads
from the "undriven input → X" subsumption for the OE=1 case; they are now
exact rather than conservatively-X. in_c/in_s stay Y = PAD. Without
both A and OE pins the conservative Y = PAD still stands. Unit test:
aig::gf180mcu_chip_top_tests::bi_24t_models_tristate_readback evaluates
the full Y truth table. (#107's $isunknown x-assert work can now assert
bidir read-backs go definite when OE is asserted.)