Modules & Imports (Code Organization)
Luma applications are organized into modules. A module is simply a .luma file. Modules help you split large programs into smaller, reusable pieces.
This page explains how modules work, how to import them, and how the entrypoint of a Luma program is determined.
What Is a Module?
Every .luma file is a module.
Example project structure:
project/
├── a.luma
├── b.luma
└── utils.lumaa.lumais a module named"a"b.lumais a module named"b"utils.lumais a module named"utils"
Modules can define:
- functions
- structs
- traits
- constants
- top-level executable statements
- public exports using
pub
Entrypoint - The File You Run
Luma does not have a special main.luma file and does not use fn main().
Instead:
The entrypoint of your program is the file you run using the CLI.
Examples:
luma run a.lumaHere, a.luma is the entrypoint.
luma run utils.lumaNow utils.luma is the entrypoint.
You can run any Luma file directly. How your project behaves depends entirely on which file you choose as the starting point.
Importing Other Modules
You can import another .luma file using the import keyword:
b = import "b"
print(b.add(1, 2))This loads b.luma and returns a module namespace object.
Example:
a.luma
b = import "b"
print(b.add(10, 20))b.luma
pub fn add(a: int, b: int) -> int {
return a + b
}Running
luma run a.lumaOutput:
30Running:
luma run b.lumaRuns only b.luma (since it doesn’t import anything).
Public Exports (pub)
By default, module symbols are private.
Use pub to export something:
pub fn add(a: int, b: int) -> int {
return a + b
}From another file:
b = import "b"
result = b.add(1, 2)If a function, struct, or variable is not marked pub, it cannot be accessed from outside the module.
Top-Level Code Execution
Modules may contain top-level executable code:
print("Loading module...")Execution rules:
- When you run a file, its top-level code executes first.
- When that file imports another module:
- the imported module is loaded,
- its top-level code runs once,
- then control returns to the importer.
Example:
a.luma
print("from a")
b = import "b"b.luma
print("from b")Running:
luma run a.lumaOutput:
from a
from bRunning b.luma directly prints only:
from bCircular Imports (Not Allowed)
Luma does not allow circular imports.
Example:
a.luma
b = import "b"b.luma
a = import "a"This creates a cycle:
a -> b -> aThe compiler will report:
Error: circular import detected: "a" -> "b" -> "a"Why?
Circular imports make top-level execution order unpredictable, complicate initialization, and make the compiler and the mental model more complex.
Luma chooses clarity and simplicity.
If you need two modules to share something - put that shared code in a third module:
a.luma --> common.luma <-- b.lumaRecommended Project Structure
A common layout:
project/
├── app.luma // entrypoint
├── users.luma // module with pub functions
├── models.luma // structs, domain types
└── util.luma // helper functionsapp.luma:
users = import "users"
print(users.get_user_count())users.luma:
pub fn get_user_count() -> int {
return 42
}When to Use Top-Level Code vs Functions
Top-level code is best for:
- starting a server
- running a script
- initializing configuration
Functions (especially pub ones) are best for:
- reusable logic
- libraries
- exporting functionality to other modules
Example of a “library-style” module:
// math_utils.luma (no side effects)
pub fn square(x: int) -> int {
return x * x
}Summary
Luma’s module system is simple and explicit:
- Every
.lumafile is a module. - The file you run is the entrypoint.
- Imports are opt-in: only imported files are part of the program.
- Top-level code executes once per module load.
pubcontrols what is visible outside a module.- Circular imports are not allowed.
- Any file can be run directly - it’s up to you to organize your project.
This keeps Luma easy to understand while still enabling powerful code organization techniques.
Public vs Private
By default, everything in a module is private.
Use pub to export:
pub fn add(a: int, b: int) -> int { return a + b }
pub struct Vector2 {
x: float
y: float
}
pub const PI: float = 3.14159Only pub items appear in the module value returned by import.
Importing a Module
Import inline (direct one-shot usage)
result: int = (import "math/utils").add(1, 2)This shows exactly where add comes from. Perfect for small scripts or when clarity is more important than brevity.
Import once, reuse many times
math = import "math/utils"
result1 = math.add(1, 2)
result2 = math.sub(5, 3)
pos: math.Vector2 = math.Vector2 { x: 0.0, y: 0.0 }
print(math.PI)Since import is an expression returning the module, storing it in a variable is natural.
What a Module Contains
A module value contains all its public items:
- Functions
math.add,math.sub - Struct Types
math.Vector2 - Traits
math.Movable - Constants
math.PI
Example:
utils = import "math/utils"
print(utils.add(1, 1))
pos: utils.Vector2 = utils.Vector2 { x: 1.0, y: 2.0 }All names are accessed through the module object.
No Special Import Grammar
There is:
- no alias syntax
- no selective imports
- no
import -> { ... } - no renaming
You do everything using regular Luma code:
math = import "math/utils"
add = math.add
sub = math.sub
result = add(10, 20)This keeps the module system small and fully orthogonal.
Module Resolution Rules
To keep modules coherent and predictable:
- Import paths must be string literals.
- Modules are resolved at compile time, not runtime.
- The compiler evaluates all import
"path"expressions, wherever they appear. - Modules are compiled once, even if imported many times in expressions.
So this:
fn loop() {
for i in 0..10 {
print((import "math/utils").add(i, 1))
}
}does NOT repeatedly reload modules.
Circular Dependencies
Circular dependencies between modules are a compile-time error:
a.luma imports b.luma
b.luma imports a.lumaThe compiler reports the cycle with a clear error message.
To fix it:
- Move shared logic into a third module (e.g.
common/types.luma) - Import that from both
aandb
Complete Example
File: math/utils.luma
pub fn add(a: int, b: int) -> int {
return a + b
}
pub fn sub(a: int, b: int) -> int {
return a - b
}
pub struct Vector2 {
x: float
y: float
}
pub const PI: float = 3.14159File: main.luma
math = import "math/utils"
print(math.add(1, 2))
pos: math.Vector2 = math.Vector2 { x: 0.0, y: 1.0 }
print(pos)
add = math.add
print(add(10, 20))Available Modules
Luma includes ready-to-use modules written entirely in Luma:
| Module | Import | Description |
|---|---|---|
| CSV | import "csv" |
Parse, query, filter, and write CSV files |
| Table | import "table" |
Format data as terminal tables (box-drawing, ASCII, compact, markdown) |
| JWT | import "jwt" |
Sign and verify JSON Web Tokens (HS256) |
| MySQL | import "mysql" |
Pure Luma MySQL driver — connect, query, read results |
Example — import CSV data and display it as a table:
csv = import "csv"
table = import "table"
data: [[str]] = csv.read("employees.csv")
table.print_table(data)Why This Design?
-
Perfect clarity
You always know where any symbol comes from. No mysteries. No hidden imports. -
One concept only
No aliasing, selective imports, or import statements. Just expressions. -
Beginner-friendly
Students don’t need to learn a new syntax category just for modules. -
Advanced enough for large projects
Module values can be passed, stored, forwarded, wrapped, and re-exposed manually. -
Easy for tools (IDE, formatter, analyzer)
Module graph = all literal imports in the file. -
Extremely small grammar
Cleaner language, easier maintenance.