An import makes declarations from another module visible in one source file.
Paths are unquoted, use / between segments, and always name a canonical
module — never a filesystem location and never a path relative to the importing
file.
Every example below is a file of this project:
ledger/
atoll.toml module = "example.dev/ledger"
main.at module example.dev/ledger
money/
amount.at module example.dev/ledger/money — Money, usd, total, with_tax
report/
format.at module example.dev/ledger/report — Line, render, line
audit/
format.at module example.dev/ledger/audit — Line, render, linereport and audit deliberately export the same two names; that collision is
the subject of the last section. A single file of a multi-file project cannot be
compiled alone, so these listings are marked nocheck — each one was run as
part of the tree.
Named imports
A named import puts selected declarations directly into local scope.
import { usd, with_tax } from example.dev/ledger/money
import { Line, render } from example.dev/ledger/report
fn main(): int {
due := with_tax(usd(19, 99))
println(render(Line { label: "invoice", amount: due }))
return due.dollars()
}$ atoll run ledger/main.at
invoice: $21
atoll run: unit `main` returned 21An unbraced list also parses. It is the older shape, still accepted, and it has
no as form:
import usd, with_tax from example.dev/ledger/money
import Line, render from example.dev/ledger/report
fn main(): int {
due := with_tax(usd(19, 99))
println(render(Line { label: "invoice", amount: due }))
return due.dollars()
}Prefer the braced form in new code.
Renaming a function with as
as renames an imported function. The alias is local to the importing file
and is not part of the declaration’s public identity:
import { usd, with_tax } from example.dev/ledger/money
import { Line, render as render_line } from example.dev/ledger/report
fn main(): int {
due := with_tax(usd(19, 99))
println(render_line(Line { label: "invoice", amount: due }))
return due.dollars()
}as does not alias a type
This is the sharp edge. At HEAD, as on an imported type does not create an
alias — it introduces a fresh, unresolved name that does not unify with the type
it was supposed to rename:
import { Money as Amount, usd } from example.dev/ledger/money
fn main(): int {
a := usd(2, 50)
b: Amount = a
return b.cents
}error[ATOLL2002]: expected `Amount`, found `Money` at main.at:5:17
error[ATOLL2002]: type mismatch: expected `Amount`, got `Money`
(from a type annotation) at main.at:5:17Constructing the alias directly is no better — the aliased name carries none of the original’s methods:
error[ATOLL2003]: no method `dollars` on type `Amount` at main.at:5:12So: alias functions freely, and import types under their own names. When you
genuinely want a second name for a type, declare a type alias — that one is
transparent, and the two names unify in both directions:
struct Money {
cents: int
fn dollars(self): int {
return self.cents / 100
}
}
type Amount = Money
fn twice(a: Amount): Amount {
return Money { cents: a.cents * 2 }
}
fn main(): int {
a := twice(Money { cents: 1250 })
println("dollars: " + a.dollars().to_string())
return a.dollars()
}This works on an imported type too: write
import { Money } from example.dev/ledger/money and then type Amount = Money
in the importing file. What it cannot do is disambiguate — an alias for a name
that is already ambiguous inherits the ambiguity. For two same-named types from
different modules, use a module namespace.
Module namespaces
Importing a module without braces creates a namespace whose default local name is the last path segment. Reach members with dot syntax — in value position and in type position:
import example.dev/ledger/money as cash
import example.dev/ledger/report as fmt
fn subtotal(a: cash.Money, b: cash.Money): cash.Money {
return a.plus(b)
}
fn main(): int {
due := cash.with_tax(subtotal(cash.usd(19, 99), cash.usd(4, 50)))
println(fmt.render(fmt.line("amount due", due)))
return due.dollars()
}$ atoll run ledger/main.at
amount due: $26
atoll run: unit `main` returned 26as on a namespace works properly — unlike as on a type — because it renames
the namespace, not the declaration.
One restriction: a qualified struct literal does not parse. fmt.Line { … }
is a syntax error, which is why report/format.at exports a line constructor
function. Write fmt.line("amount due", due), not fmt.Line { … }:
error[ATOLL1007]: Expected ')' at main.at:8:39Module namespaces suit an API with many related names, or one where the namespace makes call sites clearer. Named imports are shorter for a small, unambiguous set of foundational types.
Collisions
Two imports can introduce the same local name. Today the compiler does not
diagnose that: the later import silently wins. Here report and audit
both export Line and render, and the program compiles and runs against the
audit pair, with no warning:
import { usd, with_tax } from example.dev/ledger/money
import { Line, render } from example.dev/ledger/report
import { Line, render } from example.dev/ledger/audit
fn main(): int {
due := with_tax(usd(19, 99))
println(render(Line { label: "invoice", amount: due, actor: "ada" }))
return due.dollars()
}$ atoll run ledger/main.at
ada changed invoice to $21
atoll run: unit `main` returned 21The actor field is the tell: that is audit.Line, not report.Line. Delete
the audit import line and the program still compiles — against a different
type. Unaliased module namespaces collide the same way when two paths share a
final segment.
Because as cannot rename a type, the fix is a namespace on at least one side:
import { usd, with_tax } from example.dev/ledger/money
import example.dev/ledger/report
import example.dev/ledger/audit
fn main(): int {
due := with_tax(usd(19, 99))
println(report.render(report.line("amount due", due)))
println(audit.render(audit.line("ada", "invoice", due)))
return due.dollars()
}$ atoll run ledger/main.at
amount due: $21
ada changed invoice to $21
atoll run: unit `main` returned 21Placement
Imports are top-level declarations. They are not statements, not conditional, and not a runtime namespace object, so this does not parse:
fn main(): void {
import { Line } from example.dev/ledger/report
println("unreachable")
}Group them at the top of the file. An import belongs to that one file: a sibling in the same directory does not inherit it and writes its own. Only same-directory public declarations arrive automatically, without any import.
Resolution
An import path is matched, in order, against:
- modules collected from the current source tree;
- module roots contributed by path or resolved git dependencies;
- built-in
std/*compatibility paths; - otherwise, an unresolved external path.
A path with no .at files behind it is an error, and a typo in the last segment
is the common way to meet it:
error[ATOLL1040]: unknown module `example.dev/ledger/monye` — no `.at` files
found at that path (or the module hasn't been resolved yet)
--> report/format.at:1:1A path that resolves but does not export the requested name is a different
failure, and so is a name that exists but is private:
error[ATOLL1041]: module `example.dev/ledger/money` has no public export named
`Reservation`
--> report/format.at:1:1
error[ATOLL1041]: module `example.dev/ledger/money` has no public export named
`cents_of` (the declaration is `private`)
--> report/format.at:1:1These have different fixes. Editing { Money } cannot repair a missing module;
adding a dependency cannot repair a visibility boundary. See
Visibility.
Local and dependency modules are all registered before import resolution, so
mutual imports between modules resolve without ordering them — money
importing from report while report imports from money checks and runs.
Version-string dependencies do not supply source — there is no registry resolver
yet — so declaring a constraint alone cannot make such an import type-checked.
An external path whose source was never loaded produces an external-dependency
warning rather than an error, which keeps single-file and editor checking useful
without pretending the declaration was verified. That leniency is narrow: a
function imported from an unresolved path is still ATOLL1009: unresolved name
at its call site. Do not read a warning-only editor pass as a whole-project
build.
Prelude names
List, Map, Set, Option, Result, Stream, Task, and the scalar types
come from the compiler prelude. They need no import:
fn main(): void {
mut names: []string = []
names.add("ada")
names.add("grace")
mut seen: Map[string, int] = Map.new()
for n in names {
seen.put(n, n.len())
}
println("${names.len()} names, ${seen.len()} lengths")
println("first: ${names[0] ?? "none"}")
}Old documents show import { list, map, option, result } from std/collections.
That lowercase spelling contradicts the current
naming rules and imports nothing you do not already
have. Import a library module only when the API is genuinely outside the
prelude.
Re-export
There is no re-export and no wildcard export list. An import does not create a
new public declaration in the importing module: downstream code that wants
Money imports it from example.dev/ledger/money, whatever local name you
chose.
A public function may of course mention an imported type in its signature. Callers still resolve that type through its defining module.
An import controls name reachability, not dependency acquisition — configure the
source of a dependency in atoll.toml, described in
Projects.