WEP: Visibility — internal / pub / export
Context
Wado had two visibility keywords: pub (visible to other Wado modules in the
same package) and export (visible at the Component Model boundary). export
carried two unrelated jobs at once:
- Library boundary — the package's public API, consumed by other Wado packages.
- CM boundary — the ABI surface lowered through the Canonical ABI and emitted into WIT.
These are not the same thing. The CM ABI cannot represent closures (fn
values), generics (CM is monomorphic), traits / dynamic dispatch, or effect
polymorphism (<effect E>). Because crossing a package boundary required
export, and those constructs cannot be exported, a generic, higher-order,
or trait-based library could not be published to other Wado packages at all —
even though Wado→Wado linking shares Wasm GC types directly and never needs the
CM ABI (see Package Manifest
§"Wado-to-Wado Optimization").
The naming also repeated a known Rust friction: pub named the in-library
module boundary, so the genuine library boundary had to borrow the same word.
Decision
Split the single export ladder into two orthogonal axes.
Axis 1 — Wado visibility (a scope ladder):
| Keyword | Reach | Rust analogue |
|---|---|---|
| (none) | The defining file | (none) |
internal |
Other files in the same package | pub(crate) |
pub |
Other Wado packages (the library API) | pub |
Axis 2 — CM surface (an orthogonal, additive flag):
| Keyword | Meaning |
|---|---|
export |
Also lower this item at the CM boundary. Must be CM-representable. |
export is the analogue of Rust's extern "C" + #[no_mangle]: a separate
ABI surface, not a visibility level. Rules:
export ⟹ pub. A CM export is by definition part of the public API. The formerpub exportcollapses to plainexport.exportrequires CM-representability, checked at the definition site (so the "appears in WIT" guarantee stays static). Closures, generics with non-WIT bounds, and<effect E>in an exported signature remain a compile error (see WIT Interoperability).pub(withoutexport) carries no CM restriction. Generic, higher-order, and trait-based items become publishable across the Wado library boundary.
fn tokenize(s: String) -> List<Token> { ... } // file-private
internal fn build_ast(ts: List<Token>) -> Doc { ... } // package-internal
pub fn map<T, U>(f: fn(T) -> U, xs: List<T>) -> List<U> // library API (Wado-native)
export fn parse(s: String) -> Doc { ... } // library API + CM boundary
Reach of pub vs export
A pub-only item is Wado-native: it reaches any Wado consumer (a source
dependency, or a .wasm with Wado provider metadata, via the GC-sharing path).
An export item additionally reaches any CM consumer through the Canonical
ABI. A standalone .wasm consumed by a non-Wado component therefore exposes
only export items — pub-only items are invisible across that boundary.
This follows directly from the producer/consumer matrix in
Package Manifest §"Wado-to-Wado
Optimization"; the only change is that the library API is now pub, not
export.
Why no pub(crate) / pub(super) family
Wado is flat (1 file = 1 module; a package is a set of modules with no nested
module privacy). A single internal covers every in-package case, so the
scope-parameterized pub(...) forms have nothing to scope to. Unlike Rust,
pub is absolute: a pub item is library-public, never gated by an enclosing
private module.
Re-export visibility
The same ladder applies to re-exports. A use declaration may carry a
visibility modifier, re-exporting the imported names at that reach:
pub use { x } from "M"—xjoins this module's public API.internal use { x } from "M"—xis re-exported package-internal.- plain
use { x } from "M"— a file-private import; nothing is re-exported.
A re-export's reach is the re-export keyword's, not the original item's
visibility. So a pub use of an internal symbol publishes it: this is the
canonical "internal implementation, public facade" pattern, where a package's
entry module re-exports its internal submodules' items as the library API
(core:prelude and core:kiln are built this way — their submodule
definitions are internal, surfaced as pub through the facade's pub use).
The only constraint is that the re-export must itself be a legal import: you can
pub use { x } only a name visible at the re-export site (x is pub, or x
is internal and M is in this package). wado doc and the public-API query
view present such re-exports at the re-export's visibility, so a pub use-d
internal item appears as part of the facade module's public API.
Consequences
- Generic / higher-order / trait libraries are publishable across packages
(
pub), whichexportcould never express. exportkeeps its 1:1 correspondence with WITexport, with one job: the CM boundary.- Migration (pre-stable, no shim): former
pub→internal; formerexportandpub export→export; items that werepubonly because they are a package's public API and were alsoexported need no change.pub usere-exports gain aninternal usecounterpart for package-internal re-exports.
Implementation
- [x] Parse
internal(the keyword was previously reserved as a no-op).internalandpubare mutually exclusive;exportimpliespub(nopub export). The AST carries aVisibility { Private, Internal, Public }enum on every top-level declaration, orthogonal to theis_exportflag. - [x] Package identity:
ModuleSource::package_id()groups modules into packages.core:*is one package,wasi:*another (independent), the entry point and its local modules theRootpackage, and each resolved dependency / remote URL its own package. - [x] Enforcement at import resolution (analyze phase): file-private symbols are
never importable;
internalreaches only same-package importers;pub/exportreach anywhere. A violation is aPRIVATE_SYMBOLcompile error. TheSymboland re-export entries carry their declared visibility; namespace imports register only the visible members. The reachability ladder is a single predicate,Visibility::reachable_from(same_package), shared by the analyze-phase symbol registration and the elaborator's namespace-global collection so the two never disagree (a same-packageinternalglobal is reachable throughuse ns from "..."; ns::FOOas well as a named import). - [x]
internal usere-exports (the package-internal counterpart topub use). - The ladder applies to top-level items and to struct fields. A struct field's
internalreaches other files in the same package;pubreaches other packages; no modifier is file-private. Reading, setting, or binding a field beyond its reach (field access, struct literal, or destructuring pattern) is aPRIVATE_SYMBOLcompile error, checked via the sameVisibility::reachable_from(same_package)predicate as item imports. - Impl members (methods, associated constants) carry a binary
pub/ file-private visibility;internalandexporton an impl member are a compile error with a targeted diagnostic. - The bundled
core:internalmodule was renamed tocore:rtso the module name no longer collides with theinternalvisibility keyword.
