# Sparkle SoC — Current Status **Date**: 2026-04-15 **Branch**: main > **Note**: This file is an append-only development log. For a concise > at-a-glance view of current capability, see the IP Catalog in > [README.md](../README.md#ip-catalog). For release history, see > [CHANGELOG.md](CHANGELOG.md). For the Signal DSL quick-start, see > [Tutorial.md](Tutorial.md) (every code block is syntax-checked by CI). --- ## Headline Sparkle JIT exceeds Verilator on single-core and multi-core SoCs: | Config | Sparkle JIT | Verilator | Ratio | |--------|------------|-----------|-------| | **RV32I SoC** (native) | **14.2M cyc/s** | 8.7M | **1.63x** | | **LiteX 1-core** (1730 lines) | **17.9M cyc/s** | 10.5M | **1.70x** | | **LiteX + Reverse Synthesis** | **18.1M cyc/s** | 8.4M baseline | **2.14x** | | **LiteX 8-core parallel** | **12.7M per-core** | 1.1M | **11.9x** | | **Timer Oracle** (proof skip) | **49 GHz** | — | **9,900x** | --- ## Next Phases (TODO) | Priority | Phase | Description | Status | |----------|-------|-------------|--------| | 3 | **H.264 Hardware MP4 Encoder** | Frame encoder + MP4 ROM muxer → playable .mp4 from hardware | **Done** | | 3.1 | **Lock-Free CDC Infrastructure** | SPSC queue, rollback, formal proofs, JIT multi-domain runner | **Done** | | 3.2 | **Compiler Improvements** | `~~~` complement, complex lambda synthesis, `hw_let` tuple destructuring | **Done** | | 3.3 | **SV→Sparkle RTL Transpiler** | Parse PicoRV32 Verilog → Sparkle IR → JIT. 34 tests | **Done** | | 3.4 | **Inline Verilog Formal Verification** | `verilog!` macro, auto-assert via `bv_decide`, 6 proofs zero sorry | **Done** | | 3.5 | **Type-Safe JIT Wrappers** | `SimInput`/`SimOutput`/`Simulator` generated by `verilog!` macro | **Done** | | 3.6 | **Signal Operator Refactoring** | Mixed Signal/BitVec operators, compiler fix for inline expansion | **Done** | | 3.7 | **Imperative `<~` Assignment** | `Signal.circuit` macro with `<~` register assignment | **Done** | | 3.8 | **AXI4-Lite Bus Protocol** | Verified slave/master IP, 14 proofs (deadlock-free, valid persistence) | **Done** | | 3.9 | **RV32I Formal Verification** | 102 theorems, MSTATUS WPRI bug, Signal DSL ↔ spec equivalence | **Done** | | 3.10 | **Linux Boot Idle-Loop Skipping** | MIE/MTIE guard, WFI fast-path, 4 oracle accuracy tests | **Done** | | 4 | **SV Transpiler: M-Extension** | PicoRV32 MUL/DIV/REM, MemorySSA, 34 CI-safe tests | **Done** | | 5 | **JIT Optimization Phase 52** | 10 optimization phases, LiteX firmware execution, timer oracle | **Done** | | 5.1 | **Multi-Core Parallel** | 8-core CDC multi-thread, sub-module instantiation, 3.87x speedup | **Done** | | 5.2 | **RTL Pattern Detection** | Auto-detect countdown timers (7 found), idle registers (150 found) | **Done** | | 5.3 | **Proof-Driven Time Skip** | Timer oracle PoC: 49 GHz effective (9,900x), LiteX firmware verified | **Done** | | 5.4 | **Generic Auto-Detection** | Reachability DCE + frequency-based guard detection, no hardcoded signal names | **Done** | | 5.5 | **Verified Reverse Synthesis** | OracleReduction type class (proof-required), carry-save=mul proof (zero sorry), 2.14x speedup | **Done** | | 5.6 | **`runSim` auto-dispatcher** | Single `runSim` picks runSingleSim / runMultiDomainSim automatically; typed named-port connections via `toEndpoint`; 27 regression tests (equivalence / auto-select / name resolution / index alignment / stress) | **Done** | | 5.7 | **pcpi_mul standalone FSM fix** | Standalone pcpi_mul JIT now runs correctly after Issue 1 / 6 / 7 fixes. See `docs/KnownIssues.md` | **Done** | | 5.8 | **Equivalence-check command family** | `#verify_eq` / `#verify_eq_at` (cycles + latency + ❌→💡 hint) / `#verify_eq_git` for time-travel regression proofs. 13 interactive demos in `Tests/Verification/EquivDemo.lean` (8 layer-1 + 4 layer-2 + 1 layer-3 git). See `docs/Tutorial.md` §5.4–5.6 and `docs/TODO.md` V1–V4 for follow-ups. | **Done** | | 5.10 | **BitNet ⊕ picorv32 SoC (Level 1a)** | First-ever CPU IP + NN IP cohabitation. BitNet MMIO peripheral at `0x40000004`, dim=4 1-layer BitLinear. `Tests/Integration/BitNetSoCTest.lean` passes 3/3 axes (functional, structural, artifact). `fpga/U280/` scaffold in place. Pre-existing PC-stuck issue in rv32-jit-loop-test tracked as TODO S0. See `docs/CHANGELOG.md` Phase 56. | **Done** | | 6 | **Cross-Module Optimization** | Propagate wrapper const inputs into sub-modules for dead code elimination | Next | | 7 | **RTL Reverse Synthesis (Extended)** | Iterative divider, CRC, FIR filter — additional OracleReduction instances | Next | | 8 | **Verified Standard IP — FIFO, Arbiter** | Parameterized FIFO, N-way arbiter, TileLink | Not started | | 9 | **GPGPU / Vector Core** | VDD framework for concurrent, memory-bound architectures | Not started | | 10 | **FPGA Tape-out Flow** | End-to-end Sparkle-generated Linux SoCs on physical FPGAs | Not started | --- ## Completed Phases ### Verified Reverse Synthesis (Phase 54) — DONE Proof-driven IR reduction: removes multi-cycle FSM logic verified equivalent to a direct computation. **Framework**: `OracleReduction` type class with mandatory equivalence proof (`equiv` field). Users declare instances to add new reductions (e.g., iterative divider, CRC). **PicoRV32 pcpi_mul** (carry-save shift-and-add multiplier): - 38 carry-save chain assigns removed from IR (573 → 535 stmts) - Lean proof: `carrySave_add_eq_64` (bv_decide) + induction → `rd + rdx = a * b` (zero sorry) - 20+ supporting theorems in `MulProps.lean`, all zero sorry/axiom - No Mathlib dependency **Benchmark**: 8.4M → **18.1M cyc/s** (2.14x speedup, 1.72x vs Verilator) **Files**: - `Sparkle/Core/OracleSpec.lean` — `OracleReduction` type class, `resolve`, `mkOracle`, `reduceIR` - `Sparkle/Core/MulOracle.lean` — `instance : OracleReduction "pcpi_mul"` (reference implementation) - `Sparkle/Core/MulOracleProof.lean` — Full inductive proof chain (zero sorry) - `Sparkle/Verification/MulProps.lean` — 20 theorems: CSA identity, concrete tests, bit-level proofs - `Sparkle/IR/PatternDetect.lean` — `MulFSM` detection ### JIT Optimization Phase 52 + Multi-Core Parallel — DONE **Single-core optimizations** (5.62M → 17.9M, +218%): | Phase | Optimization | Effect | |-------|-------------|--------| | Dead code + hex masks | `if(0)` elimination, `eq(x,0)→!(x)` | +4% | | Constant/alias propagation | IR Phase 0, eliminates 140+ wire assigns | +17% | | Deep MUX → if-else | ≥16-arm MUX → if-else for CPU FSM | +3% | | Self-ref register if-else | `mux(en,val,self)` → `if(en) next=val;` | +9% | | Reachability DCE | BFS from output ports, eliminates unreachable signals | +4% | | Generic guard detection | Auto-detect `_valid`/`_trigger`/`_enable` guards | +14% | | evalTick wire localization | ~270 wires → stack locals | +39% | | Self-ref _next elimination | Direct register update, no _next variable | +22% | All optimizations are fully generic — no hardcoded signal names. **SVParser LiteX/Migen compatibility** (10 bug fixes): - `always @(*)` nonblocking assigns, bit-index RMW, case SSA merge - Preprocessing: named blocks, integer decls, for-loop unroll, byte-lane conversion **Multi-core parallel simulation**: - Sub-module instantiation: per-module C++ classes, function-call eval - CDC multi-thread runner: N threads with batched barrier sync (std::barrier) - 8-core parallel: 12.7M per-core (1.8x vs sequential, 11.9x vs Verilator 8-core) **RTL Pattern Detection + Timer Oracle**: - Auto-detect countdown timers (7 found in LiteX), idle registers (150 found) - Timer oracle: skip timer_value cycles → 49 GHz effective (9,900x) - Verified with LiteX firmware: CSR write → timer countdown **Test coverage**: Parser 5/5, Optimizer 10/10, RV32I Verilator ALL TESTS PASSED ### SV Transpiler: M-Extension (Phase 51) — DONE PicoRV32 M-extension (MUL/DIV/REM) fully operational on M-ext SoC via SVParser→JIT pipeline. Major compiler architecture overhaul with 12 bugs found and fixed, 34 CI-safe JIT tests. **Key Results**: - RV32I firmware: 26 UART words, ALL C TESTS OK (Fibonacci, Array Sum, Bubble Sort, GCD) - RV32IM firmware: 18 UART words, MUL/DIV/REM + factorial `10! = 3628800` correct - Simple MUL: `12345 * 6789 = 83810205` correct (runtime hardware multiply) - Store/Load: `0x12345678` round-trip through memory correct - pcpi_mul standalone: `7*6`, `100*100`, `12345*6789`, consecutive multiply all correct **Architecture Changes**: - **MemorySSA Sequential Emitter**: Replaced MUX-only approach for `always @*`. Processes statements top-to-bottom with dynamic SSA wire generation. Correctly handles read-then-overwrite patterns (carry-save accumulator) that cause cyclic dependencies under pure MUX. - **Carry-Save Accumulator Support**: PicoRV32's `pcpi_mul` with nested for-loops, concat-LHS part-select writes, and `__RMW_BASE__` read-modify-write decomposition. - **`{N{expr}}` Bit Replication**: Proper 1-bit (negate-and-mask) and multi-bit (concat) replication. - **Byte-Lane Memory Write Fix**: `buildByteStrobeWrite` now shifts data to target bit position before masking. **Bugs Found (12 total)**: 1. `{N{expr}}` replication count ignored → `mem_wstrb = 1` instead of `15` for `sw` 2. `{N{expr}}` ident width assumed 1-bit → wrong for multi-bit replication 3. `buildByteStrobeWrite` missing shift → only byte 0 written 4. `sigNames` filter flat-only → assignments inside `if/else` silently dropped 5. `processCaseArms` no `!covered` guard → later case arms override earlier matches 6. `emitSequentialSSA` case merge self-reference → cyclic dependency for don't-care variables 7. `emitSequentialSSA` if/else merge self-reference → same issue 8. Topo sort `endsWith "_0"` → `_ssa1_10` misidentified as prologue 9. `decomposeMultiConcatLhs` self-reference → `__RMW_BASE__` vs `varName` 10. `unrollForLoops` inner loop result discarded → `renamed` vs `unrolled` 11. 64-bit promotion missing → C++ UB on shifts >= 32 12. Topo sort self-reference not excluded → circular dependency **Tests (34 CI-safe)**: - Tests 1-11: Parser, lowering, JIT, PicoRV32 SoC, C firmware - Tests 12-16: IR-level regression (nested if/else, case priority, concat-LHS, for-loop SSA, array read) - Tests 17-21: pcpi_mul IR structure + JIT (7*6, 100*100, 12345*6789, consecutive, SoC wrapper) - Tests 22-29: JIT pair tests (if/else, array, case, read-overwrite, for-loop, replication, byte-lane, multi-bit repl) ### Linux Boot Idle-Loop Skipping (Phase 50) — DONE Oracle improvements for production-quality Linux boot idle-loop skipping: - **MIE/MTIE guard**: Checks interrupt enable before timer-compare skip. If `MSTATUS.MIE=0` or `MIE.MTIE=0`, skip is suppressed (interrupt wouldn't fire). - **WFI fast-path**: Detects WFI instruction via wire, triggers skip immediately (threshold=1). - **4 CI tests**: Halt loop detection, timer accuracy, MIE guard, MTIE guard — all self-contained with firmware.hex. ### RV32I Formal Verification (Phase 49) — DONE 102 formally proved theorems across 4 files. **Found real bug**: MSTATUS WPRI fields are writable via CSRRS/CSRRW (should be read-only per RISC-V spec). | File | Theorems | Highlights | |------|----------|------------| | `RV32Props.lean` | 38 | Opcode/ALUOp roundtrip, R-type field extraction, I/S/B/U/J immediate roundtrip | | `PipelineProps.lean` | 26 | Forwarding correctness, load-use hazard, flush/NOP, x0 invariance, store-to-load fwd | | `CSRProps.lean` | 21 | **MSTATUS WPRI bug proved**, trap→MRET MIE roundtrip, INT_MIN/−1, div-by-zero | | `SignalDSLProps.lean` | 17 | **Signal DSL = pure spec** for ALU (ADD/SUB/AND/OR/XOR/PASS), branch, hazard, register | Key innovation: `@[simp]` lemmas reduce `(signal).val t` through all Signal combinators via `rfl`, enabling proofs directly on synthesizable hardware code. ### AXI4-Lite Bus Protocol IP (Phase 48) — DONE Formally verified AXI4-Lite slave/master with 14 proofs + 23 simulation tests. - **Props**: mutual exclusion, valid persistence (AXI spec A3.3.1), deadlock-freedom, write priority - **Slave**: 4 registers (fsm, addr, wdata, wstrb), AW+W simultaneous acceptance - **Master**: 5 registers with command interface, read data capture - Both synthesize to clean Verilog via `#synthesizeVerilog` ### Imperative `<~` Register Assignment (Phase 47) — DONE `Signal.circuit` macro with imperative-style `<~` register assignment. One macro for both synthesis and simulation. ```lean def counter {dom : DomainConfig} : Signal dom (BitVec 8) := Signal.circuit do let count ← Signal.reg 0#8; -- declare register count <~ count + 1#8; -- assign next-state return count -- output signal -- Works for both: #synthesizeVerilog counter -- → Verilog counter.sample 10 -- → [0, 1, 2, ..., 9] ``` Supports multiple registers, local `let` bindings, `Signal.mux`, `hw_cond`, and all Signal operators. Desugars to `Signal.loop` + `Signal.register` + `bundleAll!` at compile time. `Signal.loop` was unified with C FFI memoized evaluation (previously only in `loopMemo`), eliminating stack overflow for simulation. `loopMemo` kept for backward compat but delegates to `loop`. ### Signal Operator Refactoring (Phase 46) — DONE Natural operator syntax for all Signal binary operations. Mixed Signal/BitVec operands work everywhere, including inside inlined private functions called multiple times. #### Before → After ```lean -- Before (verbose applicative syntax): let sum := (· + ·) <$> a <*> b let rounded := sarBy6 ((· + ·) <$> (s0 + d1) <*> Signal.pure 32#16) let shifted := Signal.pure 1#64 <<< pfxShift -- After (natural operators): let sum := a + b let rounded := sarBy6 ((s0 + d1) + 32#16) let shifted := 1#64 <<< pfxShift ``` #### Compiler Fix Root cause: the synthesis compiler's early binary operator interception called `translateExprToWire` on raw BitVec constants (`@OfNat.ofNat`), corrupting metavariable state. Second identical call would fail with "Unbound variable: self". Fixed by using `extractBitVecLiteral` for constant operands. #### New Instances | Operator | Signal ↔ Signal | Signal ↔ Constant | Constant ↔ Signal | |----------|:-:|:-:|:-:| | `+` `-` `*` | `a + b` | `a + 1#8` | `1#8 + a` | | `&&&` `\|\|\|` `^^^` | `a &&& b` | `a &&& 0xFF#8` | `0xFF#8 &&& a` | | `<<<` `>>>` | `a <<< b` | `a <<< 2#8` | `1#64 <<< a` | | `++` | `a ++ b` | `a ++ 0#2` | `0#24 ++ a` | | `===` | `a === b` | `a === 0#8` | — | ### Type-Safe JIT Simulation Wrappers (Phase 45) — DONE `verilog!` macro generates typed `SimInput`/`SimOutput`/`Simulator` structures. Port name typos and width mismatches caught at compile time. ```lean verilog! "module counter8_en (...) ... endmodule" open counter8_en.Verify -- Type-safe simulation (not raw UInt64 indices!) let sim : Simulator := { handle } sim.step { rst := 0, en := 1 } let out ← sim.read IO.println s!"count = {out.count}" -- BitVec 8, type-safe ``` ### Inline Verilog Formal Verification (Phase 44) — DONE `verilog!` macro + auto-assert: write Verilog with `assert(cond)`, get auto-proved theorems. ```lean verilog! " module counter8_en (...); always @(posedge clk) begin if (rst) count_reg <= 0; else if (en) count_reg <= count_reg + 1; assert(rst ? (count_reg == 0) : 1); // ← auto-proved! end endmodule " open counter8_en.Verify -- auto_assert_0 is generated and proved by bv_decide -- 6 manual theorems also proved against auto-generated nextState ``` - `verilog!` parses Verilog at elaboration time → generates State/Input/nextState - `assert(cond)` → auto-generates `theorem auto_assert_N` proved via `simp [nextState]; bv_decide` - Wrong assertion → instant red squiggly in editor (no simulation needed) - 6 manual theorems + auto-assert, zero `sorry` ### SystemVerilog RTL Transpiler (Phase 43) — DONE Parse existing SystemVerilog RTL, lower to Sparkle IR, JIT-compile, and execute C firmware — all without Verilator. #### Test Results ``` === SystemVerilog Parser Tests === Test 1: Parse counter module.............. PASS Test 2: Verify ports...................... PASS Test 3: Lower to Sparkle IR............... PASS Test 4: Round-trip to Verilog............. PASS Test 5: Parse expression (a + 8'h01)...... PASS Test 6: E2E JIT simulation............... PASS (count=9 after 10 cycles) Test 7: PicoRV32 parse................... PASS (4 modules, core: 250 items) Test 8: PicoRV32 JIT compile + simulate.. PASS (148 regs, 100 cycles) Test 9: $readmemh support................ PASS Test 10: PicoRV32 SoC with firmware....... PASS (5 UART bytes: "Hello") Test 11: C firmware (RV32I) via JIT....... PASS (26 words, ALL C TESTS OK) === Results: 11 passed, 0 failed === ``` #### C Firmware Results (riscv32-none-elf-gcc -march=rv32i -O2) | Test | Result | |------|--------| | Fibonacci (10 values) | 0,1,1,2,3,5,8,13,21,34 ✓ | | Array Sum (8 elements) | 360 ✓ | | Bubble Sort (6 elements) | 3,8,17,42,55,99 ✓ | | GCD (3 pairs) | 6,25,1 ✓ | | Final marker | **0xCAFE0000 — ALL PASSED** | #### Architecture ``` Verilog RTL (.v) → [SV Parser] → SV AST → [Lower] → Sparkle IR → [CppSim] → C++ → [GCC] → .so → [JIT] → Simulation ``` Key algorithms: - **If-Conversion**: Guarded assignments eliminate dead-code in nested case/if mux trees - **Generate Block Eval**: Parameter-based conditional compilation (TWO_CYCLE_ALU=0 → else branch) - **Concat-LHS Bit Scatter**: `{a[31:20], a[10:1], ...} <= rhs` → bit-field extraction and placement - **Byte-Strobe Memory**: `if(wstrb[N]) mem[addr][hi:lo] <= data` → read-modify-write ### Compiler Improvements (Phase 42) — DONE Three ergonomic improvements to the synthesis compiler, reducing boilerplate in Signal DSL code. #### Changes | File | Change | |------|--------| | `Sparkle/Core/Signal.lean` | Added `Complement` instance for `Signal dom (BitVec n)`, `hw_let` macro for 2/3/4-tuple destructuring | | `Sparkle/Compiler/Elab.lean` | Fixed unary primitive dispatch for NOT/NEG with typeclass args; added binary-op-with-constant handling in `Functor.map` lambda bodies | | `Tests/CompilerTests.lean` | 6 synthesis tests: complement8, complement32, lambdaConcat, lambdaAddConst, hwLet2, hwLet3 | #### Details **① `~~~` Complement for BitVec Signals:** ```lean -- Before (workaround): (fun x => !x) <$> sig -- After: ~~~sig ``` Generates: `assign out = ~_gen_a;` **② Complex Lambda with Constants:** ```lean -- Before (2-step): (· ++ ·) <$> Signal.pure 0#24 <*> sig -- After (single lambda): (fun d => (0#24 ++ d : BitVec 32)) <$> sig ``` Generates: `assign out = {24'd0, _gen_sig};` **③ `hw_let` Tuple Destructuring:** ```lean -- Before: let a := Signal.fst sig; let b := Signal.snd sig -- After: hw_let (a, b) := sig; ``` ### Lock-Free CDC Infrastructure (Phase 41) — DONE Lock-free Clock Domain Crossing for Time-Warping simulation across multiple clock domains. Four sub-phases: SPSC queue, rollback mechanism, formal proofs, JIT integration with multi-clock E2E test. #### New Files | File | Description | |------|-------------| | `c_src/cdc/spsc_queue.hpp` | Header-only SPSC lock-free queue, ARM64-optimized (release/acquire/relaxed ordering, alignas(64) false-sharing prevention, power-of-2 ring buffer). 210M ops/sec | | `c_src/cdc/cdc_rollback.hpp` | CDCConsumer: timestamp inversion detection + snapshot-based state restoration. Queue indices architecturally isolated from simulation state | | `c_src/cdc/cdc_runner.hpp` | C-compatible header defining CDCJITVtable and cdc_run() entry point | | `c_src/cdc/cdc_runner.cpp` | Multi-threaded CDC runner: two JIT domains on separate threads connected by SPSC queue, with consumer_done flag to prevent producer deadlock | | `c_src/cdc/cdc_test.cpp` | Correctness tests (10M messages, 0 loss, 0 inversion) + benchmark + rollback tests | | `c_src/cdc/cdc_example.cpp` | Complete multi-clock simulation demo (100MHz + 50MHz domains) | | `c_src/cdc/Makefile` | Standalone C++20 build for tests, example, and cdc_runner.so | | `Sparkle/Verification/CDCProps.lean` | 12 formal theorems (all proven, no sorry): SPSC safety, rollback guarantee, queue index isolation | | `Examples/CDC/MultiClockSim.lean` | Signal DSL counter + accumulator modules with `#writeDesign` synthesis | | `Tests/CDC/MultiClockTest.lean` | E2E: JIT-compile both domains, run via `JIT.runCDC`, verify message transfer | #### Modified Files | File | Change | |------|--------| | `c_src/sparkle_jit.c` | Added `sparkle_jit_run_cdc`: dlopen bridge to cdc_runner.so, CDCJITVtable packing, Lean FFI tuple return | | `Sparkle/Core/JIT.lean` | Added `JIT.runCDC` FFI binding | | `lakefile.lean` | Added `Examples.CDC` lean_lib and `cdc-multi-clock-test` lean_exe | #### Architecture ``` Lean (JIT.lean) │ JIT.runCDC handleA handleB cyclesA cyclesB outPortA inPortB │ @[extern "sparkle_jit_run_cdc"] ▼ sparkle_jit.c (compiled with -nostdinc via buildLeanO) │ Packs JITHandle → CDCJITVtable (function pointer table) │ dlopen("cdc_runner.so") → cdc_run() ▼ cdc_runner.so (compiled with -std=c++20, system headers) │ ├── Thread A (Producer) ├── Thread B (Consumer) │ eval_tick(ctx_a) loop │ pop from SPSC queue │ get_output → SPSC push │ set_input(ctx_b) + eval_tick │ │ CDCConsumer rollback detection │ │ snapshot/restore via vtable └────── SPSC Lock-Free Queue ────────┘ ``` #### Formal Proofs (12 theorems, all proven) - **SPSC Safety**: no_overflow, no_underflow, push_preserves_nonempty, pop_frees_space - **Rollback**: rollback_guarantee (inversion → flag + restore), rollback_advances_read_idx, consume_preserves_write_idx - **Monotonicity**: normal_consume_no_rollback, normal_consume_time_advances, consume_always_advances_read_idx, consume_preserves_snapshot, rollback_rewinds_time #### Test Results - **SPSC Queue**: 10M messages, 0 data loss, 0 ordering inversions, 210M ops/sec - **Rollback**: Single-threaded + multi-threaded rollback detection verified - **E2E CDC**: DomainA 200K cycles (100MHz), DomainB 100K cycles (50MHz), ~75K messages transferred, 2.34ms wall time ### Hardware MP4 Encoder (Phase 40) — DONE Hardware module that wraps the frame encoder and emits a complete playable MP4 file. Two-pass architecture: buffer encoder output, then emit ROM template (patched at runtime) + mdat + IDR NAL. #### New Files | File | Description | |------|-------------| | `IP/Video/H264/MP4Encoder.lean` | Hardware MP4 muxer FSM (12 registers, 7 phases, 2 memories, nested frame encoder sub-module) | | `IP/Video/H264/MP4EncoderSynth.lean` | Synthesis wrapper: `#writeDesign` → SV + CppSim + JIT | | `Tests/Video/H264MP4EncoderTest.lean` | JIT end-to-end test: ROM loading, table loading, encode, collect MP4 bytes | #### Modified Files | File | Change | |------|--------| | `IP/Video/H264/MP4Mux.lean` | Added `buildMP4ROMTemplate` (629-byte ftyp+moov ROM template) | | `IP/Video/H264.lean` | Added imports for MP4Encoder and MP4EncoderSynth | | `lakefile.lean` | Added `h264-mp4-encoder-test` executable | #### Architecture ``` h264MP4Encoder (Signal.loop, 12 registers) ├── h264FrameEncoder (nested sub-module) ├── IDR buffer memory (1024×8-bit, memoryComboRead) ├── MP4 ROM memory (1024×8-bit, memoryComboRead, host-loaded) └── FSM: IDLE → RUN_ENCODER → EMIT_ROM → EMIT_MDAT_HDR → EMIT_IDR_LEN → EMIT_IDR_DATA → DONE ``` **Pass 1**: Run frame encoder, skip first 27 bytes (SPS+PPS+start code), buffer IDR NAL bytes. **Pass 2**: Emit ftyp+moov from ROM (patching width/height/stsz at 16 byte offsets), then mdat header + buffered IDR bytes. #### ROM Patching (16 byte offsets) | Offset | Field | Value | |--------|-------|-------| | 232–235 | tkhd width | `width << 16` as BE32 | | 236–239 | tkhd height | `height << 16` as BE32 | | 445–446 | avc1 width | `width` as BE16 | | 447–448 | avc1 height | `height` as BE16 | | 605–608 | stsz entry | `4 + idrNalSize` as BE32 | #### Test Results - **709 bytes** output — byte-identical to software muxer reference - **6149 cycles** (5437 encode + 629 ROM + 83 mdat/IDR) - ffprobe validates as playable 16×16 H.264 Baseline MP4 - ROM vs reference (629 bytes): exact match - IDR NAL (68 bytes): exact match ### Autonomous Frame Encoder (Phase 39) — DONE Single hardware module (`h264FrameEncoder`) that takes a 16×16 frame and produces a complete H.264 Annex-B byte stream (SPS + PPS + IDR slice). 20-phase block sequencer FSM with MSB-aligned 64-bit bit buffer, 3 nested sub-modules (encoder, decoder, CAVLC), and multiple internal memories. #### New Files | File | Description | |------|-------------| | `IP/Video/H264/FrameEncoder.lean` | Autonomous frame encoder (21 registers, 20 phases, 3 sub-modules) | | `IP/Video/H264/FrameEncoderSynth.lean` | Synthesis wrapper | | `Tests/Video/H264FrameEncoderTest.lean` | JIT test: per-block CAVLC comparison + MP4 wrapping | #### Test Results - All 16 CAVLC blocks match pure Lean reference - Output: 95 bytes H.264 Annex-B (SPS + PPS + IDR slice) - Wrapped in MP4: 709 bytes, playable with ffplay ### Software MP4 Muxer (Phase 38) — DONE Pure Lean ISO BMFF container builder for wrapping H.264 NAL units in playable .mp4 files. #### New Files | File | Description | |------|-------------| | `IP/Video/H264/MP4Mux.lean` | MP4 box builders (ftyp, moov, trak, stbl, mdat) + `muxSingleFrame` | ### Playable H.264 Output (Phase 37) — DONE End-to-end test producing a playable .h264 and .mp4 file from the synthesizable pipeline. #### New Files | File | Description | |------|-------------| | `Tests/Video/H264PlayableTest.lean` | Playable H.264 test: encode frame → Annex-B → MP4 | ### H.264 Synthesizable Annex-B Encoder (Phase 36) — DONE Fully synthesizable CAVLC entropy encoder + NAL byte stream packer, producing valid H.264 Annex-B bitstreams from quantized coefficients. End-to-end: pixels → encoder pipeline → CAVLC synth → NAL packing → .h264 file. #### New Files | File | Description | |------|-------------| | `IP/Video/H264/CAVLCSynth.lean` | Synthesizable CAVLC encoder FSM (~726 lines, 23 registers, 7 memories) | | `IP/Video/H264/VLCTables.lean` | Host-side VLC table builders for coeff_token, total_zeros, run_before | | `IP/Video/H264/NALSynth.lean` | Synthesizable NAL byte stream packer (start code + header + emulation prevention) | | `IP/Video/H264/SPSPPSData.lean` | Pre-computed SPS/PPS/slice header bytes for 16x16 Baseline | | `Tests/Video/H264BitstreamTest.lean` | End-to-end Annex-B test: 16x16 gradient → 4 blocks → .h264 file | #### Modified Files | File | Change | |------|--------| | `Tests/Video/H264JITPipelineTest.lean` | Added Test 5: CAVLC synth JIT (VLC table loading, FSM, reference comparison) | | `IP/Video/H264.lean` | Added imports for new modules | | `lakefile.lean` | Added `h264-bitstream-test` executable | #### Tests - Test 5: CAVLC synth JIT — matches pure Lean `cavlcEncodeFull` for test coefficients (17 bits) - Bitstream test: 4 blocks of real encoder output all match pure reference (25-32 bits each) - Output: `IP/Video/H264/gen/test_output.h264` (42 bytes) ### H.264 Parameterized QP (Phase 35) — DONE Replaced hardcoded QP=20 constants with input ports carrying pre-computed QP-dependent values. Host software computes dequant V*scale and quant MF/f/qbits from QP using lookup tables, avoiding large hardware LUTs. #### Modified Files | File | Change | |------|--------| | `IP/Video/H264/Quant.lean` | Added `dequantScales`, `quantParams` helpers; made `getV`/`getMF` public | | `IP/Video/H264/DequantSynth.lean` | Added `vscale0/1/2` input ports; parameterized `dequantRef`/`dequantBlockRef` with `qp` | | `IP/Video/H264/QuantSynth.lean` | Added `quantMF0/1/2`, `quantF`, `quantShift` input ports; variable shift; parameterized `quantRef`/`quantBlockRef` with `qp` | | `IP/Video/H264/DecoderSynth.lean` | Added `vscale0/1/2` input ports to pipeline; parameterized `decoderPipelineRef` with `qp` | | `IP/Video/H264/EncoderSynth.lean` | Added `quantMF0/1/2`, `quantF`, `quantShift` input ports to pipeline; parameterized `encoderPipelineRef` with `qp` | | `Tests/Video/H264JITPipelineTest.lean` | Added `setDecoderQP`/`setEncoderQP` helpers; added Test 4 (QP=10 roundtrip cross-validation) | | `docs/STATUS.md` | Added Phase 35 section | #### Tests - Test 1: Decoder QP=20 — JIT matches pure Lean reference - Test 2: Encoder QP=20 — JIT matches pure Lean reference - Test 3: Roundtrip QP=20 — all pixels in [0, 255] - Test 4 (NEW): Roundtrip QP=10 — encoder/decoder JIT match parameterized reference, perfect reconstruction ### H.264 JIT Pipeline Tests (Phase 34) — DONE End-to-end JIT tests for both decoder and encoder pipelines, plus encoder→decoder roundtrip. #### New Files | File | Description | |------|-------------| | `Tests/Video/H264JITPipelineTest.lean` | JIT end-to-end tests: decoder, encoder, roundtrip | #### Modified Files | File | Change | |------|--------| | `lakefile.lean` | Added `h264-jit-pipeline-test` executable | | `Sparkle/Backend/CppSim.lean` | Fixed missing `_rdata` member declaration for `Signal.memory` and unused `Signal.memoryComboRead` | | `IP/Video/H264/DecoderSynth.lean` | Fixed `grpIdxNext` hw_cond default (was `0#3`, should be `grpIdx`) | | `IP/Video/H264/EncoderSynth.lean` | Fixed `grpIdxNext` hw_cond default (same bug as decoder) | | `docs/STATUS.md` | Added Phase 34 section | #### Bug Fixes - **CppSim `_rdata` declaration bug**: `emitStmt` for `Signal.memory` (non-combo) never declared the read data variable (`_rdata`) as a class member, causing C++ compilation errors. For `Signal.memoryComboRead` with unused results (name starts with `_`), the read data wire was also missing from declarations. Fixed by checking `typeMap` and adding declaration when the wire isn't already known. - **FSM `grpIdxNext` hold bug**: Both decoder and encoder pipelines used `hw_cond (0#3) | groupDone => grpInc` which defaults `grpIdxNext` to 0 when `groupDone` is false. This reset `grpIdx` every non-boundary cycle, preventing the IDCT/DCT from progressing past group 1. Fixed by using `hw_cond grpIdx` (hold current value as default). #### Tests - **Decoder JIT**: Loads quantized levels + prediction into memories, runs FSM (~98 cycles), compares output vs `decoderPipelineRef` - **Encoder JIT**: Loads original pixels + prediction into memories, runs FSM (~98 cycles), compares output vs `encoderPipelineRef` - **Roundtrip**: Encodes original pixels, feeds quantized levels into decoder, verifies all decoded pixels in [0, 255] ### H.264 Synthesizable Encoder Pipeline (Phase 33) — DONE Synthesizable encoder pipeline: residual → forward DCT → forward quantization, all as Signal DSL FSMs. #### New Files | File | Description | |------|-------------| | `IP/Video/H264/ForwardDCTSynth.lean` | Synthesizable forward DCT FSM (butterfly, 64 cycles) | | `IP/Video/H264/QuantSynth.lean` | Synthesizable forward quantization FSM (fixed QP=20, 16 cycles) | | `IP/Video/H264/EncoderSynth.lean` | Top-level encoder pipeline orchestrator (monolithic FSM, ~96 cycles) | | `Tests/Video/H264EncoderSynthTest.lean` | Pure Lean reference tests for all encoder sub-modules | #### Modified Files | File | Change | |------|--------| | `IP/Video/H264.lean` | Added imports for 3 new synth modules | | `Tests/AllTests.lean` | Wired in `H264EncoderSynthTest` | | `docs/STATUS.md` | Added Phase 33 section | #### Architecture ``` Original Pixels (16×16-bit) + Predicted Pixels (16×16-bit) → [Residual, 16 cycles] subtract original - predicted → [Forward DCT, 64 cycles] row + col butterfly transform → [Forward Quant, 16 cycles] (abs × MF + f) >> qbits, QP=20 → Quantized Levels (16×16-bit) ``` #### Sub-Module Design **ForwardDCTSynth** (64 cycles): - Two-phase butterfly: row transform (32 cycles) + col transform (32 cycles) - Each phase: 4 groups × 8 sub-steps (4 read + 4 write) - s0=v0+v3, s1=v1+v2, d0=v0-v3, d1=v1-v2 - Y0=s0+s1, Y1=2*d0+d1, Y2=s0-s1, Y3=d0-2*d1 (no rounding) - `FwdDCTState`: 8 registers (fsm, grpIdx, substep, done, val0-val3) **QuantSynth** (16 cycles): - Fixed QP=20: qbits=18, f=87381, MF=[10082, 4194, 6554] - Position class computed from idx bits: bothEven→10082, bothOdd→4194, mixed→6554 - Sign extraction, abs, multiply+f, shift right 18, sign restore - `QuantState`: 4 registers (fsm, idx, done, last) **EncoderSynth** (monolithic, ~96 cycles): - Phases: IDLE→RESIDUAL→DCT_ROW→DCT_COL→QUANT→DONE - All three stages in a single `Signal.loop` with shared memories - 6 memories for data passing between stages - `EncoderPipeState`: 12 registers ### H.264 Synthesizable Decoder Pipeline (Phase 32) — DONE Synthesizable decoder pipeline: dequant → IDCT → reconstruct, all as Signal DSL FSMs. #### New Files | File | Description | |------|-------------| | `IP/Video/H264/DequantSynth.lean` | Synthesizable dequantization FSM (fixed QP=20, 16 cycles) | | `IP/Video/H264/IDCTSynth.lean` | Synthesizable inverse DCT FSM (butterfly, 64 cycles) | | `IP/Video/H264/ReconstructSynth.lean` | Synthesizable reconstruction FSM (add+clamp, 16 cycles) | | `IP/Video/H264/DecoderSynth.lean` | Top-level pipeline orchestrator (monolithic FSM, ~96 cycles) | | `Tests/Video/H264DecoderSynthTest.lean` | Pure Lean reference tests for all sub-modules | #### Modified Files | File | Change | |------|--------| | `IP/Video/H264.lean` | Added imports for 4 new synth modules | | `Tests/AllTests.lean` | Wired in `H264DecoderSynthTest` | | `docs/STATUS.md` | Added Phase 32 section | | `README.md` | Updated H.264 section + roadmap | #### Generated Verilog (12 files in `IP/Video/H264/gen/`) | Module | SV Size | CppSim | JIT | |--------|---------|--------|-----| | `dequant` | 8 KB | `dequant_cppsim.h` | `dequant_jit.cpp` | | `idct` | 20 KB | `idct_cppsim.h` | `idct_jit.cpp` | | `reconstruct` | 7 KB | `reconstruct_cppsim.h` | `reconstruct_jit.cpp` | | `decoder_pipeline` | 29 KB | `decoder_pipeline_cppsim.h` | `decoder_pipeline_jit.cpp` | #### Architecture ``` Quantized Levels (16×16-bit) → [Dequant FSM] → Dequantized Coefficients → [IDCT FSM] → Decoded Residual → [Reconstruct FSM] → Clamped Pixels [0,255] ``` #### Sub-Module Design **DequantSynth** (16 cycles): - Fixed QP=20: qp%6=2, qp/6=3 → V=[13,20,16], scale=2^3=8 - Position class computed from idx bits: bothEven→104, bothOdd→160, mixed→128 - Sign extraction, abs, multiply, sign restore - `DequantState`: 4 registers (fsm, idx, done, last) **IDCTSynth** (64 cycles): - Two-phase butterfly: row transform (32 cycles) + col transform (32 cycles) - Each phase: 4 groups × 8 sub-steps (4 read + 4 write) - s0=v0+v2, s1=v0-v2, d0=v1/2-v3, d1=v1+v3/2 - Row results: no rounding. Col results: (+32)>>6 via sarBy6 - `IDCTState`: 8 registers (fsm, grpIdx, substep, done, val0-val3) **ReconstructSynth** (16 cycles): - predicted (unsigned) + residual (signed) → clamp [0,255] - Sign bit check for negative, upper byte check for >255 - Dual combo-read memories (prediction + residual) - `ReconState`: 4 registers (fsm, idx, done, last) **DecoderSynth** (monolithic, ~96 cycles): - Phases: IDLE→DEQUANT→IDCT_ROW→IDCT_COL→RECON→DONE - All three stages in a single `Signal.loop` with shared memories - 6 combo-read/regular memories for coefficient passing - `DecoderPipeState`: 12 registers #### Synthesis Patterns Used - `declare_signal_state` for state definition - `Signal.loop fun state =>` for FSM - `Signal.memoryComboRead` for same-cycle reads - `Signal.memory` for registered writes - `bundleAll!` for state bundling - `hw_cond` for FSM transitions - `===` for state comparison, `&&&`/`|||` for boolean logic - `(· * ·) <$> a <*> b` for arithmetic - `.map (BitVec.extractLsb' start len ·)` for bit extraction/shifts - `sarBy1`/`sarBy6` helpers for signed arithmetic right shift #### Tests (all pass) | Test | Description | |------|-------------| | `dequant pos0 = 10*104 = 1040` | V×scale for corner position | | `dequant pos1 = -2*128 = -256` | V×scale for mixed position | | `dequant pos4 = -8*128 = -1024` | V×scale for mixed position | | `dequant pos12 = -1*128 = -128` | V×scale for mixed position | | `idct has non-zero values` | IDCT produces meaningful output | | `reconstruct all in [0,255]` | Clamping works | | `reconstruct not uniform` | Non-trivial output | | `pipeline matches step-by-step` | Full pipeline = dequant→IDCT→recon | | `large negative clamps to 0` | Edge case | | `large positive clamps to 255` | Edge case | | `zero levels → pure prediction` | Zero input passthrough | #### Known Limitations - **Fixed QP=20 only** — parameterized QP requires runtime LUT selection (deferred to Phase 33) - **CAVLC decoder not included** — pipeline accepts pre-decoded quantized levels - **Single 4×4 block only** — no frame-level integration - **DRC warnings** — outputs not registered (combinational output from `bundleAll!` to `out`) --- ### H.264 CAVLC Decoder Fix (Phase 31c) — DONE Fixed CAVLC decoder to correctly reconstruct non-trivial residuals. Previously returned all zeros for non-zero blocks, causing frame-level MSE ~3071 at all QP levels. #### What Was Fixed | Bug | Fix | |-----|-----| | **Incomplete VLC tables** | Rewrote `decodeCoeffToken`, `decodeTotalZeros`, `decodeRunBefore` to iterate over encoder's complete lookup tables instead of hand-coded partial prefix trees | | **Missing inverse zig-zag** | Applied `inverseZigzag` at end of `cavlcDecode` — decoder now returns raster-order coefficients matching what `dequantizeBlock` expects | #### Approach Added `BitstreamReader.matchCode` helper that checks if the bitstream at current position matches a VLC code. All three decode functions now iterate over every valid table entry and match against the bitstream prefix. H.264 VLC codes are prefix-free, so exactly one match exists. Made `coeffTokenLookup`, `totalZerosLookup`, `runBeforeLookup` non-private in CAVLC.lean so the decoder can reference them. #### New Formal Proofs (CAVLCProps.lean) | Theorem | Description | |---------|-------------| | `cavlc_nonzero_roundtrip` | Roundtrip for `[0, 3, -1, 0, 0, -1, ...]` (TC=3, T1=2) | | `cavlc_single_coeff_roundtrip` | Roundtrip for `[5, 0, 0, ...]` (DC-only) | | `cavlc_trailing_ones_roundtrip` | Roundtrip for `[1, -1, 1, 0, ...]` (T1-only) | #### Frame-Level MSE Results | QP | Before (MSE) | After (MSE) | Threshold | |----|-------------|-------------|-----------| | 0 | 3071 | 3166 | ≤ 4000 | | 10 | 3083 | 2741 | ≤ 4000 | | 30 | 3071 | 284 | ≤ 500 | QP=30 improved dramatically (3071→284) because quantization keeps TC low and coefficient values small. QP=0/10 remain high due to encoder's 32-bit buffer overflow with large DCT coefficients. #### Known Limitations (pre-existing in encoder) - 32-bit bitstream buffer overflows for blocks with large coefficient values (QP=0/10) - `totalZerosLookup` only covers TC=1-6 (TC≥7 blocks won't roundtrip) - `runBeforeLookup` fallback for runBefore≥7 uses `(0,1)` (incorrect per H.264 spec) - `coeffTokenLookup` has prefix collision between TC=2,T1=1 and TC=3,T1=3 entries --- ### H.264 Frame-Level End-to-End Test (Phase 31b) — DONE Frame-level encode→decode roundtrip test for H.264 Baseline Profile. Encodes multi-block (16×16) images through the full pipeline, decodes block-by-block with neighbor reconstruction, and verifies quality at different QP levels via both direct bitstream and NAL pack/parse paths. #### What Was Built | Component | Description | |-----------|-------------| | `decodeFrame` | Frame-level decoder via direct bitstream path (reconstructs neighbors from previously decoded blocks in raster order) | | `decodeFrameFromNAL` | Frame-level decoder via NAL pack/parse path | | `makeGradientImage` | Diagonal gradient test image generator | | `makeQuadrantImage` | 4-quadrant image exercising multiple prediction modes | | `computeFrameMSE` | Frame-level mean squared error computation | #### Test Results ``` --- H.264 Frame-Level Tests --- QP=0 Bitstream: ✓ 16 blocks encoded, MSE=3166 (≤ 4000) QP=0 NAL: ✓ MSE=3166 (≤ 4000) QP=10 Quality: ✓ MSE=2741 (≤ 4000) QP=30 Quality: ✓ MSE=284 (≤ 500) Prediction Mode Diversity: ✓ 2 unique modes (≥ 2 required) Path Equivalence: ✓ bitstream and NAL paths produce identical output ``` **Note**: CAVLC decoder fixed in Phase 31c. QP=30 threshold tightened to ≤ 500 (actual MSE=284). QP=0/10 thresholds remain at ≤ 4000 due to a pre-existing encoder limitation — the 32-bit bitstream buffer overflows with large DCT coefficients at low QP. #### Files Created | File | Description | |------|-------------| | `Tests/Video/H264FrameTest.lean` | All frame-level decode logic, image generators, MSE computation, 6 test groups (7 assertions) | #### Files Modified | File | Change | |------|--------| | `Tests/AllTests.lean` | Added `import Tests.Video.H264FrameTest`, wired `h264FrameTests` into runner | --- ### H.264 Baseline Encoder + Decoder Pipeline (Phase 31) — DONE Complete H.264 Baseline Profile encoder and decoder pipeline — all 9 sub-phases implemented: DRAM Interface, DCT/IDCT, Quant/Dequant, CAVLC Decode, NAL Pack/Parse, Intra Prediction, Encoder, Decoder, and JIT End-to-End Test. Each module has pure Lean reference functions, formal proofs (no `sorry`), C++ golden value generators, and LSpec tests. #### Architecture ``` ENCODER: Pixels → Intra Pred → DCT → Quant → Scan+CAVLC → NAL → Bitstream DECODER: Bitstream → NAL Parse → CAVLC Decode → Dequant → IDCT → Intra Recon → Pixels JIT: Quant→Dequant synthesizable FSM — compile, load, run, verify via JIT API ``` #### What Was Built | Phase | Module | Files | Key Content | |-------|--------|-------|-------------| | **1** | DRAM Interface | `DRAMInterface.lean`, `DRAMProps.lean` | Pure simulation model (`IO.Ref HashMap`), read-after-write/write-write/read-default proofs | | **2** | DCT/IDCT | `DCT.lean`, `DCTProps.lean` | 4×4 integer DCT (H.264 spec 8.5.10/8.5.12), bounded roundtrip error proof, skeletal Signal FSM | | **3** | Quant/Dequant | `Quant.lean`, `QuantProps.lean` | MF/V tables (6 QP classes × 3 position classes), zero/sign preservation proofs | | **4** | CAVLC Decode | `CAVLCDecode.lean`, `CAVLCProps.lean` | Multi-pass bitstream parser (coeff_token → trailing_ones → levels → total_zeros → run_before), roundtrip proof | | **5** | NAL Pack/Parse | `NAL.lean`, `NALProps.lean` | Start code prefix, emulation prevention byte stuffing/removal, roundtrip proof | | **6** | Intra Prediction | `IntraPred.lean`, `IntraPredProps.lean` | 9 Intra_4×4 modes (vertical, horizontal, DC, diagonals), residual roundtrip proof | | **7** | Encoder Top | `Encoder.lean` | `encodeBlock`: IntraPred → DCT → Quant → Zigzag → CAVLC → NAL. `encodeFrame`: 4×4 block iteration | | **8** | Decoder Top | `Decoder.lean` | `decodeBlock`: NAL → CAVLC Decode → Dequant → IDCT → IntraPred Recon. `encodeDecodeRoundtrip` integration | | **9** | JIT E2E Test | `QuantRoundtripSynth.lean`, `H264JITTest.lean` | Synthesizable quant→dequant FSM (16 coefficients, fixed QP=0), JIT compile+load+run, 4 tests all PASS | #### Formal Proofs (all without `sorry`) | Module | Theorem | Statement | |--------|---------|-----------| | DRAM | `read_after_write` | Write v to addr a, then read addr a → returns v | | DRAM | `read_default` | Reading unwritten address returns 0 | | DRAM | `write_write_same_addr` | Last write wins | | DCT | `dct_idct_bounded_error` | `∀ i, |IDCT(DCT(x))[i] - x[i]| ≤ 1` (integer rounding) | | DCT | `dct_linearity` | `DCT(a + b) = DCT(a) + DCT(b)` | | Quant | `quant_zero` | `quant(0, qp, pos) = 0` | | Quant | `quant_sign_preserves` | `sign(quant(x)) = sign(x)` when result ≠ 0 | | CAVLC | `cavlc_roundtrip` | `decode(encode(coeffs)) = coeffs` for valid 4×4 blocks | | NAL | `nal_roundtrip` | `parse(pack(payload)) = payload` | | IntraPred | `residual_roundtrip` | `predicted + (original - predicted) = original` | #### JIT End-to-End Test (Phase 9) The CAVLC encoder FSM is not fully synthesizable (7-arg lambda in ENCODE state). Instead, a minimal **quant→dequant roundtrip** module was created as the synthesizable JIT test target: - `QuantRoundtripSynth.lean`: `declare_signal_state QDState` (4 registers: fsm, idx, done, lastOut), `Signal.memoryComboRead` for input memory, `Signal.memory` for output memory, `#writeDesign` generates SV + CppSim + JIT - `H264JITTest.lean`: Compiles JIT wrapper, loads via `JIT.compileAndLoad`, runs 4 tests: - **Test 1**: Zero block → all zeros (PASS) - **Test 2**: DCT coefficients (16 values) → exact match with `quantDequantRef` (PASS) - **Test 3**: Single large coefficient (200) → exact match (PASS) - **Test 4**: Negative coefficients (-50, -100, -200, -500) → exact match (PASS) Key technical details: - Uses `JIT.getWire` with named wire `_gen_done` (NOT `JIT.getOutput` — packed tuple outputs not assigned in JIT) - Memory indices: `memoryComboRead` = index 0 (input), `memory` = index 1 (output) - FSM stays in DONE until re-started via `startAndDone` transition #### C++ Golden Value Generators | File | Description | |------|-------------| | `scripts/Video/generate_dct_golden.cpp` | Forward/inverse 4×4 integer DCT test vectors | | `scripts/Video/generate_quant_golden.cpp` | Quant/dequant with MF/V tables, QP sweep | | `scripts/Video/generate_cavlc_decode_golden.cpp` | CAVLC decode test vectors | | `scripts/Video/generate_nal_golden.cpp` | NAL pack/parse with emulation prevention | | `scripts/Video/generate_intrapred_golden.cpp` | Intra_4×4 prediction (9 modes) | #### LSpec Test Results ``` --- H.264 Pipeline Tests --- DRAM Interface: ✓ read-after-write, write-overwrite, read-default DCT/IDCT: ✓ forward DCT matches golden, IDCT roundtrip ≤ 1 error Quant/Dequant: ✓ zero preservation, sign preservation, roundtrip CAVLC Decode: ✓ roundtrip for zero/DC/mixed blocks NAL Pack/Parse: ✓ roundtrip, emulation prevention Intra Prediction: ✓ vertical/horizontal/DC modes, residual roundtrip Full Pipeline: ✓ encode→decode roundtrip (QP=0 exact, QP>0 bounded error) ``` #### Files Created | Directory | Files | |-----------|-------| | `IP/Video/H264/` | `DRAMInterface.lean`, `DRAMProps.lean`, `DCT.lean`, `DCTProps.lean`, `Quant.lean`, `QuantProps.lean`, `CAVLCDecode.lean`, `CAVLCProps.lean`, `NAL.lean`, `NALProps.lean`, `IntraPred.lean`, `IntraPredProps.lean`, `Encoder.lean`, `Decoder.lean`, `QuantRoundtripSynth.lean` | | `IP/Video/H264/gen/` | `quant_roundtrip.sv`, `quant_roundtrip_cppsim.h`, `quant_roundtrip_jit.cpp` | | `Tests/Video/` | `DRAMTest.lean`, `DCTTest.lean`, `QuantTest.lean`, `CAVLCDecodeTest.lean`, `NALTest.lean`, `IntraPredTest.lean`, `H264PipelineTest.lean`, `H264JITTest.lean` | | `scripts/Video/` | `generate_dct_golden.cpp`, `generate_quant_golden.cpp`, `generate_cavlc_decode_golden.cpp`, `generate_nal_golden.cpp`, `generate_intrapred_golden.cpp` | #### Files Modified | File | Change | |------|--------| | `IP/Video/H264.lean` | Added imports for all 15 new H.264 modules | | `Tests/AllTests.lean` | Added imports + integration for 8 H.264 test modules | | `lakefile.lean` | Added `lean_exe «h264-jit-test»` executable target | #### Verification ```bash lake build # ✓ 247 jobs, all compile lake test # ✓ All H.264 tests pass (only pre-existing CppSim bit-accuracy failure) lake exe h264-jit-test # ✓ ALL 4 H.264 JIT TESTS PASSED ``` --- ### eval()+tick() Fusion (Phase 30) — DONE Fused `eval()` and `tick()` into a single `evalTick()` method where register `_next` variables are stack-local instead of class members. This eliminates ~260 intermediate memory operations per cycle (130 stores from eval + 130 loads from tick). Existing `eval()` and `tick()` methods preserved for backward compatibility (`runOptimized` oracle path needs separate eval/tick because the skip branch does not call tick). #### What Was Built | Component | File | Description | |-----------|------|-------------| | **C++ codegen** | `Sparkle/Backend/CppSim.lean` | `evalTickLocals` in `StmtParts`, `evalTick()` method, `jit_eval_tick` wrapper | | **C FFI** | `c_src/sparkle_jit.c` | `eval_tick` function pointer, dlsym, `sparkle_jit_eval_tick` LEAN_EXPORT | | **Lean binding** | `Sparkle/Core/JIT.lean` | `JIT.evalTick` opaque extern | | **Hot path update** | `Sparkle/Core/JITLoop.lean` | `JIT.run` and `loopMemoJITImpl` use `evalTick` | #### Benchmark Results (10M cycles, firmware.hex, Apple Silicon) | Benchmark | eval+tick | evalTick | Speedup | |-----------|-----------|----------|---------| | Pure (no wire reads) | 12.7M cyc/s | 13.0M cyc/s | 1.02x | | With 6 wire reads | 12.4M cyc/s | 12.7M cyc/s | 1.03x | Measured speedup is modest (~2-3%). Clang -O2 was already promoting `_next` class members to registers for this workload (tight self-loop). Complex workloads (Linux boot with higher register pressure) may show larger gains. #### Wire Reads After evalTick() Observable wires (e.g., `_gen_pcReg`) are class members assigned during the eval phase. The tick phase only copies `_next → current` for registers and performs memory writes/reads — it does NOT modify wires. So `JIT.getWire` after `evalTick()` returns the current cycle's values. #### Verification ``` lake exe rv32-jit-test # ✓ ALL 47 UART words, PASS at cycle 2904 lake exe rv32-jit-oracle-test # ✓ PASS, eval+tick path unchanged lake exe rv32-jit-dynamic-warp-test # ✓ PASS, memsetWord + BSS-clear lake exe rv32-jit-speculative-warp-test # ✓ ALL 3 PARTS PASSED ``` --- ### Speculative Simulation with Snapshot/Restore (Phase 29 Step 5) — DONE Full-state snapshot/restore API using C++ default copy constructor, enabling guard-and-rollback speculative simulation. The oracle takes a snapshot, speculatively applies bulk updates (memsetWord, timer advancement), checks guard conditions (e.g., timer interrupt), and rolls back if the guard fails — providing bit-accurate cycle-skipping even in the presence of interrupts. ### What Was Built | Component | File | Description | |-----------|------|-------------| | **C++ codegen** | `Sparkle/Backend/CppSim.lean` | `jit_snapshot`, `jit_restore`, `jit_free_snapshot` in `toCppSimJIT` extern "C" block | | **C FFI** | `c_src/sparkle_jit.c` | 3 function pointers, dlsym calls, 3 LEAN_EXPORT wrappers | | **Lean bindings** | `Sparkle/Core/JIT.lean` | `JIT.snapshot`, `JIT.restore`, `JIT.freeSnapshot` opaque externs | | **Speculative warp test** | `Tests/RV32/JITSpeculativeWarpTest.lean` | 3-part test: roundtrip, guard-pass, guard-rollback | ### Snapshot/Restore API ```cpp // Auto-generated in JIT wrapper void* jit_snapshot(void* ctx); // Copy constructor → new object void jit_restore(void* ctx, void* snap); // Copy assignment void jit_free_snapshot(void* snap); // Delete snapshot ``` ```lean JIT.snapshot : JITHandle → IO UInt64 -- returns opaque pointer JIT.restore : JITHandle → UInt64 → IO Unit JIT.freeSnapshot : JITHandle → UInt64 → IO Unit ``` ### Design: C++ Default Copy Constructor The generated CppSim class uses only `std::array` and scalar types — no raw pointers or dynamic allocation. The default copy constructor produces a safe full-state copy (~64MB for RV32 SoC with 8×8MB DRAM byte banks, ~3ms memcpy-bound). Acceptable for the guard-check pattern (a few snapshots per simulation, not per cycle). ### Speculative Warp Test Results | Part | Test | Result | |------|------|--------| | **Part 1** | Snapshot/restore roundtrip (PC matches after restore) | PASS | | **Part 2** | Speculative warp, guard passes (mtimecmp=0xFFFFFFFF, no interrupt) | PASS — 389 triggers, 0 rollbacks, DMEM zeroed | | **Part 3** | Guard failure with rollback (mtimecmp=5, interrupt fires quickly) | PASS — 9,955 rollbacks detected | ### Guard-and-Rollback Pattern ```lean -- Oracle with speculative simulation let s ← JIT.snapshot h -- Save full state -- Speculatively apply bulk updates JIT.memsetWord h bankIdx 0 0 64 -- Zero DMEM JIT.setReg h mtimeLoIdx newTime -- Advance timer -- Guard check let mtime ← JIT.getReg h mtimeLoIdx let mcmp ← JIT.getReg h mtimecmpLoIdx if mtime < mcmp then JIT.freeSnapshot h s -- Guard passed, keep speculative state return some skipAmount else JIT.restore h s -- Guard failed, rollback JIT.freeSnapshot h s return none -- Fall through to normal tick ``` ### Verification - `lake exe rv32-jit-speculative-warp-test` — ALL 3 PARTS PASS - `lake exe rv32-jit-dynamic-warp-test` — PASS (no regression) - `lake exe rv32-jit-oracle-test` — PASS (no regression) --- ### Linux Boot Time-Warping — Dynamic Oracle + Bulk Memory API (Phase 29 Steps 1-4) — DONE Direct JITHandle access for oracles and bulk memory API, enabling dynamic register introspection and fast BSS zeroing for Linux boot acceleration. Step 4 adds a BSS-clear speculative warp test demonstrating the full pattern: inline firmware, loop detection, bulk memory zeroing, and cycle-skipping. ### What Was Built | Component | File | Description | |-----------|------|-------------| | **memsetWord API** | `Sparkle/Backend/CppSim.lean` | `emitMemsetWordSwitch` generates per-memory bulk fill with bounds checking | | **memsetWord FFI** | `c_src/sparkle_jit.c` | `memset_word` fn ptr, dlsym, `sparkle_jit_memset_word` LEAN_EXPORT | | **memsetWord binding** | `Sparkle/Core/JIT.lean` | `JIT.memsetWord` opaque extern | | **Refactored oracle** | `Sparkle/Core/JITLoop.lean` | Oracle signature: `JITHandle → Nat → Array UInt64 → IO (Option Nat)` | | **Self-contained oracle** | `Sparkle/Core/Oracle.lean` | `mkSelfLoopOracle` no longer takes `handle` — oracle receives it per-call, handles setReg internally | | **Dynamic warp test** | `Tests/RV32/JITDynamicWarpTest.lean` | Part 1: memsetWord roundtrip. Part 2: BSS-clear speculative warp — inline firmware, byte-bank pre-fill/verify, oracle loop detection, bulk DMEM zeroing, cycle-skipping — PASS | ### Key Design Changes - **Oracle receives JITHandle per-call**: enables dynamic register reads (`JIT.getReg handle`), bulk memory ops (`JIT.memsetWord handle`), and direct state mutation (`JIT.setReg handle`) — all self-contained - **Simplified return type**: `IO (Option Nat)` instead of `IO (Option (Nat × Array (UInt32 × UInt64)))` — oracle handles all mutations internally - **memsetWord**: fills memory range with bounds checking, generated per-memory bank (same pattern as `emitMemoryAccessSwitches`) ### BSS-Clear Speculative Warp Test (Step 4) The test demonstrates the complete speculative warp pattern end-to-end: 1. **Inline RISC-V firmware** (7 instructions): LUI/ADDI setup, SW/ADDI/ADDI/BNE BSS-clear loop, JAL halt — loaded directly via `JIT.setMem` (no hex file) 2. **DMEM byte-bank pre-fill**: All 4 DMEM byte banks (memIdx 1-4) pre-filled with 0xDEADBEEF at word addresses 0-63, verified before simulation 3. **BSS-clear oracle**: Detects the 4-instruction loop (PC 0x08-0x14) via tolerance-based anchor (pcTolerance=12), bulk-zeros all 4 DMEM byte banks via `JIT.memsetWord`, advances CLINT timer, skips 256 cycles per trigger 4. **Post-simulation verification**: All 256 DMEM entries (4 banks × 64 words) confirmed zero, oracle triggers > 0 ### DMEM Memory Layout The SoC uses 4 byte-wide `Signal.memory` banks for DMEM (byte-banked architecture): | memIdx | Bank | Data | Address Width | |--------|------|------|---------------| | 0 | IMEM | 32-bit instructions | 12-bit (4096 entries) | | 1 | byte0_rdata | bits [7:0] | 23-bit (8M entries) | | 2 | byte1_rdata | bits [15:8] | 23-bit (8M entries) | | 3 | byte2_rdata | bits [23:16] | 23-bit (8M entries) | | 4 | byte3_rdata | bits [31:24] | 23-bit (8M entries) | | 5-6 | rf_rs1/rs2 | Register file | 5-bit (32 entries) | | 7-10 | dram_ifetch | DRAM instruction fetch | 23-bit (8M entries) | ### Performance Results | Test | Metric | Value | |------|--------|-------| | **Part 1 (memsetWord roundtrip)** | 10 words on DMEM bank 1 | PASS | | **Part 2 (BSS-clear warp)** | DMEM zeroed (4 banks × 64 words) | PASS | | | Oracle triggers | 389 | | | Total cycles skipped | 99,584 | | | Effective cycle count | 100,041 | | | Wall-clock time | <1 ms | | **Oracle test (firmware.hex)** | Effective cyc/s | ~1.4B | | | Oracle triggers | 9,998 | | | UART words | 48 (0xCAFE0000 marker seen) | --- ### JIT Cycle-Skipping — Self-Loop Oracle (Phase 28) — DONE Self-loop detection oracle that detects when the CPU is stuck in a tight halt loop and skips forward by advancing the cycle counter + CLINT timer registers. Achieves **706x speedup** on post-halt simulation. ### What Was Built | Component | File | Description | |-----------|------|-------------| | **Oracle module** | `Sparkle/Core/Oracle.lean` | `SelfLoopConfig`, `SelfLoopState`, `mkSelfLoopOracle` factory (Phase 29: oracle receives JITHandle per-call) | | **Skip-count API** | `Sparkle/Core/JITLoop.lean` | `runOptimized` with oracle callback (Phase 29: `JITHandle → Nat → Array UInt64 → IO (Option Nat)`) | | **End-to-end test** | `Tests/RV32/JITOracleTest.lean` | 10M-cycle firmware test with oracle — PASS | ### Self-Loop Detection Algorithm - Monitors PC wire each cycle; tracks an **anchor PC** and counts consecutive cycles where the PC stays within `pcTolerance` bytes (default 12) - When count exceeds `threshold` (default 50, safely above 34-cycle divider stall), triggers a skip - On trigger: advances CLINT timer (mtimeLo/mtimeHi) by `skipAmount` (default 1000), skips `tick()`, advances cycle counter - Handles multi-instruction halt loops (e.g., 4-instruction loop at 0x48–0x54) via tolerance-based matching ### Performance Results (firmware.hex, 10M cycles) | Metric | Without Oracle | With Oracle | |--------|---------------|-------------| | Wall-clock time | ~770 ms | **7 ms** | | Effective cyc/s | 13.0M | **~1.4 billion** | | Oracle triggers | — | 9,998 | | Cycles skipped | — | 9,998,000 | | UART output | 48 words + 0xCAFE0000 | identical | --- ## JIT Cycle-Skipping Infrastructure — Phase 1 (Phase 27) — DONE Register read/write API at every layer (C++ codegen → C FFI → Lean bindings → optimized run loop) enabling snapshot/restore of simulation state. This is the foundation for oracle-driven cycle-skipping, where an external function detects steady-state patterns (e.g., busy-wait loops) and jumps the simulation forward. ### What Was Built | Component | File | Description | |-----------|------|-------------| | **C++ codegen** | `Sparkle/Backend/CppSim.lean` | `collectRegisters`, `emitSetRegSwitch`, `emitGetRegSwitch`, `emitRegNameSwitch`; 4 new extern "C" functions in JIT wrapper | | **C FFI** | `c_src/sparkle_jit.c` | 4 function pointers in `JITHandle`, 4 `dlsym` calls, 4 Lean export wrappers | | **Lean bindings** | `Sparkle/Core/JIT.lean` | `setReg`, `getReg`, `regName`, `numRegs` opaque bindings + `findReg` helper | | **Optimized run loop** | `Sparkle/Core/JITLoop.lean` | `JIT.runOptimized` (oracle callback for cycle-skipping) + `JIT.resolveRegs` | | **Roundtrip test** | `Tests/RV32/JITCycleSkipTest.lean` | Snapshot/restore roundtrip — PASS | ### Register API ```cpp // Auto-generated in JIT wrapper (130 registers for RV32 SoC) void jit_set_reg(void* ctx, uint32_t reg_idx, uint64_t val); uint64_t jit_get_reg(void* ctx, uint32_t reg_idx); const char* jit_reg_name(uint32_t idx); uint32_t jit_num_regs(); // returns 130 ``` ```lean -- Lean bindings JIT.setReg : JITHandle → UInt32 → UInt64 → IO Unit JIT.getReg : JITHandle → UInt32 → IO UInt64 JIT.regName : JITHandle → UInt32 → IO String JIT.numRegs : JITHandle → IO UInt32 JIT.findReg : JITHandle → String → IO (Option UInt32) -- Oracle-driven run loop (Phase 29: oracle receives JITHandle directly) JIT.runOptimized : JITHandle → Nat → Array UInt32 → (JITHandle → Nat → Array UInt64 → IO (Option Nat)) -- oracle (self-contained) → (Nat → Array UInt64 → IO Bool) -- callback → IO Nat JIT.memsetWord : JITHandle → UInt32 → UInt32 → UInt32 → UInt32 → IO Unit -- Full-state snapshot/restore (Phase 29 Step 5) JIT.snapshot : JITHandle → IO UInt64 -- copy constructor → opaque pointer JIT.restore : JITHandle → UInt64 → IO Unit -- copy assignment from snapshot JIT.freeSnapshot : JITHandle → UInt64 → IO Unit -- delete snapshot ``` ### Snapshot/Restore Test ``` CycleSkip: 130 registers found CycleSkip: Running 100 cycles... CycleSkip: PC at cycle 100 = 0x80000fc CycleSkip: Snapshotting 130 registers... CycleSkip: PC at cycle 101 (reference) = 0x8000100 CycleSkip: Resetting simulation... CycleSkip: Restoring registers... CycleSkip: PC after restore+eval+tick+eval (actual) = 0x8000100 *** PASS: Register snapshot/restore roundtrip works *** ``` **Key insight**: After restoring registers, must call `eval` before `tick` to recompute `_next` values (reset doesn't clear `_next` state). ### Updated Benchmark (10M cycles, firmware.hex, Apple Silicon) | Backend | Speed | vs Verilator | |---------|-------|-------------| | **JIT (pure eval+tick)** | **13.0M cyc/s** | **1.17x faster** | | **JIT (eval+tick+6 wires)** | **12.4M cyc/s** | **1.12x faster** | | Verilator 5.044 | 11.1M cyc/s | 1.00x | | **JIT with oracle (cycle-skip)** | **~1.4B effective cyc/s** | ~126x | **Profile (macOS `sample` profiler, JIT bench):** | Component | Samples | % | |-----------|---------|---| | **eval()** (combinational) | 1059 | **72.2%** | | **tick()** (register update) | 343 | **23.4%** | | main loop overhead | 32 | 2.2% | | jit_get_wire | 29 | 2.0% | **Bottleneck: tick() at 23.4%** — 130 register `_next` → current copies (260 memory ops/cycle). The planned **eval()+tick() fusion** would eliminate these by computing values directly in topological order (est. ~1.3x → 17M cyc/s). **Wire read overhead**: 4-9% for 6 output wires (negligible). Oracle/cycle-skipping is effectively free when triggered. **Why JIT is faster than Verilator:** 1. No mutex overhead — Verilator 5.x wastes time on `VlDeleter::deleteAll`, `VerilatedMutex` 2. Observable wire optimization — 33 class members + 321 locals (L1-friendly) 3. Single eval per cycle (Verilator does 2 evals for clk=0/clk=1) ### Note on Sub-Module Registers Sub-module registers (e.g., divider's 8 internal registers) are already flattened into the single JIT class. All 130 registers (8 divider + 122 SoCState) are accessible via `JIT.setReg/getReg`. No hierarchical register work was needed for the Phase 28 oracle. ### Verification - `lake build` — compiles - `lake exe rv32-jit-cycle-skip-test` — PASS (snapshot/restore roundtrip) - Generated `verilator/generated_soc_jit.cpp` contains all 4 register functions with 130 register entries --- ## Verified Standard IP Library — SyncFIFO (Phase 26) — DONE First component of the **Verified Standard IP Library**: a Synchronous FIFO with Valid/Ready (Decoupled) interface. Establishes the pattern for future verified IP (arbiter, crossbar, cache, AXI4, etc.). ### What Was Built | Component | File | Description | |-----------|------|-------------| | **Pure formal model** | `Sparkle/Library/Queue/QueueProps.lean` | 7 theorems (no `sorry`) — no overflow, no underflow, full/empty guards, idle/simultaneous preserves, inductive invariant | | **Synthesizable hardware** | `Sparkle/Library/Queue/SyncFIFO.lean` | Depth-4 FIFO using `declare_signal_state`, `Signal.loop`, `Signal.memoryComboRead`, `hw_cond` | | **LSpec tests** | `Tests/Library/TestSyncFIFO.lean` | 16 tests — fill, drain, FIFO ordering, full/empty conditions, simultaneous enq+deq | | **Test integration** | `Tests/AllTests.lean` | Import + invocation wired up | ### Architecture - **Parameters**: depth=4 (addrWidth=2), dataWidth=32 - **State**: `SyncFIFOState` = `BitVec 2 × BitVec 2 × BitVec 3` (wrPtr, rdPtr, count) - **Data buffer**: `Signal.memoryComboRead` (same-cycle read for dequeue data) - **Output**: `BitVec 32 × BitVec 32 × BitVec 32` (enqReady, deqValid, deqData) ### Design Pattern (reusable for future IP) 1. **Extract loop body** into standalone `def` — enables sharing between synthesis and simulation 2. **Synthesis path**: `Signal.loop body` — generates valid SystemVerilog via `#synthesizeVerilog` 3. **Simulation path**: `Signal.loopMemo body` — avoids stack overflow for functional tests 4. **Pure model** (`QueueProps.lean`) — proven properties in a separate file, no hardware dependencies ### Formal Properties (all proven) | Theorem | Statement | |---------|-----------| | `no_overflow` | `count ≤ depth → nextCount ≤ depth` | | `no_underflow` | `0 ≤ nextCount` (trivial for Nat) | | `full_blocks_enqueue` | `¬ canEnqueue depth depth` | | `empty_blocks_dequeue` | `¬ canDequeue 0` | | `idle_preserves` | `nextCount c d false false = c` | | `simultaneous_preserves` | `canEnqueue ∧ canDequeue → nextCount c d true true = c` | | `count_bounded_inductive` | Same as `no_overflow` (inductive invariant) | ### Generated SystemVerilog (excerpt) ```systemverilog module Sparkle_Library_Queue_SyncFIFO_syncFIFO ( input logic _gen_enqValid, input logic [31:0] _gen_enqData, input logic _gen_deqReady, input logic clk, input logic rst, output logic [95:0] out ); // 3 registers: wrPtr (2-bit), rdPtr (2-bit), count (3-bit) // 4-entry memory: _gen_deqData [0:3] // Priority mux for count update (enq-only / deq-only / simultaneous) // Combo read for dequeue data endmodule ``` ### Test Results (all 16 pass) ``` SyncFIFO: Initial State: ✓ enqReady=1 at t=0 (empty FIFO) ✓ deqValid=0 at t=0 (empty FIFO) Enqueue Phase: ✓ enqReady=1 at t=1..3 Full Condition: ✓ enqReady=0 at t=4 (full) ✓ deqValid=1 at t=4 Dequeue Phase — FIFO Order: ✓ deqData=0xA0..0xA3 at t=4..7 Empty After Drain: ✓ deqValid=0 at t=8 (empty) ✓ enqReady=1 at t=8 Simultaneous Enq+Deq: ✓ enqReady=1, deqValid=1 at t=3 ``` ### Verification - `lake build` — all 103 modules compile - `lake test` — all 16 SyncFIFO tests pass (only pre-existing CppSim bit-accuracy failure) - `#synthesizeVerilog syncFIFO` — valid SystemVerilog with memory, registers, combinational logic - QueueProps theorems — all 7 compile without `sorry` --- ## Next Phases (TODO) ### JIT Cycle-Skipping — Completed | Task | Status | Description | |------|--------|-------------| | **Cycle-skip oracle** | DONE (Phase 28) | Self-loop detection: tolerance-based PC tracking, CLINT timer advancement | | **Sub-module registers** | DONE (Phase 27) | All 130 registers (8 divider + 122 SoCState) flattened and accessible | | **Bulk memory API** | DONE (Phase 29) | `JIT.memsetWord` for fast memory fills with bounds checking | | **Dynamic oracle API** | DONE (Phase 29) | Oracle receives `JITHandle` directly for dynamic register/memory access | | **BSS-clear speculative warp** | DONE (Phase 29 Step 4) | Inline firmware + 4-bank DMEM pre-fill/verify + loop-detection oracle + bulk zero — 389 triggers, 99K cycles skipped in <1 ms | | **Snapshot/restore API** | DONE (Phase 29 Step 5) | `JIT.snapshot/restore/freeSnapshot` — full-state copy via C++ default copy constructor | | **Speculative warp with rollback** | DONE (Phase 29 Step 5) | Guard-and-rollback pattern: snapshot → speculate → guard check → rollback if guard fails. 3-part test: roundtrip, guard-pass (389 triggers), guard-fail (9,955 rollbacks) | | **Linux boot cycle-skip** | TODO | Oracle for OpenSBI/Linux idle loops (WFI, busy-wait, larger tolerance) | ### Performance Optimization | Phase | Status | Est. Speedup | Description | |-------|--------|-------------|-------------| | **eval()+tick() fusion** | TODO | ~1.3x → 17M cyc/s | Eliminate 260 `_next` memory ops/cycle (130 stores in eval + 130 load/store in tick). Requires topological ordering for circular register dependencies | | **Multi-cycle batching** | TODO | ~1.1-1.2x | Unroll 2-4 sim-cycles, keep registers in CPU registers across cycles | | **Cycle-skipping** | DONE | unbounded | Self-loop oracle: 10M cycles in 9ms (706x). Dynamic oracle API with direct JITHandle access (Phase 29). BSS-clear speculative warp + snapshot/restore with guard-and-rollback pattern | ### Verified Standard IP Library — Remaining Components | Component | Status | Description | |-----------|--------|-------------| | **SyncFIFO** | DONE | Depth-4 FIFO with Valid/Ready interface | | **Parameterized FIFO** | TODO | Generic depth/width FIFO (power-of-2 depth) | | **Credit-based flow control** | TODO | Backpressure via credits instead of ready/valid | | **Arbiter (N-way)** | TODO | Generalize 2-client arbiter to N clients | | **Crossbar** | TODO | N×M crossbar switch with arbitration | | **AXI4-Lite** | TODO | AXI4-Lite master/slave interfaces | | **Cache** | TODO | Direct-mapped / set-associative cache with write-back | | **TileLink** | TODO | TileLink Uncached Lightweight (TL-UL) | ### H.264 Video Codec — Next Steps | Task | Status | Description | |------|--------|-------------| | **Pure Lean pipeline** | DONE (Phase 31) | Encoder + Decoder with formal proofs, 8 LSpec test modules | | **Quant/Dequant JIT** | DONE (Phase 31) | Synthesizable FSM, 4 JIT tests pass | | **DCT/IDCT FSM** | TODO | Make `forwardDCTModule` and `inverseDCTModule` fully synthesizable (currently skeletal) | | **CAVLC Encoder FSM** | TODO | Replace 7-arg lambda in ENCODE state with synthesis-compatible pattern | | **CAVLC Decoder FSM** | TODO | Synthesizable bitstream parser with DRAM interface | | **Full Encoder Top** | TODO | Integrate all synthesizable sub-modules (IntraPred → DCT → Quant → CAVLC → NAL) | | **Full Decoder Top** | TODO | Integrate all synthesizable sub-modules (NAL → CAVLC → Dequant → IDCT → IntraPred) | | **Frame-level JIT test** | TODO | End-to-end: encode test frame → decode → compare PSNR | ### Hardware Targets | Phase | Status | Description | |-------|--------|-------------| | **FPGA Tape-out** | TODO | End-to-end Sparkle → FPGA flow | | **GPGPU / Vector Core** | TODO | VDD framework applied to concurrent architectures | --- ## CppSim Phase 3 — Store Reduction via Observable Wire Threading (Phase 25) — DONE Threaded `observableWires` through the IR optimizer, CppSim backend, and `#writeDesign` command. This unblocks `_gen_` wire inlining (previously deferred in Phase 24) by passing the 6 JIT-observable wire names explicitly from the application layer (`SoCOutput.wireNames`), rather than using the blanket `_gen_` prefix guard. ### Results | Metric | Before (Phase 24) | After (Phase 25) | Change | |--------|-------------------|-------------------|--------| | **JIT class members** | 1,242 | **295** | **-76%** | | **JIT `_gen_` members** | 980 | **33** | **-97%** (947 demoted to locals) | | **JIT `eval()` locals** | 0 | **321** | Stack-allocated, register-friendly | | **`jit_num_wires`** | ~976 | **6** | Only observable wires exposed | | **JIT pure eval+tick** | ~6.3M cyc/s | **12.6M cyc/s** | **2.0x speedup** | ### How It Works 1. **Optimizer** (`Optimize.lean`): `inlineSingleUseWires` now accepts `observableWires : Option (List String)`. When `some ws`, only wires in `ws` are protected from inlining (instead of ALL `_gen_` wires). When `none`, backwards-compatible `_gen_` guard. 2. **CppSim backend** (`CppSim.lean`): Wire partitioning uses `observableWires` to decide member vs local. Wires referenced in `tick()` bodies (memory writes, non-combo reads) are always kept as members via `collectTickRefWires`. 3. **`#writeDesign` command** (`Elab.lean`): Split into 3-arg (backwards-compatible) and 4-arg variant. The 4th arg is a constant name resolving to `Array String`. Header file (`_cppsim.h`) keeps all `_gen_` as members (no `observableWires`), JIT file (`_jit.cpp`) uses `observableWires` for aggressive optimization. 4. **SoCVerilog.lean**: `#writeDesign rv32iSoCSynth "..." "..." SoCOutput.wireNames` — passes the 6 observable wire names. ### Key Fix: Tick-Referenced Wires Initial implementation failed because memory `tick()` bodies reference wires via expressions (e.g., `if (_gen_wb_en) mem[_gen_exwb_rd] = _gen_wb_result`). These wires were demoted to `eval()` locals, making them inaccessible from `tick()`. Fixed by adding `collectTickRefWires` which scans memory statements for expression references and keeps those wires as class members. ### Files Modified | File | Changes | |------|---------| | `Sparkle/IR/Optimize.lean` | Added `observableWires` param to `inlineSingleUseWires`, `optimizeModule`, `optimizeDesign` | | `Sparkle/Backend/CppSim.lean` | Added `collectExprRefs`, `collectTickRefWires`; added `observableWires` param to 6 functions; tick-ref wires stay as members | | `Sparkle/Compiler/Elab.lean` | Split `#writeDesign` into 3-arg + 4-arg syntax; added `evalStringArray` helper; `writeDesignCore` shared implementation | | `Examples/RV32/SoCVerilog.lean` | Added `SoCOutput.wireNames` as 4th argument | ### Verification - `lake build` — compiles - `lake test` — all pass (only pre-existing YOLOv8 float test fails) - `lake exe rv32-jit-test` — 47 UART words, 0xCAFE0000, ALL TESTS PASSED - `lake exe rv32-jit-loop-test` — both APIs pass - `./cppsim_soc ../firmware/firmware.hex 5000` — ALL TESTS PASSED (uses header, not JIT) ### Updated Bottleneck Analysis | Component | Status | Impact | |-----------|--------|--------| | Store count (`_gen_` members) | **DONE** — 980 → 33 JIT members | **2.0x speedup** | | Mask operations | DONE (Phase 24) — 312 eliminated | Marginal | | tick() overhead | Deferred — eval()+tick() merge | ~4.2x vs Verilator | --- ## CppSim Phase 2 — Mask Elimination (Phase 24) — DONE Extended the `exprIsMasked` analysis in the CppSim backend to eliminate redundant `& mask` operations at runtime. ### Results | Metric | Before (Phase 23) | After (Phase 24) | Change | |--------|-------------------|-------------------|--------| | **Mask operations** | 449 | **137** | **-69.5% (312 eliminated)** | | **CppSim speed** | 6.3M cyc/s | ~6.4M cyc/s | Marginal (masks are cheap on ARM64) | ### New `exprIsMasked` Cases | Pattern | Rule | Reasoning | |---------|------|-----------| | `.ref _` | always masked | Invariant: every wire is masked at its assignment site | | `.op .and [a, b]` | `a \|\| b` | AND with a masked operand constrains the result (NOT unconditional — `AND(~x, ~y)` preserves garbage) | | `.op .or [a, b]` | `a && b` | OR of two masked values stays within width | | `.op .xor [a, b]` | `a && b` | XOR of two masked values stays within width | | `.op .shr _` | always masked | Right-shift moves bits toward LSB, no new upper bits | | `.op .asr _` | always masked | Masked input is positive in signed cast, no sign extension above `w` | Also applied `exprIsMasked` to register inputs (previously always masked unconditionally). ### `_gen_` Wire Inlining — Deferred The plan's Step 1 (inline single-use `_gen_` wires with output-feeding protection) was investigated and found unsafe: - `_gen_` wires are accessed by name at runtime via `jit_get_wire()` and `loopMemoJIT` - Naming collisions (e.g., `_gen_ptwPteReg` vs `_gen_ptwPteReg_1`) mean the output-feeding set cannot reliably identify all JIT-observable wires - The `_gen_` prefix serves as a JIT observability contract — breaking it causes runtime lookup failures **Resolved in Phase 25**: Observable wire threading passes `SoCOutput.wireNames` explicitly, avoiding the naming collision problem entirely. ### Key Bug Fix: AND Mask Rule The original plan specified `.op .and _ => true` (AND always masked). This is **incorrect**: - `AND(~a, ~b)` where both operands are bitwise-NOT of 1-bit values: `AND(0xFE, 0xFE) = 0xFE` — not 1-bit - Fix: `AND(a, b)` is masked if **either** `a` or `b` is masked (the masked one constrains the result) ### Files Modified | File | Changes | |------|---------| | `Sparkle/Backend/CppSim.lean` | Extended `exprIsMasked` (6 new cases), applied to register inputs | ### Remaining 1.3x Gap — Updated Bottleneck Analysis (resolved in Phase 25) | Component | Status | Impact | |-----------|--------|--------| | Store count (`_gen_` members) | **DONE** (Phase 25) — 980 → 33 JIT members | **2.0x speedup**, JIT now faster than Verilator | | Mask operations | **DONE** — 312 eliminated (69.5%) | Marginal perf impact | | tick() overhead | Deferred — eval()+tick() merge | ~4.2x vs Verilator | --- ## CppSim Backend Optimization (Phase 23) — DONE Implemented IR-level optimizations and CppSim backend improvements to close the 2.7x performance gap with Verilator identified in Phase 22. ### Results | Metric | Before (Phase 22) | After (Phase 23) | Change | |--------|-------------------|-------------------|--------| | **CppSim speed** | 3.6M cyc/s | **6.3M cyc/s** | **+75%** | | **vs Verilator** | 2.7x slower | **1.3x slower** | Gap closed by 2.1x | | Assignments in eval() | 2,242 | 1,375 | -39% | | Member variables | 3,009 | 1,254 | -58% | | Generated C++ lines | ~6,000 | 2,669 | -56% | ### Benchmark (10M cycles, boot.hex stub, Apple Silicon) | Backend | Speed | vs Verilator | |---------|-------|-------------| | **Verilator** | **8.13M cyc/s** | 1.0x | | **CppSim (AOT, -O3)** | 6.31M cyc/s | 1.29x | | **JIT (dlopen, -O2)** | 6.27M cyc/s | 1.30x | ### Optimizations Applied 1. **Single-use wire inlining** (IR level, `Optimize.lean`): Replaces references to wires used exactly once with their defining expression, removing ~900 intermediate assignments. Preserves `_gen_` wires (JIT-observable), register outputs, memory read-data, and module outputs. 2. **Constant folding** (IR level, `Optimize.lean`): Simplifies `mux(true,t,e)→t`, `mux(false,t,e)→e`, `eq(const,const)`, `add(0,x)→x`, `or(0,x)→x`, `and(0,x)→0`, `slice(const)→const`. 3. **Local variable promotion** (CppSim, `CppSim.lean`): Partitions internal wires — `_gen_` prefix wires remain class members (JIT-observable via `jit_get_wire`), all `_tmp_` wires become local variables in `eval()`, enabling CPU register allocation. 4. **Redundant mask elimination** (CppSim, `CppSim.lean`): Skips `& mask` for constants, comparison results (already 0/1), exact-width slices, and mux of already-masked operands. ### Files Modified | File | Description | |------|-------------| | `Sparkle/IR/Optimize.lean` | Added `foldConstants`, `inlineSingleUseWires`, `substituteExpr`; integrated as Phase 3-4 in `optimizeModule` | | `Sparkle/Backend/CppSim.lean` | Local variable promotion in `emitModule`, `exprIsMasked` for redundant mask elimination | ### Remaining 1.3x Gap — Assembly-Level Analysis Profiled JIT vs Verilator at the ARM64 instruction level: | Component | Verilator | JIT | Ratio | Root Cause | |-----------|-----------|-----|-------|------------| | Stores (str/strb) | 283 | 1,115 | 3.9x | JIT writes every `_gen_` wire to memory; Verilator uses locals | | Loads (ldr/ldrb) | 256 | 691 | 2.7x | Corresponding read traffic | | Masks (and) | 80 | 292 | 3.7x | JIT applies runtime masks; Verilator uses sized types | | Conditionals (cmp/csel) | 181 | 484 | 2.7x | JIT evaluates all muxes; Verilator may skip unchanged | | tick() instructions | 89 | 373 | 4.2x | JIT copies all registers unconditionally | **Next optimization targets:** - Promote `_gen_` wires that aren't read by `jit_get_wire` to locals (biggest impact) - Extend mask elimination to more expression patterns - Merge eval()+tick() into single pass (eliminate register copy overhead) --- ## Simulation Performance Analysis (Phase 22) — DONE Benchmarked all simulation backends at 10M cycles (Linux boot) and identified the root cause of the CppSim/JIT vs Verilator performance gap. ### Benchmark Results (10M cycles, Linux boot, Apple Silicon) | Backend | Time | Speed | Instructions | CPU Cycles | IPC | |---------|------|-------|-------------|-----------|-----| | **Verilator** | 1.03s | **9.7M cyc/s** | 27.65B | 4.13B | 6.69 | | **CppSim (AOT)** | 2.78s | 3.6M cyc/s | 54.94B | 11.38B | 4.83 | | **JIT (dlopen)** | 2.77s | 3.6M cyc/s | — | — | — | | **Lean loopMemo** | ~2000s | ~5K cyc/s | — | — | — | ### Root Cause: CppSim generates 2x more instructions per cycle | Factor | Verilator | CppSim | Impact | |--------|-----------|--------|--------| | **Assignments/cycle** | 831 | 2,242 | **2.7x more work** | | **Member variables** | 375 | 3,009 | **8x more memory traffic** | | **Local variables** | 145 (stack) | 0 | Verilator benefits from register allocation | | **Expression inlining** | 20+ level nested | 1 per assignment | Verilator eliminates intermediate stores | | **Constant handling** | Inline literals | `_tmp = 0` in member | CppSim wastes stores on constants | | **Memory arrays** | 6 (35 MB) | 12 (68 MB) | CppSim duplicates regfile + DRAM | | **If-else usage** | 35 (reset/WE) | 0 | Verilator skips code via branch prediction | | **Bit masking** | Sized types (CData) | Runtime `& ((1< do ...) ``` ### Design Decisions - Uses **named output wires** (`_gen_pcReg`, `_gen_uartValidBV`, etc.) instead of internal state wires — immune to DCE and name collisions - `SoCOutput` struct (6 fields) instead of full `SoCState` (122 fields) — only output-port-observable values - `JIT.resolveWires` maps wire names → indices at init time - Wire read overhead: ~0.5% (negligible) ### Files Created/Modified | File | Action | Description | |------|--------|-------------| | `Sparkle/Core/JITLoop.lean` | **Created** | `loopMemoJIT` + `JIT.run` + `JIT.resolveWires` | | `Sparkle/Core/StateMacro.lean` | Modified | Added `wireNames` + `fromWires` generation | | `Examples/RV32/SoC.lean` | Modified | `SoCOutput`, `rv32iSoCJITSimulate`, `rv32iSoCJITRun` | | `Tests/RV32/JITLoopTest.lean` | **Created** | Two-part test (Signal API + Streaming API) | | `Sparkle.lean` | Modified | Added `import Sparkle.Core.JITLoop` | | `lakefile.lean` | Modified | Added `rv32-jit-loop-test` executable | ### Test Results Both Part 1 (Signal API via `loopMemoJIT`) and Part 2 (Streaming API via `rv32iSoCJITRun`) pass — 47 UART words, `0xCAFE0000` marker at cycle 2904. --- ## Linux Boot Verified on Generated SoC (Phase 20) — DONE Verified that the holdEX/divStall fix (Phase 13) resolves the Linux boot hang. The generated SoC now boots Linux 6.6.0 via OpenSBI v0.9, matching the hand-written SV reference behavior. ### Test Results | Metric | Previous (broken holdEX) | Generated SV (fixed) | Hand-written SV (reference) | |--------|-------------------------|----------------------|----------------------------| | UART bytes | 1906 | **5250** | 3944 | | Hang point | ~3.3M cycles (recursive page fault) | Still running at 10M | Still running at 10M | | Final PC region | 0xC0001C88 (recursive page fault) | 0xC013A9xx (kernel init) | 0xC013A9xx (kernel init) | | Page faults | Recursive (infinite loop) | 3 total (normal) | Normal | ### Key Findings 1. **holdEX/divStall fix works**: Kernel no longer hangs at recursive page fault (0xC0001C88). Page tables populated correctly, kernel proceeds through memory initialization. 2. **Generated SV matches hand-written SV behavior**: Both reach the same kernel PC region (0xC013A9xx–0xC013B5xx) — the calibrating delay loop / kernel init busy-wait. Neither is hung; both show different PCs at each 100K-cycle sample. 3. **Generated SV produces more UART output (5250 vs 3944)**: Multi-cycle divider (34 cycles) changes timing vs combinational divider in hand-written SV, causing SBI console to output at different intervals. 4. **OpenSBI boots normally**: Full platform info printed (Sparkle RV32IMA SoC, rv32imasu ISA). 5. **Kernel boot progresses**: "Linux version 6.6.0" printed, memory regions detected correctly (`base=0x80400000, size=0x01c00000`), kernel well into init. ### Build Fix `tb_soc.cpp` referenced `_gen_dTLBMiss` which Verilator optimizes away. Replaced two references with `0` (debug tracing only). ### Verification ```bash $ lake build Examples.RV32.SoCVerilog # Regenerate SV from latest Lean source $ cd verilator && make build # Build Verilator simulation $ ./obj_dir/Vrv32i_soc ../firmware/opensbi/boot.hex 10000000 \ --dram /tmp/opensbi/build/platform/generic/firmware/fw_jump.bin \ --dtb ../firmware/opensbi/sparkle-soc.dtb \ --payload /tmp/linux/arch/riscv/boot/Image # === OpenSBI simulation ended (5250 UART bytes) === # Both generated and hand-written SV reach same kernel init region at 10M cycles ``` --- ## DRC/Linter Pass — Registered Output Check (Phase 19) — DONE Added an automated Design Rule Check (DRC) pass that warns when output ports are driven by combinational logic rather than registers. Similar to commercial linters like SpyGlass, this catches backend-unfriendly RTL patterns (synthesis + STA issues) at compile time. ### Implementation Pure function `checkRegisteredOutputs` analyzes `Module` IR before Verilog emission: 1. For each output port (skipping `clk`/`rst`), traces the driving statement 2. If driven by `Stmt.register` or synchronous `Stmt.memory` → pass 3. Otherwise → emit Lean warning (combinational path to output) Integrated into all 4 synthesis commands: `#synthesizeVerilog`, `#synthesizeVerilogDesign`, `#writeVerilogDesign`, `#writeDesign`. ### Example Output ``` Tests/TestDRC.lean:17:0: warning: [DRC] Module 'drc_combo_output': output 'out' is not driven by a register (driven by wire '_tmp_result_0') ``` Registered outputs produce no warnings. ### Files | File | Action | Description | |------|--------|-------------| | `Sparkle/Compiler/DRC.lean` | Created | `findDriver`, `checkRegisteredOutputs` — pure functions on `Module` IR | | `Sparkle/Compiler/Elab.lean` | Modified | Import DRC, add `runDesignDRC`, call in 4 synthesis commands | | `Sparkle.lean` | Modified | Added `import Sparkle.Compiler.DRC` | | `Tests/TestDRC.lean` | Created | Combinational (warns) + registered (clean) test circuits | ### Verification ```bash $ lake build # Full build — ✓ $ lake env lean Tests/TestDRC.lean # Combo warns, registered clean — ✓ ``` --- ## Verification-Driven Design Framework + Round-Robin Arbiter (Phase 18) — DONE Created a formal Verification-Driven Design (VDD) framework document and a worked example demonstrating the full workflow: pure state-machine spec with 10 formal proofs, then synthesizable Signal DSL implementation. ### Framework Document (`docs/Verification_Framework.md`) Professional ~200-line guide covering: 1. **Bug Classification**: Safety vs Liveness with hardware examples (bus contention, starvation, deadlock) 2. **Four Proof Patterns**: Invariant, Round-trip, Responsiveness, Refinement — each with tactic recipes 3. **Worked example**: 2-client Round-Robin Arbiter (referencing the implementation files) 4. **Tactic quick-reference** for hardware engineers ### Formal Proofs (`Sparkle/Verification/ArbiterProps.lean`) Self-contained file (follows ISAProps.lean pattern) with: - **State machine**: `ArbiterState` (Idle/GrantA/GrantB), 12-entry `nextState` truth table - **Output functions**: `grantA`, `grantB` - **10 theorems**, all closing via `cases s <;> cases reqA <;> cases reqB <;> simp [...]` — zero `sorry` | # | Theorem | Category | Statement | |---|---------|----------|-----------| | 1 | `mutual_exclusion` | Safety | Never both granted after transition | | 2 | `mutual_exclusion_current` | Safety | Never both granted in any state | | 3 | `no_spurious_grant` | Safety | No grant without request | | 4 | `progress_A` | Liveness | A requesting → A granted or B holds | | 5 | `progress_B` | Liveness | B requesting → B granted or A holds | | 6 | `starvation_free_A` | Liveness | A granted within 2 cycles | | 7 | `starvation_free_B` | Liveness | B granted within 2 cycles | | 8 | `round_robin_A_to_B` | Fairness | GrantA + contention → GrantB | | 9 | `round_robin_B_to_A` | Fairness | GrantB + contention → GrantA | | 10 | `idle_tiebreak` | Fairness | Idle + contention → GrantA | ### Signal DSL Implementation (`Examples/Arbiter/RoundRobin.lean`) Synthesizable arbiter mirroring the proven spec: - State encoded as `BitVec 2` (0=Idle, 1=GrantA, 2=GrantB) - `Signal.loop` + `Signal.register` for state feedback - `===` for state comparison, `&&&`/`|||` for Bool combinators - `hw_cond` for priority mux next-state logic - `#synthesizeVerilog arbiterSignal` — generates clean SystemVerilog - `#eval simTest` — simulation confirms round-robin alternation ### Generated Verilog (excerpt) ```systemverilog module Sparkle_Examples_Arbiter_RoundRobin_arbiterSignal ( input logic _gen_reqA, input logic _gen_reqB, input logic clk, input logic rst, output logic [1:0] out ); // ... combinational next-state logic (priority mux) ... assign _gen_nextState = (_gen_goGrantA ? 2'd1 : (_gen_goGrantB ? 2'd2 : 2'd0)); always_ff @(posedge clk or posedge rst) begin if (rst) _tmp_loop_body_12 <= 2'd0; else _tmp_loop_body_12 <= _gen_nextState; end assign out = _tmp_loop_body_12; endmodule ``` ### Simulation Output ``` Cycle | reqA reqB | state | grantA grantB ------+----------+-------+-------------- 0 | false false | Idle | false false 1 | true false | GrantA | true false 2 | true true | GrantB | false true ← round-robin 3 | true true | GrantA | true false ← alternates 4 | true true | GrantB | false true 7 | true true | GrantA | true false ← tie-break from Idle ``` ### Files | File | Action | Description | |------|--------|-------------| | `Sparkle/Verification/ArbiterProps.lean` | Created | Pure state machine + 10 formal proofs | | `Examples/Arbiter/RoundRobin.lean` | Created | Signal DSL implementation + synthesis + sim test | | `docs/Verification_Framework.md` | Created | VDD framework document | | `lakefile.lean` | Modified | Added `lean_lib «Examples.Arbiter»` | ### Verification ```bash $ lake build Sparkle.Verification.ArbiterProps # 10 proofs, zero sorry ✓ $ lake build Examples.Arbiter.RoundRobin # Synthesis + simulation ✓ $ lake test # No regressions ✓ ``` --- ## Compiler Tracing & Handler Extraction (Phase 17) — DONE Refactored `Sparkle/Compiler/Elab.lean` — added Lean tracing infrastructure and broke the monolithic `translateExprToWireApp` function (~393 lines, ~20 sequential `if name == ...` arms) into 9 categorized handler functions with a clean ~25-line dispatcher. No synthesis behavior changes. ### Tracing Infrastructure Added `initialize registerTraceClass `sparkle.compiler`` — enables structured trace output for debugging the synthesis compiler. **Usage:** ```lean set_option trace.sparkle.compiler true #synthesizeVerilog myDesign ``` Trace points at: - `translateExprToWire` entry (hint, isTopLevel) - `translateExprToWireApp` entry (name, args.size) - Each handler match (e.g., `→ tuple projection (fst)`, `→ register`, `→ loop`, `→ definition unfold {name}`) ### Handler Extraction | # | Handler | Handles | ~Lines | |---|---------|---------|--------| | 1 | `handleErrorPatterns` | `ite`/`dite`, `Decidable.rec`/`casesOn` — throws on match | 20 | | 2 | `handleTupleProjections` | `Signal.fst`, `Signal.snd`, `Signal.map`+`Prod.fst`/`Prod.snd` | 50 | | 3 | `handleApplicative` | `Signal.ap` — binary op lifting, concat/sshiftRight special cases | 45 | | 4 | `handleBitVecOps` | `extractLsb'`, shifts, concat, `isPrimitive` dispatch | 55 | | 5 | `handleRegister` | `Signal.register`, `Signal.registerWithEnable` | 30 | | 6 | `handleMux` | `Signal.mux`, `lutMuxTree` | 40 | | 7 | `handleMemory` | `Signal.memory`, `Signal.memoryComboRead` | 35 | | 8 | `handleLoop` | `Signal.loop`, `HWVector.get` | 35 | | 9 | `handleDefinitionUnfold` | `unfoldDefinition?` → inline, fallback → sub-module synthesis | 50 | Each handler is a `partial def` returning `CompilerM (Option String)`: - `some wireName` → handled - `none` → not my pattern, try next handler - Throw → error detected The main dispatcher chains handlers in order: ```lean handleErrorPatterns e name args hint isNamed -- throws or returns () if let some w ← handleTupleProjections e name args hint isNamed then return w if let some w ← handleApplicative e name args hint isNamed then return w -- ... 6 more handlers ... if let some w ← handleDefinitionUnfold e name args hint isNamed then return w ``` ### Files Changed | File | Action | Description | |------|--------|-------------| | `Sparkle/Compiler/Elab.lean` | Modified | Added `registerTraceClass`, extracted 9 handlers, added trace points | ### Verification ```bash $ lake build # Full build — ✓ $ lake test # Test suite — ✓ (all pass) $ lake build Examples.YOLOv8.Blocks.Bottleneck # Bottleneck synthesis — ✓ $ lake build Examples.RV32.SoCVerilog # SoC synthesis + CppSim + JIT — ✓ ``` All produce identical output to pre-refactoring. --- ## `declare_signal_state` Macro (Phase 16) — DONE Replaced error-prone manual `projN!` state indexing with a `declare_signal_state` command macro that generates synthesis-compatible accessor defs. Eliminates magic-number indices for the 122-register SoC and all smaller state tuples. ### Problem Hardware state in the Signal DSL uses right-nested tuples (`BitVec 32 × Bool × BitVec 8 × ...`). Accessing fields requires `projN! state N i` with numeric indices: ```lean -- 122 lines of this — one typo silently breaks the design let pcReg := projN! state 122 0 let fetchPC := projN! state 122 1 ... let dMissIsStore := projN! state 122 121 ``` Adding/removing a field requires updating ALL subsequent indices manually. A previous attempt using Lean 4 structures failed because struct constructors (`.mk`) are not `.defnInfo` — the synthesis compiler's `unfoldDefinition?` can't inline them. ### Solution The `declare_signal_state` command macro generates synthesis-compatible `def`s (which ARE `.defnInfo`): ```lean declare_signal_state BottleneckState | fsmReg : BitVec 2 := 0#2 | residualReg : BitVec 8 := 0#8 | resultReg : BitVec 8 := 0#8 | doneReg : Bool := false ``` Generates: 1. **Type alias**: `abbrev BottleneckState := BitVec 2 × BitVec 8 × BitVec 8 × Bool` 2. **Accessor defs**: `BottleneckState.fsmReg`, `.residualReg`, etc. (each expands to `projN!`) 3. **Default value**: `BottleneckState.default : BottleneckState` 4. **Inhabited instance**: `instance : Inhabited BottleneckState` Each accessor is a regular `def` → `.defnInfo` → `unfoldDefinition?` inlines it → synthesis works. ### Usage ```lean -- Before: magic numbers everywhere let pcReg := projN! state 122 0 let fetchPC := projN! state 122 1 -- After: named accessors, no indices let pcReg := SoCState.pcReg state let fetchPC := SoCState.fetchPC state ``` ### Files | File | Action | Description | |------|--------|-------------| | `Sparkle/Core/StateMacro.lean` | Created | `declare_signal_state` command macro | | `Sparkle.lean` | Modified | Added `import Sparkle.Core.StateMacro` | | `Examples/YOLOv8/Blocks/Bottleneck.lean` | Modified | Replaced Record Wrapper with macro (4 fields) | | `Examples/RV32/SoC.lean` | Modified | Replaced 122-field `projN!` block with accessor calls | ### Verification ```bash $ lake build Examples.YOLOv8.Blocks.Bottleneck # Verilog successfully generated! (synthesis works through accessor defs) $ lake build Examples.RV32.SoCVerilog # Written 1 modules to verilator/generated_soc.sv # Written C++ simulation to verilator/generated_soc_cppsim.h # Written JIT wrapper to verilator/generated_soc_jit.cpp $ lake exe rv32-flow-test # CppSim JIT: ✓ ALL TESTS PASSED ``` --- ## Signal DSL Ergonomics (Phase 15) — DONE Improved Signal DSL readability with new operators, implicit coercions, and a hardware conditional macro. Refactored YOLOv8 Backbone to demonstrate the improvements — FSM transition logic reduced from 18 lines of nested `Signal.mux` to 9 lines of flat `hw_cond`. ### New Features (Sparkle/Core/Signal.lean) | Feature | Syntax | Expansion | Status | |---------|--------|-----------|--------| | Hardware equality | `a === b` | `(· == ·) <$> a <*> b` | Synthesizes | | Implicit BitVec lift | `(1#4 : Signal dom _)` | `Signal.pure 1#4` via `Coe` | Synthesizes | | Implicit Bool lift | `(true : Signal dom _)` | `Signal.pure true` via `Coe` | Synthesizes | | Hardware conditional | `hw_cond default \| cond => val` | Nested `Signal.mux` | Synthesizes | | Bool AND | `a &&& b` | `(· && ·) <$> a <*> b` | Synthesizes (Phase 13) | | Bool OR | `a \|\|\| b` | `(· \|\| ·) <$> a <*> b` | Synthesizes | | Bool NOT | `~~~a` | `(fun x => !x) <$> a` | **Does NOT synthesize** | ### `hw_cond` Macro Design Default-first syntax avoids PEG parser greedy-`|` ambiguity: ```lean -- Syntax: hw_cond | => | => | ... let fsmNext := hw_cond fsmReg | startAndIdle => (1#4 : Signal dom _) -- IDLE → STEM | stemDone => (2#4 : Signal dom _) -- STEM → STAGE_CONV | stageConvDone => (3#4 : Signal dom _) -- STAGE_CONV → STAGE_C2F | isDone => (0#4 : Signal dom _) -- DONE → IDLE ``` **Implementation notes:** - Defined after `end Signal` namespace (line 649) so ``Signal.mux`` resolves correctly via double-backtick - Uses `Lean.mkIdent ``Signal.mux` to bypass macro hygiene — prevents `_hyg.N` suffixes that would break the synthesis compiler's `name.endsWith ".mux"` check - PEG issue: `| else =>` fails because greedy `*` repetition consumes `|` before the default branch; default-first syntax avoids this entirely ### Refactoring Demonstration (Backbone.lean) Before (verbose): ```lean let isIdle := (· == ·) <$> fsmReg <*> Signal.pure 0#4 let startAndIdle := (· && ·) <$> start <*> isIdle let fsmNext := Signal.mux startAndIdle (Signal.pure 1#4) (Signal.mux stemDone (Signal.pure 2#4) (Signal.mux stageConvDone (Signal.pure 3#4) (...10+ levels of nesting...))) ``` After (clean): ```lean let isIdle := fsmReg === (0#4) let startAndIdle := start &&& isIdle let fsmNext := hw_cond fsmReg | startAndIdle => (1#4 : Signal dom _) | stemDone => (2#4 : Signal dom _) | stageConvDone => (3#4 : Signal dom _) | ...9 flat lines... ``` ### Known Limitation: `~~~` (Complement) Does Not Synthesize The `Complement` instance for `Signal dom Bool` (`~~~a` → `(fun x => !x) <$> a`) causes the synthesis compiler to see `Complement.mk` as a module instantiation attempt. The `unfoldDefinition?` mechanism doesn't fully reduce the typeclass instance before the compiler's expression walker encounters it. **Workaround:** Use `(fun x => !x) <$> signal` directly (which the compiler recognizes). **Future fix:** Add `Complement.complement` / `Complement.mk` to the synthesis compiler's unfolding list in Elab.lean. ### Files Modified | File | Change | |------|--------| | `Sparkle/Core/Signal.lean` | Added `===`, `Coe` instances, `hw_cond` macro | | `Examples/YOLOv8/Backbone.lean` | Refactored using new ergonomic features | ### Verification ```bash $ lake build Examples.YOLOv8.Backbone # Build completed successfully (23 jobs). # Generated Verilog for all 3 sub-modules: c2fController, sppfController, backboneController $ lake exe rv32-flow-test # CppSim JIT: ✓ ALL TESTS PASSED (no regressions) ``` --- ## JIT FFI Implementation (Phase 14) — DONE Implemented JIT-accelerated simulation via dynamic compilation. The CppSim backend now generates self-contained `.cpp` files with `extern "C"` wrappers, compiled to shared libraries (`.dylib`) at runtime, and loaded from Lean via `dlopen`/`dlsym` FFI. ### Architecture ``` ┌─────────────┐ #writeDesign ┌──────────────────┐ │ Lean DSL │ ──────────────────► │ *_jit.cpp │ │ (Signal) │ │ (CppSim class + │ └─────────────┘ │ extern "C" API)│ └────────┬─────────┘ │ c++ -shared ▼ ┌──────────────────┐ │ *.dylib │ │ (shared library) │ └────────┬─────────┘ │ dlopen/dlsym ▼ ┌──────────────────┐ │ Lean JIT.lean │ │ (eval/tick/reset │ │ setMem/getWire) │ └──────────────────┘ ``` ### Performance - **JIT simulation**: ~1M+ cycles/sec (firmware test: 2904 cycles) - **Interpreted loopMemo**: ~5K cycles/sec - **Speedup**: ~200x over Lean-native simulation ### API (Sparkle.Core.JIT) | Function | Description | |----------|-------------| | `JIT.compileAndLoad cppPath` | Compile `.cpp` → `.dylib` (hash-cached) and load | | `JIT.eval handle` | Evaluate combinational logic | | `JIT.tick handle` | Advance clock (register update) | | `JIT.reset handle` | Reset all registers | | `JIT.setInput handle idx val` | Set input port by index | | `JIT.getOutput handle idx` | Get output port by index | | `JIT.getWire handle idx` | Get named internal wire by index | | `JIT.wireName handle idx` | Get wire name by index (for discovery) | | `JIT.findWire handle name` | Find wire index by name | | `JIT.setMem handle memIdx addr data` | Write to memory array | | `JIT.getMem handle memIdx addr` | Read from memory array | | `JIT.destroy handle` | Destroy instance (also runs on finalize) | ### Generated Wrapper (extern "C") The `toCppSimJIT` function generates a self-contained `.cpp` with: - Full CppSim class inlined (no header dependency) - `jit_create/destroy/eval/tick/reset` — lifecycle management - `jit_set_input/get_output` — port access by index - `jit_get_wire/jit_wire_name` — named wire observation (980 wires for SoC) - `jit_set_mem/get_mem` — direct memory access (11 memories for SoC) - `jit_num_inputs/outputs/wires/memories` — metadata queries ### Files | File | Action | Description | |------|--------|-------------| | `Sparkle/Backend/CppSim.lean` | Modified | Added `toCppSimJIT` + helpers | | `c_src/sparkle_jit.c` | Created | dlopen/dlsym FFI with `lean_external_class` | | `Sparkle/Core/JIT.lean` | Created | `@[extern]` opaque declarations + compile/load helpers | | `Sparkle.lean` | Modified | Added `import Sparkle.Core.JIT` | | `Sparkle/Compiler/Elab.lean` | Modified | `#writeDesign` now auto-emits JIT wrapper | | `lakefile.lean` | Modified | Added `extern_lib sparkle_jit` + `rv32-jit-test` exe | | `Tests/RV32/JITTest.lean` | Created | End-to-end JIT test with firmware | | `verilator/Makefile` | Modified | Added `build-jit` and `run-jit` targets | ### Verification ``` $ lake exe rv32-jit-test verilator/generated_soc_jit.cpp firmware/firmware.hex 5000 JIT: Compiling verilator/generated_soc_jit.cpp... JIT: Loaded shared library JIT: Wire indices — pcReg=8, uartValid=979, uartData=50 JIT: Loading firmware from firmware/firmware.hex... JIT: Running for 5000 cycles... UART[1]: 0xdead0001 ... UART[47]: 0xcafe0000 *** ALL TESTS PASSED (cycle 2904) *** ``` ### Build & Run ```bash # Generate all outputs (SV + CppSim + JIT wrapper) lake build Examples.RV32.SoCVerilog # Run JIT test from Lean lake exe rv32-jit-test verilator/generated_soc_jit.cpp firmware/firmware.hex 5000 # Build JIT shared library manually cd verilator && make build-jit # Run JIT test via Makefile cd verilator && make run-jit ``` --- ## holdEX/divStall Store Bug Fix (Phase 13) — DONE Fixed a critical bug where `holdEX` included `divStall`, causing `suppressEXWB` to kill valid stores during multi-cycle division. This prevented `memblock_add` stores from persisting during Linux boot, leaving `swapper_pg_dir` empty and causing a recursive page fault at PC 0xC0001C88. ### The Bug | | Hand-written SV (working) | Generated SoC (broken) | |---|---|---| | `holdEX` | `pendingWriteEn` | `pendingWriteEn \|\| (divStall && !flushOrDelay)` | | `suppressEXWB` | `trap_taken \|\| dTLBMiss \|\| holdEX` | Same — but `holdEX` is wider | | **Effect on stores** | Stores only suppressed during pending AMO/TLB miss/trap | **Stores also suppressed during 34-cycle division** | ### Fix (4 logical edits in `Examples/RV32/SoC.lean`) 1. **`holdEX` simplified** to `pendingWriteEn` only — controls `suppressEXWB` 2. **New `freezeIDEX`** = `holdEX || (divStall && !flushOrDelay)` — controls pipeline register freezing only 3. **`squash`** uses `freezeIDEX` (not `holdEX`) 4. **All 36 pipeline register muxes** use `freezeIDEX` (not `holdEX`) ### Verification - `lake build Examples.RV32.SoCVerilog` — passes, regenerates SV + CppSim - CppSim firmware test — **ALL TESTS PASSED** (49 UART words, 0xCAFE0000 at cycle 2904) - `lake exe rv32-flow-test` — CppSim category passes --- ## Signal Bool Operator Instances (Phase 13) — DONE Added boolean operator overloading for `Signal dom Bool` in `Sparkle/Core/Signal.lean`. ### New Instances | Syntax | Operator | Expansion | |--------|----------|-----------| | `a &&& b` | AND | `(· && ·) <$> a <*> b` | | `a \|\|\| b` | OR | `(· \|\| ·) <$> a <*> b` | | `a ^^^ b` | XOR | `(xor · ·) <$> a <*> b` | | `~~~a` | NOT | `(fun x => !x) <$> a` | ### Refactoring Demo Flush logic in `SoC.lean` simplified: ```lean -- Before (verbose): let flush := (· || ·) <$> ((· || ·) <$> branchTaken <*> idex_jump) <*> ((· || ·) <$> ((· || ·) <$> trap_taken <*> idex_isMret) <*> ((· || ·) <$> ((· || ·) <$> idex_isSret <*> idex_isSFenceVMA) <*> dMMURedirect)) -- After (clean): let flush := branchTaken ||| idex_jump ||| trap_taken ||| idex_isMret ||| idex_isSret ||| idex_isSFenceVMA ||| dMMURedirect ``` Synthesis compatibility confirmed — compiler already has `Bool.and`/`Bool.or`/`Bool.not` in primitive registry. --- ## JIT FFI Design Document (Phase 13) — DONE Created `docs/JIT_FFI_Plan.md` — design document for native-speed simulation via dynamic compilation. Covers architecture, C++ shared library generation, background compilation with hash-based caching, Lean FFI bindings (`sparkle_jit.c` + `JIT.lean`), and `loopMemoJIT` integration with fallback to interpreted `loopMemo`. --- ## LSpec Flow Tests (Phase 12) — DONE Automated LSpec tests covering the full RV32 SoC build/simulation pipeline. Catches regressions across 4 independent paths, skips gracefully when external tools are unavailable. ### Test Results (18 assertions) | Category | Tests | Status | |----------|-------|--------| | Verilog Compilation | 12 | All pass — verifies `generated_soc.sv` and `generated_soc_cppsim.h` content | | Lean-native Simulation | 1 | Skips on macOS (8MB stack limit) — passes on Linux with sufficient stack | | CppSim JIT | 3 | All pass — compiles with clang++, runs 5000 cycles, `ALL TESTS PASSED` | | Verilator Simulation | 3 | All pass — builds via Make, runs 5000 cycles, `ALL TESTS PASSED` | ### Architecture - **Category 1 (Verilog Compilation)**: Reads generated files, checks for expected module names, port declarations, `always_ff` blocks, CppSim class methods - **Category 2 (Lean-native Simulation)**: Runs `rv32iSoCSimulateFull` via separate subprocess (`LeanSimRunner.lean`) to work around macOS stack limit; checks PC starts at 0, advances, stays in IMEM range - **Category 3 (CppSim JIT)**: Detects `clang++`/`g++` availability, compiles `tb_cppsim.cpp`, runs firmware test, checks output - **Category 4 (Verilator)**: Detects `verilator` availability, builds via `make obj_dir/Vrv32i_soc` (no re-generate), runs firmware test ### Files Added | File | Description | |------|-------------| | `Tests/RV32/TestFlow.lean` | 4 test categories (synthTests, leanSimTests, cppSimTests, verilatorTests) | | `Tests/RV32/TestFlowMain.lean` | Standalone `main` entry point | | `Tests/RV32/LeanSimRunner.lean` | Subprocess for Lean simulation (avoids stack overflow) | ### Files Modified | File | Change | |------|--------| | `Tests/AllTests.lean` | Added `import Tests.RV32.TestFlow`, integrated `flowTests` | | `lakefile.lean` | Added `rv32-flow-test` and `rv32-lean-sim-runner` executable targets | ### Build & Run ```bash # Run all tests including flow tests lake test # Run flow tests standalone lake exe rv32-flow-test # Build simulation runner (needed for lean sim tests) lake build rv32-lean-sim-runner ``` --- ## CppSim Benchmark (Phase 11) — DONE End-to-end C++ simulation backend: generates optimized C++ from IR, compiles, and runs the RV32I SoC firmware test — **~170x faster than Verilator** for the firmware test workload. ### Benchmark Results | Metric | Verilator | CppSim | Speedup | |--------|-----------|--------|---------| | Firmware test (2904 cycles) | ~160ms | **0.9ms** | **~170x** | | Sustained throughput (1M cycles) | N/A | **3.6M cycles/sec** | — | - UART output: **47/47 data words identical** to Verilator - `0xCAFE0000` pass marker at cycle 2904 (same as Verilator) ### Completed | # | Task | Status | |---|------|--------| | 1 | IR optimization pass (`Sparkle/IR/Optimize.lean`) — concat/slice chain elimination | Done | | 2 | CppSim backend >64-bit type handling + wide assign skip | Done | | 3 | Combined `#writeDesign` command (single synthesis → both Verilog + CppSim) | Done | | 4 | C++ testbench (`verilator/tb_cppsim.cpp`) — firmware load, UART, timing | Done | | 5 | Makefile targets (`build-cppsim`, `run-cppsim`, `benchmark`) | Done | | 6 | ALL TESTS PASSED — CppSim matches Verilator output | Done | ### IR Optimization - **Problem**: 5,451 wires wider than 999 bits from tuple packing/unpacking (nested 2-element concats + slice chains) - **Solution**: `Sparkle.IR.Optimize.optimizeDesign` — recursive `resolveSlice` with HashMap lookups - Follows ref aliases: `X = Y` → follow to Y - Composes slice chains: `X = Y[h1:l1], Z = X[h2:l2]` → `Z = Y[l1+h2:l1+l2]` - Resolves concat slices: `X = {a, b}, Z = X[h:l]` → `Z = a` (if aligned) - Fuel=500 to handle 244-level deep chains (124 slice + 120 concat levels) - **Result**: 20,543 → 4,919 lines (76% reduction), 7,928 → 0 wide arrays in expressions ### Architecture - `#writeDesign id "path.sv" "path_cppsim.h"` — synthesizes once, emits both Verilog and CppSim - IR optimization runs only on CppSim path (Verilog output is unoptimized, matches previous behavior) - Wide types (>64-bit): declared as `std::array`, assigns skipped (dead after optimization) - Testbench loads firmware directly into IMEM array (no CPU cycles consumed during loading) ### Build & Run ```bash # Build CppSim cd verilator && make build-cppsim # Run CppSim cd verilator && make run-cppsim CYCLES=5000 # Benchmark CppSim vs Verilator cd verilator && make benchmark CYCLES=5000 ``` ### TODO (Future Phases) - [x] LSpec flow tests for RV32 SoC (Phase 12 — done) - [x] Fix holdEX/divStall store bug (Phase 13 — done) - [x] Signal Bool operator instances (Phase 13 — done) - [x] JIT FFI design document (Phase 13 — done, see `docs/JIT_FFI_Plan.md`) - [x] JIT FFI implementation (Phase 14 — done) - [x] Signal DSL ergonomics: `===`, `Coe`, `hw_cond` macro (Phase 15 — done) - [x] `declare_signal_state` macro — named state accessors, no magic indices (Phase 16 — done) - [x] Compiler tracing & handler extraction — `registerTraceClass`, 9 handler functions (Phase 17 — done) - [x] Verification-Driven Design framework + Round-Robin Arbiter — 10 formal proofs, Signal DSL implementation (Phase 18 — done) - [x] DRC/Linter Pass — registered output check, warns on combinational outputs (Phase 19 — done) - [ ] Fix `~~~` (Complement) synthesis — add unfolding for `Complement.mk` in Elab.lean - [x] `loopMemoJIT` integration — transparent JIT behind Signal.loopMemo (Phase 21 — done) - [x] Simulation performance analysis — root cause of 2.7x Verilator gap identified (Phase 22 — done) - [ ] CppSim expression inlining — eliminate single-use `_tmp_*` wires (biggest optimization opportunity) - [ ] CppSim constant folding — inline literal constants instead of storing as member variables - [ ] Fix Lean simulation stack overflow on macOS (reduce tuple nesting depth or use worker thread with larger stack) - [x] Fix Verilator testbench internal signal access — `_gen_dTLBMiss` replaced with `0` (Verilator optimizes it away) --- ## C++ Simulation Backend (Phase 10) — DONE JIT simulator foundation: generates C++ code from IR (`Module`/`Design`), producing a C++ class with `eval()`/`tick()`/`reset()` methods. ### Completed | # | Task | Status | |---|------|--------| | 1 | `Sparkle/Backend/CppSim.lean` — C++ code generator (~410 lines) | Done | | 2 | `Tests/TestCppSim.lean` — 25 tests (counter, memory, combinational, registered memory) | Done | | 3 | Integrated into `Sparkle.lean` and `Tests/AllTests.lean` | Done | | 4 | `lake build` + `lake test` — all 25 CppSim tests pass | Done | ### Architecture - Mirrors `Sparkle/Backend/Verilog.lean`: same IR traversal, different target language - **Type mapping**: `bit`/`bv≤8` → `uint8_t`, `bv≤16` → `uint16_t`, `bv≤32` → `uint32_t`, `bv≤64` → `uint64_t`, `bv>64` → `std::array`, arrays → `std::array` - **Bit-width masking**: mask at assignment only (widths ∉ {8,16,32,64}) - **eval()/tick()/reset() split**: combinational in `eval()`, register update in `tick()`, initialization in `reset()` - **Expression translation**: constants as `(uint32_t)42ULL`, signed ops as `(int32_t)` casts, concat as shift+OR chain, slice as `>> lo & mask` - **Sub-module instantiation**: uses `Design` to resolve input/output port directions --- ## SoC Synthesis (Phase 9) `#writeDesign rv32iSoCSynth "verilator/generated_soc.sv" "verilator/generated_soc_cppsim.h"` generates both SystemVerilog and CppSim C++ from a single synthesis pass. The SoC has 119 registers. ### Generated Output - **Verilog**: `verilator/generated_soc.sv` (~28k lines) - **CppSim**: `verilator/generated_soc_cppsim.h` (~4.9k lines after IR optimization) - **Top module**: `Sparkle_Examples_RV32_SoCVerilog_rv32iSoCSynth` - **Inputs**: `clk`, `rst`, `_gen_imem_wr_en`, `_gen_imem_wr_addr[11:0]`, `_gen_imem_wr_data[31:0]`, `_gen_dmem_wr_en`, `_gen_dmem_wr_addr[22:0]`, `_gen_dmem_wr_data[31:0]` - **Output**: `out[191:0]` — packed `{pcReg[191:160], uartValidBV[159:128], prevStoreData[127:96], satpReg[95:64], ptwPteReg[63:32], ptwVaddrReg[31:0]}` - **Wrapper**: `rv32i_soc_wrapper.sv` adapts packed output to Verilator testbench interface ### Register Map (119 registers) | Range | Count | Description | |-------|-------|-------------| | 0-5 | 6 | IF stage: pcReg, fetchPC, flushDelay, ifid_inst/pc/pc4 | | 6-29 | 24 | ID/EX pipeline registers | | 30-31 | 2 | EX/WB: exwb_alu, exwb_physAddr | | 32-38 | 7 | EX/WB: rd, regW, m2r, pc4, jump, isCsr, csrRdata | | 39-44 | 6 | WB forwarding + store history | | 45-49 | 5 | CLINT: msip, mtime, mtimecmp | | 50-56 | 7 | CSR M-mode | | 57-58 | 2 | AI MMIO | | 59-60 | 2 | Sub-word + M-ext | | 61-69 | 9 | A-ext (reservation, AMO, pending write) | | 70-79 | 10 | S-mode CSRs + privilege + delegation | | 80-107 | 28 | MMU: 4-entry TLB + PTW FSM | | 108-109 | 2 | SRET + SFENCE.VMA | | 110-115 | 6 | UART 8250 | | 116-117 | 2 | mcounteren, scounteren | | 118 | 1 | divPending | ### Key Architecture Decisions 1. **`unfoldDefinition?` instead of `whnf`**: Prevents exponential blowup on 119-register tuple projections 2. **Multi-cycle restoring divider** (~34 cycles): Inlined into top module with `divPending`, `divStall`, `divAbort` 3. **Duplicated memory arrays**: `Signal.memory` + `Signal.memoryComboRead` stay in sync via identical writes 4. **Non-synthesizable functions replaced**: `mextCompute` → `mulComputeSignal` + `dividerSignal`; `amoCompute` → `amoComputeSignal` --- ## Completed Phases ### Phase 1: Compiler `memoryComboRead` support — DONE ### Phase 2: 3 bug fixes ported to SoC.lean — DONE ### Phase 3: `rv32iSoCSynth` in SoCVerilog.lean, `#synthesizeVerilog` succeeds — DONE ### Phase 9: Verilator Testing — DONE | # | Task | Status | |---|------|--------| | 1 | `#writeVerilogDesign` command in Elab.lean | Done | | 2 | `lake build Examples.RV32.SoCVerilog` — writes `verilator/generated_soc.sv` | Done | | 3 | `verilator/rv32i_soc_wrapper.sv` — unpack packed output | Done | | 4 | `verilator/Makefile` — generated SV + wrapper, `make build` | Done | | 5 | Verilator compiles generated SV successfully | Done | | 6 | Firmware test: sections 1-9 match hand-written reference exactly (45 UART words) | Done | | 7 | Firmware test: section 10 + pass marker (`0xcafe0000`) — ALL 48 UART words, cycle 2904 | Done | | 8 | Linux boot test (OpenSBI → kernel → UART output) | Done (Phase 20) | ### Compiler Bugs Fixed During Phase 9 | Bug | Fix | |-----|-----| | `bundle2` hardcoded wire width to 16 bits | Infer from expression type via `inferHWTypeFromSignal` | | `Prod.fst` slice assumed equal-width halves (`width * 2 - 1`) | Use `getWireWidth` for actual source wire width | | Verilog backend: duplicate wire/port declarations | Filter port names from internal wire list | | Core.lean: complex lambda `(fun f7 => extractLsb' 5 1 f7 == 1#1)` | Split into extract + compare steps | --- ## Linux Boot Hang Debugging — RESOLVED ### Symptoms (before fix) - **Hand-written SV**: Boots Linux 6.6.0 successfully (3944 UART bytes in ~7M cycles) - **Generated SV**: Hung at ~1906 UART bytes, PC stuck at 0xC0001C88 (recursive page fault) - OpenSBI phase worked correctly (same output as hand-written) - Kernel started, printed banner and initial messages, then hung ### Root Cause Analysis — Updated 2026-02-28 **`memblock_add` enters but fails to persist memory data → `memblock.memory` stays empty → final SATP switch page table has no kernel mapping → page fault → hang.** #### SATP switch sequence (3 switches total): | # | Cycle | SATP value | Page table | PTE[0x300] | Result | |---|-------|-----------|------------|------------|--------| | 1 | 2606985 | 0x800805F6 | setup_vm trampoline | 0x201000EF ✓ | OK | | 2 | 2607006 | 0x80080557 | initial page table | 0x201000EF ✓ | OK | | 3 | 3329886 | 0x800805F7 | setup_vm_final (swapper_pg_dir) | **0x00000000** ✗ | **CRASH** | The third SATP switch activates `swapper_pg_dir` which has PTE[0x276] and PTE[0x277] (DTB mapping) but **NO PTE[0x300]** (kernel code mapping). This causes an immediate page fault at PC 0xC0001C88 → infinite recursive page fault → hang. #### Why PTE[0x300] is missing: `paging_init()` → `setup_vm_final()` uses `create_pgd_mapping()` which relies on `memblock_alloc()` to allocate page table pages. Since `memblock.memory` is empty (total_size=0), no memory can be allocated, so `swapper_pg_dir` stays mostly empty. #### Why memblock.memory is empty: `early_init_dt_add_memory_arch` IS called and correctly clamps: - base: 0x80000000 → 0x80400000 (phys_offset = kernel load address) - size: 0x02000000 → 0x01C00000 Then it tail-calls `memblock_add(0x80400000, 0x01C00000)`. **`memblock_add` enters** (confirmed at cycle 3162213, PC 0xC0153F8C) but `memblock.memory.total_size` remains 0 when checked at cycle 3200000 and later. The "simple case" in `memblock_add_range.isra.0` (when regions[0].size==0) should write: ``` regions[0].base = base → DMEM word 0x156166 regions[0].size = size → DMEM word 0x156167 regions[0].flags = 0 → DMEM word 0x156168 type->total_size = size → DMEM word 0x156159 ``` **Something prevents these stores from completing or persisting.** #### Disproved hypotheses: 1. ~~TLB eviction by `of_get_flat_dt_prop("hotpluggable")` causes wrong physical address~~ — **DISPROVED**: MMU trace showed NO PTW activity, NO TLB misses between the two trace windows. The code path was actually correct — while loop exits properly after 1 DTB reg entry. 2. ~~DTB parsing reads wrong offset (60-byte shift)~~ — DTB parsing works correctly 3. ~~`memblock_add` never called~~ — It IS called at cycle 3162213 4. ~~`early_init_dt_add_memory_arch` never called~~ — Confirmed executing at C0151258 ### Prime Suspect: `holdEX` includes `divStall` in generated SoC but NOT in hand-written SV **Critical difference found:** | | Hand-written SV (rv32i_soc.sv:803) | Generated (SoC.lean:995-996) | |---|---|---| | `holdEX` | `pendingWriteEn` | `pendingWriteEn \|\| (divStall && !flushOrDelay)` | The generated SoC includes `divStall` in `holdEX`. Since `suppressEXWB = dTLBMiss || holdEX`, and `prevStoreEn_next = suppressEXWB ? false : idex_memWrite`, **any store in the EX stage during a multi-cycle divide (~34 cycles) gets its `prevStoreEn` killed**. This is the most likely root cause: if a `sw` instruction in `memblock_add_range` happens to execute while `divStall` is active (e.g., from a prior DIV instruction still in flight), the store will be suppressed. ### Debugging Strategy: Trace Diffing (Co-simulation comparison) Compare cycle-accurate traces from hand-written (working) and generated (broken) SoC: 1. Add per-cycle trace output: `PC, dmem_we, dmem_addr, dmem_wdata, holdEX, divStall, suppressEXWB` 2. Build both SoCs with same testbench 3. Run both with identical firmware/DTB/payload 4. `diff` the traces around memblock_add (cycle ~3162213) 5. Identify exact cycle where generated SoC diverges (store suppressed) ### Resolution **FIXED** in Phase 13 — `holdEX` simplified to `pendingWriteEn` only; pipeline freezing uses new `freezeIDEX` signal. See "holdEX/divStall Store Bug Fix" section above. ### Resolution Timeline - [x] ~~**Fix holdEX in SoC.lean**~~ — Done (Phase 13) - [x] ~~**Verify Linux boots on fixed generated SoC**~~ — Done (Phase 20): 5250 UART bytes, matches hand-written SV behavior - [ ] **Implement Trace Diffing** — add unified trace format to tb_soc.cpp for future debugging - [ ] **(Future) Formal verification** — prove Store Persistence invariant in Lean: `always (idex_isStore ∧ ¬trap ⟹ eventually (dmem_we ∧ correct_addr))` ### Relevant Verilator signal names | Signal | Description | |--------|-------------| | `_gen_useTranslatedAddr_7683` | Whether TLB-translated address is used | | `_gen_dTLBMiss_7691` | D-side TLB miss | | `_gen_effectiveAddr_7684` | Final address after MMU bypass check | | `_gen_dPhysAddr_7681` | Physical address from TLB | | `_gen_alu_result_approx_7575` | Raw ALU result (VA for loads/stores) | | `_gen_dmem_read_addr_7710` | Final 23-bit DMEM word address | | `_gen_actual_dmem_write_addr_7779` | DMEM write word address | | `_gen_actual_byte0_we_7784` | Byte 0 write enable | | `_gen_ptwStateNext_9216` | PTW state next (3-bit) | | `_gen_ptwReq_9178` | PTW request signal | | `_gen_stall_8786` | Pipeline stall | | `_gen_flush_8294` | Flush signal | | `_gen_suppressEXWB_8849` | Suppress EX/WB writeback | ### Key kernel addresses (from /tmp/linux/vmlinux) | Function | Virtual Address | Description | |----------|----------------|-------------| | `early_init_dt_scan_memory` | 0xC01513A0 | Outer DTB scanning function | | `early_init_dt_add_memory_arch` | 0xC0151238 | Clamps base/size, tail-calls memblock_add | | `memblock_add` | 0xC0153F8C | Entry point (stores base/size to stack) | | `memblock_add_range.isra.0` | 0xC0153D40 | Core logic — simple case writes to regions[0] | | `setup_vm_final` | (in paging_init) | Creates swapper_pg_dir using memblock_alloc | ### Key DMEM addresses for memblock | Address | DMEM word | Description | |---------|-----------|-------------| | 0xC0158554 (PA 0x80558554) | 0x156155 | memblock struct start | | 0xC015855C (PA 0x8055855C) | 0x156157 | memblock.memory.cnt | | 0xC0158560 (PA 0x80558560) | 0x156158 | memblock.memory.max | | 0xC0158564 (PA 0x80558564) | 0x156159 | memblock.memory.total_size | | 0xC0158568 (PA 0x80558568) | 0x15615A | memblock.memory.regions (pointer) | | 0xC0158598 (PA 0x80558598) | 0x156166 | memblock_memory_init_regions[0].base | | 0xC015859C (PA 0x8055859C) | 0x156167 | memblock_memory_init_regions[0].size | | 0xC01585A0 (PA 0x805585A0) | 0x156168 | memblock_memory_init_regions[0].flags | --- ## Firmware Test — PASSED - ALL 48 UART words match hand-written reference byte-for-byte - All 10 sections pass including `0xcafe0000` pass marker at cycle 2904 - Identical behavior to hand-written SV reference --- ## Future Work ### Next Phases 1. **CppSim Backend Optimization** — Close the 2.7x gap with Verilator by optimizing the generated C++: - **Expression inlining**: Eliminate single-use `_tmp_*` wires by folding them into consumer expressions (biggest impact — would cut assignments from 2242 to ~800) - **Constant folding**: Inline literal constants (`0`, `1`, bit masks) instead of storing as member variables - **Sized types**: Use `uint8_t`/`uint32_t` instead of `uint64_t` + runtime masking - **Memory deduplication**: Eliminate duplicate register file and DRAM arrays for multi-port reads - **If-else for reset/WE**: Use `if` blocks instead of ternary for reset paths (enables branch prediction skip) 2. **Verified Standard IP Library** — Formally proven, synthesizable components: - FIFO buffers (overflow/underflow safety proofs) - Cache controllers (coherence proofs) - AXI4/TileLink bus protocol wrappers (deadlock-free proofs) 3. **GPGPU / Vector Core** — Apply VDD framework to highly concurrent, memory-bound accelerator architectures. Thread-level parallelism + shared memory = ideal target for safety/liveness proofs. 4. **FPGA Tape-out Flow** — Deploy Sparkle-generated Linux SoCs to physical FPGAs (Gowin Tang Nano, Xilinx PYNQ, etc.). ### Completed - [x] ~~Transparent JIT (`loopMemoJIT`)~~ — Done (Phase 21): Signal API + Streaming API, 47 UART words pass - [x] ~~Simulation performance analysis~~ — Done (Phase 22): Root cause identified (2x instruction count + 1.38x IPC) - [x] ~~Verify Linux boots on fixed generated SV~~ — Done (Phase 20): 5250 UART bytes, kernel init progressing - [x] ~~Fix Verilator testbench internal signal references~~ — `_gen_dTLBMiss` replaced with `0` ### Backlog (lower priority) - [ ] Fix `~~~` (Complement) synthesis — add typeclass unfolding for `Complement.mk` in Elab.lean - [ ] More DRC rules — clock domain crossing, combinational loop detection, undriven wire detection - [ ] Apply `declare_signal_state` to remaining state tuples (Divider, Backbone, C2f, SPPF, Neck, Head) - [ ] Fix Lean simulation stack overflow on macOS (reduce tuple nesting depth or use worker thread) - [ ] Run Linux boot for more cycles (both generated and hand-written need >10M cycles to reach shell) - [ ] Fix CppSim >64-bit output port assignment (currently skipped, `getOutput` returns 0 for packed outputs) --- ## Build & Test Commands ```bash # Lean build (SoC simulation) lake build Examples.RV32.SoC # Lean build (Verilog synthesis → writes SV + CppSim + JIT wrapper) lake build Examples.RV32.SoCVerilog # JIT test (compile, load, run firmware from Lean) lake exe rv32-jit-test verilator/generated_soc_jit.cpp firmware/firmware.hex 5000 # JIT loop test (loopMemoJIT Signal API + Streaming API) lake exe rv32-jit-loop-test verilator/generated_soc_jit.cpp firmware/firmware.hex 5000 # CppSim test (standalone C++) cd verilator && make build-cppsim && make run-cppsim CYCLES=5000 # Verilator build (generated SV + wrapper) cd verilator && make build # Verilator build (hand-written SV, reference) cd verilator && make build-handwritten # Firmware test cd verilator && ./obj_dir/Vrv32i_soc ../firmware/firmware.hex 500000 # JIT shared library build (manual) cd verilator && make build-jit # JIT bench (eval+tick vs evalTick comparison) cd verilator && make build-jit && \ clang++ -O2 -std=c++17 -o jit_bench tb_jit_bench.cpp -ldl && \ ./jit_bench ../firmware/firmware.hex 10000000 generated_soc_jit.dylib # Oracle test (cycle-skipping) lake exe rv32-jit-oracle-test # Dynamic warp test (memsetWord + BSS-clear) lake exe rv32-jit-dynamic-warp-test # Speculative warp test (snapshot/restore + rollback) lake exe rv32-jit-speculative-warp-test # CDC lock-free queue tests + benchmark make -C c_src/cdc test # CDC multi-clock JIT simulation make -C c_src/cdc cdc_runner.so lake exe cdc-multi-clock-test # Linux boot test cd verilator && ./obj_dir/Vrv32i_soc ../firmware/opensbi/boot.hex 10000000 \ --dram /tmp/opensbi/build/platform/generic/firmware/fw_jump.bin \ --dtb ../firmware/opensbi/sparkle-soc.dtb \ --payload /tmp/linux/arch/riscv/boot/Image ```