Skip to content

Imports

Bring names or module namespaces into an Atoll source file.

Updated View as Markdown

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, line

report 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 21

An 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:17

Constructing 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:12

So: 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 26

as 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:39

Module 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 21

The 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 21

Placement

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:

  1. modules collected from the current source tree;
  2. module roots contributed by path or resolved git dependencies;
  3. built-in std/* compatibility paths;
  4. 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:1

A 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:1

These 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.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close