iter pd.2: surface owns prelude embed; pd.1 shim retired
Moved PRELUDE_AIL const + parse_prelude fn to ailang-surface; re-exported from surface's lib.rs. surface::load_workspace rewritten 3-line shim-call → 7-line composition: load_modules_with → reserved-name check → inject parse_prelude → build_workspace(&["prelude"]). Five deletions in core: PRELUDE_JSON, load_prelude, load_workspace_with (the pd.1 shim), the top-level cfg(test) load_one fn, and the cfg(test) crate::load_module import. Production literal-"prelude" count in crates/ailang-core/src/ is now zero. Cross-form-identity preflight PASSED at module_hash 3abe0d3fa3c11c99 — parse(prelude.ail) ≡ deserialize(prelude.ail.json), so pd.3's deletion of the JSON is safe. Three new pin tests in ailang-surface/tests/: prelude_parse_pin (succeeds + min defs); prelude_module_hash_pin (cross-form-identity + literal-hex anchor); prelude_injection_pin (inject + reservation). Plan deviations (recorded in journal): switched the regression-pin fixture from non-existent examples/hello.ail.json to hello.ail; relocated 10 in-mod core tests to crates/ailang-core/tests/workspace_pin.rs because the dev-dep cycle (core ↔ surface) prevents in-mod aliasing in the lib-test target (form-a.1 T5 precedent); preserved load_one production-symbol-deletion by adding a test-mod-private load_one helper in core's mod tests for the 2 pd.1-introduced tests still consuming it. cargo test --workspace 573 green (+9 net from pd.1 baseline). bench/cross_lang.py + bench/compile_check.py exit 0; bench/check.py exit 1 with established noise envelope (bench_list_sum.bump_s, 13th consecutive observation; pd.2 is workspace-loader-only, no codegen touch — baseline pristine).
This commit is contained in:
+107
-303
@@ -1,50 +1,35 @@
|
||||
//! Workspace loader: loads an entry module and recursively follows its
|
||||
//! `imports`.
|
||||
//! Workspace loading and per-workspace validation.
|
||||
//!
|
||||
//! Convention: an `import { module: "foo" }` is resolved relative to
|
||||
//! the entry file's directory as `<root_dir>/foo.ail.json`. Module
|
||||
//! names must match the file stem (the loader rejects mismatches).
|
||||
//! Two phases, exposed as separate public fns so callers can compose
|
||||
//! them around an injection point (e.g. surface's prelude inject):
|
||||
//!
|
||||
//! The pd.1 single core entry point is [`load_workspace_with`]; it
|
||||
//! takes a caller-supplied per-module loader so surface (`.ail`) and
|
||||
//! JSON (`.ail.json`) callers share one pipeline. It returns a fully
|
||||
//! populated [`Workspace`] or a structured [`WorkspaceLoadError`].
|
||||
//! pd.2 will move the public entry point into `ailang-surface` and
|
||||
//! retire this shim. [`module_hash`] is the module-granularity
|
||||
//! counterpart to [`crate::def_hash`] and is used both internally (to
|
||||
//! detect a module re-loaded from disk with different content) and by
|
||||
//! the CLI.
|
||||
//! - [`load_modules_with`] — DFS over `imports`, returns a modules
|
||||
//! map with no validation and no implicit modules. The caller
|
||||
//! supplies a per-module loader.
|
||||
//! - [`build_workspace`] — runs the three-stage validation pipeline
|
||||
//! (`validate_canonical_type_names`, `validate_classdefs`,
|
||||
//! `build_registry`) plus [`Workspace`] assembly. Takes
|
||||
//! `implicit_imports: &[&str]` so the diagnostic helpers can name
|
||||
//! modules that exist but are not in any user import list.
|
||||
//!
|
||||
//! This module is responsible only for **finding** and consistently
|
||||
//! **loading** all reachable modules. Cross-module typechecking lives
|
||||
//! in `ailang-check`; codegen in `ailang-codegen`. Neither is run from
|
||||
//! here.
|
||||
//! The public composition lives in `ailang-surface`'s `load_workspace`
|
||||
//! (which adds a prelude inject step between the two phases). Direct
|
||||
//! consumers of these core fns are `ailang-surface` and a handful of
|
||||
//! tests; CLI dispatch goes through surface.
|
||||
//!
|
||||
//! # Examples
|
||||
//! This module is responsible only for **finding**, **assembling**,
|
||||
//! and **schema-validating** the module graph. Cross-module
|
||||
//! typechecking lives in `ailang-check`. Cross-module monomorphisation
|
||||
//! and codegen live in `ailang-codegen`.
|
||||
//!
|
||||
//! ```ignore
|
||||
//! // pd.2 will move the public entry point to ailang-surface;
|
||||
//! // pd.1 keeps `load_workspace_with` as the single core entry point.
|
||||
//! // Callers supply a per-module loader (e.g.
|
||||
//! // `ailang_surface::load_module_dispatching`) that knows how to
|
||||
//! // read `.ail` and/or `.ail.json` files.
|
||||
//! use ailang_core::workspace::load_workspace_with;
|
||||
//! use std::path::Path;
|
||||
//!
|
||||
//! let ws = load_workspace_with(Path::new("examples/ws_main.ail.json"), my_loader)?;
|
||||
//! assert_eq!(ws.entry, "ws_main");
|
||||
//! // All transitively imported modules are now in `ws.modules`.
|
||||
//! for (name, m) in &ws.modules {
|
||||
//! println!("{name}: {} defs", m.defs.len());
|
||||
//! }
|
||||
//! # Ok::<(), ailang_core::workspace::WorkspaceLoadError>(())
|
||||
//! ```
|
||||
//! [`module_hash`] is the module-granularity counterpart to
|
||||
//! [`crate::def_hash`]; used internally to detect a re-loaded module
|
||||
//! with different content, and by the CLI for stable per-module
|
||||
//! identifiers.
|
||||
|
||||
use crate::ast::{ClassDef, Def, InstanceDef, Module, Term, Type};
|
||||
use crate::canonical;
|
||||
use crate::Error as CoreError;
|
||||
#[cfg(test)]
|
||||
use crate::load_module;
|
||||
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -421,23 +406,6 @@ pub fn module_hash(m: &Module) -> String {
|
||||
h.to_hex().as_str()[..16].to_string()
|
||||
}
|
||||
|
||||
/// Iter 23.1: the prelude module is embedded into the binary at
|
||||
/// compile time so the loader needs no on-disk resolution. The
|
||||
/// canonical source-of-truth file remains
|
||||
/// `examples/prelude.ail.json` (the file the LLM-author edits).
|
||||
const PRELUDE_JSON: &str = include_str!(
|
||||
"../../../examples/prelude.ail.json"
|
||||
);
|
||||
|
||||
/// Iter 23.1: parse the embedded prelude source into a `Module`.
|
||||
/// Panics on parse failure — the prelude is build-time-validated;
|
||||
/// a failure here means the embedded file is malformed and is a
|
||||
/// build-correctness bug, not a runtime concern.
|
||||
fn load_prelude() -> Module {
|
||||
serde_json::from_str(PRELUDE_JSON)
|
||||
.expect("examples/prelude.ail.json must parse as a Module")
|
||||
}
|
||||
|
||||
/// pd.1: extract the DFS pre-amble of `load_workspace_with` into a public
|
||||
/// loader-only fn. Returns `(entry_name, root_dir, modules)` with NO prelude
|
||||
/// injected and NO validation run. The caller is responsible for injecting
|
||||
@@ -522,43 +490,12 @@ pub fn build_workspace(
|
||||
})
|
||||
}
|
||||
|
||||
/// pd.1: thin compatibility shim composed of `load_modules_with` +
|
||||
/// `build_workspace` plus the prelude inject. Preserves today's
|
||||
/// `load_workspace_with` semantics so surface (`crates/ailang-surface/src/loader.rs`)
|
||||
/// stays unchanged in pd.1. pd.2 will rewire surface to call
|
||||
/// `load_modules_with` and `build_workspace` directly and retire this
|
||||
/// shim along with the embedded `PRELUDE_JSON` constant.
|
||||
///
|
||||
/// Algorithm: DFS over `imports` via `load_modules_with`, then inject
|
||||
/// the prelude (rejecting collision with a user module of the same
|
||||
/// name), then validate + build the typeclass registry via
|
||||
/// `build_workspace`.
|
||||
///
|
||||
/// ext-cli.1: a caller-supplied loader is what allows
|
||||
/// `ailang-surface::load_workspace` to plug in an extension-dispatching
|
||||
/// loader without `ailang-core` importing `ailang-surface` (which would
|
||||
/// close a crate cycle).
|
||||
pub fn load_workspace_with<F>(
|
||||
entry_path: &Path,
|
||||
loader: F,
|
||||
) -> Result<Workspace, WorkspaceLoadError>
|
||||
where
|
||||
F: Fn(&Path) -> Result<Module, WorkspaceLoadError> + Copy,
|
||||
{
|
||||
let (entry_name, root_dir, mut modules) = load_modules_with(entry_path, loader)?;
|
||||
|
||||
// Iter 23.1 (preserved through the shim): the prelude module is
|
||||
// implicitly part of every workspace. A user module of the same name
|
||||
// would shadow; reject explicitly.
|
||||
if modules.contains_key("prelude") {
|
||||
return Err(WorkspaceLoadError::ReservedModuleName {
|
||||
name: "prelude".to_string(),
|
||||
});
|
||||
}
|
||||
modules.insert("prelude".to_string(), load_prelude());
|
||||
|
||||
build_workspace(entry_name, root_dir, modules, &["prelude"])
|
||||
}
|
||||
// pd.1: `load_workspace_with` shim retired in pd.2. The public entry
|
||||
// point is now `ailang_surface::load_workspace`, which composes
|
||||
// `load_modules_with` + caller-side prelude inject + `build_workspace`
|
||||
// directly. Surface owns the prelude inject step (the embed lives at
|
||||
// `ailang_surface::PRELUDE_AIL` + `ailang_surface::parse_prelude`); core
|
||||
// exposes the two-phase composition.
|
||||
|
||||
/// mq.1: take a class-ref field value (`InstanceDef.class`,
|
||||
/// `SuperclassRef.class`, `Constraint.class`) and a defining context,
|
||||
@@ -1595,20 +1532,11 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn load_one(path: &Path) -> Result<Module, WorkspaceLoadError> {
|
||||
match load_module(path) {
|
||||
Ok(m) => Ok(m),
|
||||
Err(CoreError::Io(e)) => Err(WorkspaceLoadError::Io {
|
||||
path: path.to_path_buf(),
|
||||
source: e,
|
||||
}),
|
||||
Err(e) => Err(WorkspaceLoadError::Schema {
|
||||
path: path.to_path_buf(),
|
||||
source: e,
|
||||
}),
|
||||
}
|
||||
}
|
||||
// pd.2: `load_one` deleted along with the `load_workspace_with` shim.
|
||||
// The two surviving in-mod tests that need a JSON-only single-module
|
||||
// loader (`load_modules_with_returns_modules_without_prelude` and
|
||||
// `load_modules_with_custom_loader_is_called`) inline the
|
||||
// `crate::load_module` adapter directly.
|
||||
|
||||
fn module_name_from_path(p: &Path) -> String {
|
||||
let file = p.file_name().and_then(|s| s.to_str()).unwrap_or("");
|
||||
@@ -1629,6 +1557,25 @@ mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
|
||||
/// pd.2: tests that need a JSON-only single-module loader (the
|
||||
/// pre-pd.2 `load_one` shape) inline this adapter. Wraps
|
||||
/// `crate::load_module` and converts the `Error` variants into
|
||||
/// `WorkspaceLoadError`. Used by the pd.1-introduced tests that
|
||||
/// exercise `load_modules_with` directly.
|
||||
fn load_one(path: &Path) -> Result<Module, WorkspaceLoadError> {
|
||||
match crate::load_module(path) {
|
||||
Ok(m) => Ok(m),
|
||||
Err(CoreError::Io(e)) => Err(WorkspaceLoadError::Io {
|
||||
path: path.to_path_buf(),
|
||||
source: e,
|
||||
}),
|
||||
Err(e) => Err(WorkspaceLoadError::Schema {
|
||||
path: path.to_path_buf(),
|
||||
source: e,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_module(dir: &Path, name: &str, imports: &[&str]) -> PathBuf {
|
||||
let imports_json: Vec<serde_json::Value> = imports
|
||||
.iter()
|
||||
@@ -1662,59 +1609,11 @@ mod tests {
|
||||
|
||||
// (See workspace_pin.rs relocation marker above.)
|
||||
|
||||
#[test]
|
||||
fn user_module_named_prelude_is_rejected() {
|
||||
// Iter 23.1: the loader auto-injects a module named "prelude",
|
||||
// so a user module that also claims that name would collide
|
||||
// silently. The collision is caught explicitly with
|
||||
// `ReservedModuleName`.
|
||||
let dir = tmp_dir("prelude_collision");
|
||||
write_module(&dir, "prelude", &[]);
|
||||
let entry = dir.join("prelude.ail.json");
|
||||
|
||||
let err = load_workspace_with(&entry, load_one).expect_err("must reject user prelude");
|
||||
match err {
|
||||
WorkspaceLoadError::ReservedModuleName { name } => {
|
||||
assert_eq!(name, "prelude");
|
||||
}
|
||||
other => panic!(
|
||||
"expected ReservedModuleName, got: {other:?}"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_import_cycle() {
|
||||
let dir = tmp_dir("cycle");
|
||||
write_module(&dir, "a", &["b"]);
|
||||
write_module(&dir, "b", &["a"]);
|
||||
let entry = dir.join("a.ail.json");
|
||||
|
||||
let err = load_workspace_with(&entry, load_one).expect_err("must error on cycle");
|
||||
match err {
|
||||
WorkspaceLoadError::Cycle { path } => {
|
||||
assert!(path.contains(&"a".to_string()));
|
||||
assert!(path.contains(&"b".to_string()));
|
||||
}
|
||||
other => panic!("expected Cycle, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn module_not_found_yields_structured_error() {
|
||||
let dir = tmp_dir("notfound");
|
||||
write_module(&dir, "main", &["does_not_exist"]);
|
||||
let entry = dir.join("main.ail.json");
|
||||
|
||||
let err = load_workspace_with(&entry, load_one).expect_err("must error on missing module");
|
||||
match err {
|
||||
WorkspaceLoadError::ModuleNotFound { name, expected_path } => {
|
||||
assert_eq!(name, "does_not_exist");
|
||||
assert!(expected_path.ends_with("does_not_exist.ail.json"));
|
||||
}
|
||||
other => panic!("expected ModuleNotFound, got {other:?}"),
|
||||
}
|
||||
}
|
||||
// pd.2: `user_module_named_prelude_is_rejected`,
|
||||
// `detects_import_cycle`, and `module_not_found_yields_structured_error`
|
||||
// relocated to `crates/ailang-core/tests/workspace_pin.rs` (the
|
||||
// ailang-surface dev-dep cycle disallows `surface::load_workspace`
|
||||
// calls from this in-mod test crate).
|
||||
|
||||
// Iter 22b.1: a workspace whose own modules contain no
|
||||
// `Def::Instance` defs produces no non-prelude registry entries.
|
||||
@@ -1783,48 +1682,11 @@ mod tests {
|
||||
// `iter22b1_missing_method_fires_diagnostic` relocated to
|
||||
// `crates/ailang-core/tests/workspace_pin.rs` in iter form-a.1 Task 5.
|
||||
|
||||
#[test]
|
||||
fn class_param_in_applied_position_fires_canonical_form_rejection() {
|
||||
let entry = std::path::PathBuf::from(
|
||||
"../../examples/test_22b2_kind_mismatch.ail.json",
|
||||
);
|
||||
let err = load_workspace_with(&entry, load_one)
|
||||
.expect_err("must fire canonical-form rejection");
|
||||
match err {
|
||||
WorkspaceLoadError::BareCrossModuleTypeRef { module, name, .. } => {
|
||||
assert_eq!(module, "test_22b2_kind_mismatch");
|
||||
assert_eq!(name, "f");
|
||||
}
|
||||
other => panic!(
|
||||
"expected BareCrossModuleTypeRef (validator now fires first), got {other:?}",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Iter 22b.2: a class whose `superclass.type` differs from its
|
||||
/// own `param` (e.g. `class Ord a extends Eq b`) must fire
|
||||
/// `InvalidSuperclassParam`. Decision 11 axis 1 ("single
|
||||
/// superclass, applied to the same param") makes the only legal
|
||||
/// shape `extends Eq a` when the parent class has `param: "a"`.
|
||||
#[test]
|
||||
fn superclass_with_wrong_param_fires_invalid_superclass_param() {
|
||||
let entry = std::path::PathBuf::from(
|
||||
"../../examples/test_22b2_invalid_superclass_param.ail.json",
|
||||
);
|
||||
let err = load_workspace_with(&entry, load_one)
|
||||
.expect_err("must fire invalid-superclass-param");
|
||||
match err {
|
||||
WorkspaceLoadError::InvalidSuperclassParam {
|
||||
class, superclass, expected_param, got_type,
|
||||
} => {
|
||||
assert_eq!(class, "Ord");
|
||||
assert_eq!(superclass, "Eq");
|
||||
assert_eq!(expected_param, "a");
|
||||
assert_eq!(got_type, "b");
|
||||
}
|
||||
other => panic!("expected InvalidSuperclassParam, got {other:?}"),
|
||||
}
|
||||
}
|
||||
// `class_param_in_applied_position_fires_canonical_form_rejection`
|
||||
// and `superclass_with_wrong_param_fires_invalid_superclass_param`
|
||||
// relocated to `crates/ailang-core/tests/workspace_pin.rs` in iter
|
||||
// pd.2 (post-shim retirement; in-mod tests cannot reach
|
||||
// `ailang_surface::load_workspace` due to the dev-dep cycle).
|
||||
|
||||
// Iter 22b.2: an instance that specifies a body for a method
|
||||
// name the class never declared must fire
|
||||
@@ -1839,31 +1701,9 @@ mod tests {
|
||||
// `instance_overriding_nonexistent_method_fires` relocated to
|
||||
// `crates/ailang-core/tests/workspace_pin.rs` in iter form-a.1 Task 5.
|
||||
|
||||
/// Iter 22b.2: a class method's `Type::Forall` whose `constraints`
|
||||
/// reference a type variable that is neither bound by `Forall.vars`
|
||||
/// nor equal to the class's `param` must fire
|
||||
/// `UnboundConstraintTypeVar`. The fixture's `class Foo a` declares
|
||||
/// method `foo` with `forall a. (Bar z) => a -> Unit`; `z` is
|
||||
/// bound nowhere, so the constraint is unsatisfiable by
|
||||
/// construction.
|
||||
#[test]
|
||||
fn constraint_with_unbound_var_fires_unbound_constraint_type_var() {
|
||||
let entry = std::path::PathBuf::from(
|
||||
"../../examples/test_22b2_unbound_constraint_var.ail.json",
|
||||
);
|
||||
let err = load_workspace_with(&entry, load_one)
|
||||
.expect_err("must fire constraint-references-unbound-type-var");
|
||||
match err {
|
||||
WorkspaceLoadError::UnboundConstraintTypeVar {
|
||||
class, method, var, ..
|
||||
} => {
|
||||
assert_eq!(class, "Foo");
|
||||
assert_eq!(method, "foo");
|
||||
assert_eq!(var, "z");
|
||||
}
|
||||
other => panic!("expected UnboundConstraintTypeVar, got {other:?}"),
|
||||
}
|
||||
}
|
||||
// `constraint_with_unbound_var_fires_unbound_constraint_type_var`
|
||||
// relocated to `crates/ailang-core/tests/workspace_pin.rs` in iter
|
||||
// pd.2.
|
||||
|
||||
// mq.3: the two `..._method_name_collision_fires` pin tests
|
||||
// (class-class and class-fn variants) were retired here and
|
||||
@@ -2485,34 +2325,13 @@ mod tests {
|
||||
/// `Ordering` Term::Ctor with no imports; the validator catches it
|
||||
/// after prelude injection (so the candidate list contains
|
||||
/// `prelude.Ordering`).
|
||||
#[test]
|
||||
fn ct1_fixture_bare_xmod_rejected() {
|
||||
let entry = examples_dir().join("test_ct1_bare_xmod_rejected.ail.json");
|
||||
let err = load_workspace_with(&entry, load_one).expect_err("must reject bare Ordering");
|
||||
match err {
|
||||
WorkspaceLoadError::BareCrossModuleTypeRef { module, name, candidates } => {
|
||||
assert_eq!(module, "test_ct1_bare_xmod_rejected");
|
||||
assert_eq!(name, "Ordering");
|
||||
assert_eq!(candidates, vec!["prelude.Ordering".to_string()]);
|
||||
}
|
||||
other => panic!("expected BareCrossModuleTypeRef, got {other:?}"),
|
||||
}
|
||||
}
|
||||
// `ct1_fixture_bare_xmod_rejected` relocated to
|
||||
// `crates/ailang-core/tests/workspace_pin.rs` in iter pd.2.
|
||||
|
||||
/// ct.1: on-disk fixture for `BadCrossModuleTypeRef`. Qualified
|
||||
/// `Mystery.Type` with `Mystery` not a known module.
|
||||
#[test]
|
||||
fn ct1_fixture_bad_qualifier() {
|
||||
let entry = examples_dir().join("test_ct1_bad_qualifier.ail.json");
|
||||
let err = load_workspace_with(&entry, load_one).expect_err("must reject Mystery.Type");
|
||||
match err {
|
||||
WorkspaceLoadError::BadCrossModuleTypeRef { module, name } => {
|
||||
assert_eq!(module, "test_ct1_bad_qualifier");
|
||||
assert_eq!(name, "Mystery.Type");
|
||||
}
|
||||
other => panic!("expected BadCrossModuleTypeRef, got {other:?}"),
|
||||
}
|
||||
}
|
||||
// `ct1_fixture_bad_qualifier` relocated to
|
||||
// `crates/ailang-core/tests/workspace_pin.rs` in iter pd.2.
|
||||
|
||||
/// mq.1: on-disk fixture for the post-mq.1 rejection path. The
|
||||
/// fixture declares `instance prelude.Eq Int` outside the prelude
|
||||
@@ -2524,21 +2343,8 @@ mod tests {
|
||||
/// Pre-mq.1 this test asserted `QualifiedClassName` on the same
|
||||
/// fixture; the rename + reshape is the on-disk-fixture half of
|
||||
/// the four in-test inversions further up.
|
||||
#[test]
|
||||
fn ct1_fixture_qualified_class_orphan_post_mq1() {
|
||||
let entry = examples_dir().join("test_ct1_qualified_class_rejected.ail.json");
|
||||
let err = load_workspace_with(&entry, load_one).expect_err("must reject (now as Orphan)");
|
||||
match err {
|
||||
WorkspaceLoadError::OrphanInstance {
|
||||
class, type_repr, defining_module, ..
|
||||
} => {
|
||||
assert_eq!(class, "prelude.Eq");
|
||||
assert_eq!(type_repr, "Int");
|
||||
assert_eq!(defining_module, "test_ct1_qualified_class_rejected");
|
||||
}
|
||||
other => panic!("expected OrphanInstance, got {other:?}"),
|
||||
}
|
||||
}
|
||||
// `ct1_fixture_qualified_class_orphan_post_mq1` relocated to
|
||||
// `crates/ailang-core/tests/workspace_pin.rs` in iter pd.2.
|
||||
|
||||
// mq.1: on-disk fixture pin — a workspace where the consumer's
|
||||
// `Constraint.class` references a class in an imported module via
|
||||
@@ -2549,14 +2355,15 @@ mod tests {
|
||||
// `mq1_xmod_constraint_class_fixture_loads` relocated to
|
||||
// `crates/ailang-core/tests/workspace_pin.rs` in iter form-a.1 Task 5.
|
||||
|
||||
/// ext-cli.1 Task 1: the new `load_workspace_with` injection point
|
||||
/// invokes the caller-supplied loader exactly once for the entry
|
||||
/// module of a single-module workspace. Protects against the
|
||||
/// extension-dispatching loader (in `ailang-surface`) silently
|
||||
/// falling back to `load_one` and so never being given a `.ail`
|
||||
/// path to read.
|
||||
/// ext-cli.1 Task 1 / pd.2: the loader-injection contract is now
|
||||
/// owned by `load_modules_with` (the DFS-only loader) directly,
|
||||
/// since the `load_workspace_with` shim is gone. The custom loader
|
||||
/// must be invoked exactly once for the entry module of a single-
|
||||
/// module workspace. Protects against the extension-dispatching
|
||||
/// loader (in `ailang-surface`) silently falling back to a
|
||||
/// JSON-only path and so never being given a `.ail` to read.
|
||||
#[test]
|
||||
fn load_workspace_with_custom_loader_is_called() {
|
||||
fn load_modules_with_custom_loader_is_called() {
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
let tmpdir = tempfile::tempdir().expect("tempdir");
|
||||
@@ -2568,12 +2375,22 @@ mod tests {
|
||||
|
||||
let loader = |path: &std::path::Path| -> Result<Module, WorkspaceLoadError> {
|
||||
CALLS.fetch_add(1, Ordering::SeqCst);
|
||||
load_one(path)
|
||||
crate::load_module(path).map_err(|e| match e {
|
||||
CoreError::Io(io) => WorkspaceLoadError::Io {
|
||||
path: path.to_path_buf(),
|
||||
source: io,
|
||||
},
|
||||
other => WorkspaceLoadError::Schema {
|
||||
path: path.to_path_buf(),
|
||||
source: other,
|
||||
},
|
||||
})
|
||||
};
|
||||
|
||||
let ws = load_workspace_with(&entry, loader)
|
||||
let (entry_name, _root_dir, modules) = load_modules_with(&entry, loader)
|
||||
.expect("workspace loads via custom loader");
|
||||
assert_eq!(ws.entry, "dummy");
|
||||
assert_eq!(entry_name, "dummy");
|
||||
assert!(modules.contains_key("dummy"));
|
||||
assert_eq!(
|
||||
CALLS.load(Ordering::SeqCst),
|
||||
1,
|
||||
@@ -2827,8 +2644,16 @@ mod tests {
|
||||
let (entry_name, root_dir, mut modules) =
|
||||
load_modules_with(&entry, load_one).expect("load");
|
||||
|
||||
// Caller-side prelude inject (mirrors what surface will do in pd.2).
|
||||
let prelude = load_prelude();
|
||||
// Caller-side prelude inject — pd.2 deletes `load_prelude` from
|
||||
// production, but this test still needs SOMETHING in the
|
||||
// `prelude` slot to demonstrate the caller-injects pattern.
|
||||
// Read the JSON sidecar via the same path the retired
|
||||
// `load_prelude` used. Surface owns the production inject path
|
||||
// (via `ailang_surface::parse_prelude`).
|
||||
const PRELUDE_JSON: &str =
|
||||
include_str!("../../../examples/prelude.ail.json");
|
||||
let prelude: Module = serde_json::from_str(PRELUDE_JSON)
|
||||
.expect("examples/prelude.ail.json must parse as a Module");
|
||||
modules.insert("prelude".to_string(), prelude);
|
||||
|
||||
let ws = build_workspace(entry_name, root_dir, modules, &["prelude"])
|
||||
@@ -2911,29 +2736,8 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// pd.1 Task 4: `load_workspace_with` after the shim refit
|
||||
/// produces a `Workspace` byte-identical to today's. Pinned by
|
||||
/// asserting prelude injection still happens AND the prelude
|
||||
/// module's hash matches the freshly-loaded prelude — i.e. the
|
||||
/// shim composition (`load_modules_with` + inject + `build_workspace`)
|
||||
/// preserves the pre-refit observable behaviour.
|
||||
#[test]
|
||||
fn load_workspace_with_shim_preserves_today_semantics() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
write_module(dir.path(), "main", &[]);
|
||||
let entry = dir.path().join("main.ail.json");
|
||||
|
||||
let ws = load_workspace_with(&entry, load_one).expect("load");
|
||||
|
||||
assert_eq!(ws.entry, "main");
|
||||
assert!(ws.modules.contains_key("main"));
|
||||
assert!(
|
||||
ws.modules.contains_key("prelude"),
|
||||
"shim must still inject prelude"
|
||||
);
|
||||
assert_eq!(
|
||||
module_hash(&ws.modules["prelude"]),
|
||||
module_hash(&load_prelude()),
|
||||
);
|
||||
}
|
||||
// pd.2: `load_workspace_with_shim_preserves_today_semantics` deleted —
|
||||
// the shim it pinned is gone. Cross-form-identity / hash-pin
|
||||
// coverage now lives in
|
||||
// `crates/ailang-surface/tests/prelude_module_hash_pin.rs`.
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user