Skip to main content
Next (unreleased) · docs c218d875 · core f63c1e69 · built 2026-10-26 22:39 UTC

Functions

fun add(a: i64, b: i64) -> i64 {
return a + b;
}

fun main() -> i64 {
return add(2, 3);
}

Named Function Declarations​

Parameter type annotations are optional when types can be inferred from contextL2. The return type follows -> and is also optional — a function with no return annotation and no return expr; returns ()D1. return expr; and bare return; are both valid.

Formal rules
Legality Rule №1​

Named function declarations begin with fun; fun is not an anonymous-function expression introducer.

Referenced by: rfc-0041

Legality Rule №2​

Function parameter and return-type annotations may be omitted when their types can be inferred from context.

Dynamic Semantics №1​

A function with no return annotation and no return expr; returns ().

Program Entry Point​

Changed inv0.14.0RFC-0167the program's main function is checked during typechecking, not lazily at evaluator startup

The root module must declare exactly one function named main, callable with zero arguments, with a body the type checker can type. This is a purely static, declarative fact — answerable from the declaration table alone, with no dependency on program execution — and a violation is a compile-time error, not a runtime one.

Formal rules
Legality Rule №1​

The root module declares exactly one function named main, non-generic, callable with zero arguments, with a body the type checker can type. Violating this is a compile-time error, not a runtime one.

Tested by (4)

Associated Functions​

extend blocks may contain functions with no self parameter. These are called on the type via :: syntaxL1 and serve as the canonical constructor pattern:

struct Point {
x: f64,
y: f64,
}

extend Point {
fun new(x: f64, y: f64) -> Point {
return Point { x = x, y = y };
}
}

fun main() -> i64 {
let p := Point::new(1.0, 2.0);
return p.x as i64;
}
Formal rules
Legality Rule №1​

A function declared in an extend block without a self, &self, or &var self parameter is an associated function and is called through its target type with ::.

First-Class Functions​

GapGAP-FUNCTIONS-001A module can declare several free functions with one name and the compiler resolves calls by exact argument types, but the spec states no rules for it, including whether an overloaded name can be used as a value.

Functions are first-class values and can be assigned, passed, and returnedL2:

fun add(a: i64, b: i64) -> i64 {
return a + b;
}

fun apply(f: |i64| -> i64, x: i64) -> i64 {
return f(x);
}

fun main() -> i64 {
let f := add;
let inc := |x: i64| -> i64 { return x + 1; };
return f(1, 2) + apply(inc, 4);
}

The type of a function or closure is written as |ParamTypes| -> ReturnType (RFC-0154; (ParamTypes) -> ReturnType before v0.13.0). -> and the return type are always present in a written function type.

A named function declared with its own <T> generics (fun identity<T>(x: T) -> T { ... }) may always be called directly (identity(3), identity::<i64>(3)).

Changed inv0.13.0RFC-0138generic named functions are no longer limited to direct calls or explicitly typed contexts

A generic named function may be bound with a bare, unannotated let; the binding stays polymorphic, so its later uses may each instantiate it differently (let alias = identity; alias(3); alias("x");). It may also be passed as a higher-order argument when the receiving parameter position is concrete — for example, apply(identity, 3) when apply's parameter is |i64| -> i64, not itself generic.

Referencing it in a position where nothing pins down a concrete instantiation — a parameter position that is itself still generic in the callee (rank-2), or an expression position with no expected type and no enclosing let — is still T0003. There is also no standalone instantiation-without-calling value form: identity::<i64> not immediately followed by a call is a parse error, not a type error — see TurbofishL3.

A written function type (|T| -> U) is move-only for its by-value useL3, at every nesting depth: a function value may be called through it any number of times, but using it by value twice — let a := f; let b := f; — is a use-after-move, even when the underlying value (a named function, or a closure whose captures are all Copy) is itself copyable (ClosuresL20). Copyability doesn't survive the written type: it's erased the moment such a value flows into a slot whose written type is a function typeL4 — a let / var binding, an ascription, an argument, or a return — and isn't recovered further downstream. A bare generic type parameter is not a written function type and keeps the resolved value's own capability.

GapGAP-FUNCTIONS-003#936RFC-0163A written function type is always move-only; the copy |T| -> U qualifier and the Erased (capability unknown) state are not available.
GapGAP-FUNCTIONS-002#923RFC-0161Callable<A, B> is not defined in std::core, so a function or closure type cannot satisfy an aspect bound and dyn Callable is unavailable.
Formal rules
Legality Rule №1​

Function and closure types use |ParameterTypes| -> ReturnType (RFC-0154); -> and the return type are always written. Neither (ParameterTypes) -> ReturnType (the spelling before v0.13.0) nor fun(ParameterTypes) -> ReturnType is a function-type syntax.

Referenced by: rfc-0041, rfc-0154

Tested by (2)
Legality Rule №2​
Sincev0.13.0RFC-0138

A non-generic named function and a closure are values of their function type and may be bound, passed as arguments, and returned as results. A generic named function (declared with its own <T> generics) may be called directly, bound with a bare unannotated let (staying polymorphic across that binding's own later uses), or passed as a higher-order argument whose receiving parameter position is itself concrete. Referencing it anywhere else that doesn't pin down a concrete instantiation — including a parameter position that is itself still generic in the callee — is T0003.

Referenced by: rfc-0138

Tested by (7)
Legality Rule №3​
Sincev0.13.0RFC-0166

A written function type (|T| -> U) has move-only by-value use-multiplicity at every nesting depth, matching exactly against a copyable one: a value of it may be called (subject to once / var) and moved, but not duplicated by value. A bare generic parameter is not a written function type.

Referenced by: rfc-0166

Tested by (6)
Legality Rule №4​
Sincev0.13.0RFC-0166

A function value the compiler proved copyable is accepted into a written function-type slot by moving; the slot's copyability is not carried by the written type and is not recoverable downstream.

Tested by (2)

Closures​

Anonymous functions are written with the [captures]? once? var? |params| (-> ret)? { body } formL1:

fun main() -> i64 {
let double := |x: i64| -> i64 { return x * 2; };
return double(5);
}

A closure parameter may be annotated with an open record whose tail is anonymous (|p: { x: i64, .. }| { p.x }); the closure is then polymorphic in that row and accepts records of different shapes (open rowsGenerics L5).

Capture lists​

A closure that reads an outer binding captures it. A capture list […] before the parameter list names each captured binding with a specifier:

fun main() {
var count := 0;
let cfg := Config::load(); // non-Copy, read-only in the closure
let name := "log"; // non-Copy, moved in

let handler := [&var count, &cfg, name] var |req: Request| -> Response {
count += 1;
route(req, cfg, name)
};
}
  • [&var x] captures x by exclusive reference; the body may read and write it.
  • [&x] captures x by shared reference; the body may only read it.
  • [x] captures x by value — a copy for a Copy binding, a move for a non-Copy one (the outer binding is consumed).
  • [x.clone()] captures an explicit independent copy of a Clone binding, leaving the outer binding usable.

The list is required whenever the closure references a free non-Copy local, or captures anything by & / &varL6; it is omissible only when every free variable is a Copy binding captured by value, or there are none. When present it is exhaustiveL7: every free local binding the body references must appear. Module-level functions, constants, types, and aspects are not captures and never appear in the list.

A &var capture requires the outer binding to be declared varL13, and a closure literal cannot reference its own let bindingL13. A [&x] capture is read-only: assigning to it, or taking &var of it, in the body is a compile error at the captureL21, not a silent mutating upgrade. Capturing a free variable of a type parameter T follows the bounds in scope at the closure's definitionL17 — unbounded T is non-Copy for every instantiation. When an inner closure captures a binding that is itself an enclosing closure's capture, the enclosing closure must list it, an inner [s] cannot move out of an enclosing [&s] borrow, and an inner [s] that moves an enclosing-held binding makes the enclosing closure onceL22; an inner & / &var of an enclosing by-value capture is an interim rejectionL11.

Call multiplicity and the mutation axis​

A closure's function type carries two written qualifiers besides its parameter and return types:

  • once — invoking the closure consumes one of its captures. Written when the body moves a capture out (returns it, or passes it by value to something that takes ownership). Omitting it when the body consumes a capture is an errorL8; the default is many (reusable).
  • var — invoking the closure mutates a capture. Written when the body assigns to a by-value capture, takes &var of one, or calls a &var self method on one, and always when the closure captures [&var x]L25, regardless of what the body does through it. The default is reading.

The two qualifiers are order-insensitive as a type spellingL24; in a closure literal the fixed order is [captures] once? var? (params)L23. An unqualified literal in a typed position takes its once / var from the expected typeL18 rather than defaulting and then failing. Verification runs in a L19; the two axes are checked independently.

A function value may be used where a less permissive multiplicity is expectedL9 — a many value satisfies a once slot, a reading value satisfies a var slot, a Copy value satisfies a non-Copy slot — at first-order argument, ascription, field-init, and return positions. The reverse is rejected. A conditional's type is the least-permissive of its arms, each arm widening to it. Widening changes only the static typeD12: a widened reading value keeps its plain call behaviour, and a var-typed field that holds one is thereafter observed var, with no re-narrowingD14.

Whether a closure value is Copy is exactly whether every capture is CopyL20; a Copy closure is necessarily many.

A mutating call needs exclusive access to the closure value for the call's durationL10: the callee must be an owned binding, an owned temporary, an exclusive projection off one, or a &var parameter — not a shared-& callee. Overlapping and reentrant mutating calls on the same closure value are rejectedD9. If a var call exits early via ? or return, its partial mutations persist and the closure stays callableD13; a panic is uncatchable and ends the process, so no post-exit state is observable.

Capture semantics​

By-value capture of a non-Copy binding moves it into the closure at creation, consuming the outer binding; a Copy binding is copied; the captured environment is built once and not re-cloned per callD5.

fun make_counter() -> var || -> i64 {
let n := 0;
[n] var || -> i64 { n += 1; n } // `n` moved in; writes persist
}

fun main() -> i64 {
var c := make_counter(); // `var`: a `mutating` call is a `&var self`-shaped borrow of `c` (legality-10)
c();
c() // returns 2 — state lives in `c`'s environment
}

A mutating closure's writes to its captures persist across callsD7; a reading closure reads its environment in place. Copying a Copy mutating closure gives the copy independent environment stateD8 — the copies do not share a counter.

A [&x] / [&var x] capture stores a reference in the environment; the borrow is held for the closure value's whole lifetime. Captured owned values are dropped when the closure value is droppedD11, in capture-list order.

Closures satisfy no aspectsL12: ==, <, .clone(), and other aspect-gated operations on closure values do not type-check — including Share once RFC-0158 (1-under-review) adds it. A closure's Send / Sync follows the aggregate rule over its capturesL14; a mutating closure is not Sync.

Formal rules
Legality Rule №1​

An anonymous function is written as an optional capture list […], optional once and/or var qualifiers, a pipe-delimited parameter list |…|, an optional -> ReturnType annotation, and a body block. It may appear wherever an expression is accepted. (RFC-0154: before v0.13.0 the parameter list was parenthesized and -> was written before every body; |…| self-disambiguates, so -> now appears only with a return type.)

A parameter's type annotation is optional. An unannotated parameter takes its type from the expected function type (an argument position, an annotated let, a struct field with a written function type) or, failing that, from the body, exactly as an omitted return type does. A closure whose parameter stays fully generic is polymorphic (let id := |x| { x }). A parameter that nothing determines is T0002.

Referenced by: rfc-0041, rfc-0154

Tested by (2)
Legality Rule №2​

A closure literal is introduced by its |…| parameter list; the -> is written only when a return type is (RFC-0154, superseding RFC-0041's rule that -> precede every body). A bare block { … } with no preceding |…| is a block expression, not a closure.

Referenced by: rfc-0041, rfc-0154

Legality Rule №3​

A zero-argument closure is written || { body } (RFC-0154; () -> { body } before v0.13.0). A bare block { … } is not an anonymous function.

Referenced by: rfc-0041

Legality Rule №4​

The former anonymous fun(parameters) -> return_type { body } spelling is rejected.

Referenced by: rfc-0041

Legality Rule №5​

A capture list is [ followed by zero or more comma-separated capture items and ]. A capture item is &var ident, &ident, ident, or ident.clone(). A binding may not appear more than once in the list, under any combination of specifiers. The literal prefix order is legality-23L23; the function-type spelling's order rules are legality-24L24.

Referenced by: rfc-0050

Legality Rule №23​

In a closure literal the prefixes appear in one fixed order: capture list, then once, then var, then the parameter list. var once, a qualifier before the capture list, and a capture list placed after a qualifier are parse errors — even though the corresponding function type spelling is order-insensitive (legality-24L24).

Legality Rule №24​

As a function type spelling the once and var qualifiers are order-insensitive: once var |T| -> U and var once |T| -> U denote the identical Type::Fun. The fixed order of legality-23L23 is a grammar rule for closure literals only.

Referenced by: rfc-0153, rfc-0154

Legality Rule №6​

A closure must carry a capture list if its body references a free non-Copy local binding, or captures any binding by & or &var. Referencing a free non-Copy local with no capture list is a compile error. A closure whose only free variables are Copy bindings used by value, or which has no free variables, may omit the list.

Referenced by: rfc-0050

Tested by (5)
Legality Rule №7​

When a closure has a capture list, every free local binding its body references must appear in the list, with a specifier consistent with how the body uses it. A referenced free local absent from a non-empty list is a compile error. Module-level functions, constants, types, and aspects are resolved by ordinary name resolution and are never capture items.

Tested by (15)
Legality Rule №8​

once is a written qualifier, verified against the body at the closure's creation site; the default is many. A closure whose body moves a non-Copy capture out — returns it, or passes it by value to something that takes ownership — written without once, is a compile error naming the offending capture and the fix (add once, or stop moving the capture).

Referenced by: rfc-0134

Legality Rule №25​

var is a written qualifier, verified against the body at the closure's creation site; the default is reading. A closure whose body assigns to a by-value capture, takes &var of one, or calls a &var self method on one — and always a closure that captures any binding [&var …], regardless of what the body does through it — written without var, is a compile error naming the offending capture and the fix (add var, stop the mutation, or capture [&x] instead).

Referenced by: rfc-0153

Tested by (2)
Legality Rule №9​

A function value of call multiplicity m, mutation u, and Copy-ness c satisfies a slot requiring m', u', c' when m is at least as permissive as m' (many ≥ once), u at least as permissive as u' (reading ≥ var), and c at least as permissive as c' (Copy ≥ non-Copy). The reverse — a less permissive value into a more permissive slot — is rejected.

Referenced by: rfc-0134, rfc-0152

Tested by (2)
Legality Rule №15​

The satisfaction rule of legality-9 applies at first-order positions only: a function-typed argument passed to a function-typed parameter, a let / field ascription, a struct-field initializer, and a return. Below the first level of function-type nesting an exact match is required.

Referenced by: rfc-0152

Legality Rule №16​

A conditional or match expression whose arms are function-typed has, as its type, the least-permissive arm type under legality-9's order, and each arm is widened to it. A diverging (!-typed) arm does not contribute. A join that would require narrowing an arm is the ordinary type mismatch.

Referenced by: rfc-0152

Legality Rule №10​

A mutating call e(args) requires e to denote a place the caller can exclusively borrow for the call's duration: an owned var binding, an owned temporary, an exclusive (&var / owning) projection off one, or a &var parameter — the ordinary &var self receiver rule. An owned but non-var (let) binding, or any shared-& callee (a &Self / &self receiver, a place reached through a & step, or an &-captured closure), is a compile error. This holds for every mutating closure, whether it mutates its own by-value captures or only drives mutation through a captured &var.

Referenced by: rfc-0153

Legality Rule №11​

An inner closure may not capture, by & or &var, a binding that is a by-value capture of an enclosing closure. It may capture such a binding by value ([s], which moves it out of the enclosing closure's environment). This restriction is lifted when the borrow checker lands.

Legality Rule №12​

A closure value satisfies no aspects: ==, <, .clone(), and other aspect-gated operations on a closure value are a compile error. The only way to duplicate a closure value is the by-value copy available when it is Copy (legality-20L20). Structural equality of two function types is a type relation and does not make their values comparable.

Legality Rule №13​

A [&var ident] capture requires ident to be a var binding; capturing a non-var binding by &var is a compile error. A closure literal cannot reference its own let binding — the name is not in scope inside its own initializer.

Referenced by: rfc-0050

Legality Rule №14​

A closure value is Send (respectively Sync) when every one of its captures is Send (respectively Sync), applying the reference rules for &T / &var T captures. A mutating closure value is additionally not Sync.

Legality Rule №17​

Whether a captured free variable of type parameter T is Copy is decided from the bounds in scope at the closure's definition, not re-decided per instantiation. A capture of an unbounded T is non-Copy: [t] moves it, and a body that moves it out makes the closure once for every instantiation of the enclosing generic, T = i64 included. A definition wanting the copyable behaviour adds T: Copy, after which [t] is a copy and consumes nothing.

Legality Rule №18​

An unqualified closure literal in a position with an expected function type — a let or parameter ascription, a struct-field initializer, a return, or the tail expression of a typed block — is checked against that type's once / var qualifiers rather than taking the many / reading default and then failing. When the expected type does not fix a qualifier (an unresolved inference variable, or a bare generic parameter), the literal takes the default and legality-9L9 widening resolves any remaining gap at the concrete site.

Legality Rule №19​

A closure literal is resolved in a fixed stage order: (1) capture classification (referenced free variables, each one's Copy-ness, whether a list is required, and — when present — its exhaustiveness and specifier-match); (2) use_multiplicity (legality-20L20); (3) once verification (legality-8L8); (4) var verification (legality-25L25). The first failing stage is reported; later stages are suppressed. Stages 3 and 4 are independent: a body that both consumes and mutates without the qualifiers is reported against both.

Referenced by: rfc-0050

Legality Rule №20​

A closure value is Copy exactly when every one of its captures is Copy. [x] of a Copy binding and [&x] (a shared reference is Copy) preserve it; [x] of a non-Copy binding, [x.clone()] of a non-Copy type, and [&var x] (an exclusive reference is not Copy) make the closure non-Copy. A Copy closure is necessarily many — it holds nothing non-Copy for a call to consume.

Referenced by: rfc-0134

Legality Rule №21​

A closure that captures x by shared reference [&x] and whose body assigns to x, takes &var x, or calls a &var self method on it is a compile error at the capture — the closure is not silently reclassified mutating. The fix is to capture [&var x], which requires x to be a var binding (legality-13L13) and makes the closure mutating (legality-25L25).

Legality Rule №22​

When an inner closure captures a binding s that is itself a capture of an enclosing closure: the enclosing closure must list s in its own capture list; an inner [s] requires the enclosing closure to hold s by value (naming an enclosing [&s] / [&var s] capture is a move out of borrowed content error); and an inner [s] that moves an enclosing-held s makes the enclosing closure once (legality-8L8), even if the inner closure is never called. An inner [&s] / [&var s] does not change the enclosing closure's multiplicity, but is subject to legality-11L11's interim borrow restriction.

Referenced by: rfc-0050

Dynamic Semantics №1​

When a closure is created, each free variable named in its capture list (or, for a listless closure, each free Copy variable) is placed into the closure's environment according to its specifier: [x] moves a non-Copy value / copies a Copy value, [x.clone()] stores an independent copy, [&x] / [&var x] store a reference.

Referenced by: rfc-0006

Dynamic Semantics №2​

A reading closure does not modify its environment; a mutating closure modifies it in place. In neither case does a write inside the closure body affect an outer binding that was captured by value — the closure operates on its own environment copy.

Referenced by: rfc-0006

Dynamic Semantics №3​

Closures that capture the same binding by [&x] / [&var x], or that capture the same reference value, observe the same referent; a write through an exclusive reference by one closure is visible through the others.

Referenced by: rfc-0006

Tested by (3)
Dynamic Semantics №4​

A closure that escapes its defining function while holding a captured owned value keeps that value alive; the environment travels with the closure value. A closure holding a captured reference cannot outlive the referent (checked by the borrow checker when it lands; unenforced before then).

Referenced by: rfc-0006

Dynamic Semantics №5​

Capturing a non-Copy binding by value ([x]) moves it, consuming the outer binding; a Copy binding captured by value is copied; [x.clone()] produces an independent copy regardless of Copy-ness. The captured environment is constructed once, at closure creation, and is not re-cloned per call.

Referenced by: rfc-0157

Tested by (19)
Dynamic Semantics №7​

A mutating closure's assignments to its by-value captures are retained in its environment and are visible to subsequent calls of the same closure value — the closure holds private mutable state.

Referenced by: rfc-0153

Dynamic Semantics №8​

Copying a closure value whose captures are all Copy copies its environment. The copies have independent environment state: a mutating call on one does not affect the other.

Referenced by: rfc-0153

Dynamic Semantics №9​
GapGAP-OWNERSHIP-001#1293RFC-0122RFC-0067Nothing prevents a shared and an exclusive reference to the same place from coexisting.

For the dynamic extent of a mutating call the callee place is exclusively borrowed. A second mutating call on the same closure value reached from inside the first — directly or through a structure the body can reach — is rejected: before the borrow checker lands, as a runtime error; after, as a static borrow conflict.

Referenced by: rfc-0153

Dynamic Semantics №10​

A once call consumes the callee at the call expression, before the body runs. Any later use of that closure value is a moved-value error, whether the body returned normally or exited early.

Referenced by: rfc-0134

Dynamic Semantics №11​
LimitationLIMIT-EVALUATION-005#261RFC-0071closure environments are not destroyed until destructors run.

When a closure value is dropped, its environment is dropped: each owned capture is dropped in capture-list order, as a struct's fields are. A once-consumed or partially-moved environment drops only its still-owned captures.

Exempt from fixture coverage — blocked on metel-core#261: Destructor invocation and observable drop order are scheduled separately. Non-empty drop bodies are rejected until #261 lands, so a fixture cannot observe closure-environment field destruction or its order.

Dynamic Semantics №12​

Multiplicity widening (legality-9L9) changes only the static type at the slot, never the closure value's runtime behaviour. A reading closure value widened into a var-typed slot is still invoked by the plain, non-exclusive call path and consults no in-call flag, because call lowering branches on the closure value's own mutation axis, not on the slot type. A many value in a once slot is likewise not consumed by the call.

Tested by (2)
Dynamic Semantics №13​

An early exit from a mutating call is a ?-propagated Err or None, or an early return, not a panic — a panic is uncatchable and leaves no observable post-exit state (runtime.mdPanics D1). On such an exit from a var (non-once) call, mutations already made stay in the environment, the exclusive borrow and the in-call flag release as the frame returns, and the closure remains an ordinary, callable value; there is no rollback, and the next call runs the body from the top over that state. A once / once var closure was already consumed at the call expression (dynamics-10D10) regardless of how the body exited; its still-owned fields are dropped when the value goes out of scope.

Referenced by: rfc-0182

Dynamic Semantics №14​

Storing a reading closure into a var-typed field, or returning it where a var function type is named, yields a value thereafter observed as var: every later read of that field or result is a var value that callers must invoke under exclusive access (legality-10L10), even though the underlying closure never mutates. The coercion is one-way — there is no automatic re-narrowing back to reading.

Turbofish​

Sincev0.8.0

When a generic function's type parameters cannot be inferred from the arguments, they can be specified explicitly with turbofish syntax: name::<T, U>(args)L1.

fun identity<T>(x: T) -> T { x }

fun main() -> i64 {
let x := identity::<i64>(42);
return x;
}

Turbofish is most useful when two or more independent type parameters must be pinned at the call site — for example, a zip function that pairs elements from arrays of different types:

fun zip<A, B>(a: [A], b: [B]) -> [(A, B)] { /* ... */ }

fun main() {
let pairs := zip::<i64, String>([1, 2], ["a", "b"]);
}

Type ascription (: T) remains available for annotating the result type. Turbofish and ascription can be used together:

let result := parse::<i64>("42") : Option<i64>;
Formal rules
Legality Rule №1​

A generic call may supply explicit type arguments with name::<T, U>(arguments), pinning each named parameter to the given type. A pinned type must satisfy that parameter's own bounds (e.g. T: Display).

Referenced by: rfc-0023

Legality Rule №2​

The call's arguments must unify with their pinned types exactly as they would with an inferred one.

Referenced by: rfc-0023

Tested by (3)
Legality Rule №3​

Turbofish is a call-postfix production, fused to the immediately following call's parentheses. There is no standalone instantiation-without-calling value form: name::<T> not immediately followed by (arguments) is a parse error.

The ? Operator​

SinceMatching-error ?v0.1.0From-based error coercionv0.4.0? on Option<T>v0.15.0RFC-0182

Inside a function returning Result<T, E>, ? propagates errors earlyD2:

fun parse_int(s: String) -> Result<i64, String> {
if (s == "21") {
return Ok { value = 21 };
}
return Err { error = "not a number" };
}

fun parse_and_double(s: String) -> Result<i64, String> {
let n := parse_int(s)?; // returns Err early if parse_int fails
return Ok { value = n * 2 };
}

fun main() -> i64 {
match (parse_and_double("21")) {
Ok { value } => value,
Err { error } => 0,
}
}

? desugars to: if the expression is Err(e), return Err(E2::from(e)) immediatelyD2 (where E2 is the enclosing function's error type); otherwise unwrap to the Ok valueD1.

The inner expression's error type E1 and the function's return error type E2 must satisfy E2: From<E1>L2. When E1 == E2 no conversion is performed. When they differ, From::from is called automatically on the error value before re-wrapping in Err.

? also applies to Option<T> in a function or closure returning Option<U>: e? is the Some value, or returns None from the enclosing function. The operand's type constructor must match the enclosing return type's (legality rule 1L1); mixed use is a type error (T0001) whose help line names the bridge, .ok_or(...) for an Option in a Result function and .ok() for a Result in an Option function.

fun sum_first_two(xs: List<i64>) -> Option<i64> {
let a := xs.get(0)?; // Option<i64> -> i64, or return None
let b := xs.get(1)?;
Some { value = a + b }
}

fun half(r: Result<i64, String>) -> Option<i64> {
let n := r.ok()?; // Result -> Option, then `?`
Some { value = n / 2 }
}

fun main() {
var xs: List<i64> := List::new();
xs.push(3);
xs.push(4);
assert(sum_first_two(xs).unwrap_or(0) == 7);
assert(half(Ok { value = 8 }).unwrap_or(0) == 4);
}

An early None exit leaves the enclosing function or closure body, not the block containing the ?.

GapGAP-FUNCTIONS-004RFC-0182? works on the core Result and Option and on no other type; a user-defined type cannot be propagated with ?. A future Try aspect would generalise the rule.
Formal rules
Legality Rule №1​

The ? operator requires an operand of type Result<T, E> or Option<T> whose type constructor equals that of the enclosing function or closure's return type; any other operand, or any other pairing, is a type error. When the return type is not yet known, the operand's constructor fixes it.

Referenced by: rfc-0182

Tested by (6)
Legality Rule №2​

For a Result operand, the enclosing function's return type must be Result<U, E2>, and the operand error type E1 must equal E2 or satisfy E2: From<E1>. An Option operand has no error type and no conversion.

Dynamic Semantics №1​

Evaluating Ok { value }? produces value and evaluation continues in the enclosing function.

Dynamic Semantics №2​

Evaluating Err { error }? immediately returns Err { error } from the enclosing function; when the error types differ, the returned error is E2::from(error).

Dynamic Semantics №3​

Evaluating Some { value }? produces value; evaluating None? immediately returns None from the enclosing function or closure body, dropping every live binding as an ordinary return does.

Tested by (3)

Native Functions (Standard Library Only)​

Standard library declarations may be marked native, binding them to an implementation provided by the host interpreter instead of a Metel body:

// from std::core — not writable in user code
native(@std.core.println) public fun println<T>(x: T);
native(@std.core.clock) public fun clock() -> i64;

A native declaration has no body — it ends with ; instead of a block. The @-path inside the parentheses is the binding key that selects the host implementation. The form is also valid on methods inside extend blocks; for example, the primitive Display implementations in std::core are declared this way:

extend i64: Display {
native(@std.core.to_string) fun to_string(&self) -> String;
}

native is reserved for the standard libraryL1. Using it in any module outside the std namespace is a compile error, and user projects cannot place modules under std:: (see §Modules). From the caller's side, native functions are indistinguishable from ordinary functions: they are imported, typechecked, and called exactly like any other declaration — the binding key is an implementation detail of the standard library's source.

Native declarations must annotate every parameter typeL3; an omitted return type means the function returns ().

Formal rules
Legality Rule №1​

Only a declaration in the std namespace may use the native modifier; a native declaration in a user module is rejected.

Legality Rule №2​

A native declaration has a dotted @ host-binding key and no Metel body: it ends with ;.

Legality Rule №3​

Every native-function parameter has an explicit type annotation. An omitted return type denotes ().

Known gaps​

knownGAP-FUNCTIONS-001 — Free-function overloading is implemented but the Language Spec does not describe it. A module can declare several free functions with one name and the compiler resolves calls by exact argument types, but the spec states no rules for it, including whether an overloaded name can be used as a value. Open record ↗ Architecture limitation: LIMIT-TYPE-CONSTRUCTION-002 ↗plannedGAP-FUNCTIONS-002 — Callable<A, B> does not exist, so a function type cannot be bounded by an aspect. Callable<A, B> is not defined in std::core, so a function or closure type cannot satisfy an aspect bound and dyn Callable is unavailable. Open record ↗plannedGAP-FUNCTIONS-003 — Written function types are move-only, with no copy qualifier or Erased state yet. A written function type is always move-only; the copy |T| -> U qualifier and the Erased (capability unknown) state are not available. Open record ↗knownGAP-FUNCTIONS-004 — ? is defined for the core Result and Option only; there is no Try aspect. ? works on the core Result and Option and on no other type; a user-defined type cannot be propagated with ?. A future Try aspect would generalise the rule. Open record ↗