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 twoargv[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 conflictingrefs — 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 onlyconst methodmethods 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 tospawnis 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). thiscannot 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
| Aspect | function | lambda |
|---|---|---|
| Name | Required, becomes the function's identity | Anonymous (empty name internally) |
| Declaration | Direct child of the root or a module | Expression |
| First-class | Yes, as a non-null environment-free pointer | Yes, with an optional captured environment |
| Recursion | Can call itself by name | Must reference the variable it is assigned to |
| Hoisting | Available throughout its module | Only after assignment |
When to use which:
- Use
functionfor 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)); // 15Bound 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.