Wado

Packages

A package is a set of modules that is built, versioned and depended on as one unit. This chapter covers which modules make up a package, the manifest that describes it, the dependencies it declares, and what it offers the packages that depend on it.

The Modules of a Package

A package bounds how far internal reaches (Visibility) and which impls it may write (Package Boundary). A program's modules fall into packages this way:

The Manifest

A package's manifest is the file wado.toml in its root directory. Every path the manifest holds is relative to that directory. This is the manifest of package-gale-highlight-wado:

[package]
name = "gale-highlight-wado"
description = "Syntax highlighter for the Wado language, built on Gale"
repository-directory = "package-gale-highlight-wado"
lib = "src/lib.wado"
# version / repository / namespace / license / authors inherited from [workspace.package]

[world]
"wasi:cli/command" = "src/main.wado"

# gale is used in two roles: the Kiln generator that turns the bundled grammar
# into a parser at build time, and the `gale-highlight` runtime lib (`run_cli`).
[build-dependencies]
"lib:gale" = { path = "../package-gale", package = "wado-lang:gale", version = "^0.0.19" }

[dependencies]
"lib:gale" = { path = "../package-gale", package = "wado-lang:gale", version = "^0.0.19" }

[test]
# `build/` holds Kiln intermediates; not source (see package-gale's note).
exclude = ["build/**"]

The manifest holds these tables:

Table Holds Specified in
[package] The package's identity and metadata, and its library [package]
[world] The hosted worlds the package targets Selecting a World
[dependencies] The packages its code imports Dependencies
[dev-dependencies] The packages only its development needs Dependencies
[build-dependencies] The Kiln generators its imports run Kiln Generators
[registries] Names for the registries its dependencies come from Registries
[workspace] The packages developed together with it Workspaces
[test] Which files wado test discovers Test Discovery
[format] Which files wado format discovers Wado Formatter

A key the manifest does not define draws a warning and is ignored. This holds at the top level, in [package], in [workspace.package], and in the table form of a [world] entry. A misspelled key is reported, and the build goes on. Every other mistake in a manifest is an error.

[package]
name = "geo"
version = "0.2.0"
descripton = "Plane geometry"   # warning: unknown field "descripton" in [package] (ignored)

A file with no wado.toml above it is compiled on its own. It can still name a dependency, through an inline source on the use (Inline Sources).

[package]

[package] names the package and describes it.

[package]
namespace = "acme"
name = "geo"
version = "0.2.0"
lib = "src/lib.wado"
description = "Plane geometry"
homepage = "https://geo.acme.example"
repository = "https://github.com/acme/tools"
repository-directory = "packages/geo"
documentation = "https://docs.acme.example/geo"
license = "MIT OR Apache-2.0"
authors = ["Alice <alice@acme.example>"]
wado-version = ">=0.5"
Field Type Required Meaning
name string yes The package's name
version string yes The package's version, in semantic versioning (0.2.0)
namespace string no The organization or person the package belongs to
lib string no The entry module of the package's library world
description string no A short summary
homepage string no The home page URL; repository when absent
repository string no The source repository URL, without a subdirectory
repository-directory string no The package's directory inside repository
documentation string no The documentation URL; repository when absent
license string no An SPDX license expression
license-file string no A file holding a license that has no SPDX identifier
authors string[] no The people or organization responsible
wado-version string no The compiler versions that can build the package (>=0.5)
publish bool no false keeps the package from being published; true when absent

name and namespace are each 1 to 64 characters from [a-zA-Z0-9_-]. The two together, namespace:name, are the package's coordinate: its identity in a registry, and the specifier other packages import it by.

[package]
namespace = "acme"   # coordinate: acme:geo
name = "geo"

A package with no namespace has no coordinate. It cannot be published, which suits an application or an internal tool. Other packages can still depend on it through a path or git source and a lib: key.

version is a full semantic version, so 0.1 is an error. wado-version is a semantic-versioning requirement, such as >=0.5.

license is checked as an SPDX expression, and one that does not parse is an error. license and license-file exclude each other.

[package]
name = "geo"
version = "0.2.0"
license = "MIT"
license-file = "LICENSE.txt"   # error: `license` and `license-file` are mutually exclusive

lib names the library world's entry module. That module is what the package offers other packages (The Library World). A package declares at least one world (Selecting a World).

Dependencies

A dependency is another package that this one uses. Three tables declare them, all with the same form:

A key is a coordinate or a lib: alias, and Package Specifiers says how a use names it. Each part of a key between colons is 1 to 64 characters from [a-zA-Z0-9_-].

[dependencies]
"acme:geo" = { path = "../geo" }             # a coordinate
"lib:geo" = { path = "../geo" }              # an alias
"lib:geo.v2" = { path = "../geo-next" }      # error: invalid dependency key "geo.v2"

Each value is an inline table that gives the dependency's source: where its code comes from. A source is a local path, a git repository, or a registry.

Path Sources

A path source names a directory on the local file system, or a single .wado file.

[dependencies]
"lib:geo" = { path = "../geo" }          # a directory with its own wado.toml
"lib:shared" = { path = "../shared.wado" }   # one file

A directory must hold a wado.toml whose [package].lib names the entry module (The Library World). A single .wado file is its own entry module. A path dependency is read from disk on every build and is never locked.

A path source may also carry a git or registry source. The build uses the path, and ignores the other source. Publishing replaces the path with the other source, so the published package does not point at a directory on the publisher's machine (Publishing).

[dependencies]
# Built from ../package-gale; published as a dependency on wado-lang:gale ^0.0.19.
"lib:gale" = { path = "../package-gale", package = "wado-lang:gale", version = "^0.0.19" }
# Built from ../shared; published as a dependency on the git repository.
"lib:shared" = { path = "../shared", git = "https://github.com/acme/shared.git", version = "^0.1.0" }

Git Sources

A git source names a repository by its URL, on any host.

Field Required Meaning
git yes The repository URL
version one of these A version requirement, matched against the repository's tags
ref one of these A tag, a branch or a commit SHA
directory no The package's directory inside the repository; the root if absent

A git source takes exactly one of version and ref. Giving both, or neither, is an error.

[dependencies]
"lib:router" = { git = "https://github.com/user/router.git", version = "^1.0.0" }
"lib:router-next" = { git = "https://github.com/user/router.git", ref = "main" }
"lib:foo" = { git = "https://github.com/org/monorepo.git", version = "^1.0.0", directory = "packages/foo" }
"lib:bad" = { git = "https://github.com/user/bad.git", version = "^1.0.0", ref = "main" }   # error

A version is matched against the tags that name a version. Leading letters are dropped from a tag before it is read, so v1.2.0 and release1.2.0 both name 1.2.0. A tag that is not a version after that is ignored.

directory is accepted only beside git. A path source names its directory directly.

Registry Sources

A registry source has a version and no path or git. The package comes from a registry, by its coordinate.

Field Required Meaning
version yes A version requirement
package for a lib: key The coordinate of the package the alias stands for
registry no A name from [registries]; default when absent

A coordinate key is the package's own coordinate, so it takes no package. A lib: key names the coordinate it stands for with package. A lib: key with neither package nor another source is an error, since nothing says which package it is.

[registries]
default = "oci://ghcr.io"
acme = "oci://registry.acme.example/wado"

[dependencies]
"wado-lang:cm-catalog" = { version = "^0.1.0" }                         # default registry
"lib:catalog" = { package = "wado-lang:cm-catalog", version = "^0.1.0" }  # an alias of it
"lib:geo" = { registry = "acme", package = "acme:geo", version = "^0.2.0" }
"lib:nothing" = { version = "^1.0.0" }                                  # error: which package?

A registry source with no registry field needs [registries].default, and without one it is an error.

Version Requirements

A version in the manifest is a requirement: the range of versions the dependency may resolve to. It starts with one of three operators, and a version with no operator is an error.

Requirement Meaning Accepts
^1.2.3 compatible >=1.2.3, <2.0.0
^0.2.3 compatible, before 1.0 >=0.2.3, <0.3.0
^0.0.3 compatible, before 0.1 >=0.0.3, <0.0.4
~1.2.3 patch updates only >=1.2.3, <1.3.0
=1.2.3 exactly this version 1.2.3
[dependencies]
"acme:geo" = { version = "^0.2.0" }   # OK
"acme:ui" = { version = "0.2.0" }     # error: bare version "0.2.0" requires explicit prefix (^, ~, or =)

A compatibility range is the set of versions ^ accepts from one version: one major version, one minor before 1.0.0, one patch before 0.1.0. Resolution groups the requirements on a package anywhere in the dependency graph by the range they fall in. In each group it picks the highest version that meets every requirement, and when none does, resolution fails and says which requirements conflict. Two groups resolve to two versions, which are two packages (One Package, One Set of Types). This is how two lib: aliases depend on two major versions of one coordinate.

Not yet implemented: the resolver gives each package one version, so requirements in two compatibility ranges conflict.

A range is allowed only in wado.toml, where a lock file resolves it. A specifier's @version and an inline source's version are a bare version (1.0.0), which names exactly that version. An operator or a range there is an error.

Package Specifiers

A use names a dependency by a coordinate <ns>:<pkg> in an open namespace, or by an alias lib:<nick> (Module Source Types). Either is resolved from the [dependencies] entry whose key is byte-identical to the specifier, or from an inline source on the use. An undeclared coordinate or alias is an error.

A key under an open namespace is the dependency's own coordinate. lib is the one place an alias lives. An alias renames a dependency, shortens its name, tells two major versions apart, or names a dependency with no public coordinate. A lib: key names the coordinate it stands for with its package field (Registry Sources).

[dependencies]
"docs:regex" = { version = "^0.1.0" }                            # use { … } from "docs:regex"
"lib:rx" = { package = "docs:regex", version = "^0.1.0" }        # use { … } from "lib:rx"
"lib:shared" = { path = "../shared" }                            # use { … } from "lib:shared"
"router" = { path = "../router" }                                # warning: bare name (deprecated)

A bare key ("router") is deprecated and draws a warning. It is the one bare name a use may name.

Inline Sources

An inline dependency source lets a single-file script name a dependency with no wado.toml:

use { Regexp } from "docs:regex@1.0.0" with { registry: "oci://ghcr.io/acme" };  // exact pin via the specifier
use { Router } from "lib:router" with { git: "https://github.com/user/router.git", ref: "v1.0" };
use { Parse } from "lib:rx" with { registry: "oci://ghcr.io/acme", package: "docs:regex", version: "1.0.0" };

An inline source takes the same keys as a [dependencies] value: git, ref, directory, registry, package, path, and an exact version. An inline source and a wado.toml entry for the same specifier are mutually exclusive.

Registries

[registries] gives registry URLs short names. A registry source picks one with its registry field, and default is the one a source without the field uses.

[registries]
default = "oci://ghcr.io"
acme = "oci://registry.acme.example/wado"

A registry is an OCI registry, written oci://<host>[/<prefix>]. The package namespace:name at version x.y.z is the image <host>/<prefix>/<namespace>/<name> tagged x.y.z. With the default above, wado-lang:cm-catalog version 0.1.0 is ghcr.io/wado-lang/cm-catalog:0.1.0. That image holds the library world. Each other world a package publishes is its own image, one path segment further down, named after the world with : and / written as -: ghcr.io/wado-lang/gale/core-kiln-generator:0.0.9.

A registry package is a prebuilt component. Its specifier imports the interfaces the component exports, as a use of a component file does (Components). The package's export items sit in its default interface, so they import by bare name, as from a source dependency. A pub item the component does not export does not reach the consumer. Whether a registry package will also carry its pub items to a Wado consumer is undecided (WEP: Provider Metadata):

use { println, Stdout } from "core:cli";
use { id_list_u8, id_string, id_u32 } from "wado-lang:cm-catalog";
let n = id_u32(42);
let s = id_string("hello");

The Library World

A package offers other packages its library world (What is a World?). The world's entry module is the file [package].lib names, and a specifier that names the package imports that module. A dependency without [package].lib cannot be imported, unless it is a single .wado file named by a path source, which is its own entry module.

These two files are a package geo, with lib = "src/lib.wado":

// geo/src/lib.wado - the file `[package].lib` names
pub use { Square } from "./shapes.wado";

pub fn area(s: Square) -> i32 { return s.side * s.side; }

pub fn map_sides<T>(squares: List<Square>, f: fn(i32) -> T) -> List<T> {
    return squares.into_iter().map(|s| f(s.side)).collect();
}

export fn unit_side() -> i32 { return 1; }

internal fn scale() -> i32 { return 2; }

fn margin() -> i32 { return 0; }

pub fn grown(s: Square) -> Square { return Square { side: s.side * scale() + margin() }; }
// geo/src/shapes.wado - a file the entry module re-exports from
pub struct Square { pub side: i32 }

pub fn perimeter(s: Square) -> i32 { return s.side * 4; }

What a Dependency Offers

A package that depends on geo may import the entry module's pub and export items, and the names it re-exports with pub use (Visibility):

// app/src/main.wado - `"lib:geo" = { path = "../geo" }` in app/wado.toml
use { Square, area, grown, map_sides, unit_side } from "lib:geo";

test {
    let s = Square { side: 3 };
    assert area(s) == 9 && grown(s).side == 6;
    assert map_sides([s, Square { side: unit_side() }], |n| n * 10) == [30, 10];
}

An internal item stays inside geo, and a private one inside its file. Naming either from another package is a compile error:

use { scale } from "lib:geo";   // error: `internal` to the geo package
use { margin } from "lib:geo";   // error: private to geo's entry module

The specifier reaches the entry module and nothing else. A pub item of another file in the package reaches a consumer only when the entry module re-exports it. perimeter is pub in shapes.wado, but lib.wado does not re-export it:

use { Square, perimeter } from "lib:geo";   // error: `perimeter` is `pub` in a file the entry does not re-export

Source Dependencies

A path or git dependency is Wado source. It is compiled into the consuming program, and its pub items cross as Wado declarations. A pub signature may therefore take a closure, a generic or a trait, which no component interface can carry. map_sides above is one. So is the run_cli that package-gale-highlight-wado imports from its lib:gale path dependency:

use { highlight } from "./lib.wado";
use { run_cli } from "lib:gale";
export fn run() with (Stderr, Environment, Preopens, MonotonicClock) {
    run_cli(|src: &String| highlight(*src), args());
}

A registry dependency is a component, so a consumer reaches only what the component exports (Registries).

Whatever its source, a dependency's types and traits are foreign to the consumer when the orphan rule asks (Package Boundary).

One Package, One Set of Types

A package is compiled once into a program, however many keys name it. Two keys whose sources are the same package reach the same declarations, so a type imported through one is the type imported through the other:

// Two [dependencies] keys whose sources are the same package
use { area } from "lib:geo";
use { Square } from "acme:geo";

test {
    assert area(Square { side: 2 }) == 4;   // one package, so one `Square`
}

A consumer's [dependencies] do not reach into its dependencies. A use inside a dependency never resolves against them.

The Library World as a Component

Built on its own, the library world is a component. It exports one interface, the package's default interface, which carries every export item of the entry module and no pub-only item. Its functions never become world-level exports, whatever their signatures name. The interface is named after the package, <namespace>:<name>/<name>@<version>, so building it needs [package].namespace. The world itself is named root, so no package may take that name, in any letter case. An entry module that exports nothing gives the component no interface, and building it is an error.

The Lock File

wado.lock, beside wado.toml, records the exact version each registry and git dependency resolved to. A build uses the versions the lock file records. Committing it makes every build of the package use the same dependencies. This is the lock file of example/hello-packages:

# This file is auto-generated by wado. Do not edit manually.
version = 1
deps-hash = "sha256:370208cbcf9701ecfad219813fade7ae411ef3661b7696a2a276d38c8912304f"

[[package]]
id = "registry+oci://ghcr.io/wado-lang:cm-catalog"
version = "0.1.0"
integrity = "sha256:e477653fa5dd32e208697f5e07464bab0bfb466a32bf5f0ed84e20ce9aa77d45"
deps = []

[[build-dependency]]
id = "registry+oci://ghcr.io/wado-lang:gale"
version = "0.0.9"
integrity = "sha256:1a49e9adc93a919e40c40fc155d2162061ece67bcc85b371fb1336995dd84ba0"
world = { "core:kiln/generator" = "" }
deps = []

The header holds two fields:

Field Meaning
version The lock file format. It is 1, and any other value is an error.
deps-hash A hash of [dependencies], [dev-dependencies] and [build-dependencies], sha256:…

Each [[package]] entry is one resolved package, direct or transitive. Each [[build-dependency]] entry is one resolved generator, in the same form:

Field Present for Meaning
id every entry The package's source and identity
version every entry The exact version
resolved-ref a git package The commit SHA the version resolved to
integrity a registry package The digest of the downloaded package, as sha256:<hex>
dev a development package true when only [dev-dependencies] reach the package
world when it has any The package's [world] table, as an inline table
deps every entry The entries the package depends on, each as <id>@<version>

An id is registry+<registry URL>/<coordinate> for a registry package and git+<repository URL>/<key> for a git one. The same coordinate in two registries is two packages. Entries are sorted by id, then by version.

A path dependency has no entry.

Workspaces

A workspace is a set of packages developed together. Its root wado.toml has a [workspace] table, whose members lists the member directories as glob patterns:

[workspace]
members = ["packages/*"]

[workspace.package]
version = "1.2.0"
repository = "https://github.com/example/tools"
namespace = "example"
license = "MIT"
authors = ["Alice <alice@example.com>"]

[workspace.package] holds metadata every member inherits, with no marker in the member. A member inherits the fields in one of two ways:

Field Inheritance
version Forced: a member that sets the field is an error
repository Forced
namespace Forced
license A default the member may replace
license-file A default the member may replace; resolved from the root
authors A default the member may replace
wado-version A default the member may replace

Forcing the identity fields keeps every member on one version, from one repository, under one namespace. license and license-file are one setting: a member that sets either replaces both.

# package-grog/wado.toml, a member of the workspace above
[package]
name = "grog"
version = "0.1.0"   # error: [package].version is inherited from [workspace.package]; remove it from this member
lib = "src/lib.wado"

Any other [package] field belongs to the member. A key in [workspace.package] that is not one of the seven draws the unknown-key warning. The root may also have a [package] of its own, which inherits nothing.

Publishing

Publishing puts a package in a registry, under its coordinate. A package is publishable when all of these hold:

Each world a package declares is published as its own component (Registries). A [world] entry in table form keeps one world back:

[world]
"wasi:cli/command" = "src/main.wado"                                 # published
"core:kiln/generator" = "src/generator.wado"                         # published
"wasi:http/service" = { entry = "src/server.wado", publish = false } # not published

[package].publish = false keeps the whole package back, whatever its worlds say. A package with no publishable world has nothing to publish, which is an error.

Tools

The wado command edits and reads the manifest and the lock file. wado init writes a new manifest. wado update resolves the dependencies and writes wado.lock. wado fetch downloads what the lock file records. wado publish checks the rules in Publishing and uploads each publishable world.

Rationale: WEP: Package Manifest and WEP: Package and Module Specifier Syntax.