handwired-rs
A Rust port of chenglou/handwired by Cheng Lou, plus a #[handwired] attribute
that compiles ordinary Rust functions into handwired circuits, and a WebGPU backend.
A handwired program is a fixed list of f32 operations: weighted sums, ReLU products, a rounding-to-bit atom and table reads. It runs as layers of sparse matrix multiplications, the shape of a neural net, and stays differentiable, while computing exactly what the ordinary code computes. The ideas, the instruction set, the exactness rules and the comparison constructions are handwired's; this crate ports them to Rust and checks itself against handwired's published numbers.
Status: 0.1, experimental, checked by the tests listed below on one machine and in CI. Read Limitations before relying on it.
use ;
// const result = (a > 4) ? 10 : 12
let mut circuit = new;
let a = circuit.source;
circuit.assume_range;
circuit.assume_grid;
let four = circuit.constant;
let above_four = emit_compare;
let result = circuit.affine;
let program = circuit.compile;
Affine forms are written with operators on Wire (x * 0.5 - 26.0, a - b) and appended by passing them to a builder
method. Builder methods panic on a contract violation (e.g. comparing an operand with no finite declared range), since
that is a bug in the program being built; evaluation returns Error for bad input data.
#[handwired]: compile ordinary Rust
use handwired;
let compiled = toggle_step_circuit; // the circuit, compiled to tensor layers
compiled.verify.unwrap; // vs. the original function, on sampled inputs
let = compiled.eval.unwrap;
The attribute keeps the function as it is, as the oracle, and adds toggle_step_hw (the body over circuit values,
which other #[handwired] functions call) and toggle_step_circuit() (builds and compiles it). The body runs once at
build time with every input-dependent value as a typed Val<T>; operators on Vals append operations, so ordinary
code like a + b or x.min(y) stays as written. The macro rewrites only what Rust can't overload: comparisons,
&&/||, indexing, assignment, if, match and as.
The contract: the circuit agrees with the function on every input where the function returns without panicking.
Overflow and out-of-bounds indexing panic in the oracle, so their circuit values are unspecified. verify skips
inputs where the oracle panics; run it with overflow checks on (e.g. a debug build).
Supported: bool, u8–u64, i8–i64, usize, f32, arrays and tuples as parameters, results and locals;
+ - * !, comparisons, && || & | ^; integer / and % by a constant (as a table read); f32 / by a power of two;
min max clamp abs wrapping_add/sub saturating_add/sub; as casts between them (f32 → int truncates and saturates
like Rust); if/else as a statement or value; match with literals, ranges, |, guards, tuples, bindings and
consts; for over ranges or arrays that don't depend on the inputs (they unroll); array reads and writes at computed
indices, including nested arrays; calls to other #[handwired] functions; return, break and continue anywhere,
including under conditions that depend on the inputs.
Early exits: the trace runs every path, so an exit can't leave it. A function that returns early gets a done bit
and a result slot: each return records its value where no earlier return happened, and the function's value picks
the recorded one. A loop with break/continue gets a halted bit, and assignments inside it apply only where it
still runs. Under native conditions, e.g. if index == 3 { break; }, exits leave the unrolled loop for real, and
selects on flags that no input-dependent branch has set fold away, so they cost nothing. A return outside input-dependent
branches and flagged loops also leaves for real.
Declarations: #[range(lo, hi)] is required on f32 parameters and on integer types wider than 224 (u32,
i32, ...), whose values must stay within ±224 (the build panics, pointing at the operation, if they may not).
#[grid(step)] declares a power-of-two grid, e.g. 1.0 / 64.0 for pointer coordinates, which makes comparisons
cheap.
See Limitations for what isn't supported.
GPU: handwired-rs-gpu
The gpu/ crate runs a compiled program as one WebGPU compute shader (wgpu 25), one invocation per record, so a
batch of records runs in parallel. It reproduces the CPU reference evaluator bit for bit.
let gpu = new?;
let batch = new?;
let results: = batch.run?; // records: Vec<([u8; 32], [u8; 3])>
Shader compilers break f32 exactness in ways handwired measured on Metal, and that the tests here reproduce on AMD
(RADV) and Mesa's lavapipe: they fuse a + w * x into a multiply-add and regroup sums. The generated WGSL wraps every
inexact product and every addition in max(x, lowest), with lowest read from a uniform buffer, which the compiler
can't see through; with those guards turned off, the random-circuit test fails on both drivers. Booleans are written
as select(0, 1, score >= 0.5), since compilers fold (score + K) - K, and every literal is one exact f32. Not
guarded: GPUs may flush subnormals to zero, which would change a comparison of values closer than 2^-126 (untested
here), and WGSL lets drivers assume no NaN or infinity occurs.
cargo run --release -p handwired-rs-gpu --example search searches 100,000 32-byte records for a 3-byte pattern with a
#[handwired] function (890 operations in 127 layers) and checks every result against the function. On an AMD Ryzen
laptop's integrated Radeon (Renoir):
| 100,000 records | |
|---|---|
| the plain Rust function | 1.7 ms |
| CPU tensor executor, one thread | 754 ms |
| GPU, including upload and read-back | 27 ms |
cargo run --release -p handwired-rs-gpu --example kv_store is a fixed-capacity key-value store (16 slots, get, put,
delete) written as one #[handwired] function from the store's state to its next state and the answer. A get is hard
attention: a one-hot match of the key against every slot, multiplied into the values. The example verifies it against
the function, runs a put/get/delete session through the circuit, and runs 100,000 operations on 100,000 separate stores
in one GPU dispatch (1,082 operations in 70 layers; 50 ms on the GPU against 6.4 ms for the plain function).
The circuit form doesn't make a program like this faster than native Rust; what it adds is that the same exact
program is a stack of sparse matrix multiplications with ReLUs, which runs on tensor hardware and is differentiable.
Set WGPU_ADAPTER_NAME (e.g. llvmpipe) to choose an adapter; the GPU tests skip when there is none.
What's ported
| Area | Status |
|---|---|
Four instructions (affine, reglu, boolean, index_read), stored-order f32 rounding |
✅ |
| Builder: term normalization, constant folding, small-sum composition, deduplication | ✅ |
Declarations: assume_range, assume_grid, assume_bit, rounding points; range and grid inference |
✅ |
multiply, select, round_affine, emit_min/max, emit_and/or/not, emit_round |
✅ |
emit_compare: integer bits on grids, sign cascade, end_with_boolean |
✅ cost table matches handwired's docs |
emit_minimum_exact, emit_maximum_exact, emit_clamp_exact |
✅ |
| Reference evaluator and reverse-mode backprop (boolean / index gradient rules) | ✅ |
| Layered sparse compiler with dead-operation removal, CPU executor | ✅ |
Source table cells (input, event, state, state_bit, grids, validation) |
✅ simplified |
One-hot groups and gather lowering, recompute_depths |
⏳ not yet |
Springs / host timing, emit_argmax, emit_first_active, inline_circuit, prefix networks |
⏳ not yet |
#[handwired] proc-macro frontend (macros/, src/dsl/) |
✅ see above |
GPU backend (gpu/: WGSL codegen, batch runner) |
✅ bit-exact on RADV and lavapipe |
One deliberate difference: cells declared as exact bits also get grid 1, so comparisons over sums of bits take the cheap integer-bit form.
Limitations
What's exact, and where. Every circuit computes exactly what its source computes on every input where the source
returns without panicking. Overflow and out-of-bounds indexing panic in Rust, so the circuit's value there is
unspecified. verify() samples inputs (plus each range's ends); it is a test, not a proof.
The #[handwired] subset.
- Not supported:
while/loop(every loop needs bounds that don't depend on the inputs), labeledbreak/continue, division or remainder by an input value, shifts and integer bitwise operators, structs, enums,Option, closures, references, generic functions,if let, and calls to ordinary (non-#[handwired]) functions. - Integer types wider than 224 (
u32,i32,u64, ...) need declared#[range]s, and every value must stay within ±224, the integers f32 holds exactly.f32parameters need#[range]s. - An unannotated
let mut x = 0defaults toi32where plain Rust would infer it from later use; annotate it. - A lowercase single-name call
foo(x)is taken to be another#[handwired]function (foo_hw). Iterate arrays by value (for x in arr), not with.iter(). - An
ifwith both branches ending inreturnneeds an annotated binding when used as a value. - Each
ifunder an input-dependent condition emits both branches, so deeply nestedifs andmatches with early returns in many arms grow the generated code. - Error messages for unsupported code come from rustc's type checker and can be indirect.
Cost. A circuit evaluates every path. Array writes at input-dependent indices cost one select per element (gather
lowering isn't ported), early returns chain into deep layer stacks (the search example has 127 layers), and
integer division by a constant is a table read over the dividend's range (at most 65,536 entries). On the CPU, a
circuit is far slower than the function it came from, and on the GPU the search example is still about 16x slower
than plain Rust: the point is exactness, differentiability and a tensor-shaped program, not speed.
GPU. Bit-exact against the CPU reference on AMD RADV (Renoir iGPU) and Mesa lavapipe; not tested on NVIDIA, Intel, Apple or browsers. Subnormal flushing isn't tested, and would change comparisons of values closer than 2^-126. WGSL lets drivers assume there are no NaNs or infinities. There's no backward pass on the GPU.
Differentiability. The reference evaluator has a backward pass, tested on small cases; nothing here trains a circuit yet, and there's no export to a deep learning framework.
Not ported from handwired. One-hot groups and gather lowering, depth recomputation, springs and frame timing,
argmax/first_active, circuit inlining, prefix networks, and its demos (HandwiredOS, the learned gallery, JSON
search).
License
MIT. handwired is MIT-licensed, Copyright (c) 2026 Cheng Lou; this port keeps that notice in LICENSE.
Tests
cargo test runs:
-
tests/compare.rs: the operation/layer counts of every comparison construction against the table in handwired'sdocs/execution-model.md, and every bit against the native f32 comparison on equal values, one-ulp neighbors, subnormal differences and range ends, through both evaluators. -
tests/tensor.rs: seeded random circuits, where the compiled program has to reproduce every live value of the reference evaluator. -
tests/backprop.rs: gradients of products, selects, boolean atoms and index reads. -
tests/helpers.rs:emit_round(ties to even),emit_or,emit_clamp_exactandemit_maximum. -
tests/macro_*.rs:#[handwired]functions (the README example, the toggle, histograms, table lookups, nested array writes,match, wrapping arithmetic, casts, order-sensitive f32 sums, earlyreturn/break/continuein nested loops), each verified against its own function. -
tests/toggle.rs: handwired's toggle-switch template (without its spring), compared frame by frame with the same switch in plain Rust, over a scripted trace and random traces near the track edges. -
gpu/tests/exact.rs: random circuits, comparisons at their edges and#[handwired]batches on the GPU, compared bit for bit with the CPU reference (skipped without an adapter).
cargo run --example toggle builds the switch and prints its size. cargo run --release --example ui_panel runs a
toggle and a slider (pointer handling, arming, dragging with clamping and snapping, and an easing animation: 262
operations) through a scripted pointer, and cargo run --release --example life runs Conway's Game of Life on a
24 × 24 torus (6,912 operations in 8 layers); both check every frame against their function and print the frames.
CI (.github/workflows/ci.yml) runs all of the above on GitHub's Ubuntu runners, with the GPU tests on Mesa's lavapipe
software Vulkan (it fails rather than skips if no adapter appears), runs the search example, and checks cargo fmt,
clippy -D warnings on the libraries, and packaging of the macro crate.