Implementation Status

Implementation Status

The language reference describes Kairo as designed. This page lists where the compiler, version 0.2.0, falls short of it. Each entry is removed when the gap closes.

What 0.2.0 is

0.2.0 is the first self-hosted release. The compiler (Stage 1) is written in Kairo and has built itself since 2026-11-06; the C++ transpiler it was bootstrapped from (Stage 0) is retired. The test suite is about 2,066 lit tests, of which 49 are expected failures that name the feature they wait on and 3 are known failures. Only x86_64 Linux is covered by the suite; prebuilt packages exist for Linux, macOS, Windows, FreeBSD and NetBSD (Installation).

Built and working: the lexer, preprocessor, parser, name and type resolution, the semantic checks (including purity inference and the visibility levels), generics, classes with the copy / move model, nullables, panics, closures, generators, eval, pattern matching, C and C++ interop in both directions (ffi imports and kcc), parallel Sema and code generation, --emit-headers, and incremental compilation phase 1 (unchanged files skip code generation).

Not built yet:

  • Tether, the lifetime and memory-safety analysis, is Stage 2 work. Using a moved-from value is already an error; the rest of Tether's checks come later.
  • Virtual interfaces (calls through *Iface) and async (await, spawn, thread expressions) are not compiled.
  • Compile-time evaluation of function calls: an eval constant cannot yet call a function to compute its value.
  • The parser still hands some malformed input to later passes as ordinary nodes. Poison nodes for invalid syntax, and fully safe pointers through the compiler's own AST, are scheduled before 0.3.5.
  • Incremental compilation phase 2 (reusing work inside a changed file's neighbours) is planned for 0.2.5.

The list below is the detailed set of gaps in what is built.



Not Compiled Yet

FeatureStatusMeanwhile
Initializing a const field or a field without a default in a constructorCompiles, including in a generic constructor and for an array field built element by element (also an array of arrays, or of a generic length). Not yet (SC116E): a constructor with an expression body; try, an inline block, label! or jump! before the last such initialization; a static or array local declared before it and used after itMove that code after the last initialization, or give the field an initializer
Virtual interfaces (virtual interface, calls through *Iface)Not compiled yet: the declaration is accepted, but &conformer does not convert to *Iface (SC063E)Dispatch through a base class with virtual methods
An initializer on an eval array-typed field (eval E: [i32; 2] = [1, 2] in a class)Rejected with SC116EDeclare the constant at module scope
A generic record's field default that reads another field (class <T> G { var a: i32 = 1 var b: i32 = a + 1 })Rejected with SC116ERepeat the value instead of reading the field
Struct destructuring (var {a, b} = s)Rejected with SC116ERead the fields one by one: var a = s.a
Functions, classes and enums declared inside a function bodyCompile. Not yet (SC116E): one that uses a generic parameter of the function (or class) around it, and an extend inside a function bodyDeclare such a declaration at file scope with its own generic parameter
In a script: a top-level variable of nullable type whose initializer is not a constant (var g: i32? = find()), and top-level destructuring (var (a, b) = pair())Rejected (... is not supported yet)var g: i32? = null, then g = find() on the next line; destructure inside a function
Top-level statements in an imported module (module initialization)Rejected: only the file the program starts from runs top-level statementsPut the code in a function and call it from the script
for ... in over a Kairo stringRejected with SC116E: the library string does not declare its op in yet. Vectors, sets and maps workLoop over a slice, an array or a range, or index the string
Yielding a literal from a generator (yield 7)Fails in the C++ step with CL001E (a non-const reference cannot bind to the temporary)Yield a variable: var v = 7 then yield v
eval for directly in a module or class bodyRejected with SC116EUse an eval for in a function body
An eval for in fn main() that has no return typeFails in the C++ step with C886E (non-void function 'main' should return a value)Put the loop in another function, or give main a return type and return 0
In an eval for: a label (outer: eval for ...), a return of a class or struct value from inside the loop, and iterating an array whose length is a generic value parameterRejected with SC116EReturn after the loop
An eval if branch that is valid only for some instantiations of a generic (eval if typeof T == i32 { return int_to_bytes(value) }, the Type-based branching example)Compiles when every branch is valid for every T. A branch invalid for the chosen T is not skipped: it type-checks without a diagnostic, then the compiler stops with an internal error (I003E "poisoned expr reached codegen"). Waits on per-instantiation branch selectionUse overloads
Vectors, sets, maps and f-strings at compile time (eval V: [i32] = [1, 2], f"n={n}" in an eval fn)Rejected with SC116EUse an array; string literals, integers, floats, bool, char, arrays and tuples are evaluable
An eval field computed by an eval fn used as an array length in a field of its own class (class B { eval J = two() var b: [i32; J] })Rejected with SC116E: the class is incomplete inside its own bodyDeclare the constant at module scope, or make the constant fold (literals and arithmetic over eval constants)
A generic value parameter of a type other than an integer or an eval class (<eval B: bool>, <eval F: f64>)Rejected with SC116EUse an integer parameter
A generic class's requires predicate that names an eval constant or eval fn (class <T> B requires sizeof T <= LIMIT)Accepted and not checked (a predicate of the class's own parameters, literals and sizeof is checked)Write the limit as a literal in the class's clause, or check it in a constructor's requires
A function that reads a priv member of a class in its file, when that class declares a constructor, and whose signature names a nested type (fn f(k: Outer::Inner, v: *Vault))Fails in the C++ step with I003E (the emitted C++ does not let the function read the private member)Make the member pub, or move the nested type to file scope
v.insert(v.begin(), ...) on a cxx::std::vector (an iterator where the function takes a const_iterator)Rejected with SC063E: the conversion between the two iterator types is a C++ converting-constructor template, which is not consideredPass v.cbegin() / v.cend()
An unqualified call of a function declared in an imported header's namespace (tag(b, x) with b: adl::Box), found in C++ by argument-dependent lookupNot found (R020E) when no function of that name is visible otherwise; with a Kairo function of the same name in scope, the C++ ones are found and ranked with it as in C++. Operators are always found this wayQualify the call: adl::tag(b, x)
Inheriting constructors in an imported C++ class (using Base::Base;)The class imports, but the inherited constructors are not callable from Kairo: Der(5) is SC063E "no matching overload"Default-construct and assign, or add a forwarding constructor in a header
A C++ template value argument the parameter cannot hold (Narrow<256> for unsigned char, Flag<2> for bool)Not checked by the compiler; the C++ step rejects it at the use (C115E, clang's wording)Write a value in the parameter's range
for x in v over an imported C++ template used inside a generic Kairo function over its own parameter (fn <T> f(v: cxx::std::vector<T>))Fails with an internal compiler error (I003E, "EmitIR: ranged for"). v.size(), v.begin() and v.end() in such a function workLoop with v.begin() / v.end() and an iterator, write the function for the concrete element type, or pass the values in a Kairo container
map::operator[] (and any C++ member that default-constructs its element) with a Kairo ADT enum as the element (cxx::std::map<i32, Shape>, then m[1] = ...)Fails in the C++ step ("no matching constructor"): an ADT has no default valueUse insert_or_assign / at, or wrap the value in a nullable (map<i32, Shape?>)
An imported class template whose non-type parameter's default depends on an earlier parameter (llvm::SmallVector<T>, whose N is computed from T)The argument must be written (SmallVector<i32, 4>); omitting it is R032E "requires at least 2 generic arguments"Write the inline size
await, spawn and the thread expression (thread work())Can be declared as operators, but using spawn or thread is an internal compiler error (I003E, "the async lowering owns it"), and await on a type with op await is rejected (SC086E: only a Future can be awaited). (A thread variable is a different construct and is compiled.)-
Panic (panic, try/catch/finally around a panicking call, the C++ view ::builtin::value_or_throw)Compiled and run. Panic sets are inferred and checked, as specified: unhandled calls, declared sets and where they are required, the subset rules for overrides, conformers and function types, main, panic with -> !, constructors and destructors (which never panic), the error-type rules, and try/catch arms (dead, duplicate, after a bare catch); an expression arm may return, break or continue. A panicking function returns its error as a value (no unwinding): propagation, both try forms, re-panic (keeping the original site), finally (one written after a try runs when its block exits, like any finally, and warns), where groups, closures and function values work, in registers up to 16 bytes. Generic functions and methods of generic types panic too: the set is computed at the definition (a bound's requirement adds its declared set at the bound's arguments, a generic callee its set at the call's), and one C++ template serves every instantiation, its set ordered per instantiation. Not supported (SC116E): panics in generators and async functions, and in generic closures; a function type whose set names a generic parameter; a for loop over a panicking op in, a panicking op [] that returns a pointer, a panicking op as, and === through a panicking op == (every other panicking operator works, derived forms included); a method that implements two requirements, or overrides a method and implements a requirement, whose panic sets differ; a panicking interface requirement whose result names Self; keeping a bare catch e binding in another variable. The C++ view works through #include "foo.k" and --emit-headers: the inspection helpers, value_or_throw and what() (from op panic, a type's own or an extend's), and a header declaring [[nodiscard]] Panic<T, E...>. A C++ exception arm (catch e: cxx::std::exception) works in both try forms. The source location in what() and ::builtin::site() is found on ELF (Linux, the BSDs) only so far: elsewhere what() gives the type without a location, and a C++ host linked against a Kairo --shared library gets no location for an error raised inside the library. An extend of a generic type supplying op panic is not used by what(). crash!() and the asserting downcast p as *D (a crash on a miss) workWithout a location, log ::builtin::type_id<E>
Variadic enums (enum <T, ...E>, pack-expanded variants V { ... }...) and generic variadic functionsCompile: pack parameters, forwarding with ..., for over a pack (including a return from inside it), packs in class and base arguments, and pack-expanded variants built and matched by payload type. Not yet (SC116E or a not-viable call): writing a pack's types as generic arguments (f<i32, bool>(...)), a pattern over a pack (Box<T>...), a pack-expanded field typed other than the pack itself (e: *E), and building or matching a pack-expanded variant in a generic function. A partial specialization whose pattern holds a pack, as an argument or nested in one (Traits<Box<T>, Args...>, Traits<Tup<T...>>), is SC116EPass the types through arguments; build and match the variant where its type is concrete; specialize without the pack
The checked as into a closed enum, and unwrap!(), in a file-scope initializer (var c = f() as Color outside a function)Fails in the C++ compiler ("statement expression not allowed at file scope"); a constant operand is checked at compile time and worksInitialize the variable from a function, or with a constant
n as E? from an integer into a nullable plain enumFails in the C++ compilerAssign n as E to an E? instead
A C string (*const std::c_char, a C++ const char*) in an f-string (f"{e.what()}")Compiles, and prints the pointer (const c_char @ 0x...): there is no C-string formatter yetPass it on to C or C++ code to print it
@unwind on a closureRejected with SC116E (it parses as an expression attribute)Use a named @unwind function, or build with -funwind
Attributes on expressions other than @create T(args) / @destroy p (which compile, with no import), and the initializer form @create T{...}Parsed; rejected with SC116EWrite @create T(args), or call create<T>(...)
where guards on functionsCompile on free functions and methods (methods of generic classes too), with the fallback, priority order, coverage and the unreachable / overlap warnings. Generic functions group too when their overloads differ only in the names of their generic parameters (type and value parameters alike); they must name those parameters alike and have the same bounds. Not yet (SC116E): on a generic function or method with a pack parameter; a virtual or override method, an extend member, an operator or constructor, an out-of-line method definition, a forward declaration, a generator, an async function, a variadic function, or an overload with default arguments. An unsafe overload that differs from a guarded one only in unsafe is rejected (R039E): it is one function redeclaredTest the condition with an if inside one function
typeofCompiles as a variable's whole declared type (var y: typeof x). Not yet, each a compile error: as a value (var info = typeof x; TypeInfo is not implemented), nested in another type (*typeof x), or as a parameter, return, field or cast typeWrite the type out
Closures and lambdas (fn () { ... } as a value)Compile, with every capture mode (by copy, |*|, |*x|, |const *x|, mixed, ||), stored in a fn variable or field, passed, returned and nested. One that captures nothing can be passed where a C function pointer is expected; one that captures, or a fn value, is a compile error there. A closure's default parameter values work when it is called through the variable it was written into (var f = fn (a: i32, b: i32 = 5) ...; f(1)). Not yet: generic closures (fn <T>(x: T)) are rejected with SC116E; once the closure is copied to another variable or typed as a fn value, its default values are gone and a call must pass every argument (SC099E); async on a closure is ignored (it runs as a plain closure). Copying a closure that holds a moved-in MOVE value stops the programUse a named generic function; pass every argument; capture a MOVE value by pointer (|*x|) when the closure is copied
Nullables (T?, null, x?, == null, ??, ?., ?->, ===, unwrap!(), match on a T?)T? and null compile: a T? is ::builtin::Nullable<T> in C++, and a value or null converts into one anywhere a T? is expected (including *T?, a nullable pointer (*T)?, and nested (T?)?). var x? = e, the null test x?, == null / != null, ??, ?., ?->, ===, unwrap!(), a match on a T? and the checked downcast p as *D? compile. Narrowing works: a checked nullable reads as its T, through conditions, early exits, loops, match arms, @inout and mutable-self calls, pointers and closures, and an unchecked read is a compile error. Not yet: bool? is not one byte (a bool has no spare value the compiler uses yet); T? -> U? between different inner types fails in the C++ compiler; a nullable reached through -> or a subscript is not narrowed by a check; a checked downcast from a nullable pointer (maybe_animal as *Dog?) is rejected (SC027E)Copy a nullable reached through -> or a subscript to a local before checking it; unwrap a nullable pointer before downcasting it (if a? { unwrap!(a) as *Dog? })
A partial specialization selected by requires whose clause needs more than literals and the sizeof / alignof of builtins, pointers and arrays (a struct's size, an eval function), or names a conformance (requires P impl I)The first is an error (SC001E) where an instantiation is used, the second is rejected with SC116E; two constrained partials with the same pattern that both hold are ambiguous even where C++ would order themSelect by the argument's pattern (<P> X<*P>), or by a size of a builtin type
A generic class whose body names a pointer to itself at a growing argument (var p: *L<Wrap<T>> inside L<T>)Rejected with SC001E "instantiation depth exceeds 128", although nothing is instantiated through a pointerName the pointer at a fixed argument, or type-erase it (*void)
A float-to-integer cast of an out-of-range value, an infinity or NaNCompiles, but does not saturate: the result is whatever the target's conversion instruction gives (on x86_64, 1.0e18 as i32 is i32 min and -1.0 as u8 is 255)Clamp the value into the target's range before the cast
x as bf16 on an f16, and x as f16 on a bf16Fail in the C++ stepGo through f32: x as f32 as bf16
x as f16 on an i128 or u128, x as i128 / as u128 on an f16, and x as bf16 on an f80Fail to link: the runtime's conversion routines for these pairs are missingGo through f32 (or i64 for an in-range value)
An integer literal where a byte is expected (var b: byte = 0xFF)Rejected (SC028E "cannot convert 'i32' to 'byte'")0xFFu8 as byte
A char literal outside ASCII ('漢', 'é')Fails in the C++ stepBuild it from its code point: 0x6F22u32 as char

Checked by C++, Not by Kairo

An assignment to a const variable (const x: i32? = 42 then x = null, or to a static const / thread const) is reported by the C++ compiler at build time rather than as a Kairo diagnostic: --type-check-only does not report it. One case is caught by neither: an assignment to a top-level const of a script whose initializer is not a constant (const LIMIT = load() then LIMIT = 6) compiles and runs.


Not Diagnosed

Passing a slice literal to a function that stores the slice is not caught. The literal's storage ends with the statement, so the stored slice dangles. This becomes an error when Tether lands; until then, pass a var or a [T] to any function that keeps what it is given. See Primitives.

Assigning to a const binding introduced by a match pattern (case .Error(const message)) is not reported. See Enums.

Declaring var x: T with no initializer in a generic body is not reported when no bound requires a default constructor. See Generic Bodies.

Calling a method that a generic class inherits from a generic base with an argument of the wrong type (Const<i32>().put(x) where put(self, v: T) is declared in Source<T> and Const<T> derives it) is not reported by Kairo: the base's T is taken from the argument, and the C++ step rejects the call. A method the class declares itself is checked against the instance's type.

A class or struct that declares impl I but does not conform to I (a missing method, a missing associated type, or two interfaces it declares giving one inherited associated type different defaults) is not reported at its declaration, only where the conformance is used (passing it to a T impl I). See Conformance.

Four extend rules are not diagnosed yet:

  • A plain extend outside the type's file, or an extend ... impl in neither the type's nor the interface's file, is accepted. See Same-File Restriction.
  • Calling an extension member whose bounds the type arguments do not satisfy is not reported at the call site. See Generic Extends.
  • An extend ... impl that is missing interface methods is not reported at the extend declaration. See Interface Conformance.
  • An extension member is only meant to be visible in files that import the extension's file, directly or through other imports. A generic bound checks this (a type that meets T impl I only through an extension the file does not import is an error there), but a plain member call does not: m.scale() can find an extension the file never imports, depending on the order files are compiled in.

A generator that outlives the object it was created from, or an @inout argument it was given, is not caught; it dangles. This becomes an error when Tether covers generators. Until then, do not keep a generator longer than the object it came from.

A closure that captures a local by pointer (|*x|, |*|) and outlives it, by being returned or stored, is not caught; the pointer dangles. This becomes an error when Tether lands. Until then, capture by copy in a closure that leaves its scope.


Not in This Release

  • Tether, Kairo's memory-safety system, is not part of this release. Using a moved-from value is already a compile error; the rest of Tether's checks come later.
  • Some generic code is checked less strictly than it will be in 1.0.
  • The standard library is minimal: printing goes through std::io::println.
  • A program cannot read its command-line arguments yet: main takes no parameters, and sys::args (for scripts and programs alike) is planned.
  • Objective-C headers cannot be imported with ffi yet; C and C++ headers can. Plain C and Objective-C files cannot #include a .k file yet; C++ and Objective-C++ files can.

Language Gaps

  • A string literal passes to a C const char* parameter as it is, and any other Kairo string through s.c_str(). See C & C++ Interoperability.