Luce Base
LuceEngineeringLuciaOS

9. Functions and methods

9.1 Declaration and calls#

pub func clamp(value: f64, minimum: f64, maximum: f64) -> f64:
    if value < minimum: return minimum
    if value > maximum: return maximum
    return value

func render(scene: Scene*, samples: u32 = 64, denoise: bool = true) -> Image!:
    ...

let image = try render(&scene, samples = 256, denoise = false)
  • Parameter types and every non-unit result are explicit. A missing -> means unit.
  • Arguments are positional or named with name = value; positional ones come first. A default is a compile-time constant embedded at the call site. Duplicate, unknown, and missing arguments are compile errors.
  • One scope holds at most one callable with a given name: no overloading. Alternatives get semantic names: Image.from_file, Image.from_bytes.
  • No variadic Base functions in this revision (§20). Calls to variadic C functions are §17.2.
  • Recursion is allowed; running out of stack is a trap, never a crash.
  • inline func is a hint. An implementation without an inliner ignores it.
  • A function that never returns is declared -> never; extern func abort() -> never binds C's _Noreturn.

Why = for named arguments. : already means "has this type" everywhere in the language. Using it for "takes this value" would put two relations behind one symbol at exactly the two places a reader confuses them.

9.2 Parameters and the calling convention#

A T[N] parameter is a value copy. A T[] parameter is a lent view. A T* parameter is a pointer. There is no inout; pass a pointer. Every Base function whose signature is C-representable (§17.6) uses the target C calling convention whether or not it is exported, so its address may be handed to C. Sub-word parameters (bool, i8, u8, i16, u16, c.char, c.short) use the ABI's sub-word rules, which differ between AAPCS64 and Apple arm64; the backend handles them.

9.3 Multiple results#

func divide(value: i64, divisor: i64) -> (i64, i64):
    return (value // divisor, value % divisor)

let (quotient, remainder) = divide(17, 5)

Tuples replace C's out-parameters for the common case. Public data with names should be a struct.

9.4 Function values#

let operation: func(i64, i64) -> i64 = add
let result = operation(2, 3)

A function value is a C function pointer (§5.6). A non-fallible function converts to the corresponding fallible function type; nothing else converts.

9.5 Methods#

struct Point:
    pub let x: f64
    pub let y: f64

    func distance_to(self, other: Point) -> f64:
        let dx = self.x - other.x
        let dy = self.y - other.y
        return math.sqrt(dx * dx + dy * dy)

    func origin() -> Point:
        return Point(0.0, 0.0)

struct Cursor:
    pub var position: usize

    mutating func advance(self, amount: usize):
        self.position += amount
  • A function declared inside a type with an explicit first self parameter is a method; point.distance_to(other) passes point as self. A function without self is called through the type: Point.origin(). There is no static keyword.
  • mutating marks a method that assigns var fields or replaces self. Its receiver at the call site must be a var, a mutable pointer, or a mutable span element.
  • A non-mutating method receives self as const Self*; a mutating method receives self as Self*. This is deterministic so that exported headers are stable. Because self aliases the receiver, a callee that mutates the receiver through another pointer changes what self.x reads mid-method. self = value in a mutating method stores through the pointer.
  • A method is callable on a value, a var, or a pointer; p.advance(3) on p: Cursor* needs no dereference. Calling on an rvalue materialises a temporary.
  • value.member without () is always a field. There are no computed properties.
  • A method named init is the initialiser (§10.1).

Why self is a pointer. In full Luce a non-mutating method receives a copy, which is safe under reference counting and invisible to the caller. In Base a copy of a large struct on every method call is a cost C programmers would notice, and a struct method in C3, Zig, and Odin takes a pointer. Making the convention deterministic, rather than "by value if small", is what lets the generated C header say const Point* and mean it.

9.6 Closures#

There are none. A capture-free lambda (x) => x + 1 converts to a function pointer. A callback that needs state is written the way C writes it: a function pointer beside a void* or T* context.

Why. A closure that captures by reference needs its environment to outlive the frame, and without reference counting there is no owner for that environment. Every manual-memory language that offers closures either restricts them to non-escaping, or hands the programmer an allocator at closure creation. Both are plausible later; neither belongs in the first revision.

9.7 The entry point#

pub func main(arguments: str[]) -> i32:
    print("Hello")
    return 0

main takes str[] or cstr[] and returns i32 or i32!. The startup shim builds arguments from argc and argv; with str[] it validates each argument as UTF-8 and traps invalid_utf8 on failure, and a program that must accept arbitrary bytes declares cstr[]. A returned error is reported and becomes exit status 1.