9. Functions and methods
9.1 Declaration and calls#
import io
import luce
from io import Location
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!:
...
func log(level: Level, message: fmt, at: Location = luce.location):
_ = io.stderr().write(f"{at.file}:{at.line}: {message}\n") catch: recover 0
let image = try render(&scene, samples = 256, denoise = false)
log(.warn, f"lost {count} packets")- Parameter types and every non-
unitresult are explicit. A missing->meansunit. A fallible function that returns nothing is written-> !, the short form of-> unit!; both are accepted. - Arguments are positional or named with
name = value; positional ones come first. A default is a constant expression embedded at the call site.luce.locationis a compile-timeLocation(fileandfunctionasstr,lineasu32); as a default it expands at the call site, which is how a log records the caller without a macro. Duplicate, unknown, and missing arguments are compile errors. - A parameter of type
fmtaccepts a formatted string or astr. Inside the function it may be written to aWriter, passed toprintorstrings.format, or passed on to anotherfmtparameter, and nothing else: it cannot be stored, returned, or compared. The caller formats the text before the call, as for aWriter(§5.5), and the function receives it whole, whatever its length; a function that may discard the message is called behind a check at the call site,if verbose: log(f"..."), so a discarded message costs nothing. This is what makes a logging function possible without a macro. - One scope holds at most one callable with a given name: no overloading. Alternatives get semantic names:
Image.open,Image.decode. - 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 (§11.5).
- A function that never returns is declared
-> never;extern func abort() -> neverbinds 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.
Why fmt and luce.location. Function-like macros in C exist for two things above all: a logging call that takes a format string and forwards it, and a check that reports its own file and line. A parameter type that takes a formatted string as the text it formats to, and a compile-time source location expanded at the use site, cover both without a preprocessor. location() as a fake function would have been a runtime-shaped lie about a fact the compiler already knows.
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. A parameter of pointer or span type may be declared noalias, out: noalias f32[], asserting that the memory it addresses is not addressed by any other parameter or reachable pointer for the duration of the call; the backend may then keep loads in registers, and a violation is undefined (§12.6).
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) 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 is better served by a struct. A tuple is not C-representable (§17.6), so its convention is the program's own: one of up to four words comes back in registers on every target, as a fallible result does (§11.2), with no memory between the callee's values and the caller's.
9.4 Function values#
func main(arguments: str[]) -> i32:
let operation: func(i64, i64) -> i64 = add
return i32(operation(2, 3))A function value is a C function pointer (§5.6). A non-fallible function converts to the corresponding fallible function type, and a function that only reads through a const T* parameter, a method that changes nothing among them, to the type taking the T* there; nothing else converts. Either conversion is of a named function, f, module.f or Type.method, never of a value that holds one: the compiler calls the function through an adapter of exactly the wanted type, whose receiver, for a method, is the first parameter. A top-level let that names a function, pub let add = lib.add, is that function under another name: a call through it, qualified or not, takes the function's defaults and named arguments, so a module hands on another's function unchanged.
9.5 Methods#
import luce_std.math
struct Point:
pub let x: f64
pub let y: f64
func distance(other: Point) -> f64:
let dx = self.x - other.x
let dy = self.y - other.y
return math.sqrt(dx * dx + dy * dy)
static func origin() -> Point:
return Point(0.0, 0.0)
struct Cursor:
pub var position: usize
func advance(amount: usize):
self.position += amount- A
funcdeclared inside a type is a method.selfis implicit: it names the receiver inside the body, is not written in the parameter list, and cannot be used as a parameter name.point.distance(other)passespointasself. static funcdeclares a function of the type, with no receiver:originabove. It is called through the type,Point.origin(), as a static method is in C++ or Java; called through a value it is an error that names the type to call it through. Any other function in a type is a method, whether or not its body readsself, and is called on a value; through its type it is a function value taking the receiver first (§5.6), never called there.staticis written only in a type, never oninit, and astaticfunction does not implement an interface's requirement (§14.1), which a view calls with a receiver.- A method changes its receiver when its body assigns to
selfor a part of it (a field or an array element reached without passing through a pointer or a span), calls a method that changes its receiver onselfor such a part, stores to one of its atomics, allocates through an allocator it holds (new T in self.arena), or takes the address ofselfor a part, or a span of one, where a mutable pointer, a mutable span, or a view of a conformer that changes its receiver is wanted, or an optional one: as an argument, as the method's result, as the value of a binding or a field whose type is written, or in an assignment. A method whose result is such an address or view of its receiver hands out what may change it, so it is one that changes it. A local holding such an address counts as the part it reaches,let cell = &self.cells[i]and thencell.value = 0, and so does afor p in &self.itemsloop's binding. Nothing is written for any of this: the compiler reads it from the bodies, following methods that call each other until nothing changes, before it checks one. - A method that does not change its receiver may be called on anything: a
let, aconst T*, a temporary. The receiver of one that does must be avar, a mutable pointer, or a mutable span element, and the diagnostic names the method and why. - A method that does not change its receiver receives
selfasconst Self*; one that does receivesselfasSelf*, and so doesinit. In the bodyselfnames the receiver itself, a place of typeSelf:self.xreads a field and&selfis the receiver's address, which is how aSelf*is passed on. The address ofselfor a part, taken where no mutable pointer is wanted,let p = &self.part, isconstin a method that changes nothing else; written with its type,let p: Part* = &self.part, it is mutable and the method changes its receiver. Becauseselfaliases the receiver, a callee that mutates the receiver through another pointer changes whatself.xreads mid-method.self = valuestores through the pointer. - What the compiler found is part of the method's description (§17):
describelists a method, amutating method, or astatic method, so a module compiled apart and full Luce need not read the body. Alinkedmethod (§9.8), whose body is the library's, says it in its signature. - A method is callable on a value, a
var, or a pointer;p.advance(3)onp: Cursor*needs no dereference. Calling on an rvalue materialises a temporary. value.memberwithout()is always a field. There are no computed properties.- A method named
initis the initialiser (§10.1).
A type's methods may also be declared under extend Type: at the top level of any fragment of the module that declares Type (§16.1), when a type with many methods is better split by concern across files:
# canvas/selecting.lucb, a fragment of the module `canvas` that declares `Canvas`
extend Canvas:
pub func select_all():
self.selected = self.area()- An extension holds methods only (with
pub, attributes, and doc comments as in the body), never fields, so a type's layout stays in its declaration. Typeis a struct, enum, or union this module declares; extending another module's type is an error, so a type's methods are all found within the module that owns it. It sees the module's private names and the type's private fields, as a method in the body does.- The methods are the type's methods like the ones in its body: one type, one set of methods, a name declared twice is an error, and the order of fragments does not matter.
describe(§17) lists them with the rest, so full Luce sees every one. extendis a word only in that position; elsewhere it is an ordinary name.
Why extensions. A facade type with hundreds of methods would otherwise hold every one of them in one file, or repeat each as a one-line method calling a function in another file. Swift's extensions and Go's methods anywhere in the package split a type across files; Base keeps Go's limit, the owning module, so the full set of a type's methods is still known in one place.
Why implicit self. Full Luce writes self as the first parameter, which is Python's convention and is justified in a language where a function inside a type may or may not be a method. In Base, having a receiver is what a function inside a type does by default, and a C programmer reading func distance(other: Point) inside a struct knows what it is. The explicit parameter would have been a parameter in every method that carries no information.
Why nothing is written about the receiver, and static is. Whether a method changes its receiver is in its body; a word that repeats it is one more thing to keep true, and the diagnostic about a wrong one is about the word, not the code. Swift asks for mutating and Rust for &mut self; Base reads the body, as Luce does, and a caller that needs a var is told so at the call, with the method's name. The rule is the body's own assignments and calls, not a guess: the compiler never makes a method changing because it might be, and what it found is in the description, so nothing downstream reads a body twice. Whether a function has a receiver at all is different: it decides how every caller spells the call, so it is stated where the function is declared, with the one word C++, Java and C# use, rather than left to a reader to infer from whether the body happens to read self. A method that does not need its receiver yet is still a method, and its callers do not change when it starts to.
Why self is a pointer. In full Luce a method that changes nothing 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*.
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 0main takes str[] or c.str[] 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, so a program that takes paths from the command line declares c.str[], because a path on POSIX is bytes. A returned error is printed to standard error as its code and message and becomes exit status 1.
9.8 Declaration attributes#
A small closed set of words may precede a func or a top-level var, each one a fact the linker or the code generator needs and nothing else can express:
| Word | Meaning |
|---|---|
inline func | inline at call sites; an implementation without an inliner ignores it |
noinline func | never inline |
cold func | rarely executed; place and optimise accordingly |
naked func | no prologue or epilogue; the body is one asm block per architecture (§8.9) |
weak func, weak var | a weak symbol that another definition may override |
used func, used var | keep the symbol even if nothing references it |
section("name") func, section("name") var | place the symbol in the named linker section |
linked func, linked var, linked let | the definition is in a library the program links: the function has a signature and no body, the global a type and no initialiser, the constant its type and, when the interface carries it, the value the library gives it, which the program neither stores nor folds; a linked let names storage, so it is not a constant expression (§6.4). An interface written by luce-base interface is made of these; a generic or inline function is compiled where it is used and cannot be linked. A linked method has no body to read (§9.5), so it names its receiver as its first parameter: self: T* when it changes it, self: const T* when it reads it; a linked static func of a type names none |
They combine, used section(".isr_vector") var vectors: Handler[64] = .... There is no general attribute syntax; this set is the language. A section name is passed to the target as written; a Mach-O target, whose sections live in segments, places a name without a comma in __DATA (a variable) or __TEXT (a function) under that name with its leading dot dropped, so .isr_vector is one spelling for every target.
Why. Embedded and kernel code needs an interrupt table in a named section, a weak default handler, a symbol the linker must keep, and a function with no prologue. Each is a fact about one declaration, and a word before the declaration is the shortest way to state it.