Documentation menu
Language

Functions

Functions are first-class values — you can assign them to variables, pass them as arguments, and return them from other functions.

Named functions

The most common way to define a function. Return type and parameter types are optional:

fn int add(int a, int b) {
    return a + b
}

fn void greet(string name) {
    out "Hello, {name}!"
}

fn bool isAdult(int age) {
    return age >= 18
}

out add(3, 7)         // → 10
out isAdult(16)       // → false
greet("Sergio")       // → Hello, Sergio!

Arrow functions

Arrow functions are great for short callbacks. The return type goes before the parentheses:

let double = int (int n) => {
    return n * 2
}

let shout = string (string s) => {
    return s.toUpperCase() + "!"
}

out double(5)       // → 10
out shout("hello")  // → HELLO!

Anonymous functions

Functions without a name — useful when you need to pass logic around:

let run = fn void () {
    out "running..."
}

run()   // → running...

Default parameters

Parameters can have default values. If the caller doesn't pass the argument, the default is used. Default parameters must come after required ones:

fn string greet(string name = "World") {
    return "Hello, {name}!"
}

out greet()           // → Hello, World!
out greet("Sergio")   // → Hello, Sergio!

fn int add(int a, int b = 10) {
    return a + b
}

out add(5)      // → 15  (b defaults to 10)
out add(5, 3)   // → 8   (b supplied)

Recursion

Functions can call themselves. The call stack is tracked and shown clearly if an error occurs:

fn int factorial(int n) {
    if (n <= 1) { return 1 }
    return n * factorial(n - 1)
}

out factorial(6)   // → 720

fn int fibonacci(int n) {
    if (n <= 1) { return n }
    return fibonacci(n - 1) + fibonacci(n - 2)
}

out fibonacci(10)   // → 55

Functions as values

Functions can be stored in variables and passed as arguments:

fn int double(int n) { return n * 2 }
fn int square(int n) { return n * n }

let op = double   // store a function
out op(5)         // → 10

op = square       // swap it out
out op(5)         // → 25

// Pass as argument
fn int applyTwice(fn int op, int x) {
    return op(op(x))
}

out applyTwice(double, 3)   // → 12  (double(double(3)) = double(6) = 12)

Closures

Functions capture variables from the scope they're defined in:

let multiplier = 3
let nums = [1, 2, 3, 4]

// The lambda captures 'multiplier' from outside
let tripled = nums.map(x => x * multiplier)
out tripled   // → [3, 6, 9, 12]

// Making a counter with closure
fn make_counter() {
    let count = 0
    return fn void () {
        count = count + 1
        out count
    }
}

let counter = make_counter()
counter()   // → 1
counter()   // → 2
counter()   // → 3

Shared Cell Semantics

Serez Code implements shared-cell closure semantics (similar to JavaScript's let). A closure and its surrounding scope share captured variables at any nesting level. Mutations inside the closure escape to the outer scope, and subsequent outer writes are visible inside:

let x = 10
let printX = () => { out x }
x = 20
printX()   // → 20 (reads the updated value)

Loop Variables in Closures

  • For loops: The loop counter variable is fresh per iteration. Closures created inside a for loop capture the value of that specific iteration:
    let funcs = []
    for (let i = 1; i <= 3; i++) {
        funcs.push(() => { out i })
    }
    funcs[0]()   // → 1 (instead of 3 or 4)
    funcs[1]()   // → 2
    funcs[2]()   // → 3
  • While loops: A variable declared outside or controlled manually inside a while loop is captured as a single shared cell across all iterations.

Memory Optimization: The compiler only captures variables that are actually referenced in the closure body, preventing memory leaks and avoiding unnecessary overhead.

Lambda syntax (for callbacks)

When passing functions to map, filter, reduce, or similar, use the short lambda syntax:

let nums = [1, 2, 3, 4, 5]

// Single parameter — no parentheses needed
nums.map(x => x * 2)

// Two parameters (value + index)
nums.map((x, i) => "{i}: {x}")

// Multi-line lambda
nums.filter(x => {
    let isEven = x % 2 == 0
    return isEven
})

// Reduce — accumulator + current value
nums.reduce(0, (acc, x) => acc + x)   // → 15