# circ — full documentation bundle > Concatenation of every public `.md` twin on https://circ-lang.org. > Built from commit `e0609e6bd656096899b9cb98cef451768d961d1e` by `scripts/build-llm-mirror.ts`. > Canonical source: https://github.com/jeffersonmourak/circ-compiler. --- # circ > Logic circuits, written down. 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) ``` [Edit this in the playground](https://circ-lang.org/playground#pick=example:half-adder) ## Six primitives. Five macros. One import. A program is a list of declarations: name input pins, place gates, and connect their ports. Combine circuits with imports to build larger ones. If you know the early Nand2Tetris hardware chapters, these building blocks will look familiar. - Primitives: `and`, `not`, `led`, `wire`, `rom`, `ram`. - Macros: `or`, `nand`, `nor`, `xor`, `xnor` — expand to primitives. - Signals: 1–64 bits, `input[4] a`, `a[0..2]`, `{a, b}`. - Files: `import adder "adder.circ"` — one circuit each. 1. **Write a .circ file.** Use any text editor, or write in the playground to check your wiring as you type. 2. **Compile it.** Run circ-compile to produce a self-contained WebAssembly module. Use --preview to print a schematic in your terminal. 3. **Run the circuit.** Set input pins and read outputs from JavaScript. Run the module in a browser, in Node, or in the playground. ## Source on the left. Schematic on the right. This half-adder uses XOR for the sum and AND for the carry. Run circ-compile with --preview to print its schematic in your terminal. ```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) ``` ``` ╭───╮ ╭───╮ │ a ├○●──▶┤ │ ╭───────╮ ╰───╯ │ │AND├○───────▶┤ carry │ │╭─▶┤ │ ╰───────╯ ╭───╮ ││ ╰───╯ │ b ├○┼╯ ╰───╯ ││ ╭───────╮ ╰┼─▶┤ │ ╭─────╮ │ │[xor:s]├○───▶┤ sum │ ╰─▶┤ │ ╰─────╯ ╰───────╯ ``` [Open it in the playground](https://circ-lang.org/playground#pick=example:half-adder) ## From the gallery [two-bit-adder](https://circ-lang.org/gallery.md): Adds two 2-bit numbers and produces a sum and carry. [four-bit-adder](https://circ-lang.org/gallery.md): Adds two 4-bit numbers, passing the carry from one bit to the next. [rom-lookup](https://circ-lang.org/gallery.md): Stores the squares of 0 through 15. Set an address to read its square. [All examples](https://circ-lang.org/gallery.md) ## Install Run it on your own machine. Linux · macOS · Windows · GPL v3 Download `circ-compile` for Linux, macOS, or Windows: https://circ-lang.org/download.md --- # 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 native-memories > release: imports, input/output pins, primitive components (`and`, `not`, > `led`, `wire`), the memories `rom` and `ram` (§3.5, §6.5), 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 | component-decl | connection . comment = "//" { any-char-except-newline } newline . import = "import" alias string-literal . alias = identifier . input-decl = "input" [ param-intro ] [ width-annot ] ident-list . param-intro = "<" identifier { "," identifier } ">" . (* declares width parameters *) width-annot = "[" width-arg "]" . width-arg = integer | identifier . (* a literal or a parameter *) ident-list = identifier { "," identifier } . component-decl = type [ width-annot ] identifier [ call-widths ] bindings | type [ width-annot ] ident-list . (* port-less form, see below *) type = "output" | "bus" | "led" | "and" | "or" | "not" | "xor" | "nand" | identifier . (* wire, nor, xnor, rom, ram and every sub-circuit alias arrive through this branch *) call-widths = "[" width-arg { "," width-arg } "]" . (* also a memory's [W, A] *) bindings = "(" port-binding { "," port-binding } ")" . port-binding = identifier "=" signal . signal = concat | indexed-ref . concat = "{" signal { "," signal } "}" . indexed-ref = base-ref [ subscript ] . subscript = "[" integer ".." integer "]" | "[" integer "]" . base-ref = anonymous-component "." identifier | identifier [ "." identifier ] . (* "name" or "name.port" *) anonymous-component = type bindings . connection = signal "<>" signal . (* parsed, then discarded *) identifier = ( letter | "_" ) { letter | digit | "_" } . integer = digit { digit } . string-literal = '"' { any-char-except-double-quote } '"' . ``` A few intentional shapes, and a few accidents, 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. * `circ` has no statement terminator. Whitespace separates items; a single declaration may span multiple lines as long as its parentheses balance. * `output` is an ordinary component-shaped declaration with one port. The translator **ignores the binding name**: `output r(zzz = g.out)` compiles, and it drops any binding after the first. Write `in` anyway. * The `type` keywords carry no word boundary, so the parser splits an identifier in type position that merely *begins* with one: `andx g(a=a, b=a)` parses as `and x(...)` plus a syntax error (`recovery_keyword_prefix.circ`). A sub-circuit alias must therefore not start with `and`, `or`, `not`, `xor`, `nand`, `led`, `bus`, or `output`. * `bus` is a keyword nothing implements: `bus x(in = y)` resolves to an undeclared name (`E001`). * The port-less form `type ident-list` (`led x`, `and g1, g2`) parses but has no use today: a gate or LED declared that way reports `E004` (unconnected input), and `output q` makes the pin its own driver (`E008`). * The parser accepts a `<>` connection line, and the translator discards it without a diagnostic (`recovery_connection_line.circ`). --- ## 2. Lexical Structure ### 2.1 Whitespace Spaces, tabs, carriage returns, and newlines are insignificant outside 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. `circ` does not support block comments (`/* … */`). ### 2.3 Identifiers Identifiers match `[A-Za-z_][A-Za-z0-9_]*` and are case-sensitive. They name component instances, ports, input/output pins, and import aliases. The language draws no distinction between "user" and "system" identifiers, but type keywords (`input`, `output`, `and`, `not`, `wire`, etc.) are reserved 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" ``` The compiler resolves paths relative to the directory of the file containing the import. --- ## 3. Declarations A `circ` program is a sequence of declarations. This section describes the four kinds. Every declaration that carries a signal may name its width in bits with a `[N]` annotation; a missing `[N]` means width 1, which keeps 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; elsewhere in the program you reference its single output as **``** or, equivalently, **`.out`**. The host drives input pins 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. The host reads output pins through 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 You instantiate a component 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` | `out` | Visualisation sink; `out` re-drives `in` so an LED can feed a gate. 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 | You reference a user-defined sub-circuit 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 compiler resolves the path relative to the importing file. Built-in macros live at the virtual path `/.circ`, and the compiler auto-imports one whenever a file uses it. A single-file program may write `xor s(a=a, b=b)` with no `import` at all, and every mode (compile, preview, truth table, sim, analyze) resolves it through the project pipeline. Writing the import explicitly is still valid, and is the clearer form when a file mixes built-ins with its own siblings: ``` import xor "/xor.circ" ``` The one exception is `--inspect`, which stays a single-module debugging view and reports an unimported built-in as `E001`. ### 3.5 Memories (declaration shape) `rom` and `ram` are built-in memory types. You declare a memory like a parametric sub-circuit instance: the type keyword, an instance name, exactly two instance-position width arguments `[W, A]` (data width and address width), and a port list: ``` input[4] pc rom code[8, 4](addr = pc.out) // 16 words of 8 bits, read-only output[8] out(in = code.out) 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) ``` | Type | Input ports | Output | Port widths | | ----- | --------------------------- | ------ | -------------------------------------------- | | `rom` | `addr` | `out` | `addr` is `A` wide; `out` is `W` wide | | `ram` | `addr`, `din`, `we`, `clk` | `out` | `addr` is `A`, `din`/`out` are `W`, `we`/`clk` are 1 | `W` must be in `1..64` and `A` in `1..16` (`E018`); a declaration with any other number of width arguments, or with a width written after the keyword (`rom[8] m[8, 4]`), is `E017`. Inside a parametric sub-circuit, the arguments may name introduced parameters (`ram m[W, A](...)`). Every listed input port is required (`E004`), and ports are checked at their own widths (`E014`). A `ram` breaks combinational loops; a `rom` does not (`E008`). `rom` and `ram` are reserved: an instance or input named `rom` is `E006`, and `import rom "..."` is `E011`. Memory contents never appear in source; the host loads them at runtime (or `--sim` does). This section is only the declaration shape; the read and write semantics, the X rules, the edge rule, and how contents get in are in §6.5. --- ## 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 one or more signals into a wider one, low-on-left: ``` input a, b, c, d input[4] mask and[4] combine(a = {a, b, c, d}, b = mask) // bits: [0]=a, [1]=b, [2]=c, [3]=d ``` The output width is the sum of operand widths. **Anonymous nested components.** You may instantiate a component 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 pure syntactic sugar: the resolver lowers it to an unnamed component instance with the same wiring rules as a named one. Any component's port binding (`and`, `not`, `wire`, `led`, a macro, a sub-circuit) accepts it; an `output` declaration does **not**: `output o(in = and(a = a, b = b).out)` loses the nested gate's bindings and fails with `E004`. When an output needs one, name the gate or route it through a `wire`. ### 4.1 Validation Rules The compiler enforces a small set of rules on the resulting graph; violations produce diagnostics with stable codes (`E001`–`E018`, `W001`–`W003`; the catalogue with each code's message is in `circuit-format.md` "Diagnostic Codes", the registry in `lib/validator/codes.zig`): * 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`. * A memory declaration must carry exactly two width arguments `[W, A]` (`E017`) with `W` in `1..64` and `A` in `1..16` (`E018`); see §6.5. --- ## 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 descriptive 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 used more than once.** Inline anonymous components have no name, so you cannot re-use them; 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 are *transparent* to cycle detection: a cycle that runs only through `wire`, `led`, `output`, `slice`, `concat`, `rom`, and sub-circuit boundaries has no delay element and is a hard error (`E008`), as is any component driving itself. A pair of wires that drive each other's `in` is the simplest case: ``` // E008_wire_loop.circ — rejected at compile time wire w1(in = w2.out) wire w2(in = w1.out) ``` `and`, `not`, and `ram` are *cycle-breaking*: a loop that passes through one of them is sequential logic (a latch), not a combinational loop, and the validator accepts it. The fixture `clean_gated_feedback.circ` is a ring of two NOT gates and two wires — a genuine cycle in the signal graph — and compiles cleanly because the NOT gates break it: ``` 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 ring; legal because it passes through gates ``` The check says nothing about whether such a ring settles: a ring with an odd number of inverters compiles and oscillates at run time. (This snippet forward-references `w2.out`; 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 need either one, the language does not yet model it. --- ## 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; the user-visible numbering and the runtime numbering need no conversion. ### 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) ``` A file may introduce several parameters, ordered by the 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 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`, and `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 n[8](a = x, b = y) // bitwise NOR across all 8 bits output[8] z(in = n.out) ``` Pass the width of a macro or sub-circuit instance in *instance* position, as call-widths (`n[8]`), exactly as for a user sub-circuit (§6.3). A `[N]` in *type* position (`nor[8] n(...)`) sizes only primitives; on a macro it leaves the callee at width 1, and the call fails with `E014`. ### 6.5 Memories (`rom`/`ram`) Memories are the first primitives whose instance takes *two* width parameters: `[W, A]` is the same `CallWidths` list a parametric sub-circuit call takes (§6.3), read as the data width `W` and the address width `A`. A memory therefore holds `2^A` words of `W` bits. The declaration shape is in §3.5; this section is the reference for what a memory *does*. ``` input[4] pc rom code[8, 4](addr = pc) // 16 words × 8 bits output[8] instr(in = code.out) input[4] a input[8] d input w, clk ram data[8, 4](addr = a, din = d, we = w, clk = clk) output[8] q(in = data.out) ``` **Ports.** | Type | Port | Direction | Width | Meaning | | ----- | ------ | --------- | ----- | ---------------------------------------------------- | | both | `addr` | input | `A` | the address whose word appears on `out` | | both | `out` | output | `W` | the word at `addr` (asynchronous read) | | `ram` | `din` | input | `W` | the word to write | | `ram` | `we` | input | 1 | write enable | | `ram` | `clk` | input | 1 | the clock; a write happens on its rising edge | Every input port is required (`E004`) and is checked at the width in the table (`E014`); a port name outside the table is `E002`. `W` must be in `1..64` and `A` in `1..16` (`E018`). **Reading.** Both kinds read asynchronously: `out` is always the word stored at the presented `addr`, and it follows every address change without a clock, exactly like any other combinational output. A cell that has never been loaded or written reads fully undefined. If *any* bit of `addr` is undefined, `out` is fully undefined; the memory never does a partial lookup. **Writing (`ram` only).** A `ram` writes `din` into the cell at `addr` on a *defined low → defined high* transition of `clk`, and only if `we` is defined-high and every bit of `addr` is defined at that moment. The clock's previous level must have been a defined `0`: the very first `set clk 1` (or `setPin(clk, 1n, 1n)`) after power-on is *not* an edge, because the previous level was undefined. A circuit can therefore never write on its way out of the all-undefined initial state. The `ram` stores `din` as presented, bit for bit, including its definedness; a partially undefined `din` writes a partially undefined cell. `clk` and `we` are ordinary width-1 inputs; the language has no clock primitive. From a host you pulse the clock by driving it low and then high; from `--sim` that is `set clk 0` then `set clk 1`. **Contents come from the host.** Source never carries an image. A memory is filled at run time through one of three doors, all taking the same headerless raw image — `ceil(W/8)` little-endian bytes per word, at most `2^A` words, padding bits clear (see `wasm-api.md` "Image format"): * a compiled artifact's `getMemInfo` / `memBuffer` / `memLoad` / `memStore` / `memClear` / `setMemWord` / `getMemValue` / `getMemDefined` exports (`wasm-api.md`); * `--sim` and `--truth-table` with `--mem==` on the command line; * `--sim`'s `load`, `save`, `peek`, `poke`, `mem`, and `clear` verbs (`sim-protocol.md`). Loading is *replace-all*: a shorter image leaves the remaining cells undefined; an empty image clears the memory. **Loops.** For `E008`, a `ram` behaves like a gate: a path through it does not form a combinational loop, so feeding `q` back into `d` or `a` is legal. A `rom`, by contrast, is transparent, so `rom` `out → addr` with nothing in between is an `E008` cycle (§5.2). **Parametric memories.** Inside a `` sub-circuit, the width arguments may name the introduced parameters, and callers bind them positionally like any other parametric call: ``` input[A] addr input[W] din input we, clk ram m[W, A](addr = addr, din = din, we = we, clk = clk) output[W] q(in = m.out) ``` **Tooling.** `--truth-table` tabulates a circuit whose memories are all `rom` (preload them with `--mem`; unloaded cells print `?`) and refuses one that contains a `ram`, whose clock it would otherwise enumerate as an input. Drive those with `--sim`. `--preview` draws a `rom` as a one-input box and a `ram` as a four-input box. `--emit-zig` does not support memories. --- ## 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) // The carry bit, named so both an output and the gate below can read it. and carry_gate(a = a, b = b) // First fan-out: feed the sum into a NOT gate inline (an anonymous nested // component). Second fan-out: drive the 'sum' output pin from the same wire. and busy_gate( a = not(in = sum_w.out).out, b = carry_gate.out ) output sum (in = sum_w.out) output carry(in = carry_gate.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-4:1] Imports (0) Inputs (1) Input a [f0:1:1-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. It spawns no `zig` subprocess. 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; it rejects circuits with combinational loops (E008) before any simulation, and any circuit containing a `ram` (stateful; drive it with `--sim` instead). The mode caps at 16 total input *bits* (an `input[4]` counts four; 2^16 = 65,536 rows) to avoid accidental blow-up. `--truth-table-cap=N` raises the cap to 24 at most, and wider circuits belong on the `.wasm` runtime. 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 core export list emitted by `circ-compile … -o out.wasm` is `topology_alloc`, `init`, `run`, `setPin`, `getOutputValue`, `getOutputDefined` (plus `memory`). Every artifact also exports the memory family `getMemInfo`, `memBuffer`, `memLoad`, `memStore`, `memClear`, `setMemWord`, `getMemValue`, `getMemDefined`, meaningful only for the ids of `rom`/`ram` components (step 7 below). 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 ``` The runtime silently masks bits set beyond the declared width. 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`) The compiler auto-imports the built-in gates `or`, `nand`, `nor`, `xor`, and `xnor` whenever a file uses one: it takes the project pipeline for any root that declares an import *or* instantiates a built-in, so a standalone file can write `xor x(a=a, b=b)` with no `import` line. The explicit import from the virtual `/` filesystem remains valid and is the form used below: ```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.** `--inspect` alone stays in single-module mode and does not auto-import built-ins, so a bare `xor` shows up there as `E001: undeclared name 'xor'`. Compile (`-o`), `--emit-zig`, `--preview`, `--truth-table`, and `--sim` all resolve it; add the explicit `import xor "/xor.circ"` only if you want `--inspect`'s view of the file to be clean. Once a file participates in the project pipeline, the compiler assigns root-pin component IDs 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 today's compiled artifacts lack it.) ## 6. Compose with a sub-circuit import Sub-circuits live in their own `.circ` files, and a root file imports them 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 ``` The serializer fully flattens sub-circuits 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. In deeper hierarchies, the easiest way to discover global IDs is `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. ## 7. Loading a program into ROM and stepping a clocked circuit `rom` and `ram` are built-in memories. A declaration gives only the *shape*: `[W, A]` is `W` bits per word and `2^A` words. You load the contents at run time, so the same compiled circuit can run any program you hand it. This step authors an image, loads it three ways, and pulses a RAM's clock by hand. ### 7.1 The circuit Save this as `prog_rom.circ` (it is `tests/fixtures/circuits/sim_rom_pc_walk.circ` verbatim): a 4-bit program counter, a 16-word ROM of 8-bit instructions, and the instruction at the counter as the output. ```text input[4] pc rom code[8, 4](addr = pc) output[8] instr(in = code.out) ``` No register primitive exists yet, so the program counter is an input pin the host drives; "stepping" is `set pc N`. ### 7.2 Author an image A memory image is a headerless raw file: `ceil(W/8)` bytes per word, little-endian, high padding bits zero, at most `2^A` words. For `W = 8`, that is one byte per word, so four instructions are four bytes. `printf` with octal escapes writes them from any POSIX shell (`\xHH` escapes are a bash/zsh extension; the octal form is the portable one): ```sh printf '\020\041\062\103' > prog.bin xxd prog.bin ``` ``` 00000000: 1021 3243 .!2C ``` Word 0 is `0x10`, word 1 `0x21`, word 2 `0x32`, word 3 `0x43`; the file omits words 4–15, and they read as undefined. ### 7.3 Preload it and walk the counter with `--sim` `--mem==` loads an image into the memory declared with that name before the session starts. Type the lines after the handshake (or pipe them in): ```sh zig-out/bin/circ-compile prog_rom.circ --sim --mem=code=prog.bin ``` ``` ready proto=1 pins=2 warnings=0 pin pc in 4 pin instr out 8 set pc 0 ok get instr ok 0x10 0xff set pc 3 ok get instr ok 0x43 0xff mem code 0 4 cells 4 0x0 0x10 0xff 0x1 0x21 0xff 0x2 0x32 0xff 0x3 0x43 0xff quit ok bye ``` Every reply is a `value mask` pair in hex; a mask of `0xff` means all eight bits are defined. `mem code 0 4` dumps four cells starting at address 0. Try `set pc 4` and `get instr` to see an unloaded cell come back as `0x0 0x0` (undefined), and `load code other.bin` to swap programs without leaving the session. The full verb list is in `DOCS/sim-protocol.md`. ### 7.4 A RAM, stepped by hand Save `tests/fixtures/circuits/sim_ram_write_read.circ` as `scratch_ram.circ`: ```text input[4] a input[8] d input w, clk ram data[8, 4](addr = a, din = d, we = w, clk = clk) output[8] q(in = data.out) ``` A `ram` reads asynchronously — `q` always shows the word at `a` — and writes `d` into that word on a *defined low → high* edge of `clk` while `w` is high. The clock is an ordinary input pin, so a pulse is two `set`s. Because the first level after power-on is undefined, drive the clock low once before the first rising edge: ```sh zig-out/bin/circ-compile scratch_ram.circ --sim ``` ``` ready proto=1 pins=5 warnings=0 pin a in 4 pin d in 8 pin w in 1 pin clk in 1 pin q out 8 mems mems 1 mem data ram 8 4 set a 2 ok set d 0x2a ok set w 1 ok set clk 0 ok set clk 1 ok get q ok 0x2a 0xff set a 3 ok get q ok 0x0 0x0 poke data 3 0x99 ok get q ok 0x99 0xff save data ram.bin ok words=16 quit ok bye ``` `poke` writes a cell from the host side without a clock, and `q` follows at once because `a` was still presenting address 3. `save` writes the whole memory as an image, turning undefined cells into `0x00`: ```sh xxd ram.bin ``` ``` 00000000: 0000 2a99 0000 0000 0000 0000 0000 0000 ..*............. ``` `poke`-then-`save` is also the quickest way to author an image interactively for `--mem` later. ### 7.5 The same ROM from Node A compiled artifact exposes the memory through the `mem*` exports. Compile `prog_rom.circ` and note the ids `--inspect` prints; `pc` is component `0` and `code` is `1` here (for multi-file projects, read them from the `circ.topology.v0.full` section, as [`DOCS/wasm-api.md`](https://circ-lang.org/reference/wasm-api.md) describes): ```sh zig-out/bin/circ-compile prog_rom.circ -o prog_rom.wasm zig-out/bin/circ-compile prog_rom.circ --inspect | grep 'kind=rom' # id=1 name=code kind=rom[W=8,A=4] width=8 ``` `run_rom.mjs` instantiates the module as in step 4, then loads the image through the staging buffer: ```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: () => {} }, }); const [topoSection] = WebAssembly.Module.customSections(mod, "circ.topology.v0.min"); const topoBytes = new Uint8Array(topoSection); const topoPtr = w.topology_alloc(topoBytes.length); new Uint8Array(w.memory.buffer).set(topoBytes, topoPtr); w.init(); const pc = 0, code = 1; // ids from `circ-compile prog_rom.circ --inspect` const info = w.getMemInfo(code); if (info < 0) throw new Error("not a memory"); const W = (info >> 8) & 0xff, A = info & 0xff; console.log(`code: W=${W} A=${A}`); const img = fs.readFileSync(process.argv[3]); const ptr = w.memBuffer(code); // may grow memory: re-view after new Uint8Array(w.memory.buffer).set(img, ptr); const rc = w.memLoad(code, img.length); if (rc !== 0) throw new Error(`memLoad failed with ${rc}`); for (const addr of [0n, 3n, 4n]) { w.setPin(pc, addr, 0xfn); // setPin settles; no run() needed console.log(`pc=${addr} -> instr=0x${w.getOutputValue(code).toString(16)} defined=0x${w.getOutputDefined(code).toString(16)}`); } const n = w.memStore(code); fs.writeFileSync("rom-after.bin", new Uint8Array(w.memory.buffer, w.memBuffer(code), n).slice()); console.log(`stored ${n} bytes`); ``` ```sh node run_rom.mjs prog_rom.wasm prog.bin xxd rom-after.bin ``` ``` code: W=8 A=4 pc=0 -> instr=0x10 defined=0xff pc=3 -> instr=0x43 defined=0xff pc=4 -> instr=0x0 defined=0x0 stored 16 bytes 00000000: 1021 3243 0000 0000 0000 0000 0000 0000 .!2C............ ``` `memStore` exports all `2^A` words, writing `value & defined` so the twelve undefined cells come out as zeros. Every mutator returns `0` on success or a negative status code (`-2` length not a whole number of words, `-3` a word with bits beyond `W`, …). The table is in [`DOCS/wasm-api.md`](https://circ-lang.org/reference/wasm-api.md). Read `getOutputValue(code)` directly on the memory's id, or on whatever it drives; the ROM's `out` is the same asynchronous read `instr` sees. ## Where to go next - `DOCS/circuit-format.md` — the complete `.circ` language reference (declarations, ports, anonymous components, built-ins). - `DOCS/language.md` §6.5 — the `rom`/`ram` reference: ports, X rules, the edge rule, and how contents get in. - `DOCS/wasm-api.md` — every export and import on the compiled artifact, plus the topology custom-section layout. - `DOCS/sim-protocol.md` — the `--sim` drive protocol, including `--mem` preloads and the memory verbs. - `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 that maps 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 a 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. The host drives input pins from outside the circuit; they 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) | | `rom` | `addr` | declared `[W, A]` (data width, address width); `addr` is `A` wide, `out` is `W` wide | Read-only memory of `2^A` words; contents are loaded at runtime (see [`language.md`](https://circ-lang.org/reference.md) §6.5) | | `ram` | `addr`, `din`, `we`, `clk` | declared `[W, A]`; `addr` is `A`, `din`/`out` are `W`, `we`/`clk` are 1 | Read/write memory; writes `din` on the rising edge of `clk` when `we` is high; contents loaded at runtime (see [`language.md`](https://circ-lang.org/reference.md) §6.5) | `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. Memories are the exception: they take exactly two width arguments, `[W, A]`, after the instance name. **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`, they need **no `import`**: 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`, and so on). - `[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 You can define a component inline as the value of a port connection. The nested component has no instance name, but its `.out` output wires directly to the enclosing port: ``` and combine ( a = pin1.out, b = not ( in = pin2.out ).out ) ``` Here an anonymous `not` gate drives 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` exposes no programmatic graph-construction API; the topology is baked into the artifact as a custom section and materialised by the embedded runtime at `init()`. Hosts see only 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 come from the `circ.topology.v0.full` section, where each record carries the component's name and kind. `circ-compile --inspect` prints resolver-local ids that match the artifact's only for a single-file circuit without sub-circuits. ## Grammar The maintainer's langlang fork (`-output-language zig`) generates the parser from a PEG grammar at `lib/grammar/proto-circ.peg`; the generated file is vendored at `lib/parser/parser.zig`, and `lib/syntax/translate.zig` walks it. Parse tree node types used by `lib/syntax/translate.zig`: | Node type (`parser.runtime.NodeType`) | Meaning | |---------------------------------------|------------------------------------------| | `.sequence` | Ordered list of child nodes | | `.node` | Named grammar rule match | | `.string` | Matched literal text (identifier, etc.) | | `.err` | A recovered parse error at this position | ## Topology Format Version Compiled `.wasm` artifacts embed the resolved circuit as a custom section. The current format identifier is `CIRC` (v03). v02 added a `width: u8` byte to every serialised component (the runtime materialises the corresponding pool tier on `init()`); v03 adds the memory kinds `rom = 8` and `ram = 9`, whose records carry one trailing `addr_width` byte, and the port bytes `addr`/`din`/`we`/`clk`. The topology excludes memory contents; they are loaded at runtime. 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 `lib/validator/codes.zig` registers the stable validator surface (E001–E018, W001–W003); the rules behind each code are in [`language.md`](https://circ-lang.org/reference.md) §4.1. The full catalogue follows, with each code's default message: | Code | Meaning | | --- | --- | | E001 | undeclared name | | E002 | unknown port (also an out-of-range or inverted slice, and an unknown memory port) | | E003 | multiple drivers for input port | | E004 | required input is unconnected | | E005 | duplicate instance name | | E006 | name shadows built-in (`and`, `not`, `wire`, `led`, `rom`, `ram`, `input_pin`, `output_pin`, …) | | E007 | output has no assigned driver | | E008 | combinational loop detected | | E009 | import not found | | E010 | import cycle detected | | E011 | import alias collision (with another import or a built-in name) | | E012 | unknown sub-circuit port | | E013 | sub-circuit arity mismatch (a required sub-circuit input left unconnected) | | E014 | width mismatch between a driver and the port it feeds (added with v02) | | E015 | sub-circuit is not parametric (caller passed `[N]` call-widths to a scalar callee; v02) | | E016 | parameter count mismatch (wrong number of `[w0, w1, ...]` call-widths at the call site; v02) | | E017 | memory parameter list malformed (a `rom`/`ram` declaration without exactly two instance-position width arguments `[W, A]`; v03) | | E018 | memory width out of range (`W` outside `1..64` or `A` outside `1..16`; v03) | | W001 | unused input declaration | | W002 | dangling output declaration | | W003 | unused import declaration | --- # 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 shipped to hosts). The optional `--emit-zig` path generates a standalone Zig source with a richer experimental export surface; that surface is unstable, and this document does not describe it. ## 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 runtime owns the buffer; do not call back into the runtime to free it. | If `debugEnabled` returns `0`, the runtime never calls `onDebugLog`, 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 getSimulationStatus: () => i32 // -1 before init, 0 usable, 1 settle budget exhausted (new artifacts) 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 // Memory family — meaningful only for ids of rom/ram components (see "Memory exports") getMemInfo: (id: i32) => i32 // (kind << 16) | (W << 8) | A, or -1 memBuffer: (id: i32) => i32 // pointer to the memory's staging buffer, or -1 memLoad: (id: i32, len: i32) => i32 // replace every cell from staging[0..len]; status memStore: (id: i32) => i32 // export every cell into the staging buffer; bytes written or a negative status memClear: (id: i32) => i32 // every cell becomes undefined; status setMemWord: (id: i32, addr: i32, value: i64, defined: i64) => i32 // write one word; status getMemValue: (id: i32, addr: i32) => i64 // BitVecState.value of one cell (0 on any error) getMemDefined: (id: i32, addr: i32) => i64 // BitVecState.defined of one cell (0 on any error) ``` 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` read as zero, and the runtime silently drops them 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 must copy 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. With no topology loaded, it initialises an *empty* circuit and latches, so the runtime ignores a later `topology_alloc` + `init()`; only a topology that fails to parse leaves the runtime uninitialised and `init()` retryable. Always copy the section before the first `init`. ### `run()` Drains the engine's event queue until empty or its settling work budget is exhausted. Settling delays are `5` time-units per gate (memories included) and `1` per `wire`/`output_pin`/`led`/`slice`/`concat` (see `lib/circuit.zig`); successful calls settle the cascade without host-driven ticks. `run()` is a no-op if `init()` has not run successfully. ### Settling failures and `getSimulationStatus()` New artifacts cap each settle at 1,000,000 work units (event pops, inertial candidate validations, downstream evaluations, and rejected-target recalculations). Gate-feedback circuits still compile, but a non-settling drive cannot keep the host inside WASM forever. This is a work budget, not a hard wall-clock limit; very large finite circuits can also exceed it. On exhaustion, `run`, `setPin`, `memLoad`, `memClear`, and `setMemWord` trap rather than silently returning success. The pending queue is discarded and the runtime remains failed: valid subsequent mutations and `run` trap until the host creates a fresh instance. `init()` remains idempotent and does not clear this failure. Output/memory value and defined-mask getters return zero; `memStore` refuses with `-1`. These undefined values are failure placeholders, not settled results. The additive `getSimulationStatus()` export distinguishes this failure from an unrelated WASM trap: `-1` means not initialized, `0` means usable, and `1` means the settle budget was exhausted. Hosts must feature-detect it for older artifacts. The playground maps status `1` to `E_NOSETTLE`, shows the failure, and supports reset. Existing artifacts need regeneration to receive the bound; the compiler/runtime/topology calling signatures otherwise remain unchanged. ### `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` settles the circuit before returning (it enqueues the change and drains the event queue itself), so you can read outputs immediately. Calling `run()` afterwards is a harmless no-op; the examples below keep it to make the drive/settle/read rhythm explicit. ### `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. `--inspect` prints resolver-local ids (`driver=.` under `Outputs (...)`) that equal the artifact's ids only for a single-file circuit without sub-circuits; for a project, read the `circ.topology.v0.full` section, whose records carry each component's name and kind. #### 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. ## Memory exports (`rom` / `ram`) A memory declares only its shape in source (`rom code[8, 4](addr = pc.out)`, `ram data[8, 4](addr = …, din = …, we = …, clk = …)`); its contents are runtime state that the host loads and reads back through the eight `mem*`/`getMem*` exports. Ids are the same positional component ids `setPin` uses. A memory's id is the id of its record in the `circ.topology.v0.full` section (kind `8` for `rom`, `9` for `ram`, matched by `name`); `getMemInfo` confirms the id and returns the widths, so a host can validate its mapping once up front. A memory's `out` is its asynchronous read port: `getOutputValue(mem_id)` / `getOutputDefined(mem_id)` return the word at the currently presented address, exactly like any other component's output. Cells that were never loaded or written read as undefined (`defined = 0`). A `ram` additionally writes `din` into the addressed cell on a *defined low → defined high* transition of `clk` while `we` is high; the clock is an ordinary input pin the host pulses with `setPin` (`setPin(clk, 0n, 1n)` then `setPin(clk, 1n, 1n)`). ### Image format Contents cross the boundary as a **headerless raw image**: word `i` occupies bytes `[i * bpw, (i + 1) * bpw)` with `bpw = ceil(W / 8)`, little-endian, and every bit at or above `W` must be zero. The image length must be a whole number of words and at most `2^A` words. A shorter image leaves the remaining cells undefined; an empty image clears the memory. Definedness is not representable on disk: `memLoad` marks every loaded word fully defined, and `memStore` writes `value & defined` (undefined bits become `0`). ### Staging protocol `memBuffer(id)` returns a pointer to a per-memory staging buffer sized for one full image (`bpw << A` bytes). The runtime allocates it once and reuses it for every later call; the allocation may grow linear memory, so **re-take your `Uint8Array` view of `memory.buffer` after each `memBuffer` call**. - **Load:** `ptr = memBuffer(id)`; copy the image bytes to `ptr`; `rc = memLoad(id, bytes.length)`; treat any `rc !== 0` as a failure (codes below). The whole memory is replaced. - **Export:** `n = memStore(id)`; the image is `memory.buffer[memBuffer(id) .. +n]`. - **Single words:** `setMemWord(id, addr, value, defined)` writes one cell (masked to `W`, canonicalised to `value & defined`); `getMemValue`/`getMemDefined(id, addr)` read one cell. - **Clear:** `memClear(id)` makes every cell undefined. Every mutator re-presents the memory's `out` before returning: after `memLoad`, `setMemWord`, or `memClear`, `getOutputValue` on the memory — or on anything it drives — already reflects the new contents, with no `run()` needed. A load or write that leaves the currently presented word unchanged does not advance simulated time. ### Status codes `memLoad`, `memStore`, `memClear`, and `setMemWord` return `0` on success, or: | Code | Meaning | |------|---------| | `-1` | runtime not initialised, id out of range, or the id is not a `rom`/`ram` | | `-2` | image length is not a whole number of words | | `-3` | a word has a bit set at or above the data width `W` | | `-4` | more words than the memory holds (`> 2^A`) | | `-5` | `len` is negative or exceeds the staging buffer | | `-6` | `memBuffer` was never called for this memory | | `-7` | `addr` is negative or `>= 2^A` | `memStore` returns the number of bytes written (`bpw << A`) on success. `getMemInfo` returns `-1` for anything that is not a memory. `-4` cannot occur through `memLoad`: the staging buffer is exactly one full image, so an oversize `len` is reported as `-5` before the image is parsed. The same image rules still apply to file-backed loading in `--sim`, where an oversize image is reported as an `E_MEMFMT` reason. These codes matter because `init()` swallows its own errors: a mutator's status is the host's one diagnostic. ### Node example ```js const mem = 1; // id of the kind-8/9 record named "code" in circ.topology.v0.full const info = w.getMemInfo(mem); if (info < 0) throw new Error("not a memory"); const W = (info >> 8) & 0xff, A = info & 0xff; // data width, address width // Load a raw image (here: word i = i * 0x11). const image = new Uint8Array(1 << A).map((_, i) => i * 0x11); const ptr = w.memBuffer(mem); // may grow memory: re-view after new Uint8Array(w.memory.buffer).set(image, ptr); const rc = w.memLoad(mem, image.length); if (rc !== 0) throw new Error(`memLoad failed with ${rc}`); w.setPin(pc_id, 3n, 0xfn); // present address 3 console.log(w.getOutputValue(mem), w.getOutputDefined(mem)); // 0x33n 0xffn w.setMemWord(mem, 3, 0x77n, 0xffn); // out follows at once, no run() needed const n = w.memStore(mem); // export: value & defined per word const dump = new Uint8Array(w.memory.buffer, w.memBuffer(mem), n).slice(); ``` ## Custom sections | Section name | Contents | |---------------------------|---------------------------------------------------------------------| | `circ.topology.v0.min` | Compact topology consumed by `init()`. Required. | | `circ.topology.v0.full` | Verbose topology for external tooling (names, origin chains, per-port labels); decode it with `lib/topology/full_decoder.zig`. The runtime never reads it, and the in-tree modes (`--preview`, `--sim`, `--truth-table`) build the same payload in process rather than reading it back. | `-Dwasm-optimize` defaults to ReleaseSmall and strips the runtime, so a shipped artifact carries no `name` section; only a `-Dwasm-optimize=Debug` build keeps one. 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 appear in the artifact that `circ-compile … -o out.wasm` produces 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 `┼` 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, and 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 ├○───▶┤ │ ╭─────╮ ╰───╯ │[xor:g]├○───▶┤ out │ ╭──▶┤ │ ╰─────╯ ╭───╮ │ ╰───────╯ │ 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`, `--inspect`, `--truth-table`, and `--sim`. `-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 a multi-bit `led[N]` (widths 2..7) as a row of `·` indicator glyphs inside its box, LSB on the left, instead of the `0x?` hex display; a width of 8 or more falls back to the hex display and warns on stderr. 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 port `┤` on the left edge like every sink; a multi-bit LED shows `0x?` (one `?` per nibble) or, with `--expand-display`, one `·` per bit | 5×3 (wider for the multi-bit labels) | | `subcircuit` (opaque) | `╭─...─╮` / `┤ │` / `│[:]├○` / `┤ │` / `╰─...─╯` — width grows to fit the label, one input port per odd border row | max(8, label width + 2) × (2·inputs + 1, at least 3) | | `rom` | `╭─...─╮` / `┤ rom [W,A] ├○` / `╰─...─╯` — one input port `addr` | (label width + 4) × 3 | | `ram` | same label as `rom`; input ports `addr`, `din`, `we`, `clk` on rows 1, 3, 5, 7 | (label width + 4) × 9 | The renderer truncates names longer than the cell width; 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.** The renderer routes wires as orthogonal segments and draws them with these glyphs: - `─` horizontal rail, `│` vertical rail. - `╭` `╮` `╰` `╯` corners between perpendicular segments. The renderer picks the glyph from the two segment directions: `{W,S} → ╮`, `{E,S} → ╭`, `{W,N} → ╯`, `{E,N} → ╰`. - `┬` `┴` `├` `┤` 3-way junctions. The same neighbour-inspection pass that handles corners picks them. Under the channel router every fan-out branches at a `●` tap, so no current golden contains one; the glyphs remain for a wire that branches into a T off another. - `┼` 4-way crossings — drawn on any cell where two wires cross and neither *diverges* there: two wires that share a source or a destination but merely pass over each other still get `┼`. The renderer prefers ┼ over the older "jump-arc" trick. - `●` fan-out / fan-in branch point — drawn where two wires of one net diverge (one corners at the cell while the other passes straight through), and on any source cell from which three or more wires leave. - `○` port-side bubble — drawn only on the cell immediately outside a component's *output* port (the cell where the wire begins); the destination end always carries an arrowhead. - `▶` `◀` `▲` `▼` 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 with 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 layout pipeline (collapse → layering → ordering → coords → channels, described below), 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 pin declared with a width annotation (`input[4] a`, `wire[8] bus`) renders with the width appended to its label as `[N]`; a multi-bit LED shows a hex placeholder instead (`led_4bit_default.circ`): ``` ╭──────╮ ╭─────╮ │ a[4] ├○───▶┤ 0x? │ ╰──────╯ ╰─────╯ ``` 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: no "bus" glyph exists. ### LED labels The preview is static, so it never knows a signal's value; an LED's label depends only on its width and the flags: | Label | When | |-------|------| | `LED` | width 1 | | `0x?…?` (one `?` per nibble) | width > 1 (default) | | `····` (one `·` per bit, LSB left) | width 2..7 with `--expand-display`; width 8 or more keeps the hex form and warns once on stderr | ## 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 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`, plus kind-dispatched aux bytes: `slice` carries `lo`/`hi`, `rom`/`ram` the address width) + connections. Magic `CIRC`, version `0x03`. The "lightweight" payload — what the runtime needs. | | `circ.topology.v0.full` | Adds per-component instance names + subcircuit-origin chains. Magic `CIRF`, version `0x03`. 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 layout. 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. ## How the layout is computed `lib/preview/layout/` turns the `full` topology into a `LayoutGrid` (boxes with port cells, wires as axis-aligned segments) that `lib/preview/render.zig` paints. It is a layered-graph layout in the Sugiyama tradition, with channel routing: 1. **Collapse** (`collapse.zig`). Wires, slices, and concats fold away; in opaque mode every primitive that came from one subcircuit instance becomes one box with a synthetic id. 2. **Layering** (`layering.zig`). Longest-path layers: input pins in layer 0, every other node one past its furthest upstream, `led`/`output` pins forced to the last layer. Edges that close a cycle (a DFS back edge) and edges that sink-forcing turns leftward are flagged as *back edges*. A forward edge spanning several layers is split through one dummy node per intermediate layer, so every later stage sees only layer-adjacent segments. 3. **Ordering** (`ordering.zig`). Inside each layer, barycenter sweeps down and up over the neighbours' *port* positions (an `and`'s `a` sits above its `b`), kept while they lower the crossing count, then adjacent swaps to a fixed point. Integer arithmetic throughout; ties fall back to ascending id. 4. **Coordinates** (`coords.zig`). Every node gets its own row: the top that makes the wire into its highest input port a straight horizontal, packed in the ordering with one free row between boxes; dummies are wire rows on their source's port row. A node with one outgoing wire may move down to straighten it. Columns are each layer's widest box plus the gap the router asks for. 5. **Channels** (`channels.zig`). The stage routes each inter-layer gap as a channel: a *net* is one source port and its sinks in the next layer; a net whose terminals share a row is a straight horizontal; every other net gets a vertical *track* in the gap, assigned by the left-edge rule under a constraint graph (a source rail and a sink rail on one row order their tracks so no rail reaches another net's corner). The router breaks constraint cycles with a dogleg on a free row, or a spacer row when none is free; back edges leave down a track to a *return row* below the diagram, run along it, and rise to their sink; a gap is `tracks + 2` cells wide (never under five). A net no track can hold falls back to a bounded cell search — counted, never silent. Every stage is deterministic: the same topology produces byte-identical output. `tests/preview/layout_conformance_test.zig` pins the grid of every previewable fixture as JSON under `tests/fixtures/preview/layouts-json/` plus one table of invariants (`tests/fixtures/preview/layout-invariants.golden`): no wire cell inside a box, no cell shared by two nets running the same way, no junction between nets that is not a clean `┼` crossing, every net a tree — asserted over the whole corpus, with the crossings, bends, and size of each fixture on record. The renderer at `circ-renderer/src/layout/` ports the same stages, so the canvas and the ASCII preview agree. ## 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. - **A macro input port the topology cannot name is drawn as one port.** The collapse stage maps every subcircuit input whose name is not `a`, `in`, or `b` onto `in`, so a parametric macro with inputs such as `data` and `select` receives two wires on one port cell; the two rails necessarily share that cell (a `●` merge in the picture). The six such fixtures are the only rows of `layout-invariants.golden` with a non-zero I1/I2, exempted by name of the cause, not of the fixture. - Dense circuits get taller and wider than they did under the fixed-gutter layout: every wire now owns its cells, a gap is as wide as its track count, and a return lane adds a row per back edge (`stress_grid_10x10` is 47×599 cells). The layout has no wrapping and no height cap. - 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, check the relevant fixture against the goldens under `tests/fixtures/preview/renders/`. --- # Simulation Protocol > The line-oriented protocol for driving circ simulations and memory. `circ-compile .circ --sim` compiles the circuit, then serves a small line-oriented request/response protocol over stdin/stdout. It lets an external process (a test runner, a REPL, an editor) drive the circuit by pin name: set inputs, settle, read outputs. It writes no artifact (only the `save` verb touches the filesystem, to write a memory image). The mode runs the same native simulation engine as `--truth-table`, built from the same resolved topology, so behavior matches the compiled `.wasm`. Each command maps 1:1 onto the artifact's WASM exports (see [`wasm-api.md`](https://circ-lang.org/reference/wasm-api.md)): `set` → `setPin`, `run` → `run`, `get` → `getOutputValue`/`getOutputDefined`, and for memories `load` → `memLoad`, `save` → `memStore`, `poke` → `setMemWord`, `peek` → `getMemValue`/`getMemDefined`, `clear` → `memClear`. ## Transport and framing - Line-oriented and synchronous: write one command per line to stdin; the process replies before you send the next. - A reply is either a **status line** (`ok ...` / `err `) or a **counted block**: a header line ending in a count, followed by exactly that many record lines. - The process ignores blank lines and lines starting with `#` and replies to neither (so you can drive a session by hand or from a script). - EOF on stdin, or `quit`, shuts the process down. ## Handshake The first output line is the handshake. On a circuit that compiles, with one `pin` record per top-level pin and one `diag` line per warning: ``` ready proto=1 pins=3 warnings=0 pin a in 1 pin b in 1 pin out out 1 ``` `pin `. A circuit with hard errors leaves no session to drive; the process emits the diagnostics and exits nonzero: ``` error diags=1 diag error E004 in.circ:3:1 required input 'b' is unconnected ``` `diag :: `, reusing the stable validator codes (`E001`-`E018`, `W001`-`W003`). ## Value and width encoding | Aspect | Rule | | --- | --- | | Input literals | Decimal `12`, hex `0x0c`, binary `0b1100`, octal `0o14` (`_` separators ok). | | Output literals | Lowercase hex, `0x`-prefixed (`0x0`, `0x1f`). | | Value + mask | Every signal is a `(value, mask)` pair; mask bit 1 = defined, mask bit 0 = undefined (X). | | Default mask | An omitted mask means fully defined (all bits of the pin's width). | | Undefined bits | Value bits where the mask is 0 are emitted as 0, matching the engine's `BitVecState` equality. | | Width | 1 to 64. A literal with bits set beyond the pin's width is an error (`E_WIDTH`), not silently masked. | ## Commands | Command | Reply | Notes | | --- | --- | --- | | `pins` | `pins ` block + `pin` lines | Re-query of the handshake pin table. | | `set []` | `ok` / `err` | Drives a top-level input and settles. `set a 0 0` is fully undefined; `set bus 0x5 0xf` is per-bit. | | `get ` | `ok ` / `err` | Reads current state (inputs or outputs). | | `dump ` | `vals ` block + ` ` lines | Whole-vector read for snapshots. | | `run` | `ok` | Drains the event queue. Redundant after `set` (which already settles); kept for 1:1 parity with the artifact's `run()`. | | `eval => ` | `ok =/ ...` / `err` | One-shot vector: `pin=value[/mask]` assignments, then the queried pins. Operates on current state (no implicit reset). | | `reset` | `ok` | Rebuilds the circuit to its post-`init` state (all pins undefined), then re-applies any `--mem` preloads. The rebuild drops contents from mid-session `load`/`poke`. Per-scenario isolation without respawning. | | `quit` | `ok bye` then exit | EOF on stdin does the same. | | `mems` | `mems ` block + `mem ` lines | The root-level memories, in declaration order (decimal widths). The `ready` block never lists them. | | `load ` | `ok words=` / `err` | Replaces every cell from a raw image file; `n` is the words loaded; cells `n..2^A-1` become undefined (an empty file clears). `out` follows at once. | | `save ` | `ok words=<2^A>` / `err` | Writes the whole memory as a raw image (`value & defined` per word; undefined bits become 0). | | `peek ` | `ok ` / `err` | Reads one cell. | | `poke []` | `ok` / `err` | Writes one cell and settles, like `set`. An omitted mask means fully defined. | | `mem [ []]` | `cells ` block + ` ` lines | Dumps a range (default: the whole memory). `start` past the end is `E_ADDR`; `count` is clipped to the end; `count` 0 gives an empty block. | | `clear ` | `ok` | Every cell becomes undefined and the circuit settles. | **Error codes** (`err `): `E_PROTO` (unknown/malformed command), `E_NOPIN` (no such pin — including a memory name given to `set`/`get`), `E_NOTIN` (`set` target is not a top-level input), `E_WIDTH` (bits beyond the pin or memory width), `E_BADVAL` (not a valid integer literal), `E_NOMEM` (no root-level memory of that name — including a pin name given to a memory verb), `E_IO : ` (`load`/`save` could not open, read, or write the file), `E_MEMFMT : ` (the image breaks a raw-image rule: length not a whole number of words, more words than the memory holds, a word with bits at or above the data width, or a file over the 16 MiB read cap), `E_ADDR ` (address `>= 2^A`). `E_NOSETTLE` means the circuit exhausted its settling work budget. `set`, `eval`, `run`, and memory mutations can report it. Afterward, reset the session before driving, reading values, or exporting memory. `pins`, `mems`, `reset`, and `quit` remain available. ## Settle model `set` drives an input and settles the circuit in one step, exactly as the shipped artifact's `setPin` does (the native engine's `propagateEvent` enqueues and then drains to quiescence). The protocol has no clock to pump: a single `set` settles any combinational cascade. For sequential circuits, state persists across commands, so a scenario is just `set`/`get` repeated; only `reset` clears it. Because each `set` settles independently, `eval` applies its assignments in order, each settling, before reading its queries. Each settle permits at most 1,000,000 work units: one per popped event, inertial candidate validation, downstream evaluation, or rejected-target recalculation. Exhaustion returns `err E_NOSETTLE settle work budget exceeded; reset required`, discards pending events, and invalidates the session's values until reset. Earlier writes in an `eval` may already have occurred; this is not a rollback. The cap is a deterministic safety budget, not a wall-clock deadline or proof of oscillation. Gate feedback remains legal for latches; passing `E008` validation does not guarantee that every input sequence settles. ## Memories `rom`/`ram` components declare only their shape in source; their contents are runtime state. `--sim` addresses a memory by its declared name, exactly as it addresses pins, and exposes contents two ways: - **`--mem==` on the command line** (repeatable, up to 16, also accepted by `--truth-table`) loads a raw image before the handshake. Every flag is checked first: the name must be a root-level memory, the file must be readable, and the image must pass the format rules. A failure prints one line to **stderr** and exits 2 before any handshake byte, so stdout stays a clean protocol channel: ``` --mem code=prog.bin: no memory named 'code' (declared memories: rom boot[8, 4]) --mem code=prog.bin: file not found --mem code=prog.bin: 17 words exceed capacity 16 ``` Because the pre-flight runs before the handshake, a preload failure prints none of the warnings that would have appeared as `diag` lines; fix the flag and re-run. - **The verbs above** (`load`, `save`, `peek`, `poke`, `mem`, `clear`) during a session. The image format is the headerless raw image described in [`wasm-api.md`](https://circ-lang.org/reference/wasm-api.md#memory-exports-rom--ram): `ceil(W/8)` little-endian bytes per word, bits at or above `W` clear, at most `2^A` words. Loading marks every loaded word fully defined; unloaded and unwritten cells read undefined; saving writes `value & defined`. Paths are single whitespace-free tokens (the protocol tokenizer splits on spaces and tabs and offers no quoting) and resolve against the process working directory, like the input path. Files are read with a 16 MiB cap, far above the largest legal image (512 KiB). `load` and `save` are the only commands that touch the filesystem; a `save` that fails mid-write may leave a partial file. Only root-level memories are addressable by name; a memory inside an imported or macro sub-circuit is reachable from a compiled artifact by id but not from `--sim`, mirroring the rule for pins. A ram writes on the defined low → high edge of its `clk` port while `we` is high, and `clk` is an ordinary input pin: pulse it with `set clk 0` then `set clk 1`. ## Example session ``` $ circ-compile and_gate.circ --sim ready proto=1 pins=3 warnings=0 pin a in 1 pin b in 1 pin out out 1 set a 1 ok set b 1 ok get out ok 0x1 0x1 eval a=1 b=0 => out ok out=0x0/0x1 quit ok bye ``` A memory session on `input[4] addr` → `rom code[8, 4](addr = addr.out)` → `output[8] out(in = code.out)` (a test in `lib/sim/loop.zig` replays this transcript verbatim, so it cannot drift from the code): ``` $ circ-compile rom.circ --sim ready proto=1 pins=2 warnings=0 pin addr in 4 pin out out 8 mems mems 1 mem code rom 8 4 poke code 3 0x2a ok set addr 3 ok get out ok 0x2a 0xff peek code 4 ok 0x0 0x0 mem code 2 3 cells 3 0x2 0x0 0x0 0x3 0x2a 0xff 0x4 0x0 0x0 clear code ok get out ok 0x0 0x0 reset ok quit ok bye ``` Loading from and saving to files follows the same shape: ``` load code prog.bin ok words=16 save code snapshot.bin ok words=16 load code nope.bin err E_IO nope.bin: FileNotFound ``` ## The protocol in the browser In Live and Truth, the playground's console sits in the drawer below the source and canvas. It speaks this protocol to the circuit the page compiled. The grammar is the CLI's (`site/src/scripts/sim-protocol.ts` copies `parseLine` and `parseValue` verb by verb), the replies are the CLI's (`site/src/scripts/sim-executor.ts` copies the strings of `lib/sim/loop.zig`), and `site/test/sim-transcripts.test.ts` replays every script in `tests/fixtures/sim/` through both and matches `tests/fixtures/expected-sim/` byte for byte. What follows is the list of differences, all of them about the browser having no process, no working directory and no stdin. - **The handshake prints when the session is built**, which is the first time the Live view, Data panel or a Truth-table action needs the compiled circuit's runtime, and again after every `reset`, whichever face caused it. Its `` is the root file's name, which is the selected editor file in a multi-file project. Its `warnings` count and `diag` lines come from the analysis the page ran on the same source. - **The log records every face.** A pin clicked on the canvas, a value typed in the Data panel, a cell written or a memory cleared in the memory panel, and a Reset pressed anywhere are logged as the line that would have done the same — `> set a 0x1`, `> poke data 0x2 0x5a`, `> clear data`, `> reset` — followed by the reply the session gave, in the order they happened. A partly-known value carries its mask (`> set a 0x1 0x3`). A ROM image edited or loaded in the memory panel is applied as a `--mem` preload, not as a `load`, so it is logged as a comment naming the memory and the word count. - **The page boots low.** Before the first `reset` the circuit is in the state the canvas has always shown: every root input driven to 0 and settled once. `reset` (and `quit`) leave every pin undefined, as after `init()`, so from then on the session is a fresh `--sim` process. A script that depends on floating pins begins with `reset`. - **`quit` prints `ok bye` and is otherwise `reset`.** There is no process to end; the prompt stays live. - **`load` and `save` use the memory panel.** The page has no working directory, so both verbs are refused in the protocol's own shape, and the reason says where to go: ``` load code prog.bin err E_IO prog.bin: load images in the memory panel save code snapshot.bin err E_IO snapshot.bin: save images from the memory panel ``` The memory panel's `Load image…` opens the ROM file picker and hex editor; `Save` downloads `.bin` with the bytes `save` would write. Preloads are the memory panel's ROM images: they are applied when the session is built and again on every `reset`, as `--mem` is. - **`help` is a browser-only verb.** It prints the command table as `#` comment lines. The CLI answers `help` itself with `err E_PROTO malformed command`, so a copied script that contains it prints one error and goes on. - **No line-length cap.** The CLI's `err E_PROTO command line exceeds limit` is never printed; a browser line has no 8 KiB buffer to overflow. - **The log is a transcript; `Copy script` is the script.** Every line in the Console's scrollback is a reply, the echo of a line (`> …`) or a comment (`# …`). The CLI would refuse an echo as written (`> set a 1` is a malformed command), so the console's `Copy script` button copies the lines `--sim` accepts — each echo without its `> `, and the comments — and drops every reply. Saved as a `.script` and fed to `circ-compile .circ --sim`, it replays what the reader did; `Copy log` copies the whole scrollback. The closed terminal line shows the last command and reply. Click it or focus it to open the drawer; its height is resizable and saved. Declared memories appear beside the console, or below it on a narrow screen. A defined address highlights its word in the grid. Close folds the drawer; Escape in a nonempty prompt clears the line, and Escape in an empty prompt folds it. Schematic hides the whole console/memory row. Returning to Live or Truth restores its previous open/closed state and height. Selecting another source file starts a fresh circuit and console for that file. Its sibling files remain available for imports, so an imported circuit can be exercised on its own. Pin state resets on this switch; ROM images stay with their source file and initialize each imported instance at build and reset. Changing only the output view keeps the same session. Images are saved with the project when they fit the saved-state budget; the page reports when they are too large to survive a reload. Console memory commands still address only memories declared in the selected root file. ## Agent simulation controls The playground agent contract uses the same session as the canvas, Data panel, memory panel, and console. `circ_drive` has the ordering of `eval`: assignments settle one at a time. It accepts and returns canonical lowercase `0x` strings for values and defined masks so 64-bit states do not cross JSON as numbers. `circ_reset` is the protocol reset and creates a new session identity. Root memory pages and mutations map to `peek`/`mem`, `poke`, `clear`, and `load`; source ROM preloads are separately addressed by project/file/declaration. `circ_run_verification` uses disposable sessions and writes no visible console records. It can run ordered vectors and stateful scenarios, but cannot read or copy live state. See `agent-playground.md` for revision preconditions, limits, and result paging. --- # Agent Playground > Set up a browser agent to discover, author, simulate, inspect, and hand off circ playground projects. Use **Connect agent** beside Share to copy an active-project handoff into ChatGPT Desktop, Claude Code, Claude in Chrome, or another browser-capable agent. The prompt includes a freshly generated Share URL containing all current file bodies, including unsaved edits, plus the project name and selected entry file. It instructs the agent to restore the snapshot in a new tab and select that entry before asking for its next task. It prefers native WebMCP and includes the public page-registry fallback. Like Share, it transfers source, not ROM images, live state, settings, or artifacts. If the source exceeds the Share URL cap, the prompt includes the combined source as JSON for import instead. If clipboard access is blocked, a selected text area provides the same handoff for manual copying. `/playground` exposes a browser-resident tool contract for an agent working beside a person. Compilation, simulation, project storage, and knowledge search run locally in the browser. The static deployment has no application backend, relay, custom MCP server, or custom browser extension. Tool arguments and bounded results requested by a user are shared with the connected agent through that client's browser integration or browser-control tool. They are subject to that client and provider's policies. The early share-fragment scrub prevents source-bearing fragments reaching deferred page integrations; it is not a claim that the page makes no network requests. The playground status line reports observed tool activity, not a persistent connection. It starts at `agent tools ready · no requests yet`, shows `agent active` while a discovery or tool request is being handled (with a short visible grace period), then settles to `agent used tools recently`. Native registration alone does not activate the indicator, and the page cannot identify or count agents. ## Setup 1. Build the site from `site/` with `bun --bun run build`. 2. Start one static preview with `bun --bun run preview --host 127.0.0.1` and use its printed loopback URL, or use an authorized static host. 3. Open `/playground` in the client-specific clean browser profile. Do not reuse another client's profile or storage. 4. Select the visible tab using the client's verified browser connection mechanism. 5. Discover tool schemas before invoking a tool. Read `circ_get_status` before every revision-guarded mutation. Native WebMCP is available only when the browser exposes `document.modelContext.registerTool` or `navigator.modelContext.registerTool`. It routes to the same registry as the external API and is abort-scoped on page suspension/disposal. A provider that rejects an undiscovered tool name has not called the application; record that as a provider result, not an application code. The external page-registry fallback is `window.circPlayground`: ```js async () => window.circPlayground?.listTools() async () => window.circPlayground?.callTool('circ_get_status', {}) ``` Select the tab before evaluating either expression. Pass user text as browser-tool arguments, never by interpolating it into evaluated JavaScript. OpenCode and Claude Code require separately configured browser tooling. Native WebMCP tooling also requires its browser/tool-specific experimental support. ## Workflows The public catalogue covers discovery, corpus help, project/file reads, authoring, compilation, diagnostics, simulation, verification, inspection, visible workbench controls, source/share/transcript handoff, and browser download dispatch. Every result is JSON-safe and capped at 32 KiB. For the acceptance fixture, use the frozen corpus records `ref:language:parametric-sub-circuits`, `diagnostic:E014`, `tour-example:full-adder-imports`, `example:half-adder`, `example:rom-lookup`, and `example:ram-write-read`. Read the corpus/compiler identities first; a mismatch is a preflight failure. 1. Discover schemas and call `circ_get_status`; compare the returned project, selected entry, view, and session state with the visible page. 2. Use `circ_help` to read the reference and diagnostic, then retrieve the named tour and examples. Help is read-only and does not select, compile, or simulate a project. 3. Create/open a scratch project, read the manifest, and make only revision-guarded edits. Wait for `circ_compile`, read file-local diagnostics, repair, and compile a current artifact. 4. Call `circ_prepare_simulation`, wait if necessary, then use `circ_drive` with current artifact/session/live-state provenance. Its `assignments` array contains `{ pin, value, defined? }` items and `queries` contains pin-name strings; assignments settle one at a time. `circ_update_memory.action` is exactly one of `{ kind: 'poke', address, value, defined? }`, `{ kind: 'clear' }`, or `{ kind: 'load', hex }`. 5. Use `circ_run_verification` for isolated bounded vectors. It never changes the live session, UI, or console. 6. Request/read schematic and Truth results, read bounded topology, set supported workbench/view state, and apply then clear a source highlight. Confirm each changed state in the visible workbench. 7. Use `circ_export_source`, `circ_create_share_link`, and `circ_get_transcript` for bounded handoff. Source/share omit images, live RAM, live pins, settings, transcript, and artifacts as applicable. 8. `circ_download_artifact` and `circ_download_memory` prove dispatch only. Capture browser downloads outside the tool channel to prove completion; compare one captured filename and byte length with each receipt and record SHA-256. WASM validation includes topology/provenance decoding and vectors; source/live memory bytes compare against their source/readback. `example:ram-write-read` supplies runtime RAM initialization, not a source preload. Load its corpus image with `circ_update_memory`, read `q=0xa5/defined=0xff` at `a=0x5`, then perform the ordered `we=1, clk=0, clk=1` write and read `q=0x3c/defined=0xff`. After `circ_reset`, drive the address again and expect an undefined `q`: live RAM is reset/replacement/reload state. `circ_set_memory_preload` accepts only ROM declarations and must return `PRELOAD_CONFLICT` for RAM. ## Lifecycle And Bounds Page, project, source, target, artifact, image, session, operation, result, cursor, and presentation identities are page-scoped. Re-read after a conflict; stale calls are refused instead of applying to newer work. Reload creates a new page ID and registration. A genuine bfcache restoration retains the page ID, resumes the registry, and re-registers native tools. `pagehide` aborts native registrations before a non-cached page facade is discarded, without an `unload` listener that would disqualify Chrome bfcache. Project/files/entry/settings/source preloads use the documented local-storage envelope when persistence succeeds. Sessions, live pins/RAM, highlights, operations, result stores, and page IDs are memory-only. Storage failure is reported as `memory_only` or refusal; never modify local storage behind the app to manufacture a pass. - Tool input is limited to 128 KiB; result envelopes are limited to 32 KiB. - Source and stores have their published caps; text/topology/diagnostic/Truth/verification results page with provenance-bound cursors. - Full topology is limited to a 4 MiB section, 32,768 components, and 65,536 connections. - Truth refuses RAM and projected work above 4,096 rows; filtered Truth requires its documented live-session scope. - Verification permits at most 128 cases, 512 steps, 2,048 actions/assertions, and a 10-second deadline. - Unknown semantic tools and retained-facade `PAGE_DISPOSED` are page-registry cases. Native unknown-name rejection and registration disappearance after reload are provider/lifecycle observations. ## Privacy And Exports Local computation does not make requested tool data private from the connected agent. Do not use credentials, personal source, account information, cookies, or browser-profile paths in acceptance evidence. Record only public fixture inputs, provider/browser/tool versions, app commit, result codes, relevant provenance, payload sizes, and hashes. Replace local origins/paths with neutral labels when publication would reveal machine-specific information. Artifact and memory receipts report `state: "dispatched"` and `completionKnown: false`; the application neither reports a filesystem path nor claims a completed download. Normal browser controls cannot reliably induce pre-dispatch `DOWNLOAD_FAILED`; that branch remains automated coverage, while real-client runs record successful dispatch or a provider/browser block after dispatch. ## Troubleshooting - `REVISION_CONFLICT`, `TARGET_CONFLICT`, `ARTIFACT_CONFLICT`, `IMAGE_CONFLICT`, or session/live-state conflicts: reread status and retry with the returned provenance. - `PAGE_SUSPENDED` or `PAGE_DISPOSED`: return the tab to view or rediscover after reload; do not reuse a retained facade. - `HELP_UNAVAILABLE`, `HELP_INVALID_CORPUS`, `HELP_CORPUS_CHANGED`, `HELP_NOT_FOUND`, `HELP_REMOVED`, or `HELP_BUSY`: retain the reported corpus identity and retry only as the code permits. - Native tool unavailable/rejected: use the page registry when the client has an evaluator. This fallback does not convert native absence into a native pass. - Browser download blocked: preserve the dispatch receipt and record the browser/provider observation. Do not report the receipt as file completion. ## Compatibility And Evidence | Client / product | Access path | Current Phase 6 result | | --- | --- | --- | | OpenCode | Native WebMCP and page registry through attached Chrome tooling | Author/repair, full-adder, half-adder, ROM, RAM, inspection, handoff, persistence, and reload observations recorded; same-origin bfcache restoration demonstrated. | | ChatGPT/Codex in-app browser | Native WebMCP | Fresh-session retest discovered 35 tools, loaded/drove RAM, and returned successful status with a new page ID after reload. Product/browser versions were not exposed. | | Claude Code 2.1.270 / Chrome 153.0.8010.36 | Page registry through Claude in Chrome MCP | RAM load/write/reset, half-adder verification, inspection, handoff, and reload passed in the reported workflow. Native API was unsupported; Back produced a new document. | Automated tests cover the registry, adapter, built-page public surface, documentation registration, and prior-phase regressions. They are not substitutes for client evidence. The repository archive at `DOCS/archive/plan-agent-driven-playground.md` preserves the evidence summary and canonical commit containing the full STATUS history. The initiative was closed for archiving at the user's request with the storage/edge matrix deferred. The original full Phase 6 signoff, including a unified three-client run against one frozen application commit, is not claimed. ### OpenCode browser evidence, 2026-09-12 Attached Chrome DevTools tooling used the visible tab at `[loopback preview]/playground` after the Phase 6 site gate. The application commit was `7af1faafb18770d677a19d5e559953662dd9ea52`; the tested build also contained uncommitted Phase 6 documentation/copy-only changes, so this is not final frozen-commit acceptance. Bun was `1.2.10`, Node was `v24.20.0`, Chrome reported `153.0.0.0` to the attached tooling, and the client was this OpenCode browser-tool session. The native API was `document.modelContext.registerTool`; the compiler was `0.0.3/e8869c8`; corpus ID was `ef1f2fb0918d8fad1248b957784557442b26140e432751767ac091428ecd42c7`. - Native discovery listed 33 tools before and after reload. Native status and `diagnostic:E014` help returned `ok: true`; the help result's active compiler comparison was `match`. - The page registry independently listed the same 33 tools. A malformed known call returned application `INVALID_ARGUMENT`; `phase6_unknown_tool` returned application `UNKNOWN_TOOL`. - On the existing public NOT-chain project, page-registry drive of `a=0x0` returned `out=0x1/defined=0x1`; isolated verification passed 2 cases, 2 steps, and 2 assertions. Schematic returned the three-NOT path, topology returned 5 components/4 connections, and exhaustive Truth returned `0 -> 1`, `1 -> 0`. Source/share/transcript handoff returned the documented omissions. The visible page showed the corresponding Truth view, Data card, source, schematic, and local-computation disclosure. - Native artifact dispatch returned receipt `download:126`, `not-chain.wasm`, 23,995 bytes, `state: dispatched`, `completionKnown: false`. This tooling could not expose a captured browser-download file, filename, byte count, or SHA-256; no completion claim is made. - Full reload replaced page ID `21bc63f2-be10-4d7d-854a-19aeee8ee733` with `7c0160e4-e1f0-4f4d-a440-9782e0594ce6`; native tools rediscovered and native status again returned `registered`. This proves reload/re-registration, not bfcache. The initial stable-ID and workspace-revision blockers were corrected and rerun in a new isolated browser context. The regenerated corpus is `a466f055a7de0274ba7531547a09f4e18bfb186772e57b587842fa6c218b6f2a`; `tour-example:full-adder-imports` now returns the existing `half_adder.circ`/`root.circ` project, and status publishes `workspaceRevision` for create/open preconditions. - The isolated profile created a persisted two-file scratch project, changed root `output sum` to `output[2] sum`, received compile-owned `E014`, repaired the exact range, and rebuilt a current artifact. All eight `(a,b,cin)` vectors returned binary-addition `sum`/`cout` values with scalar `defined: 0x1`. - `example:half-adder` returned the four required vectors and isolated verification passed 4 cases, 4 steps, and 8 assertions. Full-adder exhaustive Truth returned eight rows; workbench settings/view, source highlight/clear, and visible source/Schematic/Data state agreed. Reload restored the persisted scratch project, replaced the page ID, and re-registered 33 native tools. - `example:rom-lookup` accepted its corpus ROM image, returned `out=0x90/defined=0xff` at `pc=0xc`, and dispatched source/share/WASM/preload/transcript handoffs with their stated omissions. No browser download capture/hash is available through this tooling. - `example:ram-write-read` is a live-memory acceptance pass: its corpus image loaded with `circ_update_memory` yielded `q=0xa5/defined=0xff` at `a=0x5`, and the ordered write yielded `q=0x3c/defined=0xff`. `circ_set_memory_preload` correctly refused RAM with `PRELOAD_CONFLICT`; after reset and an explicit address drive, `q` was undefined. This is the documented live-RAM reset contract, not a source-preload failure. The final attached-Chrome rerun reloaded the rebuilt page, rediscovered 33 native tools, and repeated that RAM sequence through the page registry. Its fresh page ID was `bf3c2caf-f2b6-4f64-a123-a582e40170ef`; corpus `a466f055a7de0274ba7531547a09f4e18bfb186772e57b587842fa6c218b6f2a` returned the runtime image. The session reset to `...:session:127`, and the post-reset drive returned `q.value=0x0`, `q.defined=0x0`. Native `circ_set_view` then returned a JSON-safe success response with its retained `session_build` operation ID; the visible Data panel showed `q` as `?`. No console warnings/errors were observed. After removing the redundant `unload` teardown listener, attached Chrome navigated same-origin from `/playground` to `/gallery` and Back. The public registry kept page ID `331221c6-734a-48da-8c90-4991ecee286d`; native discovery reappeared and native `circ_get_status` succeeded with that same ID. This demonstrates bfcache restoration in this Chrome/tooling context, not full Phase 6 acceptance. Subsequent user-supplied evidence completed the scoped ChatGPT/Codex native RAM/reload retest and Claude Code page-registry RAM/verification/inspection/handoff/reload workflow summarized above. External filesystem capture found `half-adder.wasm` at 25,140 bytes (matching Claude's receipt), SHA-256 `71a2521b4fc6ad0164a2e883aa96574b2c0ad8975ee6e012b41eef192c5133ba`, and `not-chain.wasm` at 23,995 bytes, SHA-256 `9f674cf5810c67b3959f91fdc232e6f43380f92c03225d035cd9eca191bf738e`. These observations span iterative builds. The complete native/page-registry edge matrix, storage/quota scenarios, and one frozen-commit final rerun remain deferred rather than passed. --- # Tour examples > Retained multi-step playground examples with stable source anchors. ## 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) ``` ## 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 gate's output goes to a single output pin named `out`. ### Source ```circ input a, b and g(a=a, b=b) output out(in=g.out) ``` ## Naming an intermediate signal A `wire` is a one-port pass-through: after evaluation, the value on `in` is exactly the value on `out`. It exists so you can give a derived signal a name. Without `clock_buf` here, you would wire `gate.a` straight 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) ``` ## Anonymous nested components You can instantiate a component inline as the value of a port. The inverter here has no instance name; its `.out` wires straight into `gate1`'s `b` port. The same wiring rules apply: anonymous nesting is syntactic sugar for declaring an unnamed component. ### Source ```circ input a input b and gate1(a=a, b=not(in=b).out) output out(in=gate1.out) ``` ## 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. A single file needs no `import` line to use a macro; the explicit form shown here still works. This is the classic Nand2Tetris milestone. ### 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) ``` ## 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) ``` ## Stable feedback through wires The validator rejects combinational feedback at compile time: a chain of gates whose output drives its own input is a hard error (`E008`). But it looks at the signal graph, not the textual order, and a loop that passes through a gate is sequential logic, not a combinational loop. Here two `not` gates connect end to end through two `wire` pass-throughs: the ring closes, but the NOT gates break it, 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) ``` --- # Gallery > A collection of `circ` circuits — primitives, multi-bit arithmetic, and stateful latches. Each entry shows the source and its `--preview` output. ## NOT chain Three inverters in series. The output is the inverse of `a`, and the chain still compiles and simulates 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 │ ╰───╯ ╰───╯ ╰───╯ ╰───╯ ╰─────╯ ``` ## 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 ```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 │ ╰───╯ ╰────╯ ``` ## 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 ```circ input a, b xor g(a=a, b=b) output out(in=g.out) ``` ### `circ-compile --preview` ``` ╭───╮ ╭───────╮ │ a ├○───▶┤ │ ╭─────╮ ╰───╯ │[xor:g]├○───▶┤ out │ ╭──▶┤ │ ╰─────╯ ╭───╮ │ ╰───────╯ │ b ├○╯ ╰───╯ ``` Source file in the repo: `tests/fixtures/circuits/builtin_xor.circ`. ## 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 ```circ input[4] a output[4] o(in={a[0..2], a[2..4]}) ``` ### `circ-compile --preview` ``` ╭──────╮ ╭──────╮ │ a[4] ├○───▶┤ o[4] │ ╰──────╯ ╰──────╯ ``` Source file in the repo: `tests/fixtures/circuits/slice_then_concat.circ`. ## 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 ```circ input[8] a not[8] inv(in=a) output[8] o(in=inv.out) ``` ### `circ-compile --preview` ``` ╭──────╮ ╭───╮ ╭──────╮ │ a[8] ├○───▶┤NOT├○───▶┤ o[8] │ ╰──────╯ ╰───╯ ╰──────╯ ``` Source file in the repo: `tests/fixtures/circuits/multibit_not_full.circ`. ## 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 ├○●──▶┤ │ ╭───────╮ ╰───╯ │ │AND├○───────▶┤ carry │ │╭─▶┤ │ ╰───────╯ ╭───╮ ││ ╰───╯ │ b ├○┼╯ ╰───╯ ││ ╭───────╮ ╰┼─▶┤ │ ╭─────╮ │ │[xor:s]├○───▶┤ sum │ ╰─▶┤ │ ╰─────╯ ╰───────╯ ``` ## 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 ├○──╮ ╰───╯ │ │ ╭─────╮ │ ╭───╮ ╭───╮ │ sel ├○┼●─▶┤NOT├○───▶┤ │ ╭───────────╮ ╰─────╯ ││ ╰───╯ │AND├○───▶┤ │ ╭─────╮ ╰┼───────────▶┤ │ │[or:out_or]├○───▶┤ out │ │ ╭───╮ ╰───╯ ╭──▶┤ │ ╰─────╯ ╰─▶┤ │ │ ╰───────────╯ ╭───╮ │AND├○──────────╯ │ b ├○─────▶┤ │ ╰───╯ ╰───╯ ``` ## 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 ```circ // 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 │ ╰─▶┤ │ ╰───────╯ ╰───╯ ``` Source file in the repo: `tests/fixtures/circuits/demux_1to2.circ`. ## 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 ├○──●──▶┤ │ ╭───────╮ ╰───╯ │ │AND├○───────────────────────▶┤ │ ╭──────╮ │╭─▶┤ │ │[or:c3]├○───▶┤ cout │ ╭───╮ ││ ╰───╯ ╭──▶┤ │ ╰──────╯ │ b ├○──┼╯ │ ╰───────╯ ╰───╯ ││ ╭────────╮ │ ╰┼─▶┤ │ ╭───╮ │ ╭─────╮ │ │[xor:s1]├○●──▶┤ │ │ │ cin ├○╮╰─▶┤ │ │ │AND├○─────╯ ╰─────╯ │ ╰────────╯ │╭─▶┤ │ ●──────────────┼╯ ╰───╯ │ │ │ │ ╭────────╮ │ ╰──▶┤ │ ╭─────╮ │ │[xor:s2]├○─────────────────▶┤ sum │ ╰─────────────────▶┤ │ ╰─────╯ ╰────────╯ ``` ## 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 ```circ 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] │ ╰───────╯ ╰───────────────╯ ╰────────╯ ``` Source file in the repo: `tests/fixtures/circuits/rom_lookup.circ`. ### Memory initialization - `code` (load before driving inputs): `00010409101924314051647990a9c4e1` ## 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 ```circ import xor "/xor.circ" import or "/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]├ ╰─▶┤ │ ╰────────╯ ``` ## 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 ```circ import xor "/xor.circ" import or "/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]├ ╰───╯ ╰──▶┤ │ ╰────────╯ ``` ## 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 ```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├○───────────────────────▶┤ │ ╭───╮ ╰───╯ ╰───╯ │AND├○●──▶┤NOT├○───╮ ╭──▶┤ │ │ ╰───╯ │ ╭───╮ ╭───╮ ╭───╮ │ ╰───╯ │ │ │ s ├○───▶┤NOT├○───▶┤ │ ╭───╮ │ │ ╭───╮ │ ╰───╯ ╰───╯ │AND├○●──▶┤NOT├○╯ ╰──▶┤ q │ │ ╭──▶┤ │ │ ╰───╯ ╰───╯ │ │ ╰───╯ ╰───────────────────╮ │ │ │ ╭──────╮ │ │ ╰──▶┤ qbar │ │ │ ╰──────╯ │ ╰──────────────────────────────────────────╯ ``` ## 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 ```circ 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 ├○─╯ ╰─────╯ ``` Source file in the repo: `tests/fixtures/circuits/ram_write_read.circ`. ### Memory initialization - `data` (load before driving inputs): `a0a1a2a3a4a5a6a7a8a9aaabacadaeaf` --- # Download > Pre-built `circ-compile` binaries for Linux, macOS, and Windows. Alpha software — expect breaking changes between builds. Latest build: `0.0.3`. ## 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.3-linux-x86_64.tar.gz`](https://circ-lang.org/downloads/circ-compile-0.0.3-linux-x86_64.tar.gz) | Kernel 3.2+ | | macOS | aarch64 (Apple Silicon) | [`circ-compile-0.0.3-macos-aarch64.tar.gz`](https://circ-lang.org/downloads/circ-compile-0.0.3-macos-aarch64.tar.gz) | 11 (Big Sur) | | Windows | x86_64 | [`circ-compile-0.0.3-windows-x86_64.zip`](https://circ-lang.org/downloads/circ-compile-0.0.3-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).