Compilation Pipeline

Here’s how the compilation process works:

  • Lexing: Source code is tokenized into symbols.
  • Parsing: Tokens are parsed into an AST (Abstract Syntax Tree).
  • Semantic Analysis: Types and scopes are resolved.
  • Code Generation: The AST is translated into Go code (compile-first, prelude-second).
  • Go Compilation: Go code is compiled with go build.
  +------------------+
  |  .luma Source    |
  +--------+---------+
           |
           v
+----------------------+
|     Lexer            |  ← Tokens (e.g., IDENT, NUMBER, etc.)
+----------------------+
           |
           v
+----------------------+
|     Parser           |  ← AST (Abstract Syntax Tree)
+----------------------+
           |
           v
+----------------------+
|   Type Resolver      |  ← Type info (int, float, str, etc.)
+----------------------+
           |
           v
+----------------------+
|  Go Code Generator   |  ← Phase 1: user code  (marks slices & imports)
|                      |  ← Phase 2: prelude     (emits only what's needed)
|                      |  ← Phase 3: combine     → `main_temp.go`
+----------------------+
           |
           v
+----------------------+
|     go build         |  ← Native binary
+----------------------+

Compile-first, prelude-second

The code generator uses a 3-phase pipeline so that only the imports and core runtime slices a program actually needs are included:

Phase 1 — Compile user code

The compiler walks the AST and emits Go code for all top-level declarations (func, struct, trait, impl, type) and the main() body. During this phase, every codegen site that emits a call to a _luma_* helper function calls two tracking methods as a side effect:

  • c.useCoreSlice("name") — marks a core slice as needed (e.g. "list", "print", "file") and automatically registers its Go import dependencies.
  • c.useImport("pkg") — marks a Go standard library package as needed (e.g. "fmt", "os").

For example, when the compiler emits _luma_list_add_int(...), it also calls c.useCoreSlice("list"), which marks the CoreList slice and registers its Go imports (sort, fmt, reflect).

Phase 2 — Emit prelude

After all user code is compiled, the compiler builds the prelude from tracked state:

  1. Imports: all packages in requiredImports are sorted and emitted inside import (...).
  2. Core slices: only the Core* constants whose keys appear in usedCoreSlices are emitted.

Phase 3 — Combine

The prelude is prepended to the user code, producing the final main_temp.go.

Demand-driven imports

Go imports are not hardcoded. Each core slice declares which Go packages its .lcore code needs via the coreSliceImports mapping in compiler.go:

var coreSliceImports = map[string][]string{
    "print":    {"fmt", "strings", "reflect"},
    "convert":  {"strconv", "strings", "reflect"},
    "list":     {"sort", "fmt", "reflect"},
    "file":     {"os", "path/filepath", "fmt"},
    "datetime": {"time", "fmt", "strconv", "strings"},
    // ... etc for all 19 slices
}

When useCoreSlice("list") is called, it automatically adds sort, fmt, and reflect to the import set. Codegen sites can also call useImport() directly for imports not tied to a slice (e.g. useImport("bufio") for file walk).

This means a simple print("Hello") generates only import ("fmt" "reflect" "strings"), while a program using lists, files, and JSON will get the full set of imports those features require — but nothing more.

Output structure

Every Luma program gets compiled into a single Go main() file with:

  • A package main declaration.
  • An import (...) block containing only the Go packages actually needed.
  • Core runtime slices — only the Core* feature slices the program uses (see Core Runtime for the full slice list).
  • All user code translated into idiomatic Go.
  • A func main() that includes REPL or CLI interface, depending on the mode.
Last updated on