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,threadexpressions) are not compiled. - Compile-time evaluation of function calls: an
evalconstant 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
| Feature | Status | Meanwhile |
|---|---|---|
Initializing a const field or a field without a default in a constructor | Compiles, 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 it | Move 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 SC116E | Declare 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 SC116E | Repeat the value instead of reading the field |
Struct destructuring (var {a, b} = s) | Rejected with SC116E | Read the fields one by one: var a = s.a |
| Functions, classes and enums declared inside a function body | Compile. Not yet (SC116E): one that uses a generic parameter of the function (or class) around it, and an extend inside a function body | Declare 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 statements | Put the code in a function and call it from the script |
for ... in over a Kairo string | Rejected with SC116E: the library string does not declare its op in yet. Vectors, sets and maps work | Loop 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 body | Rejected with SC116E | Use an eval for in a function body |
An eval for in fn main() that has no return type | Fails 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 parameter | Rejected with SC116E | Return 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 selection | Use overloads |
Vectors, sets, maps and f-strings at compile time (eval V: [i32] = [1, 2], f"n={n}" in an eval fn) | Rejected with SC116E | Use 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 body | Declare 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 SC116E | Use 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 considered | Pass 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 lookup | Not 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 way | Qualify 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 work | Loop 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 value | Use 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) work | Without a location, log ::builtin::type_id<E> |
Variadic enums (enum <T, ...E>, pack-expanded variants V { ... }...) and generic variadic functions | Compile: 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 SC116E | Pass 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 works | Initialize the variable from a function, or with a constant |
n as E? from an integer into a nullable plain enum | Fails in the C++ compiler | Assign 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 yet | Pass it on to C or C++ code to print it |
@unwind on a closure | Rejected 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 SC116E | Write @create T(args), or call create<T>(...) |
where guards on functions | Compile 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 redeclared | Test the condition with an if inside one function |
typeof | Compiles 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 type | Write 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 program | Use 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 them | Select 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 pointer | Name 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 NaN | Compiles, 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 bf16 | Fail in the C++ step | Go 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 f80 | Fail to link: the runtime's conversion routines for these pairs are missing | Go 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++ step | Build 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
extendoutside the type's file, or anextend ... implin 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 ... implthat is missing interface methods is not reported at theextenddeclaration. 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 Ionly 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:
maintakes no parameters, andsys::args(for scripts and programs alike) is planned. - Objective-C headers cannot be imported with
ffiyet; C and C++ headers can. Plain C and Objective-C files cannot#includea.kfile 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 Kairostringthroughs.c_str(). See C & C++ Interoperability.