This site is version 0.0.1. Most of the code on this site is drafted by an agent and needs review. I apologize for the AI-speak, we are working on rewriting it!
viv-lang.org
Python Viv

A four-bit adder from NAND gates, simulated exhaustively

Build a full adder out of nine NAND gates, chain four of them into a ripple-carry adder, and check it on all 512 combinations of two four-bit numbers and a carry in. The gate logic and the schematic's gate outlines are Viv (gallery/hardware/adder.viv, which draws with lib/gates.viv), loaded with vp.load; the simulation also counts gate delays, which shows the carry rippling from bit to bit.

One full adder

A full adder adds three bits: a, b and the carry in. Its sum is a xor b xor c and its carry out is ab or c(a xor b). The schematic shows that logic as two XOR gates, two AND gates and an OR; adder.viv builds each from NAND gates, nine in all, since NAND alone can make every gate. The truth table is all eight inputs.

import numpy as np
import vipyr as vp
from vipyr.figure import figure

hw = vp.load("gallery/hardware/adder.viv")
print(" a b c | sum carry")
for k in range(8):
    a, b, c = (k >> 2) & 1, (k >> 1) & 1, k & 1
    s, co = hw.full_adder(float(a), float(b), float(c))
    print(" %d %d %d |  %d    %d" % (a, b, c, s, co))

AND, OR, XOR = 0, 1, 2
fig = figure("A full adder: the gates", "", "", window=(-0.6, 7.6, -0.4, 4.4))
fig.aspect("equal")
gates = {
    "xor 1": (XOR, 1.5, 3.2),
    "xor 2": (XOR, 4.0, 3.0),
    "and 1": (AND, 1.5, 1.4),
    "and 2": (AND, 4.0, 1.9),
    "or": (OR, 5.8, 1.1),
}
w, h = 1.0, 0.8
for name, (kind, x, y) in gates.items():
    fig.plot(hw.gate_x(kind, x, y, w, h), hw.gate_y(kind, x, y, w, h), "gates")
wires = [
    ((0.0, 3.4), (1.5, 3.4)),
    ((0.0, 3.0), (1.5, 3.0)),  # a, b into xor 1
    ((0.0, 1.6), (1.5, 1.6)),
    ((0.0, 1.2), (1.5, 1.2)),  # a, b into and 1
    ((2.5, 3.2), (4.0, 3.2)),
    ((0.0, 2.5), (3.6, 2.5)),  # a xor b, c into xor 2
    ((3.6, 2.5), (3.6, 2.8)),
    ((3.6, 2.8), (4.0, 2.8)),
    ((3.2, 3.2), (3.2, 2.1)),
    ((3.2, 2.1), (4.0, 2.1)),  # a xor b into and 2
    ((0.6, 2.5), (0.6, 1.7)),
    ((0.6, 1.7), (4.0, 1.7)),  # c into and 2
    ((5.0, 1.9), (5.4, 1.9)),
    ((5.4, 1.9), (5.4, 1.3)),
    ((5.4, 1.3), (5.85, 1.3)),
    ((2.5, 1.4), (5.4, 1.4)),
    ((5.4, 1.4), (5.4, 0.9)),
    ((5.4, 0.9), (5.85, 0.9)),
    ((5.0, 3.0), (7.2, 3.0)),
    ((6.8, 1.1), (7.2, 1.1)),
]
xs, ys = [], []
for (x0, y0), (x1, y1) in wires:
    xs += [x0, x1, float("nan")]
    ys += [y0, y1, float("nan")]
fig.plot(xs, ys, "wires")
fig.style("gates", "s1 thick nomarker")
fig.style("wires", "s2 thin nomarker")
for text, x, y in (
    ("a", -0.45, 3.3),
    ("b", -0.45, 2.9),
    ("c", -0.45, 2.4),
    ("a", -0.45, 1.5),
    ("b", -0.45, 1.1),
    ("sum", 7.25, 2.95),
    ("carry", 7.25, 1.05),
):
    fig.label("%s %.1f" % (text, y), text, x, y)
 a b c | sum carry
 0 0 0 |  0    0
 0 0 1 |  1    0
 0 1 0 |  1    0
 0 1 1 |  0    1
 1 0 0 |  1    0
 1 0 1 |  0    1
 1 1 0 |  0    1
 1 1 1 |  1    1
A full adder: the gatesgateswires012340246abcabsumcarry

All 512 additions

add4(a, b, cin) runs the four full adders on every pair at once and returns the five-bit result. The maps show it for carry in 0: the sum bits (a + b mod 16) and the carry out, with a across and b down.

a, b = np.meshgrid(np.arange(16.0), np.arange(16.0))
wrong = 0
for cin in (0.0, 1.0):
    got = hw.add4(a.ravel(), b.ravel(), cin)
    wrong += int(np.sum(got != (a + b + cin).ravel()))
    if cin == 0.0:
        res = got.reshape(16, 16)
print("additions checked: %d, wrong: %d" % (2 * 256, wrong))
print(
    "7 + 9 = %d, 15 + 15 + 1 = %d"
    % (res[9, 7], hw.add4(np.array([15.0]), np.array([15.0]), 1.0)[0])
)
fig = figure("Sum bits, a + b mod 16", "a", "b")
fig.heatmap(res % 16, "sum")
fig = figure("Carry out", "a", "b")
fig.heatmap(np.floor(res / 16), "carry")
additions checked: 512, wrong: 0
7 + 9 = 16, 15 + 15 + 1 = 31
Sum bits, a + b mod 16051015051015ab051015
Carry out051015051015ab00.51.0

The carry ripples

Each NAND output settles one gate delay after its later input. With every input present at time 0, settle_times returns when each output bit is final. Every full adder has to wait for the carry from the one below, so the delay grows by two gates per bit: s_i settles after 6 + 2i delays, so the top sum bit of a 32-bit ripple adder would need 68. Carry-lookahead adders exist to shorten that chain.

t = hw.settle_times()
names = ["s0", "s1", "s2", "s3", "carry out"]
for n, v in zip(names, t):
    print("%-9s settles after %2d gate delays" % (n, v))
fig = figure("When each output settles", "", "gate delays")
fig.bars(np.arange(5), t, "settle time")
for k, n in enumerate(names):
    fig.xtick(n, k)
s0        settles after  6 gate delays
s1        settles after  8 gate delays
s2        settles after 10 gate delays
s3        settles after 12 gate delays
carry out settles after 11 gate delays
When each output settlessettle time0510s0s1s2s3carry outgate delays

Source

gallery/hardware/plot_four_bit_adder.py, run as written when these pages were built. The output and the figures above are from that run.

The Viv program it calls is posts/2026-09-22-building-a-four-bit-adder-out-of-nand-gates.viv: its atlas, the defs and the calls between them, or open it in the workbench.