Wado

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:

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:

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:

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

Implementation

References