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.tpzsrc/plan.tpz owns the data model and reusable operations:
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:
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:
topaz check --root . --locked
topaz run --root . --locked
tasks=2, remaining=25 minutesThe 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:
| Goal | Canonical shape |
|---|---|
| Select a few names | import src.plan { StudyTask, makeTask } |
| Rename one selected name | import src.plan { summarize as renderSummary } |
| Bind a namespace | import 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.tomlchooses 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:
error[TPZ3006]: import cycle: a -> b -> a
--> a.tpz:1:8
|
1 | import b
| ^
a.tpz: 1 diagnosticEvery 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.