Learn Topaz

First Application

Turn the study plan into a locked, tested two-module package and run its Python product without Topaz source or compiler.

Outcome: assemble a locked two-module package, run one selected test and the package entry, then execute its Python product without the .tpz project or Topaz compiler.

Prerequisite: complete Failures and Resources, install topaz, and have Python 3.11 or newer available.

Start with the ordinary scaffold

From the directory that should contain your project, run:

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

init refuses to overwrite an existing project. It creates topaz.toml and src/main.tpz; it does not create a lockfile or test. Replace the entry source and add the module and selected test so the tree becomes:

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

The scaffold manifest is:

TOML
[package]
name = "study-plan"
version = "0.1.0"
language = "5.10"
entry = "src/main.tpz"

[build]
target = "native"
deterministic = true

[dependencies]
std = "5.10"

Create the two modules

src/plan.tpz owns the domain 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"
}

Only the names marked export are visible to another module. parseMinutes remains a private implementation detail.

src/main.tpz imports those visible names and supplies the package entry:

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

The explicit main receives command-line arguments and standard input, then returns either an exit code or an error string. This application needs neither input, but the signature keeps the product boundary visible.

Add tests/plan.tpz:

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

match makeTask("Read syntax map", "15") {
    case Ok(task) => assert(task.minutes == 15, "valid minutes")
    case Err(message) => assert(false, message)
}

match makeTask("Read syntax map", "later") {
    case Ok(_) => assert(false, "invalid minutes were accepted")
    case Err(message) => assert(
        message == "minutes must be a positive integer",
        "invalid minutes",
    )
}

let tasks: Array<StudyTask> = [
    StudyTask { title: "Run first program", minutes: 10, done: true },
    StudyTask { title: "Build application", minutes: 25 },
]
assert(
    summarize(tasks) == "tasks=2, remaining=25 minutes",
    "summary",
)

The isolated TestHost supplies the canonical assert(...) function for tests. The file checks valid input, invalid input, and the final summary without changing production code.

Lock, format, check, test, and run

Create and inspect the dependency lock:

BASH
topaz lock --root .

The generated topaz.lock is:

TOML
[[package]]
name = "study-plan"
version = "0.1.0"
source = "root"
manifest_hash = "sha256:780ff549a9fb9b09d27c62df8ce48e70c1fc7b33d6d429e6bb826685f42e7615"

Now run the local quality loop in order:

BASH
topaz fmt --check --root .
topaz check --root . --locked
topaz test tests/plan.tpz --root . --locked

fmt --check reports drift without rewriting files, check validates the resolved unit, and the explicit path selects one test entry. The test output is:

Output
tests/plan.tpz: test-ok

Run the package entry:

BASH
topaz run --root . --locked

It prints:

Output
tasks=2, remaining=25 minutes

Build and run without the Topaz project

The scaffold’s default target is native, but an explicit command can select another target. Build the course’s Python product:

BASH
topaz build --target python --root . --locked --out-dir ../study-plan-product

Python is deliberate here: it needs Python 3.11 or newer at runtime, but it does not add a Rust installation to the beginner path. The managed product directory contains exactly:

study-plan-product/
├── GENERATED-OUTPUT-NOTICE.txt
├── LICENSE
├── NOTICE
├── program.py
├── topaz-artifact.json
└── topaz_py_rt.py

Move the entire Topaz project out of the way, then run from the product directory:

BASH
cd ..
mv study-plan source-unavailable
cd study-plan-product
python3 program.py

In PowerShell, replace the move command with Rename-Item study-plan source-unavailable, and use python program.py. The result is byte-for-byte the package output:

Output
tasks=2, remaining=25 minutes

The product still contains generated Python and its runtime support; “without source” here means it contains no .tpz files and does not need the Topaz project, compiler, registry, or build workspace.

Keep the boundaries straight

  • A file is one .tpz source file.
  • A module is the namespace a file exposes through export.
  • A unit is the set of Topaz files resolved from the entry and its imports.
  • A package records product identity and locked dependencies in topaz.toml and topaz.lock.
  • A target chooses the product shape, such as Python.
  • An artifact is the managed build result you deliver and run.

Decision: source loop or product boundary?

Use check, test, and run while changing the Topaz project because they give fast source-level feedback. Use build when you need the exact deliverable and then test that deliverable outside the project. Running only inside the source tree proves the program, not the delivery boundary.

Try this

Change the second task’s minute text in src/main.tpz from "25" to "30". Which expectations must change before the complete loop passes?

Answer

The package and Python runs should print tasks=2, remaining=30 minutes. The selected test constructs its own 25-minute task, so its existing summary remains correct and still passes. This separation shows that the test owns its fixture rather than silently depending on main.

Ready to continue when

You can explain all six boundaries above, regenerate and enforce the lockfile, run fmt --check, check, the selected test, and run, and execute the Python product after the .tpz project is unavailable.

Use Syntax at a Glance as your map, then deepen the parts you need in Modules & Visibility, Application Loop, and Artifacts & Deployment.