Skip to content

Types

Every variable must carry a declared type. No implicit inference, no implicit casts, no shadowing, and a condition must be a real bool. An auditor can read line by line and know exactly what happens — that is not a style preference, it is an auditability requirement.

Type Meaning C representation
int Signed 64-bit integer, overflow-checked int64_t
float IEEE 754 binary64 double
bool true / false bool
str Byte string with explicit length hl_str*
list[T] Dynamic array hl_list*
map[str, T] Hash map with str keys hl_map*
tainted[T] Taint wrapper — a compile-time property represented as T
Cap[E] Capability token for effect E —
void Return type only —
Name, Name[T…] User-defined struct or enum Name*

int → float requires x.to_float(); the reverse requires x.to_int().

From To How
int float x.to_float() — exact
float int x.to_int() — truncates toward zero
any str x.to_str()
str int int_parse(s) returns Result[int, str]
list[T] list[U] the list_convert_* family of std.list

int → float is exact because int is 64-bit and float has a 64-bit mantissa. float → int truncates, and the language does not hide that.

struct Point {
x: int,
y: int
}
enum Shape {
Circle(float),
Rect(int, int)
}
fn area(s: Shape) -> float {
return match s {
Shape.Circle(r) => 3.14159 * r * r,
Shape.Rect(w, h) => (w * h).to_float()
}
}

match is an expression and must be exhaustive. This is where “no null” is enforced at the type level: there is no null to check, so Option[T] and Result[T, E] are the only two ways to represent “might not be there”.

Generic types are monomorphised: each instantiation produces its own code, with no boxing and no virtual dispatch — performance equal to hand-written code.

enum Option[T] { Some(T), None }
enum Result[T, E] { Ok(T), Err(E) }
fn unwrap_or(r: Result[int, str], fallback: int) -> int {
return match r {
Result.Ok(v) => v,
Result.Err(e) => fallback
}
}

clone() makes an independent deep copy; take(x) transfers ownership and marks the original binding as moved; drop(x) releases it explicitly.

let xs: list[int] = [10, 20, 30]
let ys: list[int] = clone(xs)
ys.push(40) # only ys changes
# xs.len() == 3, ys.len() == 4

Using a moved value is a compile error (use of moved value: s), so there is no double free and no use-after-free to audit at runtime.

fn safe_div(a: int, b: int) -> Result[int, str] {
if b == 0 {
return Result.Err("division by zero")
}
return Result.Ok(a / b)
}
fn chained_div(a: int, b: int, c: int) -> Result[int, str] {
let q: int = safe_div(a, b)? # `?` returns early on Err
let r: int = safe_div(q, c)?
return Result.Ok(r)
}

panic is not for expected failures — only for programming bugs. Expected failures (missing file, bad parse, invalid input) go through Result.