Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ Facts that materially shape edits:
4. **Propagation is per-timestamp batched.** `propagate()` drains every event at the current timestamp `T` in Phase 1 (commit state, collect changed), then in Phase 2 walks the outputs of changed components, recalculating and rescheduling. Without that batching, a downstream gate with multiple upstream events at the same `T` can read partial state, dedup the corrective re-enqueue, and stick on the wrong final value. See `DOCS/simulation-engine.md` for the full rationale.
5. **Delays are compile-time constants:** `PROPAGATION_DELAY = 5`, `WIRE_PROPAGATION_DELAY = 1`. `wire`, `output_pin`, and `led` use the wire delay; everything else uses the gate delay.
6. **The allocator is global, not parameterised.** Allocations route through `memory.allocator` from `lib/memory.zig`. On WASM that is `std.heap.wasm_allocator`; on native (test builds) it is a `GeneralPurposeAllocator`. Do not add an allocator parameter to engine functions.
7. **Today only width=1 (tier 0) is wired; wider widths trap.** The `BitVecState` / `PoolHandle` split exists so wider widths can be added by introducing a new pool tier without re-touching the propagator. This is the active multi-tier work tracked in `DOCS/plan-multi-bit-language.md`.
7. **Widths 1 through 64 are wired; pool tiers are lazily allocated per width.** Each tier indexes a separate SoA pool (`tier == width`; tier 0 is unused). `PoolHandle.tier`/`.slot` make every read/write a single dispatch followed by a direct bitmap op against the tier's `(values, defined)` u64 buffers. The width=1 tier packs 64 slots per word; wider tiers store one u64 per slot. Widths > 64 trap at allocation time. The historical roll-out lives in `DOCS/archive/plan-multi-bit-language.md`.

`COLLECT_METRICS` is a compile-time switch wired by `build.zig` per consumer: `false` for native and WASM, `true` only for `zig build bench`. When false, `Circuit.metrics` is `void` and every counter bump is dead-code stripped, so production and test builds are byte-identical to a metrics-free engine.

Expand Down
2 changes: 1 addition & 1 deletion DOCS/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ Allocations route through `memory.allocator` from `lib/memory.zig`. In the WASM

Wire state does **not** live inline on `Component`. Each component carries an opaque `PoolHandle` into a width-tiered Structure-of-Arrays pool owned by `Circuit`. Reads and writes flow through `Circuit.readState` / `Circuit.writeState`, which dispatch on `PoolHandle.tier` exactly once and then perform a direct bitmap operation against the pool's `(values, defined)` u64 buffers. The width=1 tier packs 64 slots per word.

The value currency above the pool is `BitVecState` (`value`, `defined`, `width`). It is what events carry, what gates evaluate, and what listeners receive. The split exists so wider wires (4/8/32-bit busses) can be added by introducing a new pool tier without re-touching the propagator. Today only width=1 (tier 0) is wired; wider widths trap. See `DOCS/simulation-engine.md` for the full surface.
The value currency above the pool is `BitVecState` (`value`, `defined`, `width`). It is what events carry, what gates evaluate, and what listeners receive. The split lets every legal width (1 through 64) live in its own lazily-allocated pool tier, with the propagator untouched — each tier indexes by `tier == width` (tier 0 is unused). The width=1 tier packs 64 slots per word; wider tiers store one u64 per slot. Widths > 64 trap at allocation time. See `DOCS/simulation-engine.md` for the full surface.

## Layer 2 — Prebuilt runtime template (`templates/`)

Expand Down
1 change: 1 addition & 0 deletions DOCS/archive/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ Each file is a highlight view. The full plan prompt, every phase plan, and every
| [plan-test-speed.md](plan-test-speed.md) | Test-suite speed-up plan (highlights; full bundle in git history) |
| [plan-zig-free-cli.md](plan-zig-free-cli.md) | Self-contained `circ-compile`: no Zig at user runtime (highlights; full bundle in git history) |
| [plan-cli-preview.md](plan-cli-preview.md) | `circ-compile <file> --preview`: ASCII circuit schematic rendering with opaque/expanded macro modes and ANSI color (highlights; full bundle in git history) |
| [plan-multi-bit-language.md](plan-multi-bit-language.md) | Multi-bit wires language extension (literal widths, slice / bit-index / concat, parametric sub-circuits, topology format v02). Landed across stages S1–S12. |
54 changes: 40 additions & 14 deletions DOCS/circuit-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,22 @@

## 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.
A `.circ` file contains a sequence of top-level declarations. Each declaration either names input pins or instantiates a component with named port connections. Every signal-carrying declaration may carry an optional width annotation `[N]` where `N` is in `[1, 64]`; a missing `[N]` means width 1.

```
input <name> [, <name> ...]
input [<width>] <name> [, <name> ...]
output [<width>] <name> [, <name> ...]

<type> <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`](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`](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`.
Expand All @@ -27,9 +32,11 @@ Line comments start with `//` and run to end of line (see `Annotation` in `lib/g

```
input pin1, pin2
input[4] addr
input<W>[W] data
```

Declares one or more named input pins for the circuit. Input pins are driven externally (by the JavaScript host) and have no input ports of their own.
Declares one or more named input pins for the circuit. Input pins are driven externally (by the host) and have no input ports of their own. An optional `[N]` after `input` makes the pin `N` bits wide; the parametric form `input<W>[W]` declares both a width parameter and an input of that width (see [`language.md`](language.md) §6.3).

### Component Instances

Expand All @@ -42,17 +49,17 @@ Declares one or more named input pins for the circuit. Input pins are driven ext

`<type>` is the gate kind. Recognised types:

| Type | Ports | Description |
|----------|----------|---------------------------------------------------------------------|
| `and` | `a`, `b` | AND gate |
| `not` | `in` | NOT gate (inverter) |
| `led` | `in` | Output indicator |
| `wire` | `in` | Pass-through |
| `output` | `in` | Externally observable output pin (special-cased declaration kind) |
| Type | Ports | Width rule | Description |
|----------|----------|-------------------------------------------------------------------------------|---------------------------------------------------------------------|
| `and` | `a`, `b` | bit-parallel; both inputs must match the instance width | AND gate |
| `not` | `in` | bit-parallel; input must match the instance width | NOT gate (inverter) |
| `led` | `in` | input must match the instance width; widths `> 1` render as a numeric display | Output indicator |
| `wire` | `in` | input must match the instance width | Pass-through |
| `output` | `in` | input must match the instance width | Externally observable output pin (special-cased declaration kind) |

`input` is a sibling pin declaration with its own syntax (no port list), described above.
`input` is a sibling pin declaration with its own syntax (no port list), described above. All primitives accept an optional `[N]` width annotation; absent it, width is 1.

**Built-in macro gates** expand at compile time to nested `and` / `not` (and optionally other macros). They use **`a`** and **`b`** as input ports and expose **`out`**. Unlike `and`/`not`, **no `import`** is required — the compiler behaves as if `import … from "<builtin>/<name>.circ"` were present.
**Built-in macro gates** expand at compile time to nested `and` / `not` (and optionally other macros). They use **`a`** and **`b`** as input ports and expose **`out`**. Unlike `and`/`not`, **no `import`** is required — the compiler behaves as if `import … from "<builtin>/<name>.circ"` were present. Each macro is parametric: `or[8] g(a=x, b=y)` produces a bit-parallel 8-bit OR.

| Type | Ports | Expansion |
|--------|----------|-----------|
Expand All @@ -67,7 +74,12 @@ Declares one or more named input pins for the circuit. Input pins are driven ext
`<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`, etc.).
- An inline component expression (see nested components below)
- `<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`](language.md) §4 and §6 for the full signal grammar and width-checking rules.

## Signal References

Expand Down Expand Up @@ -129,3 +141,17 @@ Parse tree node types used by `lib/syntax/translate.zig`:
| `NodeType_Error` | Parse failure at this position |

`lib/syntax/nodes/declaration.zig` handles `Declaration` rule nodes and recognises the sub-rules `input`, `output`, and `component` to determine declaration kind.

## Topology Format Version

Compiled `.wasm` artifacts embed the resolved circuit as a custom section. The current format identifier is `CIRC` (v02), bumped from the v01 format that preceded multi-bit support. Each serialised component now carries a `width: u8` byte; the runtime materialises the corresponding pool tier on `init()`. See [`wasm-api.md`](wasm-api.md) for the host-facing ABI and `lib/topology/serializer.zig` for the wire layout.

## Diagnostic Codes

The stable validator surface (E001–E016, W001–W003) is enumerated in [`language.md`](language.md) §4.2 and `lib/validator/codes.zig`. The multi-bit codes added with v02 are:

| Code | Meaning |
| --- | --- |
| E014 | width mismatch between a driver and the port it feeds |
| E015 | width annotation `[N]` on a declaration whose declared type is scalar |
| E016 | sub-circuit call-site arity mismatch (wrong number of `<...>` parameters) |
113 changes: 113 additions & 0 deletions DOCS/decisions/language.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,116 @@ The surface syntax of `.circ` is documented in [../circuit-format.md](../circuit
**Rationale.** Conflating the two would mean every sub-circuit author has to choose between "this is for display" and "this is for the parent", or worse, every LED becomes implicitly part of the public interface. Keeping them separate lets a sub-circuit have internal LEDs (debug indicators) without polluting its external contract, and lets the top-level circuit have outputs without forcing a visual element.

**Alternatives.** A single "sink" concept covering both. Simpler vocabulary, but loses the distinction between rendering and interface that consumers actually care about.

---

## Multi-bit wires

The 17 decisions below were locked during the multi-bit wires initiative (stages S1–S12). The full historical plan with rationale and stage ordering moved to `DOCS/archive/plan-multi-bit-language.md` after S12 closed; that file is the place to read for *why* each decision shaped the way it did.

### 1. Width annotation syntax

**Decision.** A width is declared with `[N]` after the type keyword: `input[4] a, b`, `and[4] g(...)`, `output[4] r(in=...)`. A missing `[N]` means width 1.

**Rationale.** Postfix annotation keeps existing scalar `.circ` files legal as-is, which is the load-bearing property — the validator's job becomes a single equality check per connection and no caller has to be touched when a sub-circuit goes multi-bit.

### 2. Bit numbering

**Decision.** `a[0]` is the LSB; bit `i` carries weight `2^i`.

**Rationale.** Matches `BitVecState.value`'s bit layout exactly, so the topology format, the engine, and the language all agree byte-for-byte.

### 3. Slice notation

**Decision.** `a[lo..hi]` is half-open. `a[0..4]` covers bits 0, 1, 2, 3 — four bits.

**Alternatives.** Closed-range (`a[0..3]` covers four bits) is mathematically equivalent but every empty/identity case ends in an off-by-one trap. Half-open keeps `hi - lo == width`.

### 4. Concatenation

**Decision.** `{low, high}` with braces, low-on-left ordering. `{a, b}` produces a width `a.width + b.width` value where bits `[0, a.width)` come from `a` and bits `[a.width, ...)` come from `b`.

**Rationale.** Low-on-left lines up with how `BitVecState.value` is stored (LSB at bit 0) and how slice notation places the lower bound on the left. A Verilog-flavoured high-on-left ordering was rejected because it forces every shape transformation to mentally reverse the operand list.

### 5. Sub-circuit parametricity

**Decision.** A sub-circuit becomes parametric by introducing parameters with `<Identifier>` on its `input` declarations. Use sites inside the file write `[Identifier]`. Callers bind via `name[N, M, ...]` positionally at the instance name.

### 6. `<W>` versus `[W]`

**Decision.** Angle brackets are the *introduction* form (declares a parameter). Square brackets are the *reference* form (an integer literal or a previously-introduced parameter name).

**Rationale.** Visually different brackets make it immediately obvious whether you're looking at a declaration or a use. The compiler can also produce better diagnostics: `[W]` without a corresponding `<W>` triggers `E015`, and the suggestion can point at the exact spelling change.

### 7. Where parameters can appear

**Decision.** Width annotations only. Parameters never appear inside slice bounds, indices, arithmetic, or anywhere else.

**Rationale.** Keeping parameters to declaration positions only means the resolver can do all specialization with a tree rewrite at one fixed place (`Identifier.width` and `ComponentInstance.width_args`). Allowing parameters inside `a[W..W+4]` would require an arithmetic-evaluation pass and pull resolver complexity sideways for marginal gain.

### 8. `[N]` on a scalar sub-circuit

**Decision.** Compile error `E015`. The diagnostic suggests adding `<W>` to the sub-circuit.

### 9. Multiple parameters

**Decision.** Allowed. Positional at call sites; ordered by the position of first introduction in the source file.

**Rationale.** Positional binding matches every other language's convention for parameterised types and avoids the readability cost of keyword arguments at every call site. Source-order-of-introduction is the only ordering that's both well-defined and visible to the reader; declaration order in `<W, X>` may differ from reference order in the body, and the introducer wins.

### 10. Missing `[N]` at call site

**Decision.** Defaults all parameters to 1.

**Rationale.** This is the entire reason every existing scalar caller of the built-in macros kept working after S8 rewrote them as parametric. Without this rule, S8 would have been a breaking change for every `.circ` file in the world.

### 11. `<W>` discipline

**Decision.** A parametric sub-circuit always declares its parameter in angle brackets on the introducing `input` line, never elided or implicit.

**Rationale.** Two reasons to read a `.circ` file: understand its logic and understand its interface. Hiding parametricity would force the reader to infer it from internal usages. The `<W>` mark is small, visible, and unambiguous.

### 12. LED at width > 1

**Decision.** Default `--preview` rendering is a single hex numeric display (`0x?` for fully undefined). With `--expand-display` and `N < 8`, render `N` indicator glyphs (LSB on the left). At `N >= 8` with `--expand-display`, fall back to numeric and emit one stderr warning per offending LED.

**Rationale.** Indicator rows wider than 7 don't fit comfortably in a typical terminal; the cap is a UX call, not a technical one. The warning is non-fatal so the user still gets a render and can re-run without the flag.

### 13. Truth table

**Decision.** One column per pin; multi-bit values rendered per format flag. `--truth-table-format binary|hex|decimal` (default binary). Hard cap at 16 total input bits across all inputs; `--truth-table-cap` overrides up to 24.

**Rationale.** `2^16 = 65,536` rows is the practical ceiling for a glance-readable truth table; pushing past 24 (16 M rows) is almost never what the user wanted, so the override is opt-in.

### 14. WASM host API

**Decision.** Three exports per pin:

- `setPin(id: i32, value: i64, defined: i64) -> void`
- `getOutputValue(id: i32) -> i64`
- `getOutputDefined(id: i32) -> i64`

The two i64 fields are read by JS as `BigInt`. The full rationale (paired exports vs. a single out-pointer call) lives in `DOCS/wasm-api.md`.

### 15. Macro migration

**Decision.** The five built-in macros (`or`, `xor`, `nand`, `nor`, `xnor`) are written with `<W>` annotations. `nor` and `xnor` propagate width through their internal `or` / `xor` calls (`or inner[W]`, `xor inner[W]`).

**Rationale.** Decision #10 is what makes this safe: scalar callers default `W` to 1 and get byte-identical IR and topology. Bench's `golden matches` invariant tracks this empirically.

### 16. Signal-expression composition

**Decision.** A signal source on the right-hand side of a port binding is one of:

- a bare name (`a`)
- `name.port`
- `name[i]` or `name[lo..hi]`
- `name.port[i]` or `name.port[lo..hi]`
- an anonymous component instance (`not(in = a).out`)
- a brace concat (`{ ... }`) containing any of the above

### 17. Topology format version

**Decision.** Bumped from `0x01` (pre-multibit) to `0x02`. Both `ComponentRecord` (min) and `FullComponentRecord` (full) carry a `width: u8` byte per component. Slice records carry auxiliary `(lo, hi)` bytes in the min section. Concat records carry their operand list in the full section's auxiliary slot.

See `DOCS/circuit-format.md` for the exact byte layout.
Loading
Loading