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

TokenNotes
Identifier[A-Za-z_][A-Za-z0-9_]*
Integer literal[0-9]+
Boolean literaltrue / false
String literal"..." with \n \t \r \\ \" escapes
Operators+ - * / % < <= > >= == != = -> => ? ! && || & | ^ << >>
Punctuation( ) { } [ ] , ; : :: . _ & .. ..=
Keywordsfn 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 / functionNotes
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) -> Tindex; returns a shallow copy
vec_len(v: &Vec<T>) -> i64element count
string_new() -> String ! { alloc }empty growable string
string_push_str(s: &mut String, other: String) -> i64append (! { 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>) -> i64entry count
hashset_new() / _insert / _contains / _lenHashSet<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

FunctionNotes
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) -> i64byte length
str_char_at(s: &String, i: i64) -> i64byte at i (0..255), or -1 past end
str_eq(a: &String, b: &String) -> boolbyte-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.

  1. if requires an else. A bare if c { ... } is a parse error; use if c { ... } else { ... } or if c { ... } else {}.
  2. A &mut parameter is passed by move, not auto-reborrowed through recursive calls — thread state through return tuples instead. &mut local at a single call site is fine.
  3. Strings have no NUL terminator ({ ptr, len, cap }); vec_get returns 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 a HashMap<String, i64> (the numeric-object subset capstone: a top-level object of "key": integer members; 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

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

flagmeaning
--effects=opt-indefault — an absent row is unchecked
--effects=strictan absent row asserts purity (the pre-v0.81 rule)
--effects=extendedalso 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:

LabelMeaning
allocHeap allocation
ioFile / network / stdio / general syscalls
panicUnrecoverable failure (unwinds via panic(msg))
asyncYields to the scheduler
unwindStack 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: alloc matches alloc, with no subtyping or variance between, say, io (general) and a hypothetical more-specific file_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. if is an expression and requires an else (if c { … } else { … }, never a bare if 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 K or V stored inside the table leaks — a documented leak in the same class as the closure-env / match-payload leaks that Phase 29 did close. Plain Vec / String / Box and droppable Vec elements, user-struct fields, closure-env captures, and match-payload bindings all drop deterministically.
  • Async-frame interior free is deferred. A completed Future's heap frame is reclaimed by neither block_on nor 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/epoll only 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.toml local-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

FileRole
compiler/src/lexer.cpptokenizer
compiler/src/parser.cpprecursive-descent + Pratt parser
compiler/src/types.cppType representation, unification helpers
compiler/src/typecheck.cppHM typechecker, trait/effect/assoc-type resolution, stdlib schemas
compiler/src/pattern_match.cppMaranget decision-tree compiler for match
compiler/src/borrow_check.cppNLL ownership / borrow checker
compiler/src/codegen.cppLLVM IR emission + builtin runtime + opt pipeline (largest file)
compiler/src/main.cppdriver: prelude, module resolver, REPL, JIT, AOT, --test runner
compiler/src/fmt_main.cppkardfmt entry point
compiler/src/lsp_main.cppkard-lsp LSP server over stdio
compiler/src/ast_print.cppAST/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 generic Iterator<T> trait + its impl for the built-in Range, and the Option/Result/iterator combinators (map/filter/fold, etc.). The inclusion is by-mention (a grep over the source), so a program that declares its own Option or Iterator suppresses 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, pub gates path-qualified foo::bar references). The kard wrapper's kard.toml local-path dependency resolution works by staging each dependency's library .kd as a mod-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/impl bindings and function schemas, allocating fresh generic type variables and effect rows (where clauses are desugared to inline bounds);
  • body-check every function: parameters bound to schema variables, expressions typed by Hindley-Milner unification, match arms 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 e makes a function effect-polymorphic over its callbacks), so a pure caller invoking an io function 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 / RefExpr references it (its last use).
  • Pass 2 walks in the same order maintaining the active-loan set. Each let r = &x records a loan expiring at r'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 &mut is 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, the Vec/String/HashMap scaffolding) is declared up front, while per-type collection operations are emitted lazily on first use via getOrEmit* helpers — e.g. getOrEmitVecOp(op, T) and getOrEmitHashMapOp(op, K, V) emit a monomorphic specialization per element/key/value type, with a DataLayout-sized stride. The file-I/O runtime (which calls libc fopen/fseek/...) is emitted only when usesFileIo is 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 of run(). 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 Trait is 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's Err(...) shape and early-returns — no intermediate match desugaring.
  • Drop / RAII: every owning local gets a per-local drop flag (an i1 alloca). emitDropGlue recursively frees a value (Vec/String/HashMap/Box, aggregates that transitively own one, and any type with an impl Drop); getOrEmitDropThunk wraps it as a uniform void(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 programContainsPanic scan 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}; each async fn lowers to a resumable poll function over a heap frame that switches on a resume state and spills locals live across awaits. .await genuinely suspends (returns Pending) and resumes; spawn/join/block_on/sleep_ms drive a process-global round-robin executor.
  • finish() runs the LLVM PassBuilder pipeline on the module before returning, unless verifyModule already 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 up main (or each test_*), and calls it as a function pointer. With no real argv, the __kd_argc/__kd_argv globals default to 0/null.
  • AOT (kardc -o <out> <file.kd>): an LLVM TargetMachine writes a PIC object file; the driver renames the user's main to __kd_main and synthesizes a C-compatible int main(int argc, char** argv) that stores argv into the __kd_argc/__kd_argv globals (so args() sees the CLI), calls __kd_main(), and returns its i64 (or bool) result truncated to a process exit code. The host's clang then links the object against libc (invoked via llvm::sys::ExecuteAndWait with an argv vector, not a shell string).

Build system

  • Bazel + rules_kardashev is the canonical build. An LLVM module extension autodetects the toolchain via llvm-config; bazel build //... && bazel test //... reproduces the CI matrix on a developer machine. rules_kardashev/defs.bzl defines the kardashev_library / kardashev_binary rules that let other Bazel targets compose kardashev sources.
  • Makefile.local is 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.