Plan: xroots — backward X-source frontier query
Issue: #98. Builds on the
netlist-graph cone/driver fixes in #101
(PR1), which is why this branch is stacked on fix/netlist-graph-cone-drivers.
Problem
When a signal reads X under jacquard cosim --xprop, finding why is a
manual trace→guess→re-run loop: the VCD can only report wires you already
chose to trace, so you must half-know the answer to ask the question. But the
information is static: an X originates at a known X-source (unreset DFF Q,
SRAM read port, undriven primary input) and propagates forward. So "what
makes S read X?" = "which X-sources lie in S's backward cone?" — a pure
netlist query.
Design decisions (confirmed)
- Driven-input set comes from jacquard, not re-derived in Python. The
authoritative driven set (clock/reset/constant/peripheral pins + GPIO→port
mapping) is computed in Rust during cosim setup. A new
jacquard xsourcessubcommand dumps the X-source set (including the undriven-input complement) as JSON;netlist-graph xrootsconsumes it. No drift, single source of truth. - Dominators deferred. v1 ships the frontier query + classification +
--emit-trace. Dominators (X-sources every path passes through) need a careful formulation over the DFF-feedback cyclic graph and land as a scoped follow-up once the frontier query proves useful.
Part A — jacquard xsources (Rust)
A new clap subcommand alongside Sim / Cosim in src/bin/jacquard.rs.
jacquard xsources <netlist> --config <sim_config.json> -o xsources.json
Reuses the existing cosim setup path to build the AIG + NetlistDB and the
driven-input set, then:
- DFF-Q / SRAM-read X-sources from
AIG::compute_x_sources()(src/aig.rs:3277) — already enumerates these as AIG pins. - Undriven primary inputs = primary-input bits not in the cosim driven
set (the same complement
cosim --xproptreats as X; seecosim-xprop.mdX-source taxonomy rows 3–4). - Name resolution: map each X-source AIG pin → hierarchical net name via
the existing
aigpin → (cell_id, cell_type, output_pin_name)map (src/aig.rs:265) andNetlistDBpin/net names.WordSymbolMap(src/flatten.rs:138) is the precedent for this AIG→name translation.
Output schema (schema_version: "1.0", additive-only):
{
"schema_version": "1.0",
"netlist": "design.v",
"x_sources": [
{"net": "top.cpu.regs[7]", "kind": "unreset-dff", "cell": "..."},
{"net": "top.sram.rd_data[3]", "kind": "sram-read", "cell": "..."},
{"net": "ext_in[2]", "kind": "undriven-input"}
]
}
kind ∈ {unreset-dff, sram-read, undriven-input}. The net names are
emitted in the same conventions --trace-signals resolves, so they round-trip
into the Python tool and back into a confirming --xprop run.
Note on "unreset": jacquard marks all DFF Qs as X at cycle 0; a DFF with a connected reset resolves once reset asserts. For a static backward query the useful classification is "is this DFF reset-connected?" — emitted as
unreset-dffonly when no reset/set pin is wired. This is a static over-approximation (documented in the command help).
Part B — netlist-graph xroots (Python)
netlist-graph xroots <netlist> <signal> [--xsources xsources.json] [--emit-trace <file>] [-d DEPTH]
- Resolve
<signal>to a net (reuseresolve_name). - Reverse-reachability from the net through
find_drivers, continuing through DFF data pins (the same data-path-through-registers walk aslogic_cone(through_regs=True)restricted to_DFF_DATA_PINS— reuses PR1's corrected_is_register). Clock/reset pins are not followed. - X-source set:
- With
--xsources: use the manifest's authoritative net set + classification (coversundriven-input, which the netlist alone can't classify). - Without it: classify natively from the netlist — DFF-Q nets (cells
_is_registermatches) and SRAM read-port nets — and emitundriven-inputas unknown (warn that--xsourcesis needed for the driven-set complement). Genuinely-undriven internal nets (PR1's[undriven — X-source]leaves) are reported as candidate roots.
- With
- Frontier: BFS outward; when a reached net is an X-source, record it and stop expanding past it (it is a root). Non-source nets keep expanding. Frontier = X-sources reachable without passing through another X-source.
- Report: frontier X-sources, classified and grouped by kind, nearest
first (BFS depth).
--emit-trace <file>writes them as a--trace-signalslist (one net per line,# kindcomments) so a confirming--xproprun is one command.
Reuse / new code
- Reuses:
resolve_name,find_drivers,_is_register,_DFF_DATA_PINS,_short_net, theout_driver/is_drivenmachinery from PR1. - New:
xrootsgraph method (reverse-reachability + frontier intersection),xrootsCLI command, manifest loader.
Tests
- Rust:
xsourcesunit test on a small netlist+config — assert DFF-Q, SRAM-read, and undriven-input nets appear with correctkind; reset- connected DFFs are notunreset-dff. - Python: synthetic netlist with a known X-source behind two logic
levels and a reset-defined path; assert the frontier finds the source, the
classification matches the manifest,
--emit-traceoutput round-trips, and the "no manifest" mode warns. - Integration:
xsourcesontests/timing_test/minimal_build→xroots --emit-trace→ feed back tocosim --xprop --trace-signalsand confirm the surfaced wires carry X (smoke test, gated on Metal availability).
Sequencing
- Part A (
jacquard xsources) + Rust tests. - Part B (
netlist-graph xroots) + Python tests, consuming Part A's manifest. - Docs: a
docs/x-debugging.mduser guide (xsources → xroots → confirming --xprop run) + CHANGELOG entries + a one-line pointer in CLAUDE.md's debugging-tools section.
Out of scope (follow-ups)
- Dominator analysis (decision 2).
- Wire-bundle reconstruction of multi-bit X-source buses (tracked separately).