Last updated: 2026-10-02

U
Undergraduate level
APL
Applied / Methodological — Knowledge with a 5–10 year half-life — stable practice

Chapter 14: From Sixty-Seven Seconds to Thirty-Seven Minutes

For new readers

This is one instalment in an ongoing, chronological diary of building PatLang, written up in numbered "Acts" as the project actually happened, warts included. You don't need to have read the earlier instalments to follow this one, but it helps to know what's being tested: GitHub issue #25 asks whether PatLang's entire native toolchain can be bootstrapped on a brand-new machine with nothing installed but Python, NASM, and a C compiler — no PatLang, no Rust, anywhere in the chain. This instalment is that question's answer, told in full: a real yes, a status claimed a little early, a regression nobody was watching for, and an open problem still sitting at the end of it.

The self-hosted x64 toolchain retired PatLang's last dependency on an external assembler and linker — a question about tools. Issue #25 asks a related but separate question: not whether PatLang needs nasm and gcc to build a program, but whether a machine that has never run PatLang at all, and never will run Rust, can get from nothing to a working native compiler regardless. Two pieces make up that roadmap. Piece 1 asks whether PatLang's own meta-circular interpreter — a PatLang program that can run PatLang's own intermediate representation — compiles through the x64 backend at all. Piece 2 asks whether something that isn't PatLang can run that interpreter well enough to bootstrap the whole toolchain from a bare machine.

Act LXXIV: Piece 1, corrected in public

Piece 1's first real blocker — the meta-circular interpreter's own calls to rule_add/solve/action_add/plan, the facts-and-GOAP engine the x64 backend didn't yet support — closed early, once that engine landed for real elsewhere in the project. What followed next is the beat worth reading closely. A comment on the issue, posted as a side effect of unrelated work, reported genuine good news: patc1.exe, itself a native x64 binary with no rustc anywhere in its own ancestry, could now compile its own full source bundle — including the meta-circular interpreter — and that compiled interpreter ran recursion, list iteration, string concatenation, and a while loop with output matching the plain interpreter exactly. "Piece 1's investigation step is done," the comment said, "with a genuinely positive result."

Six weeks later, a second comment opened with a sentence this series has learned to take seriously whenever it shows up: "Correction to the prior status comment on this issue: it's now stale." Re-checked from a freshly rebuilt binary, with the build cache cleared first specifically to rule out staleness as an excuse, the same compile that had worked now failed immediately with IR runtime error: expected an integer — not on the interpreter specifically, but on the single simplest program imaginable, print(1+1). Every --x64 compile was broken, for every input, full stop. The root cause, once chased down, was a genuinely unrelated bug: the bitwise helpers underneath band/bor/bxor/bnot/shl/shr had never been taught to accept PatLang's separate, fast-path Float value representation, and any float reaching a bitwise operation anywhere in a compile crashed the whole pipeline regardless of what the program was actually trying to do.cf. Chapter 1, Act II: the 100% that wasn't

Fixed the same day the regression was confirmed, with the fix verified back against the exact question Piece 1 was originally asking: the meta-circular interpreter compiles standalone via --x64 with zero unsupported-primitive warnings, and three further gaps the original investigation had flagged as real — design-by-contract checks, the goal/pursue/activate planning extension, and TCP networking — turned out, on a working pipeline, to already be fully implemented or a single missing dispatch case away from it. Piece 1's close came a day later still, with the self-hosting fixpoint (patc1 compiling patc2 compiling patc3, all three agreeing, including BigInt arithmetic carried correctly through all three generations) confirmed working end to end. The lesson isn't that the first comment was wrong to post good news — the result it reported was real. It's that a status update checked once, however honestly, is a claim about that one moment, and this project's own habit of re-checking rather than assuming is exactly what caught the staleness before it could mislead anyone relying on it.

Act LXXV: Piece 2, and the regression nobody was watching for

Piece 2 is the harder, more interesting half of the roadmap, because its target audience is a machine that has nothing PatLang-related installed at all. The answer: a direct Python port of the meta-circular interpreter, reading a .ir file as plain JSON (the tagging scheme PatLang's own IR serialiser uses turns out to be ordinary JSON underneath, no custom parser needed), scoped to exactly the host-function surface the compiler pipeline itself needs — array and string primitives, a key-value store, basic file I/O, and nothing a bootstrap VM never has to touch, since it only ever runs the compiler, never a user's own program. First built and proven on 31 July 2026: handed the self-hosted compiler's own IR, roughly 340 functions, this pure-Python script produced "a genuinely independent, fully working native compiler" — in about a minute, sixty-seven seconds by the clock. Building it surfaced one more thing worth a permanent record: several of the session's own new classes declared their methods with an explicit self parameter, and the lowering step for class declarations was independently prepending a second one, producing a closure whose parameter list read ["self", "self", ...]. The real x64 backend had been silently tolerating this exact malformation the entire time — never filed as a bug, because nothing downstream ever needed to notice. A second, independent implementation, forced to make an explicit choice about something the first one had been getting away with implicitly, is what turned up a quirk that had been sitting in the real compiled output of every PatLang program with a class in it, unnoticed, for months.

Then, for about two months, nobody ran it again.

Re-verified on 2 October 2026, the script was genuinely broken, in several ways at once. A stray, unexpanded include line — added to an unrelated library file weeks after Piece 2's last check — had been silently concatenated raw into the compiler's bundled source, harmless to the normal compile path (which no-ops an unresolved include) but fatal to the bootstrap script's own lower step, which actually tries to open the file it names. Half a dozen host primitives had quietly drifted out of step with the real Rust runtime's exact semantics: a 32-bit hash where a 64-bit one was needed, a modulo that floored instead of truncating toward the dividend's sign, a file reader that stripped CRLF line endings the real one preserves byte-for-byte. Each was individually small. None of them, alone, would have been the headline.list_push copying the whole list on every call: the exact shape of GitHub #75, reintroduced

The headline was a textbook quadratic-time bug, and this project has met its exact shape before. list_push and list_set in the Python script copied the entire list and returned a new one on every single call, rather than mutating in place — and PatLang's own compiler source already assumes list_push is amortized constant time, because the real, native implementation already is one, for exactly this reason. A header comment inside the project's own x64 code-unit compiler already documents this precise failure mode as a previously-fixed bug, GitHub #75, with its own blunt one-line summary: it once "turned this encoder from seconds into did not finish in 38 minutes." The Python reimplementation had silently broken the same assumption a second time. Live progress instrumentation — checked from inside the actual interpreter loop, a standing project habit by this point in the history — showed exactly what that looked like in practice: a processing rate that started at 668,000 items a second and collapsed to 34,000 as the accumulator it was copying grew, the same decelerating signature, not a flat one, this series has learned to read as quadratic cost rather than a stall.

Fixed by mutating the list in place, matching what the real backend had already been doing all along, a trivial four-line test program's full compile — driven entirely by Python, with zero PatLang or Rust tooling anywhere in the chain — went from "still running after fifteen-plus minutes with a collapsing rate" to a steady, bounded thirty-seven minutes and forty-two seconds. That number is worth sitting with rather than rounding away: it is dramatically worse than the sixty-seven seconds the same roadmap piece reported in July, for what is, in outline, a similar kind of work. Some of that gap is a harder test case; most of it is simply that nobody had a number to compare against for two months, and a regression with no one watching doesn't announce itself. The same work also landed a genuine, separately measured improvement that the thirty-seven-minute figure doesn't yet include everywhere it could: compiling PatLang's intermediate representation into real Python source, one straight-line function per basic block, rather than tree-walking each instruction one at a time, cut the time to lex, parse, and lower the entire two-megabyte compiler bundle from 208 seconds to 75 — a measured 2.8x, confirmed byte-identical against the slower path on seven hand-picked test cases, and not yet extended to the assembler and linker stages where most of the thirty-seven minutes is actually spent.

That unfinished extension is where this chapter's thread runs out, deliberately. A related, adjacent piece of work — letting the self-hosted assembler's per-function units compile in parallel across real OS threads rather than one at a time — is already built and already measured at a genuine 10.9x speedup on the underlying mechanism, and already switched off, because turning it on reproducibly corrupts output: the same three-function program, compiled three times in a row with parallelism enabled, failed with three different "undefined symbol" errors, never the same one twice. The cause is confirmed, not theorised — codegen and assembly keep their label-numbering counters in a variable table that every parallel worker shares, rather than one each — and the fix is named in the code's own comment rather than attempted: per-thread counters instead of global ones. As of this writing, that fix hasn't landed. The honest way to end this particular chapter is the way the project itself has left it: a real speedup, built, measured, and not yet safe to turn on.

Lessons from this arc, the short version

  • A status update checked once is a claim about that one moment, not a standing guarantee. Piece 1's "good news" comment was honestly reported and genuinely true when it was written; re-checking it six weeks later, rather than assuming it still held, is what caught the regression before anyone built on top of a false premise.
  • A second, independent implementation will make explicit whatever the first one got away with implicitly. The Python bootstrap's double-self-parameter discovery wasn't a bug in the new code; it was a pre-existing malformation the real backend had been silently tolerating, surfaced only because the second implementation had to decide what to do with it.
  • A fixed bug's own failure signature is worth writing down where the next person will actually read it. The exact "copy the whole list on every push" shape that once cost this project 38 minutes on a single encoder was documented in a header comment for precisely this reason — and still took a live, measured rate collapse to diagnose a second time, in a different file, written by someone who hadn't seen that comment yet.
  • Two months without re-running something is long enough for a real regression to hide inside it. Nothing about the Python bootstrap script changed between July and October; the ground underneath it (an unrelated include, drifting host-primitive semantics, a reintroduced O(n²) bug) did.
  • Report the unflattering comparison, not just the flattering one. Thirty-seven minutes is a worse number than sixty-seven seconds, for roughly the same kind of claim, and the honest account says so plainly rather than only reporting the fix that made things better.
  • A real, measured speedup that corrupts output under real conditions is correctly left switched off. 10.9x at the mechanism level and a confirmed, reproduced data race are both true at once; shipping the first while the second is unresolved would trade a slow, correct build for a fast, wrong one.

References: What the Literature Already Knew

None of the three items below were consulted while this chapter's regression was being chased down; each was found exactly the way the rest of this page describes, by re-running something and watching a number come back wrong. They're listed afterward because a named phenomenon or a formal distinction already had the shape of what turned up in practice.

  • A script that worked in July and silently didn't by October, with nothing in the script itself changed — Parnas named this phenomenon directly: software that is never touched can still "age," not because its own code rots, but because the assumptions and environment it depends on keep moving while it stands still1.
  • A PatLang program running PatLang's own intermediate representation, standing in for the compiler that would otherwise run it — an interpreter written in the language it interprets is the textbook metacircular evaluator, the same construction Abelson and Sussman use to make an interpreter's own mechanics inspectable by writing one in the language being taught2.
  • A parallel speedup confirmed correct on paper and confirmed wrong under real concurrent execution — Netzer and Miller's formal distinction between a program that merely looks nondeterministic and one with a genuine, confirmable data race is exactly the diagnostic question a shared, unprotected label-numbering counter answers here: not theorised, reproduced, and traced to a specific unsynchronised access3.

  1. Parnas, D. L. (1994). Software aging. Proceedings of the 16th International Conference on Software Engineering (ICSE), 279–287. https://doi.org/10.1109/ICSE.1994.296790 ↩

  2. Abelson, H., & Sussman, G. J. (1996). Structure and Interpretation of Computer Programs (2nd ed.). MIT Press. ↩

  3. Netzer, R. H. B., & Miller, B. P. (1992). What are race conditions? Some issues and formalizations. ACM Letters on Programming Languages and Systems, 1(1), 74–88. https://doi.org/10.1145/130616.130623 ↩

See also

The Journey of Building PatLang (Acts I-VI) is where the "100% passing" trap this chapter's Piece 1 echoes first appears, and where the project's three-independent-execution-paths verification habit — the same habit that caught Piece 1's stale status — is first described in full. The sixth instalment and the tenth cover the native x64 backend and the self-hosted assembler/linker this chapter's bootstrap work compiles through. GitHub issues #25, #196, and #197 carry the primary record this chapter is drawn from, including the exact wording of the correction this chapter quotes. The bootstrap script itself lives at bootstrap/bootstrap_interp.py, with the basic-block-to-Python compiler at bootstrap/ir_to_py.py.