essay

the philosophy of the compiler

kanso's compiler makes some choices that look backwards at first: it started as a deliberately slow interpreter, its print doesn't print, its scariest theoretical problem was solved by deleting the solution, and its formatter doesn't exist. each of these is the same decision wearing a different outfit. this essay walks through all four — and ends with the audience the design accidentally optimized for.

preludeon taste

paul graham wrote that taste is real — that "good design" is not a matter of opinion, and the people who insist it is are usually the ones without any. most languages are terrified of this idea. they ship eight ways to write a loop and call it freedom, they bolt on a formatter as an apology, and they let a thousand style guides bloom because taking a position might cost them a user. kanso takes the other bet: it has opinions the way DHH has opinions — loudly, in writing, and enforced by machinery. your formatting preferences are a compile error here. we're aware of how that sounds. we're comfortable.

the opinions aren't ours alone; they're inherited from a lineage that spent forty years learning them the expensive way. from kent beck: the four rules of simple design, and the discipline of never trusting a test you haven't seen fail. from sandi metz: duplication is cheaper than the wrong abstraction. from avdi grimm: confident code — a raise is for the case you do not handle, and nil should never leak. from martin fowler: refactoring means behavior-preserving, and the words matter. from rob mee and abhi hiremagalur and the pivotal school of extreme programming: the codebase belongs to everyone, so it must read as if one careful person wrote it. from DHH: convention over configuration, omakase — the chef chooses, and that's a feature. kanso is what happens when that value system stops being a style guide you have to teach and becomes a compiler that already knows.

part 01why build the slow one first

the question every new language gets — usually within minutes — is "why rust and not llvm?" it's a fair question with a surprising answer: it's a category error. rust is the host, the language the compiler is written in. llvm is a backend, the thing that turns an intermediate representation into machine code. and phase 1 of kanso has no backend at all. it's a tree-walking interpreter: it evaluates the syntax tree directly, the slowest respectable way to run a program.

slow on purpose. the implementation plan (spec §15) doesn't even start with code — phase 0 is a paper proof that inference and dispatch, the language's two founding features, are mutually consistent (more on how that went in part 3). phase 1 is the reference interpreter, built query-based from day one so that phase 2, the lsp, shares the same query engine instead of reimplementing the language a second time. the risky part of kanso was never code generation; it's semantics. you don't optimize a thing you haven't proven means anything.

and when the native compiler does come, phase 3 targets c — the path koka and lean proved out for perceus-style reference counting — or cranelift for a jit story. that is not the slow road: emitting c means renting clang and gcc's entire optimizer pipeline without maintaining a line of ir bindings, which is exactly how koka and lean post c++-competitive numbers. kanso's headline speed lives in its own analyses — perceus refcount elision, functional-but-in-place updates, monomorphization so generic code carries zero dynamic dispatch, and an ordering principle that licenses the runtime to parallelize anything unconnected by . or >>. direct llvm ir lands when profiling shows the c detour leaves speed on the table: a measurement, not a milestone. performance is a founding goal, tracked from phase 1 by an abstract cost model — operation counts as ci baselines — so every optimization proves itself against recorded fixtures before anyone times a machine.

part 02the print that doesn't print

here's a two-line program. what does print "one" do when it's evaluated?

nothing. it returns a value — a description of printing. every kanso function is pure; >> glues descriptions into bigger descriptions; and nothing in the entire program executes until the runtime receives main's description and walks it. the same program, run two ways:

examples/effects.kso
fn main
  print "one" >> print "two" >> print "three"
kanso run examples/effects.kso
one
two
three
kanso run examples/effects.kso --plan
plan:
  print "one"    // from line 2
  print "two"    // from line 2
  print "three"    // from line 2

--plan isn't a debugging trick bolted on afterwards — it falls out of the design. the description already exists as inspectable data; the flag just renders it instead of handing it to the executor. (each step carries its origin span, which is where the // from line 2 provenance comes from.)

the payoff lands in testing. in most languages, testing effectful code means mocks: intercept the i/o call, record what would have happened, hope the interception is faithful. in kanso there is nothing to intercept, because evaluation never does i/o. the interpreter's runtime is a trait with two implementations — a real executor that prints, and a scripted one that appends each effect to a transcript:

src/eval.rs
pub struct ScriptedExecutor {
    pub transcript: Vec<String>,
}

tests run main, hand the resulting description to the scripted executor, and assert on the transcript: assert_eq!(executor.transcript, ["print \"a\"", "print \"b\""]). no mocks, no stubbing framework, no "was this called with these arguments" — just data equality on what the program said it would do.

part 03the conjecture that dissolved

this is the best story in the repository, and it's a mystery in three acts.

act one: the circle. kanso has no interfaces, and return types are never written — they're inferred. it also has overload dispatch, and it auto-generates pass-throughs: for any failure type an overload group doesn't handle, the compiler adds an identity overload so err and none flow through untouched. put those together and three facts chase each other's tails. dispatch needs types: which overload a call targets depends on its arguments' types. types come from bodies: a function's return set is computed from whichever body dispatch selects. and pass-through generation changes return sets: the argument's type set leaks into the result. sets determine dispatch, dispatch determines bodies, bodies determine sets.

act two: it gets worse. the standard cure for circular definitions is a fixpoint: start from nothing, apply the rules, repeat until nothing changes. that only works if the rules are monotone — learning more can never un-conclude something. and dispatch, framed naively, isn't. suppose inference has established that an argument's type set is {string}, dispatching a call to the generic body. a later round grows it to {string, int} — and now an int-specific overload outranks the generic one. dispatch shifted. learning more changed an earlier answer, which is exactly the thing fixpoint iteration cannot survive. draft 0.1 of the formalization built the machinery you build when you're scared: a two-phase algorithm — an over-approximate phase, an exact phase, a verification round — plus a reserved class of rejected programs, the ones "where dispatch would feed back into sets."

act three: the resolution was in the spec the whole time. section 5 of the design freeze says dispatch resolution is fully static per monomorphized instantiation. read that carefully: dispatch was never a function of type sets. it's a function of vectors — concrete type assignments to a call site's arguments, drawn from the product of the argument sets. and a vector's dispatch reads no inferred set at all: it depends only on the declared overloads and the concrete types in hand. re-run the scary example under that lens. growing {string} to {string, int} shifts nothing — the vector (string) dispatches to the generic body before and after, and the vector (int) is new, arriving with its int-specific body in tow. nothing is removed; return sets only grow. the non-monotonicity was an artifact of asking the wrong question. the two-phase algorithm collapses into a single monotone kleene fixpoint with exact dispatch — no over-approximation, no verification round.

and here's the payoff, the part that matters to someone who will never read a proof: the rejection class is empty. under vector-indexed dispatch there is no feedback channel, so every program that passes the ordinary static checks has well-defined inference. no kanso developer will ever hit a "your program confused the fixpoint" wall — a wall that, had it existed, would have been unexplainable at exactly the moment it appeared. the conjecture wasn't proven. it dissolved. (the full argument, including what's still genuinely owed — finiteness of the type universe, termination of monomorphization — is in design/fixpoint.md.)

part 04the formatter that doesn't exist

every serious language eventually ships a formatter, and every formatter is a confession: the grammar admits many renderings of the same program, so a second tool exists to pick one. kanso skips the confession. canonical form is the grammar — one rendering per program — so there is nothing left for a formatter to do. non-canonical spacing is a syntax error. blank lines inside a body, trailing whitespace, declarations out of alphabetical order: compile errors. so are unused bindings and unused expressions, because a line that does nothing is a rendering decision too. these are real diagnostics from the interpreter's golden-file corpus:

tests/golden/errors/trailing_whitespace.stderr
error[formatting]: trailing whitespace is not part of the canonical grammar
  --> trailing_whitespace.kso:2:13
   2 |   print "hi" 
                   ^
tests/golden/errors/fn_order.stderr
error[formatting]: function declarations appear in alphabetical order: `main` before `zeta`
  --> fn_order.kso:4:4
   4 | fn main
          ^
tests/golden/errors/unused_expression.stderr
error[unused]: unused expression: every non-final line binds a name (sequence effects with `>>`)
  --> unused_expression.kso:2:5
   2 |   1 + 1
           ^

the error corpus matters as much as the success corpus, deliberately — half this language's value is its compile errors. notice what each message does: it doesn't just reject, it states the rule, as if the rule had always been obvious. that's the whole posture of the language. there is no style to have an opinion about, so the diagnostic can afford to be calm.

part 05strictness is machine ergonomics

none of this was designed for language models. kanso's founding principle — anything a style guide, linter, or code review would enforce by convention becomes a compile error or unrepresentable — was written for human readers, so that no reviewer ever spends attention adjudicating style. but follow the consequence to its end and you arrive somewhere interesting.

think about what teams actually write into their llm system prompts today: don't leave unused variables, delete dead code, match the project's formatting, don't drift from the house style. it's the same list that used to live in style guides and code-review checklists, migrated to a third home. in kanso that list has nowhere to live, because every item on it fails to compile. unused bindings, unused expressions, non-canonical whitespace, out-of-order declarations — the compiler already rejects them, for everyone, with the diagnostics you just read. there is nothing subjective left to teach a model, and no slop for it to gradually accumulate: the classic failure mode where generated code is correct but slightly off — an orphaned binding here, a dead expression there — is a compile error, caught before any reviewer sees it.

one rendering per program has a second-order effect on review itself. if a program has exactly one canonical form, then any diff between two versions of a file is purely semantic — no formatting churn, no reflowed lines, no argument about where the blank line goes. review, whether the reviewer is a human or a machine, shrinks to the only thing it was ever supposed to be about: decisions.

the honest framing is that this is a consequence, not a motivation. kanso was designed so that readers never adjudicate style; it just happens that the cheapest reader to confuse — the one most prone to absorbing a codebase's bad habits and repeating them at scale — is now a language model. a language with exactly one rendering per program is unusually friendly to machine-generated code, not because anyone benchmarked it, but by construction.