Functions & Lambdas

Functions and Lambdas

Sun supports both named functions and anonymous lambda expressions.

Named Functions

Functions are defined using the function keyword. They are hoisted and available throughout their module:

function add(a: i32, b: i32) i32 {
    return a + b;
}
 
function main() i32 {
    println(add(3, 4));  // 7
    return 0;
}

A function declaration must be a direct child of the program root or a module, including a nested module. A function, method, constructor, lambda, or control-flow block cannot contain another function declaration. Move the helper to module scope and pass its state as explicit parameters, or use a lambda when lexical capture is required.

A method of a class or interface opens with method instead (see Classes); function declares free functions only.

There are no implicit returns. A function whose signature promises a value must leave through an explicit return on every path — a trailing expression is not a return, and a body that can fall off the end is a compile error:

function f() i32 { 42; }             // ❌ ERROR: can reach the end without a value
 
function g(c: bool) i32 {
    if (c) { return 1; }             // ❌ ERROR: falls through when c is false
}
 
function h(c: bool) i32 {
    if (c) { return 1; } else { return 2; }   // ✓ every path returns
}

The same rule applies to methods and lambdas. C and C++ merely warn here and leave the fall-through as undefined behavior; Sun rejects it outright.

A parameter declared ref T borrows the caller's value and may change it; one declared const ref T borrows it for reading only. A constant, or a value already borrowed with const ref, can only be passed to the second kind. See Constant references.

function total(v: const ref Vec<i32>) i32 {
    var sum: i32 = 0;
    var i: i64 = 0;
    while (i < v.size()) { sum = sum + v[i]; i = i + 1; }
    return sum;
}

Entry point

A program starts at main, which takes either no parameters or exactly two — the argument count and the argument vector:

function main() i32 { return 0; }
 
function main(argc: i32, argv: raw_ptr<raw_ptr<i8>>) i32 {
    return argc;
}

raw_ptr<raw_ptr<i8>> is C's char**. The compiler dispatches on the number of parameters, so those are the only two shapes it accepts.

An AOT-compiled main must return i32 or void. Under the JIT it may also return i1, i8, i16, i64, f32, f64 or a string — useful for scripts and tests.

Arguments after the script file, or after --, are passed through:

sun script.sun -- one two      # argc = 3, argv[0] = "script.sun"
sun -c -o prog script.sun && ./prog one two

argv[0] is the script path under the JIT and the executable's path after AOT compilation.

Sun does not discover the arguments on its own, so anything that wants them has to be handed argc and argv from main. Reading /proc/self/cmdline would work for a compiled binary but not under the JIT, where the running process is the compiler and its argv is the compiler's. Env.args turns the pair into a Vec<String>:

using std;
using std.env;
 
function main(argc: i32, argv: raw_ptr<raw_ptr<i8>>) i32 {
    var alloc = make_heap_allocator();
    var environment = Env(alloc);
    var cli = environment.args(argc, argv);
    for (var arg: ref String in cli) { println(arg); }
    return 0;
}

Function Pointers

A named function is also a first-class, non-null function pointer. Its type is written with function, followed by a type-only parameter list and its return type:

function double(x: i32) i32 { return x * 2; }
 
function apply(callback: function (i32) i32, value: i32) i32 {
    return callback(value);
}
 
function choose() function (i32) i32 {
    return double;
}
 
var callback: function (i32) i32 = double;
callback = choose();
var answer = callback(21);

Function pointers can be stored in locals, globals, fields, and enum payloads, and can be passed and returned. They occupy one word and copy like a primitive value. A pointer has no environment: lambdas and bound methods remain distinct lambda values and do not convert to function pointers.

Parameter names are not allowed inside a function-pointer type. A fallible pointer returns a result enum like any other function:

var risky: function (i32) SystemResult<i32> = may_fail;

The result is part of the ordinary return type. A function returning i32 does not implicitly convert to one returning SystemResult<i32>; an adapter must explicitly return SystemResult.Ok(value). Match the result to handle failures, or use try risky(value) inside a function with a compatible result type.

An overloaded function name needs an expected pointer type so the compiler can select one signature:

function parse(value: i32) i32 { return value; }
function parse(value: f64) i32 { return 0; }
 
var parse_int: function (i32) i32 = parse;

A bare overloaded name without type context is ambiguous. Generic functions must still be called with a specialization and cannot currently be taken as bare function pointers.

Lambdas

Lambdas are anonymous functions written with a parameter list and fat arrow. No keyword precedes the parameter list; lambda is an ordinary identifier. A literal must be assigned to a variable or used immediately:

function main() i32 {
    var square = (x: i32) => i32 { return x * x; };
    println(square(5));  // 25
    return 0;
}

A lambda may declare lifetime parameters before its parameter list. The names scope over its parameter and return types and over annotations in its body, just as they do for a named function:

var store = <'a>(
    cb: <'a>(i32) => i32,
    dst: ref 'a Holder
) => void {
    dst.set(cb);
    return;
};

At every call, the compiler requires the callback environment to outlive the destination associated with the same name. An optional capture list follows the binder: <'a> [ref count](...) => .... Lambda literals may declare lifetimes, but they do not declare type parameters.

Captures

A lambda may use variables from its enclosing scope. By default they are captured by value — the lambda gets a private, read-only copy. To mutate a captured variable (or to capture a class, interface, or array at all), declare it in a bracketed by-reference capture list before the parameter list (and after its lifetime binder, when present):

function main() i32 {
    var count: i32 = 0;
    var tick = [ref count]() => void {
        count += 1;       // mutates the original
    };
    tick();
    tick();
    return count;         // 2
}

A capture the lambda only reads is written [const ref x]. It borrows the variable read-only, so writing it inside the lambda is a compile error:

function main() i32 {
    var limit: i32 = 10;
    var under = [const ref limit]() => bool {
        return limit > 5;     // reading is fine; limit = 5 would not compile
    };
    return under() ? 1 : 0;
}

Capture rules:

  • Scalars (integers, floats, bool) capture by value by default. Mutating a by-value capture is a compile error with a hint to add [ref x].
  • Compound types (classes, interfaces, arrays) must be captured with [ref …] or [const ref …] — copying them into a closure would silently break aliasing.
  • Globals are accessed directly and cannot appear in a capture list.
  • A [ref x] capture registers a mutable borrow of the variable for as long as the lambda value is in scope, so conflicting refs — including a second lambda capturing the same variable — are rejected.
  • A [const ref x] capture registers a shared borrow instead, so several lambdas may capture the same variable at once. Everything reached through it is read-only: fields cannot be assigned, and only const method methods can be called.
  • A lambda with a capture list holds state in the enclosing frame — pointers into it for [ref …] captures, the environment itself for owned ones — and this shows in its type: it is a <'_>(…) => … lambda, not a plain (…) => … one (see Lambda Types). It cannot be returned from the function: the frame it depends on would be gone when the lambda runs. Passing it to spawn is fine: the thread is joined when its handle's scope ends, while the frame is still alive — and the handle inherits the restriction, so it cannot be returned, stored in a field, or passed by value (see Threads).
  • this cannot be captured, in the list or implicitly. A lambda that needs the object's data reads the fields it needs into locals and captures those, takes them as parameters — or the method itself is bound as the callable (this.method, below).

Semantic Differences

Aspectfunctionlambda
NameRequired, becomes the function's identityAnonymous (empty name internally)
DeclarationDirect child of the root or a moduleExpression
First-classYes, as a non-null environment-free pointerYes, with an optional captured environment
RecursionCan call itself by nameMust reference the variable it is assigned to
HoistingAvailable throughout its moduleOnly after assignment

When to use which:

  • Use function for named operations that need no captured environment
  • Use a lambda when the callable needs lexical captures or a bound receiver

Lambda Types

A lambda type annotation uses the fat arrow: (i32) => i32. A fallible lambda returns an enum, for example (i32) => SystemResult<i32>. It is distinct from the environment-free function (i32) i32 pointer type.

The plain form accepts only environment-free lambdas: values with no capture list and no bound receiver. A lambda with a capture list, or a bound method, carries an environment in a stack frame and has the type <'_>(i32) => i32:

function apply(cb: <'_>(i32) => i32, dst: ref Holder) i32 {
    dst.bump();
    return cb(1);      // cb may be called, but its lifetime is unrelated to dst
}

'_ is a true anonymous lifetime. Every occurrence is fresh and relates to nothing else in the signature. That is useful for callbacks that are only called: unlike the retired [ref] spelling, the compiler does not guess that the callback might be stored into every ref argument or receiver.

The marker widens one way: an environment-free lambda is accepted wherever a <'_> lambda is expected, never the reverse. A frame-bound lambda type may be used for parameters, locals, fields, and generic arguments. It cannot be an anonymous return type, and a global cannot have it. The restriction is transitive, so a class with a frame-bound lambda field and a Vec<<'_>() => i32> are frame-bound too.

Inside a callee, an anonymous callback may be called, forwarded to another anonymous parameter, or kept in local storage that dies before the call returns. It cannot be stored into this, a ref argument, or any other destination that may outlive the call: '_ makes no promise that its environment lives that long. A callback-storing API must name that relationship.

Named Lifetimes

A lifetime name is a Rust-style leading-apostrophe name declared in angle brackets. Reusing the name in a signature says those positions refer to the same frame. A free function that stores a callback into a destination writes the relationship explicitly:

/* wire may keep cb inside dst, so both positions carry 'a */
function wire<'a>(cb: <'a>(i32) => i32, dst: ref 'a Holder) void {
    dst.set(cb);
}
 
var h = Holder();
if (true) {
    var n = Node(1);
    wire(n.onMsg, h);   // ❌ ERROR: n dies before h
}

Inside a class or interface member, the builtin 'this names the receiver's lifetime. A method that stores its argument in a field uses it on the parameter:

class Holder {
    var cb: <'_>(i32) => i32;
    public method set(cb: <'this>(i32) => i32) void {
        this.cb = cb;
    }
}

'this needs no declaration and is only legal in members. Classes can also declare lifetime slots (class Bus<'a>), which callers bind through types such as ref Bus<'this>. Call sites never write function lifetime arguments; the compiler infers their concrete scopes from the values passed.

A declared lifetime may also appear on a return type, because it tells the caller which input frame the result depends on:

function pick<'a>(x: <'a>() => i32, y: <'a>() => i32, first: bool) <'a>() => i32 {
    if (first) { return x; }
    return y;
}

The result remains pinned to the contributing arguments at the call site. An anonymous <'_> return is rejected because it names no input frame. See Memory Safety for the complete storage and scope rules.

First-Class Lambdas

Lambdas are first-class values: they can be assigned to variables, passed as arguments, and returned from functions when their captured environment permits it:

// Assign a lambda to a variable
var double = (x: i32) => i32 { return x * 2; };
println(double(5));  // 10
 
// Pass a lambda as an argument
function apply(fn: (i32) => i32, x: i32) i32 {
    return fn(x);
}
println(apply(double, 3));  // 6
 
// Return a lambda from a function
function makeAdder(n: i32) (i32) => i32 {
    return (x: i32) => i32 { return x + n; };
}
var add5 = makeAdder(5);
println(add5(10));  // 15

Bound Methods

Class methods are first-class values: obj.method (without parentheses) produces a value of lambda type <'_>(params) => ret — it holds its receiver by reference, so it carries the <'_> marker — that can be stored in variables and passed wherever a <'_> lambda is expected. This makes class methods usable as callbacks:

class Counter {
    var count: i32;
    init() { this.count = 0; }
    method add(amount: i32) i32 {
        this.count = this.count + amount;
        return this.count;
    }
}
 
function apply(f: <'_>(i32) => i32, x: i32) i32 {
    return f(x);
}
 
function main() i32 {
    var c = Counter();
    apply(c.add, 5);      // pass a method as a callback
    var f = c.add;        // or store it in a variable
    f(7);
    return c.count;       // 12 — calls mutate the original object
}

Rules and caveats:

  • The receiver is captured by reference. Calls through the bound value see and mutate the original object. The bound value must not outlive its receiver, so — like a lambda with a [ref …] capture — its type is <'_>(…) => … and it cannot be returned, and neither can a thread handle spawned from it.
  • Overloaded methods need type context to disambiguate: a type annotation (var f: <'_>(i32) => i32 = c.add;) or a lambda-typed parameter of the called function. Without context, referencing an overloaded method is an error.
  • Generic methods cannot be referenced as values.
  • Interface-typed receivers are not supported: only concrete class methods can be bound.
  • A bound method keeps its declared return type. A fallible method can be bound to <'_>(i32) => SystemResult<i32> when its signature matches; callers handle or propagate the returned enum.