kanso language specification — v0.1 (design freeze, july 2026)
working name kanso (japanese: simplicity through elimination of clutter), extension .kso. pronounced KAHN-so, unvoiced s.
this document is the authoritative record of every design decision gaveled to date. anything marked open is not yet decided. the founding principle behind every rule:
anything a style guide, linter, or code review would enforce by convention, kanso enforces by making the alternative a compile error or unrepresentable. programs have a canonical form: one rendering per program. the source file contains only decisions; all derivable information lives in tooling (LSP inlays, publish diffs).
the annotation doctrine (governs everything): an annotation is legal iff it carries a stipulation the compiler cannot derive. redundant annotations are compile errors. this one rule decides return types (never written), interfaces (deleted), type variables (written only when stipulating), typeset guards (written because enumeration is non-derivable), and field types (written because declarations have no body).
the ordering principle: order is never implicit. ., >>, and recursive data flow are the complete vocabulary of "before." where there is no mutation, the runtime owns order (reorder, parallelize, dedupe, cache freely).
section 01purity and effects
- every function is pure and returns exactly one value.
- effects are values (descriptions).
print "hi"returns a description of printing; nothing executes until the runtime receivesmain's description. .(spaces around it) composes descriptions with data flow (pipe).>>composes with pure sequencing, no data.- descriptions are inspectable data: testing asserts on descriptions or runs them against a scripted executor and asserts on the transcript. no mocks. (structural equality holds only up to closure boundaries; the transcript method is the general story.)
- effect sets are inferred per function and propagate up the call graph. no function coloring, no async/await.
main's inferred effect set is the program's permission manifest. - pure calls are plain calls: zero monadic overhead when both the effect set and propagable set are empty.
- no
donotation. ordinary bindings and pipes are the only composition syntax. - descriptions carry origin spans (provenance) for error reporting.
section 02errors
- no exceptions, no panics. failure is a value.
- no
ok/somewrappers. success is the bare value;err reasonandnoneare ordinary types that propagate. - fixed-width arithmetic overflow returns
err. lookup misses returnnone. division by zero returnserr. - fine-grained failure types are idiomatic (
timeout,parse_failure d); bareerris the base case, not the convention. - multi-argument propagation rule: when several arguments carry contexts, the leftmost context-carrying argument propagates, deterministically, regardless of evaluation order.
section 03types
- every type is a single-constructor record. no union declarations;
true,false,none,errare independent types. - fields are typed (declarations have no body to infer from):
type user
admin: bool
email: string
name: string
- fields in alphabetical order, enforced (formatting error otherwise). general rule: wherever order is semantically inert (fields, imports, typeset members, overloads within specificity rank), canonical order is mandatory.
- positional construction only, in alphabetical field order:
user false, "e@x.com", "clay". no keyed construction. single-field types construct bare:err reason,circle r. - record update is the only keyed form (it's a different operation — partial, by name):
u { admin: true }. keys within an update: alphabetical. - adding a field is loud: every construction site and destructuring pattern becomes arity-wrong (compile error with locations). renaming a field can reorder the constructor — mitigated by distinct field types catching transposition, LSP field-name inlays at call sites, and rename refactors flagging call sites.
_discards in patterns and is exempt from unused-binding errors.- types are opaque outside their defining module (law of demeter, structural).
section 04typesets and unions
- a typeset names a set of concrete types. newline-separated members, composition by inclusion, flattened and closed at declaration, cycles are errors:
typeset signed
int
int16
int32
int64
int8
typeset numeric
float32
float64
integral
- typesets/unions may appear anywhere a type can: parameter guards
(x: int float64)(inline, space-separated), field types (result: done err pending), container elements, inferred return sets. - values may inhabit unions. the only eliminator is dispatch. no
match, no tag tests, no narrowing syntax, noinstanceof. runtime discriminants exist but are compiler-owned; no user syntax reads them. monomorphic code carries no tags. - primitives (math, string ops) are concretely typed at the bottom, so every union is resolved by dispatch before computation touches it.
- ownership/coherence: a module may define an overload only if it owns the function name or the argument type. no extending others' typesets. overlapping guard sets on one function: compile error.
- there is no
anytype and no capability-constraint syntax (capabilities are always body-derivable → banned as redundant; enumerated sets are stipulations → legal). do not use the word "union" in user-facing docs; say typeset.
section 05dispatch and overloads
- overloads dispatch on literal values, concrete types (via destructuring or annotation), or typeset guards. resolution is fully static per monomorphized instantiation.
- specificity ladder: literal > single concrete type > typeset (any size) > unannotated generic. sets never rank against each other; overlap is illegal.
- annotations distinguishing overloads are legal exactly when bodies don't (
fn process (x: float32)vs genericfn process xwhere both callto_string). if bodies structurally distinguish the types, the annotation is redundant → error. - return-type-directed dispatch: a concrete return annotation is legal where context can't infer it (
decode: config). switchdoesn't exist; literal/constructor dispatch is the switch.ifis a binary expression conditional. multi-way conditional: deferred pending evidence from real code.
section 06auto-propagation
- for any type a function doesn't handle, the compiler generates an identity pass-through overload. you write only the cases you care about;
err,none,loading, anything else flows through. - inferred return sets are honest unions computed as a monotone least fixpoint over the call graph.
- endpoint rule: a constructor reaching a chain endpoint unhandled is a compile error.
errreachingmainunhandled is uncompilable. in processes, unhandled err at a process top becomes a message to its supervisor.
section 07constraints and generics (no interfaces)
- there is no interface/class/instance construct. generic functions' requirements are inferred from bodies, minimal by construction, transitive through call chains. the LSP renders the effective contract (requirements, effect set, return set) as derived views with provenance chains.
- publish tooling diffs inferred contracts and mandates semver bumps (elm-style, enforced).
- generic library functions replace default methods (define
compare, getsort/max/<for free). transitivity replaces superclasses. eq,compare,hashderive structurally for records of derivables. floats get total ordering (IEEE totalOrder);sort pricesworks, period.- explicit type variables: bracket slot on declarations, go-style, used only where sharing is a stipulation the body/declaration can't imply:
type cache[a]
entries: a[string]
fallback: a
type matrix[n: numeric]
...
- functions may use the slot (
fn zip_with[a] f, (xs: a[]), (ys: a[])) only when stipulating sharing beyond what the body forces; otherwise redundant → error. - requirements attach to usage paths, not type existence:
stats[string]constructs fine; callingabsorb(which adds) on it fails at that call site. eager narrowing is opt-in via slot constraint (type stats[a: numeric]). - instantiation mirrors declaration:
cache[user],pair[int, string]. no variance annotations (immutability → everything covariant), no higher-kinded types, no explicit instantiation of inferables.
section 08containers
- lists:
T[]in type position (postfix). literals[1, 2, 3]. the wordlistdoes not appear in surface syntax. - maps:
T[K]— element type first, key type in brackets. postfix composes left-to-right:user[string][]is a list of string-keyed user maps. - lookup
at m, kreturns the value ornone(propagates). no panicking access anywhere. put m, k, vis functional; perceus makes it in-place when uniquely owned.- map keys require derivable
eq/hash(inferred constraint). entries mreturns pairs in sorted key order — deterministic iteration, always.- 1-based indexing, everywhere, no exceptions.
slice xs, 2, 4is inclusive both ends.index_ofreturns a position ornone. - map literals: open (candidate
["k": v], no pressure yet).
section 09bindings, rebinding, flow
=binds immutably. rebinding a name is legal (SSA under the hood); each version must be used before the next rebind (nothing-wasted per version). closures capture values, not names.- no
var, noconst— one kind of binding, zero keywords. - unused expressions and unused bindings are compile errors.
- pipe is canonical for linear chains where each intermediate is used once; rebind ladders are for what pipes can't express. (tentative: pipeable ladder = compile error; fallback: tooling auto-pipes. decide during implementation.)
- commas separate arguments, mandatory for multi-arg calls and signatures. juxtaposition = application:
forward to_string xis one argument. pipe binds tighter than comma. lists use commas. no semicolons. - string interpolation
"{x}". comments//. 2-space indent. snake_case, all lowercase, always. - canonical formatting is grammar: non-canonical whitespace is a syntax error. no formatter tool exists.
section 10memory model
- values immutable → heap is a DAG by construction → reference counting is complete. no GC, no borrow checker, no lifetimes, no memory syntax in source.
- perceus-style: counts elided statically (no shadowing + nothing-wasted make liveness exact); frees inserted at last use; unique ownership turns record update into in-place mutation (FBIP). tail-recursive state loops compile to loops mutating registers.
buildregions for cyclic construction:slotcells are mutable inside; writers return effect descriptions executed by the region's executor; the return value freezes (slots become immutable fields). the slot type may not appear inbuild's return set (compile check reusing propagation set-tracking). slot effects outsidebuildare unhandled-type errors. everything from one region freezes into a single RC block; internal refs (including cycles) are offsets; the block frees as a unit — DAG preserved at RC granularity.- magic budget: intrinsics only (
slot,read,write+ runtime effect vocabulary: file, net, clock, random, spawn/send/receive...). the spec maintains a literal intrinsics list; everything else is ordinary code. stdlib has no privileged powers. - build totality: open — unwritten slot at freeze is probably a runtime
err(consistent with overflow-as-value). two writes to one slot with no./>>path between them: compile error (race inside the region).
section 11concurrency and processes
- arguments and independent bindings evaluate in runtime-chosen order; only the description graph constrains effects. implicit parallelism falls out.
- processes, erlang-shaped:
spawn f(description yielding pid),send pid, m(effect),receive(effect yielding next message). servers are recursion-held state:fn serve state/receive >> (m -> serve (handle state, m)). message handling is dispatch-by-overload on message types. - no shared memory, no locks, no channel primitives. supervision trees: a child's unhandled err becomes a supervisor message with restart policy as data. the root supervisor is user code — global error reporting (sentry etc.) is an ordinary overload there, with provenance spans riding along. no ambient handlers.
section 12modules
- modules are directories (go-style); subdirectories private to parent. imports:
import "std/http", alphabetized. - bare identifiers mandatory; qualification only on collision; qualifying a unique name is a compile error.
- qualification syntax: open —
json.decode(whitespace-distinguished from pipe) vsjson/decode(matches import paths, frees.). recommendation on file: slash.
section 13numerics and strings
intis arbitrary-precision, the default. fixed-widthint8..64,uint8..64,float32/64. fixed-width overflow →err.- strings are opaque UTF-8; string positions 1-based.
- coercion/promotion details: open (recommendation: none — explicit conversion functions only).
section 14open questions (priority order)
- inference fixpoint formalization. dispatch depends on types; types depend on generated pass-throughs; pass-throughs change return sets. plan: propagable sets as monotone least fixpoint over the call graph, then dispatch resolution, with programs where dispatch would feed back into sets rejected. do this on paper before any code. unions-as-values made the lattice natural; the stratification proof is still owed.
- destructuring syntax: list patterns (
fn sum [], cons/rest spelling), multi-field record patterns, partial patterns with_. - qualification syntax (§12).
- build totality (§10).
- pipe-vs-rebind canonicality (§9).
- map literals.
- multi-way conditional.
- ffi (route through effect descriptions at the edge; 1-based translation layer lives there).
- build tool (
kanso run/build/test/publish; publish does contract diffs). - numeric coercion.
- process details: mailbox ordering guarantees, selective receive or strict FIFO, restart policy vocabulary.
section 15implementation plan
- phase 0 (paper, ~a week): fixpoint formalization (§14.1). the language's two founding features must be proven mutually consistent before code.
- phase 1: tree-walking interpreter as reference implementation. suggested host: rust (query-based/salsa-style incremental architecture from day one — the LSP shares the query engine; this was always the architecture). deliverables: lexer/parser for the canonical grammar (formatting errors included), name resolution, the inference engine (sets + unification + constraints), dispatch resolution, pass-through generation, endpoint checking, description-building runtime with a pluggable executor (real + scripted-for-tests),
kanso run, golden-file test corpus of programs and expected errors (the error corpus matters as much as the success corpus — half this language's value is its compile errors). - phase 2: LSP on the same query engine: contract inlays, field-name inlays at construction sites, provenance chains.
- phase 3: native compiler. target C (the koka/lean/nim-proven path for perceus) or cranelift for a jit story. not llvm first — kanso's performance lives in its own analyses (perceus, FBIP, monomorphization); llvm is the backend for the year kanso needs to win benchmarks, and that is not year one. monomorphization + pass-through generation is a code-size multiplier: measure early. adopt rust's ban on polymorphic recursion.