Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

RFD 0030 — Package dependencies ([dependencies], path deps v1)

  • State: committed
  • Opened: 2026-06-12
  • Decides: how an Argon package declares a dependency on another package and consumes its pub surface — the mechanism that makes vocabulary publishing real. A vocabulary team ships a package (e.g. argufo); a modeler in a separate package declares [dependencies] argufo = { path = "../argufo" }, writes use argufo::kind;, and uses argufo’s pub metatype kind as a declaration keyword in its own catalog. Built as the Wave-A arc7-packaging work of the v0.2.1 program; closes register blocker R-B1 and majors R-M1 (use-import diagnostics) and R-M12 (manifest honesty). Builds D1 of RFD 0022 (which reserved “a dependency package name ([dependencies])” as a resolution root) and the §3.3/§3.4 manifest/resolution surface. Relates to Modules and Build.

This RFD records, as built, the dependency mechanism the book described aspirationally (§3.3 [dependencies], §3.4 dependency-package roots, §16 the ox orchestrator’s package graph). v1 is path dependencies only; the registry/version/git surface is recognized and refused loudly, reserved for a later RFD.


Question

Until now ox.toml parsed only [package]/[project]/[schema], and the resolver registered only the embedded stdlib (std::*) plus intra-package paths. The two-package vocabulary journey — the central use case for an ontology-modeling language — was impossible: every vocabulary and every model that used it had to live as sibling modules in one package. The book documented the opposite in detail, “as if real.”

  1. How does a package name and resolve a dependency? What [dependencies] shape, what resolution semantics, what happens across the package boundary for use, qualified paths, and — the acceptance test — dependency-provided introducers (pub metatype / pub metarel)?
  2. What about the registry/version surface the book showed? ufo = "1.0" is a version requirement against a registry that does not exist. Accept it silently (the old failure mode), or refuse it?
  3. What is honest about an ox.toml the compiler does not fully consume — unknown sections, [lattice].max_tier?

Context

The substrate already composes a multi-file package into one artifact: the driver’s pre-elaboration sweep builds a WorkspaceSymbols table over every reachable module, the §3.4 introducer gate (resolve_metatype_introducer/resolve_metarel_introducer) resolves a declaration keyword against the pub metatype/pub metarel declarations visible in the workspace, and the resolver (oxc-resolver) resolves use/qualified paths over the workspace’s file set. The embedded stdlib is folded into that same file set via synthetic <stdlib>/std/<pkg>/root.ar paths (oxc_instantiate::stdlib_source_files). A dependency package is the same shape as the stdlib: a set of pub-surfaced modules folded into the consumer’s workspace.

So the mechanism is not a new subsystem. It is: parse the dependency, load its modules, give them a package-qualified namespace, fold them into the workspace exactly like the stdlib, and teach the resolver to treat the dependency package name as a path root. Everything downstream — the introducer gate, use, qualified paths, module extraction, the .oxbin — composes for free, because all of those already operate over the workspace.


Decision

D1 — Manifest: [dependencies], path deps only

[dependencies]
argufo = { path = "../argufo" }
  • The key is the dependency name the consumer uses as the path root (argufo::Person, use argufo::kind;). It must equal the dependency package’s own [package].name — a mismatch is refused with OE1241, naming both the declared key and the found package name. (A rename-on-import surface is a possible later extension; v1 keeps the package’s published name authoritative, as Cargo does by default.)
  • The only supported source is path = "<relative path to a package directory>". The path is resolved relative to the consumer package directory; it must point at a directory containing an ox.toml.
  • A shorthand string requirement (argufo = "1.0") or any of the registry/VCS keys (version, git, branch, tag, rev, registry) is recognized and refused with OE1240, naming the feature (“registry/version dependencies are not yet supported; cite RFD 0030”) rather than silently ignored. This is the loud-refusal discipline: a documented input is never silently dropped.

D2 — Resolution: a dependency is a package-qualified namespace folded into the workspace

For each declared path dependency, the workspace loader:

  1. Loads the dependency package (its own ox.toml + entry + reachable module closure), exactly as it loads the consumer package.
  2. Re-homes the dependency’s modules under synthetic VFS paths <dep>/<name>/… and assigns each a package-qualified module path rooted at the dependency name: the dependency’s root module becomes module argufo, its submodule endurant becomes argufo::endurant, and so on.
  3. Folds the dependency’s source files into the consumer’s Workspace file set and its modules into the package’s reachable-module list — the same fold the stdlib already gets.

The resolver (oxc-resolver) recognizes a leading path segment equal to a registered dependency name as a root (§3.4): argufo::Person and use argufo::kind; resolve into the dependency’s <dep>/argufo/… namespace, the way std::core::rel resolves into <stdlib>/std/core/…. A dependency root is detected from the synthetic-path pattern — a leading segment argufo is a dependency root iff the dependency’s root file <dep>/argufo/root.ar is in the workspace file set — so no separate dependency-name registry is threaded onto the Workspace input (the same way the stdlib is recognized by <stdlib>/std/<pkg>/root.ar). Inside a dependency, pkg:: anchors at that dependency’s root, so a multi-file dependency’s internal package-absolute paths resolve within its own subtree. Only the dependency’s pub (and pub use-re-exported) surface is visible to the consumer; module visibility rules (§3.1) are unchanged across the boundary.

Because the dependency’s pub metatype/pub metarel declarations are in the workspace symbol table, the §3.4 introducer gate resolves them transparently: after use argufo::kind;, a consumer declaration pub kind Person { … } resolves kind to argufo::kind and elaborates as a concept under that metatype — the acceptance property. A dependency-shipped catalog check (a pub check over iof/meta/specializes) runs on the consumer’s catalog at the consumer’s ox check, because the dependency’s rules and the consumer’s individuals share one elaborated event stream. That is what makes a vocabulary package real, and it is tested (see Consequences).

Transitive dependencies resolve recursively (a dependency’s own [dependencies] are loaded into the same workspace). Cycles are refused loudly with OE1242, naming the cycle path. Two declared dependencies resolving to the same canonical directory are one package (de-duplicated, not an error). The same dependency name resolving to different canonical directories is refused with OE1243, naming both paths.

D3 — Artifact: dependency modules embed into the consumer’s .oxbin (monolithic, v1)

Dependency modules are elaborated into the same event stream as the consumer’s modules and embed into the consumer’s single .oxbin, exactly as the stdlib embeds today (§16.3). v1 is one monolithic artifact per ox build; the per-package .oxc cache + workspace merge (§16.1) remains the reserved v0.2+ shape. Because the dependency’s elaborated content participates in the event stream, module/content hashes (§16.5) incorporate it: a change to a dependency changes the consumer’s artifact_hash.

D4 — No lockfile for path dependencies

Path dependencies are unlocked (Cargo precedent — a path dep is whatever is at that path now). There is no ox.lock for a path-only dependency graph. ox.lock is reserved here for the registry/git story (D1’s refused surface): a versioned/VCS dependency graph needs a lockfile to pin resolved versions; a path graph does not.

D5 — Use-import diagnostics (R-M1): loud, never silent

A use that does not resolve is refused loudly:

  • use a::b::C; where the path does not resolve emits OE0103 (UnresolvedUseImport) at the use itself, with a did-you-mean suggestion over the names visible in the target namespace (the dependency’s pub surface, a sibling module, an intra-package path). Previously a broken use was accepted clean and the only error was a misleading downstream OE0605 far from the cause.
  • A glob use pkg::*; / use argufo::*; resolves correctly (it already does, via the re-export-aware module_exports) — it brings in the target’s pub surface. A glob whose prefix does not resolve is the same OE0103 as a named import. A glob is never a silent no-op.

D6 — Manifest honesty (R-M12)

  • An unknown ox.toml section or key (anything outside the recognized [package]/[project]/[schema]/[dependencies]/[lattice]/[standpoints] surface, or an unrecognized key within them) emits a Cargo-style OW1240 (UnusedManifestKey) warning naming the section/key — it no longer parses with serde silently dropping it.
  • [lattice].max_tier is wired to the §10 tier classifier: it sets the artifact’s tier ceiling, and a declaration whose classified tier exceeds the ceiling is refused at ox check/ox build with OE1230 (TierCapExceeded, previously reserved). The ceiling string is validated against the seven-tier ladder names; an unknown tier name is OW1240.

Rationale

  • A dependency is the stdlib shape. The most robust, lowest-surface-area design reuses the fold-into-workspace machinery the stdlib already proves. The introducer gate, use resolution, qualified paths, module extraction, and the .oxbin all already operate over the workspace; a dependency that lives in the workspace inherits all of them with no new code path. This is why the acceptance property (imported introducers + imported checks fire on the consumer’s catalog) falls out rather than being special-cased.
  • Path-first, registry-refused. Path deps are the v1 vocabulary-authoring need (a Sharpe team’s packages live in one tree). A registry is a distribution-infrastructure project (provenance, resolution, lockfiles) orthogonal to the language. Recognizing-and-refusing the registry keys keeps the manifest forward-compatible and honest: the book can show the shape, and a user who writes it gets a feature-named refusal, not a silent no-op.
  • No lockfile for path deps matches Cargo and the “no ceremony you don’t need” instinct: a path graph is fully determined by the filesystem.
  • Loud manifest, loud imports. Both R-M1 and R-M12 are instances of the program’s silent-accept blocker class. A mistyped import or a typo’d manifest key that the compiler silently swallows is a trap; a warning/refusal that names the thing is debuggable.

Alternatives

  • A separate dependency resolver pass that produces per-package .oxc and a workspace merge (the full §16.1 orchestrator). Deferred: it is the right end state for incremental builds and a registry, but it is a large build-system project; folding into one workspace is correct for v1 and keeps the artifact monolithic (§16.3’s stated v0.x shape).
  • Allow version/git to parse and warn, resolving via path anyway. Rejected: a version requirement that silently resolves to a path is exactly the silent-wrong the program forbids — the user thinks they pinned a version.
  • Rename-on-import (argufo = { path = "…", package = "ufo_foundational" }). Deferred: a useful ergonomic, but v1 keeps the published [package].name authoritative (mismatch = OE1241), matching Cargo’s default.
  • A lockfile for path deps. Rejected (Cargo precedent): a path graph has nothing to lock.

Consequences

  • ox.toml now has a [dependencies] table and recognizes [lattice]/[standpoints]; unknown keys warn (OW1240). [lattice].max_tier is enforced (OE1230, no longer reserved).
  • No change to the Workspace salsa input: dependency roots are detected from the synthetic <dep>/<name>/root.ar path pattern in the file set (mirroring stdlib’s <stdlib>/std/<pkg>/…), so Workspace::new’s signature is unchanged.
  • New diagnostics (allocated in grammar.toml, generated via cargo xtask gen): OE0103 UnresolvedUseImport, OE0104 GlobImportUnsupported (reserved — glob currently resolves; held for a future feature-named refusal of an unsupported glob shape), OE1240 DependencyVersionUnsupported, OE1241 DependencyNameMismatch, OE1242 DependencyCycle, OE1243 DuplicateDependencyName, OE1244 DependencyPackageLoadFailed, OW1240 UnusedManifestKey. OE1230 TierCapExceeded is wired (was reserved).
  • The two-package journey is the living proof: examples/vocab_pkg_v0 (a UFO-shaped vocabulary package with pub metatype kind/category, a pub metarel, and a pub check catalog rule) and examples/vocab_consumer_v0 (a separate package, [dependencies] vocab_pkg_v0 = { path = ".." }, declaring pub kind Person via the dependency’s kind, with the dependency’s catalog check firing on the consumer’s catalog). Both are corpus-pinned and run in the examples harness; GO-journey (a) of the v0.2.1 release gate.

Open questions

  • When the registry/git story lands (its own RFD), ox.lock and the per-package .oxc + workspace merge (§16.1) become live; the path-dep fold described here stays as the unlocked fast path.
  • Should a future rename-on-import key (package = "…") be added once a registry makes the published name vs. local name distinction load-bearing?
  • Should pub(pkg) become a true visibility cut at the dependency boundary (RFD 0022 open question) — i.e. should a dependency’s pub(pkg) items be invisible to a consumer (today pub(pkg) == pub)? v1 keeps the RFD 0022 simplification.