Function Dispatching & Built-ins

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:

  1. Type conversion methods (.to_int(), .to_str(), .to_float(), .to_bool())
  2. Module namespace calls (json.decode(), datetime.now(), date.parse())
  3. Trait dynamic dispatch (if receiver implements a trait)
  4. Struct impl methods (methods defined in impl blocks)
  5. Built-in type dispatch (tryLower functions listed above)
  6. Struct constructors (Person("Alice", 30))
  7. 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:

  1. Compiler identifies names has type []string
  2. tryLowerListBuiltin is called with method name "add"
  3. It determines the element type suffix: "str"
  4. 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:

  1. Compiler identifies tags has type map[string]bool (a set)
  2. tryLowerSetBuiltin handles "contains" with suffix "str"
  3. Emits: _luma_set_contains_str(tags, "go")

Example: datetime dispatch

now: datetime = datetime.now()
year: int = now.year()

Dispatch flow:

  1. datetime.now() is recognized as a static namespace call, emits _luma_datetime_now()
  2. now.year() is dispatched through tryLowerDatetimeBuiltin, 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:

  1. Create the Go implementation in a .lcore file under core/. Use the correct filename prefix so coregen routes it to the right slice (e.g. _luma_list_*.lcore for list operations).
  2. Add a case to the appropriate tryLower*Builtin function in compiler.go.
  3. 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 include c.useCoreSlice("list").
  4. 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 in coreSliceImports are sufficient.)
  5. Regenerate the core: cd cmd/coregen && go run main.go
  6. 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), true

The 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.

Last updated on