Wado

WEP: Package and Module Specifier Syntax

Context

The module specifier (from "...") carried two unreconciled designs:

Neither lets a specifier name a Component Model package by its real registry coordinate (ns:pkg@ver), so the WIT identity a Wado package already has does not show through in source — counter to "Wasm in plain sight". Bare names are also ambiguous on sight ("router" reads like a mistyped path), and a single-file script has no way to declare an external dependency at all.

This WEP settles the specifier grammar and the resolution rules behind it. It supersedes the namespace grammar of Module Loader and the bare-name resolution of Package Manifest; the latter is updated in place as a living spec.

Decision

A specifier is a CM package coordinate, a lib: alias, a local path, or a remote URL. Two principles anchor the rest:

Specifier forms

Form Resolver
wasi:… / core:… Reserved → compiler-bundled
ns:pkg[@ver] (open ns) Default registry, or with/manifest override
lib:nick Indirection: alias / rename / private dep
./ ../ Local file
http(s):// Remote

wasi:/core: are not a separate scheme — they are coordinates whose namespace happens to be bundled. Nested namespaces (a:b:pkg) follow WIT.

A specifier names a package only; it carries no interface segment. Interfaces and their members are selected in the use { ... } list (Iface, Iface::{op}), as Wado already does. A package's internal file layout (the bundled stdlib's core:prelude/array.wado-style splits) is a loader detail, never a user-facing specifier.

lib: is the sole home for indirection

Renames, short names, multiple coexisting majors, and dependencies that have no public coordinate (closed-source git/path, the natural state of an unpublishable package) all use lib:nick. Aliases are forbidden under any open ns:: that keeps a real coordinate (from "foo:regexp", transparent) visually distinct from a local indirection (from "lib:rx", "see the manifest").

Bundling is a default, not a lock-in: because lib: resolution is pure indirection, a registry-hosted alternative to a bundled namespace (a forked or newer wasi:/core: package) is reachable by aliasing it under lib: — slightly ugly, but possible. Easy things stay easy; hard things stay possible.

The package field bridges a lib: alias to its real coordinate:

Key (≡ specifier) package Source
"foo:regexp" omitted key is the coordinate
"lib:rx" required registry alias → package = "foo:regexp"
"lib:shared" optional git/path; resolved package self-declares

Multiple majors fall out of this without special keys:

"lib:http1" = { package = "std:http", version = "^1.0" }
"lib:http2" = { package = "std:http", version = "^2.0" }

Version: a range only where a lock backs it

Position Form
Manifest version range required (^/~/=); bare = error
Specifier @ver, single-file with exact only; a range here is an error

A range is meaningful only when a wado.lock resolves it. Lock-less positions take an exact pin; a range there is reported with a clear message (use an exact version, or declare the dependency in wado.toml).

Single-file parity

With no wado.toml, an inline with { ... } carries the same source vocabulary as a [dependencies] value (git/ref/registry/package/path/exact version). Inline with and a manifest entry for the same specifier are mutually exclusive. Single-file scripts have no lock file — reproducibility comes from the exact pins in with.

Bare names (from "router") are rejected everywhere.

Consequences

Positive

Trade-offs