Gallery

A collection of interactive circuit examples in three tiers: an introduction that adds one language feature at a time, the building blocks those primitives compose into, and advanced circuits where width, state, and macros meet.

Introduction

One idea each: gates and wiring, the built-in macros, and the bit-shape syntax.

NOT chain

Three inverters in series. The output is the inverse of `a`, and the chain still compiles and simulates faithfully.

source
input a
not n1(in=a)
not n2(in=n1.out)
not n3(in=n2.out)
output out(in=n3.out)
circ-compile --preview
╭───╮     ╭───╮     ╭───╮     ╭───╮     ╭─────╮
│ a ├○───▶┤NOT├○───▶┤NOT├○───▶┤NOT├○───▶┤ out │
╰───╯     ╰───╯     ╰───╯     ╰───╯     ╰─────╯
Open in playground →
live simulation click input pins to toggle

One input, three destinations

A single input drives three independent NOT gates. The `●` glyphs in the ASCII preview mark fan-out taps, where the circuit reuses one wire.

source
input a
not n1(in=a)
not n2(in=a)
not n3(in=a)
output o1(in=n1.out)
output o2(in=n2.out)
output o3(in=n3.out)
circ-compile --preview
╭───╮     ╭───╮     ╭────╮
│ a ├●●──▶┤NOT├○───▶┤ o1 │
╰───╯ │   ╰───╯     ╰────╯
      │
      │   ╭───╮     ╭────╮
      ●──▶┤NOT├○───▶┤ o2 │
      │   ╰───╯     ╰────╯
      │
      │   ╭───╮     ╭────╮
      ╰──▶┤NOT├○───▶┤ o3 │
          ╰───╯     ╰────╯
Open in playground →
live simulation click input pins to toggle

A built-in macro

The five built-in macros — or, nand, nor, xor, xnor — expand to primitives at compile time, and a single file needs no import to use one.

source
input a, b
xor g(a=a, b=b)
output out(in=g.out)
circ-compile --preview
╭───╮     ╭───────╮
│ a ├○───▶┤       │     ╭─────╮
╰───╯     │[xor:g]├○───▶┤ out │
      ╭──▶┤       │     ╰─────╯
╭───╮ │   ╰───────╯
│ b ├○╯
╰───╯
Open in playground →
live simulation click input pins to toggle

Slicing and joining a bus

Takes a 4-bit input apart with a[0..2] and a[2..4], then puts it back together with a concat that reconstructs the original.

source
input[4] a
output[4] o(in={a[0..2], a[2..4]})
circ-compile --preview
╭──────╮     ╭──────╮
│ a[4] ├○───▶┤ o[4] │
╰──────╯     ╰──────╯
Open in playground →
live simulation click input pins to toggle

One gate across eight bits

A primitive written not[8] inverts a whole bus at once, so the width lives on the gate rather than in eight copies of it.

source
input[8] a
not[8] inv(in=a)
output[8] o(in=inv.out)
circ-compile --preview
╭──────╮     ╭───╮     ╭──────╮
│ a[8] ├○───▶┤NOT├○───▶┤ o[8] │
╰──────╯     ╰───╯     ╰──────╯
Open in playground →
live simulation click input pins to toggle

Building blocks

The standard combinational parts, each built from the primitives above.

Half-adder

`sum = a XOR b`, `carry = a AND b`. The simplest circuit that does arithmetic. Click "Run": the `xor` macro expands into the gates you can see in the live canvas.

source
import xor "<builtin>/xor.circ"
input a, b
xor s(a=a, b=b)
and c(a=a, b=b)
output sum(in=s.out)
output carry(in=c.out)
circ-compile --preview
╭───╮     ╭───╮
│ a ├○●──▶┤   │         ╭───────╮
╰───╯ │   │AND├○───────▶┤ carry │
      │╭─▶┤   │         ╰───────╯
╭───╮ ││  ╰───╯
│ b ├○┼╯
╰───╯ ││  ╭───────╮
      ╰┼─▶┤       │     ╭─────╮
       │  │[xor:s]├○───▶┤ sum │
       ╰─▶┤       │     ╰─────╯
          ╰───────╯
Open in playground →
live simulation click input pins to toggle

2-to-1 multiplexer

`out = sel ? b : a`. Built from two ANDs that route the picked input through, an inverter for the selector, and an OR that combines them.

source
// 2-to-1 multiplexer: out = sel ? b : a
// When sel=0 the gate routes 'a' through; when sel=1 it routes 'b'.
import or "<builtin>/or.circ"
input a, b, sel
not sel_inv(in=sel)
and pick_a(a=sel_inv.out, b=a)
and pick_b(a=sel, b=b)
or out_or(a=pick_a.out, b=pick_b.out)
output out(in=out_or.out)
circ-compile --preview
╭───╮
│ a ├○──╮
╰───╯   │
        │
╭─────╮ │   ╭───╮     ╭───╮
│ sel ├○┼●─▶┤NOT├○───▶┤   │     ╭───────────╮
╰─────╯ ││  ╰───╯     │AND├○───▶┤           │     ╭─────╮
        ╰┼───────────▶┤   │     │[or:out_or]├○───▶┤ out │
         │  ╭───╮     ╰───╯ ╭──▶┤           │     ╰─────╯
         ╰─▶┤   │           │   ╰───────────╯
╭───╮       │AND├○──────────╯
│ b ├○─────▶┤   │
╰───╯       ╰───╯
Open in playground →
live simulation click input pins to toggle

1-to-2 demultiplexer

The mux read backwards: the circuit routes one input to whichever output sel names and holds the other at 0.

source
// 1-to-2 demultiplexer: routes `in` to `out_a` when sel=0, to `out_b` when sel=1.
// The unselected output is held at 0.
input in, sel
not sel_inv(in=sel)
and route_a(a=in, b=sel_inv.out)
and route_b(a=in, b=sel)
output out_a(in=route_a.out)
output out_b(in=route_b.out)
circ-compile --preview
╭────╮                ╭───╮
│ in ├○─●────────────▶┤   │     ╭───────╮
╰────╯  │             │AND├○───▶┤ out_a │
        │         ╭──▶┤   │     ╰───────╯
╭─────╮ │   ╭───╮ │   ╰───╯
│ sel ├○┼●─▶┤NOT├○╯
╰─────╯ ││  ╰───╯
        ││
        ││  ╭───╮
        ╰┼─▶┤   │               ╭───────╮
         │  │AND├○─────────────▶┤ out_b │
         ╰─▶┤   │               ╰───────╯
            ╰───╯
Open in playground →
live simulation click input pins to toggle

Full-adder

Two XORs, two ANDs, one OR. Adds three bits (a, b, cin) into a sum bit and a carry-out.

source
import xor "<builtin>/xor.circ"
import or  "<builtin>/or.circ"
input a, b, cin
xor s1(a=a, b=b)
xor s2(a=s1.out, b=cin)
and c1(a=a, b=b)
and c2(a=s1.out, b=cin)
or  c3(a=c1.out, b=c2.out)
output sum (in=s2.out)
output cout(in=c3.out)
circ-compile --preview
╭───╮       ╭───╮
│ a ├○──●──▶┤   │                         ╭───────╮
╰───╯   │   │AND├○───────────────────────▶┤       │     ╭──────╮
        │╭─▶┤   │                         │[or:c3]├○───▶┤ cout │
╭───╮   ││  ╰───╯                     ╭──▶┤       │     ╰──────╯
│ b ├○──┼╯                            │   ╰───────╯
╰───╯   ││  ╭────────╮                │
        ╰┼─▶┤        │     ╭───╮      │
╭─────╮  │  │[xor:s1]├○●──▶┤   │      │
│ cin ├○╮╰─▶┤        │ │   │AND├○─────╯
╰─────╯ │   ╰────────╯ │╭─▶┤   │
        ●──────────────┼╯  ╰───╯
        │              │
        │              │   ╭────────╮
        │              ╰──▶┤        │                   ╭─────╮
        │                  │[xor:s2]├○─────────────────▶┤ sum │
        ╰─────────────────▶┤        │                   ╰─────╯
                           ╰────────╯
Open in playground →
live simulation click input pins to toggle

A ROM the host loads

A rom is a compile-time shape and a run-time image: the circuit declares rom code[8, 4], and the sixteen bytes below fill it before you touch anything. This one holds the squares, so driving pc to 12 reads 144 back out.

source
input[4] pc
rom code[8, 4](addr = pc.out)
output[8] out(in = code.out)
circ-compile --preview
╭───────╮     ╭───────────────╮     ╭────────╮
│ pc[4] ├○───▶┤ rom code[8,4] ├○───▶┤ out[8] │
╰───────╯     ╰───────────────╯     ╰────────╯
Open in playground →
live simulation click input pins to toggle

Advanced

Where width, state, and macros meet — multi-bit arithmetic, feedback, and memory.

2-bit ripple-carry adder

The operands are buses, so the truth table reads as arithmetic rather than as four loose bits. A ripple-carry adder still takes them apart: the carry is the one part of an addition that has to happen in sequence.

source
import xor "<builtin>/xor.circ"
import or  "<builtin>/or.circ"

// Two-bit operands and a two-bit sum, plus the carry that falls off the top.
input[2] a, b

// A bus has to be taken apart to reach a single lane. Each slice goes
// through a wire, which is what gives the lane a name to wire from.
wire a0(in=a[0])
wire a1(in=a[1])
wire b0(in=b[0])
wire b1(in=b[1])

// Bit 0 is a half adder: nothing carries into it.
xor s0(a=a0.out, b=b0.out)
and c0(a=a0.out, b=b0.out)

// Bit 1 is a full adder fed by that carry.
xor p1(a=a1.out, b=b1.out)
xor s1(a=p1.out, b=c0.out)
and g1(a=a1.out, b=b1.out)
and t1(a=p1.out, b=c0.out)
or  c1(a=g1.out, b=t1.out)

// Low bit first, which is how a concat reads.
output[2] s(in={s0.out, s1.out})
output cout(in=c1.out)
circ-compile --preview
╭──────╮     ╭───╮
│ a[2] ├●●──▶┤   │                         ╭───────╮
╰──────╯ │   │AND├○───────────────────────▶┤       │     ╭──────╮
         │╭─▶┤   │                         │[or:c1]├○───▶┤ cout │
╭──────╮ ││  ╰───╯                     ╭──▶┤       │     ╰──────╯
│ b[2] ├●┼╯                            │   ╰───────╯
╰──────╯ ││  ╭────────╮                │
         ●┼─▶┤        │     ╭───╮      │
         ││  │[xor:p1]├○─●─▶┤   │      │
         │●─▶┤        │  │  │AND├○─────╯
         ││  ╰────────╯ ╭┼─▶┤   │
         ││             ││  ╰───╯
         ││  ╭───╮      ││
         ●┼─▶┤   │      ││  ╭────────╮
         ││  │AND├○─────╯╰─▶┤        │                   ╭──────╮
         │●─▶┤   │      │   │[xor:s1]├○─────────────────▶┤ s[2] │
         ││  ╰───╯      ╰──▶┤        │                   ╰──────╯
         ││                 ╰────────╯
         ││  ╭────────╮
         ╰┼─▶┤        │
          │  │[xor:s0]├
          ╰─▶┤        │
             ╰────────╯
Open in playground →
live simulation click input pins to toggle

4-bit ripple-carry adder

The same adder twice as wide, and visibly the same shape: four lanes off a pair of buses, each carrying into the next. 256 rows, and the sum is one column rather than four.

source
import xor "<builtin>/xor.circ"
import or  "<builtin>/or.circ"

// Four-bit operands and a four-bit sum, plus the carry that falls off the top.
input[4] a, b

// A bus has to be taken apart to reach a single lane. Each slice goes
// through a wire, which is what gives the lane a name to wire from.
wire a0(in=a[0])
wire a1(in=a[1])
wire a2(in=a[2])
wire a3(in=a[3])
wire b0(in=b[0])
wire b1(in=b[1])
wire b2(in=b[2])
wire b3(in=b[3])

// Bit 0 is a half adder: nothing carries into it.
xor s0(a=a0.out, b=b0.out)
and c0(a=a0.out, b=b0.out)

// Bits 1 to 3 are full adders, each fed by the carry below it. Every one is
// the same five gates: propagate, sum, generate, this bit's carry, and the
// or that lets either source of a carry through.
xor p1(a=a1.out, b=b1.out)
xor s1(a=p1.out, b=c0.out)
and g1(a=a1.out, b=b1.out)
and t1(a=p1.out, b=c0.out)
or  c1(a=g1.out, b=t1.out)

xor p2(a=a2.out, b=b2.out)
xor s2(a=p2.out, b=c1.out)
and g2(a=a2.out, b=b2.out)
and t2(a=p2.out, b=c1.out)
or  c2(a=g2.out, b=t2.out)

xor p3(a=a3.out, b=b3.out)
xor s3(a=p3.out, b=c2.out)
and g3(a=a3.out, b=b3.out)
and t3(a=p3.out, b=c2.out)
or  c3(a=g3.out, b=t3.out)

// Low bit first, which is how a concat reads.
output[4] s(in={s0.out, s1.out, s2.out, s3.out})
output cout(in=c3.out)
circ-compile --preview
╭──────╮     ╭───╮
│ a[4] ├●●──▶┤   │
╰──────╯ │   │AND├○─────╮
         │╭─▶┤   │      │
╭──────╮ ││  ╰───╯      │
│ b[4] ├●┼╯             │
╰──────╯ ││  ╭────────╮ │
         ●┼─▶┤        │ │
         ││  │[xor:s0]├ │
         │●─▶┤        │ │
         ││  ╰────────╯ │
         ││             │
         ││  ╭───╮      │
         ●┼─▶┤   │      │
         ││  │AND├○─────┼╮
         │●─▶┤   │      ││
         ││  ╰───╯      ││
         ││             ││
         ││  ╭────────╮ ││
         ●┼─▶┤        │ ││                                                            ╭────────╮
         ││  │[xor:p3]├○┼┼●──────────────────────────────────────────────────────────▶┤        │                   ╭──────╮
         │●─▶┤        │ ╰┼┼────────────────────────────────────────────────────────╮  │[xor:s3]├○─────────────────▶┤ s[4] │
         ││  ╰────────╯  ╰┼─────────────────────────────────────────╮   ╭───────╮ ╭┼─▶┤        │                   ╰──────╯
         ││               │                                         ╰──▶┤       │ ││  ╰────────╯     ╭───────╮
         ││  ╭───╮        │                                             │[or:c2]├○╮╰────────────────▶┤       │     ╭──────╮
         ●┼─▶┤   │        │                                         ╭──▶┤       │ │                  │[or:c3]├○───▶┤ cout │
         ││  │AND├○─────╮ │                                         │   ╰───────╯ │              ╭──▶┤       │     ╰──────╯
         │●─▶┤   │      │ │                                         │             │              │   ╰───────╯
         ││  ╰───╯      │ │                                         │             │              │
         ││             │ │                                         │             │              │
         ││  ╭────────╮ │ │                                         │             │              │
         ●┼─▶┤        │ │ │                              ╭───╮      │             │              │
         ││  │[xor:p2]├○┼●┼─────────────────────────────▶┤   │      │             │              │
         │●─▶┤        │ ││╰───────────────────────────╮  │AND├○─────╯             │              │
         ││  ╰────────╯ ╰┼───────────────╮ ╭───────╮ ╭┼─▶┤   │                    │              │
         ││              ╰──────────────╮╰▶┤       │ ││  ╰───╯                    │   ╭───╮      │
         ││  ╭────────╮                 │  │[or:c1]├○╯╰───────────────────────────┼──▶┤   │      │
         ●┼─▶┤        │     ╭───╮      ╭┼─▶┤       │ │                            │   │AND├○─────╯
         ││  │[xor:p1]├○─●─▶┤   │      ││  ╰───────╯ │   ╭────────╮               ╰──▶┤   │
         │●─▶┤        │  │  │AND├○─────╯╰────────────┼──▶┤        │                   ╰───╯
         ││  ╰────────╯ ╭┼─▶┤   │                    │   │[xor:s2]├
         ││             ││  ╰───╯                    ╰──▶┤        │
         ││  ╭───╮      ││                               ╰────────╯
         ╰┼─▶┤   │      ││  ╭────────╮
          │  │AND├○─────╯╰─▶┤        │
          ╰─▶┤   │      │   │[xor:s1]├
             ╰───╯      ╰──▶┤        │
                            ╰────────╯
Open in playground →
live simulation click input pins to toggle

SR latch

Two cross-coupled NOR cells, each a pair of NOTs feeding an AND (De Morgan), with feedback inverters closing the loop. Drive `s` to set `q`, `r` to reset it; the cycle passes through gates, so it compiles as sequential logic.

source
// SR-latch built from cross-coupled NOR gates.
//   Q    = NOR(R, Qbar)
//   Qbar = NOR(S, Q)
//
// NOR(a, b) = NOT(a OR b) = NOT a AND NOT b   (De Morgan)
// so each NOR is one AND and two NOTs.
input s, r

not nr(in=r)
not ns(in=s)

// Cross-coupled cells. Forward references are fine — names resolve globally.
and qcell(a=nr.out, b=nqbar.out)
and qbcell(a=ns.out, b=nq.out)

// Feedback inverters that close the loop.
not nq(in=qcell.out)
not nqbar(in=qbcell.out)

output q(in=qcell.out)
output qbar(in=qbcell.out)
circ-compile --preview
╭───╮     ╭───╮                         ╭───╮
│ r ├○───▶┤NOT├○───────────────────────▶┤   │     ╭───╮
╰───╯     ╰───╯                         │AND├○●──▶┤NOT├○───╮
                                    ╭──▶┤   │ │   ╰───╯    │
╭───╮     ╭───╮     ╭───╮           │   ╰───╯ │            │
│ s ├○───▶┤NOT├○───▶┤   │     ╭───╮ │         │   ╭───╮    │
╰───╯     ╰───╯     │AND├○●──▶┤NOT├○╯         ╰──▶┤ q │    │
                ╭──▶┤   │ │   ╰───╯               ╰───╯    │
                │   ╰───╯ ╰───────────────────╮            │
                │                             │   ╭──────╮ │
                │                             ╰──▶┤ qbar │ │
                │                                 ╰──────╯ │
                ╰──────────────────────────────────────────╯
Open in playground →
live simulation click input pins to toggle

A RAM with a clock

Single-port RAM: the write commits on a rising clock edge when we is high, so this one needs the simulator rather than a truth table. It starts loaded with a0 through af, so every cell says its own address until you overwrite one.

source
input[4] a
input[8] d
input we, clk
ram data[8, 4](addr = a.out, din = d.out, we = we.out, clk = clk.out)
output[8] q(in = data.out)
circ-compile --preview
╭──────╮     ╭───────────────╮
│ a[4] ├○───▶┤               │
╰──────╯     │               │
         ╭──▶┤               │     ╭──────╮
╭──────╮ │   │ ram data[8,4] ├○───▶┤ q[8] │
│ d[8] ├○╯╭─▶┤               │     ╰──────╯
╰──────╯  │  │               │
         ╭┼─▶┤               │
╭────╮   ││  ╰───────────────╯
│ we ├○──┼╯
╰────╯   │
         │
╭─────╮  │
│ clk ├○─╯
╰─────╯
Open in playground →
live simulation click input pins to toggle