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 scope:

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

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. sun.env.args turns the pair into a Vec<String>:

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

Lambdas

Lambdas are anonymous functions created with the lambda keyword. They must be assigned to a variable or used immediately:

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

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 after the lambda keyword:

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

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 …] — copying them into a closure would silently break aliasing.
  • Globals are accessed directly and cannot appear in a capture list.
  • A by-ref capture registers a mutable borrow of the variable for as long as the lambda value is in scope, so conflicting refs are rejected.
  • A lambda with by-ref captures holds pointers into the enclosing frame, so it cannot be returned from the function or passed to spawn — the captured variables would be dead (or racing) when used.

Semantic Differences

Aspectfunctionlambda
NameRequired, becomes the function's identityAnonymous (empty name internally)
DeclarationTop-level or nested statementExpression (must assign to variable)
First-classNo—cannot be assigned or passedYes—can be assigned, passed, returned
RecursionCan call itself by nameMust reference the variable it's assigned to
HoistingAvailable throughout scopeOnly after assignment

When to use which:

  • Use function for named, top-level operations called directly
  • Use lambda for callbacks, higher-order function arguments, or when you need to store/pass functions

First-Class Lambdas

Lambdas are first-class values—they can be assigned to variables, passed as arguments, and returned from functions (named functions are not, but class methods are—see Bound Methods):

// Assign a lambda to a variable
var double = lambda (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 lambda (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 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;
    function init() { this.count = 0; }
    function 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.
  • 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 non-throwing method can be bound where a throwing lambda type ((i32) i32, IError) is expected; a throwing method requires a throwing lambda type and its calls follow the usual try/, IError rules.