Semantics
This document describes the execution model of Nexus.
Evaluation Strategy
Nexus is call-by-value. Each expression runs in full before its result reaches a function or constructor.
Evaluation Order
Strict left-to-right:
- Function arguments:
f(a: e1, b: e2)evaluatese1beforee2 - Binary operators:
e1 + e2evaluatese1beforee2 - Records and constructors: fields evaluated in source order
Label Order Independence
Labeled arguments at call sites may appear in any order. The calls f(b: 2, a: 1) and f(a: 1, b: 2) end up with the same pairing. Argument expressions still evaluate left-to-right in source order, regardless of which labels they carry.
Scoping
Lexical scoping. Bindings are visible in the block where they are defined and in nested blocks.
Shadowing is permitted. An inner let can reuse a name from an outer scope, masking it until the inner block ends.
Sigil Behavioral Semantics
Sigils are no mere annotations; they pin down runtime behavior.
Mutability (~)
- Stack-confined: mutable bindings exist only on the stack of the defining function
- No escape: cannot be returned, stored in heap structures, or captured by closures
- Assignment:
~x <- exprupdates the value - Concurrency: cannot be captured into a thunk (
@) that may evaluate in parallel — preserves stack confinement and prevents data races (see lazy.md)
Linearity (%)
- Exactly-once consumption (composites): must be consumed via function call, pattern match, or return
- Auto-drop (primitives):
i32,i64,f32,f64,bool,char,string,unit(plus their%/&/~wrappers) are released at scope end. See drop.md §Auto-Droppable Types for the full predicate. - Static enforcement: the type checker tracks linear bindings and rejects programs that leak or double-use them
- No discard:
_cannot discard composite linear values - No mutable ref:
~cannot hold linear types
Borrowing (&)
- Immutable view: read-only access without consumption
- Non-consuming: the source binding remains live
- Coercion:
&Tcoerces toTfor reading operations
Closures and Captures
- Lexical captures: lambdas capture immutable bindings from enclosing scope
- No mutable capture: closures cannot capture
~bindings - Linearity propagation: capturing a
%binding makes the closure linear (single-use) - Recursive lambdas: must use an immutable
letbinding with explicit type annotation
Exception Propagation
A throw ends the current computation at once and unwinds the call stack up to the nearest try/catch. The Exn value lands in the catch parameter:
try
throw NotFound(msg: "key")
catch e ->
// e : Exn
match e do
| NotFound(msg: m) -> ()
| _ -> ()
end
end
Exceptions are checked. Any function that may throw must declare throws { Exn }. A try/catch clears Exn from the protected region.
Loops
While Loop
while condition do
body
end
Evaluates condition before each iteration. If the condition is false, exits. The condition must be bool. Returns unit.
For Loop
for var = start to end_expr do
body
end
Desugared to:
let ~var = start
let ~__end = end_expr
while ~var < ~__end do
body
~var <- ~var + 1
end
start and end_expr must be i64. The loop variable is immutable in the body. The range is [start, end_expr), so the upper bound is exclusive. When start >= end_expr, the body never runs.
Match as Expression
Match can appear in expression position. Each arm body produces a value:
let result = match x do
| 1 -> 10
| 2 -> 20
| _ -> 30
end
Every non-diverging arm body must produce the same type. An arm diverges, and so drops out of the unified result type, when its last statement is one of:
return e(function-level return)throw eused as an expression statementlet μx = throw e'(the binding’s RHS never produces a value)
When every arm diverges, the match expression takes a fresh type variable, which the surrounding context pins. See the tail and branchType definitions in type-system-formal.md for the formal carve-out, which if/else and pattern-let reuse.
let result = match x do
| A -> 5
| B -> throw NotFound(path: "x") // diverges — `B` does not pin the result type
end // result : i64 (from arm A)
Concurrency Model
Nexus expresses deferred computation through the @ (thunk) sigil. A thunk let @x = expr suspends expr until forced via @x. To force a list of thunks side by side, opt in through force_all, exported from the lazy stdlib module.
let @p1 = compute1()
let @p2 = compute2()
let xs = force_all(tasks: [p1, p2])
@thunks are unevaluated until forced (@x)- Thunks cannot capture mutable (
~) bindings — the~stack-confinement rule rules out cross-thread aliasing - The current runtime forces each
force_alltask in turn. Parallel execution over WASI threads is tracked as future work. See lazy.md for the dispatch primitives and the migration plan.
Implicit Unit Return
A function whose return type is unit may omit the trailing return (). When the body has no return at all, the compiler tacks on a return () for you:
let greet = fn (name: string) -> unit require { Console } do
Console.println(val: "Hello, " ++ name)
// implicit return ()
end
A function with a non-unit return type still needs an explicit return.
Entrypoint
main Function
Every Nexus program must define a main function with these constraints:
- Signature:
() -> unit - Effects: must be empty (all exceptions handled internally)
- Requirements: may include any subset of
{ PermFs, PermNet, PermConsole, PermRandom, PermClock, PermProc, PermEnv } - Visibility: must not be
export
let main = fn () -> unit require { PermConsole } do
inject stdio.system_handler do
Console.println(val: "Hello")
end
return ()
end
The runtime calls main. Side effects flow through injected handlers. The exit code is 0 on success and non-zero on any error left unhandled.