v0.1.0-alpha open source systems language
Kairo: The Language Beyond C++
C++ can #include a .k file. Kairo can call any C++ library
with zero bindings. The boundary doesn't exist.
stage(0) compiler: C++
stage(1) self-hosting: in progress
kairo --version
kairo v0.1.0-alpha
stage(0) · bootstrap compiler · C++
transpiles Kairo -> C++ · no LLVM
status: frozen, kept to build stage(1)
exit 0
stage(1) · self-hosted compiler · Kairo
builds with stage(0) + patched LLVM
bar is illustrative, not a measurement
next: stage(1.5) -> stage(2) · Tether
-> /docs/status/ what isn't compiled yet
01 - Code
See it in action. Clean syntax. No magic.
hello.k
import std::io::println // import only what you need
// No main() required: a file with top-level code is a script, run top to bottom.
const name = "Kairo"
var count = 0
for i in 0..5 {
println(f"Hello, {name}! {i} squared is {i * i}.")
count++
}
println(f"Ran {count} times.") ffi.k
// Call C++ directly from Kairo: no bindings, no wrappers, no glue code.
// my_cpp_lib.hpp contains:
// inline int add(int a, int b) { return a + b; }
// namespace my_cpp_lib {
// struct Vec2 { double x, y; double dot(Vec2 o) const { return x * o.x + y * o.y; } };
// template <class T> T clamp(T v, T lo, T hi) { return v < lo ? lo : hi < v ? hi : v; }
// }
// C++ namespaces map directly to Kairo module syntax; add() has none, so it's unqualified.
import std::io::println
ffi "c++" import "my_cpp_lib.hpp" // the bundled clang parses it: classes, templates, all of it.
ffi "c++" import "vector" // the C++ standard library is just another header.
var points: cxx::std::vector<my_cpp_lib::Vec2> // a C++ template, instantiated from Kairo.
points.push_back(my_cpp_lib::Vec2 { x: 3.0, y: 4.0 })
points.push_back(my_cpp_lib::Vec2 { x: 6.0, y: 8.0 })
var total = 0
for p in points { // C++ iterators drive a Kairo for loop.
total = add(total, my_cpp_lib::clamp(p.dot(p) as i32, 0, 50)) // clamp<int>, deduced.
}
println(f"{points.size()} points, total {total}") // 2 points, total 75 kairo_ffi.cpp
// Include a Kairo module straight from C++ with kcc, a drop-in clang-compatible driver:
// `kcc kairo_ffi.cpp -o app` finds kairo_ffi.k, compiles it and links it in.
// Shipping a library? `kairo kairo_ffi.k -c --emit-headers include/` writes the headers.
// kairo_ffi.k:
// pub struct Vec2 { var x: f64; var y: f64 }
// pub fn dot(a: Vec2, b: Vec2) -> f64 { return a.x * b.x + a.y * b.y }
// pub struct Overdrawn { var short_by: i64 }
// pub fn withdraw(balance: i64, amount: i64) panic -> i64 {
// if amount > balance { panic Overdrawn { short_by: amount - balance } }
// return balance - amount
// }
// The module's C++ namespace is its file name: kairo_ffi.
#include "kairo_ffi.k"
#include <iostream>
int main() {
kairo_ffi::Vec2 v{3.0, 4.0}; // a Kairo struct is a plain C++ aggregate.
std::cout << kairo_ffi::dot(v, v) << std::endl; // 25
try { // a Kairo panic is a value; value_or_throw turns it into a C++ exception.
auto left = builtin::value_or_throw(kairo_ffi::withdraw(100, 30)); // 70
builtin::value_or_throw(kairo_ffi::withdraw(left, 500));
} catch (const builtin::panic_error<kairo_ffi::Overdrawn>& e) {
std::cout << "short by " << e.error.short_by << std::endl; // 430
}
} memory.k
import std::io::println
import std::System
// The whole program's heap is yours: five static functions, no vtable, no indirection.
// Every allocation in the program, the standard library's included, comes here.
class Counting {
static live: i64 = 0
static fn alloc(n: usize, a: usize) -> unsafe *u8 { Self::live += 1; return System::alloc(n, a) }
static fn free(p: unsafe *u8, n: usize, a: usize) { Self::live -= 1; System::free(p, n, a) }
static fn resize(p: unsafe *u8, o: usize, n: usize, a: usize) -> bool { return System::resize(p, o, n, a) }
static fn realloc(p: unsafe *u8, o: usize, n: usize, a: usize) -> unsafe *u8 { return System::realloc(p, o, n, a) }
static fn alloc_zeroed(n: usize, a: usize) -> unsafe *u8 { Self::live += 1; return System::alloc_zeroed(n, a) }
}
@set_global_allocator(Counting) // System is the default heap: the bundled mimalloc.
class Node { var value: i32; fn Node(self, v: i32) { self.value = v } }
class Owner {
var node: *Node
fn Owner(self, v: i32) { self.node = @create Node(v) } // on the heap, because you said so.
@copy fn Owner(self, other: Self) = delete // one owner, so no double free.
fn op delete(self) { @destroy self.node } // the destructor.
}
fn work() {
var a = Owner(1)
var b = Owner(2)
// var c = a // compile error: Owner cannot be copied.
println(f"inside: {Counting::live} live allocations") // inside: 2 live allocations
} // b, then a: destructors run here, on every path out of the scope.
work()
println(f"after: {Counting::live} live allocations") // after: 0 live allocations errors.k
import std::io::println
struct InUse { var port: i32 } // any type can be panicked:
struct Denied { var port: i32 } // no base class, no exception hierarchy.
struct BadPort { var port: i32 }
struct Socket { var port: i32 }
// A declared panic set is a contract: panicking anything else is a compile error.
fn open(port: i32) panic(InUse, Denied) -> Socket {
if port < 1024 { panic Denied { port: port } } // privileged
if port == 8080 { panic InUse { port: port } } // already listening
return Socket { port: port }
}
fn listen(port: i32) panic -> Socket { // set inferred: Denied, BadPort.
if port > 65535 { panic BadPort { port: port } }
try {
return open(port)
} catch e: InUse {
println(f"{e.port} is already ours, reusing it")
return Socket { port: e.port }
} catch e { // Denied is the caller's problem: re-panic it, original site intact.
panic e
}
}
// No `panic` here: every error listen() can raise is handled below.
// Delete either arm and this function no longer compiles; add a `catch e: InUse`
// and the compiler flags it as dead: InUse can never reach this scope.
fn serve(port: i32) -> Socket {
try {
return listen(port)
} catch e: Denied {
println(f"{e.port} needs root, using 3000")
} catch e: BadPort {
println(f"{e.port} is not a port, using 3000")
}
return Socket { port: 3000 }
}
for port in [8080, 80, 99999, 4000] {
const s = serve(port) // callers of serve() need no error handling at all.
println(f"listening on {s.port}")
} Simple, clean syntax
- Type inference Write
var count = 0and the compiler knows it's an integer. Annotate when you want to, not because you have to. - String interpolation
f""strings embed any expression directly,{i * i}included. No concatenation, no format specifiers. - Top-level code Skip the
main()boilerplate. A file with top-level statements is a script: they run in order, as the program.
Drop in, not rewrite
- No binding step Point Kairo at a C++ header and call it. Classes, methods and templates arrive as native Kairo symbols, with no generated wrappers and no intermediate layer.
- Namespaces and templates just work C++ namespaces map to Kairo module syntax, and
cxx::std::vector<my_cpp_lib::Vec2>instantiates a C++ template from Kairo. Your existing library surface stays exactly as you designed it. - Migrate at your own pace Move files to Kairo one at a time while the rest of your codebase stays in C++. There's no cutover point, no rewrite cliff.
Adopt Kairo incrementally
- Include like any header With
kcc, a Kairo module is#include-able directly from C++. The only change to your build is the C++ compiler it calls. - Native C++ types Kairo structs are plain C++ aggregates and functions are ordinary C++ calls, under a namespace named after the file. A panic is a value;
builtin::value_or_throwturns it into a C++ exception when you want one. - Ship Kairo as a C++ library Use
kairo -cto compile the module and--emit-headersto generate its C++ headers. Consumers see a normal library.
Control without the footguns
- Manual allocation You decide what goes on the heap:
@createand@destroy, no GC, no hidden allocations. The default heap is the bundled mimalloc. - Your allocator, everywhere
@set_global_allocatorswaps the heap for the whole program, the standard library included. An allocator is five static functions, with no vtable. - Automatic cleanup Destructors run where each value goes out of scope, in reverse order, on every path out. Delete the copy constructor and an accidental copy, the classic double free, is a compile error.
The compiler knows what can fail
-
panicon the signature Mark a functionpanicand the compiler infers its panic set from everything it can reach. Declare the set,panic(InUse, Denied), and it becomes a contract the compiler enforces. - Exhaustive catch blocks Catch blocks work like a match on an enum. Handle every reachable panic type and the function needs no
panicat all, so its callers need no error handling. - Propagate what you don't own Use
catch e { panic e }to handle what you care about and push the rest upstream. The original panic travels intact, no wrapping, no lost context. - Dead catches are flagged too If a catch block handles a panic type that can't actually reach that scope, the compiler warns and offers to remove it. Catch blocks stay honest as code changes over time.
02 - Install
Install Kairo
Alpha 0.2.0: the compiler is written in Kairo and builds itself. APIs may change.
Full Install Guide ~/kairo - sh
Quick install
curl -fsSL https://kairolang.org/install.sh | sh From source (needs a 0.2.0+ release on PATH as the seed)
git clone https://github.com/kairolang/kairo.git && cd kairo git submodule update --init --recursive python3 build.py --release kairo kcc kld kfmt PowerShell
irm https://kairolang.org/install.ps1 | iex Or build from source (needs a 0.2.0+ release on PATH as the seed)
git clone https://github.com/kairolang/kairo.git cd kairo git submodule update --init --recursive python3 build.py --release kairo kcc kld kfmt Seed: unpack a 0.2.0+ release, put its bin/ on PATH
export PATH="/path/to/kairo-0.2.0/bin:$PATH" Clone
git clone https://github.com/kairolang/kairo.git && cd kairo git submodule update --init --recursive Build
python3 build.py --release kairo kcc kld kfmt Test
build/llvm/bin/llvm-lit Tests -s 03 - Get Started
The source
is open.
Compiler, standard library, and toolchain are on GitHub. Apache 2.0 with runtime exception.