A short tour

Each step adds one new idea on top of the previous one. By the end you'll have read enough circ to feel at home in the reference. Source on the left, the --preview output on the right.

Step 1. A single NOT gate

The smallest possible circuit. One input pin `a`, one inverter, one output pin. Every program is a flat list of declarations: name a thing, then wire its ports. The `.out` suffix on a primitive's instance name is its single implicit output port.

source
input a
not n(in=a)
output out(in=n.out)
circ-compile --preview
╭───╮     ╭───╮     ╭─────╮
│ a ├○───▶┤NOT├○───▶┤ out │
╰───╯     ╰───╯     ╰─────╯

Step 2. AND of two inputs

Two inputs, named `a` and `b`, feed an AND gate. The gate has two input ports — `a` and `b` — that you bind by name in the parenthesised list. The output of the gate goes to a single output pin named `out`.

source
input a, b
and g(a=a, b=b)
output out(in=g.out)
circ-compile --preview
╭───╮     ╭───╮     ╭─────╮
│ a ├○───▶┤   │ ╭──▶┤ out │
╰───╯     │AND├○╯   ╰─────╯
       ╭─▶┤   │
       │  ╰───╯
       │
╭───╮  │
│ b ├○─╯
╰───╯                      

Step 3. Naming an intermediate signal

A `wire` is a one-port pass-through: the value on `in` is, after evaluation, exactly the value on `out`. It exists so you can give a derived signal a name. Without `clock_buf` here, `gate.a` would be wired directly to `clk` — same logic, less self-documenting.

source
input clk, data
wire clock_buf(in=clk)
and gate(a=clock_buf.out, b=data)
output out(in=gate.out)
circ-compile --preview
╭─────╮      ╭───╮     ╭─────╮
│ clk ├○────▶┤   │ ╭──▶┤ out │
╰─────╯      │AND├○╯   ╰─────╯
         ╭──▶┤   │
         │   ╰───╯
         │
╭──────╮ │
│ data ├○╯
╰──────╯                    

Step 4. Anonymous nested components

A component can be instantiated inline as the value of a port. The inverter here has no instance name; its `.out` is wired immediately into `gate1`'s `b` port. The same wiring rules apply — anonymous nesting is just syntactic sugar for declaring an unnamed component.

source
input a
input b
not inv(in=b)
and gate1(a=a, b=inv.out)
output out(in=gate1.out)
circ-compile --preview
╭───╮     ╭───╮     ╭───╮     ╭─────╮
│ a ├○┬──▶┤NOT├○╮ ╭▶┤   │ ╭──▶┤ out │
╰───╯ │   ╰───╯ │ │ │AND├○╯   ╰─────╯
      ├─────────┴─┴▶┤   │
      │             ╰───╯
      │
╭───╮ │
│ b ├○╯
╰───╯                                

Step 5. A half-adder

Two single-bit numbers `a` and `b` sum to `(carry, sum)` where `sum = a XOR b` and `carry = a AND b`. The `xor` keyword is a built-in macro that expands to primitives at compile time. In a single-file program you import macros explicitly; this is the classic Nand2Tetris milestone, eight lines of source.

source
// half_adder.circ
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 ├○─●─▶┤   │ ╭──────▶┤ carry │
╰───╯  │  │AND├○╯       ╰───────╯
      ╭┼─▶┤   │
      ││  ╰───╯
      ││
╭───╮ ││  ╭───────╮     ╭─────╮
│ b ├○●╰─▶┤       │ ╭──▶┤ sum │
╰───╯ │   │[xor:s]├○╯   ╰─────╯
      ╰──▶┤       │
          ╰───────╯

Step 6. A full-adder, by importing the half-adder

Imports glue files together. The root file declares a `half_adder` alias and uses it twice — once for the low-bit sum, once to fold in the carry-in. The carry-out is the OR of the two intermediate carries. The half-adder's ports (`a`, `b`, `sum`, `carry`) are exactly its `input` and `output` declarations. Notice in the preview how the half-adders show up as `[half_adder:ha1]` and `[half_adder:ha2]` boxes — opaque sub-circuits, just like the built-in macros.

source
// half_adder.circ
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)

// root.circ
import half_adder "half_adder.circ"
input a, b, cin
half_adder ha1(a=a, b=b)
half_adder ha2(a=ha1.sum, b=cin)
or cout_or(a=ha1.carry, b=ha2.carry)
output sum(in=ha2.sum)
output cout(in=cout_or.out)
circ-compile --preview
╭───╮       ╭────────────────╮     ╭────────────────╮     ╭────────────╮     ╭─────╮
│ a ├○─────▶┤                │ ╭──▶┤                │   ╭▶┤            │   ╭▶┤ sum │
╰───╯       │[half_adder:ha1]├○●   │[half_adder:ha2]├○● │ │[or:cout_or]├○╮ │ ╰─────╯
       ╭───▶┤                │ │ ╭▶┤                │ ●─┼▶┤            │ │ │
       │    ╰────────────────╯ │ │ ╰────────────────╯ │ │ ╰────────────╯ │ │
       │                       ╰─┼────────────────────┼─╯                │ │
╭───╮  │                         │                    │                  │ │ ╭──────╮
│ b ├○─╯                         │                    ╰──────────────────┴─┴▶┤ cout │
╰───╯                            │                                           ╰──────╯
                                 │
╭─────╮                          │
│ cin ├○─────────────────────────╯
╰─────╯                                                                              

Step 7. Stable feedback through wires

Combinational feedback is rejected at compile time — a chain of gates whose output drives its own input is a hard error (`E008`). But the validator looks at the signal graph, not the textual order. Here two `not` gates connect end to end through two `wire` pass-throughs; the chain has length four and no cycle, so it compiles cleanly. This is the substrate the language gives you for building latches and other feedback structures.

source
not n1(in=w2.out)
wire w1(in=n1.out)
not n2(in=w1.out)
wire w2(in=n2.out)
circ-compile --preview
     ╭───╮     ╭───╮
   ╭▶┤NOT├○───▶┤NOT├○╮
   │ ╰───╯     ╰───╯ │
   ╰─────────────────╯

Done. The reference covers the full surface — every keyword, every diagnostic code, the rules the validator enforces. The examples gallery has more circuits to read through.