Skip to content

Projects

Configure module roots, dependencies, and source discovery with atoll.toml.

Updated View as Markdown

A project is a source tree rooted by atoll.toml. The manifest names the module root; the directory layout does the rest.

ledger/
  atoll.toml            module = "example.dev/ledger"
  main.at               module  example.dev/ledger
  money/
    amount.at           module  example.dev/ledger/money
    tax.at              module  example.dev/ledger/money
  report/
    format.at           module  example.dev/ledger/report
  audit/
    format.at           module  example.dev/ledger/audit
# ledger/atoll.toml
module = "example.dev/ledger"
name = "ledger"
version = "0.1.0"

module is the only required field, and it is the import prefix for every file in the tree. A file’s module path is module plus its directory relative to the manifest, so money/amount.at is in example.dev/ledger/money and main.at is in example.dev/ledger itself.

Field Purpose
module Required canonical import root
name Human-readable project name
description Project description
version Validated semantic version
package JVM package metadata

Omitting module is a load error, not a default, and version must be a full semantic version:

$ atoll check ledger
atoll check: atoll.toml: missing required `module` field

$ atoll check ledger          # with version = "1.0"
atoll check: atoll.toml: `version = "1.0"` is not valid semver:
  unexpected end of input while parsing minor version number

TOML is the supported format; historical YAML manifests are not part of the current toolchain.

atoll new demo scaffolds atoll.toml, src/main.at, tests/, docs/ and build/. Be aware that at HEAD its generated @test fn smoke calls an unresolved assert, so the fresh scaffold does not check until you delete that function:

error[ATOLL1009]: unresolved name `assert` at src/main.at:10:5

Entry points and units

A unit is an entry point plus everything it transitively calls. An entry point is fn main or any fn entry_*. One file may declare several, and each one becomes an independently linkable wasm module:

fn tax_cents(cents: int): int {
    return cents * 825 / 10000
}

fn main(): int {
    due := 1999 + tax_cents(1999)
    println("due cents: " + due.to_string())
    return due
}

fn entry_audit(): int {
    tax := tax_cents(1999)
    println("tax cents: " + tax.to_string())
    return tax
}
$ atoll build ledger -o out
Wrote 2 unit(s):
  - main         (out/atoll-unit-main)
  - entry_audit  (out/atoll-unit-entry-audit)

$ find out -name '*.wasm'
out/atoll-unit-main/main.wasm
out/atoll-unit-entry-audit/entry_audit.wasm

tax_cents is reachable from both entries, so it is linked into both bundles. The unit boundary is what makes per-unit incremental rebuilds possible: editing one entry’s body re-emits one bundle, editing a shared helper re-emits every bundle that reaches it.

Only reachable code is lowered

The consequence people trip over: a function no entry point reaches is type-checked but never lowered to wasm. Here probe calls a prelude method that has no lowering path at HEAD, and the build still succeeds:

fn probe(): void {
    body := Http.get("https://example.com")
    println("${body}")
}

fn main(): void {
    println("probe is never called, so it is never lowered")
}

Call it from main and the same program type-checks but fails to build:

fn probe(): void {
    body := Http.get("https://example.com")
    println("${body}")
}

fn main(): void {
    probe()
}
$ atoll check main.at
OK (2 diagnostics)

$ atoll build main.at -o out
error[ATOLL2004]: builtin method `to_string` has no lowering path — declared in
  the prelude but never implemented (no body, host import, or intrinsic), so
  this call cannot be compiled to wasm
1 error(s); build aborted

atoll check passing is not the same as atoll build passing. Gate on the build for anything you intend to ship.

Build, check, run

$ atoll check ledger                          # whole-project type-check
OK (0 diagnostics)

$ atoll run ledger/main.at                    # default unit
amount due: $156
atoll run: unit `main` returned 156

$ atoll run ledger/main.at --unit entry_audit
ada changed invoice to $21
atoll run: unit `entry_audit` returned 21

$ atoll build ledger -o out                   # one wasm per unit
Wrote 2 unit(s):
  - main         (out/atoll-unit-main)
  - entry_audit  (out/atoll-unit-entry-audit)

atoll check and atoll build accept the project directory. atoll run needs a file, and refuses a directory outright:

$ atoll run ledger
atoll run: read ledger: Is a directory (os error 21)

Pass a path with a directory component (ledger/main.at, ./main.at). A bare main.at from inside the project directory falls back to single-file compilation and will not see the cross-module imports.

Depending on another project

Everything in this section is one worked example. A library project:

common/
  atoll.toml          module = "example.dev/common"
  money.at
struct Money {
    cents: int
}

fn dollars(m: Money): int {
    return m.cents / 100
}

An application that depends on it by path:

# app/atoll.toml
module = "example.dev/app"
name = "app"
version = "0.1.0"

[dependencies]
"example.dev/common" = { path = "../common" }
import { Money, dollars } from example.dev/common

fn main(): int {
    m := Money { cents: 12345 }
    println("dollars: " + dollars(m).to_string())
    return dollars(m)
}

atoll walks both trees, resolves the import, and emits one unit:

$ atoll check app
OK (0 diagnostics)

$ atoll run app/main.at
dollars: 123
atoll run: unit `main` returned 123

$ atoll build app/main.at -o out
Wrote 1 unit(s):
  - main  (out/atoll-unit-main)

Note the dependency key: it is the dependency’s module path, not its directory name, and it must match the module value in that project’s own manifest.

Dependency forms

[dependencies]
"example.dev/common" = { path = "../common" }
"example.dev/http" = { git = "https://example.com/http.git", rev = "7a6c7f0" }
"example.dev/forms" = "^1.2"
Form Current behavior
{ path = "…" } Walked locally and compiled with the project
{ git = "…", rev = "…" } CLI fetches the pinned checkout, then walks it locally
version string Constraint is validated; there is no registry fetch or version solver

A dependency carrying its own atoll.toml uses that manifest’s module as its root; a bare local directory uses the dependency key as its root. Path entries are relative to the owning project unless absolute, and a missing directory is a load error. A git dependency must supply both git and rev — no branch or default revision is inferred, so builds stay reproducible.

Contexts that cannot fetch, such as some editor and test harnesses, reject an unresolved git dependency and defer to the CLI pipeline. Once fetched, the checkout is walked exactly like a path dependency.

Because a version-string dependency supplies no source, an import from it can never be fully type-checked. Prefer path dependencies locally and pinned git dependencies for shared builds.

Extra source roots

[libs] adds directories whose contents join the current project’s module namespace — no dependency entry, no separate manifest:

# libs/atoll.toml
module = "example.dev/libs"
name = "libs"

[libs]
paths = ["lib", "../shared"]

Each library path is resolved relative to the project root, and its own subdirectories are appended to the configured module root. With the manifest above, lib/util/clamp.at lands in example.dev/libs/util — the lib/ segment itself disappears:

fn clamp(v: int, lo: int, hi: int): int {
    if v < lo { return lo }
    if v > hi { return hi }
    return v
}
import { clamp } from example.dev/libs/util

fn main(): int {
    v := clamp(12, 0, 10)
    println("clamped: " + v.to_string())
    return v
}
$ atoll run libs/main.at
clamped: 10
atoll run: unit `main` returned 10

Because those files share one namespace with the main root, duplicate declarations across the two roots collide. Use [libs] for vendored source you own; use [dependencies] when the other tree has its own module identity.

Source discovery

The loader recursively collects .at files under every root, skipping dot-prefixed directories, target, node_modules, and build. Files are sorted before loading so project input and diagnostics are deterministic; other extensions do not participate in module scope.

atoll check path/to/tree also works on a directory with no manifest. The directory name becomes a synthetic root module and the same exclusions apply, but there are no dependencies, datasources, or host allow-list. A lone .at file compiles as a one-shot target when no project contains it. Create atoll.toml as soon as module identity or reproducible behavior matters.

Host capabilities

[host] controls which @host namespaces the project may call.

Configuration Behavior
No [host] section Allow all namespaces
allow = [] Deny every host namespace
Non-empty allow Permit exactly the listed namespaces

Denial is a compile-time diagnostic, not a runtime surprise. This program is ordinary Atoll:

fn main(): int {
    println("hello")
    return 0
}

With allow = ["atoll_runtime::io"] it checks and runs. With allow = [] it is rejected before anything is emitted:

error[ATOLL4001]: host call `println` requires the `atoll_runtime::io`
  capability, which this project does not grant
1 error(s) found

Note that println needs atoll_runtime::io — an empty allow-list really does deny everything, including printing. Write an explicit allow-list for anything you deploy; omitting the section is convenient but expresses no least privilege.

Datasources

Each [datasources.NAME] entry requires schema, engine, and url:

[datasources.PRIMARY]
schema = "ledger"
engine = "PostgreSQL"
url = "${DATABASE_URL}"
readOnly = false
pool = { min = 2, max = 16 }

Engine names are case-sensitive, and a typo fails the manifest load with the full set spelled out:

atoll check: atoll.toml [datasources.PRIMARY]: unknown engine `Postgres`
  (expected one of: PostgreSQL, MySQL, SQLite, Turso, DataFusion, DuckDB)

${VAR} references in a datasource URL are substituted while parsing, so an unset variable is a configuration error rather than a runtime one:

atoll check: atoll.toml: environment variable `${DATABASE_URL}` referenced by
  `datasources.PRIMARY.url` is not set

Datasource names label configuration entries; query routing is driven by the declared logical schema. readOnly = true rejects compiled writes routed to that datasource. See Datasources.

Locking

atoll lock records each local and git dependency with a hash of its .at source tree:

# atoll.lock — generated by `atoll lock`. Commit this file.
# Records resolved dependencies for reproducible builds.

version = "1"
project = "example.dev/app"
locked_at_unix = 1785316334
compiler = "0.1.0"

[deps."example.dev/common"]
kind = "local"
source = "../common"
content_hash = "8fd19a88fae7d39b"

atoll lock --check is the CI gate. Editing a path dependency without re-locking is intentionally visible:

$ atoll lock --check
atoll.lock is up to date

$ atoll lock --check          # after editing common/money.at
atoll lock: atoll.lock is out of date for: example.dev/common (changed)
  (run `atoll lock`)

atoll build and atoll run may fetch a pinned git dependency without rewriting the lockfile; use the explicit command when you mean to update recorded state. Registry resolution does not exist yet, so a lockfile is not a version-solver output.

The lock covers dependency source and nothing else:

Input Lock coverage
Project .at files not locked — use your VCS revision
Path dependency source source-tree hash
Pinned git dependency revision and source-tree hash
Version constraint no resolved package
Datasource engine, schema, URL not locked
Host allow-list not locked

Reproducible deployment therefore needs the lockfile and an external build record naming the compiler version, source revision, manifest, and required environment variables.

Other sections

The manifest also parses [identity], [embed], [messages], [dev], and package. These configure compilers and runtimes; none of them introduce names into Atoll source. [messages] requires a non-empty locale list whose default appears in it, and [dev] ports must fit the platform port range. A section being parsed does not mean every historical framework feature behind it has been implemented — see Feature Status.

Keeping failures distinct

Boundary Example failure
Manifest parsing invalid engine name or pool range
Environment substitution missing ${DATABASE_URL}
Type checking denied host namespace; unresolved import
Lowering reachable call with no lowering path (ATOLL2004)
Runtime startup database host unreachable
Runtime operation credentials lack table permission

Inlining a secret into atoll.toml removes a substitution error and creates a source-control exposure. It is not a fix for deployment configuration.

For CI: choose a stable module, pin every git dependency, run atoll lock after intentional dependency changes, run atoll lock --check in verification jobs, gate on atoll build rather than atoll check, and declare host namespaces and datasource variables explicitly.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close