about

about kanso

kanso ships two engines. its print returns a value instead of printing. it has no formatter, and no plans for one. those three choices come from one idea about where a language should put its opinions, and this page works through them in order.

preludeon taste

most languages treat style as a matter of preference. they admit several ways to write the same thing, ship a formatter to settle the argument afterwards, and leave the rest to a style guide nobody reads twice. kanso takes the opposite position: the choices a style guide would make are made once, in the grammar, and your formatting preferences are a compile error.

the positions are borrowed. kent beck: the four rules of simple design, and never trust a test you have not seen fail. sandi metz: duplication is cheaper than the wrong abstraction. avdi grimm: a raise is for the case you do not handle, and nil should never leak. martin fowler: refactoring means behaviour-preserving, and the word matters. rob mee, abhi hiremagalur and the pivotal school: the codebase belongs to everyone, so it should read as if one careful person wrote it. DHH: convention over configuration. what kanso adds is enforcement — a value system that used to be taught, compiled instead.

part 01two engines, one truth

kanso ships two engines. kanso run compiles your program to a native binary cached by content hash. an unchanged program re-runs in about five milliseconds without invoking the compiler at all; a changed one rebuilds in tens of milliseconds against a runtime that is already optimized and already cached.

kanso build --release is the shipping artifact. it lowers kanso straight to llvm ir — dispatch becomes branch chains, patterns become field offsets, every call in tail position is a guaranteed tail call, so ten million frames of mutual recursion run in constant stack — and hands the ir to clang.

on a 188kb json benchmark, read at runtime and decoded 150 times, that binary keeps pace with hand-tuned serde_json — the live board on the compiler page is remeasured by ci on every merge, interleaved so the ratio holds whatever the machine is doing. peak footprint is the smallest and flattest on the board. whole-program inference feeds the emitter, so paths it has proved carry no tag checks and no failure guards.

the wider margin is on demand-driven work, where the clear kanso form runs about fifteen times ahead of rust as naturally written, because work nobody demands is never born. the deterministic scheduler credits wall time: real time a fiber spends computing counts against every pending sleep, so a concurrent program shrinks to its longest branch and the transcript stays byte-identical.

the gap to serde used to be allocation, kanso copying where serde reuses. rewinding arenas closed that — the strict tier of a two-tier memory model with no collector, whose ratified destination is reference counting over kanso's values. what remained was representational: tagged values and per-call dispatch. three later changes closed it. an inlining fix recovered work -flto was silently dropping across the .ll/.o boundary, a simd two-byte scan sped up the string scanner, and integers got a fast path for the common case.

two engines exist to refuse a trade. go bought instant compiles and pays for them in slower binaries. rust bought the fastest binaries and charges every developer for the wait. kanso separates the two paths so the dev loop never pays the optimizer and the release path never compromises.

the price of that is agreement. both engines have to mean exactly the same thing, so ci builds every example native and requires byte-identical output against the interpreter — floats included, since both render the shortest round-trip. a one-bit disagreement fails the build.

go refused llvm because it was too big and too slow for the compile times they wanted, and slower binaries are what that refusal cost. kanso gets both, by never asking llvm's optimizer to be part of the dev loop. dev builds compile unoptimized against a cached runtime and answer in tens of milliseconds. -O3 -flto runs only when a release binary is being made.

which also retires the question every new language gets: "why rust and not llvm?" is a category error. rust is the host—the language the compiler is written in. llvm is the backend, and kanso employs it directly: the emitter writes llvm ir, clang turns it into machine code. the speed still on the table lives in kanso's own analyses, not in the backend—llvm multiplies what the front end proves.

so why keep an interpreter, once kanso run compiles? three reasons.

it is the oracle. two independent implementations of the semantics, held byte-identical by ci, catch each other's bugs. the differential suite has caught a float-rendering divergence, a utf-8 validation gap, and — on the day kanso run went native — an interpolation bug and a missing pattern form in the backend. a compiler tested only against itself moves confidently in whatever direction its bugs point.

it is the interactive tier. the repl and --plan, which renders an effect plan rather than running it, want evaluation without artifacts.

and it is what runs in the browser. the toolchain compiles to about half a megabyte of webassembly, which is how the playground works. there is a third backend now: it emits wasm bytecode directly, with no llvm anywhere, so a program you type compiles to a wasm module in the tab and runs as machine code, tail calls included. llvm is a dependency measured in hundreds of megabytes and could not follow kanso into a browser. the front end proves what it can, and each backend multiplies that.

mugi the tanuki - runs the interpreter; pays for everything in boba pearls
mugi the tanuki—runs the interpreter; pays for everything in boba pearls
tsuru - folds your program until it is small enough to fly
tsuru—folds your program until it is small enough to fly

part 02the print that doesn't print

what does print "one" do when it is 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
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 1
  print "two"    // from line 1
  print "three"    // from line 1

--plan falls out of that. the description already exists as inspectable data, so the flag renders it instead of handing it to the executor. each step carries its origin span, which is where the // from line 1 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

kanso nearly shipped with a class of programs it would refuse to compile. the class turned out to be empty, and the reason is worth following.

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.

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."

the resolution was already in the spec. section 5 of the design freeze says dispatch resolution is fully static per monomorphized instantiation, and dispatch was therefore 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.

so the rejection class is empty. under vector-indexed dispatch there is no feedback channel, and every program that passes the ordinary static checks has well-defined inference. nobody will meet a wall that says their program confused the fixpoint — a wall that would have been unexplainable at exactly the moment it appeared. two things are still owed, named here so they cannot hide: finiteness of the type universe, and termination of monomorphization.

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 is maintained as carefully as the success corpus, because a good share of this language's value is in its diagnostics. each message states the rule it is applying. there is no style left to have an opinion about, so a 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 or linter 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 no diff between two versions of a file is ever formatting churn—no reflowed lines, no argument about where the blank line goes, no imports to re-sort. what remains is real: a changed decision, a renamed thing, a helper extracted or inlined. review, whether the reviewer is a human or a machine, no longer spends attention on form and shrinks toward the thing it was always 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 by construction.

dedication

kanso is dedicated to abhi hiremagalur—pivot, extremist of simplicity, patient zero of several of these opinions. he would have deleted half of this page, and it would have been better.