Skip to content

Visibility

Control whether a declaration can be used outside its source file.

Updated View as Markdown

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

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

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

Navigation

Type to search…

↑↓ navigate↵ selectEsc close