Program Structure

Modules & Visibility

Connect two files with imports and exports, choose the root, and fix private-name and import-cycle errors.

Split a program across files and choose which names other files can use. You will run a two-file study planner, then fix private-name and import-cycle errors.

Install topaz using First Program. Each .tpz file is a module. No separate module declaration is needed.

Run a program from two files

In a working folder without an existing study-plan project, run these commands.

BASH
topaz init --root study-plan
cd study-plan

init creates topaz.toml and src/main.tpz. Keep the generated manifest. Add src/plan.tpz and replace src/main.tpz with the code below. After you generate the lock file, the layout is as follows.

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

Save the task data and functions in src/plan.tpz.

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"
}

In src/main.tpz, import the three names used by the application.

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)
}

Run these commands inside study-plan, beside topaz.toml.

BASH
topaz lock --root .
topaz check --root . --locked
topaz run --root . --locked

lock creates topaz.lock. The check exits with code 0, and the program prints the summary.

Output
tasks=2, remaining=25 minutes

The path src.plan selects src/plan.tpz under the chosen root. The compiler checks the entry file and the modules it imports, following their imports too. These files form the compilation unit. Unrelated files are not checked for this entry.

Choose which names to export

A declaration is private by default. Add export to make it available to another module. Here, StudyTask, makeTask, and summarize are exported. parseMinutes stays private.

If src/main.tpz tries to import or access parseMinutes, checking fails. Call the exported makeTask instead. To expose the helper intentionally, add export to its declaration in src/plan.tpz. Imported bindings are read-only.

TaskImport form
Select namesimport src.plan { StudyTask, makeTask }
Rename a selected nameimport src.plan { summarize as renderSummary }
Group names under a prefiximport src.plan as plan

The last form creates a namespace, a prefix for looking up exported names during compilation. Call plan.makeTask(...) through it. The namespace itself is not a runtime value.

Use plan.StudyTask in a type annotation. To construct StudyTask { ... }, select StudyTask directly. The form plan.StudyTask { ... } is rejected. If you wrote import src.plan as plan { summarize }, replace it with one of the separate import forms above.

Find files from the right root

Match each dotted path to the exact file and folder names under the root. Put imports at the top of the file, before other items.

Package commands read the entry, dependencies, and build target from topaz.toml. With an explicit file argument, the entry file's folder is the default root. Use --root to select a containing folder. If src.plan cannot be found, check both --root and the location of src/plan.tpz.

Dependencies initialize once, before the module importing them. An imported module can contain declarations and bindings. Free top-level loops and assignments are rejected, so put that work inside a function and call it from the entry.

Break an import cycle

In a separate folder, save this as a.tpz.

import b

export function fromA() -> int {
    1
}

Save this as b.tpz beside it.

import a

export function fromB() -> int {
    2
}

In that folder, topaz check a.tpz --root . exits nonzero and reports the cycle.

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

a.tpz: 1 diagnostic

Move shared declarations from a.tpz and b.tpz into a third module, then import it from both files. Alternatively, remove the dependency in one direction. Every import participates in cycle detection, including imports used for types.

Forms the compiler rejects

The compiler rejects use, string or template paths, an alias combined with a selection list, and side-effect-only imports. It also rejects export lists, wildcard exports, re-exports, export-site renaming, and export let mut. Import paths cannot leave the root, and namespaces cannot expose private names.

Continue with First Application to add tests and build this program. Use Packages & Distribution for dependencies and locks, or Forbidden & Deferred Forms to compare familiar syntax with supported forms.