- File extension:
.fn. - Statements end with
;. - Blocks are delimited with
{}. - Comments:
//single-line,/* ... */block. - Visibility: prefix a declaration with
pubto export it; withoutpubit is module-private.
A regular // comment immediately above a declaration becomes its
documentation, picked up by the website reference and by hover in the
language server. This applies to module summaries, public symbols,
compound fields, and shape members. Keep them short and
declaration-specific.
use std.io;
// Optional value container. Named Maybe here (not Option) only to avoid
// colliding with std.option's own Option<T> for this standalone example.
pub compound Maybe<T> {
// True when a value is present.
flag has;
// Stored value.
T value;
}
// Value that can render itself as text. Named Renderable here (not
// Display) only to avoid colliding with std.shapes' own Display.
pub shape Renderable {
// Produce a textual representation.
to_string() str;
}
fun main() {
Maybe<num> opt;
opt.has = true;
opt.value = 42;
println_fmt("has={flag} value={num}", opt.has, opt.value);
}| Fun type | C representation | Notes |
|---|---|---|
num |
int64_t |
Signed 64-bit integer, the default integer type. |
dec |
double |
64-bit floating-point. A decimal literal keeps every digit it needs (3.141592653589793, 0.000000001), written to the generated C so it reads back as exactly the same double. |
i8 / u8 |
int8_t / uint8_t |
Fixed-width 8-bit integer. |
i16 / u16 |
int16_t / uint16_t |
Fixed-width 16-bit integer. |
i32 / u32 |
int32_t / uint32_t |
Fixed-width 32-bit integer. |
i64 / u64 |
int64_t / uint64_t |
Fixed-width 64-bit integer. |
f32 |
float |
Fixed-width 32-bit float. |
f64 |
double |
Fixed-width 64-bit float. |
iN / uN (arbitrary width) |
Nearest standard container up to 128 bits, _BitInt(N)/unsigned _BitInt(N) past that |
See Platforms & Compilers, compiler support past 128 bits varies. |
flag |
bool |
Boolean. |
chr |
char |
Character. |
str |
char* |
Null-terminated string. |
raw |
void (raw* for void*) |
Opaque type. |
The null pointer/string sentinel (a keyword; lowers to C NULL). It
coerces to any pointer type and to str, and compares with ==/!=:
num* p = nil;, if p == nil { ... }, Node{next = nil}. No import is
needed, unlike the C macro NULL, which requires use std.c.def;.
A backtick-delimited literal (`...`) needs no escaping at all: a
backslash or an embedded double-quote is just a literal byte.
use std.io;
fun main() {
let path = `C:\Users\name\file.txt`;
let msg = `she said "hi" and left`;
println_fmt("path={str}", path);
println_fmt("msg={str}", msg);
}Two forms, both starting with a backtick, disambiguated purely by whether you close it on the same line:
- Inline: closed by another backtick on the same line (as above).
- Multi-line: a backtick left unclosed before the line's newline starts a block. Each subsequent line that begins (after leading whitespace) with its own backtick contributes its own content, joined with a real newline byte, ending at the first line that doesn't:
use std.io;
fun main() {
let sql =
`SELECT *
`FROM users
`WHERE id = ?
;
println_fmt("sql={str}", sql);
}Trailing code that needs to sit on the same line as the last content line
can close the block explicitly instead: `WHERE id = ?`;.
A literal backtick is written as two (``), the one escape a raw
string has. A single backtick still closes the string, so `` on its
own is the empty raw string, and content that is itself made of backticks
(a markdown fence, for one) is written by doubling each:
use std.io;
fun main() {
let quoted = `a``b`; // a`b
let fence = ```````fun`; // ```fun
println_fmt("quoted={str}", quoted);
println_fmt("fence={str}", fence);
}Array literals require uniform element types: num[] arr = [1, 2, 3];.
A bracketed size makes it a fixed-size array instead: num[3] c; is a
real, inline, stack-allocated C array, not a pointer and not
Vec<T> (the separate, heap-backed, growable stdlib type) - a
compound holding one has no indirection to its own fields:
use std.io;
compound Vec3 {
num[3] c;
}
fun main() {
Vec3 v;
v.c[0] = 1;
v.c[1] = 2;
v.c[2] = 3;
println_fmt("{num} {num} {num}", v.c[0], v.c[1], v.c[2]);
}The size can be any expression, including a named const, and
brackets can repeat for a multi-dimensional array: num[N][N] m;.
Pointer depth is written Type*: Node* next;. Self-referential and
forward-declared types are supported.
(T1, T2, ...) groups two or more differently-typed values into one
real type, usable anywhere a type is: a variable's declared type, a
function parameter or return type, or a generic argument - with no
separate compound declaration needed.
use std.io;
fun min_max(num a, num b) (num, num) {
if a < b {
ret (a, b);
}
ret (b, a);
}
fun main() {
(num, str) person = (30, "Ada");
println_fmt("{num} {str}", person.0, person.1);
let (low, high) = min_max(9, 3);
println_fmt("{num} {num}", low, high);
}- A tuple literal needs at least two comma-separated elements:
(1, "hi"). A single parenthesized value ((1)) stays an ordinary grouped expression, not a one-element tuple. - Read an element back positionally with
.0,.1, and so on; an out-of-range index is a compile-time error. A tuple-of-tuples chains directly -t.0.1reads element1oft's own element0- even though0.1would otherwise lex as one decimal number: the compiler splits it back into two positional hops from the token's own raw source digits, not its parsed value, so a multi-digit chained index (t.0.10) still reads back correctly as element10, not1. let (a, b, c) = expr;destructures a tuple into individually- typed names in one step.expris evaluated exactly once no matter how many names it destructures into, and each name must actually be used or it's anunused_variablewarning like any other local (prefix with_to opt out, same convention as elsewhere).(T1, T2) (a, b) = expr;destructures with an explicit declared type instead of inferring one, the same waydec x = 1;declares a type rather than inferring it: each name gets its own declared element type, andexprmust fit the declared type as a whole (numeric widening included), not just whatever it happens to infer to.for (a, b) : pairs { ... }destructures each element of a tuple-elemented iterable (Vec<(K, V)>) into its own names per iteration, the same wayletdestructures a plain tuple value - no combined index-tracking form (for (a, b) :: xsis not supported; the existingfor i, item :: xstwo-name form already covers index tracking).- A tuple works as an ordinary generic argument (
Box<(num, str)>) and as an ordinary type alias's own body - see Type Aliases for theals Args = (num, str);pattern this enables with generic aliases. fitmatches a tuple subject structurally:(0, y) -> ...matches when element0equals0, bindingyto element1. Each position is independent - a bare (non-_) identifier binds that position's own value,_matches without binding, and anything else (a literal, or any other expression) is a guard that position's own value must equal. A later branch is only reached when an earlier one's guard positions don't all match:use std.io; fun main() { (num, str) t = (0, "go"); fit t { (0, s) -> { println_fmt("zero, {str}", s); } (n, s) -> { println_fmt("{num}, {str}", n, s); } } }
Variables can be explicitly typed or inferred with let.
fun add(num a, num b) num {
ret a + b;
}
fun main() {
num x = 1;
str name = "fun";
let count = add(1, 2);
_ = x;
_ = name;
_ = count;
}let always requires an initializer; the compiler infers the declared
type from the expression:
- Numeric literals infer
numordecdepending on literal form:1->num,1.5->dec. "text"infersstr,'a'inferschr,true/falseinferflag.- Array literals infer element type and become
T[]:[1, 2, 3]->num[],[Point{x = 1, y = 2}]->Point[]. - Function calls infer the function's return type:
let p = make_point(1, 2);->Point. - Member access uses the receiver's type:
let x = p.x;->num. - Indexing an array yields its element type:
let v = nums[i];->numwhennumsisnum[]. - If the expression mixes numeric types, inference prefers the wider
category (
decovernum).
compound Point { num x; num y; }
fun make_point(num x, num y) Point {
ret Point{x = x, y = y};
}
fun main() {
let n = 42; // num
let d = 3.5; // dec
let s = "hello"; // str
let c = 'Z'; // chr
let b = true; // flag
let nums = [1, 2, 3]; // num[]
let p = make_point(1, 2); // Point
let x = p.x; // num
let px = nums[0]; // num
}const declares an immutable binding, at top level or local scope:
const MAX = 10; (inferred, like let) or const num MAX = 10;
(explicit type). Both forms always require an initializer. pub const
exports a top-level constant. Reassigning a const (directly or via a
compound-assignment operator like +=) is a compile-time typecheck
error, for both local and global constants:
const num MAX = 100;
fun main() {
const local_max = MAX;
MAX = 200; // error: cannot assign to const 'MAX'
local_max += 1; // error: cannot assign to const 'local_max'
}enum Color { Red, Green, Blue }
fun main() {
Color c = .Red;
_ = c;
}- Longhand access:
Color.Red(works even if the enum is declared later in the file). - Shorthand access:
.Red(contextual; the expected enum type must be known from assignment, argument, orfit). - Function arguments:
fun takes(Color c) { ... }called astakes(.Blue);. - Pattern matching:
fit c { .Red -> { ... }, .Green -> { ... }, _ -> { ... } }, or the longhand formColor.Red -> { ... }. - Exhaustiveness: missing enum variants in
fitmay emitfit_non_exhaustive; redundant branches or catch-alls may emitfit_unreachable_branch.
A variant may carry a positional payload, turning the enum into a tagged union. An enum becomes a tagged union as soon as any variant has a payload; payload-free variants still coexist.
compound Vec2 { num x; num y; }
enum Shape {
Circle(num), // primitive payload
Rect(num, num), // multiple payload fields
At(Vec2), // compound payload (by value)
Empty, // payload-free variant
}
fun area(Shape s) num {
fit s {
Shape.Circle(r) -> { ret r * r; } // r binds the payload
Shape.Rect(w, h) -> { ret w * h; } // w, h bind the payload
Shape.At(p) -> { ret p.x + p.y; } // p is the compound payload
Shape.Empty -> { ret 0; }
}
ret -1;
}
fun main() {
Shape s = .Circle(5); // shorthand construction
_ = area(s);
}-
A payload is a parenthesized, comma-separated list of types. Payloads can be primitives, compounds (by value), or monomorphized generic instances (
Boxed(Box<num>)). -
Construction: longhand
Shape.Circle(5), shorthand.Circle(5)when the expected type is known, orShape.Emptyfor a payload-free variant. -
Pattern matching destructures the payload into locals visible in the arm body; a
_ -> { ... }catch-all covers the remaining variants. -
A tagged-union enum lowers to a C
struct { Enum_tag tag; union { ... } payload; }; payload-free (plain) enums keep the classic Cenumlowering. -
The same
fit_non_exhaustivecheck applies: cover every variant or add a_catch-all. -
A tagged-union enum has no
==or!=: it is a struct in the generated C, and comparing two is a compile error that points atfit, the way to ask which variant a value is. A plain enum (no payloads) still compares with==, and a pointer to a tagged-union enum compares againstnil. -
Any variant, payload-carrying or not, may declare its own explicit discriminant (
Ok = 200, NotFound = 404, Unknown(num) = -1) - useful when the numbers mean something (a wire status code) rather than being arbitrary. A variant with no explicit value keeps the next ordinal after the previous one, same as a plain enum. Read any enum's own discriminant back with.tag_value() num, a builtin available on every enum (plain or tagged-union) with noimplof its own needed:// fun:no-run enum HttpStatus { Ok = 200, NotFound = 404, Unknown(num) = -1 } fun code(HttpStatus s) num { fit s { .Unknown(c) -> { ret c; } _ -> { ret s.tag_value(); } } }
Sugar over std.option/std.result, replacing the repeated
check-then-unwrap shape with a single postfix operator:
// fun:no-run
use std.option;
use std.result;
fun half(num x) Option<num> {
if x % 2 == 1 { ret .None; }
ret .Some(x / 2);
}
fun to_result(num x) Result<num, str> {
if x < 0 { ret .Err("negative"); }
ret .Ok(x);
}
fun combine(num x) Option<num> {
num a = half(x)?; // .None short-circuits: returns .None here
ret .Some(a + 1);
}
fun combine_result(num x) Result<num, str> {
num a = to_result(x)!; // .Err(e) short-circuits: returns .Err(e) here
ret .Ok(a + 1);
}-
expr?unwraps anOption<T>:.Some(v)evaluates tov;.Nonereturns.Nonefrom the enclosing function immediately. The enclosing function must itself returnOption<...>. -
expr!unwraps aResult<T, E>:.Ok(v)evaluates tov;.Err(e)returns.Err(e)from the enclosing function immediately. The enclosing function must returnResult<_, E>with the exact same error type; there is no automatic conversion between error types. -
Both work anywhere an expression is legal, not just statement-final: a
letinitializer, a call argument, a chained access (half(x)?.field), nested inside another expression. -
A postfix propagation binds as tightly as a call,
.or[], so a prefix*,-,!,~or&applies to the propagated value:*get(p)?is*(get(p)?). To propagate the dereferenced value instead, parenthesize:(*opt_ptr)?.awaitand a channel receive go the other way, propagating what they produce:await f()?is(await f())?. -
foo()!=xstill lexes as the!=comparison operator (a space-free!immediately before=always folds), so it never means "propagate, then compare"; writefoo()! == xif propagation was intended. -
This is pure sugar: the equivalent
if/retform still works everywhere and is what these operators expand to. -
A propagation can also be used directly as a
fitsubject (fit expr? { ... }/fit expr! { ... }), with no intermediateletneeded:// fun:no-run fun describe(num x) Option<str> { fit half(x)? { 0 -> { ret .Some("zero"); } _ -> { ret .Some("nonzero"); } } }
?/! only ever propagate a matching signal: an inner .None becomes
an outer .None, an inner .Err(e) becomes an outer .Err(e). ?!
and !? bridge the other direction, between Option and Result:
// fun:no-run
fun find(num x) Option<num> {
if x > 0 { ret .Some(x); }
ret .None;
}
fun combine(num x) Result<num, str> {
num a = find(x)?!("not found"); // .None -> .Err("not found") here
ret .Ok(a + 1);
}
fun parse(num x) Result<num, str> {
if x > 0 { ret .Ok(x); }
ret .Err("bad");
}
fun safe_parse(num x) Option<num> {
num a = parse(x)!?; // .Err(_) -> .None here, discarded
ret .Some(a * 2);
}-
expr?!(err)unwraps anOption<T>:.Some(v)evaluates tov;.Nonereturnserrfrom the enclosing function immediately, wrapped in whatever shape it needs -.Err(err)if it returnsResult<_, E>, or.Some(err)if it returnsOption<E>(the error-channel convention where.Somecarries the error and.Nonemeans success).errmust be assignable to that slot's own type, the same no-conversion discipline!already has. -
errcan be a bare enum-variant shorthand: it reads against the enclosing function's own error type, the same wayret .Variantreads against its return type.// fun:no-run enum LookupError { NotFound, Broken(num) } fun combine(num x) Result<num, LookupError> { num a = find(x)?!(.NotFound); // .None -> .Err(.NotFound) num b = find(a)?!(.Broken(a)); // a payload variant works too ret .Ok(a + b); } -
expr!?unwraps aResult<T, E>:.Ok(v)evaluates tov;.Err(_)returns.Nonefrom the enclosing function immediately, discarding the error entirely. The enclosing function must returnOption<...>; no relationship between its own generic argument andEis required, since.Nonecarries no payload. -
Each operator asks for exactly what it structurally needs:
?!takes an argument because conjuring an error value from nothing isn't possible;!?takes none because discarding one needs nothing. Reading order is mnemonic:?!starts Option-side and ends Result/error-shaped ("this is optional, missing means this error");!?starts Result-side and ends Option-shaped ("this can fail, I only care whether it worked"). -
Same rules as
?/!: pure sugar for the equivalentif/retform, works anywhere an expression is legal (aletinitializer, a call argument, a chained access, afitsubject), and both operators are their own single tokens -expr?!(err)/expr!?never collide with anything else, the way a space-free!=folds ahead of?/!. -
Either operator can be a
fitsubject, matching on the unwrapped value with no intermediatelet:// fun:no-run fun label(num x) Result<str, LookupError> { fit find(x)?!(.NotFound) { 0 -> { ret .Ok("zero"); } _ -> { ret .Ok("nonzero"); } } } fun label_opt(num x) Option<str> { fit parse(x)!? { 0 -> { ret .Some("zero"); } _ -> { ret .Some("nonzero"); } } }
Compounds are like C structs, and can have methods via impl.
compound Point {
num x;
num y;
}
fun main() {
Point p;
p.x = 1;
p.y = 2;
}A compound field whose name begins with an underscore is
module-private: readable/writable only from code in the same module as
the compound's declaration (including its own impl methods via
self._field). Another module must go through public accessor methods.
use std.io;
compound Account {
num id; // public
num _balance; // private to this module
}
impl Account {
pub balance() num { ret self._balance; } // ok: same module
}
// In another module: `acc._balance` is rejected; `acc.balance()` works.
fun main() {
Account acc;
acc.id = 1;
acc._balance = 100;
println_fmt("balance={num}", acc.balance());
}shape HasArea {
area() num;
}
compound Square {
num side;
}
impl Square as HasArea {
area() num { ret self.side * self.side; }
}
fun main() {
Square s;
s.side = 4;
num area = s.area();
_ = area;
}- Shape values can be used for dynamic dispatch, like trait objects.
- Shape methods follow normal visibility rules: non-
pubmethods are callable inside the declaring module, but not from importing modules. - Formatting with
{}usesDisplay.to_string()only when that method is accessible at the call site. Ifto_string()is private in another module, formatting falls back to pointer-style output for that value.
use std.io;
shape HasArea {
area() num;
}
compound Point {
num x;
num y;
}
compound Rectangle {
num w;
num h;
}
impl Point {
translate(num dx, num dy) {
self.x += dx;
self.y += dy;
}
}
impl Rectangle as HasArea {
area() num { ret self.w * self.h; }
}
fun main() {
Point p;
p.x = 1;
p.y = 2;
p.translate(3, 4);
Rectangle r;
r.w = 3;
r.h = 4;
println_fmt("p=({num},{num}) area={num}", p.x, p.y, r.area());
}A plain impl Point { ... } attaches methods to a compound directly;
impl Rectangle as HasArea { ... } implements a shape for it.
An impl method with one of the following names is called for the
matching operator instead of being written as a .method() call:
| Operator | Method name | Operator | Method name |
|---|---|---|---|
+ |
op_add |
== |
op_eq |
- (binary) |
op_sub |
!= |
op_ne |
* |
op_mul |
< |
op_lt |
/ |
op_div |
<= |
op_le |
% |
op_mod |
> |
op_gt |
- (unary) |
op_neg |
>= |
op_ge |
! (unary) |
op_not |
[] |
op_index |
use std.io;
compound Vec3 {
num x;
num y;
num z;
}
impl Vec3 {
op_add(Vec3 other) Vec3 {
Vec3 r;
r.x = self.x + other.x;
r.y = self.y + other.y;
r.z = self.z + other.z;
ret r;
}
op_mul(num s) Vec3 {
Vec3 r;
r.x = self.x * s;
r.y = self.y * s;
r.z = self.z * s;
ret r;
}
}
fun main() {
Vec3 a; a.x = 1; a.y = 2; a.z = 3;
Vec3 b; b.x = 10; b.y = 20; b.z = 30;
Vec3 scaled = a * 2;
Vec3 sum = scaled + b;
println_fmt("{num} {num} {num}", sum.x, sum.y, sum.z);
}- A binary operator's one declared parameter is the right-hand operand
(taken by value, like any other method parameter); a unary
-/!takes none. The result type is whatever the method declares. - There is no overloading by parameter type - a type gets exactly
one
op_mul, for instance, not one forVec3 * Vec3and a separate one forVec3 * num. This matchesimpl's own existing rule (one method per name per type); pick whichever single operand type suits the type best. - A mismatched operand is a real compile error naming the resolved method, the same as calling any other method with the wrong argument type.
- A type with no matching
implmethod keeps the operator's ordinary builtin meaning completely unchanged - this only ever adds a new capability, never changes what+/==/etc. already do onnum,str, and every other type that doesn't define one. - Only a bare local variable is dispatched -
a + band-awork, but a chaineda * 2 + bor a field access used directly as an operand (self.pos + other.pos) does not yet resolve; bind the intermediate value to its ownletfirst (let scaled = a * 2; let sum = scaled + b;). op_indexonly covers reading (a[i]); assigning through an index (a[i] = v) on a type with no real array/Vec<T>field is not yet supported.
Compounds, impls, and free functions can be generic: compound Vec<T> { ... }, fun identity<T>(T x) T { ret x; }. Use Vec<num> etc. where a
concrete instantiation is required.
Type parameters can be constrained with : and |, on impls, compounds,
and free functions alike:
use std.io;
// Named Accum here (not Vec) only to avoid colliding with std.vec's own
// Vec<T> for this standalone example; a real project would just use that.
compound Accum<T> {
T[] data;
num len;
}
impl Accum<T: num | dec> {
pub sum(T zero) T {
T out = zero;
num i = 0;
for i < self.len {
out = out + self.data[i];
i = i + 1;
}
ret out;
}
}
compound Box<T: num | str> {
T value;
}
fun identity<T: num | str>(T x) T {
ret x;
}
fun main() {
Accum<num> nums;
nums.data = [1, 2, 3];
nums.len = 3;
println_fmt("sum={num} id={num}", nums.sum(0), identity(5));
}This lets one body work for a fixed set of concrete types; the compiler
monomorphizes each concrete instantiation and rejects a call/instantiation
whose type argument isn't in the declared bound at compile time. A bound
alternative can also name a shape instead of a concrete type, checked by
"does this type implement it" rather than an exact match, or be a full type
expression like a generic instantiation (T: User | Vec<num>), not just a
bare identifier - a bound list is a union of concrete types, shapes, and
type expressions, mixed freely.
A trailing type parameter can have a default, written = Type after its
name (and after its bound, if it has one). A reference that leaves out
those trailing arguments gets them filled in:
use std.io;
enum Res<T, E = str> {
Ok(T),
Err(E),
}
compound Pair<A, B = A> {
A a;
B b;
}
fun half(num x) Res<num> {
if x % 2 == 1 {
ret .Err("odd");
}
ret .Ok(x / 2);
}
fun main() {
Pair<num> same = Pair<num>{ a = 3, b = 4 };
Pair<num, str> mixed = Pair<num, str>{ a = 1, b = "x" };
println_fmt("{num} {str}", same.a + same.b, mixed.b);
}Here Res<num> means Res<num, str>, and Pair<num> means Pair<num, num>: a default can name an earlier parameter, which stands for whatever
was written for it, so Pair<num, str> keeps its own second argument.
- Enums, compounds, shapes, aliases, generic functions and a method's own
type parameters can all have defaults. An
impl's type parameters cannot, since they come from the type it implements, and an impl header must write every type argument of the type it names. - Defaults are trailing: once a parameter has one, every later parameter
must too (
<T = num, U>is an error). - A default may only name earlier parameters, never its own or a later one, and never the declaration it belongs to. A default that itself uses a type with defaults gets those filled in as well. Defaults that refer back to each other are reported instead of looping.
- Only a reference that writes at least one argument is filled in. Writing
too few for a declaration whose remaining parameters have no default is an
error naming how many it needs (
'Wide' expects at least 2 type arguments, found 1). - A generic function's parameter is usually inferred from its arguments, so
its default only applies to a parameter nothing else pins down, such as
one that appears only in the return type:
fun make<T = num>() Vec<T>called asmake()builds aVec<num>, andmake<str>()builds aVec<str>. - Filling happens before typechecking, so everything downstream sees the arguments as though they had been written out.
- The standard library uses this itself:
Result<T, E = Error>instd.result, soResult<num>meansResult<num, Error>and a custom error type is still written out (Result<num, ParseErrorKind>).
A shape can be generic too: shape To<T> { to() T; }, and impl Point as To<JsonValue> { pub to() JsonValue { ... } } binds a concrete
instantiation.
- A concrete instantiation dispatches the same way a non-generic shape
does: a direct method call (
p.to()), or a shape-typed parameter/variable naming the same concrete instantiation (fun to_json_value(To<JsonValue> value) JsonValue { ret value.to(); }, called asto_json_value(&p)). - Each concrete instantiation (
To<JsonValue>,To<num>, ...) is its own shape identity: animplbinds one specific instantiation, and a shape-typed parameter/variable must name that same instantiation to dispatch. - A still-generic reference to a shape's own type parameter (
impl Vec<T> as Iterator<T>) is a different, symbolic binding that resolves through the enclosing type's own generic instantiation instead of naming one concrete type.
als Name<T1, T2> = <type>; names a reusable type expression, expanded
wherever it's referenced before typechecking ever runs - the compiler
never sees the alias name itself, only its fully-resolved body, so it's
never a textual macro: the underlying type is still fully enforced.
use std.io;
// A non-generic alias: just a shorter, more meaningful name.
als Meters = num;
// A generic alias whose body is a function type.
als Callback<A, R> = fun(A) R;
fun square(num x) num {
ret x * x;
}
fun apply(Callback<num, num> cb, num x) num {
ret cb(x);
}
// A bound-list alias: stands for a union of types, spliced into a
// generic constraint wherever it's referenced.
als Numeric = num | dec;
fun double<T: Numeric>(T x) T {
ret x + x;
}
fun main() {
Meters distance = 5;
println_fmt("distance={num} applied={num} doubled={num}", distance, apply(square, 4), double(3));
}- An alias can be
pub, same as any other top-level declaration. - An alias's own body can reference another alias (
als Top = Middle;); a reference cycle among aliases is a compile-time error, not infinite recursion. - An alias's own body can never be a pointer type
(
als NodePtr = Node*;is rejected, and so is a pointer-typed alternative in a bound-list alias's own body): the point of writingNode* x;is that the pointer is visible right there at the use site, not hidden behind a name that reads like an ordinary value. Applying a pointer at a reference to a non-pointer alias (Meters* m;) is unaffected - the pointer is still written explicitly there, exactly like any other type. - A bound-list alias (the
a | bform) can't itself be generic - it has no single instantiation site of its own the way an ordinary alias does. - An alias's own body can name a shape (
als Drawable = HasArea;), and dynamic dispatch through it works exactly like a plain shape-typed variable:Drawable d = □thend.area(). This is still a value type, not a pointer -Drawable*follows the same explicit- pointer-at-the-use-site rule as any other alias. - An alias's own body can be a tuple (
als Args = (num, str);), and it's then an ordinary generic type parameter like any other:als Callback<A, R> = fun(A) R;plusCallback<Args, str>expands tofun((num, str)) str- no special-casing needed anywhere once tuples themselves exist. - A type param written in parentheses in the alias's own declaration
(
als Callback<(Args), Ret> = fun(Args) Ret;) is a spread param: when it's bound to a tuple and fills a wholefun(...)parameter position in the alias's body, that tuple's own members spread into separate positional parameters instead of staying one tuple-struct parameter.Callback<(num, dec, str), str>expands tofun(num, dec, str) str, matching a hand-written variable-arity function signature. A bare (non-parenthesized) param bound to the same tuple stays a single tuple-struct parameter, as above - the parentheses at the declaration, not the argument's own shape, decide which.
use std.io;
fun add(num a, num b) num {
ret a + b;
}
fun main() {
println_fmt("sum={num}", add(1, 2));
}- No nested function declarations.
ret value;returns from a function.- Generic functions:
fun id<T>(T x) T { ret x; }, type arguments inferred from call sites (num v = id(1);).
A parameter may declare a default with = expr; a call that omits it
uses the default.
use std.io;
fun greet(str name, num times = 1, str sep = ", ") {
println_fmt("name={str} times={num} sep={str}", name, times, sep);
}
fun main() {
greet("a"); // times = 1, sep = ", "
greet("a", 3); // times = 3, sep = ", "
greet("a", 3, "; "); // all explicit
}- Trailing only: defaulted parameters must come last, a required parameter cannot follow a defaulted one.
- Self-contained defaults: a default expression is evaluated at the
call site, so it may not reference
selfor an earlier parameter (a constant, a global,nil, an enum variant, or another self-contained expression is fine). - Works for free functions and methods, including generic,
async, and pointer-receiver methods.
fun(T1, T2, ...) R names the type of a function taking T1, T2, ...
and returning R (omit R for a void function). A function value is
written as a bare reference to a named function, and works as a
parameter, a local variable, a function's own return type, and a
compound field:
use std.io;
fun add(num a, num b) num { ret a + b; }
fun apply(num a, num b, fun(num, num) num cb) num { ret cb(a, b); }
fun get_op() fun(num, num) num { ret add; }
compound Ops {
fun(num, num) num op;
}
fun main() {
println_fmt("result={num}", apply(2, 3, add)); // 5
fun(num, num) num f = get_op();
println_fmt("result={num}", f(4, 5)); // 9
Ops o = .{op = add};
println_fmt("result={num}", o.op(6, 7)); // 13
}Every call site is checked against the declared signature: arity, every
parameter type, and the return type must match. This holds whether the
function value is passed by name as an argument, called through a
parameter inside the function that received it, or called through a
compound field, and a generic method's own type parameter (Vec<T>'s T
in sort_by(cmp)) is substituted with the receiver's real type first. A
mismatch is a compile error, caught before it can reach the C compiler.
Vec<T>.sort_by(cmp) is the standard library's own use of this, for
custom comparators. Its parameter is typed Comparator<T>, an alias std.vec
exports for fun(T, T) num (negative when a sorts before b, positive
when after, 0 when equal), so a function of your own can take a comparator
the same way: fun sort_with(Vec<num>* v, Comparator<num> order).
A non-void function that can fall off the end without returning
triggers missing_return, always checked. The analysis is conservative:
a trailing ret, an exhaustive if/elif/else where every branch
returns, a fit with a default branch whose arms all return, and
infinite loops (for {}/for true {} with no break) all count as
returning.
fun main() {
num x = 1;
if x > 0 {
_ = x;
} elif x == 0 {
_ = x;
} else {
_ = x;
}
}- Array:
for item : arr { ... } - Indexed array:
for i, item :: arr { ... } - While-style (condition):
for i < len { ... } - Infinite loop:
for true { ... } - Everything else - a range,
Vec,Map,Set, or any user-defined type - iterates through theIterator<T>shape, below.
A raw C array (num[] arr) is the one special case: it iterates by
direct index, since it's a primitive language construct with no shape
impls of its own. Every other iterable dispatches structurally through
Iterator<T>.
// fun:no-run
pub shape Iterator<T> {
next() Option<T>;
}Any type implementing Iterator<T> directly works with for - no
.iter() indirection, no wrapper type. for x : v { ... } checks
whether v's own type implements Iterator<Elem> and, if so, splices
that type's own next() body directly into the loop (full inlining,
not a per-element function call): a yielded .Some(x) becomes binding
the loop variable(s) and running the loop body; .None ends the loop.
Range (for i : a..b { ... }, needs use std.range;), Vec<T>,
Map<K, V> (Iterator<(K, V)>, yielding key/value pairs), and Set<T>
all implement Iterator<T> this way already; so does any type you write
yourself:
use std.option;
compound Countdown { num n; }
impl Countdown as Iterator<num> {
pub next() Option<num> {
if self.n <= 0 { ret .None; }
self.n = self.n - 1;
ret .Some(self.n + 1);
}
}
fun main() {
Countdown c = Countdown{n = 3};
for x : c {
// 3, 2, 1
}
}Binding forms, driven by the iterable's own Elem type (T in
Iterator<T>):
- One name (
for x : it { ... }):xbinds toElemdirectly. ForMap, that means the whole(K, V)pair. - Two names (
for a, b :: it { ... }): ifElemis itself a 2-tuple (asMap's is),a/bbind to its two fields - this is howfor k, v :: someMap { ... }gets a real key and value, not a pair. Otherwise, it enumerates:ais a running 0-based count,bisElem- the same form arrays already use. - Destructuring (
for (a, b) : it { ... }): bindsElem's own tuple fields by position, same arity/type rules as an ordinaryletdestructure. Works over anyIteratorwhoseElemis a tuple, not justMap.
Reentrancy: a for loop always iterates a fresh copy of the value
it started from, never the caller's own variable - a type implementing
Iterator<T> directly typically needs a cursor field of its own (like
Vec<T>'s __iter_pos), and this copy-before-iterate rule is what lets
two independent loops over "the same" value, or one nested inside
another over the same value, not corrupt each other's position. Driving
.next() manually (outside a for loop) advances the real value's own
cursor directly, with no such protection - the same as calling any
other mutating method.
This style is common in the standard library (for example std/string.fn,
std/net.fn, and std/fs.fn).
Range (above) is fixed to num, backing the a..b syntax sugar - that
stays exactly as it is. For a range over any other ordered type (dates,
a custom counter, ...), implement Steppable<T> and use StepRange<T>
directly; there's no .. syntax for it, only explicit construction.
// fun:no-run
pub shape Steppable<T> {
succ() T;
reached(T end) flag;
}Steppable<T> is self-referential the same way Iterator<T> is:
impl MyType as Steppable<MyType> binds the shape's own T to the
implementing type itself, so succ() returns a real, concrete MyType
with no separate Self keyword needed.
use std.step_range;
use std.c.io;
compound Day { num n; }
impl Day as Steppable<Day> {
pub succ() Day {
Day d;
d.n = self.n + 1;
ret d;
}
pub reached(Day end) flag { ret self.n >= end.n; }
}
fun main() {
Day start = Day{n = 1};
Day end = Day{n = 5};
for d : StepRange<Day>{start = start, end = end} {
printf("day %lld\n", d.n); // day 1, day 2, day 3, day 4
}
}StepRange<T: Steppable<T>> implements Iterator<T> the same way Range
does, so every binding form and the reentrancy guarantee above apply to
it unchanged.
enum Color { Red, Green, Blue }
fun main() {
Color c = .Red;
fit c {
.Red -> { _ = c; },
.Green -> { _ = c; },
_ -> { _ = c; }
}
}Missing variants may produce fit_non_exhaustive unless _ is present.
See Enums above for fit over data-carrying (tagged-union) enums.
fit matches one subject at a time; it has no multi-value/tuple form. A
comma inside one arm's condition (0, 1 -> { ... }) is not that - it's
an OR of several patterns against the same single subject, and works the
same way for an enum's dot-shorthand (.Red, .Blue -> { ... }):
// fun:no-run
fun warm(Color c) flag {
fit c {
.Red, .Green -> { ret true; }
.Blue -> { ret false; }
}
}Each named alternative counts as its own arm for fit_non_exhaustive
purposes, so a comma-separated arm covering every remaining variant is
still exhaustive with no _ needed. The comma form is restricted to
plain (non-destructuring) patterns - an alternative that binds a payload
(.Circle(r), .Rect(w, h) -> { ... }) isn't allowed, since the arm body
would need consistent bindings across every alternative.
To match on several values together, build a short combined key first and
fit on that:
// fun:no-run
str key = format("{chr}{chr}{chr}", a, b, c);
fit key {
"str" -> { ... }
"num" -> { ... }
_ -> { ... }
}which reads far more clearly than an if a == .. && b == .. && c == .. { ... } elif ... chain once there are more than two or three
combinations to cover.
fit can also recover the concrete type behind a shape-typed value,
dispatched at runtime through the same vtable an ordinary shape method
call already uses:
use std.io;
use std.string;
shape HasArea {
area() num;
}
compound Circle {
num radius;
}
compound Square {
num side;
}
impl Circle as HasArea {
area() num { ret self.radius * self.radius; }
}
impl Square as HasArea {
area() num { ret self.side * self.side; }
}
fun describe(HasArea s) str {
str out = "unknown shape";
fit s {
Circle(c) -> { out = format("circle, radius {num}", c.radius); }
Square(sq) -> { out = format("square, side {num}", sq.side); }
_ -> { }
}
ret out;
}
fun main() {
Circle c;
c.radius = 3;
println(describe(&c));
}- A downcast arm is written bare, with no leading dot:
Circle(c), not.Circle(c)- the dotted form is an enum-variant pattern, this is a different thing.Circlemust be a realimpl Circle as HasArea { ... }for the shape being matched, checked at compile time. - Exactly one binding recovers the whole concrete value as a pointer
(
c: Circle*above), never a payload destructure - a shape isn't a tagged union, so there's nothing to pull apart field by field the way.Circle(x, y, r)would for a data-carrying enum variant. fit_non_exhaustiveapplies the same way it does for an enum: every type registered as implementing the shape anywhere in the program must be covered, or a catch-all_arm is required. Covering the same implementor twice reportsfit_unreachable_branch.- Ordinary shape method dispatch and this downcast use the same runtime representation, so there is no extra cost to make a value fit-able - a shape-typed value already carries a vtable pointer.
- A concrete instantiation of a generic implementor can be named too:
Box<num>(b) -> ...againstimpl Box<num> as Sized { ... }, orWrapper<Circle>(w) -> ...againstimpl Wrapper<Circle> as Sized { ... }. - A bare generic name auto-resolves when it's unambiguous:
Box(b) -> ...(no<Args>) works exactly likeBox<num>(b) -> ...whenBox<num>is the only instantiation ofBoximplementing the shape being matched. Two or more instantiations make the bare form ambiguous - write the generic arguments explicitly to pick one. An unbound generic itself is still rejected outright:impl Box<T> as Sized { ... }has no single concrete vtable to dispatch to at all, so there's no valid arm to write for it, bare or otherwise.
- Purpose: run cleanup logic automatically when the current lexical scope exits.
- Order: LIFO (last
deferruns first). - Forms: expression (
defer close(fd);) or block (defer { log("done"); cleanup(); }). - Scope semantics: a
deferruns when the scope where it appears exits; function-scope defers run beforeretand before implicit function end; loop-body defers run at the end of each iteration; oncontinue/break, defers in the current loop iteration run before control leaves it.
// fun:no-run
use std.io;
fun main() {
num x = 21 + 21;
num y = 0;
// `volatile` comes before `arch`. This runs when compiled on an
// aarch64 host; an x86_64 build would instead use:
// asm volatile arch x86_64 (out y: "=r" = y; in x: "r" = x; clobber "memory") {
// movq %[x], %[y]
// };
asm volatile arch aarch64 (out y: "=r" = y; in x: "r" = x; clobber "memory") {
mov %[y], %[x]
};
println_fmt("y={num}", y);
}- Block form:
asm { ... };. String form:asm "...";(use this for exact formatting/escapes). - Volatile:
asm volatile { ... };prevents reordering/elision. - Architecture guard:
asm arch x86_64 { ... };errors if the target arch mismatches. Supported names:x86_64/amd64,x86/i386,aarch64/arm64,arm. - Operands & clobbers: reference named operands in templates with
%[name]. Outputs come first, then inputs, then clobbers (GCC-style extended asm). - Asm block contents are preserved as raw text (including whitespace and comments). Fun does not validate assembly syntax inside asm blocks; correctness is decided by the downstream assembler/dialect (clang/GAS vs NASM, for example).
- ISA mismatch: AArch64 register names (
x0) will not assemble on x86_64. Guard by arch. - Operand order: x86_64 uses
movq src, dst(AT&T syntax) while AArch64 usesmov dst, src. - Missing size suffix: x86_64
movneeds a size suffix (movb/movw/movl/movq). - Implicit clobbers: if the asm touches memory not listed in
operands, include
"memory". - Named receiver errors: if you move values into locals via asm,
ensure
outtargets are assigned to named locals. - Example:
jmp $may fail under clang/GAS inline asm; label form like1: ... jmp 1bis typically more portable in that pipeline.
- Standard library:
use std.c.io;maps to C standard headers;use std.string;imports Fun-native stdlib modules. - Relative imports:
use ..foo.bar;for user modules. - Import alias:
use mod1 as one;, then call symbols asone.some_fn(). - Duplicate export collisions: import modules that export the same public symbol by aliasing each module and calling through the alias namespace:
// file: mod1.fn
pub fun pick() num { ret 1; }
// file: mod2.fn
pub fun pick() num { ret 2; }
// file: main.fn
use std.io;
use mod1 as one;
use mod2 as two;
fun main() {
num a = one.pick();
num b = two.pick();
println_fmt("a={num} b={num}", a, b);
}- Circular dependency detection: the compiler detects and errors on circular imports.
- Imports are transitive.
use std.io;alone also resolves every namestd.io's own imports declare (std.c.io'sputchar,std.vec'sVec<T>, and so on), not juststd.io's direct public API. This is intentional: the wholeuse-connected graph is merged into one flat program before typecheck and codegen ever run, the same way a#include-based build sees everything a header transitively pulls in. There is no per-file "only what I directly imported" visibility boundary, and none is planned - it would mean giving every file its own scoped symbol table, a structural change disproportionate to the mild "completion offers a name I didn't import directly" symptom this currently produces. - A private (non-
pub) top-level name must be unique across the whole compiled program, not just within its own file. Two files each declaring their own private_collectconflict:'_collect' is already declared as a function in a.fn:1. This is intentional too, for the same reason: the merged program is one flat namespace, and true per-module privacy would need every private name's own C symbol mangled with its declaring file, which is a wide change for real code that already works around this today the same way most C code does - a project-specific prefix (_mymod_collect) on a private helper whose bare name would otherwise collide. The diagnostic names the exact conflict and where, so the fix (rename, or make onepub) is immediate.
- C macros: ALL_CAPS identifiers (
NULL,INT_MAX) are allowed if the right header is imported. - Direct mapping:
use std.c.*;maps to C headers (stdio.h,limits.h, etc.). Fun stdlib modules understd.c.*only declare signatures; C provides the implementations. - Printf formats:
numisint64_tin C. UsePRId64(from<inttypes.h>) or cast tolong longwith%lldwhen printing. - Names the generated C cannot accept are rejected with a clear error
instead of a C compiler failure far from the cause. None of these are
special in Fun itself, but every declared name is emitted to C, so:
- C's reserved words (
do,int,for,while,void, ...) can't name a function, parameter,let/const/global variable, compound, enum, shape, field, type parameter,fitbinding, or aforloop's item, index or destructured names. - Neither can a macro or type the always-included C headers declare:
NULL,EOF,bool,stdin/stdout/stderr,FILE,size_t,INT_MAXand the other limit macros,int64_tand its relatives. Reading one from C stays fine (see C macros above); it is declaring a Fun name with that spelling that is rejected. Function-like macros such asva_argare not affected. - A function with a body can't be named after a function the generated C
already declares through
stdlib.h,string.h,stdio.horctype.h(div,abs,strlen,printf,toupper, ...). Names from headers only included on demand, such asmath.h, are reported as duplicate declarations when the program pulls them in. - A generic function's own name is exempt: it always monomorphizes with
its concrete type arguments (
double<T>becomesdouble__num,double__dec, ...), so it never reaches C bare -fun double<T: num | dec>(T x) T { ret x + x; }is fine. Thestd.c.*files, which mirror C's own names on purpose, are exempt too.
- C's reserved words (
See Platforms & Compilers for C compiler selection and per-platform behavior.
- Type mismatches, undeclared symbols, duplicate declarations, missing imports, and incomplete shape implementations are compile errors.
- Pointer-return,
fitexhaustiveness, redundantfitbranches, unreachable statements, constant assertions, and optional unused-* diagnostics are emitted as warnings (see Warning Controls below).
return_local_ptrfit_non_exhaustivefit_unreachable_branchunreachable_codeassert_constantunused_variable(with-warn-unused)unused_import(with-warn-unused)unused_function(with-warn-unused)unused_compound(with-warn-unused)unused_type_alias(with-warn-unused)missing_return: a non-voidfunction/method that can reach the end of its body without returning a value. Always checked, not gated behind-warn-unused.blocking_fork_deadlock(with-warn-unused): aWaitGroupcreated with a literalwait_group_new(0)(whose internal signal buffer holds only one completion) isdone()'d from tasks spawned byforkinside a loop; the producer can block before any receiver drains it. Size the WaitGroup to the task count.shared_mutable_capture_race(with-warn-unused): a mutable compound with no internalMutex/Channelfield is passed by&into a mutatingasync funthat isforked multiple times (e.g. in a loop), so several tasks mutate the same value without synchronization. Guard it with aMutexor give each task its own copy.Channel/WaitGroup(self-synchronizing) are exempt.integer_literal_out_of_range(with-warn-unused): a compile-time integer literal cannot be represented in the declared arbitrary-width integer type, a value larger than the type's width (u2 x = 5;,i6 y = 100;), or a negative value assigned to an unsigneduN(u8 z = -3;).channel_capacity_overflow(with-warn-unused): more blocking sends are issued into a bounded channel than its capacity with no concurrent receiver, so the producer blocks forever (e.g.let c = channel_new_cap(0, 1); c <- 1; c <- 2; c <- 3;). Conservative: only fires when the capacity and the send count are statically known literals, the channel is never received-from, and noforkruns first.
allow <warning_id>, "reason";suppresses the next emitted warning with that ID, then stops - a later occurrence of the same ID reports normally again.allow <warning_id>s, "reason";(the plural spelling of the same ID) suppresses every emitted warning with that ID for the rest of the file, not just the next one.expect <warning_id>, "reason";/expect <warning_id>s, "reason";follow the same singular/plural split, but compilation fails if that ID is never emitted at all.
allow/expect are statement directives that work inside function
bodies; unused_variable, unused_import, unused_function,
unused_compound, and unused_type_alias may also be controlled at
module scope, anywhere before the imports/declarations they cover. The
reason string is required and documents why the warning is being
allowed/expected. No warning ID needs a plural form registered by
hand: unused_imports, unused_variables, fit_non_exhaustives, and
so on all resolve automatically from the same ID's ordinary English
plural.
fun bad() num* {
expect return_local_ptr, "tracked until allocator refactor";
num x = 1;
ret &x;
}
fun partial(flag x) {
allow fit_non_exhaustive, "legacy branch set, cleanup pending";
fit x {
true -> { }
}
}
fun noisy_helper() {
// Every unused local in here is deliberate scaffolding, not just
// the first one - the plural form covers the whole function.
allow unused_variables, "scaffolding while wiring the real call sites";
num a = 1;
num b = 2;
}
fun main() {
partial(true);
num* p = bad();
_ = p;
noisy_helper();
}See also:
- examples/advanced/warning_allow.fn
- examples/advanced/warning_expect.fn
- examples/advanced/warning_expect_plural.fn
- examples/advanced/return_local_ptr_allow.fn
- examples/advanced/unused_variable_warning.fn
- examples/advanced/unused_variable_allow.fn
- examples/advanced/unused_variable_expect.fn
- examples/advanced/unused_import_warning.fn
- examples/advanced/unused_import_allow.fn
- examples/advanced/unused_import_allow_plural.fn
- examples/advanced/unused_import_expect.fn
- examples/advanced/unused_function_warning.fn
- examples/advanced/unused_function_allow.fn
- examples/advanced/unused_function_expect.fn
- examples/advanced/unused_compound_warning.fn
- examples/advanced/unused_compound_allow.fn
- examples/advanced/unused_compound_expect.fn
- examples/advanced/unused_type_alias_warning.fn
- examples/advanced/unused_type_alias_allow.fn
- examples/advanced/unused_type_alias_expect.fn
- examples/advanced/fit_unreachable_branch_warning.fn
- examples/advanced/unreachable_code_warning.fn
- examples/advanced/assert_constant_warning.fn
- examples/error_cases/warning_expect_unmet.fn (expected compile failure)
use std.io;
compound Point { num x; num y; }
impl Point {
translate(num dx, num dy) {
self.x += dx;
self.y += dy;
}
}
fun main() {
Point p;
p.x = 1; p.y = 2;
p.translate(3, 4);
println_fmt("p=({num},{num})", p.x, p.y);
}- Forward declarations: compounds can reference each other regardless of order.
- Self-referential types: supported via pointers.
- Exhaustive and non-exhaustive pattern matching: with a
_default branch. - CLI tooling: compile, transpile, and run Fun code from the command line, see Tooling for the full reference.