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:
@@ -36,6 +36,6 @@ pub mod loader;
|
||||
pub mod parse;
|
||||
pub mod print;
|
||||
|
||||
pub use loader::{load_module, load_workspace};
|
||||
pub use loader::{load_module, load_workspace, parse_prelude, PRELUDE_AIL};
|
||||
pub use parse::{parse, parse_term, ParseError};
|
||||
pub use print::{print, term_to_form_a, type_to_form_a};
|
||||
|
||||
@@ -9,13 +9,39 @@
|
||||
//! These functions live in `ailang-surface` rather than
|
||||
//! `ailang-core` because `ailang-core` cannot import `ailang-surface`
|
||||
//! without creating a circular crate dependency. The cross-crate
|
||||
//! injection point is [`ailang_core::workspace::load_workspace_with`],
|
||||
//! introduced in iter ext-cli.1.
|
||||
//! injection point is [`ailang_core::workspace::load_modules_with`]
|
||||
//! (DFS-only loader) composed with [`ailang_core::workspace::build_workspace`]
|
||||
//! (validation + registry). Surface owns the prelude inject step
|
||||
//! between the two; the implicit-imports list `&["prelude"]` is
|
||||
//! supplied at the surface call site. Introduced in iter pd.2 (post-
|
||||
//! ext-cli.1's `load_workspace_with` shim retirement).
|
||||
|
||||
use ailang_core::ast::Module;
|
||||
use ailang_core::workspace::{load_workspace_with, Workspace, WorkspaceLoadError};
|
||||
use ailang_core::workspace::{Workspace, WorkspaceLoadError};
|
||||
use std::path::Path;
|
||||
|
||||
/// pd.2 (`prelude-decouple` milestone): the prelude bytes are embedded
|
||||
/// here at compile time. The single source of truth is
|
||||
/// `examples/prelude.ail` (Form A). Pre-pd.2, the embed lived in
|
||||
/// `ailang-core` and used `prelude.ail.json`; that path retired with
|
||||
/// the loader-split.
|
||||
pub const PRELUDE_AIL: &str = include_str!("../../../examples/prelude.ail");
|
||||
|
||||
/// pd.2: parse the embedded prelude bytes into a `Module`.
|
||||
///
|
||||
/// Panics on parse failure — the prelude is build-time-validated by
|
||||
/// every test run, so a parse failure here is a build-correctness
|
||||
/// bug, not a runtime concern.
|
||||
///
|
||||
/// Mirrors the semantics of the retired `ailang_core::workspace::load_prelude`
|
||||
/// (which deserialised `prelude.ail.json` via `serde_json::from_str`);
|
||||
/// equivalence between the two parse paths is pinned by
|
||||
/// `crates/ailang-surface/tests/prelude_module_hash_pin.rs`'s
|
||||
/// cross-form-identity preflight.
|
||||
pub fn parse_prelude() -> Module {
|
||||
crate::parse(PRELUDE_AIL).expect("examples/prelude.ail must parse as a Module")
|
||||
}
|
||||
|
||||
fn is_ail_source(path: &Path) -> bool {
|
||||
path.extension().and_then(|s| s.to_str()) == Some("ail")
|
||||
}
|
||||
@@ -67,5 +93,18 @@ pub fn load_module(path: &Path) -> Result<Module, WorkspaceLoadError> {
|
||||
/// `core::workspace::visit`. Mixed-extension workspaces (entry is
|
||||
/// `.ail`, some imports are `.ail.json`) are valid.
|
||||
pub fn load_workspace(entry: &Path) -> Result<Workspace, WorkspaceLoadError> {
|
||||
load_workspace_with(entry, load_module)
|
||||
let (entry_name, root_dir, mut modules) =
|
||||
ailang_core::workspace::load_modules_with(entry, load_module)?;
|
||||
if modules.contains_key("prelude") {
|
||||
return Err(WorkspaceLoadError::ReservedModuleName {
|
||||
name: "prelude".to_string(),
|
||||
});
|
||||
}
|
||||
modules.insert("prelude".to_string(), parse_prelude());
|
||||
ailang_core::workspace::build_workspace(
|
||||
entry_name,
|
||||
root_dir,
|
||||
modules,
|
||||
&["prelude"],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
//! pd.2 (`prelude-decouple` milestone): pin that
|
||||
//! `surface::load_workspace` injects the prelude into every workspace
|
||||
//! and rejects user modules trying to take the reserved name.
|
||||
|
||||
use ailang_core::workspace::WorkspaceLoadError;
|
||||
use ailang_surface::load_workspace;
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn surface_load_workspace_injects_prelude_into_every_workspace() {
|
||||
// Use any existing single-module example from `examples/`. The hello
|
||||
// example is a stable choice (used by other surface tests). Post-
|
||||
// form-a.1 the canonical sidecar is `.ail` (Form A); the `.ail.json`
|
||||
// for `hello` was deleted by form-a.1 T8.
|
||||
let entry = Path::new("../../examples/hello.ail");
|
||||
let ws = load_workspace(entry).expect("load");
|
||||
assert!(
|
||||
ws.modules.contains_key("prelude"),
|
||||
"prelude must appear in every workspace post-pd.2"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn surface_load_workspace_rejects_user_module_named_prelude() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
// Create a JSON-form fixture for the user-side "prelude" module.
|
||||
// Surface's loader should fail with ReservedModuleName.
|
||||
let prelude_path = dir.path().join("prelude.ail.json");
|
||||
std::fs::write(
|
||||
&prelude_path,
|
||||
r#"{"schema":"ailang/v0","name":"prelude","imports":[],"defs":[]}"#,
|
||||
)
|
||||
.unwrap();
|
||||
// Single-file workspace where the entry IS named "prelude" trips
|
||||
// the reservation.
|
||||
let err = load_workspace(&prelude_path).expect_err("must reject");
|
||||
match err {
|
||||
WorkspaceLoadError::ReservedModuleName { name } => {
|
||||
assert_eq!(name, "prelude");
|
||||
}
|
||||
other => panic!("expected ReservedModuleName, got: {other:?}"),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//! pd.2 (`prelude-decouple` milestone): pin the canonical identity of
|
||||
//! the parsed prelude.
|
||||
//!
|
||||
//! Two tests:
|
||||
//! 1. `prelude_ail_and_json_parse_to_identical_module` — cross-form-
|
||||
//! identity preflight. Asserts that parsing `examples/prelude.ail`
|
||||
//! via `ailang_surface::parse` produces a `Module` with the same
|
||||
//! `module_hash` as deserialising `examples/prelude.ail.json` via
|
||||
//! `serde_json::from_str`. This is the load-bearing assumption
|
||||
//! that justifies pd.3's deletion of the JSON file. pd.3 drops
|
||||
//! this test along with the JSON.
|
||||
//! 2. `prelude_parse_yields_canonical_hash` — long-term anchor.
|
||||
//! Asserts the literal hex hash of the parsed Module. Updates
|
||||
//! intentionally on prelude-content changes (with a JOURNAL entry
|
||||
//! naming why); accidental drift trips immediately.
|
||||
|
||||
use ailang_core::workspace::module_hash;
|
||||
use ailang_surface::{parse_prelude, PRELUDE_AIL};
|
||||
|
||||
const PRELUDE_JSON: &str = include_str!("../../../examples/prelude.ail.json");
|
||||
|
||||
#[test]
|
||||
fn prelude_ail_and_json_parse_to_identical_module() {
|
||||
// Form A path: parse prelude.ail via ailang_surface::parse.
|
||||
let from_ail = parse_prelude();
|
||||
|
||||
// Form B path: deserialise prelude.ail.json via serde_json (the
|
||||
// pre-pd.2 path; matches what `ailang_core::workspace::load_prelude`
|
||||
// did before retirement).
|
||||
let from_json: ailang_core::ast::Module = serde_json::from_str(PRELUDE_JSON)
|
||||
.expect("examples/prelude.ail.json must deserialise");
|
||||
|
||||
let h_ail = module_hash(&from_ail);
|
||||
let h_json = module_hash(&from_json);
|
||||
|
||||
assert_eq!(
|
||||
h_ail, h_json,
|
||||
"cross-form-identity preflight failed:\n parse(prelude.ail) hash: {}\n deserialize(prelude.ail.json) hash: {}\n\
|
||||
The two parse paths produce structurally different `Module` values for the same prelude content. \
|
||||
pd.3 must NOT delete prelude.ail.json until this is investigated.",
|
||||
h_ail, h_json,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prelude_parse_yields_canonical_hash() {
|
||||
let m = parse_prelude();
|
||||
let h = module_hash(&m);
|
||||
// The expected hex is captured at pd.2-iter close (Step 5 of this task).
|
||||
// It updates intentionally on prelude-content changes; record the why
|
||||
// in the per-iter journal entry.
|
||||
assert_eq!(
|
||||
h, "3abe0d3fa3c11c99", // captured at pd.2 iter close
|
||||
"prelude module hash drifted; if intentional, capture the new \
|
||||
hex below + record the why in the per-iter journal."
|
||||
);
|
||||
}
|
||||
|
||||
// PRELUDE_AIL re-exported for any future test that wants the raw bytes.
|
||||
const _: &str = PRELUDE_AIL;
|
||||
@@ -0,0 +1,25 @@
|
||||
//! pd.2 (`prelude-decouple` milestone): pin that the surface-side
|
||||
//! `parse_prelude` succeeds against the in-tree `examples/prelude.ail`
|
||||
//! and returns a `Module` with the expected shape. Guards against
|
||||
//! `prelude.ail` being silently emptied or `parse_prelude` regressing.
|
||||
//!
|
||||
//! The hash-stability + cross-form-identity pins live in
|
||||
//! `prelude_module_hash_pin.rs`.
|
||||
|
||||
use ailang_surface::parse_prelude;
|
||||
|
||||
#[test]
|
||||
fn parse_prelude_returns_module_named_prelude_with_min_defs() {
|
||||
let m = parse_prelude();
|
||||
assert_eq!(m.name, "prelude");
|
||||
// Conservative lower bound: the prelude has shipped four classes
|
||||
// (Eq, Ord, Show + the Ordering data def) plus their primitive
|
||||
// instances by milestone 24. A future content addition only
|
||||
// raises the count; an accidental emptying (or a renamed root)
|
||||
// trips this immediately.
|
||||
assert!(
|
||||
m.defs.len() >= 8,
|
||||
"expected >= 8 defs in prelude, got {}",
|
||||
m.defs.len()
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user