RFD 0038 — The prelude, ambient scope, and symbol-precise stdlib loading
- Status: discussion
- Depends on: RFD 0009 (MLT-as-library / nothing-privileged), RFD 0030 (path dependencies)
- Blocks: RFD 0037 D8 (relation-property re-home needs a real prelude to surface library attribute-macros without ceremony)
Summary
Argon’s prelude is specified but unbuilt: the reference designs a full
Rust-like prelude (§3.4 four-tier resolution; §15 std::prelude::v1 with an
auto-import set + #![no_implicit_prelude] opt-out), but the compiler implements
none of it — three ad-hoc mechanisms stand in for it, it diverges from the
reference, and the reference contradicts itself on the most basic question:
whether type/rel are ambient. This RFD settles the ambient-scope
architecture:
- Tier 0 — substrate. A fixed, hardcoded, non-opt-outable set: the names that are the language, not library declarations.
- Tier 1 — the prelude. A real, configurable, auto-imported (opt-out)
prelude that carries no ontology by default — no
type/rel. The prelude is a package feature (ox), not a compiler feature: there is no compiler-default prelude. - Tier 2 — explicit
use, with stdlib and dependencies loaded symbol-precisely (⊥-locality extraction) from the transitive use-graph — only what the program actually uses, never whole packages.
The frame is the existing oxc = rustc, ox = cargo split (the binaries
already encode it: oxc is “the language compiler … pure source-to-artifact
passes”; ox is “the project CLI”). oxc compiles loose files with substrate +
explicit uses and no prelude; ox is the package tool where the manifest
supplies the prelude, dependencies, tier, and world. We copy Rust’s split and
manifest model but deliberately do not copy rustc’s rich auto-applied
std::prelude — Rust can privilege std because it is a universal library;
Argon structurally cannot privilege any vocabulary (RFD 0009). So the governing
principle is: nothing from the standard library is privileged into ambient
scope — not categorizes, and not the no-commitment baseline type/rel.
Ontological commitment is always explicit and visible at the top of a module.
This resolves the reference-manual contradiction in favor of its own better half
(§4 / §15.0.1) and deletes the “auto-injected ambient type/rel” claims
(§5.2 / §15).
Background — the validated problem
Ground truth, verified against the compiler (compiler/crates/) and by
ox check:
- The specified prelude is unbuilt. The reference fully specifies a four-tier
resolution (local →
use→ auto-preludestd::prelude::v1::*→ primordials,03-modules.md§3.4) with#![no_implicit_prelude]opt-out, and §15 even enumerates a rich Rust-like auto-prelude (Option/Result/Ordering/traits/ macros). But the compiler builds none of tier 3: nostd::prelude::v1, no#. The prelude exists on paper, not in code. - The substrate/prelude boundary (clarification). Of §15’s auto-prelude list,
the items that actually resolve today —
Option/Result/Ordering/List/Set/Map/Range/Truth4/Diagnostic/Severity— are Tier-0 substrate (hardcodedis_builtin_type_form+ the check surface), not a library prelude. So “empty default prelude” (D2) never strips these — a package always has them. The empty default applies to the ontology/library layer (type/rel/ vocabulary). The not-yet-shipped traits/macros (Display/format!/…) are deferred: substrate-vs-prelude is decided when they land (lean: language macros and core operator-traits are substrate). - All seven stdlib packages load unconditionally.
STDLIB_SOURCES(oxc-instantiate/src/lower.rs) is a fixed seven-element list;load_stdlibiterates it with no dependency graph, no opt-in, no opt-out. Every artifact carriesstd::mlt/std::kripke/std::fin/… axioms whether used or not. - Ambient leak in classifier position (demonstrated). Type references
require
use(pub rel R(World, World)with no import →OE0101, test T1). But metatype/metarel classifiers resolve local → ambient pin → workspace-unique (resolve_metatype_introducer/resolve_metarel_introducer,lower.rs:1171/1209); because every std package is loaded into the workspace, the workspace-unique arm makes every loaded metatype/metarel an ambient classifier —pub categorizes AB(A, B)checks clean with nouse(test T2). This directly violates RFD 0009 (“MLT is an explicitly-imported library, nothing privileged”) and §5.2. type/relare hardcoded pins.if classifier == "type" { return std::core::type }(lower.rs:1182;:1220forrel) — special-cased, not resolved through any inclusion mechanism.Top/Bothave two sources of truth. Defined both as primordials (resolve.rs primordial_kind) and aspub types instd/core/root.ar.- The reference contradicts itself. §15.0.1: “No vocabulary ships with the
language or the stdlib … external vocabularies may re-export
std::core::relin their own prelude as a convenience”; §4: “afteruse std::core::rel;the modeler can writepub rel …” — both explicit-inclusion. Yet §5.2 / §15: “std::coreis auto-injected …pub type Fooworks with nouse.” These cannot both hold.
Infrastructure that already exists and we build on: ox.toml package manifests
with [package]/[dependencies] (RFD 0030); inner-attribute parsing
(#![...] → ATTR_INNER CST, validated against DIRECTIVE_REGISTRY); the
prelude.ar / pub use pkg::prelude::* convention (§3). Infrastructure that does
not exist: selective stdlib loading, a transitive use-graph (only
mod-chain reachability and per-file import maps exist), and any prelude-control
directive.
Decision
D0 — oxc is the compiler, ox is the package tool (rustc / cargo)
The two binaries (crates/oxc-driver/src/bin/) already encode the split:
oxc(rustc): compiles loose source files — “the pure source-to-artifact passes.” A loose file is not a package, so it has no prelude: only the Tier-0 substrate is in scope, and everything else is an explicituse. A bareoxc foo.arwithpub type Person {}and no import is an error (OE0605, unresolved metatypetype) with a hint touse std::core::{type, rel}.ox(cargo): the package tool. It requires anox.toml, and the manifest supplies the prelude ([package].prelude, D4), dependencies, tier ceiling, and world.oxinvokes the sameoxcpasses with that manifest context — exactly as cargo drives rustc.
Consequence: the prelude is a package feature, not a compiler default. The
current standalone path single_file_workspace_with_stdlib (oxc-driver), which
injects all seven stdlib packages into any loose file, is deleted — it is the
auto-load-everything behavior this RFD removes. Loose-file compilation keeps
working under oxc with substrate + explicit uses + symbol-precise extraction
(D3); there is no separate “standalone mode” with divergent rules.
D1 — Tier 0: the substrate is fixed and is not the library
The always-in-scope, non-opt-outable set is exactly the language primitives — the carriers of the meta-calculus and type system, which have no library declaration form:
- Primordials:
Nat, Int, Real, Decimal, Money, Date, Time, DateTime, Duration, Bool, String, Top, Bot(and⊤/⊥). - Builtin type forms:
List, Set, Map, Range, Option, Result, Ordering, Truth4, Truth4Of, Path, Metatype, TypeRef, Entity. - Reflection intrinsics:
meta, iof, specializes, extent, implements, implementors. - Aggregate heads:
count, count_distinct, sum, min, max, avg, exists. - Modal operators, operators,
true/false, the check surface (Diagnostic/Severity).
These stay hardcoded (resolved before imports), and the set is closed: adding
to it is a language change, not a library change. Top/Bot are primordials
(Tier 0); the duplicate pub type Top/Bot in std/core are removed (D5).
D2 — Tier 1: ONE prelude, carrying no ontology by default
There is exactly one auto-import mechanism. Two distinct roles were being conflated under the word “prelude” — separating them is what unsticks this (both exist in Rust too):
- The auto-import set (role 1 — the prelude): what every module of a
package gets without writing
use. Configured byox.toml[package].prelude(D4), default empty, opt-out per-module via#![no_implicit_prelude]. This is the one thing called “the prelude,” and it is a package concept. - A package’s exported public module (role 2): a normal module conventionally
named
preludethatpub uses the package’s public surface; consumers opt in explicitly withuse pkg::prelude::*, or feed it into their own role-1 config. No special mechanism — just a module. The §3prelude.arconvention is this. It is not a competing auto-prelude.
The earlier confusion (std::prelude::v1 and std::core::prelude both acting
as global auto-preludes) is gone: there is one role-1 prelude per package, empty
by default — the extension point, not a dumping ground.
type/rel are opt-in, with no special prelude for them. A package that
wants the baseline auto-available in its own modules lists it in role 1 —
[package].prelude = ["std::core::{type, rel}"]. A module (or a loose oxc file)
that wants it without a package prelude writes use std::core::{type, rel}
directly. A foundational-ontology package sets [package].prelude = ["std::ufo::prelude::*"] and never sees type/rel. The hardcoded pins
(lower.rs:1182/1220) are removed; type/rel resolve through the prelude /
imports like every other classifier.
A future #[cfg(...)]-style mechanism (Argon has none today) could
conditionally configure the prelude — a clean home for “this build wants the
core baseline” — but the manifest config covers the need now without a new
language feature; cfg is noted as a later generalization, not a v1 dependency.
Rationale. The no-commitment baseline is itself a commitment — to neutrality. Defaulting it privileges that choice and forces every committed package to opt out of a commitment it never made (backwards). Making it opt-in (a) keeps the language neutral (RFD 0009), (b) makes every module’s metatype basis visible at its top, and (c) unifies resolution: classifiers and references both resolve local → imports/prelude → substrate, eliminating the T1/T2 asymmetry.
D3 — Tier 2: explicit use, symbol-precise extraction (not whole-package)
Loading is symbol-precise, not package-granular. use A::b::{c, D} brings in
c and D and the transitive closure of declarations they depend on
(endpoint types, supertypes, referenced relations, the symbols their bodies
mention) — and nothing else from A::b. Pulling in all of A::b::* because one
symbol was named is a hollow approximation and is rejected: the artifact must
contain only what the program actually uses.
This is exactly ⊥-locality module extraction, which the substrate already
mechanizes and proves conservative (spec/lean/Argon/Locality/; AGENTS.md §3.5 —
“Σ-scoped CWA-conservativity, domain-conservative extraction for ghost
individuals, chained extraction across import chains”). The compiler does not
implement it today (all stdlib loads whole); D3 wires the existing, proven theory
into the build:
- Build the transitive use-graph — a new pass over
useedges plus the dependency edges between declarations — seeded from the entry module. - Mint events for exactly the reachable declaration closure, across stdlib and
path dependencies alike.
use std::fin::{MonetaryAmount}⇒MonetaryAmountand its dependency closure, not theCurrency/RoundingModesurface it never references; nouseofstd::fin⇒ zerostd::finaxioms. - The metatype/metarel introducer’s workspace-unique arm sees only the extracted/in-scope metatypes (imported or in the prelude) — never every loaded package. This is what closes the T2 leak.
Conservativity is a composition of two properties (validated by reading the
whole spec/lean/Argon/ tree), both now Lean-proven for their core:
- Ontology/concept layer — ⊥-locality module extraction → Σ-scoped
CWA-conservativity (concept entailment): proven
(
Locality/ScopedConservativity.lean,DomainConservative,ChainedCwa). - Datalog/derived layer — a program slice (the rules in the
dependency-closure of the used predicates) preserves the least-fixpoint on the
closure predicates, i.e. derived extents + checks: proven for positive Datalog
(
Scratch/DatalogSliceRelevance.lean,agree_on_closure, axiom-free — Lean obligation L1, discharged). The existingLocality/mechanization did not cover this (Seminaive.leanproves the module-id column is harmless — a different result). The extension to stratified negation + aggregates is L2 (below), gated meanwhile by the differential test.
The differential and determinism tests below guard the implementation; L1 discharges the positive-Datalog core of the derived-layer proof gap.
D4 — Prelude configuration: manifest default + per-module opt-out
Two controls, both honored:
-
ox.toml[package].prelude— an array ofuse-tails, each parsed exactly as if writtenuse <entry>;and auto-prepended to every module of the package. Reusing the realuseparser gives single / brace-list / glob / alias forms for free, with identical resolution and identical participation in the D3 use-graph (a prelude entry is an implicituse). Default empty. An entry that fails to parse or resolve is a hard error (explicit author intent), not an OW1240 warning (OW1240 stays for unknown keys).[package] prelude = [ "std::core::{type, rel}", # opt into the no-commitment baseline "std::ufo::prelude::*", # or glob a foundational ontology's public prelude ]Large preludes: point at your own prelude module. For a non-trivial prelude, the cleanest shape is a one-line config that globs the package’s own
preludemodule, with the actual re-exports written as real Argon code:[package] prelude = ["pkg::prelude::*"]// prelude.ar pub use std::core::{type, rel}; pub use std::ufo::prelude::*; // … grows here, commentable, reviewableValidated against the resolver:
pkg::is the package self-anchor (resolve.rs:680, and it anchors at that package’s root even as a dependency,:681-684); a globuse pkg::prelude::*pulls in the module’s public surface including itspub usere-exports, which resolve transitively and cycle-guarded (resolve.rs:410-416,:452-453). This is role-1 (auto-import) pointing at role-2 (the exported module) — one mechanism, composed. Caveat (new): the prelude module must be exempt from auto-prelude injection into itself (elseuse pkg::prelude::*is prepended topkg::prelude); the resolver’s cycle-guard catches the loop, but exempting the prelude module is cleaner. -
#![no_implicit_prelude]— a per-module override. Add it toDIRECTIVE_REGISTRY(module position,ATTR_INNER); expose inner attributes on theSourceFileAST (todayargon.ungrammarhas onlyitems:Item*); thread the flag into prelude injection. A module with the flag gets Tier 0 only — no auto-prelude — and mustuseeverything else explicitly.
D5 — Resolve the spec contradiction; dedupe Top/Bot
The reference is amended to a single consistent rule: nothing from the stdlib is
ambient; type/rel require use std::core::{type, rel} (or a prelude that
re-exports them). Delete the §5.2 / §15 “auto-injected ambient type/rel”
clauses; keep §4 / §15.0.1. Remove the pub type Top/Bot declarations from
std/core/root.ar (Top/Bot are Tier 0 primordials).
Migration
This is a breaking change for any code relying on the leak or on the deleted standalone auto-load. Accepted (the corpus is ours).
- Delete
single_file_workspace_with_stdlib(oxc-driver). Loose-file compilation runs underoxcwith substrate + explicituses + extraction. - A corpus migration pass: every module that declares
pub type/pub rel, or uses a stdlib classifier without importing it, gains the rightuse(std::core::{type, rel},std::mlt::*, …) — or, if it is a package, the entry in[package].prelude. - Examples and single-file tests either run under
oxcwith an explicituse std::core::{type, rel}, or gain a one-lineox.toml(prelude = ["std::core::{type, rel}"]); the test harness (write_temp_source) writes whichever it needs. - The
OE0605/OE0606unresolved-introducer diagnostics gain a hint suggesting the likelyuse(e.g. “did you meanuse std::core::{type, rel}?”). - The stdlib packages themselves declare their own dependencies via
use(e.g.std::mltusesstd::core).
Acceptance tests
- Leak closed:
pub categorizes AB(A, B)with nouse→OE0606(unresolved metarel introducer), notok. (Inverts test T2.) - Baseline opt-in:
pub type Foo {}with nouseand#![no_implicit_prelude]→OE0605; withuse std::core::{type}→ok. - Symbol-precise extraction:
use std::fin::{MonetaryAmount}yields an artifact withMonetaryAmountand its dependency closure but not thestd::findeclarations it never references; nouseofstd::fin⇒ zerostd::finevents. Assert over the event stream. - No regression for committed vocabularies:
use std::ufo::prelude::*; pub kind Person {}checks clean with nostd::corein scope. - Determinism preserved: byte-identical
.oxbinacross builds (S5) holds under extraction (the extracted set is a deterministic function of the use-graph). - Extraction is conservative: for any program, the answers (derived extents,
check firings, tiers) under symbol-precise extraction equal those under
load-everything — spot-checked differentially over the corpus. The DL/concept
half is the Lean-proven ⊥-locality property (
Locality/); the positive-Datalog half is L1 (Scratch/DatalogSliceRelevance.lean, proven, axiom-free); the stratified-negation/aggregate cases (L2) rest on the differential test until mechanized.
Lean obligations
- L1 — positive Datalog slice-relevance: DISCHARGED. Proven in
spec/lean/Scratch/DatalogSliceRelevance.lean(theoremagree_on_closure): for a ground positive program, the slice (rules whose head predicate is in the dependency-closure of the used predicate setQ) and the full program agree, at every fixpoint iteration — hence at the lfp — on every atom whose predicate is in the closure ofQ. So loading only the use-graph closure yields the same derived extents over the used predicates. Self-contained (no Mathlib), nosorry, and#print axiomsreports it depends on no axioms at all (fully constructive) — the agreement is proven by induction on the fixpoint chain, not assumed ([[no-axiomatizing-the-conclusion]]). This is D3’s derived-layer conservativity for the monotone core, composed with the ⊥-locality result below. - L2 — stratified negation + aggregates (remaining). L1’s
Tis monotone (positive Datalog). Argon’s reasoning is stratified well-founded with negation and aggregates; the relevance result extends in the standard way, but a faithful proof must model the strata, so L1 does not by itself cover the non-positive cases. Until L2 is mechanized, the differential test (#6) gates those. (Per AGENTS.md the Rust reasoner leads the Lean on the reasoning layer.) - Already discharged (ontology layer): ⊥-locality → Σ-scoped
CWA-conservativity (
Locality/ScopedConservativity.lean), domain-conservative extraction (DomainConservative), chained across imports (ChainedCwa).
Spec reconciliation (reference edits)
05-constructs.md§5.2,15-stdlib.md§15/§15.0.1: delete “auto-injected ambienttype/rel”; state the explicit-inclusion rule and the prelude tiers.03-modules.md§3.4: make the resolution order match D1–D3 (substrate is the floor, not auto-prelude-then-primordials); document#![no_implicit_prelude].appendix-a-reserved-keywords.md:type/relare not reserved ambient names.
Resolved (this RFD)
oxc= rustc,ox= cargo. The prelude is a package feature; there is no compiler-default prelude. Looseoxcfiles use substrate + explicituses.single_file_workspace_with_stdlibis deleted.- One auto-import mechanism (role 1), distinct from a package’s exported
preludemodule (role 2). Nostd::prelude::v1, nostd::core::prelude. [package].prelude= an array ofuse-tails, default empty; per-module opt-out#![no_implicit_prelude]. Bad entry = hard error.type/relopt-in via[package].preludeor explicituse— never privileged ambient.- Loading is symbol-precise (⊥-locality extraction), never whole-package.
Open questions
- Extraction staging. Symbol-precise ⊥-locality extraction is substantial.
It may land in stages, but the hollow package-level version must not ship —
if an interim step is needed it must still be sound (over-approximate only in
ways that never change answers, and
log/document any conservatism). Sequence the use-graph pass and the extraction pass against the existing Lean theory. #[cfg(...)]as the eventual conditional-prelude mechanism — out of scope for this RFD, noted as the natural later home for build-conditional baselines.