Program Structure

Modules & Visibility

Connect named Topaz files with imports and exports, configure a root, and resolve visibility or import-cycle failures.

Use modules when a file defines responsibilities required by another file. Each .tpz file constitutes a single module, with no separate module declaration.

Build one program from two files

The study-plan application uses the following source layout:

study-plan/
├── topaz.toml
├── topaz.lock
└── src/
    ├── main.tpz
    └── plan.tpz

src/plan.tpz defines the data model and reusable operations:

TOPAZ
export record StudyTask {
    title: string,
    minutes: int,
    done: bool = false,
}

function parseMinutes(text: string) -> Result<int, string> {
    match toInt(text) {
        case Some(value) if value > 0 => Ok(value)
        case _ => Err("minutes must be a positive integer")
    }
}

export function makeTask(
    title: string,
    minuteText: string,
    done: bool = false,
) -> Result<StudyTask, string> {
    let minutes = parseMinutes(minuteText)?
    Ok(StudyTask { title: title, minutes: minutes, done: done })
}

export function summarize(tasks: Array<StudyTask>) -> string {
    let mut remaining = 0
    for task in tasks {
        if !task.done {
            remaining = remaining + task.minutes
        }
    }
    "tasks={tasks.length}, remaining={remaining} minutes"
}

src/main.tpz imports the required three names:

TOPAZ
import src.plan { StudyTask, makeTask, summarize }

export function main(args: Array<string>, stdin: string) -> Result<int, string> {
    let tasks: Array<StudyTask> = [
        makeTask("Run first program", "10", true)?,
        makeTask("Build application", "25", false)?,
    ]
    print(summarize(tasks))
    Ok(0)
}

From the package root, check and execute the resolved unit:

BASH
topaz check --root . --locked
topaz run --root . --locked
Output
tasks=2, remaining=25 minutes

Specifying an explicit root resolves src.plan to src/plan.tpz. The entry file and all targets reachable through its imports constitute the compilation unit. Unrelated files outside this closure are excluded from verification for this entry.

Export only the boundary another module needs

Declarations are private by default unless prefixed with export. StudyTask, makeTask, and summarize cross the module boundary. parseMinutes remains an internal implementation detail; importing or accessing it from src/main.tpz results in a static error. Imported bindings are read-only.

A selected import binds exported names directly:

GoalCanonical shape
Select a few namesimport src.plan { StudyTask, makeTask }
Rename one selected nameimport src.plan { summarize as renderSummary }
Bind a namespaceimport src.plan as plan

With the namespace form, exported values are accessed as plan.makeTask(...). A namespace acts as a compile-time lookup name rather than a runtime value. A qualified exported type such as plan.StudyTask is valid in type position, but plan.StudyTask { ... } is not a construction form. Select StudyTask when direct nominal construction is needed.

Common correction: import src.plan as plan { summarize } does not combine the two import forms. Choose either a namespace import or a selected import.

Root, initialization, and package are different decisions

  • A module path is a dotted address relative to the selected root, matching exact file and directory names.
  • Imports must be placed in the top-level import prologue, preceding other items.
  • Dependencies initialize once, prior to the module that imports them. An imported module may include declarations and bindings, but no free runtime-bearing statements such as a top-level loop or assignment.
  • topaz.toml specifies the package identity, entry point, dependencies, and build target for the toolchain. It does not introduce a second module declaration syntax.

The package command can supply the entry point and root, as demonstrated above. When specifying a file directly, the directory of the entry file serves as the default root unless --root explicitly designates a containing root.

Import cycles are rejected

Suppose a.tpz imports b.tpz:

import b

export function fromA() -> int {
    1
}

and b.tpz imports a.tpz:

import a

export function fromB() -> int {
    2
}

Executing topaz check a.tpz --root . reports the detected cycle:

Output
error[TPZ3006]: import cycle: a -> b -> a
 --> a.tpz:1:8
  |
1 | import b
  |        ^

a.tpz: 1 diagnostic

Every import edge participates in cycle detection, even when an imported name is used solely as a type. Resolve the cycle by moving shared declarations into a third acyclic module or by assigning the dependency to one direction. No exception exists for type-only cycles.

Exact boundaries

The current module syntax does not include use, string or template paths, an alias combined with a selection list, export lists, wildcard exports, re-exports, export-site renaming, or side-effect-only imports. export let mut is also rejected. Imports cannot reference paths outside the root, and namespaces cannot expose private members.

Continue with First Application for the complete tested package, Packages & Distribution for manifests and locks, and Forbidden & Deferred Forms for familiar module constructs that Topaz deliberately does not accept.