Search

Sign in to launch Copilot/Codex from the palette.

Riding Handwired in public

I am learning Cheng Lou's Handwired. These are my training wheels. You can push them too.

Field note 2026.10.16

Handwired is a research project by Cheng Lou. The README opens with this line: "Turn your regular JavaScript code into a neural net; no training, exact, yet also fully differentiable!"

I read that line wrong. I thought there was a tool that takes my JS file and gives back a net. There is no such tool. You write the program again, in a special style. This post is me learning that style in public, one wobble at a time.

The widgets below run on a toy I wrote: toy-circuit.ts, about 70 lines. It is a toy of the idea, not Handwired's API. The real runtime is in src/runtime.

What a program is

A Handwired program is a fixed list of FP32 operations. Most are weighted sums and products with a ReLU. ReLU is max(0, x).

You do not write that list by hand. You write builder code. The tutorial calls it "ordinary JavaScript that runs once and appends operations to a Circuit, so nothing in the program branches or loops at runtime."

const circuit = new ToyCircuit()
const a = circuit.input(0, "a")
const above4 = step(circuit, a, 4)
const result = select(circuit, above4, ten, twelve)

const wires = circuit.evaluate(Float32Array.of(4.5))
wires[result] === 11

Wobble 1: the trick

This is the README's first example. The top line is the JS. The bottom two lines are the ops version, which the README marks "For an integer a":

const result = a > 4 ? 10 : 12

const above4 = Math.max(0, a - 4) - Math.max(0, a - 5)
const result = above4 * 10 + (1 - above4) * 12

Here, step builds relu(a-4) - relu(a-5). Move the slider between 4 and 5. At a = 4.5, the JS gives 10 and the ops give 11. The step is not a step there. It is a ramp.

JS
a > 4 ? 10 : 12
10
ops
above4*10 + (1-above4)*12
11

Different answer. above4 = 0.5, not a clean 0 or 1.

every wire
#oplabelvalue
0inputa4.5
1addsum0.5
2relurelu(x-4)0.5
3addsum-0.5
4relurelu(x-5)0
5addabove40.5
6const1010
7const1212
8mulbit*yes5
9add1-bit0.5
10mul(1-bit)*no6
11addresult11

Turn on the snap. Now a can only be 4 or 5, and the ramp is never hit. This is why Handwired cells declare a range and a grid. The host rounds inputs onto the grid before the program sees them. From the template's toggle:

viewportWidth: input([0, 16_384], {initial: 1280, grid: 1}),
down: {active: event(), x: coordinate(), y: coordinate()},
on: stateBit(),
armed: stateBit(),

Wobble 2: both branches always run

There is no if. Both sides get computed. A 0 or 1 wire picks one with a multiply. Move x and watch the op count. It does not change.

winner
else
ops run per evaluation
12
result
102

Both branch wires are yellow. Both get a value on every evaluation. The losing one is multiplied by 0.

#oplabelvalue
0inputx2
1addsum-2
2relurelu(x-4)0
3addsum-3
4relurelu(x-5)0
5addx > 40
6addif: x*24
7addelse: x+100102
8mulbit*yes0
9add1-bit1
10mul(1-bit)*no102
11addresult102

Wobble 3: wires and the host loop

The program does not own any state. A host loop does four things: write inputs into a Float32Array source table, run one evaluation, write next-state cells back into the table, and write outputs to the DOM.

Click the switch to do all of it at once. Or use the two buttons to do it in slow motion. The real template/toggle.ts does much more: it acts on release, hit-tests the track and springs the thumb. Mine only flips a bit: next = on + click - 2·click·on.

source table: Float32Array [0, 0] (click, on) · evaluations: 0
#oplabelvalue
0inputclick (event)0
1inputon (state)0
2mulclick*on0
3addnext on0

Wobble 4: loops become fixed work

A program "runs every operation on every frame and can't stop early, so a loop becomes a count, a tree, a table, or the same work on every item." That line is from docs/patterns.md.

Here is findIndex over 8 numbers. Every item gets compared. Then one pass keeps only the first match. JS stops early. The ops do not. Move the threshold: the op count stays the same wherever the match is. The real patterns doc has smarter versions, such as a count when the test flips once, or a tree.

31728594
index
2
JS findIndex checked
3 items
ops run
83
every wire
#oplabelvalue
0inputv03
1inputv11
2inputv27
3inputv32
4inputv48
5inputv55
6inputv69
7inputv74
8inputt6
9addv0-t-3
10addsum-3
11relurelu(x-0)0
12addsum-4
13relurelu(x-1)0
14addv0>t0
15addv1-t-5
16addsum-5
17relurelu(x-0)0
18addsum-6
19relurelu(x-1)0
20addv1>t0
21addv2-t1
22addsum1
23relurelu(x-0)1
24addsum0
25relurelu(x-1)0
26addv2>t1
27addv3-t-4
28addsum-4
29relurelu(x-0)0
30addsum-5
31relurelu(x-1)0
32addv3>t0
33addv4-t2
34addsum2
35relurelu(x-0)2
36addsum1
37relurelu(x-1)1
38addv4>t1
39addv5-t-1
40addsum-1
41relurelu(x-0)0
42addsum-2
43relurelu(x-1)0
44addv5>t0
45addv6-t3
46addsum3
47relurelu(x-0)3
48addsum2
49relurelu(x-1)2
50addv6>t1
51addv7-t-2
52addsum-2
53relurelu(x-0)0
54addsum-3
55relurelu(x-1)0
56addv7>t0
57constseen00
58addsum0
59relufirst00
60addseen10
61addsum0
62relufirst10
63addseen20
64addsum1
65relufirst21
66addseen31
67addsum-1
68relufirst30
69addseen41
70addsum0
71relufirst40
72addseen51
73addsum-1
74relufirst50
75addseen61
76addsum0
77relufirst60
78addseen71
79addsum-1
80relufirst70
81addseen81
82addindex2

Wobble 5: it is differentiable

The weights need no training because a person writes them. But the program is still made of sums, products and ReLUs, so gradients can flow through it.

Raise the temperature to soften the step into a smooth curve. Then press train. Gradient descent moves the threshold from 4 toward the gap in the labels, and the loss falls. Press freeze to snap back to a hard step with the learned constant. My curve is a sigmoid, not Handwired's ReLU ramp, to keep the toy short.

threshold
4.00
loss
0.209
correct as hard step
9/12

This is a tiny version of the learned gallery demo. There, a photo layout comes from a small network whose coefficients are source cells, and Adam trains them with gradients from circuit.backpropagateReference.

What it is not

  • Not a JS-to-net converter. Nothing reads your existing source. You write builder code.
  • Not unbounded. Cells declare ranges and grids. Off the grid, the tricks give wrong answers, as in wobble 1.
  • Not early-exit. Every op runs every frame.
  • Not a GPU runtime by default. src/runtime evaluates on the CPU in JS. The JSON search demo and some lab work use WebGPU.
  • Not a product. It is one person's research project, with about 1,000 GitHub stars today.

The scale is real, though. The demos page lists the Mini OS program at 9,909 weights in 130 layers, with no training.

What I want to try next

  • Can learned constants be written back as readable code, like the line freeze prints?
  • Can a small, exact policy travel as data that an agent carries and runs?
  • Where does my toy break first when I port one real pattern from the docs?

Start with the repo, the tutorial and the demos.