The cycle simulator
Combinational within a cycle, sequential at the edge, and the edge is a real boundary.
The simulator evaluates the graph in topological order. Combinational nodes settle first, then registers sample their inputs and take their new values at the clock edge. Everything else — memories, handshakes, the lot — falls out of that one rule.
The rule matters because it is the rule real hardware uses. A design that works in a simulator which re-evaluates everything in arbitrary order is a design whose behaviour you have not actually checked.
Step by step
- 01
Compile once, then settle
Sim::newcompiles the circuit into an evaluation program.combsettles the combinational cone;stepdoes the whole cycle and returns the snapshots either side of the edge.use ferrite_lithic_sim::Sim; let mut sim = Sim::new(&design)?; sim.initialize_to(&state, ferrite_lithic_bits::Bits::zeros(8)?)?; let (before, after) = sim.step()?;
- 02
Three explicit points around the edge
before_clock_edge,at_clock_edgeandafter_clock_edgeare separate methods because separate methods are what let a testbench place stimulus on the right side of the edge. The alternative — astepthat takes an input — is what caused theHandle::setbug recorded inDETAILS.md: it silently acquired an extra clock edge.sim.before_clock_edge()?; // sample here sim.set_input(&d, &value)?; sim.at_clock_edge()?; // registers take their new values sim.after_clock_edge()?;
- 03
Registers update last, and visibly
reg_updatesandmem_updatesexpose what the edge actually did. A testbench that wants to assert on the transition rather than on the value needs exactly this, and it is the reason the corpus can check a one-symbol-per-cycle decoder rather than only its final answer.let updates = sim.reg_updates()?; for update in updates { println!("{}: {:?} -> {:?}", update.signal, update.before, update.after); } - 04
Errors rather than silent zero
An undriven wire, a width mismatch and a combinational loop are all errors. A simulator that substitutes zero for a missing driver produces plausible waveforms for a broken design, which is the failure mode this project has the least tolerance for.
What bites
initialise-to-value refuses memories
Not a limitation left unstated: it is a deliberate refusal, because there is no initialised-memory node in the IR to initialise. Adding one is the change that would unlock it.
Widths are not coerced
A
Bitshanded toset_inputmust match the port's width or the call errors. Silent zero-extension here would be a bug that only appears on one backend.