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  [<width>] <name> [, <name> ...]
output [<width>] <name> [, <name> ...]

<type> [<width>] <instance-name> [<call-widths>] (
    <port> = <signal> [,
    <port> = <signal> ...]
)

<width> is a literal [N] (or a parameter inside a parametric sub-circuit; see language.md §6.3). <call-widths> 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 <W> introduction form, built-in parametric macros, and diagnostic semantics), see language.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>[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>[W] declares both a width parameter and an input of that width (see language.md §6.3).

Component Instances

<type> <instance-name> (
    <input-port> = <signal>,
    ...
)

<type> is the gate kind. Recognised types:

TypePortsWidth ruleDescription
anda, bbit-parallel; both inputs must match the instance widthAND gate
notinbit-parallel; input must match the instance widthNOT gate (inverter)
ledininput must match the instance width; widths > 1 render as a numeric displayOutput indicator
wireininput must match the instance widthPass-through
outputininput must match the instance widthExternally observable output pin (special-cased declaration kind)
romaddrdeclared [W, A] (data width, address width); addr is A wide, out is W wideRead-only memory of 2^A words; contents are loaded at runtime (see language.md §6.5)
ramaddr, din, we, clkdeclared [W, A]; addr is A, din/out are W, we/clk are 1Read/write memory; writes din on the rising edge of clk when we is high; contents loaded at runtime (see language.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 "<builtin>/<name>.circ" were present. Each macro is parametric: or[8] g(a=x, b=y) produces a bit-parallel 8-bit OR.

TypePortsExpansion
ora, bOR from not/and
nanda, bNAND
nora, bNOR (uses internal or macro)
xora, bXOR
xnora, bXNOR (uses internal xor macro)

<input-port> is the name of the port being driven (e.g. a, b, in).

<signal> is one of:

  • <instance-name>.out — the output of a named component or primitive with a single implicit output .out.
  • <instance-name>.<port> — the output pin of an imported subcircuit that exposes <port> (sum, carry, and so on).
  • <signal>[i] — single-bit index into a multi-bit signal (yields width 1).
  • <signal>[lo..hi] — half-open slice [lo, hi) of a multi-bit signal (yields width hi - lo).
  • {<signal>, <signal>, ...} — 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 §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; 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
.sequenceOrdered list of child nodes
.nodeNamed grammar rule match
.stringMatched literal text (identifier, etc.)
.errA 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 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 §4.1. The full catalogue follows, with each code’s default message:

CodeMeaning
E001undeclared name
E002unknown port (also an out-of-range or inverted slice, and an unknown memory port)
E003multiple drivers for input port
E004required input is unconnected
E005duplicate instance name
E006name shadows built-in (and, not, wire, led, rom, ram, input_pin, output_pin, …)
E007output has no assigned driver
E008combinational loop detected
E009import not found
E010import cycle detected
E011import alias collision (with another import or a built-in name)
E012unknown sub-circuit port
E013sub-circuit arity mismatch (a required sub-circuit input left unconnected)
E014width mismatch between a driver and the port it feeds (added with v02)
E015sub-circuit is not parametric (caller passed [N] call-widths to a scalar callee; v02)
E016parameter count mismatch (wrong number of [w0, w1, ...] call-widths at the call site; v02)
E017memory parameter list malformed (a rom/ram declaration without exactly two instance-position width arguments [W, A]; v03)
E018memory width out of range (W outside 1..64 or A outside 1..16; v03)
W001unused input declaration
W002dangling output declaration
W003unused import declaration