iter pd.3: prelude.ail.json retired — milestone prelude-decouple closed
Deleted examples/prelude.ail.json (-559 lines). The cross-form-identity preflight test that ratified pd.3's load-bearing assumption at module hash 3abe0d3fa3c11c99 has discharged its purpose and was removed along with its supporting bytes; the long-term prelude_parse_yields_canonical_hash anchor stays. The ail-CLI migrate-bare-cross-module-refs subcommand's defensive include + lockstep skip-branch removed; the rewrite logic's prelude-fallback capability silently retired with the synthetic insert (known debt, doc-comment updated to reflect). One in-mod core test relocated to crates/ailang-core/tests/workspace_pin.rs (pd.2.4 dev-dep cycle precedent — in-mod call to ailang_surface still structurally impossible). carve_out_inventory.rs: 8 → 7 carve-outs, §C4(b) row dropped, header sentence updated. form-a-default-authoring.md §C4(b) gets a "RETIRED 2026-05-14 by milestone prelude-decouple" status marker; original historical text preserved. New prelude_decouple_carve_out_pin.rs asserts examples/prelude.ail.json does NOT exist. Milestone prelude-decouple closed: prelude exists on disk only as examples/prelude.ail; ailang-core embeds zero prelude bytes; CLAUDE.md "authors write .ail" doctrine holds without exception. cargo test --workspace 573 green; bench/cross_lang.py + bench/compile_check.py exit 0; bench/check.py exit 1 with the established noise envelope (14th observation of bench_list_sum.bump_s since audit-cma; pd.3 is filesystem + spec text + test deletion only, no codegen / runtime / typecheck path edited — baseline pristine). Folds in the orphan pd.2 INDEX entry that was left uncommitted in the working tree.
This commit is contained in:
+5
-21
@@ -231,9 +231,11 @@ enum Cmd {
|
||||
///
|
||||
/// Disambiguation: the first import (in declaration order) that
|
||||
/// owns a matching type wins. Ties are NOT diagnosed — the
|
||||
/// migration silently picks the first match. Prelude is consulted
|
||||
/// as an implicit last-resort fallback when no listed import owns
|
||||
/// the name.
|
||||
/// migration silently picks the first match. (The prelude
|
||||
/// last-resort fallback was retired in pd.3 alongside the
|
||||
/// `prelude.ail.json` embed; bare cross-module refs that resolve
|
||||
/// only via the prelude are now left bare and surfaced by the
|
||||
/// validator instead.)
|
||||
MigrateCanonicalTypes {
|
||||
/// Directory containing `*.ail.json` to migrate (typically
|
||||
/// `examples/`).
|
||||
@@ -469,20 +471,6 @@ fn main() -> Result<()> {
|
||||
modules.insert(m.name.clone(), (path, m));
|
||||
}
|
||||
|
||||
// Include the embedded prelude (so the implicit fallback
|
||||
// matches the loader's behaviour).
|
||||
let prelude_json: &str = include_str!(
|
||||
"../../../examples/prelude.ail.json"
|
||||
);
|
||||
let prelude: Module = serde_json::from_str(prelude_json)
|
||||
.expect("embedded prelude must parse");
|
||||
if !modules.contains_key("prelude") {
|
||||
modules.insert(
|
||||
"prelude".to_string(),
|
||||
(PathBuf::new(), prelude),
|
||||
);
|
||||
}
|
||||
|
||||
// Pre-pass: per-module local-types index.
|
||||
let mut local_types: BTreeMap<String, BTreeSet<String>> =
|
||||
BTreeMap::new();
|
||||
@@ -497,12 +485,8 @@ fn main() -> Result<()> {
|
||||
}
|
||||
|
||||
// Pass 2: rewrite each module's bare cross-module refs.
|
||||
// Skip the synthetic prelude entry (no path to write to).
|
||||
let mut rewritten = 0usize;
|
||||
for (mod_name, (path, m)) in modules.iter_mut() {
|
||||
if mod_name == "prelude" && path.as_os_str().is_empty() {
|
||||
continue;
|
||||
}
|
||||
let import_names: Vec<String> =
|
||||
m.imports.iter().map(|i| i.module.clone()).collect();
|
||||
let owning = mod_name.clone();
|
||||
|
||||
@@ -2630,39 +2630,12 @@ mod tests {
|
||||
assert!(modules.contains_key("main"));
|
||||
}
|
||||
|
||||
/// pd.1 Task 2: `build_workspace` accepts a pre-assembled modules
|
||||
/// map (with the caller having already injected any implicit
|
||||
/// modules) and runs the three-stage validation pipeline. The
|
||||
/// `implicit_imports` slice is what the diagnostic helpers consult
|
||||
/// for fallback candidate suggestions (Task 3 wires it through).
|
||||
#[test]
|
||||
fn build_workspace_accepts_assembled_modules_and_runs_validation() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
write_module(dir.path(), "main", &[]);
|
||||
let entry = dir.path().join("main.ail.json");
|
||||
|
||||
let (entry_name, root_dir, mut modules) =
|
||||
load_modules_with(&entry, load_one).expect("load");
|
||||
|
||||
// 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"])
|
||||
.expect("build");
|
||||
|
||||
assert_eq!(ws.entry, "main");
|
||||
assert!(ws.modules.contains_key("main"));
|
||||
assert!(ws.modules.contains_key("prelude"));
|
||||
}
|
||||
// Note: tests that need a real prelude module via
|
||||
// `ailang_surface::parse_prelude()` live in
|
||||
// `crates/ailang-core/tests/workspace_pin.rs` (integration crate)
|
||||
// because the dev-dep cycle (`ailang-core` -> `ailang-surface` ->
|
||||
// `ailang-core`) leaves the lib-test target with two distinct
|
||||
// `ailang-core` compilations whose `Module` types do not unify.
|
||||
|
||||
fn module_with_bare_classref(name: &str, class_ref: &str) -> Module {
|
||||
// A module containing a single Def::Instance whose `class` field is
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
//! Carve-out inventory check — §T4 of the form-a-default-authoring
|
||||
//! spec. Asserts that exactly the eight named carve-out files exist
|
||||
//! spec. Asserts that exactly the seven named carve-out files exist
|
||||
//! under `examples/*.ail.json` after milestone close. The list is
|
||||
//! hardcoded; any change is a deliberate, brainstorm-level decision.
|
||||
//!
|
||||
//! Eight carve-outs at iter form-a.1 close:
|
||||
//! Seven carve-outs post prelude-decouple (2026-05-14):
|
||||
//! - §C4 (a) subject-matter: 7 fixtures (canonical-form rejection / unbound / class-def rejection).
|
||||
//! - §C4 (b) compile-time-embed: 1 fixture (prelude.ail.json).
|
||||
//!
|
||||
//! When the prelude-embed-refactor milestone retires §C4 (b), this
|
||||
//! test's pass-count drops to seven and `EXPECTED` is re-published.
|
||||
//! - §C4 (b) compile-time-embed: retired 2026-05-14 by milestone
|
||||
//! prelude-decouple; the prelude is now embedded as `prelude.ail`
|
||||
//! in `ailang-surface` and parsed at compile time via
|
||||
//! `ailang_surface::parse_prelude`.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
@@ -21,8 +21,6 @@ const EXPECTED: &[&str] = &[
|
||||
"test_ct1_bad_qualifier.ail.json",
|
||||
"test_ct1_bare_xmod_rejected.ail.json",
|
||||
"test_ct1_qualified_class_rejected.ail.json",
|
||||
// §C4 (b) — compile-time-embed
|
||||
"prelude.ail.json",
|
||||
];
|
||||
|
||||
fn examples_dir() -> std::path::PathBuf {
|
||||
|
||||
@@ -21,6 +21,22 @@ use ailang_core::workspace::{Registry, RegistryEntry, WorkspaceLoadError};
|
||||
use ailang_surface::load_workspace;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
// Adapter so this integration crate can call `load_modules_with` with
|
||||
// the same Io/Schema mapping the in-mod test helper used.
|
||||
fn load_one_adapter(path: &Path) -> Result<ailang_core::ast::Module, WorkspaceLoadError> {
|
||||
match ailang_core::load_module(path) {
|
||||
Ok(m) => Ok(m),
|
||||
Err(ailang_core::Error::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 examples_dir() -> PathBuf {
|
||||
let manifest_dir = env!("CARGO_MANIFEST_DIR");
|
||||
Path::new(manifest_dir).parent().unwrap().parent().unwrap().join("examples")
|
||||
@@ -486,3 +502,42 @@ fn ct1_fixture_qualified_class_orphan_post_mq1() {
|
||||
other => panic!("expected OrphanInstance, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// pd.1 Task 2 + pd.3 Task 1 relocation: `build_workspace` accepts a
|
||||
/// pre-assembled modules map (with the caller having already injected
|
||||
/// any implicit modules) and runs the three-stage validation pipeline.
|
||||
/// The `implicit_imports` slice is what the diagnostic helpers consult
|
||||
/// for fallback candidate suggestions. Relocated from the in-mod
|
||||
/// `tests` of `ailang-core/src/workspace.rs` because the caller-side
|
||||
/// prelude inject now uses `ailang_surface::parse_prelude()`, which
|
||||
/// only typechecks across the dev-dep edge from the integration-test
|
||||
/// crate (the lib-test crate sees two distinct `ailang-core`
|
||||
/// compilations and fails to unify the `Module` types).
|
||||
#[test]
|
||||
fn build_workspace_accepts_assembled_modules_and_runs_validation() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
write_simple_module_json(dir.path(), "main", &[]);
|
||||
let entry = dir.path().join("main.ail.json");
|
||||
|
||||
let (entry_name, root_dir, mut modules) =
|
||||
ailang_core::workspace::load_modules_with(&entry, load_one_adapter)
|
||||
.expect("load");
|
||||
|
||||
// Caller-side prelude inject — surface owns the production inject
|
||||
// path; this test consumes it via the dev-dep edge to demonstrate
|
||||
// the caller-injects pattern with the same source of truth.
|
||||
let prelude = ailang_surface::parse_prelude();
|
||||
modules.insert("prelude".to_string(), prelude);
|
||||
|
||||
let ws = ailang_core::workspace::build_workspace(
|
||||
entry_name,
|
||||
root_dir,
|
||||
modules,
|
||||
&["prelude"],
|
||||
)
|
||||
.expect("build");
|
||||
|
||||
assert_eq!(ws.entry, "main");
|
||||
assert!(ws.modules.contains_key("main"));
|
||||
assert!(ws.modules.contains_key("prelude"));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
//! pd.3 (`prelude-decouple` milestone): pin that
|
||||
//! `examples/prelude.ail.json` does NOT exist on disk. Surfaces
|
||||
//! immediately if the file is recreated by accident or a future
|
||||
//! refactor reverts the embed-source swap.
|
||||
//!
|
||||
//! Companion to the `examples_ail_json_inventory_matches_carve_outs`
|
||||
//! test in `crates/ailang-core/tests/carve_out_inventory.rs`: that
|
||||
//! one asserts the carve-out list is exactly seven; this one
|
||||
//! asserts the eighth (retired) file specifically does not exist.
|
||||
//! Either test detects an accidental re-introduction; this one
|
||||
//! pinpoints the file by name in the error message.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn prelude_ail_json_does_not_exist_on_disk() {
|
||||
let path = Path::new("../../examples/prelude.ail.json");
|
||||
assert!(
|
||||
!path.exists(),
|
||||
"examples/prelude.ail.json must NOT exist after the prelude-\
|
||||
decouple milestone (closed 2026-05-14). The prelude is now \
|
||||
embedded as prelude.ail in ailang-surface. If you intentionally \
|
||||
re-introduced this file, you must also: (a) remove this pin, \
|
||||
(b) restore the §C4 (b) entry in carve_out_inventory.rs, \
|
||||
(c) revert the §C4 (b) status marker in \
|
||||
docs/specs/2026-05-13-form-a-default-authoring.md, and \
|
||||
(d) record the rationale in a per-iter journal."
|
||||
);
|
||||
}
|
||||
@@ -1,46 +1,17 @@
|
||||
//! pd.2 (`prelude-decouple` milestone): pin the canonical identity of
|
||||
//! the parsed prelude.
|
||||
//! pd.2/pd.3 (`prelude-decouple` milestone): pin the canonical
|
||||
//! identity of the parsed prelude as a literal hex hash. Updates
|
||||
//! intentionally on prelude-content changes (record the why in the
|
||||
//! per-iter journal); accidental drift trips immediately.
|
||||
//!
|
||||
//! 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.
|
||||
//! pd.2 also carried a `prelude_ail_and_json_parse_to_identical_module`
|
||||
//! cross-form-identity preflight that compared `parse(prelude.ail)`
|
||||
//! against `serde_json::from_str(prelude.ail.json)`. The preflight
|
||||
//! discharged its purpose at pd.2 close (PASSED at hash
|
||||
//! `3abe0d3fa3c11c99`) and was retired in pd.3 alongside the
|
||||
//! `prelude.ail.json` file.
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
use ailang_surface::parse_prelude;
|
||||
|
||||
#[test]
|
||||
fn prelude_parse_yields_canonical_hash() {
|
||||
@@ -55,6 +26,3 @@ fn prelude_parse_yields_canonical_hash() {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user