Function Dispatching & Built-ins
This page explains how the Luma compiler translates method calls like .add(), .contains(), or .to_int() into the correct Go function calls. Understanding this system helps contributors add new built-in methods and debug compilation issues.
The dispatch model
Luma does not use Go interfaces or vtables for built-in method calls. Instead, the compiler pattern-matches on the receiver type and method name at compile time, then emits the appropriate Go function call directly.
For example, when the compiler sees:
numbers.add(42)It checks the type of numbers. If it’s [int] (a list of ints), it emits:
_luma_list_add_int(&numbers, 42)This approach gives zero runtime overhead and clear, debuggable Go output.
The tryLower pattern
Each built-in type has a dedicated tryLower function in the compiler. These functions take a method call and attempt to lower (translate) it into Go code. Each returns two values: the generated Go code and a boolean indicating whether it handled the call.
| tryLower function | Handles methods on |
|---|---|
tryLowerListBuiltin |
Lists ([int], [str], etc.) |
tryLowerSetBuiltin |
Sets ({int}, {str}, etc.) |
tryLowerFileBuiltin |
File references (file) |
tryLowerCliBuiltin |
CLI handles (cli) |
tryLowerDatetimeBuiltin |
Datetime values (datetime) |
tryLowerDateBuiltin |
Date values (date) |
tryLowerTimeBuiltin |
Time values (time) |
Dispatch priority
When the compiler encounters a method call like obj.method(args), it checks in this order:
- Type conversion methods (
.to_int(),.to_str(),.to_float(),.to_bool()) - Module namespace calls (
json.decode(),datetime.now(),date.parse()) - Trait dynamic dispatch (if receiver implements a trait)
- Struct impl methods (methods defined in
implblocks) - Built-in type dispatch (tryLower functions listed above)
- Struct constructors (
Person("Alice", 30)) - Regular function calls
The compiler stops at the first match. If no handler recognizes the call, it’s emitted as a plain Go function call.
Type suffix generation
Many built-in operations have type-specific implementations in the core runtime. The compiler determines the element type and appends a suffix:
| Element type | Suffix | Example function |
|---|---|---|
int |
_int |
_luma_list_add_int |
float |
_float |
_luma_set_contains_float |
str |
_str |
_luma_list_remove_str |
bool |
_bool |
_luma_list_add_bool |
byte |
_byte |
_luma_list_sort_byte |
This means each list or set operation has a typed Go function for every supported element type, defined in .lcore files in the core/ directory.
Example: list dispatch
When the compiler sees:
names: [str] = ["Alice", "Bob"]
names.add("Charlie")The dispatch flow is:
- Compiler identifies
nameshas type[]string tryLowerListBuiltinis called with method name"add"- It determines the element type suffix:
"str" - It emits:
_luma_list_add_str(&names, "Charlie")
The &names (pointer) is used because add mutates the list in place.
Example: set dispatch
tags: {str} = {"go", "luma"}
has_go: bool = tags.contains("go")Dispatch flow:
- Compiler identifies
tagshas typemap[string]bool(a set) tryLowerSetBuiltinhandles"contains"with suffix"str"- Emits:
_luma_set_contains_str(tags, "go")
Example: datetime dispatch
now: datetime = datetime.now()
year: int = now.year()Dispatch flow:
datetime.now()is recognized as a static namespace call, emits_luma_datetime_now()now.year()is dispatched throughtryLowerDatetimeBuiltin, emits_luma_datetime_year(now)
Core runtime functions
Each tryLower function maps to Go implementations in the core/ directory as .lcore files. These are split by coregen into feature-specific Core* constants in core_gen.go and included in compiled programs only when needed (see Core Runtime for the full slice list).
The naming convention is consistent:
_luma_{type}_{method}_{elemtype}Examples:
_luma_list_add_int— add an int to a list_luma_set_union_str— union of two string sets_luma_file_read— read a file_luma_datetime_format— format a datetime
Adding a new built-in method
To add a new built-in method to an existing type:
- Create the Go implementation in a
.lcorefile undercore/. Use the correct filename prefix socoregenroutes it to the right slice (e.g._luma_list_*.lcorefor list operations). - Add a case to the appropriate
tryLower*Builtinfunction incompiler.go. - Mark the core slice at the codegen site by calling
c.useCoreSlice("slicename"). This is critical — without it, the runtime helper functions won’t be included in the generated program and compilation will fail. For example, if you add a new list method, your codegen case must includec.useCoreSlice("list"). - Mark any extra Go imports if your codegen emits Go code that uses standard library packages not already covered by the slice’s imports. Call
c.useImport("pkg")for these. (Most of the time, the slice’s existing imports incoreSliceImportsare sufficient.) - Regenerate the core:
cd cmd/coregen && go run main.go - Build and test:
go build -o luma . && go test ./...
Example: adding a list.last() method
// In tryLowerListBuiltin, add a case:
case "last":
c.useCoreSlice("list") // ← marks CoreList for inclusion
elemSuffix := c.goTypeSuffix(elemType)
return fmt.Sprintf("_luma_list_last_%s(%s)", elemSuffix, receiverCode), trueThe useCoreSlice("list") call ensures that CoreList (and its Go imports: sort, fmt, reflect) are included in the generated program. Without this call, the _luma_list_last_* function would not exist in the output and go build would fail.