the compiler

guardrails, not lifetimes

here is a json decoder reading a 188 kb file—nested objects, arrays, escapes, unicode. it is written in a pure language with no pointers, no manual memory, and not one lifetime annotation. beside it, on the same machine and the same file, are the decoder a rust team hand-tunes and ships, a rust parser written the way most people would write it, and go's standard library. the clock is milliseconds of cpu per decode and the four lanes ran interleaved. shorter is faster.

decoderms/decodepeak mem
kanso + arenas 0.874.2 mb
serde_json (rust, hand-tuned) 0.906.8 mb
reasonably-written rust 1.046.9 mb
go encoding/json 2.0511.8 mb

sat 2026-08-07, seven rounds, four lanes interleaved so contention leans on all of them at once. milliseconds of cpu per decode, read as a slope from the same decoder built to run 150 times and 450 times so process startup and the file read cancel. cpu time bills every thread, so go's collector counts against it. this board is re-sat by hand when a release goes out; nothing rewrites it on a merge.

the person who wrote the kanso decoder never typed the word lifetime, never named a buffer, never freed anything, never chose a memory strategy. a reasonably-written rust parser running the same algorithm trails by a fifth, and go takes better than twice as long with its collector working. the recipe at the bottom of this page runs the whole field on your own machine in a few minutes.

a pure, annotation-free language keeping pace with hand-tuned rust is a claim worth doubting. so the rest of this page is the evidence: the papers we read, the experiments that failed (twice, in one case), the one idea that did most of the work, and the recipe. no lifetimes, no garbage collector, no annotations — guardrails instead.

and the json table is now the older of the page's two scoreboards. kanso is a lazy language—a value is computed when it's demanded, or never—and on workloads where demand is conditional, the second scoreboard (§07) shows the clear kanso form fifteen times ahead of rust as naturally written and within forty percent of rust hand-restructured to skip the same work, with counters proving 95% of the work was never born. the strict-tier story below and the lazy story in §07 are two halves of one design.

01constraints buy freedom

picture a fast car on a mountain road with a cliff on one side and no railing. even a great driver takes the corners slowly there, because a single mistake goes over the edge, so caution has to live in every turn.

now bolt a guardrail along the drop. the same driver takes the same corners faster, because the railing has made going over the edge impossible, and once the worst outcome is off the table there is nothing left to be careful about. the guardrail lets a good driver off the brakes.

kanso is built out of guardrails. functions are pure. nothing mutates. every program has one canonical way it's written — a parenthesis pair that groups nothing is rejected the same way a stray blank line is, so 1 + (2 * 3) does not sit beside 1 + 2 * 3 as a second spelling of one thing. the only branch in the language is dispatch—choosing a function by the shape of its arguments—so there is no free-floating if on values for a compiler to squint at. to the person writing kanso, these are just the rules of a plain, readable language, and following them costs nothing.

to the compiler, they are railings. an ordinary compiler has to assume the worst at every turn: this value might be shared, this call might have printed something, this branch might do anything. so it drives slowly. kanso's compiler knows the worst can't happen, because the language already forbade it, and so it can take optimizations that would be reckless, even unsound, in a language without the railings. it treats each constraint as something to spend rather than work around.

02the journey, honestly

the goal was straightforward: run faster than an ordinary rust program without asking the programmer to do rust's work by hand. there are two known ways to manage memory safely, and both charge the programmer for it. rust makes you prove in the source exactly when each value dies; those proofs are lifetimes, and writing them is real work. garbage collection skips the proofs and instead pauses the program every so often to find and free dead memory; the cost moves from your keyboard to your latency. we wanted a third option, so we spent a while in the literature.

the card we expected to play: perceus

the most promising idea in the literature is reference counting with reuse. give every object a small tally of how many things currently point at it. when the tally hits zero, the object is dead and can be freed on the spot—no janitor, no pause. and if an object is about to die and the program is right then building a new one of the same shape, don't free-and-reallocate, just overwrite the old one in place. this is called perceus; koka invented it, and lean 4 and roc ship it in production. it is real, and it is good.

we expected to build it. then we did the arithmetic on kanso's own budget, and it failed twice over.

first, the tally has to live somewhere: a small counter attached to every object, paid for on every object, whether or not the reuse ever pays off. second, and worse, is what happens when a big structure dies. dropping the last pointer to the root of a large tree walks the whole tree, decrementing each child and freeing as it goes. that walk is a pause — a small garbage collection by another name, and it happens exactly when a big result is thrown away, which in a parser is constantly.

err - always arrives, never uninvited
err

a cascade is me, but for memory, and it doesn't announce itself. one pointer drops and ten thousand children get freed on a hot path. at least i ride the return values in the open, where you can see me coming.

the confession

there is an embarrassing part of this story, and leaving it out would make the page worse. for a while our own notes listed in-place reuse as built and working. then a review actually read the code instead of the notes. the analysis that was supposed to find reusable objects ran correctly and handed its answer to—nothing. no part of the compiler consumed it. it was dead code that had been dead for weeks, and meanwhile the per-object tally it was meant to justify was still sitting on every object, costing us the overhead with none of the benefit.

that's the kind of thing a differential test corpus and an append-only log exist to catch, and it's why the honesty tags on this page are load-bearing rather than decorative. the one reuse that does fire today is the narrow, provable case—appending to a list you uniquely own, overwriting in place. the general machinery got pulled back to the bench.

where perceus landed

we didn't delete the idea. we demoted it. reference counting survives in the design as a fallback — something we can drop in for a specific pattern if the static approach ever turns out to copy too much — rather than the main mechanism. the mechanism we actually use came almost for free from the shape of the program itself, which is the next section.

a few other roads we walked to the end and turned back from, so no one has to walk them again:

interaction nets (the hvm/bend line) are spectacular on a narrow class of problems and roughly ten times slower on ordinary numeric code—the retrospective saying so is by the person who invented them. adopting them means replacing the whole runtime, not adding one pass. they stay on a watch list, not the road.

an eight-byte ascii skip in the utf-8 validator looked like free speed: gulp eight bytes of text at once, and if they're all plain ascii, skip the per-byte checking. we tried it. it made the benchmark slower by 3%, because the strings in real json are mostly shorter than eight bytes, so the setup cost was paid every time and the skip almost never fired. then—months later, having forgotten—we tried the exact same thing again, and it lost again by the same margin. the log now says, in as many words: do not try a third time without a workload full of long strings.

03the strict tier: rewinding arenasshipped

this section teaches the shipped allocator for strict values — one of the two tiers named in the intro, and the one behind the json scoreboard. the lazy tier and the counted destination are §07's story; read them as two halves.

the idea that does most of the work needs no background beyond "programs use memory." each step below is small.

the problem everyone has

a running program constantly asks for little pieces of memory—a list here, a string there, thousands of times a second. asking is easy. giving the pieces back is the hard part, because handing back a piece something still needs corrupts the program, and never handing anything back fills the machine until it dies. the three classic answers are the three we've already met: do it by hand (fast, and famously how programs get hacked), collect the garbage at runtime (safe, with pauses), or prove to the compiler when each piece dies the way rust does (fast and safe, but you write the proof).

programs are loops

kanso's answer starts from a plain observation about the shape of real programs: they repeat. serve one request. handle one keystroke. decode one file. take one step of a loop. work arrives, work happens, an answer leaves—and then the whole thing repeats. call each cycle a beat. almost everything a program makes, it makes inside a beat, purely in service of that beat's answer, and the instant the answer leaves, all of that scaffolding is trash at once.

two kinds of memory

so kanso keeps two kinds of memory, matching the two kinds of thing a beat touches.

the shelf the fold state—one value, one owner updated in place, forever never swept, never counted the workbench everything made during the beat shared freely—no bookkeeping all of it dies together swept at the beat—one pointer reset the rare survivor is carried across

the shelf holds long-lived state—the thing an imperative program would keep in a mutable global. in kanso that's one value with exactly one owner, handed from beat to beat, so the compiler just updates it in place and never has to wonder about it.

the workbench holds everything else: the intermediate lists, the parsed fragments, the half-built strings, all the scaffolding a beat erects on the way to its answer.

the sweep

at the end of a beat, kanso does not go looking through the workbench asking which scraps are garbage. they all are—the answer already left. so it sweeps the whole bench with a single move: one pointer slides back to where the bench started, and everything past it is gone. a thousand things made or one thing made, the sweep costs the same, because it never touches the things individually. it's about as expensive as setting a variable to zero.

the strategy, precisely

so the story reads honestly end to end, here is the whole memory strategy in four sentences. counting is the model: kanso's values are immutable and the data graph is acyclic, so reference counting is complete—every cell freeable at its last use, no tracing, no collector, no soundness gap. the compiler then proves most counts away: a batch it can show dies together carries no counts at all and lives on a rewinding arena—one pointer reset, no walk, which is this section and the json receipts in §08; a value it can show is used linearly is reused in place, no count either. real counts survive only where lifetime is a runtime fact: thunks—a rewindable arena structurally cannot hold a pending computation (§07 has the receipts from jhc, ghc, and the ml kit)—and structures whose sharing escapes proof. the objections to counting-everywhere are real—a tally on every object, a cascading walk when a big tree dies—and they are exactly why the arena is the proved fast path inside the counted model, not a casualty of it.

the experiments behind that ruling are in §07; the engines walk there in the open, ratchet by ratchet.

a garbage collector spends its time working out which objects are still alive. the sweep never does that work, because at the end of a beat everything on the workbench is already dead.

and now sharing is free

the entire reason reference counting exists is to answer one question: when two things point at the same piece of memory, who frees it last? counting answers by tallying pointers—and paying, a little on every share and occasionally a lot on that cascade.

inside a beat, the question has no meaning. point at something twice, point at it ten times, let two half-built results share a chunk—nobody tallies anything, because it all dies at the same instant no matter who was pointing where. "who frees it last?" has one answer everywhere: the beat does.

the two loose ends

survivors. once in a while a value made on the workbench genuinely needs to outlive its beat. the compiler's escape analysis—built and tested—spots these at compile time. a small survivor is copied onto the shelf, and the copy is placed before the program ever runs. a big survivor isn't copied at all: its bench is kept whole, a fresh one is started, and the old bench carries a single count of how many survivors still point into it—one integer per bench, never per object—swept the ordinary way when that count hits zero. no walk, no cascade, no per-object tally.

long beats. a beat that piles up a mountain of scrap before it finishes can be handed intermediate sweep points mid-beat. because kanso's function bodies are flat and the whole program is visible to the compiler, it knows exactly which values are still alive at any line, so those extra sweeps are placed statically too.

what the programmer does

nothing. there is no keyword in this section. no lifetime, no annotation, no region name, no tuning flag. you write the loop, the parser, the handler; the cycle is already in the shape of the program, and the compiler reads it and places every memory decision at compile time. nothing does this work at runtime, because by the time the program runs there is nothing left to decide.

the receipt

kanso's allocator was once the worst case of this design: one enormous workbench that never got swept, so a long-running program's memory just climbed. we made the bench resettable and let the compiler drop a beat boundary between decodes of the json benchmark—the same 188 kb file, over and over. the difference is not a matter of percentages. grow-only, the memory climbs without bound; swept every beat, it stays flat.

mem decodes → before: never swept—0.76 gb at 150, 15 gb at 3000, climbing after: swept every beat—5.8 mb, flat at any count

grow-only, the peak tracks the work: 0.76 gb at 150 decodes, 15 gb at 3000, because the program keeps every scrap it ever touched. swept every beat, the peak is 5.8 mb at 150 decodes and 5.8 mb at 3000 — flat, because a decode's scraps die with the decode. and the swept version ran faster too: instead of asking the operating system for fresh, cold pages, it reused the same warm ones, so the cache stayed hot for the whole run.

the honesty tier. the compiler now emits the beat boundaries itself: an analysis proves that a loop's iterations keep nothing across the line. with the per-object header gone from the arena, the emitted version measures 5.8 mb peak—under serde's 6.7 and the reasonably-written rust's 6.8. the copy-or-pin split for survivors was the last thing here still called planned; it is now declined on its own numbers, which the ledger carries — pinning the survivor's page died to a measurement before it was built, and retaining the region instead was built and measured and lost. what remains planned is the static sweep points for long beats.

"almost everything a program makes, it makes inside a beat" is the claim this whole section rests on, and it now has a number on the four boards. at a beat boundary the live set is exactly what the carry stages, and the runtime already sizes it to allocate a buffer, so counting it costs nothing:

shelf      boundaries   live crossing (max)   arena held then
decode              1        112 bytes          1,048,576
encode            401         80 bytes          4,194,304
one-shot            3          995,504          3,145,728
basket         10,000                0          1,048,576

encode crosses a hundred and twenty-eight bytes in total across four hundred and one boundaries, and basket crosses nothing at all across ten thousand. "rare" is rarer than the design assumed. one-shot is the exception at 31.6%, which is the same board the evacuation counter singles out, and the two agree about where the remaining cost lives.

what this measures is liveness at the rewind, so it bounds how much of what the arena holds is already dead — at encode's rewinds, essentially all four megabytes of it. it does not say what a reference-counted runtime's peak would be, because such a runtime frees as values die rather than at boundaries, and its peak is the largest set live at one instant. that quantity is still unmeasured, and it is the last unknown in the comparison. the honest ceiling on the gap is smaller than the table looks in any case: the arena's floor is one block, so eighty bytes is not a footprint any policy here could reach.

this is the corpus, not the verdict. the number that will ultimately judge the design is still the one this section has always named—write the kanso compiler in kanso and count survivors on a program nobody tuned for the boards.

04the last stretch to serdeshipped

the arenas closed most of the gap on their own. rust pays a separate malloc and free for every object unless its programmer hand-builds an arena to avoid it—which is exactly the kind of expert move most code never gets around to. kanso's shape makes that arena automatic, sound, and invisible. but the arena alone left a representational gap against serde—tagged values and per-call dispatch in the hot loop—and three changes, in this order, closed most of it. (laziness later spent a slice of the margin back; the board above is the current, honest ledger.)

rung one: the inline twins −10.7%

the runtime has a handful of tiny helpers—is this value truthy? did that call fail?—each one line long. we had marked them to be folded directly into the code that calls them, and asked the linker's whole-program optimizer (the -flto pass) to do the folding at the very end of the build. it quietly declined. deep in the release binary, twenty-seven of those tiny calls were still there, each one a little detour off the hot path. finding them cost an evening of staring at disassembly, waiting for the missing pieces to explain themselves.

the fix was to stop asking the linker later and have the compiler do the inlining itself, as it emits the code. we moved the decision into the ir. it was worth more than the other two changes combined, and it left a lesson in the log in block capitals: never trust -flto to inline across that boundary—verify it in the disassembly.

rung two: look at sixteen bytes at once −1.4%

the string scanner is hunting for two bytes: the closing quote and the backslash. the plain way checks one byte, then the next, then the next. the fast way loads sixteen bytes into a single wide register and asks "is either target anywhere in here?" in one instruction—the vector units every modern chip has (neon on apple silicon, sse2 on intel). it's serde's own trick, borrowed in the open.

rung three: the integer fast path −2.2%

kanso integers are arbitrary precision—they never overflow. but almost every integer in a real json file is small enough to fit in a single machine word. so the common case runs a tight loop that just accumulates digits, and only the rare genuinely-enormous number falls through to the general, bignum-capable path. floats are left exactly as they were, because their shortest-round-trip rendering is sacred and not worth risking for a few percent.

all three produce byte-identical output to the versions they replaced, checked on both intel and apple-silicon ci. and they're held in place by cost goldens—the receipts section explains that ratchet—so none of the three can quietly regress.

the write path runs the same playbook

everything above is about reading json. writing it out uses the same ideas, and each piece is there because the obvious alternative loses.

the encoder threads one byte builder through the whole tree: an accumulator that owns a capacity-headed buffer and claims its own frontier, the same discipline the arena gives list push. a fold of appends is amortized linear, every intermediate stays an ordinary value, and each byte is written once. the obvious alternative—build a string per node and join them upward—recopies every byte at each nesting level on its way to the root, about six copies per byte on a flat document.

escaping dispatches on bytes, so the compiler gives it the same switchboard the decoder's scanner gets—dispatching on single-character strings would put a memcmp probe on every character written. and it keeps the decoder's favorite bet: one simd pass proves a string holds no quote, backslash, or control byte—almost every string in real json—and a proven-clean string is appended whole, in one copy.

numbers get the treatment their distributions deserve. almost no double survives shortest-round-trip rendering in under fifteen digits, so the precision probe starts at fifteen instead of one—probes below that are dtoa calls that cannot win. integers render through a dedicated digit loop rather than the general formatter.

the receipt lives where a user can feel it: kq's pretty-printer rides the same builder and wins every board of its interleaved race against jq—hardest on the biggest documents, where the recopying alternative hurts most. bench/kq_race.sh in that repo reproduces it, byte-identity checked before any timing starts.

05the rest of the license

the arenas and the ladder are where the headline number comes from. but the guardrails pay out in other places too, and these are the ones worth seeing before the deep end.

dispatch becomes a switchboard

a recursive-descent parser is one long question asked over and over: what character am i looking at? in kanso you answer it by writing one small function per case—one for a quote, one for a bracket, one for the letter t, and so on—and the language picks the right one by the argument. that's dispatch, and it's the only kind of branch kanso has.

the naive way to run seven cases is to check them one after another: is it a quote? no. a bracket? no. a t?—like walking into a room and asking every person in turn whether the call is for them. kanso's cases are a fixed, known set of literal bytes, so the compiler instead builds a switchboard: take the byte, use it as an index, jump straight to the one case that matches, in a single hop. seven functions in the source; one indexed jump in the machine.

and because the compiler knows the exact set of value shapes that can reach each call site, it stamps out a specialized copy of the code for each one, with the type-checking removed—the checks aren't predicted well or branch-hinted, they're simply gone, because on that path the answer was already known. the word for making one specialized copy per concrete shape is monomorphization, and it's exactly what you just watched happen to the switchboard. the receipts, with the emitted machine code, are in chapter 07.

recursion that never overflows

recursion is kanso's only loop, so a call in tail position—the last thing a function does—cannot be allowed to grow the stack, or every loop would eventually crash. it doesn't. the native backend emits musttail, the strongest promise llvm offers and one the code generator is required to honor; the browser backend emits wasm's return_call. the receipts: ten million frames of mutual recursion in constant stack natively, one million inside a browser tab. deep recursion carries no risk to manage, because in kanso it is simply how you write a loop.

arbitrary precision, paid for only when used

an int in kanso never overflows—that's a guarantee that holds on every engine and isn't for sale. what the compiler gets to decide is how rarely you pay for it. it emits each function in two versions. the fast one bets the number fits in a machine word and runs the plain, fast code, with a cheap check that bails out of the whole version if the bet is wrong. the bail restarts the call in a second version compiled against heap-backed bignums.

restarting a half-finished function would be a disaster in most languages—it might already have printed something, or mutated something, and you can't un-print. but kanso functions are pure and effects are just descriptions, so a restart changes nothing you could observe. javascript engines are famous for the same move—guess the likely type, recover when surprised—but they do it at runtime, and kanso does it ahead of time, with none of the machinery: no on-stack replacement, no deopt metadata, no interpreter to fall back into. and it was built to generalize; the integer bet is only the first probable-but-unproven fact worth wagering on.

the data rulings, briefly

three smaller decisions each hand the compiler more structure while handing the reader less to remember. destructuring always wears braces ({ artist minutes } = song), so every field read is name-resolution against a known record and compiles to fixed offsets. a type may declare zero fields (type null), so a marker like json's null is ordinary library data, not a keyword. and a field may list several permitted types (title:null string), so reads dispatch on what the value is and the "is it null or a value?" conditional becomes unwritable—the compiler sees a closed set of tags it can enumerate and often erase.

rendering is dispatch

string interpolation looks like a language feature and compiles like a library. "{x}" is a call on an ordinary dispatch group named to_string, whose arms for the primitive types ship with the primitives themselves. so giving your own type a custom rendering is not an api to learn—it's the language's one mechanism, applied:

custom_render.kso—in the differential corpus, both engines
type money
  cents:int

pub play =
  price = money 350
  print "that costs {price}"

fn to_string (money cents)
  "${cents / 100}.{cents - (cents / 100 * 100)}"

that program prints that costs $3.50. no import, no interface declaration, no registration—an arm for a type you own joins the group, and dispatch does the rest. the ownership rule from the import model is what keeps this safe: nobody can re-arm to_string for int, because no module owns both the group and the primitive, so "{42}" renders identically in every program ever compiled. and that guarantee is also an optimization license: where the compiler can see a value is primitive-only, it skips the dispatch entirely and emits the direct renderer—custom rendering costs nothing on the paths that don't use it, which the cost golden pins as a constant.

the whole toolchain in a browser tab

a third engine emits wasm bytecode directly—hand-rolled, no llvm anywhere near it, because llvm is measured in hundreds of megabytes and a browser tab is not. the whole thing is about half a megabyte of wasm, and it's what lets the playground compile the program you type into machine code inside the page, tail calls and all. its value representation is deliberately odd today (every value is a small handle into a registry the toolchain owns), and that first cut is staged toward a self-contained module over time, every step of the migration pinned by the same corpus of byte-identical golden outputs the other two engines are held to.

06the deep end

from here down the page stops holding your hand. this is the theory under the arenas and the frontier past it—genuinely dense, mostly design rather than shipped code, and safe to skip if the numbers above are what you came for.

the memory manager that isn't

the arena tier is the picture; here is the theorem it draws. the claim is that kanso's compiler never has to reject a memory bug—a leak, a use-after-free, a who-frees-it-last—because there is no way to write one down. the bug is unrepresentable, the same way "invalid states unrepresentable" works for a database schema, applied here to memory itself.

you cannot get here by building a smarter analyzer. whether a value is uniquely owned is a non-trivial semantic property, so by rice's theorem it's undecidable in general, and the usual consolation—that a human can see cases the analyzer misses—doesn't hold: a human "seeing" uniqueness is producing a proof, and where no proof exists, no one sees it. worse, a reject-what-i-can't-prove checker is fragile in the exact way that ended whole-program usage inference in ghc (wansbrough & peyton jones, 1999): a distant, semantically-irrelevant edit flips an inferred fact and rejects code three modules away. so kanso builds neither a smarter checker nor a runtime fallback. it keeps the undecidable case from ever arising, by never letting a value's ownership become something the compiler has to settle at runtime.

the first construct does most of the work for free: value semantics. nothing aliases—every value is its own thing, and "mutation" produces a new value—so a use-after-free cannot be written. the compiler never has to check for it and grant it; the danger is simply absent, the way an undeclared variable is absent, and so the compiler never rejects for memory at all. what's left is not correctness but performance: for each value, can the compiler prove it uniquely owned (and reuse its storage in place) or not (and copy it)? the fallback for "not provably unique" is a static copy, settled at compile time—no per-object header, no runtime count. a distant edit that flips a value from unique to shared makes a copy appear—one line got slower—it never breaks the build.

this is where being closed-world stops being a footnote. rust's borrow checker is local: it must survive separate compilation and unknown callers, so it makes you write the proof as lifetimes. koka's perceus gives up proving uniqueness statically and tracks it in a runtime count (reinking, xie, de moura, leijen, PLDI 2021); its fip keyword (lorenzen, leijen, swierstra, ICFP 2023) recovers a static guarantee but still rests on that count. kanso sees every call site—no separate compilation, no unknown caller—so it can try to prove statically what those systems annotate or count, and settle reuse-or-copy before the program runs. no count, so no header; freeing is a drop the compiler inserts at the statically-known last use.

value semantics reuses beautifully for tree-shaped data threaded in a straight line. the three cases where it would otherwise force a copy or a count each already have a construct in the language whose shape keeps them settled, so the problem pattern is never expressed—only its safe form:

state is a fold. persistent state—the mutable global of an imperative language—is in kanso one value, threaded single-file through pure update(state, action) → state by the one executor loop. one owner, hand to hand, always uniquely owned: the ideal in-place case, not the aliasing trap. caching, the classic memory hazard, is here the most reuse-friendly pattern there is.

local mutation is a build block. where an algorithm genuinely wants scratch mutation—a hash table filling, an array sorting, a graph wiring itself up—a build block permits it under one rule: writes happen only through set, set parses only inside build, and its target must be born in the same block. the last expression freezes to an ordinary immutable value on the way out; outside the block, mutation doesn't parse. no var, no let mut, no mutability annotation on any name—mutability is a property of the place, so auditing "where can state change?" in a codebase is grep build. it's haskell's runST without the rank-2 ceremony, because closed-world makes "nothing escapes" a syntactic check.

and the block quietly settles the one question every reference-counted language dreads: cycles. set is an identity-preserving field write—a stays the same node while its field changes—which is exactly what closing a cycle requires and rebinding can never do. that looks like it should break counting, and here is why it doesn't:

immutable values cannot point at younger values. a value that existed before the block ran was already complete; making it point into the block would be mutation of pre-existing data, which is exactly what the block forbids. so every pointer in the heap aims pastward—except inside a build block, and the block boundary contains the exception. it follows that a cycle can only exist among values born in the same block: cycles cannot cross birthdays. the strongly-connected cluster is always one block's birth cohort, so the runtime counts the cohort, not the nodes—the block allocates into its own arena, the frozen result carries one count for the whole cohort, interior pointers (cycles included) are invisible to counting, and the last outside reference frees the arena in one shot. no cycle collector, no weak annotations, no leaks. the tradeoff, stated plainly: keeping one node of a frozen graph keeps its cohort alive, the way a go slice pins its array. python bolted a tracing collector onto its counts for this; swift asks humans to annotate weak and they get it wrong constantly; kanso's answer is a scoping rule that was already there for taste reasons.

irreducible sharing is a region—was the original fourth construct, and the laziness deep-dive demoted it. regions demand that a batch share one static lifetime, and a deferred computation's true lifetime is a runtime fact; every system that parked laziness in regions leaked or gave up (jhc, ghc's compact regions, the ml kit's gc backstop—receipts in §07). what replaced it: the value graph is acyclic except inside frozen build-block cohorts, and a cohort counts as one unit—so reference counting stays complete, irreducible sharing is just counted, and a region survives as the back-end optimization for batches the compiler proves die together. tofte–talpin stays honored as the ancestor of the bench; it stopped being the answer for sharing.

so the four bind together: a pure value core that reuses in place, a fold for state, a transient block for local mutation, a region for irreducible sharing. every program sits inside the statically-settled fragment by construction: the constructs that would create a memory problem are not in the language to reach for, so a checker never has to reject anything.

the honesty tiers, because most of this is design, not code yet. built and tested: the pure value core; whole-program borrow-vs-consume inference that hands each function a printable ownership signature—ownership as a per-function contract you can pin and blame, never an ambient verdict a distant edit silently flips; in-place reuse for uniquely-owned list builders; and build blocks themselves—build/set, the block-born rule, and cycle construction run byte-identically on all three engines, pinned by differential goldens. designed, spec-reserved, unbuilt: cohort freeing (the arena-per-block story the birthday theorem licenses), regions, and the static drop/free/reuse codegen the whole scheme rests on—the one part that's memory-unsafe to rush, and the reason it waits for a careful hand rather than a fast one. the residual cost, plainly: the fine-grained persistent-sharing pattern (clojure-style shared subtrees) that value semantics copies instead of sharing—a real speed cost in collection-heavy code, rare in the parsers and compilers kanso is for, and never once a program that fails to compile.

the frontier

the standing directive for what lands next: a core change is on the table only if it doesn't tax the user and the payoff is game-changing. the queue, each entry named with the property that licenses it:

generalized speculation—the integer trick from §05, past ints: bet on any probable-but-unproven fact (a dispatch target, list-not-map, ascii-not-utf8) with bail-and-restart, jit-style, ahead of time, zero deopt metadata. cost-bound inference (the raml lineage)—type-system-driven polynomial cost bounds so kanso check can flag "this used to be o(n), your change made it o(n²)," and feed the same bounds to the parallelism scheduler. equality saturation—rewrite the ir by e-graph instead of ordered passes; pipeline fusion (map f . map g becoming one pass) is unconditionally sound under purity, so phase-ordering stops being a problem category. simd structural scanning—simdjson-class byte classification feeding the switchboards the backend already emits, behind bytes primitives with no language surface; this is the separate, harder frontier that beating serde on raw throughput actually requires.

the mined queue: kernels the profiler already names

beneath the moonshots sits a band of narrower work where a published algorithm maps one-to-one onto a line in kanso's own profiles. each entry here carries its paper, the profile line it exists to erase, and byte-identity as its acceptance test. they land in order; the status marks move as they do.

1. shortest-round-trip float rendering shipped — ryū (adams, PLDI 2018). the digit core forms the half-ulp interval, scales it by a generated 125-bit power of five, and picks the shortest digit string inside — pure integer arithmetic, no probing, no dtoa; the format layer mirrors the old %g byte format exactly, and the interpreter renders through the same rules over rust's shortest digits, so all engines agree byte for byte (a latent large-exponent divergence found and closed on the way). fuzzed over fifty million doubles: zero failures, with the only divergences being 495 legal shortenings in the subnormal range — the true-shortest canon the probe could never reach. the dtoa family is gone from the encode profile entirely. dragonbox (jeon, 2020) is ranked rather than queued: rendering does show on the encode profile, where render_ryu takes 3.8%, and dragonbox's usual margin over ryū would recover something near one percent of encode. that is a win, and it sits behind the append-and-copy pair at 19.5% and the encode walker at 13.5%. nothing here is declined for being small — the order is by ceiling.

2. float parsing at memory speed shipped — the eisel–lemire algorithm (lemire, "number parsing at a gigabyte per second," 2021), now inside gcc, chrome, and rust's core. the decode mirror of the rendering entry, verified against an independently written reference over thirty million doubles, and pinned by a presence counter: el_parses reads 318450 on the decode board and 2123 on the encode one, so a change that drops the fast path turns ci red.

3. utf-8 validation in vector registers shipped — the full keiser & lemire algorithm ("validating utf-8 in less than one instruction per byte," 2021): three nibble lookups classify every two-byte window, a saturating compare pins the 3- and 4-byte continuation runs, all-ascii blocks skip classification entirely, and a trailing zero block makes truncation need no special case. spec-strict — overlongs, surrogates, beyond-U+10FFFF rejected — which also closed a latent divergence against the interpreter's strict validator, pinned by a differential golden. verified against an independent reference over seventy million boundary-crossing sequences at zero mismatches.

4. branchless map lookup measured, declined — eytzinger layout (khuong & morin, "array layouts for comparison-based searching," 2017). built, benchmarked, and reverted: at json-realistic map sizes (tens of keys) the index costs slightly more than the binary search it replaces, and at ten thousand string keys the two tie — string comparison dominates and layout can't help it. the paper's regime is huge arrays of machine-word keys; kanso's maps aren't that. the measurement stays here so the idea doesn't get re-mined.

5. building results in their final place already won — tail recursion modulo context (leijen & lorenzen, ICFP 2023). the technique writes each cell into its final position instead of threading an accumulator, and its regime is cons-cell construction. kanso builds with flat arrays and a frontier push, which already sits at that endpoint, so there was nothing left for it to buy. read and closed rather than built; the queue slot went to the names the profile actually showed.

6. in-place as a guarantee, not a find queued — fully-in-place functional programming (lorenzen, leijen, swierstra, ICFP 2023), plus call-pattern specialization (peyton jones, ICFP 2007). the linear analysis currently discovers in-place opportunities; FBIP's discipline turns "discovered" into "stated and checked," and SpecConstr automates the accumulator-shape specialization the enumerable's typed fold arms do by hand.

7. constants that stop being recomputed shipped — constant applicative forms (peyton jones, implementing functional languages; ghc evaluates a caf at most once). a zero-argument definition is a constant, and kanso used to rebuild it per call: the json decoder's bytes_false = [102 97 108 115 101] compiled to a function that heap-allocated a five-element list every time a false was parsed. now the body emits under a build symbol and the definition becomes a load from a cell filled once, before main, into permanent storage — the only cache an arena rewind cannot invalidate. the decode gauntlet drops 1,874,992 allocations and 95 mb of allocation bytes, and one arena block; per-decode cpu falls 6–14% depending on how quiet the machine is.

the first version cached lazily, checking a ready flag on each call, and measured slower — a store on that path is an alias-analysis barrier and it sat inside the hottest dispatcher on the board. filling every constant before main leaves the read a bare load, which is where the win came from. the lesson generalizes past this one technique: a memo check in a hot path can cost more than the work it skips.

8. the fold-state shelf shipped — original to kanso, and until it landed the largest unclaimed number on this page. the beat analysis rewinds the arena between iterations when it can prove the iteration keeps nothing across the line, and an accumulator used to break that proof by construction: encode_items acc xs i hands (elem_onto acc xs[i]) onward, an expression, and an expression's result is assumed to be new. the analysis declined, and then nothing rewound — including everything the iteration allocated that was not the accumulator.

the license that fixed it is identity, never type. a bytes accumulator may cross a rewind when it is the very object that arrived at the loop's entry, threaded through appends the linearity analysis proved in-place — pointer identity, established by a greatest fixpoint over groups whose every arm returns a chain of its first parameter, through conditionals, guards, local bindings, folds whose folder chains its own accumulator, and calls to other chaining groups. the header is then below the mark; growth allocates outside the arena and a mut-grow frees its predecessor, so the payload is never above the mark either; and raw bytes hold no pointers, so nothing inside the accumulator can dangle. a fresh builder of the same type has none of those properties, and a golden pins the refusal: the fresh-builder loop reads zero rewinds, and under a deliberately type-only license it reads forty with output still correct by memory-layout luck — which is why the counter is the pin and the output cannot be.

the license also reads around a cycle. mutual recursion — a loop that steps through a helper group and returns — forms a tail cluster rather than a self-tail, and a bytes slot in one crosses when every inner edge feeds it a mut-append chain rooted at one of the caller's own chain-threaded slots: the same fixpoint, one license over. a chain-threaded slot is never carried, so a bytes-accumulator cycle becomes a plain rewinding ring, and the loop-plus-helper shape a width-conscious program naturally writes needs no rewriting to earn its rewinds.

sorted views made the same move for the same reason. the first licensed run took 362 ms against a 5 ms baseline, because every rewind freed every map's cached sorted view and each iteration rebuilt them. views are malloc-backed now: nothing arena-backed can dangle, the cache registry never fills, and the sweep each rewind used to pay walks an empty table. a transient map's view leaks with its map — the recorded trade.

what it bought, measured the day it landed: encodebench's arena blocks fell from 905 to 5 across five million rewinds — the encoder runs in constant arena space. kq pretty-printing the 1.9 mb document holds 47.5 mb against jq's 30.7, down from 211.9 before this page's uniqueness campaign began, and at 5.25 instructions per cycle it out-executes jq's 5.20 — the stall the old working set cost is gone, and the footprint bought speed on its way down: 29.5 ms against jq's 101.4, with the buffer-reuse shelf and one-build literals carrying the last leg. on the plain path query the footprints sit at parity or below jq's on both documents. the pretty-print gap closed for good when the printer learned to stream (2026-07-27): each top-level element is rendered, written, and dead before the next is built, so the output never exists as one string — 30.0 mb against jq's 30.8 on the same document, and kq now holds less memory than jq on every scoreboard row.

the carry path was measured and is not the mechanism. a probe that lifted the bytes exclusion from the carry gate turned all three encode loops into carry beats with byte-identical output and a footprint four hundred and ninety times worse — the copier hands a carried builder back at exact capacity, so every carried iteration is a copy followed by a doubling, the quadratic the exclusion exists to avoid. a threaded builder must be invisible to the carry, which is what pointer identity provides: never staged, never copied at a boundary.

8. appending without a new header shipped — the largest single source of garbage on the encode board, and it was not the bytes. a byte builder's fast path writes into its own spare capacity and then allocated a fresh twenty-four-byte header to carry the new length: 42,312,800 of them, 62% of every allocation encoding made. lists already avoided the equivalent — the linearity analysis proves a push is uniquely owned and codegen emits push_mut, which extends in place. append_mut is the same mechanism for appends, and getting it to select a site took four separate blockers off the path.

the wrapper was the first. all forty-two million appends happen inside the standard library's one-line text/append, so every call site showed the analysis a user call rather than an append, and inside the wrapper the accumulator is a parameter owned by a caller one frame away. inlining a wrapper whose whole body is one builtin call passing its own parameters in order undoes the rename before anything looks. then the byte builder had to count as a fresh value at the root of the chain, a conditional's arms had to count as one use rather than two, and a fold had to read as the accumulator loop it is — the encode path threads its builder through list/fold, and the folding lambda's parameter is unique only inside a lambda whose accumulator arrives first and appears nowhere else.

encode allocations fell from 68,640,508 to 26,327,708, bytes from 2.29 gb to 934 mb. kq's resident set went from 211.9 mb to 139.9 on the same document — the first third of a fall the escape fix, the fold-state shelf and the reuse shelf carried on to 47.5, and the streaming print finished at 30.0.

writing the adversarial spec for it turned up a divergence that had nothing to do with folds. importing a qualified name enrolls a bare-named clone so both spellings dispatch, and the clone carries the original's span; nobody calls the clone by its bare name, so every call-site test over it passes by default and it keeps a linear parameter the real declaration was refused. in-place sites are keyed by source position, which the twins share. an imported builder held by two owners was written through one of them — AB AC from the interpreter, ABC ABC from native. clones are now invisible to the analysis, which is what their declaration already claimed about provenance.

9. making the rewind's cache sweep cheap two attempts, both reverted — before every rewind the runtime walks a registry of maps holding cached sorted views, asking of each whether it survives the mark. with the byte shelf enabled and loops rewinding twenty thousand times, that walk was 1,596 samples out of about 1,600 — effectively the whole program. two fixes were tried and neither survived contact.

the first scoped the walk to entries registered since the mark, on the reasoning that older ones sit below it. the reasoning was circular: an old entry is only re-registered if it was reset, and it is only reset if it was walked, so skipping it leaves a stale pointer forever. the golden corpus caught it immediately. the second deduplicated the registry with a flag on the map, which worked and bounded the registry by distinct maps rather than by rebuilds — but the extra word grew every map, and the decode board went from four arena blocks to five for no demonstrated win. reverted on the ratchet's own terms: a regression without evidence of benefit is just a regression.

a counter settled it a fortnight later, and the answer was that neither attempt was aimed at the cost. a decode never asks a map for a sorted view at all: on the 188 kb board the runtime attempts 1,254,150 view insertions with a view behind none of them, and 632,550 beat pops carry nothing. both functions were being entered to answer a single load and a branch, which divides out to eighteen instructions per write and twenty-six per pop — the frame, not the work. moving each test to its caller drops decode 0.41% and encode 0.59%, for 1,024 bytes of machine code per binary.

10. retaining the region instead of copying the carry measured, declined — the cohort pop already refuses a copy that buys too little. it sizes the survivor before evacuating and keeps the region when that survivor is worth more than half of what grew. the loop's carry sizes the same walk to allocate its buffer and then copies regardless, so handing it the cohort's ratio is four lines.

a probe said first where the copies are. on the one-shot board, 63,967 evacuations split as one cohort pop doing about 31,986 and two loop carries doing 31,981 — and the cohort's own kept-counter reads zero, so that guard ran, priced the trade and chose to copy. with the ratio ported, decode saves three copies and 112 bytes, encode saves nothing and its arena peak goes from 4 mb to 5, and one-shot does not move at all, because both its carries hold a survivor worth less than half their region. the argument that retention is self-limiting — keep the garbage, the region grows, the rewind comes back — does not survive encode, where 352 retentions in a row grew the arena by a whole block instead of converging. the cohort tests a size floor before its ratio; adding that floor gives numbers byte-identical to baseline. the guard is a loss without it and nothing with it.

the two sites ask one question about different distributions. the cohort decides once, at a pop, where the region is large and the survivor is the whole result. the carry decides every iteration, where the region is one iteration's garbage and the survivor is the accumulator. so those 63,967 copies are not waste any policy here would remove, and avoiding them without keeping the garbage takes reference counting.

8. maximal sharing measured, declined — hash-consing, the ATerm lineage. the premise was that real-world json is deeply repetitive, so counting the repetition came before building anything. on the 188 kb board: 5,513 composite subtrees, 5,210 of them distinct. only 65 appear more than once, and sharing every one of them would avoid 2,831 bytes — 1.5% of the file. the three biggest repeats are [true], [null] and [false], six bytes each. paying a hash of every subtree during decode to save that is a straight loss.

the repetition is real, and it is all in object keys: 8,361 occurrences of 500 distinct names, 94% redundant, 38,537 duplicate bytes or 20.4% of the file. string values repeat 0% — every one of the 2,114 is distinct. so the shape that would pay here is key interning, not subtree sharing, and its downstream prize is pointer-compare map lookups, which the profile bounds at about 3%. recorded so the idea is not re-mined in the general form; a workload with genuinely repeated subtrees would reopen it.

9. the cheap experiments queued — post-link layout optimization (BOLT: panchenko et al., CGO 2019) on the emitted binary, and superoptimization (souper; STOKE, schkufza, ASPLOS 2013) of the ten hottest runtime kernels. bounded, verifiable, measured in an afternoon each — though the BOLT half waits on a linux box: it is ELF-only, and the development machine's toolchain ships no llvm-bolt and no perf to feed it.

profile-guided optimization was the first of these tried, and it is measured, declined. instrumenting the decode gauntlet, replaying it, and rebuilding against the profile moves per-decode cpu about one percent — floor 1.1%, first quartile 1.4%, median 0.6% over sixty interleaved runs, which is the size of the noise on a loaded box. the decoder's dispatch is a jump table and its inner loops are already vectorized, so there is little branch-prediction headroom for a profile to recover. the cost is a two-pass build and a checked-in profile that goes stale, against a compile story that currently fits in one pass.

examined and declined, so the queue stays honest: persistent tree collections (RRB vectors, HAMTs) — the flat-array-plus-arena model beats their constants at this language's working-set sizes, and purity plus static reuse already dissolves the sharing problem they exist to solve; and gpu/polyhedral work — the wrong workload class for a parser-and-tools language.

10. carrying the sorted view across a shared put built, measured, declined — the declination above has one measured edge. static reuse dissolves the sharing problem where the analysis can prove a map is uniquely owned, and there the read-write loop is now linear: ten thousand distinct keys cost 36 ms and 4.6 mb. where it cannot — a loop that reads the old map after writing the new one — every put copies the whole pairs array, and four thousand keys cost 368 ms and 351 mb. that is the quadratic a persistent tree exists to remove, and it is the one shape where this model loses outright.

the cheap half of the fix was built: the copying put already pays an O(n) copy, so it can carry the sorted view across rather than dropping it and leaving the next read to sort one. measured back to back on four thousand shared keys, 370 ms against 368 ms — the sort it saves costs about what the extra copy adds. the bottleneck is the pairs copy, which no view trick reaches. reverted rather than shipped, because code that buys nothing still has to be read. a real fix is a different map representation, which is a memory-model question rather than an optimization, and it is not queued as one.

11. the collection surface stopped copying shipped — a method rather than a technique from a paper. the welfare index used to read three benchmark programs and a compile, which is a narrow shelf: a string build that got twenty times faster and a sort that shed two hundred times its allocations both left the number exactly where it was. what a model leaves out it weights at zero. so the index gained a basket — string accumulation, map read-write over repeating and growing key sets, list build and index read, lazy map/select/fold, group_by and tally, arithmetic, records, join, slice and sort — and then the basket was asked, repeatedly, where its allocations were.

it answered ten times, each answer only visible once the one before it was fixed. the sort was an insertion sort rebuilding its run per element: four thousand numbers cost sixty-four million allocations and peaked at 4.4 gb. merging halves took that to 327,296, and passing index ranges rather than materialising the halves took it to 20,014. drop walked every element it discarded, so reaching the last ten of four thousand allocated 8,030 times; skipping over a cursor is arithmetic, and it became 51. a fold whose reducer pushed minted a list header per element because the mark that would have let it write in place was keyed by source position and a lifted lambda carried no file — 4,015 allocations became 15. a tally copied its whole map every element, because the check refused an accumulator that appeared in a sibling argument, when a builtin forces its arguments and the read is finished before the write: 12,024 allocations and 128 kb of map headers became 25 and nothing. reading a key then writing it built the key twice, and evaluating a repeated pure interpolation once halved it. a record update allocated a fresh record where the old one was finished. a string built by joining onto itself copied itself every append, at 162 ms for a hundred thousand of them, and now writes into one buffer at 10.

the basket went from 91,864 allocations to 20,107, and from 131 mb resident to 3.5. every item left in it is about one allocation per element, which is the data being produced rather than the machinery producing it.

two things are worth keeping from how it went. the wins came from asking a local question rather than widening the shared analysis: three attempts that changed the linearity fixpoint shipped corrupted programs, and the three that asked "does every caller hand this over, and is every mention inside this one expression" did not. and the string builder, which took four attempts across the day, needed the smallest change of all of them — the seed converted where it enters the loop, so its header predates every mark and survives without anybody widening a licence to let it.

the guards are fixtures rather than arguments. two of the corruptions lowered the allocation count, so a counting pin called them improvements; what catches them asserts bytes. reuse_guard and builder_guard read a value after the write that would have clobbered it and print the same bytes on a compiler that optimises and one that does not, because in each of those cases the analysis declines.

the faster-than-rust thesis, stated honestly: not "beat rust at a microbenchmark loop"—llvm emits the same instructions for both. the winnable claim is beating idiomatic rust on allocation-heavy real workloads, because the arenas eliminate the defensive clones the borrow checker pushes people into, fusion is unconditional under purity, speculation specializes what monomorphization can't see, and the cost model schedules parallelism the user never wrote. getting that performance out of rust asks a great deal of the programmer; the aim for kanso is to ask it of the compiler instead.

12. skipping the getters a program never reads built, measured, declined — every field of every type declares an accessor arm so the name resolves wherever it is read, and a later pass deletes the ones nothing mentions. compiling the json module builds 134 of them and deletes all 134, which reads like free work to skip: ask which accessors the program mentions, and build only those.

it is slower. jsonbench goes 8.79 to 9.24 ms and the basket 6.57 to 6.92, held across three interleaved before-and-after passes, best of nine runs of twenty compiles each. the reason is the shape of what was traded. an unbuilt arm saves a struct and a vector push; deciding not to build it costs a walk of every expression in the program, and across a module that walk runs once per file over the union of all of them. the deletion pass already walks the program for the same reason, so the second walk buys nothing the first had not already paid for.

there is a version of this that could pay: compute the mention set once and let both synthesis and deletion read it. that needs the set to survive inlining and fusion, which move calls around between the two points, and the saving on the other side is 134 vector pushes. recorded here so the arithmetic does not have to be redone.

the attempt found a real defect on the way, which is the usual reason to build one. suppressing an arm per file broke a getter of a sibling module's type used as a value — list/map ps _.x, where one file declares the type and another reads the field — because a file asked about itself alone cannot see its sibling's read. the module differential caught it and the union across files fixed it, but nothing in cargo test would have.

07the lazy betv1 shipped

the newest ruling is the largest since the arenas: kanso is lazy. write foo = expensive_calculation and nothing runs; the computation is noted, and it happens when—if—the value is actually used. a value behind a conditional that turns out false is never computed at all. the motive is the one that runs through this whole page: don't pay for work that never reaches the answer.

the bookmark

the mechanism is a thunk: a small cell holding what to compute and the ingredients to compute it with—a bookmark into a computation the program may never open. forcing the thunk runs the computation once, writes the result into the cell, and drops the ingredients; every later reader finds the answer already there. that one cell buys three things. work that might not be needed costs nothing until it is. a shared computation runs once no matter how many readers demand it—okasaki's persistent data structures rest on exactly this, laziness plus memoization making amortized bounds survive sharing. and an infinite structure is ordinary data, because only the consumed part ever exists.

what the compiler proves, and what stays lazy

the compiler sorts every value by what it can prove about demand. provably never used: deleted, as dead code. provably used, immediately, and cheap: compiled strict—the exact code this page has been describing—because a thunk forced nanoseconds after it's built pays its cost and dodges nothing. everything else keeps its thunk, and that residue is where laziness earns its keep. one refinement came late and reshaped the runtime's plans: proven demand is a fact about whether, and says nothing about when. a value certainly needed but not for a while is guaranteed-useful work the scheduler may run early. that idea gets its own subsection below.

the two leaks, named

laziness has a reputation, and it's earned: haskell programmers debug space leaks. the failure has two shapes. chains: a loop that only ever promises additions builds a million bookmark cells before anything forces the first—linear memory where constant was wanted. retention: a bookmark's ingredients can include a large structure, which stays pinned until the bookmark is forced. the same rule bounds both—force what is provably demanded, eagerly—and neither is taken on faith: the prototype below measures the chain at its worst and watches the pinned structure release at the moment of forcing.

why the workbench can't hold a thunk

a rewinding arena is a region: everything in it dies together when the beat ends. a thunk breaks that bargain, because when a thunk runs is a runtime fact—a pending computation parked on the workbench could be forced long after its beat, and the sweep would tear the ingredients out from under it. this is a known dead end, with receipts. jhc, the one haskell compiler that made regions its whole memory strategy, documents its own programs as leaking. ghc's compact regions refuse to hold a thunk at all. even the ml kit, doing region inference in a strict language, had to bolt a garbage collector back on (hallenberg, elsman, tofte, PLDI 2002). regions and laziness don't compose, so kanso doesn't ask them to. the workbench keeps the strict data it already serves; thunks get a tier of their own.

counting without an asterisk

the thunk tier is reference counted: a cell whose frame can prove no reference survives it is freed at that frame's boundary and recycled through the free list shipped. the proof is a static classification — every use of the binding must target a callee position whose arms only force the value, ignore it, or return it bare — plus one runtime pointer compare for the returned-thunk case. cells the proof can't cover (returned upward, handed onward in a tail call, or reaching a position that could store them) stay live and are counted: the .mem goldens pin allocs, frees, and escapes per program, so reclamation is a diffed fact rather than a belief. the escape cases' full story belongs to the defunctionalized-thunk frontier, where ownership can ride the calling convention. counting normally comes with an asterisk—a cycle keeps its own count above zero forever, which is why python ships a cycle collector on top of its counts and why trial-deletion collectors exist (bacon & rajan, ECOOP 2001). kanso deletes the asterisk structurally. values are immutable, so ordinary data can't point at itself. the one construct that could—the knot-tied stream, ones = 1:ones, a cell whose tail is the cell itself—is banned in favor of the generator form, which allocates a fresh cell per step and never loops back. the prototype builds both: the knot leaks exactly one cell, on cue, and the generator version of the same infinite stream runs in two. the graph stays acyclic, so counting is complete. no tracing collector, no cycle detector, no pause.

the card comes back off the bench

readers of §02 will recognize this tier. perceus—counting with reuse—was demoted for kanso's strict data because the arena reclaims the same memory with one pointer reset. but strict data was never its best fit. thunk cells are small, uniform, and constantly churning, the exact population a free list serves: a cell whose count hits zero is handed to the next allocation, and the allocator stops hearing about it. the closest published work is first-order laziness (lorenzen, leijen, swierstra, lindley, ICFP 2025, distinguished paper), which grafts perceus-style counting and reuse onto a lazy fragment—with every lazy shape declared up front, and with open, library-extensible thunk shapes named in print as the unsolved problem. that problem is a fact about separate compilation. kanso compiles whole programs—no separate compilation, no runtime loading—so every thunk shape in a program is enumerable at compile time, and the compiler defunctionalizes all of them itself (reynolds' old trick, applied totally). the fragment the literature stops at is, in a closed world, the whole language.

the prototype's receipts

the cell design ran as an instrumented prototype before any engine work, every allocation and free counted. across every workload: 21.1 million cells allocated, exactly one live at exit—the deliberately leaked knot. on the shipped engine that workload—100k items, 5% used—runs 18x faster than a strict build of the same program, and within 38% of the rust programmer who restructured the code by hand. a thunk costs about 23 ns to build and force—the price the strictness analysis erases wherever demand is provable. and with the free list and defunctionalized cells, a 10-million-element infinite stream runs at 10.9 ns per element with two calls to the allocator, total: the first cell's memory becomes the third's, the third's becomes the fifth's, forever.

proven work on idle cores

a pool of pending thunks is a work queue. when a program blocks on io, the core it was using goes idle—and the scheduler can spend that idle time forcing thunks whose demand is already proven. the work is certainly needed; only its timing moves. an out-of-order cpu does the same with its instruction window during a memory stall, and this is that move, lifted to the language. the honest ancestors are optimistic evaluation (ennals & peyton jones, ICFP 2003) and eager haskell (maessen, 2002): both showed real speedups from running lazy code early, and both stayed research prototypes, because in an impure language a wrong guess needs rollback machinery and the rollback tax ate the winnings. kanso's functions are pure and effects are inert values, so forcing a thunk early cannot do anything that needs undoing—a mis-timed forcing produces a value nobody reads yet, and an err discovered early is stored in the cell, surfacing only if the value is ever demanded. purity removes the problem those systems died on, and the deterministic scheduler keeps early forcing invisible: every engine produces the same bytes whatever got computed during the stalls.

what's real today

and the lazy realm has a scoreboard of its own now. the workload: 100,000 items, each carrying a real computation (five thousand mixing steps), one in twenty actually used, every row producing the identical checksum. shorter is faster:

programsecondswhy
rust, hand-tuned 0.08the human moved the work
kanso, as written 0.12the compiler moved it
rust, as written 1.73computes what you wrote
kanso --strict 2.15the measured worst case

read the rows top to bottom and the design argument reads itself. the tuned rust row is a human who restructured the computation into the branch; the kanso row is the same program in its clear form, within a whisker of that, because the demand analysis thunked the binding and the branch discarded nineteen of every twenty—the counters on the run say it plainly: thunk_allocs=100000, thunk_evals=5000, 95% of the work never born. rust-as-written pays for everything it wrote. and the bottom row is kanso's own --strict flag—the worst-case measurement mode—which forces every thunk and reports what this program would cost if laziness never saved a cycle: the bound you'd quote in a latency budget. the sources are in bench/; the checksums match on every row.

the honesty tier, in the house convention: v1 shipped in both engines; the pervasive form staged behind the same experimental gate. the fragment above—conditional-demand bindings, the cost gate, refcounted cells on a free list, forcing at scrutiny—runs in the native compiler and the interpreter today, held byte-identical by a new vein of golden tests. alongside every program's expected output, a .mem file pins its memory facts, and both engines must match it byte for byte, because evaluation counts are semantics, not implementation detail. the shipped receipts: the skipped computation evaluates zero times with unchanged output; the shared thunk evaluates once under two readers; the demanded thunk forces once and exits with zero cells live; and a skipped binding whose computation would have produced an err simply never births it—same output, because errs are values and effects are inert, so the classic lazy-versus-strict divergence has nothing to bite. the json gauntlet is untouched: its accumulators hit the cost gate and compile strict, and the §08 cost golden pins that at zero thunks and the same allocation count the ratchet holds. leak-freedom stops being a property we believe and becomes a constant the build diffs.

two dials came out of the same conversation, both awaiting the surface-syntax rulings: a strict mode (force everything, measure the worst case—a measurement tool, since forcing runs what laziness would skip) and a sync-style block (a scope guaranteed thunk-free, so peak memory inside equals strict memory). one gate in the compiler implements both.

the work-ahead engine design

the newest ruling on this frontier is scheduling, and it needs one picture. a fiber that blocks on io leaves a hole in the timeline. demand-driven evaluation cannot fill it — a blocked fiber demands nothing — so the hole is dead time, and the work it was saving up runs after the wait instead of during it. the strictness analyzer already knows which computations are certain to run. so the scheduler keeps a pool of them, and when a fiber parks, it works ahead:

demand-driven compute io stall — idle proven work, run late done work-ahead compute proven work, in the stall done the recovered time

what fills the hole is chosen by three rules, checked in order. that is the entire policy:

the work-ahead ladder
a fiber parks on io. the scheduler asks, in order:

  1  proven work, inputs ready?      -> run it. certain to be needed —
     |                                  early is free. program order.
     no
     v
  2  a gate of proven work?          -> run it. one branch decides its
     |                                  fate, and finishing it refills
     no                                 rung 1.
     v
  3  a free gamble, priced cheap?    -> maybe. only here is anything
     |                                  speculative, and only when the
     no                                 rungs above are empty.
     v
     sleep until the io returns      -> the driver actually rests.

determinism survives because the ladder keys off logical scheduler state, never the wall clock: same program, same seed, same work-ahead transcript, on every machine. purity is what makes the whole thing legal — evaluating early is invisible, because nothing can observe the order of effect-free work.

wall-credit: the substrate, shipped shipped

the engine's foundation is already live in both engines, and it has receipts. the deterministic scheduler used to track logical time only, so real time a fiber spent computing never counted against another fiber's pending sleep — a thread could grind for a second beside a sleeper and the sleeper still slept its full span. the scheduler now credits elapsed wall time against every deadline before it waits. the transcript and every counter stay purely logical, so replay and the goldens are untouched; only the physical wait shrinks.

the fedex rig — a 2000 ms stall beside ~900 ms of pure work (bench/workahead/)
# as-written    sleep >> report        min 2913 ms   (stall, then work)
# overlapped    report beside sleep    min 2008 ms   (work inside the stall)

# the sleep is 2000 ms. overlapped lands at 2008: the entire grind
# disappeared into the wait. min of 15 interleaved heats; the floor is
# physics — a run can never beat its own sleep, and none did.

cargo build --release
./target/release/kanso build bench/workahead/aswritten.kso --release
./target/release/kanso build bench/workahead/overlapped.kso --release
time ./aswritten; time ./overlapped
KANSO_SCHED_DEBUG=1 ./overlapped     # watch elapsed + wait = deadline

today the overlap is written by hand — the two variants above are the same program arranged two ways. the engine's remaining job is exactly the gap between those rows: find the arrangement automatically, using the two-group heuristic, so as-written code gets the overlapped number. that closes the loop this section opened.

techniquesthe ledger

every named technique in the compiler, one line each: the idea, its source, and what kanso gets from it. queue items graduate here as they ship; the list only grows.

08the receipts

none of the numbers on this page ask to be believed. here is the scoreboard and the recipe. every engine reads the file at runtime—an embedded constant would let llvm fold the decode away—and the kanso harness accumulates a checksum so the loop provably runs.

the scoreboard—per-decode floors from one interleaved sitting, and how to reproduce it
# kanso, shipped default    ~0.87 ms   4.2 mb rss, flat  (arenas, §03)
# serde_json (hand-tuned)   ~0.90 ms   6.8 mb rss
# reasonably-written rust   ~1.04 ms   6.9 mb rss
# go encoding/json          ~2.05 ms  11.8 mb rss

cargo build --release
./target/release/kanso run bench/make_jsonbench
./target/release/kanso build bench/jsonbench --release
time ./jsonbench                                  # less ~3 ms startup, over 150
(cd bench/serde_bench && cargo build --release)
./bench/serde_bench/target/release/serde_bench bench/large.json
(cd bench/naive_json && cargo build --release)
./bench/naive_json/target/release/naive_json bench/large.json

the ratchet

a benchmark that drifts from one machine or one afternoon to the next isn't evidence of much. so the wins are pinned to counts that can't drift, not to wall-clock time. the 150-decode gauntlet is a deterministic program, so it performs the exact same number of allocations every run, on every machine, on both architectures. ci commits those counts to a golden file and diffs against it: 6,272,114 allocations, 3 arena blocks, 151 beat iterations—bit-identical on arm64 and x86_64. that arena count across 150 decodes is the flat-memory guarantee written as a constant; 151 beat iterations is one rewind per decode plus one for the program itself, on the record. a change that nudges any of those numbers fails the build before anyone has to squint at a flaky stopwatch, which is how the three ladder rungs stay put and how the next regression announces itself as a failing diff in the pull request that caused it.

how fast it compiles

the front end is quick enough to sit inside an editor's save loop. kanso check—parse, whole-program inference, and every diagnostic—finishes kq in 6.6 ms and the json decoder in 6.1 ms. each figure covers the standard-library modules the program imports as well as its own source, about a thousand lines of kanso in kq's case, so the front end clears a hundred and fifty thousand lines a second. an unoptimized binary takes 116 ms end to end; a fully optimized one takes 635 ms, and llvm at -O2 is nearly all of that. for scale, go on the same box builds a 28-line program against its already-cached standard library in 98 ms. (2026-07-25, loaded desktop, best of seven.)

the clock is the softer of the two measures. bench/compile_golden.txt pins the work itself—fixpoint rounds and expression visits per sample, beside the lines, calls and branches the emitter wrote—so a change that grinds a longer fixpoint to emit the same text stays visible even when machine noise hides it from the wall clock.

09the surface a program needsshipped

a fast compiler with nothing to compile against is a demo. four pieces landed in one sitting, and each of them was a thing a real tool could not do without.

reads are applications. clay.name is a getter arm applied to a record, and _.name is that getter as a value — so list/map people _.name works, and one accessor serves every type that declares the field, because it is an ordinary dispatch arm. the expectation going in was that this would cost something. it did the opposite: field access was never a static offset. the runtime walked the record comparing field names, so binding the field by position instead is less work. twenty million reads went from 0.08s to 0.03s, interleaved, with the compile golden unmoved.

the getter carries a name no program can spell, which is what keeps a field from taking a name away from a type or a local. Haskell reached the same place from the other side — its selectors were monomorphic names, and twenty-five years of escaping that produced NoFieldSelectors and a field lookup that lives in the type system. kanso's dispatch was already there.

an os surface. before this the whole vocabulary was 39 builtins, lib/io exported five things, and none of them was stderr — so a tool's diagnostics went into whatever it was piped into. now there is stderr, an environment read where an unset variable is none rather than a failure, file existence, a directory listing, and a clock. lib/path came for free in pure kanso, since text/slice is enough for basename and dirname.

two of those decisions are about determinism, which is a running theme rather than a coincidence. a directory hands its names back in whatever order it likes, so a listing is sorted — otherwise a program's output depends on the disk. and a run that timestamps is unrepeatable, so KANSO_NOW pins the clock exactly as KANSO_SEED pins the dice. determinism also decides where a behaviour is tested: an unset variable reads the same on every machine and lives in the corpus, while a set one is asserted with the environment controlled.

starting a process. io/run cmd args answers a record of status, stdout and stderr. a non-zero status is what the process said and a caller reads it; only a process that never starts raises an err, which crosses a package boundary like any other std failure and so can be named in an arm. a browser tab cannot start anything, so the playground declines by name rather than pretending.

packages. imports are the manifest — there is no second file restating them. kanso install reads them, resolves each hako's highest release tag, fetches it into a content-addressed cache and writes hako.lock; kanso list and kanso update are the other two verbs, and there is no third. a dependency you need before its author has tagged it is kanso install --from owner/repo@branch, which writes the branch and its sha into the lock; list says the pin is interim and update walks releases past it, so the pin stays visible until a tag replaces it. the compiler never reaches the network: it reads the lock and the cache, so a build works on a train.

the resolver is where the design earns its keep. each import shape answers in exactly one way and never falls through to another, which is why a local directory called owner/repo cannot stand in for the hako of that name. the lock records a protocol beside the tag and sha, and the protocol is a protocol rather than a host: GitHub and GitLab both speak git, tag discovery and ref fetch are git standards, and the one thing a GitHub-specific fetcher adds — a tarball at a ref — trades a commit sha for a promise of byte-stability that broke on 30 January 2023 when a compression change altered archive bytes for identical files.

hako reads its own lock, in kanso. kanso list now exists twice: in Rust, which owns the CLI, and as a kanso module in the repo that reads the lock, reports what it pins and asks each remote through io/run whether the pin has fallen behind. a pin at a release is measured against the highest release its remote publishes; a remote that cannot be reached says so rather than guessing, and an interim pin sits off the tag series staleness is measured along, so the only thing worth saying about it is that it is interim. it is the first program kanso has that is neither a benchmark nor a book sample, and what it exercises is the whole of the surface above: a file read, a process started, text split and sliced, a list folded into one write.

a listing is one effect per pin, and that is where the port found the gap. a description binds a lambda, but a plain value re-enters the chain only by writing nothing to stdout — io/write "" . (_ -> x). every program that runs one effect per element of a list needs that function and io has no name for it, which is the next thing to settle.

arity is checked across a module, and carried by a function value. a module is a directory of files sharing one namespace, so the check that a call brings the number of arguments some arm can answer runs across the whole of it. a call to a sibling file used to reach the emitter unchecked and arrive as an undefined symbol from the assembler. bare names count, because no binding may shadow a declaration — adder = … beside fn adder is a name error — so a bare name matching a declared group is that group.

what the checker cannot know is a constant holding a function, whose arity is decided at runtime, so a function value now carries how many arguments it takes and the three engines agree on what to say when a call brings a different number. before that a one-argument closure called with two ran anyway: the extra argument sat in a register the callee never read.

a package can test its own failures. the two-universe rule forbids turning your own err into a value, and a library that cannot assert that its errors happen cannot be trusted. the way out needed no exemption: the harness is a foreign party, so a builtin doing the reading is a stranger rescuing. the rule's own mechanism settles it, because a rescue is attributed to the arm whose pattern names the err, and a builtin has no pattern to attribute. failed? is in scope in a _test.kso file and nowhere else, so the rule stands undiminished outside the harness.

the entry has no name a program can spell. a directory is run through the file main.kso, and a single file through its pub play. that is the whole of the mechanism, and a program may use the word main for whatever it likes. the compiler used to synthesise its entry under that name, in the reader's namespace, so a file writing main = … beside a pub play ran the binding and never ran play. the entry now carries a capitalised name on the same grounds as a getter's Get_: source identifiers are lowercase, so nothing a reader writes reaches it. an err arriving at the top says it reached the entry.

the gates are moving to kanso. the checks that guard this repo — the differential sweeps, the welfare index, the drift budget between the log and this page — were written in python, and the first of them now runs as a kanso program in CI. it is the cheapest real workload the project has. seventy lines written against the standard library turned up a text/slice that answered differently on two engines one position past the end of a string, which eight differential sweeps had missed: they probed far past the end, and an off-by-one only ever lives exactly one past.

a probe that never returns is the loudest finding a sweep has. two silences compare equal, so a differential sweep that only asks whether the engines agree will read a pair of hangs as agreement. every probe runs under a timeout, and a probe that outlives it is reported by name and fails the sweep. the sweeps that moved from python to kanso lost that property in the move and got it back — which is the more general lesson: a port deletes the thing it replaces in the same commit, so nothing compares the two, and a property can disappear without anything going red.

a segfault is not a diagnosis. a native program killed by the operating system reports that it ran out of stack, and deep recursion is only one of the things that ends a program that way. one reduced program spent an hour looking like runaway recursion and turned out to be a null pointer handed to text/join — a string carrying a length and no data, valid where it was built and empty where it was read. the sentence the compiler prints there names a cause it has not established, and the three engines were deliberately aligned on it, so correcting the wording is a change to all three or to none.

the gates that guard this repo are becoming kanso programs, and that is where the compiler's bugs are turning up. five of them have moved so far, and the move is worth more than the deletion. writing a real program against the standard library found a slice that disagreed between engines one position past the end of a string; a walk that accumulates while it performs effects that dies on the engine that ships and answers correctly on the one that reads; and a binding whose value becomes a thunk, in a function whose arguments can fail, that emits invalid machine code because every return releases every cell whether or not the cell exists on that path. none of those came from a corpus. they came from someone trying to write an ordinary program.

the trend gate is the first of them that is a program rather than a table of probes, and it wanted things a sweep does not: counters summed across lines with no map to fold into, so pairs are grouped by name and each group summed; names taken from the pairs rather than the map, because a map answers with its values and not its keys; strings joined by interpolation, because concatenation is for lists. it produces the same bytes as the python it replaces, on the path where a counter improved and on the path where a pure regression fails.

the native build refuses what it cannot represent, and there was exactly one place it did not. integers are arbitrary-precision in the spec and sixty-four bits in the machine, so the honest thing for a native program to do above that line is stop. addition, subtraction and multiplication all do. division did not: the least integer divided by minus one is one past the greatest, and the machine answered by wrapping — the same number back, with a successful exit. it raises the overflow now, like the others.

it was hiding inside a number. the sweep that compares arithmetic across the two engines reported four hundred and seventeen disagreements, which is a figure nobody reads twice. classified by what the compiler actually said, they are three hundred and ninety-six literals too wide to compile, one hundred and sixty-four overflows caught at runtime, and one wrong answer. a report large enough to skim is a place for a real finding to sit unread, and the fix for that is to say which kind each one is.

and the test that caught it made the suite lie. the numeric tests each get a work directory keyed by the program, except the key was the program's first twelve digits — and 9223372036854775807 * 2 opens with the same twelve as the division. two tests shared a directory, deleted each other's entry mid-run, and one reported the other's answer. the comment above that key already promised one directory per program; the isolation was described correctly and implemented on a prefix, and the suite was one test away from two of its members quietly trading results.

10a program written as a programshipped

kanso had one string form, and every gate in this repository is a program that writes other programs. a differential sweep holds a hundred small kanso programs and hands each one to three engines, so a hundred programs sat in the source as single lines with their newlines spelled \n and their splices spelled \{. that is unreadable in the file and unpasteable out of it, and it is where the last of the python was hiding: the scripts that had not moved were the ones whose payload was a program.

the form is swift's. """ as the last token of a statement's line opens a text block; content sits at the opening line's indent plus two; a """ alone at the opening indent closes it. every content line contributes its text and a newline, the last one included, and interpolation works exactly as it does in a quoted string. content lines are exempt from the eighty-column cap, because the width rule exists to keep a statement readable and a content line is not a statement.

two other shapes were considered and declined. ruby's <<~NAME carries a name that means nothing — the reader learns a label, uses it twice, and it says no more about the text than the quotes do. a per-line sigil, the | of yaml and swift's own multi-line comments in some styles, defends against a runaway fence by ending the block at the first unmarked line; kanso's indentation is already rigid enough that a runaway fence cannot get far, and the sigil costs paste fidelity, which is the whole point of the construct.

eight things are refused with a message each, and they are the ways a block goes wrong rather than a list of tastes: anything after the opening fence, a content line shallower than the content column, end of file before the fence, anything after the closing fence, \n inside a block where a real line break says it, \" inside a block, an interpolation that does not close on its own line, and a block of fewer than two content lines. the last one is the interesting one — a one-line block is a quoted string with more ceremony, and allowing it would make the choice between the two a matter of mood.

ninety-seven call sites moved across twenty files. the three heaviest are the differential gates, whose output is pinned byte for byte, and the migration's rule was that those goldens stay untouched: a regenerated golden during a rewrite of the thing that produces it is indistinguishable from a bug, so the only acceptable evidence that the rewrite preserved meaning is that nothing downstream moved.

11a header that outlives its storage

the strict tier rewinds an arena at every step of a bind chain, which is what makes an io walk cost new data rather than live data. three defects landed this week and they are one defect: a header that survives a rewind, holding a pointer to storage that does not. each was found by someone writing an ordinary program rather than by a corpus, and each needed a different part of the runtime to learn the same question.

the first is a list built before the chain's first bind and pushed to inside it. the header sits below the mark, so it outlives every rewind; the buffer the push allocates sits above it and does not. an in-place push now asks whether the list was born this beat, and grows a fresh buffer when it was not.

the second is the copy walk one level over. the walk prunes at any node that survives the mark — below the mark it outlives the rewind, so share it and stop — and never asked whether the storage that node points at survives too. a string taken out of a carry buffer and built into a fresh record leaves the record surviving with a field aimed at a buffer two carries from retirement. a node is shareable now only when it and its immediate interior both survive, asked in the sizer and the copier alike so the two agree about what they are doing. the exception is a string builder, whose storage is malloc'd exactly so that a rewind cannot reach it; copying one strips the capacity that says so, and the next append finds a plain string where it left a builder.

the third is laziness meeting the same edge. a call whose callee is a beat loop is bracketed with a mark and a rewind on the promise that its arguments are already evaluated and so live below the mark. a thunk is the argument that is not: it is forced inside the loop, its value is built above the mark, and the cell memoises a pointer to it that the first rewind invalidates while the cell still reads as forced. k_force declines a memo it cannot keep — when the value it just computed does not survive the innermost mark, the cell stays unforced, keeps its captures and recomputes on the next read. every cost golden is unchanged, which prices the shape: a thunk forced inside a beat whose result lands above the mark does not occur once in four benchmark veins.

the second fix is the only one that costs anything, and it costs a fortieth of a percent of encode allocations — the copies it now makes instead of sharing what it should not have shared. there is no version of it that does not pay them. welfare holds at 65.56.

what took longest in each case was the reduction. the copy-walk fault would not shrink at all: six self-contained attempts produced programs both engines agreed on, and a first generated-tree test passed with the bug because it named its tree tree — short paths mean short strings mean a different allocation sequence, and sweeping the root name's length showed the fault appears at eighteen characters and holds from there. the memo was found by building with kanso build, editing the emitted IR by hand and relinking it against the runtime object the compiler leaves behind. adding one force to a parameter answers in a single run what a week of reductions had not, and that is worth naming as a technique rather than as an anecdote.

12two things the compiler stopped letting through

an effect handed to a parameter nobody reads. an effect is a description and a description nobody forces never happens, so a call like ignored (io/write "…"), where every arm of ignored discards that position, wrote nothing and said nothing on either engine. the language's stated position was always that a body doing io must hand the io back or it abandons the effects above it — but that check reads a function's own body, so an effect abandoned inside somebody else's parameter walked straight past it. the new rule refuses only the case with no other reading: every arm throws the position away. a group where one arm reads the parameter and another does not is a real question and stays legal. it needed nothing new to decide, because inference already knows whether an argument describes an effect and the arms' own patterns already say whether all of them discard it.

a diagnostic that told the truth about a name. when a module's own pub shares a name with something a dependency exports, the read resolved to the dependency and the compiler said the name was private — sending an author to add a pub that was already there. it now says which module took the name and that import { theirs:yours } resolves it.

the digest that finishes the story is std/sha256: FIPS 180-4 in ordinary kanso over std/bits, no builtin, because a hash is arithmetic on thirty-two bit words and the bitwise operations were already there. it is what the asset fingerprinter needed, and with the fingerprinter ported the only python left in this repository is the two gates that drive a headless browser.

13the language reserves no name

kanso has no main. a directory is run through the file called main.kso, and that is a filename rather than an identifier — a program may use the word main for whatever it likes. the same rule now covers play, which the compiler used to know: it looked for a pub binding by that exact spelling and synthesised the entry from it. one magic name is a rule; two is a habit.

play is an ordinary exported constant. what makes something runnable is a file of statements, and the way a sample stays one command away is a runner beside it:

# greeter.kso — an ordinary library
fn shout name
  "hello, {name}!"

pub play = print (shout "kanso")

# main.kso — the runner
import "greeter"

greeter/play

the compiler is told nothing about either file. the runner names what it wants, and the convention lives where a convention belongs — in the harness that runs the samples, and in the playground, each of which generates the entry it needs. what this cost was one capability rather than a mechanism: a bare import used to name a sibling subdirectory, so a runner could not import the file sitting next to it. a module is a directory of files sharing one namespace, and one file is the smallest of those, so import "greeter" now reads greeter.kso the way it reads greeter/. both spellings present at once is refused: one name cannot answer two ways.

14a constant may name itself

read a data file describing relationships and you have to build the graph it describes, which means a node holding a node that holds the first one back. kanso used to refuse that outright. any constant whose body contained its own name got `graph` is defined in terms of itself, so it has no value, and the only way through was to build the shape at runtime out of ids and look each one up on every hop.

type node
  name
  peers

graph = { "a":(node "a" [graph["b"]!]) "b":(node "b" [graph["a"]!]) }

pub play = print "{graph["a"]!.peers[1]!.name}"

that prints b. the rule that admits it is about demand rather than mention. a constructor stores its arguments and never looks at them, and a list or map literal stores every element, so a name appearing inside one goes into a field and is read later, by which time the constant it names has an answer. a name appearing anywhere that scrutinises it, an operator or a guard or a call that forces what it is handed, is still a demand, and x = x + 1 gets exactly the error it always got.

admitting the program is the smaller half. the constant freezes into a cell filled once before the program starts, rather than recomputing on every read, which is what stops the definition chasing itself down the stack. a storing position inside it emits a value that waits instead of a value, and both the type inference and the code that decides where to force learn that a container read may hand back one of those. all of it is gated on the program holding such a constant at all, so a program without one emits and pays exactly what it did before.

the browser is the exception. that backend has no waiting value of any kind, so it refuses the program with browser backend: `graph` names itself, and the browser has no way to wait for a value. the differential law allows a feature on fewer engines as long as the others refuse clearly. what it forbids is two engines quietly disagreeing about what a program means. closing it means giving that backend a deferred value of its own.

15the connectives are words

a and b, a or b, not a. & | ^ stay with the bits, where they read as bit patterns rather than as questions, and there is no ! at all — a language that says and out loud and then punctuates its negation is mixing two metaphors in one expression.

none of the three costs an engine anything. the parser writes them as if at parse time, so no backend has ever seen an and node and none needed teaching. not sits on its own rung between the connectives and comparison: not a and b denies only a, and not a == b denies the whole comparison. that rung is what keeps the parentheses in not (a or b) load-bearing while calling the ones in (not a) or b superfluous.

flag == true is a compile error, and so are its three siblings. the comparison asks a question the value has already answered, and comparing two booleans to each other is exclusive-nor wearing a comparison's clothes — a spelling the language declines to offer. elsewhere this is what a linter warns about; kanso has no linter layer, and a language with one way to write a thing has nowhere to put a warning, so what a linter would flag is refused instead. the error names the replacement: the value itself, or not the value.

there is no nand and no nor. not (a and b) already says it, with the denial on the outside where it reads.

16a server is two ordinary statementsshipped

kanso speaks tcp now, and the syscalls were the easy half. a server that blocks on accept cannot share a program with the client that would connect to it, so the two would have to be two programs — which is the moment a language usually grows goroutines, channels and a select to arbitrate them. kanso already has the arbitration: adjacent statements are one parallel group, scheduled by a deterministic green-thread runtime. what was missing was for accept to answer “not yet” and go back in the queue instead of holding the runtime, and the same for starting a process. with those two as scheduling points, a program serves itself — the server on one line, the request on the next, byte-identical interpreted and compiled.

std/net/http sits on top in Go’s shape, with one difference that matters: a handler is a plain function from a request record to a response record. there is no writer to hand it and no recorder to fake, so a test calls it and reads what it answered, and injecting a different handler is passing a different function. the mux is arms on the path, and adding a route is adding an arm. many requests are a fold — the handler answers the response and what to carry to the next one, and carrying none closes the door.

processes gained the other half of Go’s pair. io/run starts one and waits; io/start answers the handle and io/kill ends what it names, because a browser told to render a page ignores its own exit budget and runs until something stops it.

17two failures, and which one you hear about

a failure in an operation propagates: hand an err to anything and you get the err back. when both sides fail, the answer carries both — the reason becomes the list of reasons, and neither failure is billed above the other, because neither caused the other. that merge is a fold rather than a pairing, so three failures answer three reasons however they were grouped, and the shape of the expression that produced them is not recoverable from the result. an err whose reason is itself a list stays one reason; the mark saying which is which is structural, because it cannot be read off the shape.

the wall is the other rule. >> is ordered, so the first failure is the answer and what follows it never speaks. adjacency accumulates because nothing there is first. short-circuit where there is order, accumulate where there is none — you never choose between the two, because each is the only behaviour its structure permits.

the two rules make the same pair of algebraic objects, over one carrier: adjacency associative and commutative, the wall associative with the first failure absorbing. that is what makes the grouping invisible, and it is also what a user-supplied version of either would have to obey.

17what a count can and cannot see

every performance claim on this page is pinned to a count rather than a stopwatch, because a count cannot drift between machines. that choice has an edge, and knowing where it sits is part of reading the numbers.

a count answers the question it was written to ask. bench/compile_golden.txt records what one inference pass costs — the rounds the fixpoint went round and the expressions it looked at — by resetting the counter and running exactly one pass. so it prices a pass, and says nothing about how many passes the front end asks for. those are different questions, and the second one is now pinned separately: the whole-program pass count is a watched number with the same standing as the golden, and moving it costs a sentence in the log naming the pass and the reason. the front end infers once and hands the result to the three checks that read it; kanso check over kq is 11.0 ms, of which about 2.4 ms is process startup.

the same edge shows up in memory, where one number is currently unpriced. a tail-recursive loop that allocates a temporary each time round runs in constant memory when it threads a scalar and retains everything when it threads a map or a list — 1.5 mb against 71.9 mb over 1.6 million iterations. the temporary need not be kept: a string that is bound, measured, and dropped costs what a string stored as a map key costs. the emitted code says why. a scalar loop carries an arena bracket and rewinds every iteration; a heap-accumulator loop carries none of it, so the loop has no rewind point at all.

the rule that declines the bracket is sound as written — rewinding would free an accumulator allocated mid-cycle. the accumulator in that shape is not allocated mid-cycle, because the in-place mutators hand back the same object, and the fix is to give a threaded container storage outside the rewound region the way a byte builder already has. that is the open item, and the retention is a pinned counter in the memory vein until it closes, so the day it moves the diff is the evidence.

every counter this page names is recorded on every merged commit and drawn on the long view, one row each — what a run costs, what compiling costs, and the welfare score they roll up into.

codathe mushroom test

every proposal on this page passed the same two filters before it earned a section. the first is the house rule you've met everywhere else on this site: polymorphism over conditionals—if the design answer is "the user writes an if," the design is wrong, because dispatch makes the arms checkable and the conditional unwritable. the second is the mushroom test. a mushroom looks like a new organism; dig, and it's the fruiting body of a mycelium that was already there. that's the question every ruling faces: does this add a concept, or reveal that an existing one already covers it? zero-field types looked like a feature and turned out to be record types with nothing left to remove. enums looked like a feature and turned out to be typesets of markers. the integer tiers looked like machinery and turned out to be one case of speculation under purity. proposals that add a concept wait; proposals that reveal one land.