Authoritative source: Check, run, build, and test

Check, run, build, and test

A single .tl file is ideal for experiments. A real program soon needs several modules, a chosen entry point, build settings, tests, and eventually dependencies. A tulam project records those facts in a typed value named project.

The project file is not YAML, JSON, or a second configuration language. It is a restricted, pure tulam module checked against a compiler-supplied schema.

From one file to a project

We will use this directory layout:

hello-project/
├── tulam.project.tl
├── src/
│   └── App/
│       ├── Greeting.tl
│       └── Main.tl
└── test/
    └── App/
        └── Test.tl

Create src/App/Greeting.tl:

module App.Greeting;

function greeting(name:String) : String =
    concat("Hello, ", concat(name, "!"));

Create src/App/Main.tl:

module App.Main;

import App.Greeting;

action main() : Eff { console:Console } Unit = {
    putStrLn(greeting("Ada"))
};

Create test/App/Test.tl:

module App.Test;

import App.Greeting;

function main() : Int =
    if greeting("Ada") == "Hello, Ada!"
    then 0
    else 1;

The module path and header agree:

src/App/Greeting.tl  →  module App.Greeting;

Project planning checks this relationship. It also checks imports, duplicate module names, cycles, source roots, entry points, and dependency visibility before compilation proceeds.

The project manifest

Create tulam.project.tl in hello-project/:

module Project;

import Tulam.Build.V1;

value project : Project = Project {
    name = "hello-project",
    products = [
        Executable {
            name = "app",
            sourceRoots = ["src"],
            entry = Entry {
                moduleName = "App.Main",
                definition = "main"
            },
            dependencies = []
        },
        TestSuite {
            name = "tests",
            sourceRoots = ["src", "test"],
            entry = Entry {
                moduleName = "App.Test",
                definition = "main"
            },
            dependencies = []
        }
    ],
    dependencies = [],
    profiles = [
        Profile {
            name = "development",
            optimization = O2,
            debugInfo = LineTables,
            fpContract = Off,
            targets = [TargetProfileId { id = "llvm.native" }]
        }
    ],
    defaultProduct = "app",
    defaultProfile = "development"
};

There is a lot of information here, but each part has one job:

The manifest evaluator is deliberately limited. It accepts pure deterministic construction and helper functions, but it cannot read the filesystem, inspect environment variables, run actions, call intrinsics, or perform target switches. Project discovery must finish before those facilities are available.

Project discovery

From hello-project/, run:

stack --stack-yaml /path/to/tulam/stack.yaml exec tulam -- project show

Use the path to your compiler checkout. If you installed the compiler on PATH, the shorter form is:

tulam project show

The compiler searches the current directory and then its parents for the nearest tulam.project.tl. You can therefore run project commands from inside src/App/ and still select hello-project.

To choose a manifest or project directory explicitly:

tulam --project /path/to/hello-project project show

project show reports the normalized project, selected product and profile, target list, module count, and dependency selection. It is a good first command when the compiler appears to be using the wrong project.

check

tulam check

With no product name, this selects the manifest’s defaultProductapp in our example. It resolves the reachable module graph and runs the strict front end, but does not produce a native executable.

Select another product either positionally or with an option:

tulam check tests
tulam check --product tests

Use check frequently. It gives the shortest feedback loop and does not invoke the compiler-shipped native toolchain.

run

tulam run

This checks the default executable product, builds it for the selected profile, and runs it. Our example prints:

Hello, Ada!

You can select an executable product by name:

tulam run app

The current project driver executes through LLVM native. A profile naming only an unavailable future target is rejected rather than silently falling back to a different implementation.

build

tulam build

This performs the same checked compilation but does not launch the result. The default artifact path is:

.tulam/build/PROFILE/PRODUCT/PRODUCT

For this project it is:

.tulam/build/development/app/app

Choose another output path when packaging or inspecting an artifact:

tulam build --output ./hello-app

Project builds take their optimization, debug information, and floating-point contraction defaults from the selected profile. An explicit command-line --fp-contract=off|fast override applies to the whole artifact without changing source code.

test

tulam test

test selects a TestSuite product, builds its entry point, and runs it. If the default product is not itself a test suite, the command selects the only test suite when that choice is unambiguous. You may always name it:

tulam test tests

Our App.Test.main returns zero when the greeting is correct and one otherwise, and the current native entry wrapper prints that result. Compilation, linking, and runtime failures make tulam test fail. At present, however, a returned nonzero Int is printed rather than forwarded as the process exit status; test automation that needs assertion-sensitive failure must use an explicit failing test mechanism or a runner that validates the reported result.

A test suite is a normal checked tulam program. It can import project modules, use the standard library, print progress, and organize many assertions. Chapter 36 develops a fuller testing style.

project graph

tulam project graph

This prints the dependency-first reachable source graph, including module ownership and imports. For the app product, App.Greeting appears before App.Main because the latter imports it.

Only reachable modules are compiled. A module below a source root must still be well formed and uniquely named, but unrelated products do not become accidental entry dependencies.

Profiles and product selection

The manifest can declare several profiles and products. Select them explicitly:

tulam build --product app --profile development

A profile currently controls:

These are artifact settings. They do not create conditional syntax inside the source language.

Dependencies and locks

The project schema supports local dependencies and Git dependencies pinned to an exact complete commit identifier. A dependency declares a package that a product may import; it does not implicitly import that package’s modules.

Normal project commands create or update tulam.lock after dependency resolution. You can request resolution directly:

tulam lock

For reproducible automation:

tulam build --locked

--locked requires the existing lock to match the manifest and dependency content exactly. It never rewrites the file.

For work without network acquisition:

tulam build --offline

--offline requires pinned Git dependency checkouts to be already available locally. It does not weaken identity or content checks.

The dependency model intentionally has no version-range solver in this foundation. Exact identity keeps the graph deterministic.

Single-file commands remain available

Creating a project does not remove the quick workflow from Chapter 3:

tulam check path/to/file.tl
tulam run path/to/file.tl
tulam build path/to/file.tl --output ./program

A positional argument ending in .tl selects a file. A positional name such as app selects a project product. The distinction is intentional.

The compiled shell

Running tulam with no command opens the canonical compiled shell. It loads the standard library and, when present, the nearest project’s selected product and profile. Declarations persist after successful checking; expressions compile through the same RuntimeCore and native-provider path used by run and build.

function twice(x:Int) : Int = x * 2
twice(21)
:type twice
:worker
:reset

The expression prints 42. Invalid declarations are rolled back, so the last valid session remains usable. :{ and :} delimit multiline input; :load, :reload, and :reset preserve normal project/module rules. Native code runs in an isolated LLVM ORC worker. A crash or Ctrl-C discards that worker while retaining the checked session.

Use --verbosity trace --diagnostic-detail detailed to see every canonical artifact pass for an expression. See Canonical Compiled REPL for architecture and failure behavior.

Useful feedback options

The compiler can adjust human diagnostics and emit machine-readable output:

tulam check --diagnostic-detail detailed
tulam build --verbosity trace
tulam check --message-format jsonl
tulam check --message-format sarif
tulam explain TLC-PROJECT-0018
tulam stages

Trace mode reports bounded compilation telemetry and pass timings; it does not dump whole intermediate programs. Stable diagnostic codes can be explained with tulam explain CODE.

Recap

The normal workflow is:

edit → check → test → run/build

Next: A guided tour of the language.