# circ — full documentation bundle > Concatenation of every public `.md` twin on https://circ-lang.org. > Built from commit `78dfc3f5ec7c9d11fd2c341f2b2528b1f9ff75d5` by `scripts/build-llm-mirror.ts`. > Canonical source: https://github.com/jeffersonmourak/circ-compiler. --- # circ > A small language for building and simulating logic circuits, made for people learning how computers work. ## Hero example: half-adder Two inputs, an XOR macro for the sum, an AND for the carry. ### Source ```circ // half_adder.circ import xor "/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]├○╯ ╰─────╯ ╰──▶┤ │ ╰───────╯ ``` Download `circ-compile` for Linux, macOS, or Windows: https://circ-lang.org/download.md ## What it is `circ` is a declarative language. Programs are flat lists of declarations: name an input pin, instantiate a gate, wire its ports to signals from other components. The primitives are `and`, `not`, `led`, and `wire`; macros for `or`, `nand`, `nor`, `xor`, and `xnor` expand to those primitives at compile time. Larger circuits live in their own `.circ` file and get pulled in with `import`. ## What it isn't `circ` is a v0. There are no multi-bit buses, no clocked registers, no analog signals, no tri-state lines. A `wire` is a single-bit pass-through, not a let-binding for a vector. The language deliberately stops at the same boundary as the early Nand2Tetris hardware chapters. ## Where it runs `circ-compile` produces a self-contained WebAssembly module: drive its input pins from JavaScript, read its outputs back, run it from Node or a browser. There is also a `--preview` flag that prints an ASCII schematic to your terminal — handy for sanity-checking the wiring before you simulate. If you've enjoyed Nand2Tetris or building NANDs from scratch in Petzold's *Code*, this is a language for doing more of that. --- # A short tour > A progressive walkthrough of `circ`. Each step adds one new idea on top of the previous one. By the end you'll have read enough to feel at home in the language reference. ## 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 ```circ 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 ```circ 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 ```circ 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 ```circ 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 ```circ // half_adder.circ import xor "/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 ```circ // half_adder.circ import xor "/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 ```circ 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 [language reference](https://circ-lang.org/reference.md) covers the full surface — every keyword, every diagnostic code, the rules the validator enforces. The [examples gallery](https://circ-lang.org/examples.md) has more circuits to read through. --- # Language Reference > The full reference for the circ digital-logic language. `circ` is a small declarative language for describing digital logic circuits. A program is a flat list of declarations: each declaration either names an external pin or instantiates a component and wires its input ports to signals produced by other components. The compiler (`circ-compile`) parses the source, resolves names, validates the resulting graph, and lowers it to a self-contained WebAssembly module. This document is the language reference. For an end-to-end tutorial see [`getting-started.md`](https://circ-lang.org/reference/getting-started.md); for the runtime API exposed by the compiled artifact see [`wasm-api.md`](https://circ-lang.org/reference/wasm-api.md). > **Status.** This reference covers the surface as of the multi-bit-wires > release: imports, input/output pins, primitive components (`and`, `not`, > `led`, `wire`), the auto-imported macro family (`or`, `nand`, `nor`, `xor`, > `xnor`), anonymous nested components, sub-circuit instantiation, and width > annotations on every signal-carrying declaration (`[N]` for literal widths, > `` for parametric ones). Slice (`a[lo..hi]`), bit-index (`a[i]`), and > concatenation (`{a, b}`) signal expressions are also part of the language. > Anything not mentioned here is not part of the language yet. --- ## 1. Abstract Grammar The grammar below uses the conventions of EBNF: *italics* mark non-terminals, **bold** marks literal terminals, `[ x ]` is optional, `{ x }` is zero or more, and `|` separates alternatives. The authoritative PEG source lives at `lib/grammar/proto-circ.peg`; this presentation is a slightly normalised reading of it. ``` program = { item } . item = comment | import | input-decl | output-decl | component-decl . comment = "//" { any-char-except-newline } newline . import = "import" alias string-literal . alias = identifier . input-decl = "input" identifier { "," identifier } . output-decl = "output" identifier "(" "in" "=" signal ")" . component-decl = type identifier "(" port-binding { "," port-binding } ")" . type = "and" | "not" | "led" | "wire" | "or" | "nand" | "nor" | "xor" | "xnor" | identifier . (* user-defined sub-circuit alias *) port-binding = identifier "=" signal . signal = qualified-ref | anonymous-component "." identifier . qualified-ref = identifier [ "." identifier ] . (* "name" or "name.port" *) anonymous-component = type "(" port-binding { "," port-binding } ")" . identifier = ( letter | "_" ) { letter | digit | "_" } . string-literal = '"' { any-char-except-double-quote } '"' . ``` A few intentional shapes to note in the grammar: * The order of items in a program is irrelevant for *semantics* (the validator resolves names globally) but parsing is strictly left-to-right line-oriented. * There is no statement terminator. Items are separated by whitespace; a single declaration may span multiple lines as long as its parentheses balance. * `output` is technically a component-shaped declaration (it has one port, `in`), but it is special-cased above to make the asymmetry with `input` explicit. --- ## 2. Lexical Structure ### 2.1 Whitespace Spaces, tabs, carriage returns, and newlines are insignificant outside of string literals and identifiers. They may appear between any two tokens. ### 2.2 Comments `circ` has one comment form, the C++-style line comment: ``` // this is a comment, ignored to end of line ``` `#` is **not** a comment introducer. Block comments (`/* … */`) are not supported. ### 2.3 Identifiers Identifiers match `[A-Za-z_][A-Za-z0-9_]*` and are case-sensitive. They are used for component instance names, port names, input/output pin names, and import aliases. There is no distinction between "user" and "system" identifiers — but type keywords (`input`, `output`, `and`, `not`, `wire`, etc.) are reserved when used in declaration position. ### 2.4 String Literals String literals appear only in `import` declarations and are double-quoted with no escape processing. They name a path on disk (or a virtual path beginning with `/`). ``` import xor "/xor.circ" import half_adder "half_adder.circ" ``` Paths are resolved relative to the directory of the file containing the import. --- ## 3. Declarations A `circ` program is a sequence of declarations. The four kinds are described below. Every declaration that carries a signal may optionally name its width in bits with a `[N]` annotation; a missing `[N]` means width 1, which is what makes pre-multi-bit `.circ` files legal as-is. ### 3.1 Input Pins ``` input a input clk, reset input[4] addr, data // 4-bit buses ``` `input` declares one or more externally driven pins. An input pin has no input ports of its own; its single output is referenced as **``** or equivalently **`.out`** elsewhere in the program. Input pins are driven from the host via `setPin(component_id, value, defined)` after compilation (see [`wasm-api.md`](https://circ-lang.org/reference/wasm-api.md) for the BigInt-pair convention). The `[N]` annotation between the keyword and the names sets the width for every name in that `input` line. To declare pins at different widths, write separate `input` lines. A sub-circuit becomes parametric by introducing a parameter with `` on its `input` lines (covered fully in §6): ``` input[W] a, b // a, b inherit the parameter W as their width ``` ### 3.2 Output Pins ``` output sum(in = adder.out) output[4] result(in = alu.out) ``` `output` declares a named externally observable pin and binds its single port `in` to a signal. Multi-bit outputs use the same `[N]` annotation. Output pins are read from the host via two paired exports: `getOutputValue(driver_id)` returns the `BitVecState.value` field as a BigInt, `getOutputDefined(driver_id)` returns `BitVecState.defined`. Both take the **driver** component id, not the output pin's own id; see [`wasm-api.md`](https://circ-lang.org/reference/wasm-api.md). ### 3.3 Component Instances A component is instantiated by writing its **type**, an optional `[N]` width, an instance **name**, and a parenthesised list of **port bindings**: ``` and gate1(a = pin1, b = pin2) not inv (in = clk.out) and[4] adder(a = x, b = y) // 4-bit AND ``` The available primitive types are: | Type | Input ports | Output | Notes | | ------ | ----------- | ------ | ---------------------------------------------------------------------- | | `and` | `a`, `b` | `out` | Bitwise AND. Width controlled by `[N]`; defaults to 1. | | `not` | `in` | `out` | Bitwise inverter. Width controlled by `[N]`; defaults to 1. | | `led` | `in` | — | Visualisation sink. Multi-bit form renders per `--preview` flags. | | `wire` | `in` | `out` | Pass-through. See §5. | The auto-imported macro family is parametric in width; the default-missing-`[N]` rule keeps every existing scalar caller working unchanged. | Type | Input ports | Output | Expansion (per width slot) | | ------ | ----------- | ------ | ---------------------------------- | | `or` | `a`, `b` | `out` | `not(and(not a, not b))` | | `nand` | `a`, `b` | `out` | `not(and a b)` | | `nor` | `a`, `b` | `out` | `not(or[W] a b)` — `or` propagates width | | `xor` | `a`, `b` | `out` | `and(or a b, nand a b)` | | `xnor` | `a`, `b` | `out` | `not(xor[W] a b)` — `xor` propagates width | A user-defined sub-circuit is referenced by the alias bound in its `import` declaration. Its ports are exactly the names declared as `input`/`output` in the imported file. If the imported sub-circuit is parametric (declares one or more `` parameters), the caller binds widths positionally with the `name[N, M, ...]` form at the instance name: ``` mux inst[4, 2](data = x, select = sel) // mux instantiated at W=4, S=2 ``` Omitting the `[N, ...]` defaults all parameters to 1. ### 3.4 Imports ``` import half_adder "half_adder.circ" ``` `import` makes a sibling `.circ` file available under an alias in the current file. The path is resolved relative to the importing file. Built-in macros live at the virtual path `/.circ` and are auto-imported as soon as the file participates in the project pipeline (i.e. has at least one explicit `import`); to use a built-in in a single-file program with no other imports, write the import explicitly: ``` import xor "/xor.circ" ``` --- ## 4. Signals and Wiring A *signal* is whatever you place on the right-hand side of a port binding. It identifies the source of the bit(s) that drive the port. Signals come in six forms: **Reference to an input pin:** ``` and g(a = pin1, b = pin2) // pin1 and pin2 are 'input' declarations ``` **Reference to a named component's output:** ``` not n1 (in = pin1.out) and g (a = n1.out, b = pin2) ``` The `.out` suffix is the implicit output port of any single-output primitive. For sub-circuit instances, use the explicit output port name from the imported file: `ha.sum`, `ha.carry`, etc. **Bit index (`name[i]` or `name.port[i]`).** Picks a single bit out of a multi-bit signal: ``` input[4] bus and g(a = bus[0], b = bus[3]) // bit 0 AND bit 3 ``` Bit 0 is the LSB. The result is a width-1 signal. **Slice (`name[lo..hi]` or `name.port[lo..hi]`).** Picks a contiguous range of bits, half-open: ``` input[8] bus and[4] low_half(a = bus[0..4], b = some_other_4bit_signal) ``` `bus[0..4]` covers bits 0, 1, 2, 3 — a 4-bit signal. The width of a slice is `hi - lo`. An out-of-range or inverted slice is `E002`. **Concatenation (`{low, high, ...}`).** Joins two or more signals into a wider one, low-on-left: ``` input a, b, c, d and[4] combine(a = {a, b, c, d}, b = ...) // bits: [0]=a, [1]=b, [2]=c, [3]=d ``` The output width is the sum of operand widths. **Anonymous nested components.** A component may be instantiated inline as the value of a port. The nested instance has no name; its `.out` is wired immediately into the enclosing port: ``` and g( a = pin1, b = not(in = pin2).out ) ``` Anonymous nesting may nest arbitrarily deep. It is purely a syntactic sugar: the resolver lowers it to an unnamed component instance with the same wiring rules as a named one. ### 4.1 Validation Rules The compiler enforces a small set of rules on the resulting graph; violations produce diagnostics with stable codes (`E001`–`E016`, `W001`–`W003`, see `circuit-format.md` for the full catalogue): * Every signal reference must resolve to a declared name (`E001`). * Every named port on a component must exist on that component's type (`E002`, `E012`). * Every input port that the component requires must be bound exactly once; binding a port twice is `E003` (multi-driver), and leaving a required port unbound is `E004`/`E013`. * Identifiers must be unique within their file (`E005`); user instances may not shadow primitive type names (`E006`). * The induced signal graph must be acyclic (`E008`). See §5 for the role `wire` plays in cycle detection. * Connection widths must agree on both ends. A connection from a width-4 source to a width-8 destination is `E014` (width mismatch). * Passing `[N]` widths to a scalar (non-parametric) sub-circuit is `E015`; the diagnostic suggests adding `` to the callee. * Parametric arity mismatch (caller writes `[N, M]` but the callee declares one parameter, or vice versa) is `E016`. --- ## 5. Wires A `wire` is a one-port pass-through component. Its single input port is `in` and its single output port is `out`; the value on `out` is, after evaluation, identical to the value on `in`. Wires are the closest thing the language has to a "let" binding for signals. ### 5.1 What wires are for Wires exist for two reasons. **(a) Naming an intermediate signal.** A bare `pin1.out` carries no documentation. Threading it through a `wire` lets you give the bit a meaningful name without changing the circuit's logical behaviour: ``` input clk wire clock_buf(in = clk) and g(a = clock_buf.out, b = data.out) ``` This is purely a readability device. The compiler does not optimise wires away in the topology section, so the named signal survives into runtime introspection. **(b) Anchoring a signal that is referenced more than once.** Inline anonymous components have no name and therefore cannot be re-used; if the same derived signal feeds two ports, you need a named anchor for it. A `wire` is the lightest anchor available: ``` input a wire na(in = not(in = a).out) // 'na' = NOT a, named once and g1(a = na.out, b = b1) and g2(a = na.out, b = b2) ``` ### 5.2 Wires and cycles Wires participate in cycle detection like any other component. A pair of wires that drive each other's `in` is a hard error (`E008`): ``` // E008_wire_loop.circ — rejected at compile time wire w1(in = w2.out) wire w2(in = w1.out) ``` This is *not* a special-case for `wire`; it is the same rule that forbids combinational feedback through any chain of components. The fixture `clean_gated_feedback.circ` illustrates the legal counterpart, where wires break two NOT gates into a sequence rather than a loop: ``` not n1(in = w2.out) // n1 is fed from w2 wire w1(in = n1.out) not n2(in = w1.out) wire w2(in = n2.out) // closes the chain — but the chain has length 4 and // — crucially — has no cycle in the *signal* graph ``` (In this snippet `w2.out` is forward-referenced; the validator resolves names globally, so order in source is irrelevant for binding.) ### 5.3 Multi-bit wires A `wire` also takes the `[N]` annotation when it carries a multi-bit signal: ``` input[4] bus wire[4] buffered_bus(in = bus) and[4] g(a = buffered_bus.out, b = some_4bit_signal) ``` The wire's input width must match the source's output width, and its output width is the same `N`. Mismatches surface as `E014`. ### 5.4 What wires are *not* A `wire` is not a tri-state line and not a clocked register. It is a value-preserving pass-through over a fixed-width signal. If you find yourself wanting either of those things, the language does not yet model them. --- ## 6. Multi-bit Wires `circ` programs may carry signals wider than one bit. Every signal-carrying declaration accepts an optional `[N]` annotation; a missing `[N]` means width 1. Sub-circuits may take their widths as parameters with the `` form. The authoritative decision record lives in [`decisions/language.md`](decisions/language.md); this section is the user- facing reference. ### 6.1 Literal widths The simplest form annotates a fixed width on a declaration: ``` input[4] a, b and[4] g(a = a, b = b) output[4] r(in = g.out) ``` `a`, `b`, the `and` gate `g`, and the output `r` are all width-4. The validator checks that every connection's source width matches its destination width. `a[0]` is the LSB. Bit `i` has weight `2^i`. This matches the `BitVecState.value` bit layout the engine uses internally; there is no conversion between the user-visible numbering and the runtime numbering. ### 6.2 Slice, bit-index, and concatenation See §4 for the signal-expression syntax. A slice or bit-index produces a narrower signal; a brace concat produces a wider one. The resolver lowers each to an engine-level component, so users never write `slice(...)` or `concat(...)` directly — the syntactic forms are the only way to invoke them. ``` input[8] bus and[4] g(a = bus[0..4], b = bus[4..8]) // AND the two halves and bit_eq(a = bus[0], b = bus[7]) // compare LSB to MSB input a, b input[2] tail output[4] out(in = {a, b, tail}) // out = a | (b << 1) | (tail << 2) ``` ### 6.3 Parametric sub-circuits A sub-circuit becomes parametric by introducing parameters with `` on its `input` declarations. Use sites inside the file reference the parameter as `[W]`: ``` // wide_not.circ input[W] a not[W] inv(in = a) output[W] o(in = inv.out) ``` Callers bind widths positionally with `name[N, ...]` at the instance name: ``` import wide_not "wide_not.circ" input[4] x wide_not inst[4](a = x) output[4] r(in = inst.o) ``` Multiple parameters are allowed and ordered by source-position of first introduction: ``` // mux_lib.circ input[W] data input[S] select // ... body uses [W] and [S] independently ``` The caller binds positionally: `mux inst[4, 2](data = ..., select = ...)` maps `[W]` to 4 and `[S]` to 2. Two rules to remember: * **Missing `[N]` at the call site defaults all parameters to 1.** This is what keeps every existing scalar caller of the built-in macros working unchanged. * **Angle brackets only on the introducing line.** A parametric sub-circuit marks its parameter once, on `input`. Internal references use `[W]`, not ``. Width-mismatch on connections involving a sub-circuit boundary, missing `` introductions, and arity mismatches at call sites surface as `E014`, `E015`, and `E016` respectively. ### 6.4 The built-in macros are parametric `or`, `xor`, `nand`, `nor`, `xnor` ship with `` declarations. Scalar callers (no `[N]`) default `W` to 1, which produces byte-identical IR and topology to the pre-multibit form. Wider callers get the natural multi-bit gate. `nor` and `xnor` propagate width through their internal `or` / `xor` calls. ``` input[8] x, y nor[8] n(a = x, b = y) // bitwise NOR across all 8 bits output[8] z(in = n.out) ``` --- ## 7. A Worked Example The program below builds a half-adder out of primitives and exposes its sum and carry as outputs. It exercises every construct in the language: imports, input pins, primitive components, a built-in macro (`xor`), a `wire` used both to name a signal and to fan it out, anonymous nested components, and output pins. ``` // half_adder_demo.circ // // Computes: sum = a XOR b // carry = a AND b // and exposes a third output 'busy' = NOT(sum) AND carry, which is always 0 // — purely to demonstrate fan-out via a 'wire'. import xor "/xor.circ" input a, b // xor primitive (auto-imports the macro expansion under the hood). xor s_gate(a = a, b = b) // 'sum_w' names the XOR output so we can use it twice without re-instantiating // the gate. Without the wire, anonymous nesting would force us to duplicate // the xor gate. wire sum_w(in = s_gate.out) // First fan-out: feed the sum into a NOT gate inline. // Second fan-out: drive the 'sum' output pin directly from the same wire. and busy_gate( a = not(in = sum_w.out).out, b = and(a = a, b = b).out // anonymous AND for the carry bit ) output sum (in = sum_w.out) output carry(in = and(a = a, b = b).out) output busy (in = busy_gate.out) ``` Compile and inspect: ```sh circ-compile half_adder_demo.circ -o half_adder_demo.wasm circ-compile half_adder_demo.circ --preview ``` The `--preview` flag prints an ASCII schematic of the resolved circuit; see [`preview.md`](https://circ-lang.org/reference/preview.md) for the rendering conventions. --- ## 8. Where to Go Next * [`getting-started.md`](https://circ-lang.org/reference/getting-started.md) — install the compiler, write your first circuit, drive it from Node. * [`circuit-format.md`](https://circ-lang.org/reference/circuit-format.md) — the original tutorial-flavored walkthrough of the format, with the full diagnostic-code catalogue. * [`wasm-api.md`](https://circ-lang.org/reference/wasm-api.md) — the runtime API exposed by every compiled `.wasm` artifact. * [`preview.md`](https://circ-lang.org/reference/preview.md) — `--preview`, `--expand-macros`, and the conventions used when rendering circuits as ASCII. --- # Getting Started > Install the compiler, write your first circuit, and drive it from Node. This walkthrough takes you from a fresh checkout to a working compiled circuit you can drive from Node. Each step has expected output so you can verify you are on track. ## 1. Install and verify the CLI You need [Zig](https://ziglang.org/) 0.15.x. Build the compiler: ```sh zig build circ-compile ``` The binary lands at `zig-out/bin/circ-compile`. Verify it works by compiling a fixture from the test suite: ```sh zig-out/bin/circ-compile tests/fixtures/circuits/inverter.circ --inspect | head -8 ``` Expected: ``` === Parse Tree === File [f0:1:1-3:23] Imports (0) Inputs (1) Input a [f0:1:7-1:8] Outputs (1) Output out [f0:3:1-3:23] NamedRef inv.out [f0:3:15-3:22] ``` If you see that, the compiler is working. ## 2. Write your first `.circ` file Create `examples/inverter.circ`: ```text input a not inv(in=a) led l(in=inv.out) output out(in=inv.out) ``` This declares one input pin `a`, drives it through a `not` gate, mirrors the result on an `led` (visualisation primitive), and exposes the inverted signal as an `output` pin. See `DOCS/circuit-format.md` for the full language reference. ## 3. Compile it to WebAssembly ```sh zig-out/bin/circ-compile examples/inverter.circ -o examples/inverter.wasm ``` The CLI runs the parser, validator, and topology serializer end-to-end, then appends the serialized circuit as `circ.topology.v0.min` and `circ.topology.v0.full` custom WASM sections to the pre-built runtime blob. No `zig` subprocess is spawned. On success it writes the combined `.wasm` to the path given to `-o`. Inspect the compiled circuit's interface: ```sh zig-out/bin/circ-compile examples/inverter.circ --inspect ``` Or visualise the circuit as an ASCII schematic without producing any artifact: ```sh zig-out/bin/circ-compile examples/inverter.circ --preview ``` Expected: ``` ╭───╮ ╭───╮ ╭───╮ │ a ├○───▶┤NOT├○●──▶┤LED│ ╰───╯ ╰───╯ │ ╰───╯ │ │ ╭─────╮ ╰──▶┤ out │ ╰─────╯ ``` The not-gate's output fans out to both the LED and the `out` pin — `●` marks the branch point, and the two `▶` arrowheads show where each branch terminates. See [`preview.md`](https://circ-lang.org/reference/preview.md) for `--expand-macros`, `--color`, and the rendering conventions. Or enumerate the circuit's behaviour against every input combination as a Markdown truth table: ```sh zig-out/bin/circ-compile examples/inverter.circ --truth-table ``` ``` | a | out | |---|-----| | 0 | 1 | | 1 | 0 | ``` `--truth-table` runs the resolver and validator first; circuits with combinational loops (E008) are rejected before any simulation. The mode caps at 16 inputs (2^16 = 65,536 rows) to avoid accidental blow-up — wider circuits should be exercised through the `.wasm` runtime instead. The relevant block tells you which component IDs to drive from JavaScript: ``` Inputs (1) id=0 name=a component=0 Outputs (1) id=0 name=out driver=1.out ``` `setPin(id, value, defined)` takes the **input pin's component id** (`0` for `a`) along with a `(value, defined)` `BitVecState` pair. The paired output reads `getOutputValue(id)` and `getOutputDefined(id)` take the **driver component id** of the output (`1` here — the `not` gate that drives `out`), not the output_pin's own id. ## 4. Drive the compiled `.wasm` from Node The compiled `.wasm` contains the runtime and a `circ.topology.v0.min` custom section. The host must load that section into WASM linear memory before calling `init()`. Create `examples/run.mjs`: ```js import fs from "node:fs"; const bytes = fs.readFileSync(process.argv[2]); const mod = await WebAssembly.compile(bytes); const { exports: w } = await WebAssembly.instantiate(mod, { env: { debugEnabled: () => 0, onDebugLog: () => {}, }, }); // Load the circ.topology.v0.min custom section into WASM linear memory const [topoSection] = WebAssembly.Module.customSections(mod, "circ.topology.v0.min"); const topoBytes = new Uint8Array(topoSection); const ptr = w.topology_alloc(topoBytes.length); new Uint8Array(w.memory.buffer).set(topoBytes, ptr); w.init(); const read = (id) => w.getOutputDefined(id) === 0n ? "undefined" : w.getOutputValue(id) === 0n ? "low" : "high"; w.setPin(0, 0n, 1n); w.run(); console.log("a=0 -> NOT a =", read(1)); w.setPin(0, 1n, 1n); w.run(); console.log("a=1 -> NOT a =", read(1)); ``` Run it: ```sh node examples/run.mjs examples/inverter.wasm ``` Expected: ``` a=0 -> NOT a = high a=1 -> NOT a = low ``` The two i64 parameters cross the boundary as JavaScript `BigInt` values; `value` and `defined` each pack one bit per signal bit. A scalar pin uses `(0n, 1n)` for low, `(1n, 1n)` for high, and `(_, 0n)` for undefined. The full export list emitted by `circ-compile … -o out.wasm` today is exactly `topology_alloc`, `init`, `run`, `setPin`, `getOutputValue`, `getOutputDefined` (plus `memory`); see [`DOCS/wasm-api.md`](https://circ-lang.org/reference/wasm-api.md) for the full contract. The two `env` callbacks (`debugEnabled` and `onDebugLog`) are required imports — supply the no-op stubs above unless you want debug logging. ### Driving a multi-bit input For a wider input declared as `input[4] a`, both `value` and `defined` use one bit per signal bit. To drive a 4-bit bus to the value `0b1010` with every bit defined: ```js w.setPin(input_id, 0b1010n, 0b1111n); w.run(); const v = w.getOutputValue(output_id); // BigInt, e.g. 0b1010n for a passthrough const d = w.getOutputDefined(output_id); // BigInt, 0b1111n ``` Bits set beyond the declared width are silently masked. See [`DOCS/wasm-api.md`](https://circ-lang.org/reference/wasm-api.md) for the full `BitVecState` semantics. ## 5. Use a built-in macro (`xor`) Built-in gates `or`, `nand`, `nor`, `xor`, and `xnor` are auto-imported when a file is part of a project — i.e. when the parser sees at least one `import` declaration. To use a built-in in an otherwise standalone file, add an explicit import to the virtual `/` filesystem: ```text // examples/xor_demo.circ import xor "/xor.circ" input a, b xor x(a=a, b=b) output out(in=x.out) ``` Compile and inspect: ```sh zig-out/bin/circ-compile examples/xor_demo.circ -o examples/xor.wasm zig-out/bin/circ-compile examples/xor_demo.circ --inspect ``` > **v0 papercut.** When the file has no user imports, single-file mode does not auto-import built-ins, so a bare `xor` raises `E001: undeclared name 'xor'`. The explicit `import xor "/xor.circ"` is the workaround until the CLI is updated to run the project pipeline unconditionally. Once a file participates in the project pipeline, root-pin component IDs are assigned in a flat layout that includes the built-in macro's expanded gates. The simplest way to discover them is to scan in JS: ```js for (let i = 0; i < 64; i++) { if (w.getOutputDefined(i) !== 0n) { console.log(`id=${i} -> value=${w.getOutputValue(i)} defined=${w.getOutputDefined(i)}`); } } ``` For a structured map, parse the `circ.topology.v0.full` custom section in JS — it carries per-file IDs, component aliases, and macro provenance. (A future runtime release may surface this through a `getFileInfo()` export, but it is not present in today's compiled artifacts.) ## 6. Compose with a sub-circuit import Sub-circuits live in their own `.circ` files and are imported by alias. The half-adder is a canonical two-file project: ```text // examples/half_adder/half_adder.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) ``` ```text // examples/half_adder/root.circ import half_adder "half_adder.circ" input a, b half_adder ha(a=a, b=b) output sum(in=ha.sum) output carry(in=ha.carry) ``` Compile from the root: ```sh zig-out/bin/circ-compile examples/half_adder/root.circ -o examples/half_adder.wasm ``` Sub-circuits are fully flattened by the serializer into a single ordered sequence of primitive components — no function calls, no hierarchy in the runtime. Loading from JS uses the same `topology_alloc` + `init()` pattern as step 4; discovering global IDs in deeper hierarchies is currently easiest via `circ-compile --inspect`, which prints the resolved IR with each `Inputs (...)` / `Outputs (...)` block annotated with component IDs. (The `circ.topology.v0.full` custom section carries the same information for programmatic readers.) More worked project fixtures, including a full-adder built from two half-adders and a 4-bit AND/OR network, live under `tests/fixtures/projects/` and double as integration tests. ## Where to go next - `DOCS/circuit-format.md` — the complete `.circ` language reference (declarations, ports, anonymous components, built-ins). - `DOCS/wasm-api.md` — every export and import on the compiled artifact, plus the topology custom-section layout. - `DOCS/architecture.md` and `DOCS/decisions/` — design rationale, useful when contributing. - `tests/fixtures/circuits/` and `tests/fixtures/projects/` — copy-and-modify templates for common circuit patterns. --- # Circuit File Format > The .circ language: declarations, ports, components, and built-ins. `.circ` files describe a digital circuit as a set of named components and their connections. The format is a simple declarative DSL designed to map directly onto the simulation engine's component and connection model. > **Status**: Parsing, semantic validation, and compilation to WASM exist for the supported surface below. A single root `.circ` (plus any sibling files it imports) produces one self-contained `.wasm` artifact via the in-process topology-splice pipeline; the runtime is prebuilt and embedded. ## Syntax Overview A `.circ` file contains a sequence of top-level declarations. Each declaration either names input pins or instantiates a component with named port connections. Every signal-carrying declaration may carry an optional width annotation `[N]` where `N` is in `[1, 64]`; a missing `[N]` means width 1. ``` input [] [, ...] output [] [, ...] [] [] ( = [, = ...] ) ``` `` is a literal `[N]` (or a parameter inside a parametric sub-circuit; see [`language.md`](https://circ-lang.org/reference.md) §6.3). `` is a comma-separated list `[w0, w1, ...]` that pins the width parameters of an imported sub-circuit at the call site. For the full language reference (parametric sub-circuits, slice/index/concat signals, the `` introduction form, built-in parametric macros, and diagnostic semantics) see [`language.md`](https://circ-lang.org/reference.md). ## Comments Line comments start with `//` and run to end of line (see `Annotation` in `lib/grammar/proto-circ.peg`). Hash (`#`) is not a comment starter in `.circ`. ## Declarations ### Input Pins ``` input pin1, pin2 input[4] addr input[W] data ``` Declares one or more named input pins for the circuit. Input pins are driven externally (by the host) and have no input ports of their own. An optional `[N]` after `input` makes the pin `N` bits wide; the parametric form `input[W]` declares both a width parameter and an input of that width (see [`language.md`](https://circ-lang.org/reference.md) §6.3). ### Component Instances ``` ( = , ... ) ``` `` is the gate kind. Recognised types: | Type | Ports | Width rule | Description | |----------|----------|-------------------------------------------------------------------------------|---------------------------------------------------------------------| | `and` | `a`, `b` | bit-parallel; both inputs must match the instance width | AND gate | | `not` | `in` | bit-parallel; input must match the instance width | NOT gate (inverter) | | `led` | `in` | input must match the instance width; widths `> 1` render as a numeric display | Output indicator | | `wire` | `in` | input must match the instance width | Pass-through | | `output` | `in` | input must match the instance width | Externally observable output pin (special-cased declaration kind) | `input` is a sibling pin declaration with its own syntax (no port list), described above. All primitives accept an optional `[N]` width annotation; absent it, width is 1. **Built-in macro gates** expand at compile time to nested `and` / `not` (and optionally other macros). They use **`a`** and **`b`** as input ports and expose **`out`**. Unlike `and`/`not`, **no `import`** is required — the compiler behaves as if `import … from "/.circ"` were present. Each macro is parametric: `or[8] g(a=x, b=y)` produces a bit-parallel 8-bit OR. | Type | Ports | Expansion | |--------|----------|-----------| | `or` | `a`, `b` | OR from `not`/`and` | | `nand` | `a`, `b` | NAND | | `nor` | `a`, `b` | NOR (uses internal `or` macro)| | `xor` | `a`, `b` | XOR | | `xnor` | `a`, `b` | XNOR (uses internal `xor` macro)| `` is the name of the port being driven (e.g. `a`, `b`, `in`). `` is one of: - `.out` — the output of a named component or primitive with a single implicit output `.out`. - `.` — the output pin of an imported subcircuit that exposes `` (`sum`, `carry`, etc.). - `[i]` — single-bit index into a multi-bit signal (yields width 1). - `[lo..hi]` — half-open slice `[lo, hi)` of a multi-bit signal (yields width `hi - lo`). - `{, , ...}` — concatenation, **low operand on the left**, so `{a, b}` puts `a` in the low bits and `b` in the high bits. - An inline component expression (see nested components below). See [`language.md`](https://circ-lang.org/reference.md) §4 and §6 for the full signal grammar and width-checking rules. ## Signal References A signal reference connects a port to the output of a previously declared name: ``` a = pin1.out ``` This wires port `a` to the output of the component named `pin1`. ## Nested Component Expressions Components may be defined inline as the value of a port connection. The nested component has no instance name but its `.out` output is wired immediately to the enclosing port: ``` and combine ( a = pin1.out, b = not ( in = pin2.out ).out ) ``` Here a `not` gate is instantiated anonymously; its output is connected to port `b` of `combine`. ## Full Example ``` input pin1, pin2 and combine ( a = pin1.out, b = not ( in = pin2.out ).out ) led result ( in = combine.out ) ``` This circuit computes `pin1 AND (NOT pin2)` and displays the result on an LED. The compiled `.wasm` does not expose a programmatic graph-construction API — the topology is baked into the artifact as a custom section and materialised by the embedded runtime at `init()`. Hosts only see the fixed export surface (`setPin`, `run`, paired `getOutputValue` / `getOutputDefined`, …) described in [`wasm-api.md`](https://circ-lang.org/reference/wasm-api.md); the component IDs they need are emitted by `circ-compile --inspect` under each module's `Inputs (...)` / `Outputs (...)` block. ## Grammar The parser is generated from a PEG grammar at `lib/grammar/proto-circ.peg` using the `langlang` tool. The generated Go parser is at `lib/parser/parser.go`; a hand-written CGo shim at `lib/parser/shim/shim.go` exposes it as a C-callable archive (`lib/parser/parser.a` + `lib/parser/parser.h`) that Zig links via `lib/syntax/CParser.zig`. Parse tree node types used by `lib/syntax/translate.zig`: | Node type | Meaning | |---------------------|------------------------------------------| | `NodeType_Sequence` | Ordered list of child nodes | | `NodeType_Node` | Named grammar rule match | | `NodeType_String` | Matched literal text (identifier, etc.) | | `NodeType_Error` | Parse failure at this position | `lib/syntax/nodes/declaration.zig` handles `Declaration` rule nodes and recognises the sub-rules `input`, `output`, and `component` to determine declaration kind. ## Topology Format Version Compiled `.wasm` artifacts embed the resolved circuit as a custom section. The current format identifier is `CIRC` (v02), bumped from the v01 format that preceded multi-bit support. Each serialised component now carries a `width: u8` byte; the runtime materialises the corresponding pool tier on `init()`. See [`wasm-api.md`](https://circ-lang.org/reference/wasm-api.md) for the host-facing ABI and `lib/topology/serializer.zig` for the wire layout. ## Diagnostic Codes The stable validator surface (E001–E016, W001–W003) is enumerated in [`language.md`](https://circ-lang.org/reference.md) §4.2 and `lib/validator/codes.zig`. The multi-bit codes added with v02 are: | Code | Meaning | | --- | --- | | E014 | width mismatch between a driver and the port it feeds | | E015 | sub-circuit is not parametric (caller passed `[N]` call-widths to a scalar callee) | | E016 | parameter count mismatch (wrong number of `[w0, w1, ...]` call-widths at the call site) | --- # WASM Runtime API > The export and import contract for every compiled .wasm artifact. Each `.wasm` produced by `circ-compile .circ -o .wasm` is a self-contained module: it embeds a prebuilt simulation runtime plus this circuit's topology as a custom section. The host loads the module, copies the topology bytes into linear memory, calls `init()`, and then drives the circuit through the small fixed export surface below. This document is the contract for the **default compile path** (the artifact actually shipped to hosts). The optional `--emit-zig` path generates a standalone Zig source with a richer experimental export surface; that surface is not stable and not described here. ## Imports (host → WASM) The host must supply two functions in the `env` namespace at instantiation time: | Import | Signature | Purpose | |---------------|--------------------------------------------|------------------------------------------------------------------| | `debugEnabled`| `() => i32` | Return `1` to receive log callbacks, `0` to suppress them. | | `onDebugLog` | `(ptr: i32, len: i32, logType: i32) => void` | Receives a UTF-8 log message in linear memory. `logType`: `0` debug, `1` info, `2` warn, `3` error. The buffer is owned by the runtime — do not call back into the runtime to free it. | If `debugEnabled` returns `0`, `onDebugLog` is never called, but **both imports must be present** or instantiation will fail. Provide no-op stubs when you don't care about logs. ## Exports (WASM → host) ``` memory: WebAssembly.Memory topology_alloc: (len: i32) => i32 // host buffer for topology bytes; returns ptr (or -1 on OOM) init: () => void // construct circuit from the topology buffer run: () => void // drain the event queue until the circuit settles setPin: (id: i32, value: i64, defined: i64) => void getOutputValue: (id: i32) => i64 // BitVecState.value of the component's output getOutputDefined: (id: i32) => i64 // BitVecState.defined of the component's output ``` The i64 fields cross the JS↔WASM boundary as `BigInt`. A component's state is a `BitVecState`-shaped `(value, defined)` pair where each bit of `defined` says whether the corresponding bit of `value` is meaningful: | `defined` bit | `value` bit | Interpretation | |---------------|-------------|----------------| | `1` | `0` | low | | `1` | `1` | high | | `0` | (ignored) | undefined | For a width-`W` component, only the low `W` bits of each i64 are meaningful; bits beyond `W` are zero on read and silently dropped on write. ### `topology_alloc(len)` and `init()` The compiled `.wasm` carries the circuit topology as a `circ.topology.v0.min` custom section, **not** in linear memory. The host is responsible for copying those bytes into the runtime's linear memory before `init()` runs. The protocol is: 1. Read the section: `WebAssembly.Module.customSections(module, "circ.topology.v0.min")`. 2. Call `topology_alloc(byteLength)`. The runtime allocates a buffer in linear memory and returns its pointer (or `-1` on allocation failure). 3. Copy the section bytes to that pointer in `memory.buffer`. 4. Call `init()`. The runtime parses the buffer, builds the circuit graph, and marks itself initialised. `init()` is idempotent: calling it after the runtime is initialised is a no-op. It silently bails out if no topology was loaded or the topology fails to parse, so always copy the section before calling `init`. ### `run()` Drains the engine's event queue until empty. Settling delays are `5` time-units per gate and `1` per wire/output_pin (see `lib/circuit.zig`); a single `run()` call is enough to settle any cascade — there is no "tick" semantics to worry about. `run()` is a no-op if `init()` has not run successfully. ### `setPin(component_id, value, defined)` Drives a top-level input pin to the BitVecState `(value, defined)`. `component_id` is the global integer ID of an `input_pin_gate`; passing the ID of a non-input or out-of-range component is a silent no-op (it does not throw or trap). For width-1 inputs the usual encodings are `setPin(id, 0n, 1n)` for low, `setPin(id, 1n, 1n)` for high, and `setPin(id, 0n, 0n)` for undefined. For wider inputs each bit of `value` and `defined` corresponds to a bit position; bits set beyond the component's declared width are masked silently. `setPin` only enqueues the change — call `run()` after to propagate it. ### `getOutputValue(component_id)` and `getOutputDefined(component_id)` Paired exports. Each call returns one of the two `BitVecState` fields of the component's current output. Returns `0n` for both if the runtime is not initialised or the ID is out of range — the host distinguishes "definitely low" from "undefined" by checking `getOutputDefined` first. The argument is the **driver component ID**, not an output-pin index — for an `output out(in=inv.out)` declaration, you pass `inv`'s component ID, not `out`'s pin ID. The `--inspect` output of the compiler prints this mapping under its `Outputs (...)` block. #### Why paired exports instead of one out-pointer call A previous draft considered a single-call shape: `getOutputState(id, out_ptr: i32)` that wrote a 16-byte `BitVecState` into linear memory at `out_ptr`. Two reasons the paired-export shape won: - **Simpler host code.** Two function calls return BigInts that the host combines directly. No buffer allocation, no pointer arithmetic, no in-band encoding to standardise (endianness, alignment, sentinel for undefined). - **The 2× call overhead is irrelevant in practice.** Hosts typically read once after settle, not in a hot loop. If a future use case needs batched reads, a sibling export (`getOutputStateBatch(ids_ptr, out_ptr, count)`) can be added without breaking the paired API. ## Custom sections | Section name | Contents | |---------------------------|---------------------------------------------------------------------| | `circ.topology.v0.min` | Compact topology consumed by `init()`. Required. | | `circ.topology.v0.full` | Verbose topology used by tooling (`circ-compile --inspect`, preview rendering). The runtime never reads it. | | `name` | Standard Zig-emitted name section. Useful for debuggers, ignored at runtime. | The `.full` section is not required for execution. Hosts that only run circuits can ignore it; tools that need names, hierarchy, or per-port labels should read `.full`. ## Minimal Node integration ```js import fs from "node:fs"; const bytes = fs.readFileSync(process.argv[2]); const mod = await WebAssembly.compile(bytes); const { exports: w } = await WebAssembly.instantiate(mod, { env: { debugEnabled: () => 0, onDebugLog: () => {}, }, }); // 1. Copy the topology section into linear memory. const [topoSection] = WebAssembly.Module.customSections(mod, "circ.topology.v0.min"); const topoBytes = new Uint8Array(topoSection); const ptr = w.topology_alloc(topoBytes.length); new Uint8Array(w.memory.buffer).set(topoBytes, ptr); // 2. Build the circuit and drive it. w.init(); w.setPin(0, 1n, 1n); // pin id=0 → high (value=1, defined=1) w.run(); const value = w.getOutputValue(1); // BigInt const defined = w.getOutputDefined(1); // BigInt console.log(defined === 0n ? "undefined" : value === 0n ? "low" : "high"); // "low" (NOT of high) ``` For wider inputs, the helper pattern is: ```js // Drive a width-4 input bus to the value 0b1010, all four bits defined. w.setPin(input_id, 0b1010n, 0b1111n); w.run(); const v = w.getOutputValue(output_id); // e.g. 0b1010n for a passthrough const d = w.getOutputDefined(output_id); // 0b1111n ``` To collapse a paired read back into the legacy 3-state encoding when integrating with code that still uses it: ```js const readScalar = (id) => w.getOutputDefined(id) === 0n ? 2 // undefined : w.getOutputValue(id) === 0n ? 0 : 1; // low / high ``` ## Things that are *not* exports today Earlier drafts of this project anticipated additional exports — `deinit`, `reset`, `stop`, `getStateSnapshot`, `getTopology`, `getPendingEvents`, `getFileInfo`, `freeBuffer` — and a separate `onStateChange` import. None of these are present in the artifact produced by `circ-compile … -o out.wasm` today. The extra exports survive only in `lib/emit/runtime.zig`, the experimental `--emit-zig` pipeline; the `onStateChange` import was never wired into any pipeline and exists nowhere in the codebase. Either may appear in a future runtime version. If your host needs change-notifications, poll `getOutputValue` / `getOutputDefined` after each `run()`. --- # ASCII Preview > Render circuits as deterministic ASCII schematics with --preview. `circ-compile --preview` renders a digital circuit as a styled ASCII schematic to stdout. The output is deterministic — the same `.circ` source produces byte-identical output on every invocation — and includes the wires, gate glyphs, fan-out taps, and jump-arc crossings that make the diagram readable in a terminal. ## Quick start ```sh zig-out/bin/circ-compile tests/fixtures/circuits/single_gate.circ --preview ``` Expected output: ``` ╭───╮ ╭───╮ ╭─────╮ │ a ├○───▶┤NOT├○───▶┤ out │ ╰───╯ ╰───╯ ╰─────╯ ``` Every node is a labeled box — input pin `a`, the `NOT` gate, output pin `out`. The `○` glyph immediately outside each port is a bubble showing where a wire enters or leaves the cell; the `▶` arrowhead marks the destination end of each wire. For a circuit using a builtin macro (xor in this case): ```sh zig-out/bin/circ-compile tests/fixtures/circuits/builtin_xor.circ --preview ``` ``` ╭───╮ ╭───────╮ ╭─────╮ │ a ├○───▶┤ │ ╭──▶┤ out │ ╰───╯ │[xor:g]├○╯ ╰─────╯ ╭─▶┤ │ │ ╰───────╯ │ ╭───╮ │ │ b ├○─╯ ╰───╯ ``` The `[xor:g]` box represents the entire `xor g(...)` instance as a single labeled subcircuit — the label sits on the row between the two left-side input ports, with the output port on the right. To render the macro's primitive expansion instead, add `--expand-macros`. ## Flags | Flag | Purpose | |------|---------| | `--preview` | Selects preview mode. Mutually exclusive with `--emit-zig` and `--inspect`. `-o` is rejected at parse time. | | `--expand-macros` | Renders subcircuits as their full primitive expansion instead of as a single labeled box. Only valid with `--preview`. | | `--expand-display` | Renders an `led[N]` (width > 1) as a row of `N` single-bit LED cells with explicit `b0..bN-1` slice connections, instead of the default opaque multi-bit numeric display box. Only valid with `--preview`. | | `--color=auto\|always\|never` | Enables ANSI color (per-kind: input pins green, gates cyan, LEDs yellow, macros magenta, wires dim). Defaults to `auto` (color when stdout is a TTY *and* `NO_COLOR` is unset). `always` overrides `NO_COLOR` per the convention used by `git`/`ls`/`grep`. | The render path is fully in-memory: parse → resolve → translate → topology build → layout → render → stdout. No `.wasm` is written, no temp directory, no subprocess. ## Rendering conventions **Per-kind glyphs.** Every node renders as a bordered box. The box body always uses `╭───╮` / `╰───╯` for the corners, with the kind-specific label and ports on the rows in between: | Kind | Cell shape | Cell size | |------|------------|-----------| | `input_pin` | `╭───╮` / `│ ├○` / `╰───╯` — name centered, output bubble on right edge | 5×3 (wider for long names) | | `output_pin` | `╭───╮` / `┤ │` / `╰───╯` — input port `┤` on left edge | 5×3 (wider for long names) | | `not_gate` | `╭───╮` / `┤NOT├○` / `╰───╯` — input `┤` left, output `├○` right | 5×3 | | `and_gate` | `╭───╮` / `┤ │` / `│AND├○` / `┤ │` / `╰───╯` — two stacked input ports flanking the label row | 5×5 | | `led` | `╭───╮` / `│LED│` / `╰───╯` — input rides on the centre row, no separate port glyph | 5×3 | | `subcircuit` (opaque) | `╭─...─╮` / `┤ │` / `│[:]├○` / `┤ │` / `╰─...─╯` — width grows to fit the label | (label width + 2) × 5 | Names longer than the cell width are truncated; shorter names pad with spaces. The `○` port-side bubbles aren't part of the box itself — they sit one column outside the `╭╮╰╯` border, so a 5-column box with bubbles looks 6 columns wide on the wire side. **Wire line art.** Wires are routed as orthogonal segments and drawn with these glyphs: - `─` horizontal rail, `│` vertical rail. - `╭` `╮` `╰` `╯` corners between perpendicular segments. The glyph is picked from the two segment directions: `{W,S} → ╮`, `{E,S} → ╭`, `{W,N} → ╯`, `{E,N} → ╰`. - `┬` `┴` `├` `┤` 3-way junctions. Picked by the same neighbour-inspection pass that handles corners; you'll see these wherever a wire branches into a T off another wire. - `┼` 4-way crossings — only drawn where two unrelated wires pass over each other (no shared endpoint). The renderer prefers ┼ over the older "jump-arc" trick. - `●` fan-out / fan-in branch point — drawn where one wire splits into two destinations, or two wires merge into one port. - `○` port-side bubble — drawn on the cell immediately outside a gate's input or output port (the cell where the wire begins or ends). - `▶` `◀` `▲` `▼` arrowhead — drawn on the destination end of every wire, just before it enters the target port. Direction matches the segment's last step. - `+` fallback — appears only on cells that have no connecting neighbours in any direction. It's a *visible warning glyph* meaning the router placed a wire that nothing connects to; if you see one, something is off. **Layout determinism.** Rendering uses a five-stage pipeline (collapse → columns → rows → place → route), followed by a junction-picker pass that resolves crossings into the right corner/T-glyph. Every decision uses ascending node id as the universal tie-breaker; hash-map iteration is forbidden as an ordering source. The same `.circ` source produces byte-identical output across runs and platforms. ## Multi-bit pins A component declared with a width annotation (`input[4] a`, `led[4] disp`, `wire[8] bus`) renders with the width appended to its label as `[N]`: ``` ╭──────╮ ╭────────────╮ │ a[4] ├○───▶┤ [led:disp] │ ╰──────╯ ╰────────────╯ ``` A scalar pin omits the suffix, so the label width-marker is the visual cue that distinguishes a 1-bit and an N-bit wire. The wire glyph itself is the same — there is no "bus" glyph. ### LED rendering modes A multi-bit `led[N]` (width > 1) has three rendering modes: | Mode | Trigger | Cell content | |------|---------|--------------| | **Numeric** | width > 1, all bits `defined` | The unsigned integer value (`0`–`2^N-1`) inside the box. | | **Numeric + warning** | width > 1, some bits `defined`, others `undefined` | The integer value formed from the defined bits, with a warning marker (`?`) showing partial state. | | **Indicator** | width = 1 | The single-bit LED glyph: lit on `high`, dim on `low`, `?` on `undefined`. | With `--expand-display`, the multi-bit form decomposes into `N` scalar LEDs wired to explicit bit-index slices of the input signal, which is the right view when you need to debug per-bit drive state. ## Macro modes Built-in macros (`or`, `nand`, `nor`, `xor`, `xnor`) and user-imported subcircuits expand into primitive gates during compilation. The renderer can display them two ways: - **Opaque (default).** All primitives that came from one subcircuit instance collapse into a single labeled box `[:]`. Connections to/from the subcircuit's published ports flow into the box's edges. - **Expanded (`--expand-macros`).** Every primitive appears individually, with its origin chain visible in the topology metadata. Useful for understanding what a macro actually does or for debugging unexpected behaviour from a builtin. ## Where the data comes from The renderer reads from a versioned topology payload embedded in compiled `.wasm` artifacts as WASM custom sections: | Section | Contains | |---------|----------| | `circ.topology.v0.min` | Flat primitive components (id, kind, `width: u8`) + connections. Magic `CIRC`, version `0x02`. The "lightweight" payload — what the runtime needs. | | `circ.topology.v0.full` | Adds per-component instance names + subcircuit-origin chains. Magic `CIRF`, version `0x02`. The "rich" payload — what the renderer (and any future inspection tooling) needs. | `--preview` builds the `full` payload in memory (skipping the `.wasm` write) and feeds it directly into the renderer. Tools that consume a `.wasm` artifact from disk can parse the same payload via `lib/topology/full_decoder.zig:decode`. The `min` and `full` sections are independent variants on a single version axis — they coexist in every produced artifact. Pre-1.0, the schemas are freely revvable: bump `vN.{min,full}` rather than carrying compatibility shims. See `lib/topology/full_format.zig` for the wire format. ## Known limitations - The `--color` flag has no effect outside `--preview` mode (no other mode renders to a terminal). It's accepted in any mode but harmlessly stored. - The compile/emit-zig modes use a `has_imports` gate that can miss builtin-macro single-file fixtures (only preview was widened to handle them — see `cmd/circ-compile/main.zig`'s `needs_project_resolution` logic). - A `+` glyph in the rendered output indicates a routed cell with no connecting neighbours — a router bug rather than a stylistic fallback. If one appears, the relevant fixture is worth checking against the goldens under `tests/fixtures/preview/renders/`. --- # Examples > A gallery of `circ` circuits — primitives, multi-bit arithmetic, and stateful latches. Each entry shows the source and its `--preview` output. ## Inverter A single NOT gate, the smallest non-trivial circuit you can write. ### Source ```circ input a not n(in=a) output out(in=n.out) ``` ### `circ-compile --preview` ``` ╭───╮ ╭───╮ ╭─────╮ │ a ├○───▶┤NOT├○───▶┤ out │ ╰───╯ ╰───╯ ╰─────╯ ``` ## AND with one inverted input Combines a NOT and an AND to compute `a AND (NOT b)`. ### Source ```circ 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 ├○╯ ╰───╯ ``` ## One input, three destinations A single input drives three independent NOT gates. The `●` glyphs in the ASCII preview mark fan-out taps where the same wire is reused. ### Source ```circ 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 │ ╰───╯ ╰────╯ ``` ## NOT chain Three inverters in series. The output is just `a` — but the chain still gets compiled and simulated faithfully. ### Source ```circ 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 │ ╰───╯ ╰───╯ ╰───╯ ╰───╯ ╰─────╯ ``` ## 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 ```circ import xor "/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]├○╯ ╰─────╯ ╰──▶┤ │ ╰───────╯ ``` ## 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 ```circ // 2-to-1 multiplexer: out = sel ? b : a // When sel=0 the gate routes 'a' through; when sel=1 it routes 'b'. import or "/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 ├○╮ ╭──▶┤NOT├○───▶┤ │ ╭──▶┤ │ ╭──▶┤ out │ ╰───╯ │ │ ╰───╯ │AND├○╯ │[or:out_or]├○╯ ╰─────╯ ╰─┼────────────▶┤ │ ╭▶┤ │ │ ╰───╯ │ ╰───────────╯ │ │ ╭─────╮ │ ╭───╮ │ │ sel ├○●──▶┤ │ │ ╰─────╯ │AND├○────────────╯ ╭───▶┤ │ │ ╰───╯ │ ╭───╮ │ │ b ├○─╯ ╰───╯ ``` ## Full-adder Two XORs, two ANDs, one OR. Adds three bits (a, b, cin) into a sum bit and a carry-out. ### Source ```circ import xor "/xor.circ" import or "/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 ├○─●───▶┤ │ ╭──▶┤ │ ╭▶┤ │ ╭──▶┤ cout │ ╰───╯ │ │AND├○╮ │ │AND├○╮ │ │[or:c3]├○╯ ╰──────╯ ╭┼───▶┤ │ │ │ ╭▶┤ │ ╰──────┼▶┤ │ ││ ╰───╯ │ │ │ ╰───╯ │ ╰───────╯ ││ ╰────┼─┼──────────────╯ ╭───╮ ││ ╭────────╮ │ │ ╭────────╮ ╭─────╮ │ b ├○●╰───▶┤ │ ●─┼▶┤ │ ╭────────────────▶┤ sum │ ╰───╯ │ │[xor:s1]├○● │ │[xor:s2]├○╯ ╰─────╯ ╰────▶┤ │ ●▶┤ │ ╰────────╯ │ ╰────────╯ │ ╭─────╮ │ │ cin ├○─────────────────● ╰─────╯ ``` ## 2-bit ripple-carry adder A half-adder for the low bit and a full-adder above it. The lower bit’s carry feeds into the upper bit’s `cin`. ### Source ```circ // 2-bit ripple-carry adder: (a1 a0) + (b1 b0) → (cout s1 s0) import xor "/xor.circ" import or "/or.circ" input a0, a1, b0, b1 // Lower bit (half adder) xor s0_xor(a=a0, b=b0) and s0_carry(a=a0, b=b0) // Upper bit (full adder, cin = s0_carry.out) xor s1_x1(a=a1, b=b1) xor s1_x2(a=s1_x1.out, b=s0_carry.out) and s1_a1(a=a1, b=b1) and s1_a2(a=s1_x1.out, b=s0_carry.out) or s1_or(a=s1_a1.out, b=s1_a2.out) output s0 (in=s0_xor.out) output s1 (in=s1_x2.out) output cout(in=s1_or.out) ``` ### `circ-compile --preview` ``` ╭────╮ ╭───╮ ╭───╮ ╭──────────╮ ╭──────╮ │ a1 ├○──●▶┤ │ ╭───▶┤ │ ╭▶┤ │ ╭──▶┤ cout │ ╰────╯ │ │AND├○╮ │ │AND├○╮ │ │[or:s1_or]├○╯ ╰──────╯ ╭─┼▶┤ │ ├───────┼───▶┤ │ ╰─────────┼▶┤ │ │ │ ╰───╯ │ │ ╰───╯ │ ╰──────────╯ │ │ ├───────┼────────────────────╯ ╭────╮ │ │ ╭───╮ │ │ ╭───────────╮ ╭────╮ │ b1 ├○●╭┼▶┤ │ │ ●───▶┤ │ ╭▶┤ s0 │ ╰────╯ │││ │AND├○● │ │[xor:s1_x2]├○╮ │ ╰────╯ ││├▶┤ │ ╰───────┼───▶┤ │ │ │ │││ ╰───╯ │ ╰───────────╯ │ │ │││ │ │ │ ╭────╮ │││ ╭───────────╮ │ │ │ ╭────╮ │ a0 ├○●●┼▶┤ │ │ ╰──────────────────┼▶┤ s1 │ ╰────╯ │││ │[xor:s1_x1]├○● │ ╰────╯ ╰┼┼▶┤ │ │ ││ ╰───────────╯ │ ││ │ ╭────╮ ││ ╭────────────╮ │ │ b0 ├○─┴┼▶┤ │ │ ╰────╯ │ │[xor:s0_xor]├○─────────────────────────────────────╯ ╰▶┤ │ ╰────────────╯ ``` ## SR latch Two cross-coupled NOR cells. The first circuit on the page that *remembers*: with both inputs low it holds whatever was last written. Try clicking S to set, then both back to 0, then R to reset. ### Source ```circ // 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├○┬──▶┤ │ ╭──▶┤NOT├○╮ ╭▶┤ │ ╭──▶┤NOT├○╮ ╰───╯ ╰───╯ │ │AND├○● ╰───╯ │ │ │AND├○● ╰───╯ │ │ ╭▶┤ │ │ ╰─┼▶┤ │ │ │ │ │ ╰───╯ │ │ ╰───╯ │ │ ├─┼───────┼───────────╯ │ │ ╭───╮ ╭───╮ │ │ │ │ ╭───╮ │ │ s ├○───▶┤NOT├○╯ │ │ ╰──▶┤ q │ │ ╰───╯ ╰───╯ │ │ ╰───╯ │ ╰───────┼─────────────────────────────╯ │ ╭──────╮ ╰──────────────────────▶┤ qbar │ ╰──────╯ ``` ## Stable feedback through wires Two NOT gates connected end-to-end through two `wire` pass-throughs. Chain length four, no cycle in the signal graph — compiles cleanly. The substrate the SR latch above is built on. (No inputs to drive — open the live canvas to read the wire states.) ### Source ```circ 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├○╮ │ ╰───╯ ╰───╯ │ ╰─────────────────╯ ``` --- # Download > Pre-built `circ-compile` binaries for Linux, macOS, and Windows. Alpha software — expect breaking changes between builds. Latest build: `0.0.2`. ## Alpha warning `circ-compile` is under active development. Expect rough edges, cryptic error messages, and breaking changes between builds. Don't use it for anything you care about preserving. Reports of what doesn't work are welcome — but for now, assume nothing here is stable. ## Pre-built binaries Prefer to build from source? See [getting started](https://circ-lang.org/reference/getting-started.md). | OS | Architecture | File | Min OS | | --- | --- | --- | --- | | Linux | x86_64 | [`circ-compile-0.0.2-linux-x86_64.tar.gz`](https://circ-lang.org/downloads/circ-compile-0.0.2-linux-x86_64.tar.gz) | Kernel 3.2+ | | macOS | aarch64 (Apple Silicon) | [`circ-compile-0.0.2-macos-aarch64.tar.gz`](https://circ-lang.org/downloads/circ-compile-0.0.2-macos-aarch64.tar.gz) | 11 (Big Sur) | | Windows | x86_64 | [`circ-compile-0.0.2-windows-x86_64.zip`](https://circ-lang.org/downloads/circ-compile-0.0.2-windows-x86_64.zip) | 10 | ## Build from source For Intel Macs, ARM Linux, ARM Windows, or any platform not listed above, build directly with [Zig 0.15.x](https://ziglang.org/): ```sh git clone https://github.com/jeffersonmourak/circ-compiler cd circ-compiler zig build circ-compile -Doptimize=ReleaseFast ./zig-out/bin/circ-compile --help ``` The full walkthrough — including writing your first `.circ` file and driving the compiled WASM from Node — lives in [getting started](https://circ-lang.org/reference/getting-started.md).