Functions

Functions are first-class in ScarScript — declare them, return multiple values, capture variables in closures, or recurse freely.

Declaration

Use the func keyword. Parameters require type annotations; the return type is optional (inferred from the return statement).

// Full annotation
func add(a: int, b: int) int {
    return a + b
}

// Return type inferred
func add(a: int, b: int) {
    return a + b
}

// No parameters, no return
func hello() {
    print("hi")
}

add(2, 3)     // 5
hello()
Optional return type: You can also use the arrow syntax func f() -> int. Both styles are accepted — pick whichever reads better.

Multiple Returns

Return a tuple of values and destructure them at the call site:

func divide(a: int, b: int) (int, bool) {
    if b == 0 {
        return 0, false
    }
    return a / b, true
}

val result, ok = divide(10, 2)
if ok {
    print("Result: {result}")
}

With explicit types on the destructuring:

val q: int, ok: bool = divide(10, 0)

Functions as Values (Closures)

Assign an anonymous function to a variable. Closures capture variables from the surrounding scope:

val multiply = func(x: int, y: int) int {
    return x * y
}

multiply(3, 4)   // 12

// Capture example
mut count = 0
val increment = func() {
    count = count + 1
}

increment()
increment()
print(count)   // 2

Anonymous functions work anywhere an expression is expected:

val nums = [1, 2, 3, 4, 5]
mut total = 0

for n in nums {
    val double = func(x: int) int { return x * 2 }
    total = total + double(n)
}
Scope isolation: Named functions get an isolated scope — locals inside a function never leak into the caller. Closures, however, share their captured variables by reference.

Recursion

Functions can call themselves. The checker registers all functions in a first pass, so call order doesn't matter:

func fib(n: int) int {
    if n < 2 { return n }
    return fib(n - 1) + fib(n - 2)
}

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

print(fib(10))        // 55
print(factorial(5))    // 120

Call functions defined later in the file — forward references are allowed:

func start() {
    print(helper())   // works — helper is registered in phase 1
}

func helper() str {
    return "done"
}

start()

Return Semantics

The checker enforces that every code path returns a value when a return type is declared:

Always returns

func f(x: int) int {
    if x > 0 { return 1 }
    return 0
}

Missing return (error)

func f(x: int) int {
    if x > 0 { return 1 }
    // error: missing return
}

Unreachable code (error)

func f() int {
    return 1
    print("never runs")
    // error: unreachable code
}

Passing Functions

Since functions are values, you can pass them around:

func apply(f: func(int) int, x: int) int {
    return f(x)
}

val square = func(n: int) int { return n * n }

apply(square, 5)   // 25
Reserved: print and log are builtins — you cannot declare functions with those names.