Simten

How it works

A short tour of Simten's model — primitives, composites, time, and export.

Five things that make Simten different from a textbook simulator. Most of this page is live circuits — read the code, hit play, drill in.

1. Circuits are TypeScript values

A circuit is an object you can read, refactor, and pass to a function. There's no separate HDL file, no synthesis script, no build step. You import it like any other module.

const HalfAdder = circuit('HalfAdder', {
  inputs:  { a: bit, b: bit },
  outputs: { sum: bit, carry: bit },
  nodes:   { xor1: Xor, and1: And },
  connect: ({ inputs, outputs, nodes: { xor1, and1 } }) => [
    inputs.a.to(xor1.a, and1.a),
    inputs.b.to(xor1.b, and1.b),
    xor1.out.to(outputs.sum),
    and1.out.to(outputs.carry),
  ],
});
Compiling...

Toggle the input switches. The output reflects the change immediately because everything here is combinational logic — no clock involved.

2. Composites zoom in

Hardware is recursive. A circuit is built of circuits, which are built of circuits, all the way down to primitives. Double-click any composite node to drill in.

Compiling...

This FullAdder is two HalfAdders and an Or gate. Double-click ha1 and you'll see the same circuit you wrote above — its XOR and AND gates exposed. The inspector keeps zooming as long as there's structure to peel.

3. Sequential vs combinational

Combinational logic answers what — the output is a pure function of the inputs. Sequential logic remembers when — state advances on each clock tick.

Compiling...

Toggle d and hit ▶. The bit shifts one stage per cycle — q1 lags d by one tick, q2 by two. Flip-flops remember; gates don't.

const DelayLine = circuit('DelayLine', {
  inputs:  { d: bit },
  outputs: { q1: bit, q2: bit },
  nodes:   { dff1: DFlipFlop(), dff2: DFlipFlop() },
  connect: ({ inputs, outputs, nodes: { dff1, dff2 } }) => [
    inputs.d.to(dff1.d),
    dff1.q.to(dff2.d, outputs.q1),
    dff2.q.to(outputs.q2),
  ],
});

The DSL is the same shape — inputs, outputs, nodes, connect. Sequentiality comes from which primitives you pick (DFlipFlop, Register, RAM), not from a separate sub-language.

4. The trace is the truth

Every value, every wire, every cycle is recorded. When something goes wrong you scrub backward to the exact cycle the bug appeared — no printf, no rerun, no guessing.

The RISC-V CPU debugger is the longest worked example. It runs compiled C through a 5-stage pipeline; every register, every pipeline stage, every cycle is inspectable.

5. Export to Verilog

The circuits you read are the same circuits we cycle-by-cycle verify against Icarus Verilog. Synthesizable circuits export to real Verilog you can flash to an FPGA.

import { exportVerilog } from '@simten/core/verilog';

const { verilog, files } = exportVerilog(HalfAdder, library, { target: 'synthesis' });

The synthesis target strips simulation-only constructs. Memories above 2k words are emitted as $readmemh with the hex contents returned alongside — matching the Yosys/Vivado/Quartus convention.

Every emitted module that contains sequential logic also gets clk and rst_n ports — the exporter auto-plumbs both (your circuit() definitions don't reference clock or reset). rst_n is active-low and synchronous: when held low, registers reset to their value arg, memories preserve their contents, and RV32I register files zero. Your board wrapper must drive both pins.

See Hardware & FPGA for the full pipeline (Yosys → nextpnr → ecppack → flash) using Snake on a ULX3S as the worked example.


Where to go next

On this page