Top-level declarations are public by default. Add private to keep one
inside the file that declares it.
struct Money {
cents: int
}
private fn cents_of(m: Money): int {
return m.cents
}
fn total(items: []Money): Money {
mut acc := 0
for m in items {
acc = acc + cents_of(m)
}
return Money { cents: acc }
}
fn main(): int {
sum := total([Money { cents: 1999 }, Money { cents: 450 }])
println("cents: " + sum.cents.to_string())
return sum.cents
}Money and total may be imported anywhere. cents_of may be used only inside
this file.
The boundary is the file
This is the rule people get wrong: private is enforced per file, not per
directory. Sibling files in the same module share every public name
automatically, and see nothing private at all.
Reference to cents_of from… |
Result |
|---|---|
| the same file | resolves |
| a sibling file in the same directory | ATOLL1009: unresolved name cents_of |
| a named import in another module | ATOLL1041: … no public export named cents_of (the declaration is private) |
a module-namespace member (money.cents_of) |
not found |
Split the program above so total lives in money/tax.at while cents_of
stays in money/amount.at, and it stops compiling even though both files are
the same module:
error[ATOLL1009]: unresolved name `cents_of`
--> money/tax.atFrom another module the same mistake reads differently, because the import itself is what fails:
error[ATOLL1041]: module `example.dev/ledger/money` has no public export named
`cents_of` (the declaration is `private`)
--> report/format.at:1:1That is stricter than the older design text, which described directory-wide privacy. A sibling that needs the behavior cannot reach for it — promote it to a public declaration with a contract you are willing to keep, or move it next to its only caller.
Where the modifier goes
private applies to top-level functions, structs, enums, errors, constants,
type aliases, and traits. Place it after any decorators and before the
declaration keyword:
private const RATE_BP: int = 825
private type Basis = int
private error DecodeError {
InvalidHeader
}
@inline
private fn scale(cents: int, bp: Basis): int {
return cents * bp / 10000
}
pub struct Money {
cents: int
}
pub fn with_tax(m: Money): Money {
return Money { cents: m.cents + scale(m.cents, RATE_BP) }
}
fn main(): int {
due := with_tax(Money { cents: 1999 })
println("cents: " + due.cents.to_string())
return due.cents
}Everything private here — the rate, the Basis alias, the error, the helper —
is invisible one file away. Money and with_tax are the exported surface.
There is no per-member visibility
private on a struct field does not parse:
struct Money {
cents: int
private rate: int
}
fn main(): void {
println("${Money { cents: 1, rate: 2 }.cents}")
}Neither does private on a method:
struct Money {
cents: int
private fn raw(self): int {
return self.cents
}
fn dollars(self): int {
return self.raw() / 100
}
}
fn main(): int {
return Money { cents: 1999 }.dollars()
}Both report ATOLL1009: Expected identifier at the modifier. Fields and methods
follow the visibility of the declaration that owns them. There is no protected
tier, no friend mechanism, and no package-private level between file and
project. To keep a helper off the public surface, make it a private top-level
function taking the value as a parameter, as cents_of does above.
Explicit pub
pub states the default explicitly:
pub struct Booking {
id: int
seats: int
}
pub fn open_booking(id: int): Booking {
return Booking { id: id, seats: default_seats() }
}
private fn default_seats(): int {
return 2
}
fn main(): void {
booking := open_booking(4)
println("${booking.id}/${booking.seats}")
}pub and the bare default are semantically identical. Reserve it for generated
code or a project style rule that wants every export marked; mixing the two
arbitrarily within a file just adds noise.
Neither form grants host capabilities or bypasses a dependency’s own visibility, and neither re-exports anything: an imported declaration keeps the identity and visibility of its defining module.
A private type can still escape
The compiler does not check that a public signature only mentions public types.
Given secret/token.at:
private struct Token {
id: int
}
fn issue(id: int): Token {
return Token { id: id }
}
fn token_id(t: Token): int {
return t.id
}another module can import the two public functions, hold the value, and pass it back — this compiles and runs:
import { issue, token_id } from example.dev/vis/secret
fn main(): int {
t := issue(7)
println("id: " + token_id(t).to_string())
return token_id(t)
}What the caller cannot do is name the type. Add Token to that import and the
import fails:
error[ATOLL1041]: module `example.dev/vis/secret` has no public export named
`Token` (the declaration is `private`)So the value is usable and unnameable: no annotation, no local variable with an explicit type, no wrapper struct field. Treat “do not expose a private type in a public signature” as a design rule you apply yourself — the alternative is a public API nobody downstream can write a type for.
What a public name commits you to
Visibility says whether a name is reachable. It does not freeze the contract behind it, so review the whole surface before exporting:
| Public declaration | Callers depend on |
|---|---|
| Function | Parameters, mutation, success type, error type, effect row, bounds |
| Struct | Type identity, fields, value behavior |
| Enum or error | Variants, payloads, exhaustiveness |
| Trait | Requirements, defaults, supertraits, associated items |
| Alias | Transparency or distinct identity, and its target |
| Constant | Name, type, value |
Public declarations also join module-wide collision checking across sibling files, so choose names at module scope rather than file scope.
Changing visibility
Making a public declaration private breaks external imports and same-module
sibling references, and the two produce different diagnostics (ATOLL1041
versus ATOLL1009). Before tightening, search for named imports, namespace
member access, and unqualified same-module uses; move the consumers in the same
change so the review reads as one decision.
Before widening, check naming, explicit signature types, error and effect stability, and module ownership. Making a helper public can create downstream dependencies and trait-coherence conflicts even when its body never changes.
Moving a public declaration between files in the same directory preserves its import path. Moving it to another directory changes that path — see Modules.