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:
- Imports: all packages in
requiredImportsare sorted and emitted insideimport (...). - Core slices: only the
Core*constants whose keys appear inusedCoreSlicesare 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 maindeclaration. - 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.