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
import std::Parallel
import std::Error::DimensionError
import std::Interfaces::Numeric
fn <T impl Numeric> dot(a: [T], b: [T]) -> T {
if a.length() != b.length() {
panic DimensionError("dot requires equal lengths")
}
var result = Parallel::reduce(
policy: Parallel::Policy::SIMD,
range: 0..(a.length()),
init: T(),
step: fn (i: usize) -> T = a[i] * b[i],
combine: fn (x: T, y: T) -> T = x + y
)
return result
}
test "dot product of two vectors" {
var a, b = [1, 2, 3, 4, 5], [6, 7, 8, 9, 10]
var result = dot(a, b)
assert result == 130, f"expected 130, got {result}"
} Project stats
$ kairo stats --repo kairolang/kairo
- GitHub Stars
- 385+
- Weekly Downloads
- 55+
- Contributors
- 5
- Forks
- 4
01 - Code
See it in action. Clean syntax. No magic.
hello.k
import std // import only what you need
// No main() required top-level code runs immediately
// or is callable when imported as a module.
const name = "Kairo"
var count = 0
for i in 0..5 {
std::print(f"Hello, {name}! Count is {count}.")
count++
}
std::println(f"Ran {count} times.") ffi.k
// Call C++ code directly from Kairo with perfect ABI compatibility.
// my_cpp_lib.hpp contains:
//
// int add(int a, int b) { return a + b; }
// namespace my_cpp_lib {
// std::string greet(std::string name) { return "Hello, " + name + "!"; }
// }
//
// C++ namespaces map directly to Kairo module syntax.
// Functions with no namespace are callable without qualification.
ffi "c++" import "my_cpp_lib.hpp" // import a C++ header directly as a module.
// no wrapper generation, no binding step.
const sum = add(3, 4) // direct call, zero overhead.
var greeting = my_cpp_lib::greet("Kairo")
greeting += " Welcome to seamless interop!" // C++ std::string, manipulated
// directly in Kairo, no conversion.
std::println(greeting) // Hello, Kairo! Welcome to seamless interop! kairo_ffi.cpp
// Call Kairo functions directly from C++ with full ABI compatibility.
// KCC (Kairo C++ Compiler) is the tool that allows you to include Kairo modules directly in C++.
// `kcc kairo_ffi.cpp` - kcc is a drop-in clang-compatible driver that can include Kairo files.
// Without -cc, use `kairo kairo_ffi.k -c -o kairo_ffi -xc++ -header kairo_ffi.hh` to generate C++ or C headers
// for shipping a Kairo library to C++ consumers.
//
// kairo_ffi.k:
// import std
// fn add(a: i32, b: i32) -> i32 { return a + b }
// fn greet(name: string) -> string { return f"Hello, {name}!" }
//
// Kairo wraps the module under its filename as the exposed C++ namespace.
#include "kairo_ffi.k" // include the Kairo module directly.
#include <iostream>
int main() {
int sum = kairo_ffi::add(3, 4); // direct call, no overhead.
std::string greeting = kairo_ffi::greet("C++");
greeting += " Welcome to seamless interop!";
std::cout << greeting << std::endl; // Hello, C++! Welcome to seamless interop!
return 0;
} memory.k
import std
struct Buffer {
const size: usize
var data: *byte // safe pointer to a heap allocation
fn op new (self, size: usize) {
self.data = std::Memory::alloc(size)
self.size = size // only the constructor can set size.
}
fn op delete (self) {
std::Memory::free(self.data)
// compiler hard errors, if we do not free the memory.
// Even though the compiler proves and has enough information
// to add the free automatically, it does not, for explicitness
// and code clarity.
}
fn op [](self, i: usize) panic -> *u8 {
if i >= self.size {
var x = 0
return &x // since this is a stack pointer, in debug builds,
// Tether changes the decl of x to a unique or shared
// pointer, so theres no undefined behavior.
// In release builds, hard error with a fix:
// var *x = std::Unique<u8>(0)
}
return self.data + i
}
}
var buf = Buffer(1024) // 1KB on the heap.
// destructor runs automatically when buf goes out of scope.
buf[0] = 42
errors.k
import std
import json // imagine this is a complex JSON library
@json::Serializable
struct Config {
var host: string;
var port: u16;
var workers: u32 = 4;
}
fn <T impl json::Serializable> json_parse(raw: string) panic -> T;
// imagine this has a complex implementation that can panic with Json::Error::Parse
fn read_file(path: string) panic -> string;
// imagine this has a complex implementation that can panic with Fs::Error::NotFound,
// or Fs::Error::PermissionDenied
fn parse_config(path: string) panic -> Config {
var raw: string
raw = read_file(path)
return json_parse(raw)
catch _: Fs::Error::NotFound {
std::println(f"Config file not found: {path}")
return Config { host: "localhost", port: 8080 } // recover with a default.
} catch e: Json::Error::Parse {
std::println(f"Failed to parse config with error: {e}")
return Config { host: "localhost", port: 8080 } // recover with a default.
} catch e { // We don't care about Fs::Error::PermissionDenied thats the
// caller's problem, if we don't catch all possible panics,
// the compiler will error, we can catch it and re-panic.
panic e // propagate unexpected errors without wrapping or losing context.
}
}
fn start_server(path: string) -> Config {
try {
const config = parse_config(path)
// parse_config is marked `panic`, so we must catch its errors that
// can reach this scope (in this case, Fs::Error::PermissionDenied).
std::println(f"Starting {config.host}:{config.port} {config.workers}workers...")
return config
} catch _: Fs::Error::PermissionDenied {
std::println(f"Permission denied when reading config file: {path}")
return Config { host: "localhost", port: 8080 } // recover with a default.
} // every reachable panic is handled, no panic is needed in this function.
} // callers of start_server need no error handling at all. Simple, clean syntax
- Type inference Write
var count = 0the compiler knows it's an integer. Annotate when you want to, not because you have to. - String interpolation
f""strings let you embed expressions directly. No concatenation, no format specifiers. - Top-level code Skip the
main()boilerplate. Code at the top level runs on execution and can still be imported as a module.
Drop in, not rewrite
- No binding step Point Kairo at a C++ header and call it. Full type and ABI compatibility, no generated wrappers, no intermediate layer.
- Namespaces just work C++ namespaces map to Kairo module syntax automatically. 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, your Kairo module is includable directly from C++. Only one change to your build systems default c++ compiler. - Full ABI compatibility Kairo functions are callable from C++ with perfect type fidelity. The module is exposed under its filename as a C++ namespace, no surprises.
- Ship Kairo as a C++ library Use
kairo -cto compile into a lib andkairo -xc++ -headerto generate C++ headers. Consumers see a normal library.
Control without the footguns
- Manual allocation You decide what goes on the heap. No GC, no hidden allocations, no surprises at runtime.
- Automatic cleanup Kairo tracks ownership and places the destructor exactly where
bufgoes out of scope, or earlier if the function returns or panics. No leaks, no double frees, no annotations required. - Opt out when you need to Safety features can be disabled per-module or per-type. When you need raw performance or bare-metal access, you're not fighting the language to get it.
The compiler knows what can fail
-
panicon the signature Mark a functionpanicand the compiler traces every failure reachable in its entire call graph. Nothing hidden, nothing implicit. - Exhaustive catch blocks Catch blocks work like a match on an enum. Handle every reachable panic type and the compiler clears your function's signature. Your callers inherit the guarantee automatically, no annotations, no propagation to think about.
- 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 tells you. Catch blocks stay honest as code changes over time.
02 - Install
Install Kairo
Alpha Stage 1 is written in Kairo and built by Stage 0. APIs may change.
Full Install Guide ~/kairo - sh
Quick install
curl -fsSL https://kairolang.org/install.sh | bash From source
git clone https://github.com/kairolang/kairo.git && cd kairo-lang && xmake build -j8 PowerShell
irm https://kairolang.org/install.ps1 | iex From source
git clone https://github.com/kairolang/kairo.git cd kairo-lang && xmake build -j8 Clone
git clone https://github.com/kairolang/kairo.git Build
cd kairo-lang && xmake build -j8 -v Test
xmake run tests 03 - Get Started
The source
is open.
Compiler, standard library, and toolchain are on GitHub. Apache 2.0 with runtime exception.