Handwired: How JavaScript Becomes an Exact Neural Net

A neural net is a list of weighted sums and ReLUs. A program is a list of ifs, loops, and state. On October 15, 2026, Cheng Lou showed that the second can be written as the first. It needs no training, and the outputs match the program exactly. The weights are not learned. They are written by hand, one comparison at a time.


Cheng Lou called Handwired his "latest toy project". The thread passed 10,000 likes in a day, and the repo had about 1,300 stars by the next morning. This essay builds the idea up from one if to a whole interactive switch, the way you would learn it from the repo. I cloned it, ran its tests, and measured each step on my own laptop. Each number below comes from those runs or from the repo's own docs, and each figure uses the real constructions.

What Handwired is

The README promises a net with "no training, exact, yet also fully differentiable!" The demo site backs that up with three demos. Mini OS is a small desktop with windows, scrolling, drag, and text input. It runs on 9,909 weights in 130 layers, with zero training. The learned gallery trains some of a program's weights live while you drag. JSON search runs one program over 100,000 documents at each keystroke on WebGPU.

A screenshot of Mini OS, the HandwiredOS desktop: three windows on a beige desk with a grid of photos in front, all drawn by one numeric program.
Mini OS · 9,909 weights · 130 layers · 0 training. Open the live demo Screenshot from the Handwired repo (MIT).

One detail matters before anything else. The docs state, "No tool converts existing JavaScript source into a program." You do not paste a function in and get weights out. You write builder code, which is ordinary JavaScript that runs once and appends operations to a Circuit. The comments next to each builder line show the JavaScript it replaces. So "turn your code into a neural net" means a translation that you do by hand, with a library that makes each translation exact.

The result is a numeric program. It is a fixed list of operations over 32-bit floats, which the docs call FP32. There are four kinds of operation:

Operations that do not depend on each other share a layer. A compiled program is a stack of those layers, each a sparse matrix multiply followed by ReLU gates. That is the same shape as a feed-forward network. The difference is where the numbers come from.

Step 1: an if-else as arithmetic

Start with the example from the README. It turns a ternary into two ReLUs and a weighted sum. Fig 1 plays it the way Cheng Lou's landing page draws it. The ternary comes first, then each line of arithmetic, and each line lights the part of the circuit it builds.

Read above4 as a step. Below 4, both ReLUs are 0, so the step is 0. Above 5, both ReLUs grow at the same rate, so their difference stays at exactly 1. Between 4 and 5, the first ReLU grows and the second is still 0, so the step climbs from 0 to 1. For every integer a, that climb never shows, and the step is an exact bit.

I built that program with the library and checked it on every integer from −100 to 100. It returned the same answer as JavaScript 201 times out of 201. It compiled to 8 live operations in 5 layers, with 9 weights. Then I fed it fractions. At a = 4.25 it returned 11.5, where JavaScript returns 10. At 4.5 it returned 11, and at 4.75 it returned 10.5.

That is the first real lesson. The arithmetic step is exact only for values that cannot land inside the ramp. Handwired makes that a declaration. You say which grid a value lives on, for example whole numbers or multiples of 1/64. The builder then scales the ramp to fit inside one grid step. The figure shows both forms side by side.

Fig 1 · Code-to-circuit morph

The README's ternary turns into ReLU arithmetic one line at a time, and each line lights its boxes and wires. Then move a and pick a form. Each row is one value, computed with FP32 rounding as the runtime does.

Scroll the drawing sideways to see all of it.

README lines and circuit as on the Handwired landing page. Grid forms use the rule the page's source states, a > b is relu(1 - relu(1 - D)), where D is a - b in grid steps. Declared grid 1 refuses a value off the grid, as the runtime's validator does. Drawing adapted from Cheng Lou's Handwired page (MIT).

Step 2: both branches run

Look again at the last line of the arithmetic. It computes 10 and 12 every time, then multiplies one of them by 0. A numeric program has no jump instruction. Every operation runs on every evaluation, so an if becomes a select. The library calls it emitSelect(circuit, condition, off, on), which computes condition × on + (1 − condition) × off.

A select over a bit costs 4 operations in 3 layers per selected number. It returns the chosen value exactly, at any size, as long as the condition is exactly 0 or 1. One case still breaks it. If the branch that is not chosen holds Infinity, the result is NaN, because 0 × Infinity is NaN. The runtime guards against that by checking that every input is finite before it runs.

This also changes how state works. In JavaScript you write if (down) armed = insideTrack(down) and the variable changes. In builder code you make a new value that holds "armed after the press". The old value still exists, unchanged, as the previous evaluation's state. Nothing is ever assigned twice.

Step 3: a comparison costs what you declare

Most of a program's decisions start at a comparison, so that is where cost hides. The builder function emitCompare(circuit, a, '>=', b) returns a wire that is exactly 1 when the comparison holds. Otherwise it is exactly 0. A wire is the repo's word for the position of one number, either an input cell or the result of an operation. How the builder makes that bit depends only on what you declared about a and b.

When both operands sit on a grid, the builder works with the integer D = (a − b) / grid. Then a >= b is relu(1 − relu(−D)), two operations in two layers. Every partial sum is a whole number of grid steps, so nothing rounds. The condition is that those sums stay within 224 grid steps, the largest range of integers that FP32 holds exactly.

Without a grid, the builder falls back to a sign cascade. It keeps the difference f(a − b) as its own operation. Then it multiplies the positive part by powers of two and clamps it, stage after stage, until even the smallest nonzero FP32 difference reaches 1. That costs 15 operations in 14 layers for >=, and 28 for ===. Both forms return the same bits. Only the size of the program tells you which one you got.

There is a third form, the boolean atom. It adds 8,388,609 to a score between 0 and 1, then subtracts it again. At that size FP32 has no room for fractions, so the sum rounds, and the score comes back as exactly 0 or 1. It costs one operation. It also breaks if anything skips the FP32 rounding, for example a shader compiler that simplifies (x + K) − K back to x. Mini OS uses atoms for its dock, because the dock divides a width by 3, and those item edges sit on no grid.

I ran the real builder over 48 combinations of range, grid, and relation. I compiled each one and compared it with JavaScript on 200 random pairs. All 9,600 pairs matched. The figure shows the compiled layers for each combination.

Fig 2 · Comparison cost bench

Declare a range and a grid for both operands, then pick a relation. The strip is the compiled program from my run: one column per layer, one chip per operation. Chips marked relu carry a ReLU gate.

Range
Grid
Relation

Measured with emitCompare and compileTensorProgram from the repo, Bun 1.4.2, October 16, 2026. Each combination was checked against JavaScript on 200 random pairs on its grid.

The last range shows the limit. Values from 0 to 262,144 on a 1/64 grid span 224 grid steps. The partial sums need a little more room than that. So the builder quietly builds the cascade. On whole numbers, the same range fits easily. The docs show what this means at the size of a real app. In Mini OS, every window hit test compares four edge distances. With grids declared, each comparison is two operations. Without them, each one grows by 13 or 14 operations, for every edge of every window.

Step 4: cells, wires, and the source table

A program reads one Float32Array, the source table. Each number in it is a cell, and you declare each cell once with what happens to it around an evaluation:

Each declaration carries a range, and optionally a grid. Those are promises. The builder trusts them to pick cheap constructions, and the evaluator checks them on every run. A pointer coordinate off its grid stops the run with a message such as down.x must be a multiple of 0.015625. A state cell that gets no next value, or two, fails while the program is built, with the cell's name in the error.

The types help too. A wire has its own TypeScript type. A plain number does not type-check where a wire is expected, so emitCompare(circuit, cells.down.x, '>=', 614) is refused. You write circuit.constant(614) instead, and the constant folds into the comparison's bias.

Step 5: write a whole switch

The repo's template/ folder is a toggle switch written as one numeric program. Like a native switch, a press on the track arms it, and a release on the track flips it. The thumb springs to the new side. Here is how a press is handled, with the JavaScript each line replaces:

// JS: x >= trackLeft && x < trackLeft + trackWidth && y ... (4 comparisons)
const insideTrack = point => all(emitCompare(circuit, point.x, '>=', trackLeft), /* 3 more */)
// JS: if (down) armed = insideTrack(down)
const armedAfterDown = emitSelect(circuit, cells.down.active, cells.armed, insideTrack(cells.down))

The press flips the switch only when three bits all hold. The helper all is AND over exact bits: relu(sum of bits − (count − 1)). NOT is 1 − bit. Fig 3 plays both lines.

Fig 3 · Switch lines as circuits

Two lines from template/toggle.ts, each beside the JavaScript its comment names. Pick a line to play its morph, then set the input bits and read what the circuit computes against the JavaScript.

Scroll the drawing sideways to see all of it.

Code lines copied from toggle.ts, comments included. Every wire into all has weight 1. emitSelect is drawn as one box. It is 4 operations in 3 layers per selected number. Drawing adapted from Cheng Lou's Handwired page (MIT).

Pointer coordinates are declared on a 1/64 px grid, so every edge test is an integer bit. Each call to insideTrack appends a whole hit test, and the program runs both hit tests on every evaluation, whether or not a press happened.

I compiled the template. It has 20 cells and 67 live operations in 21 layers, with 118 weights. The builder recorded 16 integer-bit operations, one boolean atom, and one marked rounding point, which belong to the spring's settle check. That is the entire switch, including its animation. The next step is where the animation comes from.

Step 6: the host loop and time

A program cannot loop a number of times that is only known at runtime. Animation needs exactly that, because the time between frames changes. Handwired solves this outside the program. A spring step is a 2 × 2 matrix applied to the spring's displacement and velocity, every 4 ms. So n steps are that matrix raised to the power n. The JavaScript around the program, which the docs call the host, counts the steps and writes the four numbers of that power into input cells. The program applies the matrix once.

The host has one more job. A function called advanceFrame plans each frame's evaluations. There is one evaluation per event timestamp, which first steps the spring up to that moment. There is a settle check every 64 ms of simulated time, where a spring that is close enough to rest lands exactly on its target. There is one more evaluation at the frame's own time, for drawing.

The template's host writes state back only at events and settle checks. The frame's own evaluation runs on a copy of the table. So the stored state depends on the events and not on how fast the screen refreshes. I ran the real program through a scripted trace at 30, 60, and 120 frames per second to check that claim.

Fig 4 · Host loop evaluation timeline

Two clicks on the template switch, run through the real program. Pick a frame rate and play it. Each mark is one evaluation that advanceFrame planned. Filled marks wrote state back, and hollow marks ran on a copy.

1,000 ms1,750 ms
eventsettle checkframe, on a copyframe tick

Recorded with the template's buildToggle, createEvaluator, and advanceFrame in Bun 1.4.2. Events: press at 1,005 ms, release at 1,083 ms, press at 1,171 ms, release at 1,229 ms, all on the track. The host requests no frames while nothing moves.

At every frame rate, the same 14 evaluations wrote state back, with the same thumb positions. The thumb came to rest at the settle check at 1,723 ms. Only the number of frames changed: 23 at 30 fps, 43 at 60 fps, and 81 at 120 fps. That is a design choice, and the docs say so. The more common order applies a frame's inputs first and then steps the springs. With that order, the repo measured positions that differ by up to 359 px between 10 fps and 120 fps.

Step 7: check it against plain JavaScript

The repo tests every compiled program against the library's reference evaluator. That guarantee says nothing about whether the program does what the original JavaScript did. So the template ships a second version of the switch in plain JavaScript, in float64. A test runs both over a scripted trace and compares them on every frame.

The decisions must match exactly: on, armed, and the track position. The thumb must match within 1e-5 px, because FP32 springs drift slightly from float64 springs. The test presses exactly on the track's edges, and 1/64 px outside them, because an inexact decision shows there first. It also states what each scripted step must do, so a step that does nothing fails. On my machine, the template's test passed with 306 assertions.

git clone https://github.com/chenglou/handwired && cd handwired
bun install
bun test template          # the switch against its plain JavaScript twin
bun run start:template     # the switch at http://localhost:3000/
bun lab/runtime-cost/report.ts   # sizes and µs per evaluation

Step 8: loops become counts, trees, and tables

The switch has no loop, but most interfaces do. A list finds its first visible row. A layout adds up row heights. A program cannot stop early, so the repo's patterns guide rewrites each loop into work that always runs to the end. A loop becomes a count, a tree, a table, or the same work on every item.

Running totals show the trade. Each total reads the one before it. So a plain loop over 1,024 row heights is 1,023 additions in a chain 1,023 layers deep. A prefix tree computes the same totals with 2,036 operations in only 18 layers. The tree does about twice the work and needs a fraction of the depth.

Which one to pick depends on where the program runs. Depth matters on a GPU, where one layer runs many operations at once. On the CPU, time follows the operation count instead. The guide measured a line-breaking program at 8 to 11 µs per 1,000 operations. That held both as a chain of 10,232 layers and as a tree of 230 layers. So for the CPU, several patterns tell you to keep the loop.

Some loops leave the program altogether. A chat layout that only changes at a few container widths can be found by JavaScript ahead of time. The program then compares the runtime width with each of those widths and adds constants. In the guide's example, 5 messages have 45 such thresholds, and every bubble's top, height, and width take 106 operations in 3 layers. JavaScript reruns the real layout 1,391 times, but only when the text changes.

Step 9: run it on the CPU or the GPU

All of the runtime's execution is JavaScript on the CPU. The word "tensor" in its function names means a program packed into layers of sparse matrices. It does not mean a GPU. I timed the evaluators on an M1 Max in Bun 1.4.2, and the table gathers the sizes from the same runs.

ProgramLive operationsLayersWeightsTime per evaluation
a > 4 ? 10 : 12, grid 1859not timed
a >= b, no grid151422not timed
Template switch67211181.2 µs, or 1.8 µs with every check
Mini OS window manager4,6461309,909113.5 µs median

The switch time is an average over 200,000 runs after a warm-up. The Mini OS time is the median from the repo's own cost report, which runs 500 evaluations. The slower switch time includes the validator that checks every cell against its declaration before and after the run. The docs suggest keeping that validator in tests and using the lighter evaluator in a large app.

Mini OS shows what this looks like from the inside. Its Monitor window lists each live operation with its current value, read from the evaluator's working array after every run. You can watch the whole desktop compute, one named number at a time. That is hard to do with a trained network, where the numbers have no names.

The GPU enters when one program runs over many inputs. The JSON search demo runs two programs over each document: a JSON highlighter over its first 512 code points, and a regex search. Both are builder code with no trained weights. A code writer turns the compiled programs into WebGPU shaders when the page loads. The repo reports 11 to 15 ms per keystroke over 100,000 documents on an M5 Max in Chrome 155. Native RegExp took 8 to 28 ms for ordinary patterns, and 2.1 s for ([a-z]+)+x, which makes it backtrack. The demo shows both times, because neither one always wins.

Search 100,000 JSON documents · regex matching, JSON parsing, and highlighting as WebGPU kernels. Open the live demoMap drawing from the Handwired landing page (MIT).

The reason the GPU helps is the same reason the program is strange. Every document runs the same operations and reads no other document's values. There is no branch to diverge on, so one pass covers them all. Code that was written in a single-threaded style becomes data-parallel without being rewritten.

Step 10: unfreeze the weights

Every operation is a weighted sum or a ReLU product, so the whole program is differentiable. The library ships a backward pass, backpropagateReference. That makes a strange question possible. What happens if you treat some of a program's constants as trainable weights?

The learned gallery answers it. A small network inside a program maps each of 20 slot positions to a photo's left, top, width, and height. It uses two ReGLU layers of 6 units, where a ReGLU unit is one weighted sum times the ReLU of another. Its 244 source cells are the trainable coefficients. The program is 6,320 operations in 8 layers. You draw rectangles, and Adam steps the coefficients with the program's own gradients until the photos match your sketch.

A second loss term pulls photo edges that are nearly in line onto one line. No code snaps a photo into place. The photos line up only because training lowers that penalty. The repo measures one training step at about 0.2 ms in Bun, so the page trains 600 steps a second while you drag.

Gradients have edges too, and the docs name them. A comparison is a step function, so a sign cascade gives its operands a gradient of 0. A boolean atom stops the backward pass with an error, unless you ask for a straight-through gradient. Training works on the smooth parts of a program, such as positions and sizes, and not on its decisions.

What Handwired does not do

The limits are as useful to know as the tricks. The repo is careful about them, and so is this section.

It does not compile existing source. You rewrite each expression with builder functions. The template's comments make that easy to follow, but it is still a rewrite.

It cannot stop early. A loop becomes a count, a tree, a table, or the same work on every item, and every operation runs on every evaluation. The repo's patterns guide lists the cost of each translation. For example, items.sort(compare) at 64 items is 4,096 operations in 3 layers.

It computes in FP32, not float64. An exact bit is exact for the FP32 values it compares, not for the JavaScript it replaced. The docs give a column count where FP32 arithmetic produces 6.9999995 and float64 produces exactly 7. The fix in that case is to compare the width with each threshold, which avoids the division.

Some math is approximate. Division by a runtime value uses Newton steps, 51 operations with a relative error of at most 3e-7. Square root, sine, and cosine are also approximate.

Declarations are promises, not proofs. A false grid turns an integer bit into a fraction. The fix is in the tests: validateGrids, validateIndexReads, and validateOneHotGroups check declarations against real values.

Large programs are expensive to build. The docs report a program of about 2.4 million operations. It takes about 3 s to build and 1.3 to 1.8 s to compile, and the process uses 4 to 5 GB of memory.

Why it matters

The idea is not completely new. Tracr, a 2023 research compiler, compiled small programs into transformer weights to study interpretability. Handwired points the same idea at user interfaces. It also makes the outputs exact enough to drive a real page.

Three things follow from that. First, a trained network and an exact program now share one format. Cheng Lou suggests attaching a Handwired program to a small language model and training the two end to end. The model could then do both text and "that exact specific JavaScript program". That is his idea, not a result yet. Second, exact code can sit inside a differentiable system, and gradients can tune its constants, as the learned gallery shows. Third, code written one step at a time gets GPU-style parallelism, as the JSON search shows.

The simplest lesson is about what weights are. Most of us only meet weights that came out of training, so they look like a black box. Here every weight has a reason that you can read in the builder code. The 9,909 weights of Mini OS are a desktop, written one comparison at a time.

Weights with reasons

If you try one thing, run the template and its test. Then change a grid declaration and count the operations again. The repo's whole idea fits in that loop. A decision becomes a bit. A bit picks a branch. The declarations decide what each bit costs.

rg
Rohit Ghumare

CNCF Ambassador and Google Developer Expert. I build agent infrastructure and write about the fundamentals underneath the AI stack. I have no relationship with Cheng Lou or the Handwired project. Facts about the project come from its thread, repo, and docs, read on October 16, 2026. The measurements ran on my M1 Max in Bun 1.4.2 the same day.

Related: Tokens per Second · New Names, Old Fundamentals · More posts · X