Gatework is an event-driven digital logic simulator in Haskell. It parses a plain-text netlist, runs the circuit, and writes a VCD waveform file. A report command prints the signal values as a text table. GTKWave and other waveform viewers can open the output. The simulator uses four logic values: low, high, unknown, and floating. Multi-bit buses use bracketed widths and slices. Reusable modules can live in separate library files.
Gatework makes digital state changes easy to inspect.
You can do these tasks:
- Simulate a ripple-carry adder from a netlist file.
- Simulate a four-bit counter from a netlist file.
- Simulate a register with a scheduled data stream.
- Reset a flip-flop with an asynchronous reset pin.
- Build a multi-bit register from one flip-flop declaration.
- Use NAND, NOR, and XNOR gates alongside the basic gates.
- Define reusable modules and instantiate them many times.
- Build a hierarchical adder from half-adder and full-adder modules.
- Store reusable modules in separate library files.
- Load a module library with the
--libraryoption. - Build a hierarchical adder from a module library file.
- Open the VCD output in GTKWave and inspect the waveforms.
- Print a text waveform report with the report command.
- Verify gate behavior with QuickCheck property tests.
- Compare the counter waveform with a repository golden file.
- Check netlist behavior with waveform assertions.
- Inspect assertion metadata inside the VCD file.
- Use unknown (x) and floating (z) logic values.
- Drive a tri-state buffer from data and enable inputs.
- Share one wire between two tri-state buffers.
- Resolve a low and a high driver into unknown.
- See an uninitialized flip-flop read as unknown.
- Watch a gate read a floating input as unknown.
- Declare multi-bit buses with
input a[4]andwire x[4]. - Read one bit of a bus with
a[2]. - Read a bus slice with
a[3:0]. - Apply a gate bitwise across two buses.
- Pass a bus through a module port.
- Sample a whole bus into a register on one clock edge.
- Check a single bus bit with an assertion.
- Set a whole bus on the command line with one bit string.
- Read a whole bus as one multi-bit VCD vector.
- See unknown and floating values inside a VCD vector.
- Group module-internal buses into VCD vectors too.
- Declare a gate output delay with the
delay=Nfield. - Watch a gate transition fire after its declared delay.
- Let gate delays accumulate through a chain of gates.
- Declare separate rise and fall delays with the
rise=Nandfall=Nfields. - Watch a rising output fire after its rise delay.
- Watch a falling output fire after its fall delay.
- Declare a clock-to-output delay with the
tco=Nfield. - Watch a flip-flop output commit after its clock-to-output delay.
- Capture data at the clock edge, not at the commit time.
- Delay an asserted reset by the same amount.
- Use a non-inverting
BUFgate for known and unknown signal paths. - Select data with a three-input
MUXgate.
The project has five library modules.
| Module | Responsibility |
|---|---|
Gatework.Logic |
Defines logic values and gate functions |
Gatework.Netlist |
Parses, validates, and flattens circuit files |
Gatework.Report |
Renders the waveform as a text table |
Gatework.Simulator |
Schedules signal changes |
Gatework.VCD |
Renders waveform text as signals and bus vectors |
The data flow is:
library text -> parser -> library module table
netlist text -> parser -> module table -> flattened netlist -> event queue -> waveform recorder -> VCD
waveform recorder -> text table (report command)
The parser validates names, gate arity, drivers, clocks, and references. The parser loads module definitions from library files. A library file holds only module definitions. The parser merges the main and library modules into one table. The parser rejects a module name that appears in more than one file. The parser expands each instance into the module gates. The parser expands each bus into single-bit signals before simulation. The flattened netlist uses dotted names for instance signals. The scheduler processes only changed signals. The scheduler tracks one committed contribution per gate driver. The scheduler resolves several gate drivers into one wire value. A delayed gate commits its new value after its delay elapses. A rising output uses the rise delay. A falling output uses the fall delay. A gate delay accumulates through a chain of gates. A rising clock edge samples attached flip-flops together. A flip-flop commits its output after its clock-to-output delay. A flip-flop captures the data value at the clock edge. An asserted reset forces flip-flop outputs to their initial values. The recorder keeps the initial value and every later transition. The assertion checker compares declared expectations with the waveform. The VCD writer uses stable signal order and stable identifiers. The VCD writer groups bus bits into multi-bit vectors. The report writer prints one row per change time.
The repository layout is:
src/ library modules
app/ command-line tool
test/ test suite
fixtures/ circuit files and golden output
.github/workflows/ continuous integration
Install GHC 9.6 and Cabal 3.10 or newer.
Build the package.
cabal buildRun the test suite.
cabal test --enable-testsThe frozen Cabal file pins the package versions.
You can also run the project in a container. The container avoids a local Haskell install.
Build the image.
docker build -t gatework-dev .Run the test suite.
docker run --rm -v "${PWD}:/work" -w /work gatework-dev sh -c "cabal update && cabal test --enable-tests"The first run downloads the package index. On Windows PowerShell, use the absolute repository path in the volume.
Run the bundled counter.
cabal run gatework -- --netlist fixtures/counter.net --duration 8 --output counter.vcdThe command writes this output:
Wrote counter.vcd
Signals: 11
Duration: 8 time units
Open the waveform in GTKWave.
gtkwave counter.vcdThe counter advances on rising clock edges. Its value sequence is 0, 1, 2, 3, and 4.
| Time | q3 | q2 | q1 | q0 | Value |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 0 | 0 | 0 | 1 | 1 |
| 3 | 0 | 0 | 1 | 0 | 2 |
| 5 | 0 | 0 | 1 | 1 | 3 |
| 7 | 0 | 1 | 0 | 0 | 4 |
Print the counter as a text table.
cabal run gatework -- report --netlist fixtures/counter.net --duration 8The command prints this table:
time | q0 | q1 | q2 | q3 | clk | d0 | d1 | carry2 | d2 | carry3 | d3
---- | -- | -- | -- | -- | --- | -- | -- | ------ | -- | ------ | --
0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0
1 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0
2 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0
3 | 0 | 1 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0
4 | 0 | 1 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0
5 | 1 | 1 | 0 | 0 | 1 | 0 | 0 | 1 | 1 | 0 | 0
6 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0
7 | 0 | 0 | 1 | 0 | 1 | 1 | 0 | 0 | 1 | 0 | 0
8 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0
The table shows one row per change time.
Each column shows one signal.
Each cell shows the settled value at that time.
Use --output FILE to write the table to a file.
Without --output, the table prints to standard output.
The report uses the same options as the VCD command.
Run three plus five with explicit input values.
cabal run gatework -- --netlist fixtures/adder.net --duration 0 --output adder.vcd --set a0=1,a1=1,a2=0,a3=0,b0=1,b1=0,b2=1,b3=0,cin=0The low-order bit uses index zero. The adder produces binary 1000 for this example.
Run a D flip-flop with a scheduled data stream.
cabal run gatework -- --netlist fixtures/register.net --duration 8 --output register.vcd --set d=1 --at 2 d=0 --at 4 d=1 --at 6 d=0The --set option sets the initial value at time zero.
The --at option changes an input at a fixed time.
The flip-flop samples the data input on each rising clock edge.
| Time | d | q |
|---|---|---|
| 0 | 1 | 0 |
| 1 | 1 | 1 |
| 2 | 0 | 1 |
| 3 | 0 | 0 |
| 4 | 1 | 0 |
| 5 | 1 | 1 |
| 6 | 0 | 1 |
| 7 | 0 | 0 |
Run a D flip-flop with an asynchronous reset.
cabal run gatework -- --netlist fixtures/reset.net --duration 8 --output reset.vcd --set d=1 --at 2 rst=1 --at 4 rst=0 --at 6 d=0The reset input rst is active high.
It forces the flip-flop back to its initial value without waiting for a clock edge.
The clock does not sample data while reset is high.
| Time | d | rst | q |
|---|---|---|---|
| 0 | 1 | 0 | 0 |
| 1 | 1 | 0 | 1 |
| 2 | 1 | 1 | 0 |
| 3 | 1 | 1 | 0 |
| 4 | 1 | 0 | 0 |
| 5 | 1 | 0 | 1 |
| 6 | 0 | 0 | 1 |
| 7 | 0 | 0 | 0 |
Run a two-bit register from one flip-flop declaration.
cabal run gatework -- --netlist fixtures/reg2.net --duration 10 --output reg2.vcd --set d0=1,d1=0 --at 4 d0=0,d1=1 --at 6 rst=1 --at 8 rst=0,d0=1,d1=1The d= and q= fields take comma-separated signal lists.
The register samples every bit on each rising clock edge.
| Time | d0 | d1 | rst | q0 | q1 |
|---|---|---|---|---|---|
| 0 | 1 | 0 | 0 | 0 | 0 |
| 1 | 1 | 0 | 0 | 1 | 0 |
| 4 | 0 | 1 | 0 | 1 | 0 |
| 5 | 0 | 1 | 0 | 0 | 1 |
| 6 | 0 | 1 | 1 | 0 | 0 |
| 8 | 1 | 1 | 0 | 0 | 0 |
| 9 | 1 | 1 | 0 | 1 | 1 |
Run the universal gate truth demo.
cabal run gatework -- --netlist fixtures/gates.net --duration 7 --output gates.vcd --set a=0,b=1 --at 2 a=1,b=1 --at 4 a=1,b=0 --at 6 a=0,b=0The command writes this output:
Wrote gates.vcd
Signals: 5
Duration: 7 time units
Assertions: 12 passed
The run walks the complete truth table of NAND, NOR, and XNOR. Each assertion checks one row of one table. The run reports a pass only when every assertion holds.
| Time | a | b | nand_out | nor_out | xnor_out |
|---|---|---|---|---|---|
| 0 | 0 | 1 | 1 | 0 | 0 |
| 2 | 1 | 1 | 0 | 0 | 1 |
| 4 | 1 | 0 | 1 | 0 | 0 |
| 6 | 0 | 0 | 1 | 1 | 1 |
Run a direct buffer and a delayed buffer.
cabal run gatework -- --netlist fixtures/buffer.net --duration 8 --output buffer.vcd --set d=0 --at 2 d=1 --at 4 d=0 --at 6 d=zThe command writes this output:
Wrote buffer.vcd
Signals: 3
Duration: 8 time units
Assertions: 9 passed
BUF passes low, high, and unknown values.
It maps a floating z input to x, because ordinary gates read floating inputs as unknown.
The delayed instance applies the existing gate delay rules.
| Time | d | y | delayed |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 2 | 1 | 1 | 0 |
| 4 | 0 | 0 | 1 |
| 6 | z | x | 0 |
| 8 | z | x | x |
Run the multiplexer demo.
cabal run gatework -- --netlist fixtures/mux.net --duration 10 --output mux.vcd --set d0=0,d1=1,sel=0 --at 2 sel=1 --at 4 sel=x --at 6 d0=1,d1=1 --at 8 sel=zThe command writes this output:
Wrote mux.vcd
Signals: 5
Duration: 10 time units
Assertions: 9 passed
MUX uses (d0,d1,sel) input order.
A low selector chooses d0.
A high selector chooses d1.
An unknown selector returns the common branch value when both branches agree.
A floating selected value becomes x.
The delayed instance shows the same behavior two time units later.
| Time | d0 | d1 | sel | y | delayed |
|---|---|---|---|---|---|
| 0 | 0 | 1 | 0 | 0 | 0 |
| 2 | 0 | 1 | 1 | 1 | 0 |
| 4 | 0 | 1 | x | x | 1 |
| 6 | 1 | 1 | x | 1 | x |
| 8 | 1 | 1 | z | 1 | 1 |
Run the hierarchical ripple-carry adder.
cabal run gatework -- --netlist fixtures/haddader.net --duration 0 --output haddader.vcd --set a0=1,a1=1,a2=0,a3=0,b0=1,b1=0,b2=1,b3=0,cin=0The netlist defines a half-adder module and a full-adder module. The full-adder module contains two half-adder instances. The top level joins four full-adder instances in a carry chain. The adder produces binary 1000 for three plus five.
Run the same adder with its modules in a separate file.
cabal run gatework -- --netlist fixtures/libadder.net --library fixtures/adderlib.net --duration 0 --output libadder.vcd --set a0=1,a1=1,a2=0,a3=0,b0=1,b1=0,b2=1,b3=0,cin=0The file fixtures/libadder.net holds only the top level.
The file fixtures/adderlib.net defines the half-adder and full-adder modules.
The --library option loads the module definitions.
The library adder produces the same waveform as the hierarchical adder.
Run the hierarchical four-bit counter.
cabal run gatework -- --netlist fixtures/hcounter.net --duration 8 --output hcounter.vcdThe netlist defines a one-bit flop module.
The counter4 module contains four flop instances.
The counter advances on rising clock edges.
Its value sequence is 0, 1, 2, 3, and 4.
Run a netlist that checks its own waveform.
cabal run gatework -- --netlist fixtures/assert.net --duration 6 --output assert.vcd --set a=0,b=1 --at 3 a=1 --at 5 a=0The command writes this output:
Wrote assert.vcd
Signals: 4
Duration: 6 time units
Assertions: 3 passed
The fixture declares three expectations.
Each one checks the output y at a fixed time.
The run reports a pass only when every assertion holds.
| Time | a | b | y | Assertion |
|---|---|---|---|---|
| 0 | 0 | 1 | 1 | y = 1 holds |
| 3 | 1 | 1 | 0 | y = 0 holds |
| 5 | 0 | 1 | 1 | y = 1 holds |
A wrong expectation makes the run fail. The error names the signal, the time, and both values.
assertion failed: out must be 1 at time 2, but was 0
The run still writes the VCD file. Use the file to inspect the waveform after the failure.
Run a tri-state buffer with a scheduled enable.
cabal run gatework -- --netlist fixtures/tristate.net --duration 8 --output tristate.vcd --set d=0,en=0 --at 2 en=1 --at 4 d=1 --at 6 en=0The command writes this output:
Wrote tristate.vcd
Signals: 4
Duration: 8 time units
Assertions: 8 passed
The gate TRIBUF drives y only while en is high.
When en is low, y floats to z.
The gate NOT reads a floating input as unknown.
| Time | d | en | y | nz |
|---|---|---|---|---|
| 0 | 0 | 0 | z | x |
| 3 | 0 | 1 | 0 | 1 |
| 5 | 1 | 1 | 1 | 0 |
| 7 | 1 | 0 | z | x |
Run two tri-state buffers that share one wire.
cabal run gatework -- --netlist fixtures/shared.net --duration 8 --output shared.vcd --set d0=0,e0=0,d1=1,e1=0 --at 2 e0=1 --at 4 e1=1 --at 6 e0=0The command writes this output:
Wrote shared.vcd
Signals: 6
Duration: 8 time units
Assertions: 8 passed
Both buffers drive the wire y.
The simulator resolves the two driver values into one value.
A disabled buffer floats to z.
A z contribution is neutral during resolution.
An enabled buffer drives its data value.
Two enabled buffers with different values give x.
| Time | d0 | e0 | d1 | e1 | y | ny |
|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 1 | 0 | z | x |
| 3 | 0 | 1 | 1 | 0 | 0 | 1 |
| 5 | 0 | 1 | 1 | 1 | x | x |
| 7 | 0 | 0 | 1 | 1 | 1 | 0 |
The gate NOT observer reads the resolved wire y.
Its output ny is the inverse of the resolved value.
Run a flip-flop with an unknown initial value.
cabal run gatework -- --netlist fixtures/unknown.net --duration 8 --output unknown.vcd --set d=1 --at 4 d=0The command writes this output:
Wrote unknown.vcd
Signals: 4
Duration: 8 time units
Assertions: 6 passed
The init=x field keeps the output unknown until the first clock edge.
The NOT gate shows the unknown until the flip-flop samples data.
| Time | d | q | nq |
|---|---|---|---|
| 0 | 1 | x | x |
| 3 | 1 | 1 | 0 |
| 5 | 0 | 0 | 1 |
Run two inverters with gate delays.
cabal run gatework -- --netlist fixtures/delay.net --duration 12 --output delay.vcd --set a=0 --at 2 a=1 --at 6 a=0The command writes this output:
Wrote delay.vcd
Signals: 3
Duration: 12 time units
Assertions: 7 passed
The first inverter delays its output by two time units. The second inverter delays its output by three time units. An input change reaches the first output two units later. The change reaches the second output three units after that.
| Time | a | x | y |
|---|---|---|---|
| 0 | 0 | 1 | 0 |
| 2 | 1 | 1 | 0 |
| 4 | 1 | 0 | 0 |
| 6 | 0 | 0 | 0 |
| 7 | 0 | 0 | 1 |
| 8 | 0 | 1 | 1 |
| 11 | 0 | 1 | 0 |
The input a rises at time 2.
The signal x falls at time 4, two units later.
The signal y rises at time 7, three units after x.
The input a falls at time 6.
The signal x rises at time 8.
The signal y falls at time 11.
The initial state settles at time zero without delay.
Run one inverter with different rise and fall delays.
cabal run gatework -- --netlist fixtures/asymdelay.net --duration 15 --output asymdelay.vcd --set a=0 --at 1 a=1 --at 6 a=0 --at 11 a=1The command writes this output:
Wrote asymdelay.vcd
Signals: 2
Duration: 15 time units
Assertions: 7 passed
The gate uses the rise=4 and fall=1 fields.
A rising output waits four time units.
A falling output waits one time unit.
The input changes stay farther apart than the longest delay.
| Time | a | y |
|---|---|---|
| 0 | 0 | 1 |
| 1 | 1 | 1 |
| 2 | 1 | 0 |
| 6 | 0 | 0 |
| 10 | 0 | 1 |
| 11 | 1 | 1 |
| 12 | 1 | 0 |
The input a rises at time 1.
The output y falls at time 2, one unit later.
The input a falls at time 6.
The output y rises at time 10, four units later.
The input a rises at time 11.
The output y falls at time 12.
Run a flip-flop with a clock-to-output delay and a reset.
cabal run gatework -- --netlist fixtures/tco.net --duration 22 --output tco.vcd --set d=1 --at 4 d=0 --at 8 d=1 --at 14 rst=1 --at 16 rst=0The command writes this output:
Wrote tco.vcd
Signals: 5
Duration: 22 time units
Assertions: 16 passed
The tco=2 field delays the output commit by two time units.
A rising clock edge samples the data at the edge.
The output commits the captured value two units later.
An asserted reset commits its value two units later too.
| Time | d | rst | q | nq |
|---|---|---|---|---|
| 0 | 1 | 0 | 0 | 1 |
| 3 | 1 | 0 | 0 | 1 |
| 4 | 0 | 0 | 1 | 0 |
| 7 | 0 | 0 | 1 | 0 |
| 8 | 1 | 0 | 0 | 1 |
| 11 | 1 | 0 | 0 | 1 |
| 12 | 1 | 0 | 1 | 0 |
| 15 | 1 | 1 | 1 | 0 |
| 16 | 1 | 0 | 0 | 1 |
| 19 | 1 | 0 | 0 | 1 |
| 20 | 1 | 0 | 1 | 0 |
The clock rises at time 2.
The output q commits to 1 at time 4, not at time 2.
The data d falls at time 4.
The output q stays 1 until the next edge commits at time 8.
The reset rst rises at time 14.
The output q stays 1 until the reset commit lands at time 16.
Each commit lands two time units after its trigger.
Run four-bit bus operations with gates and a register.
cabal run gatework -- --netlist fixtures/bus.net --duration 3 --output bus.vcd --set a=0101,b=1010The command writes this output:
Wrote bus.vcd
Signals: 24
Duration: 3 time units
Assertions: 7 passed
The value a=0101 sets the whole input bus a.
The string lists bits from the most-significant bit down.
So a[3]=0, a[2]=1, a[1]=0, and a[0]=1.
This command gives the same waveform as per-bit assignments.
The inputs a and b are four-bit buses.
The gate XOR combine applies bitwise across both buses.
The module invert_bus passes the bus x through a four-bit port.
The gate AND mask reads the slice a[3:2].
The gate OR first reads one bit of each bus.
The register samples the whole bus on the first rising clock edge.
Each assertion checks one bit of one bus.
| Time | a[3:0] | b[3:0] | x[3:0] | n[3:0] | q[3:0] |
|---|---|---|---|---|---|
| 0 | 0101 | 1010 | 1111 | 0000 | 0000 |
| 1 | 0101 | 1010 | 1111 | 0000 | 1111 |
The value 0101 means a[0]=1, a[1]=0, a[2]=1, and a[3]=0.
Bit 0 is the least-significant bit.
The value 1010 means b[0]=0, b[1]=1, b[2]=0, and b[3]=1.
Run the four-state vector demo.
cabal run gatework -- --netlist fixtures/vector4.net --duration 2 --output vector4.vcd --set a=0z01,b=01x0,en=1 --at 2 en=0The command writes this output:
Wrote vector4.vcd
Signals: 15
Duration: 2 time units
Assertions: 6 passed
The VCD header declares each bus as one vector.
$var wire 4 ! a[3:0] $end
$var wire 4 " b[3:0] $end
$var wire 1 # en $end
$var wire 4 $ x[3:0] $end
$var wire 2 % m[1:0] $end
The vector name shows the declared width and the bit range. The timeline lists the whole bus value at each change time.
#0
b0z01!
b01x0"
1#
b0xx1$
bx1%
#2
0#
bzz%
A vector value starts with the letter b.
The bit string lists the most-significant bit first.
The value b0xx1 means x[3]=0, x[2]=x, x[1]=x, and x[0]=1.
Unknown and floating values appear inside the string.
| Time | a[3:0] | b[3:0] | en | x[3:0] | m[1:0] |
|---|---|---|---|---|---|
| 0 | 0z01 | 01x0 | 1 | 0xx1 | x1 |
| 2 | 0z01 | 01x0 | 0 | 0xx1 | zz |
At time 2 the enable goes low. Both tri-state buffers float, so m reads zz. The whole vector changes together on one line.
The file fixtures/counter.golden.vcd holds the complete counter waveform.
This excerpt shows the first timestamp:
#0
0!
0"
0#
0$
0%
0&
1&
0'
0(
0)
0*
0+
#1
1!
1%
0&
1'
Each identifier is one signal. The header maps identifiers to signal names.
The file fixtures/counter.golden.report holds the counter report table.
It shows one row per change time.
The file fixtures/register.golden.vcd holds the complete register waveform.
This excerpt shows the first two timestamps:
#0
1!
0"
0#
#1
1"
1#
#2
0!
0#
Here ! is the data input d, " is the output q, and # is the clock clk.
The files fixtures/reset.golden.vcd and fixtures/reg2.golden.vcd hold
the reset and two-bit register waveforms.
The file fixtures/gates.golden.vcd holds the gate demo waveform.
The file fixtures/haddader.golden.vcd holds the hierarchical adder waveform.
Its identifiers include dotted instance names such as f0.p.
The file fixtures/libadder.golden.vcd holds the module library adder waveform.
Its content matches the hierarchical adder golden file.
The file fixtures/hcounter.golden.vcd holds the hierarchical counter waveform.
Its identifiers include dotted instance names such as counter.d0.
The file fixtures/adder.golden.vcd holds the flat adder waveform.
It shows the settled sum for three plus five.
The file fixtures/assert.golden.vcd holds the assertion demo waveform.
Its header carries the assertion text as a comment block:
$comment
assert y = 1 at 0
assert y = 0 at 3
assert y = 1 at 5
$end
The file fixtures/tristate.golden.vcd holds the tri-state demo waveform.
Its timeline uses z for a floating wire and x for a floating value read by a gate:
#0
0!
0"
0#
z#
0$
1$
x$
#2
1"
0#
1$
The file fixtures/unknown.golden.vcd holds the undefined-state demo waveform.
Its timeline shows x on the register output before the first clock edge:
#0
1!
x"
0#
x#
0$
#1
1"
0#
1$
The file fixtures/bus.golden.vcd holds the bus demo waveform.
Its header declares each bus as one vector:
$var wire 4 ! a[3:0] $end
$var wire 4 " b[3:0] $end
$var wire 4 # x[3:0] $end
$var wire 4 $ n[3:0] $end
$var wire 4 % q[3:0] $end
$var wire 1 & clk $end
$var wire 2 ' hi[1:0] $end
$var wire 1 ( c0 $end
Its timeline shows the settled vector values after the zero-delay transients:
#0
b0101!
b1010"
b1111#
b0000$
b0000%
0&
b00'
0(
1(
#1
b1111%
1&
Here ! is a, # is x, $ is n, and % is q.
The value b0101 means a[3]=0, a[2]=1, a[1]=0, and a[0]=1.
The value b1111 means every bit of x is high.
The register output q changes together on the clock edge.
The file fixtures/asymdelay.golden.vcd holds the asymmetric delay demo waveform.
Its timeline shows the output fall one unit after the input rise.
The output rise comes four units after the input fall:
#0
0!
0"
1"
#1
1!
#2
0"
#6
0!
#10
1"
#11
1!
#12
0"
Here ! is the input a and " is the output y.
The input rises at time 1 and time 11.
The output falls one unit later at time 2 and time 12.
The input falls at time 6.
The output rises four units later at time 10.
The file fixtures/tco.golden.vcd holds the clock-to-output delay demo waveform.
Its timeline shows the output commit two units after each clock edge and reset:
#0
1!
0"
0#
1$
0%
#2
1%
#4
0!
1#
0$
0%
#6
1%
#8
1!
0#
1$
0%
#12
1#
0$
0%
#14
1"
1%
#16
0"
0#
1$
0%
#20
1#
0$
0%
Here ! is d, " is rst, # is q, $ is nq, and % is clk.
The clock rises at time 2.
The output q commits to 1 at time 4.
The reset rises at time 14.
The output q stays 1 until the reset commit lands at time 16.
The file fixtures/buffer.golden.vcd holds the buffer demo waveform.
Its timeline shows direct transfer, delayed transfer, and floating-input handling:
#0
0!
0"
0#
#2
1!
1"
#4
0!
0"
1#
#6
z!
x"
0#
#8
x#
Here ! is d, " is y, and # is delayed.
Use one declaration per line.
Use # for comments.
Gate inputs use commas without spaces.
input a
input b
output y
wire n
wire y
clock clk period=2
gate NOT invert (a) -> n
gate AND combine (n,b) -> y
dff state clock=clk d=a q=state_q init=0
Supported gates are AND, OR, XOR, NAND, NOR, XNOR, NOT, BUF, MUX, and TRIBUF.
NAND, NOR, and XNOR use two inputs.
A gate output must have a wire or output declaration.
A flip-flop uses clock=, d=, and q= fields.
It accepts optional init=, rst=, and width= fields.
It accepts an optional tco= clock-to-output delay field.
Flip-flop clocks must be declared clock signals.
Clock periods use even integers of at least two.
The BUF gate is a non-inverting buffer.
It passes low, high, and unknown values.
It converts a floating z input to unknown x.
gate BUF pass (d) -> y
The MUX gate selects one of two data inputs.
Its first input is d0.
Its second input is d1.
Its third input is sel.
A low selector selects the first input.
A high selector selects the second input.
An unknown selector compares the normalized data values.
Equal values pass through.
Different values produce x.
The TRIBUF gate is a tri-state buffer.
Its first input is the data signal.
Its second input is the enable signal.
A low enable makes the output float to z.
A high enable drives the output from the data input.
A gate reads a z input as unknown.
gate TRIBUF driver (d,en) -> y
A gate can delay its output transitions.
Use the delay=N field after the output.
The value N must be a non-negative integer.
The default delay is zero.
gate NOT slow (a) -> y delay=2
gate NOT slower (y) -> z delay=3
The delay applies to both rising and falling transitions.
An input change fires the gate output N time units later.
A chain of gates adds the delays together.
The example output y changes two units after a.
The output z changes three units after y.
A gate can use separate rise and fall delays.
The rise=N field delays a transition to high.
The fall=N field delays every other transition.
Omitted delays default to zero.
gate NOT slow (a) -> y rise=4 fall=1
The delay=N field sets both delays to the same value.
The delay= field cannot combine with rise= or fall=.
A delay field cannot repeat on one gate.
An unknown gate field is an error.
The initial state settles at time zero without delay. Events at time zero ignore the gate delay.
One wire can have many gate drivers.
The simulator resolves the driver values into one wire value.
The value z is neutral during resolution.
A known value dominates the z contributions.
Two known values that differ give x.
Any unknown contribution gives x.
wire y
gate TRIBUF a (d0,e0) -> y
gate TRIBUF b (d1,e1) -> y
A flip-flop output stays exclusive to its flip-flop. A gate cannot drive a flip-flop output. One wire cannot have two flip-flop drivers.
Logic values use the VCD characters 0, 1, x, and z.
The value x means unknown.
The value z means floating.
Input assignments, init lists, and assertions accept all four values.
dff reg clock=clk d=d q=q init=x
assert y = z at 0
Use comma-separated lists to build a wider flip-flop.
The lists for d= and q= must have equal length.
The init= list must match that length or use one value for all bits.
The width= field is optional and must match the list length.
dff state clock=clk d=a q=state_q init=0
dff pair clock=clk d=d0,d1 q=q0,q1 init=0,0 rst=reset
The rst= field names an active-high reset signal.
Reset acts asynchronously. It does not wait for a clock edge.
An asserted reset forces every bit to its initial value.
A flip-flop can delay its output updates.
Use the tco=N field.
The value N must be a non-negative integer.
The default delay is zero.
dff state clock=clk d=d q=q init=0 tco=2
A rising clock edge samples the data input at the edge.
The output commits the captured value N time units later.
A later data change does not affect the captured value.
An asserted reset also commits N time units later.
The initial value settles at time zero without delay.
The tco= field cannot repeat on one flip-flop.
The delay applies to every bit of a wide flip-flop. All bits commit together at the same time.
dff pair clock=clk d=d0,d1 q=q0,q1 init=0,0 tco=3
Use assert to check a signal value at a fixed time.
The time must be a non-negative integer.
The value must be 0, 1, x, or z.
The signal must exist in the netlist.
assert y = 1 at 5
assert q0 = 0 at 3
The assertion compares the settled waveform value with the expectation. A mismatch makes the run report an error and fail. An assertion time beyond the run duration is an error.
A bus is a set of one-bit signals with one name. Declare a bus with a width in brackets.
input a[4]
output s[4]
wire t[4]
The bus a[4] creates the signals a[0], a[1], a[2], and a[3].
Bit 0 is the least-significant bit.
A signal without a width has one bit.
A reference selects the whole bus, one bit, or a slice.
a whole bus
a[2] one bit
a[3:0] a slice
The slice covers the indexes from the lower bound to the upper bound. A reference lists its bits from the lowest index upward. A slice bound outside the declared width is an error. A slice upper bound below the lower bound is an error.
All references in one gate must have the same width. The gate applies its function to each bit position.
gate XOR combine (a,b) -> s
gate AND first (a[0],b[0]) -> c0
gate AND mask (a[3:2],b[3:2]) -> t[1:0]
The first gate combines a[0] with b[0].
It also combines a[1] with b[1], and so on.
The second gate combines two single bits.
The third gate combines two two-bit slices.
A flip-flop reads and writes buses.
The data and output widths must match.
The init= list matches the width or uses one value for all bits.
A flip-flop bus output must have a wire or output declaration.
dff reg clock=clk d=a q=q init=0,0,0,0
An assertion addresses one bit.
assert s[0] = 1 at 0
assert s[2] = 0 at 4
The command line sets one bit or a whole bus.
--set a[0]=1
--set a=0101
--at 2 b=1010
A bit assignment names one signal. The value must be 0, 1, x, or z. A bus assignment names the bus without brackets. The value must match the bus width. The bit string lists the most-significant bit first. The value may use 0, 1, x, and z.
A module is a reusable subcircuit. Define it once, then use it many times.
module halfadd (a,b) -> (sum,carry)
gate XOR xsum (a,b) -> sum
gate AND xcarry (a,b) -> carry
end
input a
input b
wire s
wire c
instance halfadd u1 (a,b) -> (s,c)
The module signature lists input and output ports.
The body declares wires, gates, flip-flops, assertions, and other instances.
Do not use input or output inside a module.
Do not put a module inside another module.
Use end to close the module.
An instance connects module ports to top-level signals.
The port counts must match the module signature.
The internal signals of an instance use dotted names.
For example, the wire p of instance f0 becomes f0.p.
A module port may carry a width. The instance connection must match the port width. A four-bit port connects to a four-bit bus.
module invert_bus (a[4]) -> (n[4])
gate NOT invert (a) -> n
end
wire x[4]
wire n[4]
gate XOR combine (a,b) -> x
instance invert_bus inv (x) -> (n)
A flip-flop clock can be a module input port. Connect that port to a top-level clock signal. Modules may contain instances of other modules. A circular chain of instances is an error. An instance that names a missing module is an error.
Store reusable module definitions in a separate file. A library file holds only module definitions. It cannot hold inputs, outputs, wires, gates, or instances.
# halfadd.net
module halfadd (a,b) -> (sum,carry)
gate XOR xsum (a,b) -> sum
gate AND carry (a,b) -> carry
end
Load the library when you run the tool.
cabal run gatework -- --netlist top.net --library halfadd.netRepeat --library for more libraries.
A library module can use modules from another library.
A module name cannot appear in more than one file.
The main netlist cannot reuse a module name from a library.
An error names the library file that caused it.
The test suite has two parts. Deterministic tests cover parsing, validation, counter progression, adder carry propagation, scheduled inputs, reset behavior, register width, assertions, and golden VCD output. Deterministic tests also cover NAND, NOR, and XNOR truth tables, module flattening, nested instances, port validation, and circular instantiation. Deterministic tests also cover undefined and floating values, tri-state buffers, and their golden output. Deterministic tests also cover bus declarations, bitwise gates, bit references, slices, bus registers, bus assertions, bus module ports, and error cases. Deterministic tests also cover multi-driver resolution, scheduled driver changes, flip-flop output exclusivity, the shared-bus fixture, and its golden output. Deterministic tests also cover module library loading, cross-library module references, duplicate module names, and library file validation. Deterministic tests also cover the report command, its header order, its counter table, and its golden output. Deterministic tests also cover whole-bus input values, their bit order, their error cases, scheduled whole-bus transitions, and their golden output. Deterministic tests also cover VCD vectors, their header declarations, their grouped values, four-state vector values, and module-internal bus vectors. Deterministic tests also cover gate delay parsing and invalid delay fields. Deterministic tests also cover delayed transitions, delay accumulation, zero-delay behavior, multi-driver delays, and the initial settle. Deterministic tests also cover rise and fall delay parsing, conflict rules, direction-specific transitions, and the asymmetric delay demo. Deterministic tests also cover clock-to-output delay parsing, invalid tco fields, delayed commits, edge capture, delayed reset, wide register commits, and the golden output. Deterministic tests also cover BUF and MUX truth values, bus behavior, delayed paths, and their golden outputs. QuickCheck properties cover gate algebra, full adder correctness, scheduled input sampling, reset sampling, register width, and assertion soundness. QuickCheck properties also compare the hierarchical adder and counter with their flat versions. QuickCheck properties also cover the four-state model and the tri-state buffer truth table. QuickCheck properties also compare a bus circuit with a bitwise reference model. QuickCheck properties also compare the shared bus with a per-time resolution model. QuickCheck properties also compare a library adder with its flat version. QuickCheck properties also compare a delayed gate with a per-time reference model. QuickCheck properties also compare an asymmetric delayed gate with a per-time reference model. QuickCheck properties also compare a clock-to-output flip-flop with a delayed reference model. QuickCheck properties also compare the counter report with the simulated waveform. QuickCheck properties also compare whole-bus input values with per-bit reference values. QuickCheck properties also compare every VCD vector value with the per-bit waveform. QuickCheck properties also cover BUF behavior and MUX selection across all four logic values.
QuickCheck runs one hundred random cases for each property. The gate properties cover the complete truth table. The adder property compares the simulation with integer addition over random four-bit values. The scheduled-input property compares each sampled flip-flop value with a reference model. The reset property compares the waveform with a reference model of reset and clock events. The width property compares each register bit with a per-bit reference model. The entry-point property shows that both simulation entry points produce identical output. The assertion property shows that real values pass and inverted values fail. The hierarchy property shows that the hierarchical circuits match the flat circuits. The four-state property shows that the gates keep their two-state behavior for known inputs. The tri-state buffer property shows that an enabled driver passes data and a disabled driver floats. The BUF property shows that direct transfer preserves known values and maps floating input to unknown. The MUX property shows that an unknown selector passes equal branches and rejects different branches. The bus XOR property compares a bus gate with per-bit evaluation. The bus register property compares each register bit with a reference value. The bus hierarchy property shows that a bus module matches its flat circuit. The shared-bus property compares each resolved wire value with a per-time model. The library adder property compares a library netlist with the flat adder. The counter report property compares each report cell with the simulated value. The VCD vector property compares each vector value with the per-bit values at that time. The delayed-gate property compares each output sample with the reference input time. The asymmetric-delay property compares each output sample with the directional reference.
The previous release passed on GHC 9.6.7 with Cabal 3.14 in the bundled container. This workspace could not run Cabal because the executable is unavailable. The CI workflow runs the checks on Ubuntu with GHC 9.6.6. Golden tests compare each fixture VCD with its golden file. The golden report test compares the counter report table with its golden file. CI runs every demo and compares its output with the golden file. CI runs the report demo and compares it with the report golden file. CI runs the bus demo with whole-bus input values. CI runs the four-state vector demo and compares it with its golden file. CI runs the gate delay demo and compares it with its golden file. CI runs the asymmetric delay demo and compares it with its golden file. CI runs the clock-to-output delay demo and compares it with its golden file. CI runs the buffer demo and compares it with its golden file. CI runs the MUX demo and compares it with its golden file. CI confirms that a missing library file stops the run. CI confirms that an invalid gate delay stops the run.
The simulator uses four logic values: low, high, unknown, and floating.
A floating value reads as unknown inside a gate.
The BUF gate preserves low, high, and unknown values.
The MUX gate returns x when an unknown selector chooses different branches.
Several gates can drive one wire.
The simulator resolves the driver values into one wire value.
Resolution treats z as neutral and known values as dominant.
A low and a high together give x.
A flip-flop output stays exclusive to its flip-flop.
A gate cannot drive a flip-flop output.
Combinational loops stop with an event-limit error.
A gate without a delay field uses zero delay.
A gate delay applies to both rising and falling transitions.
A transition to high uses the rise delay.
Every other transition uses the fall delay.
The delay= field sets both delays to one value.
The delay= field cannot combine with rise= or fall=.
The initial state settles at time zero without delay.
Events at time zero ignore the gate delay.
A flip-flop captures data at the clock edge.
The captured value commits after the clock-to-output delay.
The clock-to-output delay applies to an asserted reset too.
The initial flip-flop value settles at time zero without delay.
The tco= field cannot repeat on one flip-flop.
A delayed transition uses the input value at its fire time.
A later input change supersedes an earlier pending transition.
Zero-delay gates can show combinational settling transients at clock edges and at time zero.
Input changes apply only at scheduled times.
They do not react to circuit state.
VCD output uses one module scope.
A bus renders as one multi-bit vector.
A scalar signal renders as one bit.
An asynchronous reset that releases on a clock edge is a race.
The event order decides the result.
A wide flip-flop uses one shared reset signal.
Assertions use the settled value at each time.
An assertion time beyond the run duration is an error.
A netlist file can define modules inline or load them from library files.
A library file holds only module definitions.
A library file cannot hold top-level declarations.
The main netlist cannot shadow a library module.
Two library files cannot define the same module.
Modules flatten before simulation, so the VCD stays flat.
A bus expands into single-bit signals internally.
The VCD writer groups the bits into one vector again.
A flip-flop bus output must have a wire or output declaration.
A reference to a whole bus uses the declared width.
An assertion addresses one bit, not a whole bus.
One module declaration cannot live inside another.
An instance output must connect to a declared signal.
The dotted instance names are part of the VCD signal names.
The report prints one row per change time.
The report uses the settled value at each time.
The report prints every signal in the stable signal order.
Release 0.17.0.0 completed the MUX gate and its waveform evidence. Release 0.16.0.0 completed the BUF gate and its waveform evidence. Release 0.15.0.0 completed the clock-to-output delay for flip-flops. Release 0.14.0.0 completed separate rise and fall gate delays. Release 0.13.0.0 completed configurable gate delays. Release 0.12.0.0 completed multi-bit values in the VCD timeline. Release 0.11.0.0 completed whole-bus input values on the command line. Release 0.10.0.0 completed the waveform report command. Release 0.9.0.0 completed module libraries. Release 0.8.0.0 completed multi-driver wire resolution. Release 0.7.0.0 completed multi-bit buses, bit references, slices, and bus module ports. Release 0.6.0.0 completed undefined and floating logic values and tri-state buffers. Release 0.5.0.0 completed universal gates and hierarchical netlists. Release 0.4.0.0 completed waveform assertions and VCD comment metadata. Release 0.3.0.0 completed reset pins and parameterized flip-flops. Release 0.2.0.0 completed scheduled input transitions.
Remaining work:
- Expand the gate library with additional circuit primitives.
Gatework uses the BSD 3-Clause License.