Cell-model IR — staged delivery plan
Status: Largely delivered. Realises ADR 0019. Tracks #130 and #67.
- C1 (foundation + #130) — ✅ landed (#132). #130 is fixed by default: a 9T netlist auto-selects its own 9T descriptor.
- C1b (converter generalization) — ✅ landed (#155): corner from Liberty PVT,
ff internal-state-var (
IQ). Surfaced by running the converter against the proprietary GF130 (GF013BCD) library. - C2 (L3 sequential + L4 timing) — ✅ landed (#132).
- C3.1/C3.2 (build-time generation + prefix selection) — ✅ landed (#132).
- SKY130
.lib.jsonreader — ✅ landed (#155): SKY130 ships only.lib.json, soliberty-parsereads it. - C3a (IHP SG13G2, zero-Rust new PDK) — ✅ landed (#155): vendored as a
sparse/shallow submodule + a generated descriptor, no
ihp_*.rs. - C3.3 (cut over + drop runtime
vendor/dep) — ✅ landed (#160): the runtime binary is self-contained for standard cells (load_pdk_modelshas no runtime callers), andPdkVariantis deleted (grep -rn PdkVariant src/= 0). - C4 (proprietary workflow +
clear_preset) — 🟡 partial:clear_presetset-dominant field landed (#155);docs/adding-a-pdk.mdreframed around the descriptor workflow (#155). The proprietary sequential end-to-end test and the round-trip honesty fix remain (see Remaining follow-ups).
Goal: make Jacquard core consume a single, generated, JSON cell-model
IR carrying all per-cell-type facts of a library — L1 directions, L2
combinational AIG, L3 sequential/classification, and L4 timing
characterization — so any library, including proprietary ones the
authors can't vendor, is selectable at runtime, and the per-PDK Rust, the
hardcoded vendor/ paths, and the runtime .lib parse all retire.
Why staged
Each step is independently useful and de-risks the next: step 1 ships the #130 fix and validates the format on real cells before the heavier L3 schema is committed; step 2 is the schema-fidelity work; step 3 is the cleanup that the first two earn.
Staged checkpoints (each ≈ one reviewable PR)
| # | Scope | Gate |
|---|---|---|
| C1 — foundation + #130 | Relocate pdk_decomp into a shared lib. Define the cell-model-IR JSON schema for L1 directions + L2 combinational AIG (D3) — the corner of the schema needed to build the AIG. Write the converter crate (Liberty function/functional.v → IR). Redirect one PDK's (GF180MCU) stdcell logic to consume the IR. | A 9T netlist (e.g. tests/jtag_minimal) simulates against its own generated 9T descriptor; result byte-identical to the current 7T-substituted run where they truly agree, and the round-trip logic check passes. |
| C2 — L3 sequential + L4 timing schema | Add the D4 sequential pin-role schema (clock+edge, D/next-state, Q, async set/reset+polarity, enable) + classification kinds, and the D5 L4 timing block (setup/hold, clock→Q, DFF/SRAM timing). Extend the converter to emit both from Liberty ff/latch and the timing groups it already parses, with L4 keyed by corner (one descriptor, all corners; mirrors the timing IR). Wire the consumer to replace the hardcoded DFF pin-name matches (src/aig.rs:2080-2260) and to read L4 from the IR instead of TimingLibrary::from_file, selecting the corner via --corner (default default_corner). | A design with sequential GF180/SKY130 cells simulates and times from the IR with no per-PDK Rust DFF handling and no runtime .lib parse; equivalence vs the current hardcoded + liberty_parser path (the oracle). |
| C3 — bundle + cut over + selection | Regenerate bundled descriptors for all built-in PDKs (AIGPDK/SKY130/GF180) in CI from the pinned vendored submodules and embed at build time — not checked in (D7); build-time generation replaces the build.rs pin-table step. Selection by descriptor-declared prefix + --cell-descriptor (D8). Drop the runtime vendor/ cell dependency, pdk_decomp, and liberty_parser::TimingLibrary from core; retire gf180mcu*.rs/sky130*.rs classifiers, PdkVariant, and the build.rs pin-table generators. | The existing PDK regression suite (incl. timed runs) passes consuming only build-time-generated descriptors; vendored cell submodules are no longer a runtime dependency of jacquard core, and CI regeneration is deterministic. |
| C3a — IHP SG13G2 (new PDK, zero Rust) | Vendor the IHP-Open-PDK SG13G2 stdcells as a submodule; add it to the bundle by generating a descriptor only — no per-PDK Rust (D7a). Exercises the Liberty-first path cleanly (every SG13G2 cell has function; every flop a full ff group with reset polarity). | An SG13G2 gate-level design simulates and times purely from its generated descriptor, with no ihp_*.rs in core — the worked proof that adding a PDK is no longer a Jacquard code change. |
| C4 (later) | A documented proprietary-library workflow: user runs the generator on their own Liberty, gets a descriptor, simulates — no Jacquard build. Honesty fix: the round-trip logic check replaces build.rs's port-only assert_eq!. | docs/adding-a-pdk.md recipe; a synthetic "private" library exercised end-to-end in a test. |
Remaining follow-ups (post-cutover)
Discovered during implementation; folded here from the working handoff so they
survive its deletion. None reopen the runtime vendor/ stdcell dependency or
reintroduce PdkVariant.
- Descriptor-drive AIGPDK
DFF/DFFSR. AIGPDK's two internal flops are still on a literal-match path. Their descriptor L3 is well-formed (clock=CLK/rising, next_state=D, async S/R active-low, reset-dominant); the conversion is gate-covered (every AIGPDK/IHP fixture exercises them). Do this first — it is bounded and safe. - Descriptor-drive the preserved
edfxtp/icgtp/icgtnseq branches. The legacySET_B/RESET_B/RN/SETNsequential-wiring branches remain because they still serve preserved cells: SKY130edfxtp(its data-enable folds intonext_statevia theffstate varIQ, soir_seq_input_wireablereturns false) and GF180icgtp/icgtnclock gates (emit no L3). No gated design exercises these, so add a smalledfxtp/icgtpfixture first, then descriptor-drive them and delete the residual literals + the per-PDK classifiers. - Gate IHP sequential. IHP
ffcells are code-supported (previously panicked) but ungated — no flop-bearing SG13G2 fixture exists (needs synthesis infra to produce an IHP gate-level netlist). Add one to lock in IHP sequential. - Flat-module
.vcross-check indexer. Commercial libraries (GF130, IHP) ship one flat-module.v, so the D6 logic cross-check reports 0 models indexed and does not run at generation (the descriptor is still emitted; correctness is validated via simulation, and the converter now warns). Teach the indexer flat-module.vso commercial descriptors are logic-cross-checked at generation. - Descriptor-backed
LeafPinProvider+ deletebuild.rspin-table gen. Confirmed feasible (descriptor L1 covers every stdcell's scalar pins; chain: power-pin → filler → descriptor L1 → preserved pad/IP/SRAM tables). Not yet wired; the per-PDKLeafPinProviders +build.rs::generate_pin_table+generatedmodules remain (inert for stdcell logic, but still the parse-time pin-direction source). This is the last mechanical cleanup. - C4 — proprietary sequential end-to-end test + round-trip honesty fix. GF130
descriptor generation is proven (the C4 proof), but no proprietary
sequential simulation test exists; add a synthetic "private" library
exercised end-to-end. Separately, the round-trip logic check should replace
build.rs's port-onlyassert_eq!(the plan's C4 honesty fix). clear_presetfor true set-dominant sequential sim. The field is emitted and the consumer honors it, but a proprietary user simulating set-dominant flops still needs the non-GF180 sequential path fully descriptor-driven (items 1–2) to observe the correct behaviour end-to-end.
Risks / open questions
- Sequential fidelity (C2). Liberty
ffclear/preset/clear_preset_var→ Jacquard async-reset DFF is the bug-prone mapping; gate it on equivalence against the current hardcoded behaviour. - L2 source (Liberty-first; resolved in ADR 0019 D6). The converter
reads Liberty
function/ff/latchfirst and falls back tofunctional.v/UDP only where Liberty under-specifies. C1 must therefore exercise both ends: a cleanfunctioncell and a cell that needs the.v/UDP fallback (a UDP-modelled mux). Where both sources exist, the converter cross-checks them (logic equivalence; timing arc-set agreement; macro/SRAM timing-value divergence) and surfaces disagreement — the structural checkbuild.rs's port-onlyassert_eq!never had (#130). - AIG payload size (D2/D3). If the JSON AIG is unwieldy for a full library, switch that payload to the FlatBuffers escape hatch — decide at C1 from the real GF180 descriptor size.
- Migration ordering (C3) — resolved: per-PDK cutover. Each PDK migrates independently with the IR consumer running alongside the per-PDK Rust, keeping the suite green; a single switch is riskier. IHP (C3a) is added the same way but greenfield (no Rust to retire).
- Bundled-descriptor provenance — resolved: CI-regenerated (D7). Not checked in; embedded at build time from pinned submodules. C3 must wire the generation into the build/CI and prove it deterministic.
- Identifier alignment (D1). The shared cell/pin-name fragment must be fixed in C1 before two IRs exist that would diverge.
References
- ADR 0019 — Cell-model IR.
- ADR 0002 — Timing IR (the pattern + scope boundary this realises), ADR 0010 / ADR 0011 (the declarative path this extends).
- Current state:
src/aig.rs:1895(hardcoded 7T path),build.rs(port-only pin gen),src/pdk.rs/src/gf180mcu_pdk.rs(hardcoded classifiers),src/liberty_parser.rs(Liberty group-walker to extend),crates/opensta-to-ir+crates/timing-ir(the sibling pattern).