[!NOTE]
A function without an explicit return type returns void implicitly. You may still write
-> void if you find it clearer, but you don’t have to.
A constant whose value names a function is that function under another name, in the same way
that X :: Name; is the type it names. Calling the constant calls the function directly, and
its value is the function’s.
Any top-level function whose first parameter is named self can be called with dot-call
syntax:
// In module point.fels:
pubdistance::fn(self: *Point) ->f64 { ... }
// Calling:
p: Point= .{ 3, 4 };
d := p.distance(); // auto-referenced to `&p`; same as distance(&p)
A *Self method auto-references its receiver, so you call it on a plain value, without an
explicit &. It works on a temporary too: the value is spilled to a stack slot, and the
method gets a pointer to it. The other way round, a method that takes the value is called
through a pointer as it is: self.get() in a *Self method passes *self. Methods also chain
on returned values:
n :=make_point(3, 4).distance(); // call on a temporary
[!NOTE]
A method on a temporary receiver sees a copy, so any change it makes is thrown away
together with the temporary. Chaining is for transforming and reading.
A type parameter declared in the list ($T: type, or $W: Writer with a constraint) is an
argument that the caller may pass or leave to inference. A call passes its type arguments
before the values, and it can pass the $T: type ones, all of them, or none. Each one you
don’t pass is inferred from the values, so max(10, 20) makes T an i32. One that cannot
be inferred has to be passed, like the T of make :: fn($T: type) -> T, which you call as
make(i64).
A pointer type is a type argument like any other, as in arena.new_slice(*Node, n) or
core::sizeof(**Node). Where a type is expected, *Node is that pointer type. That is the
case for the first argument of core::sizeof, core::ptr_from_int and the other builtins
that take one, and for an explicit $T: type argument of a generic function. In any other
argument *p is a dereference, so f(*p) loads through the pointer p.
(core::sizeof(*p) with a variable p is the size of what p points to.)
[!NOTE]
Generics are monomorphized: each unique type argument produces a specialized version
at compile time, so there is no runtime dispatch overhead.
An expression of the form fn(params) -> Ret { body } is a lambda. It is an anonymous
function that you can pass directly to a higher-order function or store in a variable. The
return type is optional and defaults to void.
// As an argument: no need to declare a named predicate.
n := slice::count_where(i32, xs, fn(x: i32) ->bool { return x <0; });
// Stored in a local, then called.
twice :=fn(x: i32) ->i32 { return x *2; };
y :=twice(21); // 42
// A bare function name is itself a value:
sq := square; // square is a top-level fn
z :=sq(8);
Functions are first-class values. A function name (or a lambda) has a function-pointer
typefn(Params) -> Ret, and you can write that type out explicitly:
A lambda in a generic function may use the function’s values and its type parameters, as
the rest of its body may. It is made once per instantiation of the function:
apply::fn(n: usize, f: fn(usize)) { for i in0..n { f(i); } }
A function value is a fat closure{ fn, env }: a function pointer plus a pointer to
its captured environment. A plain (non-capturing) function or lambda has an empty
environment, so it costs no more than a bare pointer. A capturing lambda copies the captured
values into a small environment when it is created.
Where the environment lives follows Fels’s region model:
Inside a zone, a capturing closure’s environment is allocated on the zone’s
Arena. The closure is then valid for the whole zone, and you can store it and hand it
around freely within that scope:
zone scratch: Arena {
base: i32=100;
f :=fn(x: i32) ->i32 { return base + x; }; // env on `scratch`
r :=call_it(f, 7); // safe anywhere in the zone
}
When there is an *Arena parameter, the environment is allocated in that
caller-supplied region. This is region passing, and it lets a factory return an
escaping closure that stays valid for as long as the caller’s arena:
returnfn(x: i32) ->i32 { return x + n; }; // env allocated in `into`
}
zone z: Arena {
add5 :=make_adder(&z, 5);
r :=add5(10); // 15: `add5` outlives `make_adder`, valid for the zone
}
Otherwise, the environment lives on the stack frame that created it. Such a closure
is safe to call and to pass down to a higher-order function, which is the common
case, but it cannot outlive that frame.
[!IMPORTANT]
Returning a capturing closure requires an *Arena parameter, so that the environment
lives in the caller’s region. Without one it is a compile error. The environment belongs
to a stack frame or a zone that ends with the function, so it would dangle:
error: a capturing closure cannot be returned: its captured environment lives
in this function and would be left dangling (take an `*Arena` parameter so the
closure's environment is allocated in the caller's region)
This applies whether you return the closure directly or through a local. Returning a
non-capturing function value is always fine, because it is only a pointer.
[!NOTE]
Lambdas are lifted to ordinary top-level functions at compile time. A non-capturing
lambda becomes a named function and a pointer, and nothing more.
[!NOTE]
Function values also work at compile time. A higher-order call evaluated in #run (or
any comptime context) folds to a constant, for non-capturing and capturing closures alike.
One edge case: an inline lambda used directly as an argument in a top-level constant
initializer isn’t supported yet. Bind it to a local first
(f := fn(x){ … }; K :: #run g(f);) or use a named function.
To call C or system functions, you write an FFI declaration with the #extern attribute:
#extern("kernel32", "GetStdHandle")
GetStdHandle::fn(id: i32) ->usize;
#extern("kernel32", "WriteFile")
WriteFile::fn(
handle: usize,
buf: *constu8,
len: u32,
written: *u32,
overlapped: ?*void,
) ->bool;
[!WARNING]
Calls to external functions are inherently unsafe, because the compiler cannot verify that
the foreign function’s signature or behavior is correct.
The name goes after the optional, the same way while opt |x| and match … |p| bind it.
The older spelling if x := opt { … } still compiles, but with a warning.
You can chain else if, and each one desugars to a nested if inside the else:
In value position, if produces a value. Each branch is a single expression in braces, and
the else is mandatory, because an expression must always yield something:
sign :=if n >0 { 1 } elseif n <0 { -1 } else { 0 };
mode: u32=if enabled { 1u32 } else { 0u32 }; // branch literal takes the type
return base +if hit { bonus } else { 0 }; // anywhere an expression fits
The two branches must have compatible types. The expected type flows into the branches, so
an untyped branch value (.{ … }, a bare .variant) is typed from context, exactly as in a
match expression. (Statement-position if { … } is unchanged. The expression form only
applies where a value is required.)
Optional-unwrap loop: while opt |x| re-evaluates opt on each iteration, binds the
unwrapped payload to x, and exits when opt is null. It is the clean way to walk a linked
structure or drain an iterator:
cur: ?*Node= head;
while cur |n| {
visit(n.val);
cur = n.next;
}
The condition may be any optional, and the |x| binding is itself optional:
while opt { … } loops while opt is non-null without binding the payload.
The index has the type of the bounds. A literal bound takes the type of the other bound, so
for i in 0..xs.len counts in usize and i indexes xs as it is. When both bounds are
literals, as in 0..10, the index takes the type its uses ask for, like a local inferred
from a literal (see Types). When no use asks for one, it is an i32.
for x in it also works over any value whose type has a method next(self: *Self) -> ?T.
Each iteration calls next and binds the unwrapped payload, and the loop stops when next
returns null. That is exactly what while it.next() |x| { … } does, but the loop plumbing
stays out of sight.
Range::struct { cur: i32, end: i32 }
// The iterator protocol: advance and yield, or return null when exhausted.
next::fn(self: *Range) ->?i32 {
ifself.cur >=self.end { returnnull; }
v :=self.cur;
self.cur =self.cur +1;
return v;
}
main::fn() ->i32 {
r: Range= .{ 1, 5 };
sum: i32=0;
for x in r { sum = sum + x; } // 1 + 2 + 3 + 4 = 10
return sum;
}
The index form for x, i in it is available too, and i counts iterations from 0.
Iterating by reference (for &x in it) is not allowed, because a next method yields
values, not addresses.
A collection, a value whose type has a method items(self) -> []T such as Vec, loops over
the slice items() returns. So for x in v is for x in v.items(), and the index and
by-reference forms work as they do on a slice:
v := vec::make(i32, &a);
v.push(10);
v.push(20);
for x in v { total += x; }
for x, i in v { println("v[{i}] = {x}"); }
for&x in v { *x =*x *2; } // writes into the Vec
A type with a next method loops as an iterator, even if it has an items method too.
match does pattern matching on enums and integers. An arm is pattern => body,. In a
match statement the body is a block or a single statement, and in a match expression it is a
value. The parser does not require the comma, but the style is a comma after each arm. A
match on one line may leave out the last one. In a match expression the comma also keeps a
value apart from the next pattern, because x .zero without it reads as x.zero. An arm of
a single statement, such as .a => return 1;, is written with just its own ;:
// Enum matching
match direction {
.north => { println("going north"); },
.south => { println("going south"); },
.east => { println("going east"); },
.west => { println("going west"); },
}
// Several variants that share an arm
match kind {
.buy, .sell => { /* moves shares */ },
else=> { /* the rest */ },
}
// Enum with payload capture (an arm of one variant)
match shape {
.circle |radius|=> { /* use radius */ },
.rectangle |corner|=> { /* use corner */ },
.none => { /* nothing */ },
}
// Integer matching: single values, grouped values, and ranges
[!IMPORTANT]
A match on an enum is exhaustiveness-checked. Every variant must be handled, or an
else arm or a catch-all name must be present. Otherwise the compiler reports an error
(“non-exhaustive match: variant .west is not handled”). A total match (one that
covers every variant) needs no else. Because it is provably exhaustive, it also counts
as returning on all paths, so a function whose body is only a total match over return
arms type-checks without a trailing return. A duplicate arm for the same variant is
rejected. Integer matches are unbounded, so only an else or a catch-all name (k => …)
makes them exhaustive.
[!IMPORTANT]
Match as an expression: in value position each arm is pattern => value, where the
value is an expression and the arms are separated by commas. All arms must unify to one
type, and the match must be exhaustive, so that it produces a value on every path. A
non-exhaustive value-match is an error. Untyped arm values (.{ … }, bare .variant enum
literals) take their type from context (c: C = match …, a return, a call argument).
[!NOTE]
Patterns: an arm can match enum variants (.name, optionally with a |payload|
capture) and integer values (single or grouped 1, 2, 3, negative -1, or characters
'a'). It can match integer ranges (1..5 exclusive, 'a'..='z' inclusive), and
values and ranges mixed in one arm ('0'..='9', '_'). It can also match string
literals (single or grouped), a bare name (which binds the subject as a catch-all), or
be else. Any arm may carry a guardif <bool>. The guard is evaluated after the
binding, and a failing guard falls through to the next arm. A guarded arm does not count
toward exhaustiveness. String compares are length-gated, so a shorter subject is never
read out of bounds.
break exits the innermost loop, and continue skips to the next iteration:
whiletrue {
ifdone() { break; }
ifskip() { continue; }
process();
}
A loop can have a name, written before it with a colon. break name; leaves that loop, and
continue name; goes on with its next iteration, however many loops are in between:
outer: for i in0..rows {
for j in0..cols {
if grid[i][j] == target {
found = .{ i, j };
break outer; // leaves both loops
}
if grid[i][j] <0 { continue outer; } // the next row
}
}
The defers of the loops being left run on the way out, as with a plain break. A name has to
belong to a loop around the break or continue, and a loop inside one may not take the same
name. Loops one after the other may.
defer schedules code to run when the current scope exits. That guarantees the cleanup no
matter how the scope is exited (normal return, error, break, etc.):
// Always defer
defer { cleanup(); }
// Single expression defer
deferclose(handle);
// Defer only on success
defer.ok { commit(); }
// Defer only on error
defer.err { rollback(); }
[!NOTE]
Multiple defers execute in reverse order (LIFO), so the last defer registered runs
first.
// Example: LIFO order
defer { println("first registered, last to run"); }
defer { println("second registered, first to run"); }
An unsafe block disables certain safety checks. You need one for raw pointer operations,
inline assembly and other low-level operations:
unsafe {
raw_ptr :=0x1000as*u8;
*raw_ptr =0;
}
// Unsafe expression
val :=unsafe core::unaligned_read(u64, &x);
As a statement, unsafe must be followed by a block, so unsafe (p as *u8); on a line of
its own is an error. The expression form is for places where the value is used, such as an
initializer, an argument or an assignment.
[!CAUTION]
Unsafe code bypasses the compiler’s safety guarantees, so use it sparingly and audit it
carefully. Incorrect unsafe code can cause undefined behavior, memory corruption, or
security vulnerabilities.
Fels supports compile-time evaluation through a few special directives:
// Compile-time if
#if core::sizeof(usize) ==8 {
// 64-bit platform code
} else {
// 32-bit platform code
}
// Compile-time expression
size :=#runcompute_size();
Directive
Purpose
#if
Conditional compilation based on compile-time constants
#run
Evaluate an expression at compile time (see 08)
#if takes an optional else { }, and else #if cond { } for the next test of a chain. In
a function body it chooses code. At the top level it chooses declarations, by the platform
the program is built for (see docs/08).