Program Structure

Modules & Visibility

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

Use modules when one file has a clear responsibility that another file needs. Each .tpz file is one module; there is no separate module declaration.

Build one program from two files

The study-plan application has this source layout:

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

src/plan.tpz owns 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 selects the three names it needs:

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 run the resolved unit:

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

The explicit root makes src.plan resolve to src/plan.tpz. The entry and everything reachable through its imports form the compilation unit; unrelated files outside that closure are not checked as part of this entry.

Export only the boundary another module needs

Names are private unless their declaration starts with export. StudyTask, makeTask, and summarize cross the module boundary. parseMinutes remains an implementation detail, so trying to select or access it from src/main.tpz is 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 is a compile-time lookup name, not 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 the namespace import or the selected import.

Root, initialization, and package are different decisions

  • A module path is a dotted address under the selected root, with exact file and directory names.
  • Every import belongs in the top-level import prologue, before other items.
  • Dependencies initialize once, before the module that imports them. An imported module may contain declarations and bindings, but not free runtime-bearing statements such as a top-level loop or assignment.
  • topaz.toml chooses package identity, entry, dependencies, and build target for the toolchain. It does not introduce a second module declaration syntax.

The package command can supply the entry and root, as it does above. When a file is supplied directly, the entry file’s directory is the default root unless --root selects a containing root explicitly.

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
}

Running topaz check a.tpz --root . reports the checked cycle:

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

a.tpz: 1 diagnostic

Every import edge participates, even if the imported name is used only as a type. Break the cycle by moving shared declarations into a third acyclic module or by assigning the dependency to one direction. There is no type-only cycle exception.

Exact boundaries

Current module syntax does not include use, string or template paths, an alias plus 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 escape the root, and a namespace cannot expose a private member.

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