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 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. 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
| Aspect | function | lambda |
|---|---|---|
| Name | Required, becomes the function's identity | Anonymous (empty name internally) |
| Declaration | Top-level or nested statement | Expression (must assign to variable) |
| First-class | No—cannot be assigned or passed | Yes—can be assigned, passed, returned |
| Recursion | Can call itself by name | Must reference the variable it's assigned to |
| Hoisting | Available throughout scope | Only after assignment |
When to use which:
- Use
functionfor named, top-level operations called directly - Use
lambdafor 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)); // 15Bound 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 usualtry/, IErrorrules.