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>.