Modules & Imports (Code Organization)

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.luma
  • a.luma is a module named "a"
  • b.luma is a module named "b"
  • utils.luma is 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.luma

Here, a.luma is the entrypoint.

luma run utils.luma

Now 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.luma

Output:

30

Running:

luma run b.luma

Runs 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:

  1. When you run a file, its top-level code executes first.
  2. 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.luma

Output:

from a
from b

Running b.luma directly prints only:

from b

Circular 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 -> a

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

Recommended Project Structure

A common layout:

project/
 ├── app.luma         // entrypoint
 ├── users.luma       // module with pub functions
 ├── models.luma      // structs, domain types
 └── util.luma        // helper functions

app.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 .luma file 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.
  • pub controls 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.14159

Only 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.luma

The 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 a and b

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

File: 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.

Last updated on