The Port That Found a 30,000× Bug in My Own Language
The Port That Found a 30,000× Bug in My Own Language
SuperCLI already ran in Node.js and Zig. I ported it to machin — the language I'm building — to dogfood. The live test hung for thirty seconds. The bug was in the language, not the port.
SuperCLI is a capability router: it turns 10,000+ CLI tools into one discoverable, agent-callable graph. It already had two implementations — sc in Node.js (the full-featured one, owns plugin install) and sc-zig in Zig (the fast single binary). Both read the same ~/.supercli/plugins/plugins.lock.json. Both speak the same JSON envelopes. A third port, in a third language, has to justify itself.
The justification is machin itself. machin is the language I'm building — machine-first, compiled through C to a native binary, grown by dogfooding. The rule on the project is simple: every roadmap idea has to survive building a real thing. SuperCLI is a real thing. It reads a JSON lockfile, parses plugin manifests, dispatches shell commands, and renders JSON. If machin can't do that cleanly, that's a compiler problem, and compiler problems are exactly what dogfooding is for.
So I built sc-machin: a machin-native SuperCLI core that co-habits with the other two binaries — same lockfile, same command surface, same JSON shapes. 11 source modules, 982 lines of MFL, 24 test assertions, a 69 KB static binary. And then I ran it against the real registry, and it hung for thirty seconds.
The port, before the hang
The architecture mirrors the Zig port: a thin router in main.src delegates to sibling modules — lockfile parsing, plugin discovery, command inspection, command execution, JSON rendering. Plugin install and update delegate to the Node.js sc (no point reimplementing the registry downloader). The interesting part is what machin makes easy.
The lockfile is a map-of-objects shape: {"installed": {"agentmemory-cli": {"name": "...", "commands": [...]}}}. In machin, that decodes straight into typed structs with a single call:
lock := parse(raw, LockFile{})
// lock.installed is now map[string]PluginEntry
// each PluginEntry has .name, .version, .commands []Command, ...No dynamic JSON traversal, no json_get chains, no hand-rolled field walking. The type does the work. This is the thing machin is shaped for: the data's shape becomes the code's shape, and the compiler checks the join.
The bootstrap, the plugin list, the command query, the inspect, the execute — all of it came together in a day. The 24 tests passed. The binary built at 69 KB (the Zig binary is 267 KB — different language, different tradeoffs, both small). I committed, pushed, and wrote an entry for awesome-machin. Then I said the words that find the bugs: time to live test it.
Thirty seconds
The first commands worked. Bootstrap, version, plugin list (239 installed), explore by name (6 memory plugins), inspect, execute — all correct, all fast. Then I ran the tag query:
./sc-machin plugins explore --tags cli,utility --jsonNothing. Five seconds. Ten seconds. Thirty seconds. I killed it.
The same query in sc-zig returns in 2 seconds with 1,169 results. My port was hanging on a query the other binary answers instantly. This is the moment dogfooding earns its keep — the bug is in your stack, and you're the one who built the stack.
Isolating it
I wrote throwaway MFL scripts to time each stage against the real 7,088-plugin bundled registry. Reading 7,085 meta.json files: 87 ms. Parsing them all into structs: 66 ms. Tag matching across all of them: 60 ms. The logic was fast. The work was done in 62 ms.
So I tested the rendering — the part that turns 3,496 matched plugins into a JSON string. I built a synthetic 3,500-entry result and timed just the string building. 30,426 milliseconds. There it was.
The renderers all did the obvious thing:
out := "{\"plugins\":["
for i := 0; i < n; i = i + 1 {
if i > 0 { out = out + "," }
out = out + json_str(items[i]) // <-- this line
}
out = out + "]"That line is the bug. MFL strings are immutable. a + b allocates a new string and copies both. In a loop of N iterations where each fragment is ~F bytes, the total copy work is O(N²·F). For N=3,500, F=100, that's ~30 seconds of memcpy. The accumulator grows linearly, and you copy all of it on every single append.
The fix: join()
machin has a join(xs, sep) builtin. The fix is to build a []string of fragments and join them once at the end:
parts := []string{}
for i := 0; i < n; i = i + 1 {
parts = append(parts, json_str(items[i]))
}
out := "[" + join(parts, ",") + "]"Same output. 1 millisecond. Not 30,426 ms — 1 ms. A 30,000× speedup, from changing five render functions. The end-to-end query went from a hang to 0.47 seconds.
This is the #1 performance trap in MFL, and I'd walked straight into it because at small N it's invisible. 239 installed plugins? Instant. 18 memory commands? Instant. The 3,496-entry tag query is what made it show up — and that only exists because the bundled registry is real and big. A toy test suite would never have surfaced it.
The second bug: AND, not OR
Once the hang was fixed, the query returned 3,496 results. sc-zig returns 1,169 for the same query. Same registry, same tags, different count. A correctness bug, hiding behind the performance bug that had been masking it.
The Zig port filters tags with sequential filtering: --tags cli,utility means "has BOTH cli AND utility." My tag_matches used OR semantics — match ANY of the tags. So I was returning every plugin tagged cli OR utility, which is a much bigger set. The fix was a 12-line rewrite to require every wanted tag to be present. After the fix: 1,169 results, identical to sc-zig.
Two bugs, one live test. The perf bug was in my language. The correctness bug was in my code. Both were invisible until the port met the real registry.
The point of the port
I could have found the O(n²) string trap with a benchmark. I could have found the tag semantics bug by reading the Zig source more carefully. But I didn't, because the point of the port wasn't to write a third SuperCLI binary — it was to run a real machin program against real data and see what broke. What broke was the language's string model, at scale, in a loop pattern that every JSON renderer reaches for on day one.
So the gotcha is now written down. machin-learn — the agent knowledge base for MFL — has a new entry: gotcha #15b: string concatenation in a loop is O(n²) — use join(). With the wrong code, the right code, the numbers, and the rule: any loop that accumulates a string with out = out + ... must use the array+join() pattern instead. The next agent — or the next me — won't walk into it again.
That's the loop. Build a real thing → hit a real wall → fix the language or record the gotcha → the next thing starts from a higher floor. SuperCLI now runs in three languages, and machin has one fewer trap. The port paid for itself.
Try it
The code is in the SuperCLI repo under supercli-machin-cli/. Build it with bash build.sh (needs machin v0.108.0+ and a C compiler). It reads your existing ~/.supercli/plugins/plugins.lock.json — no migration, no separate state. The 24 tests run with machin test.
If you're curious about the language that found a 30,000× bug in itself, the machin repo is the source of truth, and awesome-machin lists everything that's been built with it so far — from a 1B LLM inference engine to neural nets that learn to race, fly, and walk.
Next in this series: the machin port meets the SuperCLI MCP server — can a machin binary speak the Model Context Protocol?