I gave a five-rule shipping function and its neural-net twin the same 16 million inputs. They never disagreed once.
Then I taught the twin two new price lists. It nailed the first. On the second, it got every paid order wrong.
The thing everyone shared and nobody explained
Handwired, by Cheng Lou, went public this week under the line "Turn your regular JavaScript code into a neural net." His announcement tweet promised no training, outputs exact against the JS, and full differentiability. The tweet passed 10,000 likes, and the repo counted 1,313 stars about a day and a half after it was created. MIT license, TypeScript, demos at chenglou.me/handwired.
Hacker News was quieter. The top comment on the busiest submission (9 points), from user skyzoidbroczky, is "What's the point of this?" Two other comments say they don't get what it is for. Nobody in the thread says they tried it.
So I did, on a function of my own.
The name is the instructions
The core move fits in a couple of lines. A comparison becomes a pair of ReLUs, and an if computes both branches and blends them with a 0-or-1 bit. Pile up enough of those and your program is a stack of weighted sums and ReLU products: a neural net whose weights you wrote instead of trained.
What the tagline skips, the docs say outright: "No tool converts existing JavaScript source into a program." You translate by hand, in builder code that runs once and appends operations to a Circuit. Hence the name.
Ranges and grids are part of that translation too. The README's own example rewrites (a > 4) ? 10 : 12 as relu(a - 4) - relu(a - 5) and notes it holds for an integer a. I checked: at a = 4.5 the rewrite gives 11 while the ternary gives 10. The real API makes you declare each input's range and grid (whole cents, whole grams), then builds comparisons that are exact for every value you declared.
The repo's tooling wants Bun, which I don't have. The runtime has no dependencies, though, and Node 22 loads its .ts files directly, so I skipped the install and imported it from a scratch folder.
My function
Here is the plain version, in integer cents:
function shippingCents(subtotal, grams, member) {
if (member && subtotal >= 5000) return 0
if (subtotal >= 10000) return 0
if (grams <= 500) return 499
if (grams <= 2000) return 899
return 899 + Math.ceil((grams - 2000) / 1000) * 300
}
And the heart of my translation. Every business number is a constant cell, so the backward pass can reach it later:
const free = or(
and(cells.member, emitCompare(c, cells.subtotal, '>=', cells.memberFreeAt)),
emitCompare(c, cells.subtotal, '>=', cells.freeAt),
)
const light = emitCompare(c, cells.grams, '<=', cells.lightMax)
const mid = emitCompare(c, cells.grams, '<=', cells.midMax)
// Math.ceil((grams - 2000) / 1000) as a count of thresholds passed
const extraKg = c.affine(passed.map(bit => [bit, 1]))
const heavy = c.affine([[cells.midPrice, 1], [c.multiply(extraKg, cells.perKg), 1]])
const price = emitSelect(c, free,
emitSelect(c, light, emitSelect(c, mid, heavy, cells.midPrice), cells.lightPrice), zero)
The Math.ceil was the only line that made me stop and think. A program like this can't loop or divide by a runtime count, so "how many started kilos past 2 kg" becomes eight comparisons whose bits get added up. The docs call that a threshold count.
What came out
Fifty operations in 12 layers, holding 81 weights. The twelve comparisons account for 24 of those operations, two each, because whole cents and whole grams let every comparison take the cheap exact form.
Then I checked it the boring way: every subtotal from 0 to $200 in 25-cent steps, every gram from 0 to 10 kg, member and not. That is 16,021,602 inputs. Zero mismatches against the plain function, across all 11 prices it can return.
One thing surprised me. When I folded the ifs by hand (a light parcel is always under 2 kg, so the nested selects collapse into one sum), the same function became 37 live operations in 7 layers. Same answers on all 16 million inputs. In Handwired you are the compiler, and your algebra shows up directly as depth.
It is not fast one call at a time. On my M1 Max, busy with other jobs, the compiled program took 10 to 17 seconds for those 16 million calls. The plain function took 60 milliseconds. That gap is roughly 175 to 275 times, and it is not really the pitch: the README's speed claim is for many evaluations at once on a GPU, as in its JSON search demo over 100,000 documents. I didn't run the GPU path.
Where the gradients go
Now the "fully differentiable" part. I ran the backward pass on a $30 order weighing 3.5 kg, which costs $14.99:
- d price / d perKg = 2. Two started kilos past the first two, exactly.
- d price / d midPrice = 1.
- d price / d every threshold = 0. The $50 member line, the $100 line, 500 g, 2 kg.
Every number that scales the answer gets a clean gradient. Every number that decides which branch runs gets nothing. The docs are upfront about this, too: a comparison passes back the derivative of a step, which is zero everywhere except a sliver narrower than one grid step.
Teaching it a new price list
So I trained it, using the program's own gradients with plain Adam on 2,000 random orders, priced by a new list.
Run one: only prices change (549, 949 and 350 per kilo). By step 100 the twin had learned exactly 549, 949 and 350. Zero of 2,000 orders wrong. That part is impressive: nothing told it which constant was the per-kilo rate, and the gradients found it anyway.
Run two: same prices, but free shipping now starts at $75. The threshold never moved. It sat at $100 for 600 steps, gradient zero. Instead, training did the only thing it could: it dragged the base prices down (499 to 434.63, 899 to 792.88) and only nudged the per-kilo rate, to 313.87, to soften the error on orders that should now ship free. The mean squared error barely moved, from 245,000 to 243,380. Of the 769 orders it still charges for, it got none right: the same 769 it had wrong before training.
It learned a worse price list to cover for a rule it couldn't see.
That's the gap issue #1 goes after. Its author estimates a gradient for each decision near its flip point. In their two-column layout test, the plain backward pass leaves a breakpoint at 500, and their method moves it to 699.9 against a target of 720, which fits every example. It lives on a fork for now.
So what is the point
Here is my answer to that HN comment. Translating code into a net sorts its numbers into two piles. Numbers that scale (prices, margins, sizes, spring constants) become weights you can fit from examples. Numbers that decide (thresholds, breakpoints) stay exact and frozen.
That second pile is a feature as much as a limit. A rule you don't want a gradient to bend can't be bent, while the constants around it can be tuned. The repo's learned gallery demo works this way: a small network inside a layout program, its coefficients trained with the program's own gradients.
The limits I hit: you translate by hand, ranges and grids are on you, everything is 32-bit floats (fine for integer cents, risky for float dollars), and the repo's tests need Bun, so I didn't run them.
Which numbers in your own code would you hand to a gradient, and which would you never let one touch?
Top comments (0)