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.
The type table
Section titled “The type table”| 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* |
No implicit casts
Section titled “No implicit casts”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.
Structs and enums
Section titled “Structs and enums”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”.
Generics
Section titled “Generics”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 }}Ownership
Section titled “Ownership”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() == 4Using 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.
Errors without exceptions
Section titled “Errors without exceptions”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.