Plan — Phase 0: Timing IR and OpenSTA oracle
Status: Implemented — historical record. All five work streams
(WS1 schema, WS2 opensta-to-ir producer, WS3 SDF parser deletion +
interim runtime hook, WS4 diff harness + CI, WS5 parser-success
assertions) shipped through 2026-05-02. All eight exit criteria are
met. Ongoing scheduling for timing-model fidelity work has moved to
post-phase-0-roadmap.md. The per-WS detail and embedded status
markers below are preserved for the implementation record.
Goal
Deliver the minimum viable infrastructure to enforce Jacquard's timing correctness contract:
- A stable timing intermediate representation (IR) for SDF-equivalent annotations.
- An OpenSTA-driven subprocess converter that produces IR from the same inputs Jacquard consumes.
- A converter that produces IR from Jacquard's existing SDF parser output.
- A CI diff harness that fails loud on converter disagreement.
- Parser-success assertions on the SDF and Liberty paths.
After phase 0, Jacquard's timing pipeline has an enforced external reference. Silent failures (zero-match SDF, mis-scoped hierarchical prefixes, unexpected cell drops) surface as CI failures rather than correctness regressions detected in the field.
Prerequisites
- Requirements doc (
../timing-correctness.md) accepted. - ADR 0001 (OpenSTA oracle) accepted.
- ADR 0002 (timing IR) accepted.
- A representative test design committed to the repo with inputs needed for both Jacquard and OpenSTA (
.v+.lib+.sdfminimum;.spefif available). Candidate:tests/timing_test/inv_chain_pnror the MCU SoC subset, whichever is smaller for first-pass iteration. - OpenSTA available on developer machines and CI runners (installation documented).
Work breakdown
WS1 — IR schema
Done. Shipped as the
timing-ircrate (508baafinitial,2432d41simplification). Schema atcrates/timing-ir/schemas/timing_ir.fbs; per-DFFCLOCK_ARRIVALrecords added later inc403cc8(Pillar B Stage 1, beyond original WS1 scope). JSON round-trip verified viacrates/timing-ir/tests/.
Produce the FlatBuffers schema (schemas/timing_ir.fbs) and generated Rust bindings.
Fields (minimum viable; extend only with written justification):
SchemaVersion { major, minor, patch }.Corner { name, process, voltage, temperature }; IR holds a list of corners.CornerValue { corner_index, min, typ, max }for multi-corner floats.TimingArc { driver_pin, load_pin, rise_delay: [CornerValue], fall_delay: [CornerValue], condition, provenance }.InterconnectDelay { net, from_pin, to_pin, delay: [CornerValue], provenance }.SetupHoldCheck { d_pin, clk_pin, edge, setup: [CornerValue], hold: [CornerValue], condition, provenance }.Provenance { source_tool, source_file, origin: Asserted | Computed | Defaulted }.VendorExtension { source_tool, kind: CadenceX | SynopsysY | Other, raw_bytes }— untyped passthrough for unrecognised annotations.- Root table
TimingIR { schema_version, corners, cell_instances, timing_arcs, interconnect_delays, setup_hold_checks, vendor_extensions }.
Deliverables:
schemas/timing_ir.fbschecked in.build.rsintegration for code generation (or checked-in generated Rust with aflatcpin).- A tiny
timing-ircrate exposing read/write helpers. - JSON round-trip via
flatc --jsonverified in a unit test.
Scope guard: if you find yourself adding fields that represent computed timing graphs, cell electrical characterisation, or netlist structure, stop and re-read ADR 0002.
WS2 — opensta-to-ir production tool
Per ADR 0006, opensta-to-ir is a shipped preprocessing tool, not merely a validation helper. Post-release it remains as an alternative preprocessing path for users who want OpenSTA-computed timing.
Detailed design and phased implementation: ws2-opensta-to-ir.md.
Deliverables:
- A Tcl script runnable by OpenSTA that loads Liberty + Verilog + SDF + (optionally) SPEF + SDC, then emits a machine-readable dump of timing annotations.
- A production-quality standalone Rust binary
opensta-to-irthat parses OpenSTA's dump and emits timing IR (binary + JSON sidecar). Stable CLI, documented exit codes, clear diagnostics, man-page-worthy--help. - Invocation wrapper handling OpenSTA subprocess lifecycle, stderr capture, exit-code checking, and error propagation up through
opensta-to-ir's own exit code. - Assertion: if OpenSTA reports < expected-count cells, exit non-zero with a clear diagnostic.
- Ships as part of Jacquard's release artefacts (binary distributable, documented in user-facing docs).
WS2.4 — Multi-corner CLI flag (shipped 2026-05-02)
Status: Shipped 2026-05-02 across commits 5822343 (consumer +
--timing-corner flag), 530bb36 (builder dedupe + per-corner
[TimingValue] collection), 59fde04 (Tcl driver per-scene emission
--liberty NAME=PATHsyntax), and the integration testaigpdk_dff_emits_per_corner_timing_values. The historical scope notes below are kept for reference but are no longer "open work".
The IR schema (crates/timing-ir/schemas/timing_ir.fbs) supports per-corner TimingValue vectors today, but every record lands in the IR with a single TimingValue keyed at corner_index = 0. Both producer (opensta-to-ir) and consumer (flatten.rs) treat the world as single-corner. Multi-corner support has three pieces:
Producer (Tcl + Rust binary):
crates/opensta-to-ir/tcl/dump_timing.tcl: replace singleread_liberty+ hardcodedCORNER 0 default tt 1.0 25.0with OpenSTA'sdefine_corners+ per-cornerread_liberty -corner $name. The existing arc / setup-hold / wire / clock-arrival walks already key by(cell, …); wrap each in a per-corner loop and call[edge arc_delays $arc -corner $c]. Verify the exact-cornersyntax against the locally built OpenSTA before relying on it (similar to thevertex_worst_arrival_pathprobe done for clock arrival in commitc403cc8).crates/opensta-to-ir/src/main.rs: rework--liberty PATHto accept--corner NAME=PATH[,V=…,T=…,P=…]repeats. Validate at least one corner.crates/opensta-to-ir/src/builder.rs: today each ARC / SETUP_HOLD / INTERCONNECT / CLOCK_ARRIVAL line lands as one IR record with oneTimingValue. Multi-corner emits multiple lines per(cell, driver, load, corner_index)from Tcl; the builder dedupes them into one IR record carrying a[TimingValue]vector. Mechanical.
Consumer (jacquard root):
- Add
--timing-corner <NAME>toSimArgs/CosimArgsinsrc/bin/jacquard.rs; resolve to an index by walkingir.corners(). - Replace
flatten.rs::ir_corner0_max(...)(used in ~5 sites) withir_corner_max(idx). Thread the resolved index throughload_timing_from_ir.
Fixture: sky130 ships multi-corner Liberty (tt_025C_1v80, ss_-40C_1v62, ff_125C_1v95) on disk via volare under ~/.volare/... on dev machines that have run the cosim work. Wire two corners against the existing DFF / chain integration tests for a synthetic-but-real fixture; no external decision is needed before starting.
Land in this order: fixture probe (~hour, verifies the OpenSTA Tcl -corner flag works as expected) → producer (Tcl + binary + builder) → consumer (CLI + flatten plumbing) → integration test exercising both corners. The risk concentrates in the first hour; everything after that is mechanical.
WS3 — Remove hand-rolled SDF parser; wire interim runtime hook
Per ADR 0006, Jacquard's hand-rolled SDF parser is deleted in Phase 0 rather than maintained through later phases. The runtime gains a new IR input path; the old SDF input path becomes an interim convenience wrapper over WS2.
Detailed design and phased implementation: ws3-delete-sdf-parser.md.
Deliverables:
- Delete
src/sdf_parser.rsand the SDF→Jacquard-internal-types code path. Remove all direct consumers. - Add
jacquard sim --timing-ir <path>as the canonical post-release timing input. Loads a pre-converted timing IR file, consumes it into the simulator's internal structures. - Retarget the existing
--timing-sdf/--enable-timingCLI behaviour: when SDF is provided,jacquard simsubprocessesopensta-to-irinternally to produce IR on the fly, then consumes it. Code site tagged "INTERIM per ADR 0006; removed before first release." - Verify no remaining imports of the deleted module. Verify all existing tests that previously used the hand-rolled parser now pass via the interim hook or via checked-in IR fixtures.
- No runtime behaviour regression on Jacquard's timing-related regression suite; any design that currently works must still work after WS3.
WS4 — Diff harness and CI integration
Reframed 2026-05-02; corpus + runner shipped 2026-05-02. The original WS4 was framed as "WS2 vs WS3 IR diff" (OpenSTA-derived against Jacquard's hand-rolled SDF parser-derived). WS3 deleted that parser; the diff has only one side now. Three reframings were considered: Option A (golden-IR regression corpus for
opensta-to-ir) was chosen as the Phase 0 closure; Option B (end-to-end behavioural diff cxxrtl/CVC vs Jacquard cosim event traces) belongs intiming-validation.mdas a Phase 1+ extension; Option C (cross-tool diff vs a future native Rust SDF→IR parser) is Phase 3 work per ADR 0006.
Deliverables:
- A test binary
timing-ir-diffthat reads two IR files and produces a structured diff (missing arcs, mismatched delays past tolerance, mismatched provenance). Shipped incrates/timing-ir/src/bin/timing-ir-diff.rs. - OpenSTA vendored as a git submodule at
vendor/opensta/. Not built from Jacquard's build at runtime; present for CI version pinning, theopensta-to-irintegration tests, and stress-corpus access (see ADR 0005). Shipped. - A primary regression corpus at
tests/timing_ir/corpus/— Jacquard-specific designs with checked-inexpected.jtir(and aexpected.jsonsidecar viaflatc --jsonfor human-readable diffs). Shipped 2026-05-02 with the seed entryaigpdk_dff_chain(a minimal aigpdk DFF + AND with back-annotated wire delay; covers ARC + SETUP_HOLD + CLOCK_ARRIVAL + INTERCONNECT in a self-contained fixture). Sky130 entries (inv_chain_pnr, mcu_soc subset) remain to be added — the inputs exist undertests/timing_test/, but a CI strategy for installing the sky130 Liberty (likely volare) lands with them. - A stress corpus at
tests/timing_ir/stress/— a manifest file listing paths intovendor/opensta/<test-tree-subdir>/. Run nightly or pre-release. Exit criterion: no crashes, no hangs, no malformed IR; numerical agreement with OpenSTA not required. Manifest format specced intests/timing_ir/stress/README.md; entries pending. - A regression test that, for each design in the primary corpus, runs
opensta-to-iron its inputs and diffs againstexpected.jtirviatiming_ir::diff::diff_irswith the per-design tolerance frommanifest.toml. Shipped ascrates/opensta-to-ir/tests/corpus.rs::corpus_designs_match_golden_ir. Skips gracefully when OpenSTA isn't built; fails loud with a structured diff when there's a mismatch. - A
regenerate-goldenshelper for the OpenSTA-pin-bump workflow: bump submodule, run regen, review the diff, commit golden + submodule together. Shipped asscripts/regenerate-corpus-goldens.sh. Iteratestests/timing_ir/corpus/*/manifest.toml, runsopensta-to-irper entry with the manifest-specified flags, refreshes bothexpected.jtirand theexpected.jsonsidecar viaflatc --json. Accepts entry names as positional args for targeted regen. - A diff-machinery mutation test that perturbs a known-good IR and asserts
timing-ir-diffflags it. Shipped incrates/timing-ir/tests/diff.rs:delay_mismatch_past_tolerance_detected,delay_mismatch_within_tolerance_is_clean,arc_only_in_a_detected,arc_only_in_b_detected.
CI hookup landed 2026-05-02. The opensta-to-ir-tests job in .github/workflows/ci.yml builds CUDD (cached), builds OpenSTA via scripts/build-opensta.sh (cached on the submodule SHA), and runs cargo test inside crates/opensta-to-ir — covering the corpus regression test, the CLI tests, and the OpenSTA-driven integration tests on every PR. scripts/build-opensta.sh was extended to honour a CUDD_DIR env var so the CI job can hand it the source-built CUDD location without bypassing the script.
What this catches: OpenSTA upstream regressions, dump-format / Tcl-driver regressions, accidental schema-breaking changes in timing_ir.fbs, builder bugs in opensta-to-ir/src/builder.rs, and the diff machinery itself (via the mutation tests that perturb an IR and assert timing-ir-diff flags the perturbation).
What this doesn't catch: behavioural divergence between Jacquard and a reference simulator. That's timing-validation.md's job (CVC/iverilog event-trace comparison) — the mcu_soc/sky130 90/90 reference match is the current one-design instance, generalisable in Phase 1+.
WS5 — Parser-success assertions
Done. Both halves shipped pre-this-section being marked.
Deliverables (all live):
- Assertions in Jacquard's Liberty parsing code: non-zero cells parsed on non-empty input. Implemented as
TimingLibrary::parse(src/liberty_parser.rs:297-309); rejects with a clear diagnostic naming the input byte count and pointing at the explicit override. - Assertions in
opensta-to-ir(WS2): non-zero IOPATHs / timing arcs resolved on non-trivial SDF input. Implemented as the--min-arcs NCLI flag (default 1) in the binary (crates/opensta-to-ir/src/main.rs:71-77, :112-121); exits with codeEXIT_MIN_ARCS_FAILED = 3(see:17) and a diagnostic naming the produced count, the threshold, and the override flag. - A way to override thresholds for intentionally-empty test inputs:
TimingLibrary::parse_unchecked(src/liberty_parser.rs:316) for the Liberty path,--allow-empty-parseflag for theopensta-to-irpath.
Tests covering both halves: liberty_parser::parse_rejects_library_input_with_zero_cells and parse_unchecked_accepts_zero_cell_library; opensta-to-ir::cli::cli_min_arcs_failure_exit_3 (covers both the failure and the --allow-empty-parse override).
(Original-plan assertions for Jacquard's SDF parser are obsolete — WS3 deleted the parser they were to guard.)
Test plan
Tests live in tests/timing_ir/.
- Schema round-trip (WS1). Construct a small IR in Rust, serialize to binary, deserialize, assert equality. Same for JSON.
- OpenSTA converter unit tests (WS2). For a hand-crafted tiny design, invoke the converter, assert IR contents match expectation.
- Jacquard converter unit tests (WS3). Same, on the same tiny design, through Jacquard's parser.
- Corpus diff (WS4). For each design in the primary corpus, freshly produced
opensta-to-iroutput diffs clean against the checked-in goldenexpected.jtirwithin per-design tolerance. - Parser-success assertion tests (WS5). Feed empty Liberty, empty SDF, and non-empty-but-no-match Liberty. Each should fail loud with a clear diagnostic, not proceed silently.
Tolerances:
- Delay values: ±5% or ±5 ps absolute floor, whichever is larger. Rationale: matches the existing
timing-validation.mdconvention; per-design overrides allowed viamanifest.toml. - Missing arcs: zero tolerance. Every arc in the golden IR must appear in the freshly produced one (and vice versa).
Exit criteria (all met)
Phase 0 is complete when all of the following hold:
- ✅
schemas/timing_ir.fbschecked in (crates/timing-ir/schemas/timing_ir.fbs); round-trip unit tests incrates/timing-ir/tests/. - ✅
opensta-to-irbinary production-quality with stable CLI, documented exit codes, primary-corpus support. Seews2-opensta-to-ir.md(Implemented). - ✅
src/sdf_parser.rsdeleted;--timing-ir <path>canonical;--timing-sdfis a subprocess wrapper overopensta-to-ir(per ADR 0006 § Amendment, the shipping mechanism — Phase 3 native Rust parser deferred indefinitely). Seews3-delete-sdf-parser.md(Implemented). - ✅ OpenSTA vendored at
vendor/opensta/(ADR 0005). - ✅
timing-ir-diffruns in CI on the primary corpus (opensta-to-ir-testsjob), passes cleanly, fails loud on regressions. Mutation tests incrates/timing-ir/tests/diff.rs. - ✅ Parser-success assertions live on both halves:
TimingLibrary::parseandopensta-to-ir --min-arcs. See WS5 above. - ✅ No regression observed in Jacquard's timing-related tests after WS3 cutover.
- ✅
timing-validation.mdcarries the forward-pointing note (line 3) explicitly stating its ±5% convention will be superseded bytiming-correctness.mdonce Phase 0 ships. Phase 0 has shipped; that supersession is now effective in practice (the corpus tolerance is set per-design viamanifest.toml). Removing the in-doc note is a small follow-up if anyone authoring against the page would benefit.
Out of scope (deferred to later phases)
- Native Rust SDF→IR converter. The hand-rolled parser is removed in Phase 0 WS3 (per ADR 0006); the native Rust replacement is Phase 3 work, deferred indefinitely per ADR 0006 § Amendment (no longer release-gating). SDF input ships via the
opensta-to-irsubprocess wrapper. Seepost-phase-0-roadmap.md§ Phase 3 for revival triggers. - OpenTimer integration. Depends on the spike; tracked in
../spikes/opentimer-sky130.mdand its resulting phase-1 plan. - Private PDK (GF130) test track. Tracked in ADR 0004; plumbing deferred to its own phase.
- SPEF IR. Separate from timing-annotation IR per ADR 0002.
- Runtime violation reporting improvements (R4 critical-path refinement JSON). Phase 1 or 2.
Risks
- Licensing verification on vendored OpenSTA corpus. Per-file check needed before inclusion. May reduce corpus size if restrictive; acceptable.
- FlatBuffers build integration friction. If
build.rscodegen causes cross-compilation or CI issues, fall back to checked-in generated code with a documentedflatcversion. Pick one approach and stick to it; flip-flopping is worse than either option. - Tolerance tuning. Initial ±5% may prove too loose (hides bugs) or too tight (false positives from numerical differences). Plan to re-tune after first real-design data arrives.
- WS3 cutover risk. Deleting the hand-rolled SDF parser risks regressing designs that depend on behaviour it currently provides. Exit criterion 7 requires a clean regression run before WS3 is considered complete. If coverage gaps emerge, walk-back options per ADR 0006 apply: add dialect shims to
opensta-to-ir, or (now that Phase 3 is deferred) keep the hand-rolled parser available behind a feature flag until dialect parity is reached. - OpenSTA dialect coverage. OpenSTA may not accept every SDF dialect Jacquard's hand-rolled parser has been patched to handle. Such cases are tracked as either
opensta-to-irpost-processing fixes or upstream OpenSTA contributions. Under no condition is the fix to reinstate the hand-rolled parser unless walk-back per ADR 0006 is formally triggered.
Links
../project-scope.md../timing-correctness.md— acceptance criteria this plan satisfies.../adr/0001-opensta-as-oracle.md../adr/0002-timing-ir.md../spikes/opentimer-sky130.md— runs in parallel; no dependency either way.