The kardashev Documentation
kardashev is a Rust-flavored systems programming language whose signature
feature is lightweight effect labels in the type system: every function
declares which side effects it can produce (io, alloc, panic, async,
unwind) as part of its signature, and the compiler tracks them across call
chains — pure type-system information, with zero runtime cost. It compiles to
native code through LLVM (an ORC JIT for the REPL and an AOT path for
executables), with Rust-style ownership + non-lexical-lifetime borrow checking.
This site is the reference documentation. It is built from the Markdown in the
docs/ directory
with mdBook and lives next to the
compiler, so each language change updates its docs in the same pull request.
Contents
- Language Reference — the surface syntax and
semantics: types, generics, traits +
impl, ADTs + pattern matching, ownership,Result+?, and the effect-row notation. - Effects System — the signature feature in depth: effect labels, effect-row polymorphism, and how effects propagate across calls.
- Standard Library — the built-in prelude:
Option/Result,Vec/String/HashMap/HashSet/Box, the trait +#[derive]machinery, combinators, and string utilities. - Compiler Architecture — the pipeline from lexer through type/borrow/effect checking to LLVM codegen, monomorphization, and the JIT/AOT backends.
For the project roadmap, build instructions, and source, see the repository README.
kardashev Language Reference
The surface language as it compiles today. ../README.md
and ../ROADMAP.md are the authoritative record of everything
that has shipped; this document is the practical reference for the core
constructs and operators. Every snippet here compiles under kardc as written.
Features still on the runway are not described here.
A note on honesty: kardashev has a handful of deliberate surface limitations
(an if used as a value needs an else; a &mut parameter is not
auto-reborrowed through recursive calls; strings carry no NUL terminator).
These are real, they are called out below, and the examples obey them — they
are not hidden. Earlier drafts of this reference also listed &&/||, %,
& of a temporary, and enum-typed struct fields as missing; all four now work
(Phases 33, 36, 124, 125) and the Surface limitations section records the
correction.
Lexical structure
| Token | Notes |
|---|---|
| Identifier | [A-Za-z_][A-Za-z0-9_]* |
| Integer literal | [0-9]+ |
| Boolean literal | true / false |
| String literal | "..." with \n \t \r \\ \" escapes |
| Operators | + - * / % < <= > >= == != = -> => ? ! && || & | ^ << >> |
| Punctuation | ( ) { } [ ] , ; : :: . _ & .. ..= |
| Keywords | fn let if else return struct enum match trait impl for |
mod pub const extern while loop break continue true false |
% (modulo), && and || (short-circuit boolean operators), and the integer
bitwise operators (& | ^ << >>) are all supported. || is disambiguated
positionally: after an operand it is logical-or, while at the head of an
expression || expr it is still a zero-parameter closure. && binds tighter
than ||, and both bind looser than the comparisons.
async, await, mut, dyn, where, and self are recognized
positionally rather than as reserved words, so they can also appear as plain
identifiers (e.g. inside effect rows or as generic parameter names).
Comments are // to end of line. No block comments.
Functions
#![allow(unused)] fn main() { fn add(a: i64, b: i64) -> i64 { a + b } // pure: no effect row fn log_add(a: i64, b: i64) -> i64 ! { io } { // declares the io effect print(a + b); a + b } }
A function declares parameters, a -> ReturnType, and an optional effect
row ! { ... } after the return type. Omitting the row means pure (the
empty row). The last expression of the block is the return value; return e;
is also available. Callers must declare every effect they transitively use —
a pure function calling log_add is a type error. The effect row may carry a
row variable to be effect-polymorphic:
#![allow(unused)] fn main() { fn map<T, U, e>(xs: Vec<T>, f: fn(T) -> U ! {e}) -> Vec<U> ! { e, alloc } { let mut out = vec_new(); for x in xs { vec_push(&mut out, f(x)); } out } }
The built-in effect labels are pure (empty), alloc, io, panic,
async, and unwind. See effects.md.
let / let mut / assignment
#![allow(unused)] fn main() { let x = 5; // immutable binding let mut n = 0; // mutable binding n = n + 1; // assignment (only to a `let mut`) let y: i64 = 7; // optional type annotation }
A plain let binding cannot be reassigned. let mut permits later name = expr; assignment. Tuple-destructuring let does not bind mut — dest
into immutable names, then seed a let mut if you need to mutate (the calc
capstone does exactly this when folding).
if / else — if is an expression and REQUIRES an else
if is an expression, and the else arm is mandatory. A bare if c { ... } with no else is a parse error. Either supply both arms, or write an
explicit empty else else {} when you only want a side effect:
#![allow(unused)] fn main() { let m = if a > b { a } else { b }; // if as a value if done { cleanup(); } else {} // side-effect-only: empty else REQUIRED }
else if ladders are supported and are the idiomatic substitute for the
missing match on integers:
#![allow(unused)] fn main() { fn sign(x: i64) -> i64 { if x > 0 { 1 } else if x < 0 { 0 - 1 } else { 0 } } }
Unary -x (negation) and !x (logical not on bool) are available; both
bind tighter than the binary operators. (For a negative literal you can also
write 0 - 7, which the capstones use.)
Operator precedence, low to high: comparisons (< <= > >= == !=), then
additive (+ -), then multiplicative (* /), then unary (- !), then
postfix (.field, .method(..), [i], ?, .await).
match + patterns
#![allow(unused)] fn main() { enum Color { Red, Green, Blue } fn code(c: Color) -> i64 { match c { Red => 0, Green => 1, Blue => 2, } } }
match binds variant payloads and supports a _ wildcard:
#![allow(unused)] fn main() { fn unwrap_or(o: Option<i64>, d: i64) -> i64 { match o { Some(x) => x, // binds the payload as `x` None => d, } } }
Arm bodies may be a single expression or a { ... } block; arms are
comma-separated. Matching is exhaustive (a _ arm covers the rest). Patterns
match enum constructors, literals, bindings, and _. (Tuple patterns inside a
match are not supported — destructure a tuple with let (a, b) = t;
instead.)
Enums and structs
#![allow(unused)] fn main() { enum Shape { Circle(i64), Rect(i64, i64), Unit } // variants, some with payloads struct Point { x: i64, y: i64 } // named fields }
Construct a struct with a struct literal; read fields with . (which
auto-derefs through &T / &mut T):
fn main() -> i64 { let p = Point { x: 3, y: 4 }; p.x + p.y // 7 }
Enums are generic (enum Option<T> { Some(T), None }) and so are structs
(struct Pair<A, B> { first: A, second: B }).
A struct field may itself be enum-typed (struct Holder { t: Tree, tag: i64 }
where Tree is an enum). Read such a field by reference and match on it —
match &h.t { ... } (see smoke_test_phase36). The older i64-code idiom
(store a code in the field, lift it to the enum at the boundary via a small
function) still appears in the calc and kdlex capstones and remains valid:
#![allow(unused)] fn main() { const K_NUM: i64 = 0; const K_PLUS: i64 = 1; struct Tok { kind: i64, val: i64 } // i64 code; a TokKind field also works enum TokKind { Num, Plus, Other } fn kind_of(k: i64) -> TokKind { if k == K_NUM { TokKind::Num } else if k == K_PLUS { TokKind::Plus } else { TokKind::Other } } }
Traits and impls
A trait declares method signatures; impl Trait for Type supplies them.
impl Type { ... } (no for) defines inherent methods on a type. Both
resolve through the same method table; receivers autoref as &self /
&mut self.
#![allow(unused)] fn main() { trait Show { fn show(&self) -> i64; } struct Point { x: i64, y: i64 } impl Show for Point { fn show(&self) -> i64 { self.x + self.y } } // trait impl impl Point { fn origin_dist(&self) -> i64 { self.x + self.y } } // inherent impl }
Generics with bounds
A generic parameter may carry a single trait bound, or multiple bounds with
+ (Phase 28):
#![allow(unused)] fn main() { fn use_show<T: Show>(t: T) -> i64 { t.show() } // single bound fn keyed<K: Hash + Eq>(k: K) -> i64 { k.hash() } // multiple bounds: K: A + B }
where clauses are accepted and desugar to the inline-bound form (identical
downstream):
#![allow(unused)] fn main() { fn head<T, C: Container<T>>(c: C) -> i64 where T: Show { c.first().show() } }
Associated types
A trait may declare an associated type with type Item;, referenced as
Self::Item in the trait and C::Item at a bounded call site:
#![allow(unused)] fn main() { trait Container { type Item; fn first(&self) -> Self::Item; } }
Trait objects: dyn Trait and Box<dyn Trait>
A trait object is a {data, vtable} fat pointer. A single call site through
&dyn Trait (or Box<dyn Trait>) dispatches to multiple runtime impls via
the vtable. Object safety is enforced (a trait with a static, no-self method
is rejected as a dyn):
#![allow(unused)] fn main() { trait Shape { fn area(&self) -> i64; } impl Shape for Sq { fn area(&self) -> i64 { self.side * self.side } } impl Shape for Rect { fn area(&self) -> i64 { self.w * self.h } } fn describe(s: &dyn Shape) -> i64 { s.area() } // one site, many impls }
Box<dyn Shape> holds a heap-owned trait object; .area() dispatches the same
way. (Static dispatch via <T: Shape> is an unchanged separate path —
monomorphized, no vtable.)
Closures and function-value types
A closure is |params| body; it may capture surrounding bindings (lowered to
a heap env + a uniform fat-pointer fn-value):
fn main() -> i64 { let n = 10; let add_n = |x| x + n; // captures n by value add_n(5) // 15 }
A function-value type carries its own effect row:
fn(T) -> U ! {e}. The row variable e makes a higher-order function
effect-polymorphic — calling it with a pure closure stays pure, with an io
closure carries io:
#![allow(unused)] fn main() { fn apply(f: fn(i64) -> i64 ! {e}, x: i64) -> i64 ! {e} { f(x) } }
async fn / .await
An async fn returns the built-in Future<T>; .await suspends until the
future is ready and unwraps it. Awaiting carries the async effect.
async fn add(a: i64, b: i64) -> i64 { a + b } async fn double(n: i64) -> i64 { add(n, n).await } fn main() -> i64 ! { async, io } { print(block_on(double(21))); // drives the future to completion -> 42 0 }
A real single-threaded executor backs this: spawn(f) enqueues a task
(returns an i64 handle), block_on(f) / join(h) drive the queue, yield_now
suspends once, and sleep_ms(n) is a real CLOCK_MONOTONIC timer leaf (the
reactor sleeps rather than hot-spins when all tasks are pending). Linux/epoll
fd-readiness ships (pipe_make/pipe_send/read_pipe); macOS/kqueue
fd-readiness is a documented deferral (timers work cross-platform).
Documented leak: async-frame interior values are not yet freed on frame teardown (no use-after-free — it leaks). See README Phase 16/29.
Arrays, tuples, and destructuring
Fixed-size arrays [T; N] are stack value-aggregates; tuples (A, B, ...) are
anonymous structs:
fn sum3(a: [i64; 3]) -> i64 { a[0] + a[1] + a[2] } // array param + indexing fn main() -> i64 { let mut a = [10, 20, 30]; // array literal a[1] = 99; // element assignment let t = (1, 2); // tuple literal let (x, y) = t; // tuple destructuring (binds immutable) a[0] + t.0 + t.1 + x + y // .0 / .1 field access }
A dynamic out-of-bounds array index panics (Phase 23). (Tuple match
patterns and non-Copy array elements are not supported — destructure tuples
with let.)
const / const fn / const-generic array lengths
const LIMIT: i64 = 5; // i64/bool const, folded at every use const fn sq(n: i64) -> i64 { n * n } // runs at compile time in a const context fn main() -> i64 { let a: [i64; sq(2)] = [0, 0, 0, 0]; // const-generic length: N = sq(2) = 4 a[0] + LIMIT }
A const item is evaluated at compile time and folded to a literal at each
use (verifiable in --emit-llvm: no runtime load). A const fn runs at
compile time when called in a const context with constant args, and is also
an ordinary runtime function. The array length N in [T; N] may be any
const-expression — a const item, a const fn call, or arithmetic over them.
Integer overflow / div-by-zero in const evaluation are compile errors.
(const types are limited to i64 / bool; full const-generic type
parameters like struct Arr<const N: i64> are not in scope.)
extern "C" FFI
Declare an external C function with extern "C" fn name(args) -> T; (a block
form extern "C" { ... } also parses). It lowers to an unmangled LLVM extern +
a direct call; the JIT resolves it from the host process, AOT links via
clang. The spelling i32 maps to C int (trunc/sext at the boundary), and
&String / &[T] map to a C pointer. An extern call carries the io effect
unless the declaration gives it an explicit ! { } row.
extern "C" fn abs(x: i32) -> i32; extern "C" fn strlen(s: &String) -> i64; fn main() -> i64 ! { io } { let s = "hello"; abs(0 - 7) + strlen(&s) // 7 + 5 = 12 }
(Importing C is what ships; an export-to-C attribute is deferred.)
References, borrowing, and NLL
References are &T (shared) and &mut T (unique). The borrow checker is
Rust-style affine ownership with non-lexical lifetimes — a borrow is live
only up to its last use, so a value can be moved after its references go dead:
fn read(p: &Point) -> i64 { p.x + p.y } fn main() -> i64 { let p = Point { x: 3, y: 4 }; let r = &p; let a = read(r); // r's last use; the borrow is now dead let b = consume(p); // OK to move p now — NLL allows it a + b }
Limitation — no & of a literal or temporary. &"x" or &Foo { .. } is
rejected. Bind to a let first, then borrow the binding:
#![allow(unused)] fn main() { let s = "hello"; print_str(&s); // &s, not &"hello" }
Limitation — a &mut parameter is passed by move and is not
auto-reborrowed when threaded through recursive calls. Passing &mut v of a
local at a call site is fine (vec_push(&mut toks, t)); but you cannot thread
a &mut self-style parameter down a recursive descent. The calc capstone works
around this by threading its cursor + error through return tuples instead.
Drop / RAII
A type that implements trait Drop (and the built-in Vec / String /
HashMap / Box glue) is dropped deterministically at scope exit, in reverse
declaration order, driven by the NLL move analysis. Runtime drop flags ensure
a conditionally-moved value drops exactly once (no double-free, no
use-after-free). Moved or returned values are not dropped by the source scope.
A 2 M-iteration allocating loop runs in constant ~1.5 MB RSS.
struct Guard { id: i64 } impl Drop for Guard { fn drop(&mut self) -> i64 { print(self.id) } } fn main() -> i64 { let a = Guard { id: 1 }; let b = Guard { id: 2 }; 0 // at scope exit: b drops, then a (reverse order) }
(Closure-env and async-frame interior contents are a documented leak — no UAF; see README Phase 29.)
Built-in functions and prelude types
Auto-included prelude (a user definition of the same name suppresses the prelude one):
#![allow(unused)] fn main() { enum Option<T> { Some(T), None } enum Result<T, E> { Ok(T), Err(E) } trait Iterator<T> { fn next(&mut self) -> Option<T>; } // impl'd for the built-in Range trait Hash { fn hash(&self) -> i64; } // impls for i64, String trait Eq { fn eq(&self, other: &Self) -> bool; } // impls for i64, String }
Collections
| Type / function | Notes |
|---|---|
vec_new() -> Vec<T> ! { alloc } | empty growable buffer (per-T) |
vec_push(v: &mut Vec<T>, x: T) -> i64 ! { alloc } | append (may realloc) |
vec_get(v: &Vec<T>, i: i64) -> T | index; returns a shallow copy |
vec_len(v: &Vec<T>) -> i64 | element count |
string_new() -> String ! { alloc } | empty growable string |
string_push_str(s: &mut String, other: String) -> i64 | append (! { alloc }) |
hashmap_new() -> HashMap<K, V> ! { alloc } | K: Hash + Eq; i64 + String keys |
hashmap_insert(m: &mut HashMap<K,V>, k: K, v: V) -> i64 | ! { alloc } |
hashmap_get(m: &HashMap<K,V>, k: K) -> Option<V> | lookup |
hashmap_len(m: &HashMap<K,V>) -> i64 | entry count |
hashset_new() / _insert / _contains / _len | HashSet<T>, T: Hash + Eq |
String is { ptr, len, cap } with no NUL terminator. Vec<T> is heap
backed with a DataLayout-sized stride (works for i64, bool, structs,
enums). vec_get returns a shallow copy of the element.
Strings and I/O
| Function | Notes |
|---|---|
print(n: i64) -> i64 ! { io } | one integer + newline |
print_str(s: &String) -> i64 ! { io } | a string, no newline |
println(s: &String) -> i64 ! { io } | a string + newline |
print_no_nl(s: &String) -> i64 ! { io } | a string, no newline |
str_len(s: &String) -> i64 | byte length |
str_char_at(s: &String, i: i64) -> i64 | byte at i (0..255), or -1 past end |
str_eq(a: &String, b: &String) -> bool | byte-wise equality |
str_substring(s: &String, start: i64, len: i64) -> String ! { alloc } | owned byte slice |
int_to_string(n: i64) -> String ! { alloc } | decimal formatting |
File I/O + CLI args (Phase 30)
These are injected lazily — a program that never mentions them carries none of
the machinery. They return Result<_, IoError> and carry io:
fn main() -> i64 ! { io, alloc } { let path = "config.txt"; match fs_read_to_string(&path) { Ok(s) => str_len(&s), Err(e) => 0 - 1, } }
fs_read_to_string(&String) -> Result<String, IoError>,
fs_write(&String, &String) -> Result<i64, IoError>, fs_exists, and
args() -> Vec<String> are available. enum IoError { IoNotFound, IoPermissionDenied, IoOther }. Verified on Linux; macOS rides CI.
Concurrency
thread_spawn / thread_join run a kardashev fn-value on a real pthread;
Mutex (pthread-backed) guards shared state. Data-race floor (enforced):
thread_spawn rejects at compile time any closure that captures a binding by
reference — captures must be by value or shared via a Mutex handle. (Full
Send/Sync marker traits, channels, and atomics are deferred.)
Surface limitations (called out, not hidden)
These are real properties of the language today. Every snippet above obeys them.
ifrequires anelse. A bareif c { ... }is a parse error; useif c { ... } else { ... }orif c { ... } else {}.- A
&mutparameter is passed by move, not auto-reborrowed through recursive calls — thread state through return tuples instead.&mut localat a single call site is fine. - Strings have no NUL terminator (
{ ptr, len, cap });vec_getreturns a shallow copy.
Four items that earlier editions listed here have since shipped and are no
longer limitations: && / || short-circuit boolean operators (&& Phase
33, || Phase 124 — && binds tighter), % modulo (Phase 33), & of a
literal or temporary (Phase 125 — &5 and &Foo { .. } materialize a
statement-scoped, dropped slot), and enum-typed struct fields (Phase 36).
Modules
mod foo; at the top of a .kd file pulls in foo.kd from the same directory
and flat-merges its declarations (recursive, cycle-safe). pub fn gates
path-qualified references (foo::bar) across module boundaries; bare-name
references resolve through the flat merge.
// util.kd pub fn double(n: i64) -> i64 { n + n } // main.kd mod util; fn main() -> i64 { util::double(21) } // 42
A project may carry a kard.toml manifest with local-path [dependencies],
resolved by kard build / kard run. (Third-party dependency resolution via
the Bazel module registry is a documented deferral — it can't be verified in
this build environment, so it is intentionally not stubbed.)
Worked examples
examples/calc/— a recursive-descent arithmetic interpreter written in kardashev (enums +match, structs, tuples,Vec,const, recursion).examples/json/— a JSON parser into aHashMap<String, i64>(the numeric-object subset capstone: a top-level object of"key": integermembers; nested objects/arrays and string/bool/null values are out of scope).examples/kdlex/— a lexer for a kardashev subset.examples/rpn/— an RPN calculator.
See also
- Effects system —
! { io, alloc }semantics - Standard library — full builtin catalog
- Architecture — compiler pipeline
Effects System
kardashev's effect labels are an optional, lightweight typed side-channel.
Since v0.81.0 they are OPT-IN: write a ! { … } row only when you want the
compiler to prove a property about a function. For everyday error handling and
resource management, reach for Result + ? + ownership, not effects.
- A function with no effect row is unchecked — it may perform any
effect. (
fn greet() -> i64 { print(42) }compiles.) - A function with an explicit row (including
! { }, an asserted-pure) is strictly checked — it must declare every effect it performs, and the compiler tracks the row across the call graph. The inferred effect set is always computed and propagated to callers, so an annotated caller still sees an un-annotated callee's real effects.
Effects are pure type-system information with zero runtime cost — the emitted
LLVM IR is byte-for-byte identical whether a row is present or not. There are no
handlers or continuations for the built-in effects (user-defined
effect/perform/handle is a separate, advanced feature).
When to use a row
- To guarantee purity / IO-freedom / non-allocation — most usefully with the
#[codegen(no_alloc)]/#[codegen(no_panic)]/#[codegen(no_io)]contracts, which fail compilation if violated. - To document and enforce a public API's effect surface.
Error handling: use Result, not effects
fn read(ok: bool) -> Result<i64, MyErr> { if ok { Ok(10) } else { Err(MyErr::Bad) } } fn run() -> Result<i64, MyErr> { let v = read(true)?; Ok(v + 5) } // `?` propagates Err fn main() -> Result<(), MyErr> { run()?; Ok(()) } // Err -> non-zero exit
Modes
| flag | meaning |
|---|---|
--effects=opt-in | default — an absent row is unchecked |
--effects=strict | an absent row asserts purity (the pre-v0.81 rule) |
--effects=extended | also recognize the niche div label in explicit rows |
#[allow(missing_effect)] opts one function out of the strict-mode check. Run
kardc --explain effects for the consolidated summary.
Labels
The recognized labels are io, alloc, panic, async, unwind, and share
(the concurrency / thread-boundary effect, auto-inferred by thread_spawn /
channel ops). div (may-not-terminate) is gated behind --effects=extended.
Syntax
An effect row attaches after a function's return type, introduced by
!:
#![allow(unused)] fn main() { fn read_cfg() -> i64 ! { io, alloc } { ... } // explicit: strictly checked fn add(a: i64, b: i64) -> i64 { a + b } // no row: unchecked (here, pure) fn pure_add(a: i64, b: i64) -> i64 ! { } { a + b } // `! { }`: asserted pure }
The grammar is ! '{' label (',' label)* ','? '}'. An empty row
(! {}) or no row at all means pure: the function declares no
effects. pure is therefore the default, not a keyword you write.
Built-in labels
The five concrete effect labels are built in:
| Label | Meaning |
|---|---|
alloc | Heap allocation |
io | File / network / stdio / general syscalls |
panic | Unrecoverable failure (unwinds via panic(msg)) |
async | Yields to the scheduler |
unwind | Stack unwinding for cancellation (distinct from panic) |
pure is the empty row, not a sixth label. The five built-ins are
hard-coded; a user-declared effect form (e.g. effect Network;)
remains a future consideration.
Any other identifier in a row is an error unless it matches a generic parameter declared on the same fn — that reservation is what makes effect rows row-polymorphic (next section).
Row polymorphism
A function type carries an effect row, and that row can be a variable rather than a fixed set. Writing a generic parameter name inside the row makes the function effect-polymorphic: it inherits whatever effects its function-valued argument carries.
#![allow(unused)] fn main() { fn map<T, U, e>(xs: Vec<T>, f: fn(T) -> U ! {e}) -> Vec<U> ! { e, alloc } { let mut out = Vec::with_capacity(xs.len()); for x in xs { out.push(f(x)); } out } }
Here e is a row variable. map is pure when f is pure, and
propagates exactly the effects f introduces — its own declared row
unions e with the alloc it performs itself. The function-pointer
type fn(T) -> U ! {e} spells the effect row of the value it accepts,
so the row flows from the argument's type through to the caller's
obligation.
This is enforced, not cosmetic: a pure caller that passes an io
closure to a function expecting a pure one is rejected, and a caller of
map with an io f must itself declare io.
Propagation rule
For every function body, the typechecker collects the union of the
declared effect rows of everything it calls — direct calls (f(x)),
method calls (x.foo()), constructor calls (Some(7), which are
free), function-valued calls (f(x) where f is a closure or
fn-pointer parameter, contributing that value's row), and built-ins —
and verifies that union is a subset of the enclosing function's
declared row. Anything missing is diagnosed at the calling
function's definition site (not at runtime, and not at the call
site's caller):
$ kardc bad.kd
type error 2:1: function 'main' uses effect `io` but does not declare
it; add `! { io }` to the signature
So a pure function that calls an io function is a compile error:
#![allow(unused)] fn main() { fn raw_read() -> i64 ! { io } { 42 } fn use_it() -> i64 { raw_read() } // error: uses `io`, declares none }
Declaring the row makes it compile, and the effect keeps propagating
outward — every caller up the chain must declare io too (or sit
behind an effect-polymorphic boundary):
fn raw_read() -> i64 ! { io } { 42 } fn main() -> i64 ! { io } { raw_read() } // OK
The ? operator and .await do not introduce effects of their own;
the functions they operate on do, and those propagate through the
normal union. (async fn is the one implicit source — see below.)
Async as an effect
An async fn implicitly adds async to its own row, and a caller must
still opt in:
async fn fetch(n: i64) -> i64 { n + n } // ! { async } implicit fn main() -> i64 ! { async, io } { print(fetch(21).await); 0 }
Without ! { async } on main, the compiler reports the same
missing-effect diagnostic as any other undeclared effect. (async is
a fully real runtime now — a single-threaded executor with spawn /
join / block_on / sleep_ms and an epoll reactor on Linux — but
from the effect system's point of view it is just another label that
unions and checks like the rest.)
panic and catch
panic(msg) carries the panic effect: it prints to stderr and
unwinds (setjmp/longjmp), running Drop glue on the way out. So a
function that can panic must declare panic, and that propagates to
its callers like any other effect.
catch(f, recover) is the boundary: it runs f, and if f panics it
runs recover instead of letting the unwind escape. Because catch
contains the panic, it clears panic from the row — code wrapped
in catch does not force its caller to declare panic. This is the
effect system's analogue of Result-style recovery: a known-recovered
panic is no longer an effect the caller is obligated to acknowledge.
FFI carries io
An extern "C" call is opaque to the effect checker — kardashev cannot
see what the foreign function does — so every extern "C" call is
treated as carrying io. A function that calls into C must therefore
declare io:
#![allow(unused)] fn main() { extern "C" fn strlen(s: &String) -> i64; fn name_len() -> i64 ! { io } { // `io` required: extern call let n = strlen(&greeting); n } }
This is the conservative-but-honest choice: a C function might do anything, so the boundary is labeled with the broadest concrete effect rather than silently treated as pure.
Zero runtime cost
Effect rows live entirely in the typechecker. Programs flow through
lexer → parser → HM typechecker → NLL borrow-checker → effect
inference → LLVM IR; the effect pass is a checking pass, not a
lowering pass. The emitted IR for a function is identical whether its
row is ! { io, alloc } or omitted. There is no runtime effect ABI, no
handler dispatch, no tagging — the system is documentation plus
compile-time enforcement, and nothing survives into the binary.
The LSP surfaces the row where it matters: kard-lsp hover shows a
function's signature including its effect row.
Limitations today
- Effect-set membership is concrete:
allocmatchesalloc, with no subtyping or variance between, say,io(general) and a hypothetical more-specificfile_io. - The five built-in labels are hard-coded; there is no user-defined effect-declaration form yet.
Standard Library
What ships with kardc today. Everything here is built into the
compiler — no external crate / module is imported. The prelude declarations (Option, Result, the
Iterator / Hash / Eq traits, the combinators, the file-I/O
wrappers) are prepended to your source automatically; the builtin
functions (print, vec_*, hashmap_*, str_*, …) are recognized
by the typechecker and lowered by codegen on demand. A program that
defines its own Option / Iterator / etc. suppresses the matching
prelude entry, so there's never a duplicate-declaration error.
One reminder that governs every snippet below.
ifis an expression and requires anelse(if c { … } else { … }, never a bareif c { … }). The short-circuit&&/||operators are available, and&of a literal or temporary works (&5,&Foo { .. }) — it materializes a statement-scoped, dropped slot.
Prelude (auto-included)
Option<T> and Result<T, E>
#![allow(unused)] fn main() { enum Option<T> { Some(T), None } enum Result<T, E> { Ok(T), Err(E) } }
Available in every program. Result<T, E> drives the ? operator
(early-returns Err on failure). Tests that call kardashev::parse
directly bypass the driver and so don't get the prelude — they declare
these themselves.
Combinators
Effect-row-polymorphic library functions over an i64 payload (the
shipped MVP shape). Each lowers like an ordinary generic fn, so a
combinator inherits its closure's effects — passing an io closure to
option_map makes the call site io.
#![allow(unused)] fn main() { fn option_map(o: Option<i64>, f: fn(i64) -> i64 ! {e}) -> Option<i64> ! {e} fn option_unwrap_or(o: Option<i64>, default: i64) -> i64 fn option_and_then(o: Option<i64>, f: fn(i64) -> Option<i64> ! {e}) -> Option<i64> ! {e} fn result_map(r: Result<i64, i64>, f: fn(i64) -> i64 ! {e}) -> Result<i64, i64> ! {e} fn result_unwrap_or(r: Result<i64, i64>, default: i64) -> i64 }
fn main() -> i64 { let o = Some(20); let inc = |x| x + 1; option_unwrap_or(option_map(o, inc), 0) // -> 21 }
The Iterator<T> trait
#![allow(unused)] fn main() { trait Iterator<T> { fn next(&mut self) -> Option<T>; } }
for x in it desugars through next() for any impl, and the
fold / map / filter adaptors are generic over T. The built-in
Range (below) ships an impl Iterator<i64> for Range; user types may
impl Iterator<bool>, etc.
fn main() -> i64 ! { io } { let mut total = 0; for x in 0..5 { total = total + x; } // Range as an Iterator<i64> print(total); // -> "10" 0 }
The Hash and Eq traits
#![allow(unused)] fn main() { trait Hash { fn hash(&self) -> i64; } trait Eq { fn eq(&self, other: &Self) -> bool; } }
Built-in impls ship for i64 (identity hash, scalar equality) and
String (FNV-1a hash, byte-exact equality). These are what make a
HashMap key / HashSet element type pluggable: bound a generic on
K: Hash + Eq (multiple bounds per parameter shipped in Phase 28, both
inline and via where) and any user type with both impls becomes a
usable key.
#![allow(unused)] fn main() { struct Id { v: i64 } impl Hash for Id { fn hash(&self) -> i64 { self.v } } impl Eq for Id { fn eq(&self, other: &Id) -> bool { self.v == other.v } } }
I/O and strings
A String is a { ptr, len, cap } heap value with no NUL
terminator. String literals are backed in place (cap 0); string_new
/ str_substring / int_to_string produce fresh heap Strings. The
&String borrow forms feed every read-only string op.
Printing
#![allow(unused)] fn main() { fn print(n: i64) -> i64 ! { io } // one i64 + newline fn print_str(s: &String) -> i64 ! { io } // string + newline fn print_string(s: &String) -> i64 ! { io } // alias of print_str fn println(s: &String) -> i64 ! { io } // string + newline (conventional name) fn print_no_nl(s: &String) -> i64 ! { io } // string with NO trailing newline }
print, print_str, print_string, and println all force a
trailing newline; print_no_nl is the one that does not, so you can
compose a line piece by piece. All four return 0 and require the caller
to declare io.
fn main() -> i64 ! { io } { let greeting = "hello, kardashev"; print_no_nl(&greeting); // no newline print(42); // "42\n" 0 }
Inspecting and building strings
#![allow(unused)] fn main() { fn str_len(s: &String) -> i64 fn str_char_at(s: &String, i: i64) -> i64 // the byte at index i (0..len) fn str_eq(a: &String, b: &String) -> bool // byte-exact (length + contents) fn str_substring(s: &String, start: i64, len: i64) -> String ! { alloc } // start/len clamped into bounds fn int_to_string(n: i64) -> String ! { alloc } // decimal, via snprintf "%lld" fn string_new() -> String ! { alloc } fn string_push_str(s: &mut String, other: String) -> i64 ! { alloc } // takes a String literal directly fn string_len(s: &String) -> i64 }
str_substring clamps start and len into bounds rather than
panicking. string_push_str accepts a literal as the second argument
(string_push_str(&mut s, "cd")).
fn main() -> i64 ! { io, alloc } { let mut s = string_new(); string_push_str(&mut s, "ab"); string_push_str(&mut s, "cd"); print(string_len(&s)); // "4" let n = int_to_string(123); print_str(&n); // "123" 0 }
Containers
Vec<T>
A growable, heap-backed buffer, specialized per element type T (the
codegen sizes the stride from the DataLayout, so i64, bool,
structs, and enums all work). malloc / realloc with capacity
doubling.
#![allow(unused)] fn main() { fn vec_new() -> Vec<T> ! { alloc } fn vec_push(v: &mut Vec<T>, x: T) -> i64 ! { alloc } // appends, returns 0 fn vec_get(v: &Vec<T>, i: i64) -> T // bounds-checked (see below) fn vec_len(v: &Vec<T>) -> i64 }
vec_get is bounds-checked but does not panic: a negative index,
an index >= len, or an empty buffer yields a type-correct zero/null
value of T instead of an out-of-bounds load. (It is fixed-size array
indexing — arr[i] — that OOB-panics, since Phase 23; vec_get
predates that and keeps its safe-zero behavior.) vec_get returns a
shallow copy of the element.
fn main() -> i64 ! { alloc, io } { let mut v = vec_new(); vec_push(&mut v, 10); vec_push(&mut v, 20); print(vec_get(&v, 1)); // "20" print(vec_len(&v)); // "2" 0 }
HashMap<K, V>
Open-addressing (linear-probing) map with rehash on growth, generic
over both key and value (the headline v5 feature). K may be a
built-in i64 (inline identity-hash + icmp) or String (FNV-1a +
str_eq), or any user type with impl Hash + impl Eq. A
non-hashable key type is a clear compile error.
#![allow(unused)] fn main() { fn hashmap_new() -> HashMap<K, V> ! { alloc } fn hashmap_insert(m: &mut HashMap<K, V>, k: K, v: V) -> i64 ! { alloc } fn hashmap_get(m: &HashMap<K, V>, k: K) -> Option<V> fn hashmap_len(m: &HashMap<K, V>) -> i64 }
fn main() -> i64 ! { alloc, io } { let mut m = hashmap_new(); let key = "answer"; hashmap_insert(&mut m, key, 42); // HashMap<String, i64> let lookup = "answer"; match hashmap_get(&m, lookup) { Some(v) => print(v), // "42" None => print(0 - 1), }; 0 }
HashSet<T>
A set over a hashable element T (same key requirement as
HashMap; codegen reuses the map table with a dummy value).
#![allow(unused)] fn main() { fn hashset_new() -> HashSet<T> ! { alloc } fn hashset_insert(s: &mut HashSet<T>, k: T) -> i64 ! { alloc } fn hashset_contains(s: &HashSet<T>, k: T) -> bool fn hashset_len(s: &HashSet<T>) -> i64 }
fn main() -> i64 ! { alloc, io } { let mut s = hashset_new(); hashset_insert(&mut s, 7); let present = if hashset_contains(&s, 7) { 1 } else { 0 }; print(present); // "1" 0 }
&[T] slices
&v[a..b] produces a fat-pointer slice ({ ptr, len }) over an
existing buffer — constructing one does not allocate. The element
type is i64 in this MVP.
#![allow(unused)] fn main() { fn slice_len(s: &[i64]) -> i64 fn slice_get(s: &[i64], i: i64) -> i64 }
Box<T> and Range
Box<T> is a heap-owning pointer (also the payload of
Box<dyn Trait> trait objects). Range is the first-class iterable
produced by a..b (half-open) and a..=b (inclusive); it impls
Iterator<i64> and powers for x in a..b.
File I/O and CLI args
These are added lazily — the file-I/O / argv runtime is emitted only when your source actually mentions one of these names, so an I/O-free program carries none of the machinery.
#![allow(unused)] fn main() { enum IoError { IoNotFound, IoPermissionDenied, IoOther } fn fs_read_to_string(path: &String) -> Result<String, IoError> ! { io, alloc } fn fs_write(path: &String, contents: &String) -> Result<i64, IoError> ! { io } fn fs_exists(path: &String) -> bool ! { io } fn args() -> Vec<String> ! { alloc } // process argv (argv[0] included) fn arg_count() -> i64 fn arg_get(i: i64) -> String // a borrowed view of argv[i] (cap 0) }
Errors are classified portably via access() probes (there is no
reliance on a libc errno symbol), so the IR links on both Linux and
macOS. The variant names are Io-prefixed so the auto-injected enum
can't clash with a user's own NotFound. args() reflects real argv
in an AOT binary (the generated int main(argc, argv) captures it);
under the JIT there is no process argv, so it is empty.
fn main() -> i64 ! { io, alloc } { let path = "config.txt"; match fs_read_to_string(&path) { Ok(s) => print(str_len(&s)), Err(e) => match e { IoNotFound => print(0 - 1), IoPermissionDenied => print(0 - 2), IoOther => print(0 - 3), }, }; 0 }
Documented deferrals (honest, not stubbed)
Carried forward unchanged, exactly as the README records — these are real, known gaps, never silent stubs:
- HashMap / HashSet interior keys & values are not individually
dropped. Dropping a map/set frees its bucket array (no UAF), but a
droppable
KorVstored inside the table leaks — a documented leak in the same class as the closure-env /match-payload leaks that Phase 29 did close. PlainVec/String/Boxand droppableVecelements, user-struct fields, closure-env captures, andmatch-payload bindings all drop deterministically. - Async-frame interior free is deferred. A completed
Future's heap frame is reclaimed by neitherblock_onnor the executor, so a long-running async workload leaks frames (a bounded one-shot does not). Freeing it safely needs reworking the executor task lifecycle (read-after-free risk on the poll slot), and async is off the v5 capstone path, so it stays a known leak rather than a risky half-fix. - macOS/kqueue async fd-readiness is deferred — Linux/
epollonly here; timers (sleep_ms) work cross-platform, and CI covers macOS. - Third-party dependency resolution via the Bazel module registry
is deferred —
mod foo;+kard.tomllocal-path deps are what ship.
The v5 JSON capstone (examples/json/) is, by design, a numeric
config subset: it parses a top-level object with integer values into
a HashMap<String, i64> — the sound shape that exercises the
String-keyed map end to end. Nested / string / bool values are out of
scope. examples/kdlex/ lexes a kardashev subset into a Vec<Tok>.
Compiler Architecture
The kardashev compiler is a single C++ binary (kardc) that walks a
source file through a fixed sequence of passes before handing the
result to LLVM. The same front end backs three other entry points:
kard-lsp (the language server), kardfmt (the source formatter), and
the kard shell wrapper that drives kard build / kard run over a
kard.toml project.
.kd source
|
v
+-------+
| lex | compiler/src/lexer.cpp
+---+---+
| tokens
v
+-------+
| parse | compiler/src/parser.cpp
+---+---+
| ast::Program (with `mod` entries)
v
+---------+
| resolve | compiler/src/main.cpp — prepend prelude, read sibling
+---+-----+ `.kd` for `mod foo;`, flat-merge
| merged ast::Program
v
+-----------+
| typecheck | compiler/src/typecheck.cpp
+---+-------+ HM unification + generic instantiation + trait/assoc-type
| resolution + effect inference
| TypeCheckResult (exprTypes, schemas, methodResolutions, ...)
v
+--------+
| borrow | compiler/src/borrow_check.cpp
+---+----+ affine ownership + NLL borrow tracking
| BorrowCheckResult (errors)
v
+---------+
| codegen | compiler/src/codegen.cpp (~8.8 K lines, the largest file)
+---+-----+ LLVM IR + lazily-emitted builtins + Drop glue
| llvm::Module
v
| O2 PassBuilder pipeline (finish())
v
+-------+ +-----+
| JIT | <-- or --> | AOT |
+-------+ +-----+
ORC v2 LLJIT TargetMachine -> .o
+ synthesized C `int main(argc,argv)`
+ clang link -> exe
Source layout
| File | Role |
|---|---|
compiler/src/lexer.cpp | tokenizer |
compiler/src/parser.cpp | recursive-descent + Pratt parser |
compiler/src/types.cpp | Type representation, unification helpers |
compiler/src/typecheck.cpp | HM typechecker, trait/effect/assoc-type resolution, stdlib schemas |
compiler/src/pattern_match.cpp | Maranget decision-tree compiler for match |
compiler/src/borrow_check.cpp | NLL ownership / borrow checker |
compiler/src/codegen.cpp | LLVM IR emission + builtin runtime + opt pipeline (largest file) |
compiler/src/main.cpp | driver: prelude, module resolver, REPL, JIT, AOT, --test runner |
compiler/src/fmt_main.cpp | kardfmt entry point |
compiler/src/lsp_main.cpp | kard-lsp LSP server over stdio |
compiler/src/ast_print.cpp | AST/IR dump helpers used by --emit-llvm and the formatter |
Per-pass notes
Lexer
Single-pass with line/column tracking. Two-char operators (==, <=,
->, =>, ::, .., ..=, ...) are matched before single-char ones
to keep the grammar unambiguous. Recognises the keyword and operator set
described in language-reference.md.
Parser
Recursive descent for items and statements, Pratt precedence climbing
for expressions, with a unary layer (-x, !x) binding tighter than the
binary operators. The grammar is hand-written in
parser.cpp with no generated tooling. It
parses the full surface language: generics, traits (including
trait Name<T> and associated type Item;), where clauses, impl
blocks (trait and inherent), effect rows (! { io, alloc, e }),
closures, match, if/else if (note: if is an expression and the
else is mandatory), while / loop / for, arrays [T; N], tuples
(A, B), const items, and extern "C" declarations.
Prelude + module resolver
Both live in main.cpp, between parsing and
typechecking.
- Prelude (
applyPrelude): the root source is prepended with declarations the user has not already supplied —Option<T>,Result<T, E>, the genericIterator<T>trait + itsimplfor the built-inRange, and theOption/Result/iterator combinators (map/filter/fold, etc.). The inclusion is by-mention (a grep over the source), so a program that declares its ownOptionorIteratorsuppresses the corresponding prelude piece rather than colliding with it. Prepended combinators then lower like any other generic kardashev function. - Module resolver: for each
mod NAME;, reads<srcDir>/NAME.kd, parses it, and flat-merges its top-level declarations into the program (recursive + cycle-safe via a visited-path set; no namespacing — bare-name references resolve across modules,pubgates path-qualifiedfoo::barreferences). Thekardwrapper'skard.tomllocal-path dependency resolution works by staging each dependency's library.kdas amod-resolvable sibling of the entry point, so this same flat-merge picks it up.
Typechecker
The core semantic pass. TypeChecker::check() orchestrates:
- register struct / enum / trait schemas (opaque first, then resolved),
including a trait's type parameters (
trait Name<T>) and associated type names (type Item;); - register trait/
implbindings and function schemas, allocating fresh generic type variables and effect rows (whereclauses are desugared to inline bounds); - body-check every function: parameters bound to schema variables,
expressions typed by Hindley-Milner unification,
matcharms checked against the scrutinee's ADT; - effect inference: union each callee's effect row into the caller's and
verify it is a subset of the declared row. Effect rows are
row-polymorphic (a row variable
emakes a function effect-polymorphic over its callbacks), so a pure caller invoking aniofunction is rejected at the definition site, at zero runtime cost.
instantiate(t, subst) clones a generic schema with fresh variables per
call site; pattern_match::compileDecisionTree builds the Maranget
decision tree that codegen lowers match through. The typechecker is
also the home of the built-in stdlib schemas (print, vec_*,
hashmap_*, the file-I/O and string builtins, ...), registered at the
top of check() so user code can call them unqualified; it records a
usesFileIo flag so codegen emits the file-I/O runtime only when the
program actually references it.
Borrow checker
Two passes over each function body:
- Pass 1 assigns sequential positions to AST nodes and records, per
binding, the highest position any
IdentExpr/RefExprreferences it (its last use). - Pass 2 walks in the same order maintaining the active-loan set.
Each
let r = &xrecords a loan expiring atr's last use, so borrows die at the borrower's last use (non-lexical lifetimes). The aliasing rule (shared XOR mutable, neither permitted across a move) is checked at every borrow and move site. A&mutis passed by move and is not auto-reborrowed when threaded through recursive calls.
Codegen
A single LLVM module per program. Highlights:
- Built-ins are emitted into the module. The always-on core
(
print,print_str, string helpers, theVec/String/HashMapscaffolding) is declared up front, while per-type collection operations are emitted lazily on first use viagetOrEmit*helpers — e.g.getOrEmitVecOp(op, T)andgetOrEmitHashMapOp(op, K, V)emit a monomorphic specialization per element/key/value type, with aDataLayout-sized stride. The file-I/O runtime (which calls libcfopen/fseek/...) is emitted only whenusesFileIois set, so I/O-free programs stay byte-identical to before. - Monomorphization: monomorphic functions emit eagerly; generic
functions are discovered along the way and queued as
Instance { fnName, typeArgs }records on a worklist drained at the end ofrun(). Generic struct/enum instances get a distinct LLVM struct type per(name, typeArgs)tuple. - Trait dispatch: static calls route through
methodResolutions_(Concrete vs BoundedGeneric) to the right impl's mangled function name.dyn Traitis a{data, vtable}fat pointer with per-impl vtable globals and thunks; object-safety is enforced. - Closures lower to a heap env-struct plus a uniform fat-pointer
function value carrying an effect row;
FnMut/ capture-by-reference closures store a pointer to the captured slot. ?lowers to an inline tag-check that rebuilds the enclosing function'sErr(...)shape and early-returns — no intermediate match desugaring.- Drop / RAII: every owning local gets a per-local drop flag (an
i1alloca).emitDropGluerecursively frees a value (Vec/String/HashMap/Box, aggregates that transitively own one, and any type with animpl Drop);getOrEmitDropThunkwraps it as a uniformvoid(i8*)thunk for cleanup-stack entries. Drops run at scope exit in reverse declaration order, each guarded by its flag so a conditionally-moved value drops exactly once (no double-free / UAF). Moved or returned values are not dropped. - Panic / unwinding: a whole-program
programContainsPanicscan gates all panic machinery — the setjmp/longjmp cleanup stack,panic,catch, and array-OOB checks. The same per-local drop flag gates both the normal and the unwind path, so Drop glue runs during unwinding and every value still drops exactly once. Panic-free programs emit zero panic machinery. async:Future<T> = {poll, frame}; eachasync fnlowers to a resumable poll function over a heap frame thatswitches on a resume state and spills locals live across awaits..awaitgenuinely suspends (returnsPending) and resumes;spawn/join/block_on/sleep_msdrive a process-global round-robin executor.finish()runs the LLVM PassBuilder pipeline on the module before returning, unlessverifyModulealready flagged an error (see below).
Optimization pipeline
finish() builds a fresh llvm::PassBuilder and runs the pipeline named
by the -O0..-O3 flag (default -O2). -O1/-O2/-O3 run the matching
buildPerModuleDefaultPipeline(level); -O0 runs
buildO0DefaultPipeline (the default per-module builder asserts on O0),
which keeps the alloca-heavy bindings and trivial wrapper calls
un-inlined — so O0 IR is materially larger than O2. The opt level is
folded into the AOT compile-cache key so -O0 and -O2 objects never
collide.
JIT vs AOT
The two modes share the same optimized llvm::Module; only the final
consumer differs.
- JIT (
kardc <file.kd>, the REPL, and--test): an ORC v2 LLJIT compiles the module on the fly, looks upmain(or eachtest_*), and calls it as a function pointer. With no real argv, the__kd_argc/__kd_argvglobals default to 0/null. - AOT (
kardc -o <out> <file.kd>): an LLVMTargetMachinewrites a PIC object file; the driver renames the user'smainto__kd_mainand synthesizes a C-compatibleint main(int argc, char** argv)that stores argv into the__kd_argc/__kd_argvglobals (soargs()sees the CLI), calls__kd_main(), and returns itsi64(orbool) result truncated to a process exit code. The host'sclangthen links the object against libc (invoked viallvm::sys::ExecuteAndWaitwith an argv vector, not a shell string).
Build system
- Bazel +
rules_kardashevis the canonical build. An LLVM module extension autodetects the toolchain viallvm-config;bazel build //... && bazel test //...reproduces the CI matrix on a developer machine.rules_kardashev/defs.bzldefines thekardashev_library/kardashev_binaryrules that let other Bazel targets compose kardashev sources. Makefile.localis a thin clang shim (clang+++llvm-config) that builds the same tree against the system LLVM when Bazel isn't available; the smoke tests run identically through it.
CI runs on both ubuntu-latest and macos-latest on every push.
Documented-deferred (never stubbed): third-party dependency
resolution via the Bazel module registry (Bazel can't run in this build
environment, so it isn't verifiable here — mod foo; plus kard.toml
local-path deps are what ship) and macOS/kqueue async fd-readiness (the
epoll fd-readiness reactor is Linux-only; timers work cross-platform).
Test suite
$ make -f Makefile.local test
All lexer tests passed (23 cases)
All parser tests passed (128 cases)
All typecheck tests passed (248 cases)
All pattern_match tests passed (33 cases)
All borrow_check tests passed (45 cases)
All codegen tests passed (154 cases)
PASS: smoke test JIT fib(10)
PASS: smoke test AOT fib(10)
...
Six C++ unit suites (lexer / parser / typecheck / pattern_match /
borrow_check / codegen) plus 40 shell smoke tests covering JIT and AOT
across the whole feature set — modules, effects, closures, dyn,
iterators, containers, generic traits, where, drop / drop-leaks, async
runtime, threads, panic, FFI, const, strings, hashing, file I/O, the
toolchain, and the capstones. The capstones — examples/calc/ (a
recursive-descent arithmetic interpreter), examples/rpn/,
examples/json/ (a numeric-object JSON subset), and examples/kdlex/ (a
kardashev-subset lexer/parser) — are all written in kardashev and
compiled by kardc. CI runs the same suite under Bazel on
ubuntu-latest + macos-latest on every push.