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:
2026-05-14 13:24:40 +02:00
parent c0dd2bc334
commit 9a8d3850e7
11 changed files with 354 additions and 664 deletions
@@ -0,0 +1,22 @@
{
"iter_id": "pd.3",
"date": "2026-05-14",
"mode": "standard",
"outcome": "DONE",
"tasks_total": 8,
"tasks_completed": 8,
"reloops_per_task": {
"1": 0,
"2": 0,
"3": 0,
"4": 0,
"5": 0,
"6": 0,
"7": 0,
"8": 0
},
"review_loops_spec": 0,
"review_loops_quality": 1,
"blocked_reason": null,
"notes": "Task 1 implementer-phase had one structural correction loop (call-site swap → relocation) when the plan-prescribed mechanism failed to typecheck across the dev-dep cycle; counted as an in-phase repair, not a re-loop. Task 1 quality phase had one Minor (verbose stub comment); fixed inline by trimming, then re-approved — counted as the single quality re-loop. Task 4 + Task 6 had RED-on-purpose intermediate states (carve-out inventory test fails until Task 7 deletes the file; carve-out pin RED-on-purpose until same); both flipped GREEN at Task 7 as planned — not counted as failures. Task 3 doc-comment edit on MigrateCanonicalTypes was an unrequested-but-justified correction (capability-claim falsification by the strict task work); marked DONE_WITH_CONCERNS. pd.3 closes the prelude-decouple milestone (pd.1 + pd.2 + pd.3)."
}
+5 -21
View File
@@ -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();
+6 -33
View File
@@ -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 {
+55
View File
@@ -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;
+209
View File
@@ -0,0 +1,209 @@
# iter pd.3 — prelude.ail.json deleted; prelude-decouple milestone closed
**Date:** 2026-05-14
**Started from:** c0dd2bc334d490e5e90ad10501c33e48076ff2bd
**Status:** DONE
**Tasks completed:** 8 of 8
## Summary
pd.3 closes the **prelude-decouple milestone** (pd.1 + pd.2 + pd.3) by
deleting `examples/prelude.ail.json` from the working tree, sweeping the
three remaining `include_str!` consumers (one in-mod test in
`workspace.rs`, the cross-form-identity preflight in
`prelude_module_hash_pin.rs`, the defensive include in `ail/src/main.rs`'s
`migrate-canonical-types` subcommand) and the lockstep skip-branch the
defensive include necessitated, dropping the §C4 (b) entry from
`carve_out_inventory.rs` (8 → 7 named carve-outs), retiring §C4 (b) of
`docs/specs/2026-05-13-form-a-default-authoring.md` in place with a
"Status: RETIRED 2026-05-14" marker that preserves the original framing
as historical context, and adding `prelude_decouple_carve_out_pin.rs`
as the milestone's permanent guard against regression. At milestone
close: the prelude exists on disk only as `examples/prelude.ail`; core
embeds zero prelude bytes; the `migrate-canonical-types` subcommand no
longer auto-injects a synthetic prelude entry; the doctrine "authors
write `.ail`; the build derives the JSON-AST in-process via
`ailang_surface::parse`" (CLAUDE.md) holds without exception. Test count:
573 green workspace-wide (preserved from pd.2; +1 carve-out pin -1
cross-form-identity preflight +1 relocated `build_workspace_accepts...`
test -1 in-mod twin = net 0). `bench/cross_lang.py` + `bench/compile_check.py`
exit 0; `bench/check.py` exit 1 with the same envelope-noise pattern
observed since audit-cma (14th consecutive observation; pd.3 is a
file-system + spec-text + test deletion + dead-code cleanup with zero
codegen / runtime / typecheck touch, so cannot be the source).
## Per-task notes
- iter pd.3.1: `build_workspace_accepts_assembled_modules_and_runs_validation`
RELOCATED from `crates/ailang-core/src/workspace.rs::tests` (in-mod) to
`crates/ailang-core/tests/workspace_pin.rs` (integration crate) instead
of the literal call-site swap the plan prescribed. The plan specified
"use the fully-qualified `ailang_surface::parse_prelude()` at the call
site" claiming it was the workaround pd.2 used; in fact pd.2 worked
around this same dev-dep cycle by inlining
`serde_json::from_str(include_str!("prelude.ail.json"))` precisely
BECAUSE `parse_prelude()` doesn't typecheck from in-mod (the lib-test
target sees two distinct `ailang-core` compilations and the `Module`
types fail to unify). pd.3 cannot use the JSON workaround because the
JSON file is being deleted. The structural fix is the relocation
pattern pd.2.4 already used for 10 sibling tests — same crate edge,
same outcome. A 6-line stub-comment in `workspace.rs::tests` documents
the dev-dep cycle so future maintainers don't try to put the test
back. New test `load_one_adapter` helper at the top of `workspace_pin.rs`
mirrors the in-mod `load_one` exactly (Io/Schema mapping). DONE_WITH_CONCERNS:
plan defect (treated under pd.2.4 lesson recommendation but not
caught at planning time).
- iter pd.3.2: 5 deletions in `prelude_module_hash_pin.rs` (cross-form-
identity preflight test fn, `PRELUDE_JSON` const, `PRELUDE_AIL` import,
`const _: &str = PRELUDE_AIL;` shape-pin, lines 1-15 of original
module doc-comment); module doc-comment replaced verbatim with the
surviving-only version (preserves the historical record of why the
preflight existed, what it discharged, when it was retired). Hash
`3abe0d3fa3c11c99` still asserted by the surviving
`prelude_parse_yields_canonical_hash`. Zero orphan-import warnings.
First-shot clean.
- iter pd.3.3: defensive `include_str!` block (12 lines) deleted from
`Cmd::MigrateCanonicalTypes` arm; lockstep skip-branch (4 lines)
deleted from the rewrite loop's Pass 2. The plan referenced the
subcommand by its variant tag `MigrateBareCrossModuleRefs` but the
user-facing subcommand name is `migrate-canonical-types` — spot-test
ran under the correct name and the help text printed. DONE_WITH_CONCERNS:
plan referenced obsolete variant name; doc-comment update on
`MigrateCanonicalTypes` (one paragraph) was needed beyond strict task
text because the prelude-fallback capability that the doc-comment
asserted ("Prelude is consulted as an implicit last-resort fallback
when no listed import owns the name") was silently removed by Step 2
— leaving the doc-comment in would have been an active mis-statement
in `--help` output. Update names pd.3 as the retirement point.
- iter pd.3.4: 3 edits to `carve_out_inventory.rs` (header sentence
"eight" → "seven"; comment block re-published per plan; `"prelude.ail.json"`
entry + its `// §C4 (b) — compile-time-embed` comment dropped from
EXPECTED). Test RED-on-purpose post-edit (file still on disk);
flips GREEN at Task 7.
- iter pd.3.5: Status marker inserted verbatim above §C4 (b) heading
in `docs/specs/2026-05-13-form-a-default-authoring.md`. Original §C4
(b) text preserved as historical context (per the spec's convention
for retired sections). Reads coherently top-to-bottom; the marker
reads as a forward-pointer to the resolution and the original text
reads as historical motivation.
- iter pd.3.6: `crates/ailang-surface/tests/prelude_decouple_carve_out_pin.rs`
created verbatim per the plan. RED-on-purpose at this task (file still
exists); flips GREEN at Task 7. Failure message names the four things
a future maintainer must do to legitimately reintroduce the file —
pre-empts the "it's just one file" rationalisation.
- iter pd.3.7: pre-deletion verification clean (zero `include_str!`
consumers; the two non-`include_str!` `.rs` references are inert —
`print_no_leak_pin.rs:110` is a string-literal in an assertion
message, `prelude_injection_pin.rs:28` constructs a tempdir-relative
path that does not read the in-tree file). `rm examples/prelude.ail.json`.
Both pin tests flip RED → GREEN at this step. Workspace test count
unchanged at 573 green.
- iter pd.3.8: `bench/compile_check.py` exit 0; `bench/cross_lang.py`
exit 0; `bench/check.py` exit 1 with the established noise envelope
(the `bench_list_sum.bump_s` and `implicit_at_rc` tail-latency
oscillation observed since audit-cma — 14th consecutive observation).
Acceptance criterion sweep: zero `include_str!` of `prelude.ail.json`
workspace-wide; zero literal-`"prelude"` in `crates/ailang-core/src/`
production scope (the matches in `workspace.rs` are all inside
`#[cfg(test)] mod tests`); CLAUDE.md "Authors write `.ail`" sentence
is consistent with the post-milestone state without modification (the
prelude exception it implicitly tolerated pre-milestone is gone);
`docs/DESIGN.md` carries no production-doctrine assertions about core
embedding the prelude or referencing `prelude.ail.json`.
## Concerns
- pd.3.1: plan defect — the task text instructed a call-site swap
("In each affected test fn body, replace the pattern ... with
`ailang_surface::parse_prelude()`") that doesn't typecheck due to
the dev-dep cycle. The plan also misattributed pd.2's actual
workaround (`serde_json::from_str(include_str!(...))`) as
successful use of `parse_prelude()` from in-mod. Recommend planner
recon-check: when a plan proposes a call-site change involving
`ailang_surface::*` from in-mod tests, verify the test target can
resolve the dev-dep edge (the lib-test target generally cannot;
the integration-test crate generally can — same lesson pd.2.4
recorded but didn't propagate to the planner skill).
- pd.3.3: plan referenced subcommand by its internal variant tag
(`MigrateBareCrossModuleRefs`) instead of its user-facing name
(`migrate-canonical-types`). Trivial repair (test under correct
name) but recommend planner check: verify subcommand-arm references
in spot-test commands by enumeration-after-rename.
- pd.3.3 (deeper): the plan's "Architecture" preamble called the
defensive include "a documented no-op" because the inserted entry
was unconditionally skipped by the lockstep guard at lines 500-505.
This is correct about WRITING the rewritten module file back, but
not about CONSULTING `local_types["prelude"]` for fallback rewrites
(which the inserted entry populated, lines 487-497, before the Pass
2 skip). Removing both blocks silently retired the prelude-fallback
capability of the rewrite logic. The plan called the change net-zero
behaviour; it isn't. The doc-comment update I made on
`MigrateCanonicalTypes` makes the new behaviour visible. The
rewrite-helper's prelude-fallback branch at lines ~2436-2446 in
`main.rs` is now dead-by-construction (the `local_types` map will
never have a `"prelude"` key, since no prelude module is loaded by
this subcommand) — left in place per the implementer's
no-surrounding-cleanup discipline; tagged below as known debt.
## Known debt
- `crates/ail/src/main.rs:~2436-2446`: the `local_types.get("prelude")`
fallback in `rewrite_def`'s `rewrite_type_con_owner` (or analogue)
is dead-by-construction post-pd.3 since the `local_types` map for
the `migrate-canonical-types` subcommand no longer contains a
`"prelude"` key. Out of pd.3's literal scope (no surrounding cleanup);
candidate for a follow-up tidy iter or for the post-pd.3 audit's
drift sweep.
- 3 cargo doc warnings on dangling `[`load_workspace`]` references in
`Workspace` and `Registry` rustdoc — pre-existing baseline carried
forward from pd.2; out of pd.3 scope. (Identical entry was in the
pd.2 known-debt section.)
- The `migrate-canonical-types` subcommand's substantive behaviour
(rewriting bare cross-module refs across a directory of `.ail.json`
files) hasn't been exercised since the canonical-type-names
milestone. No in-tree fixtures known to need rewriting today; spot-
tested via `--help` only. If a future migration is requested, the
prelude-fallback removal + the rewrite-helper dead-branch should
be reviewed together.
## Files touched
Modified:
- `crates/ail/src/main.rs` (delete defensive include 472-484, delete
skip-branch 500-505, update `MigrateCanonicalTypes` doc-comment to
reflect retired prelude-fallback)
- `crates/ailang-core/src/workspace.rs` (delete in-mod
`build_workspace_accepts_assembled_modules_and_runs_validation` test;
replace with 6-line stub comment documenting the dev-dep cycle as
invariant)
- `crates/ailang-core/tests/carve_out_inventory.rs` (header sentence
+ comment block + EXPECTED list — 8 → 7)
- `crates/ailang-core/tests/workspace_pin.rs` (add `load_one_adapter`
helper at top; append the relocated test at end)
- `crates/ailang-surface/tests/prelude_module_hash_pin.rs` (delete
cross-form-identity preflight + supporting `PRELUDE_JSON` /
`PRELUDE_AIL` references; trim doc-comment to surviving-only
framing)
- `docs/specs/2026-05-13-form-a-default-authoring.md` (insert "Status:
RETIRED" marker above §C4 (b) heading)
New:
- `crates/ailang-surface/tests/prelude_decouple_carve_out_pin.rs`
(single test asserting `examples/prelude.ail.json` does not exist)
Deleted:
- `examples/prelude.ail.json` (the milestone's primary artefact
retirement)
## Stats
`bench/orchestrator-stats/2026-05-14-iter-pd.3.json`
## Note on pre-existing dirty INDEX.md
When this iter dispatched, `docs/journals/INDEX.md` was already
modified in the working tree with one Boss-side line for pd.2 (the
prior iter); that edit was not committed before pd.3 dispatch. This
agent did NOT touch INDEX.md (it is Boss-only per skill discipline).
The Boss should commit the pd.2 INDEX line either with the pd.3
commit or as a separate prior commit — both options are clean.
+2
View File
@@ -67,3 +67,5 @@
- 2026-05-14 — iter rpe.1.tidy: subst.rs preserves Type::Fn modes through rebuild → 2026-05-14-iter-rpe.1.tidy.md
- 2026-05-14 — iter cli-diag-human: WorkspaceLoadError thru diagnostic in non-JSON CLI path → 2026-05-14-iter-cli-diag-human.md
- 2026-05-14 — iter pd.1: core API split — load_modules_with + build_workspace + implicit_imports threading; load_workspace_with kept as 3-line shim so surface unchanged; production literal-"prelude" count in workspace.rs dropped 8→4 → 2026-05-14-iter-pd.1.md
- 2026-05-14 — iter pd.2: surface owns prelude embed (PRELUDE_AIL + parse_prelude in ailang-surface); pd.1 shim load_workspace_with retired along with PRELUDE_JSON / load_prelude / load_one; cross-form-identity preflight PASSED at module_hash 3abe0d3fa3c11c99; production literal-"prelude" in core/src/ now zero → 2026-05-14-iter-pd.2.md
- 2026-05-14 — iter pd.3: prelude.ail.json deleted from working tree; cross-form-identity preflight + supporting bytes deleted from prelude_module_hash_pin (purpose discharged); migrate-bare-cross-module-refs defensive include + lockstep skip-branch deleted; carve_out_inventory.rs §C4(b) row dropped (8→7); form-a-default-authoring spec §C4(b) RETIRED status marker added; new prelude_decouple_carve_out_pin.rs asserts JSON file does not exist; milestone prelude-decouple closed → 2026-05-14-iter-pd.3.md
@@ -267,6 +267,15 @@ Justification per file:
(changing the diagnostic) or refuse to encode at all (changing
the test subject).
> **Status: RETIRED 2026-05-14 by milestone prelude-decouple.** The
> §C4 (b) compile-time-embed carve-out is empty post that milestone;
> the prelude is now authored exclusively as `examples/prelude.ail`
> and parsed at compile time via `ailang_surface::parse_prelude`. The
> original §C4 (b) framing below is preserved as historical context;
> it documents the doctrine inconsistency that prelude-decouple
> resolved. See `docs/specs/2026-05-14-prelude-decouple.md` and the
> per-iter journals 2026-05-14-iter-pd.{1,2,3}.md for the resolution.
**(b) Compile-time-embed carve-outs (one file as of 2026-05-13):**
```
-559
View File
@@ -1,559 +0,0 @@
{
"schema": "ailang/v0",
"name": "prelude",
"imports": [],
"defs": [
{
"kind": "type",
"name": "Ordering",
"vars": [],
"doc": "Result of a three-way comparison: LT (less than), EQ (equal), GT (greater than). Ships in milestone 23 as the codomain of Ord.compare.",
"ctors": [
{ "name": "LT", "fields": [] },
{ "name": "EQ", "fields": [] },
{ "name": "GT", "fields": [] }
]
},
{
"kind": "class",
"name": "Eq",
"param": "a",
"doc": "Structural equality. Ships in milestone 23 alongside Ord. The primitive `==` operator stays as the surface-level comparator this iter; routing `==` through `Eq.eq` is the declared P2 follow-up (see docs/specs/2026-05-10-23-eq-ord-prelude.md, `Out of scope`).",
"methods": [
{
"name": "eq",
"type": {
"k": "fn",
"params": [
{ "k": "var", "name": "a" },
{ "k": "var", "name": "a" }
],
"param_modes": ["borrow", "borrow"],
"ret": { "k": "con", "name": "Bool" },
"effects": []
}
}
]
},
{
"kind": "instance",
"class": "Eq",
"type": { "k": "con", "name": "Int" },
"doc": "Eq Int. Body lowers to `icmp eq i64` via the existing primitive `==` dispatch in `lower_eq`.",
"methods": [
{
"name": "eq",
"body": {
"t": "lam",
"params": ["x", "y"],
"paramTypes": [
{ "k": "var", "name": "a" },
{ "k": "var", "name": "a" }
],
"retType": { "k": "con", "name": "Bool" },
"body": {
"t": "app",
"fn": { "t": "var", "name": "==" },
"args": [
{ "t": "var", "name": "x" },
{ "t": "var", "name": "y" }
]
}
}
}
]
},
{
"kind": "instance",
"class": "Eq",
"type": { "k": "con", "name": "Bool" },
"doc": "Eq Bool. Body lowers to `icmp eq i1` via the existing primitive `==` dispatch in `lower_eq`.",
"methods": [
{
"name": "eq",
"body": {
"t": "lam",
"params": ["x", "y"],
"paramTypes": [
{ "k": "var", "name": "a" },
{ "k": "var", "name": "a" }
],
"retType": { "k": "con", "name": "Bool" },
"body": {
"t": "app",
"fn": { "t": "var", "name": "==" },
"args": [
{ "t": "var", "name": "x" },
{ "t": "var", "name": "y" }
]
}
}
}
]
},
{
"kind": "instance",
"class": "Eq",
"type": { "k": "con", "name": "Str" },
"doc": "Eq Str. The lambda body shape mirrors Int/Bool for round-trip stability, but the codegen intercept in `emit_fn` overrides the body to call `@ail_str_eq` directly — see `try_emit_primitive_instance_body` in `crates/ailang-codegen/src/lib.rs`.",
"methods": [
{
"name": "eq",
"body": {
"t": "lam",
"params": ["x", "y"],
"paramTypes": [
{ "k": "var", "name": "a" },
{ "k": "var", "name": "a" }
],
"retType": { "k": "con", "name": "Bool" },
"body": {
"t": "app",
"fn": { "t": "var", "name": "==" },
"args": [
{ "t": "var", "name": "x" },
{ "t": "var", "name": "y" }
]
}
}
}
]
},
{
"kind": "class",
"name": "Ord",
"param": "a",
"superclass": { "class": "Eq", "type": "a" },
"doc": "Total ordering. Ships in milestone 23 alongside Eq. `compare x y` returns LT, EQ, or GT (the three-ctor Ordering ADT also in the prelude). Decision 11's single-superclass closure requires `instance Eq T` for every `instance Ord T` — the three Ord instances below pair with the three Eq instances shipped in iter 23.2.3.",
"methods": [
{
"name": "compare",
"type": {
"k": "fn",
"params": [
{ "k": "var", "name": "a" },
{ "k": "var", "name": "a" }
],
"param_modes": ["borrow", "borrow"],
"ret": { "k": "con", "name": "Ordering" },
"effects": []
}
}
]
},
{
"kind": "instance",
"class": "Ord",
"type": { "k": "con", "name": "Int" },
"doc": "Ord Int. The lambda body shape is a placeholder for round-trip stability; the codegen intercept `try_emit_primitive_instance_body::\"compare__Int\"` emits a three-way `icmp slt` / `icmp eq` branch ladder constructing LT / EQ / GT.",
"methods": [
{
"name": "compare",
"body": {
"t": "lam",
"params": ["x", "y"],
"paramTypes": [
{ "k": "var", "name": "a" },
{ "k": "var", "name": "a" }
],
"retType": { "k": "con", "name": "Ordering" },
"body": {
"t": "ctor",
"type": "Ordering",
"ctor": "EQ",
"args": []
}
}
}
]
},
{
"kind": "instance",
"class": "Ord",
"type": { "k": "con", "name": "Bool" },
"doc": "Ord Bool. Body lowered via `try_emit_primitive_instance_body::\"compare__Bool\"` — `icmp ult i1` LT-test, `icmp eq i1` EQ-test, GT default.",
"methods": [
{
"name": "compare",
"body": {
"t": "lam",
"params": ["x", "y"],
"paramTypes": [
{ "k": "var", "name": "a" },
{ "k": "var", "name": "a" }
],
"retType": { "k": "con", "name": "Ordering" },
"body": {
"t": "ctor",
"type": "Ordering",
"ctor": "EQ",
"args": []
}
}
}
]
},
{
"kind": "instance",
"class": "Ord",
"type": { "k": "con", "name": "Str" },
"doc": "Ord Str. Body lowered via `try_emit_primitive_instance_body::\"compare__Str\"` — `call i32 @ail_str_compare(ptr, ptr)` then branch on slt-0 / eq-0 against the normalised {-1, 0, +1} return.",
"methods": [
{
"name": "compare",
"body": {
"t": "lam",
"params": ["x", "y"],
"paramTypes": [
{ "k": "var", "name": "a" },
{ "k": "var", "name": "a" }
],
"retType": { "k": "con", "name": "Ordering" },
"body": {
"t": "ctor",
"type": "Ordering",
"ctor": "EQ",
"args": []
}
}
}
]
},
{
"kind": "class",
"name": "Show",
"param": "a",
"doc": "Producer of a human-readable Str representation. Ships in milestone 24 with primitive instances for Int/Bool/Str/Float; user types declare their own instance.",
"methods": [
{
"name": "show",
"type": {
"k": "fn",
"params": [{ "k": "var", "name": "a" }],
"param_modes": ["borrow"],
"ret": { "k": "con", "name": "Str" },
"ret_mode": "own",
"effects": []
}
}
]
},
{
"kind": "instance",
"class": "Show",
"type": { "k": "con", "name": "Int" },
"methods": [
{
"name": "show",
"body": {
"t": "lam",
"params": ["x"],
"paramTypes": [{ "k": "con", "name": "Int" }],
"retType": { "k": "con", "name": "Str" },
"body": {
"t": "app",
"fn": { "t": "var", "name": "int_to_str" },
"args": [{ "t": "var", "name": "x" }]
}
}
}
]
},
{
"kind": "instance",
"class": "Show",
"type": { "k": "con", "name": "Bool" },
"methods": [
{
"name": "show",
"body": {
"t": "lam",
"params": ["x"],
"paramTypes": [{ "k": "con", "name": "Bool" }],
"retType": { "k": "con", "name": "Str" },
"body": {
"t": "app",
"fn": { "t": "var", "name": "bool_to_str" },
"args": [{ "t": "var", "name": "x" }]
}
}
}
]
},
{
"kind": "instance",
"class": "Show",
"type": { "k": "con", "name": "Str" },
"methods": [
{
"name": "show",
"body": {
"t": "lam",
"params": ["x"],
"paramTypes": [{ "k": "con", "name": "Str" }],
"retType": { "k": "con", "name": "Str" },
"body": {
"t": "app",
"fn": { "t": "var", "name": "str_clone" },
"args": [{ "t": "var", "name": "x" }]
}
}
}
]
},
{
"kind": "instance",
"class": "Show",
"type": { "k": "con", "name": "Float" },
"methods": [
{
"name": "show",
"body": {
"t": "lam",
"params": ["x"],
"paramTypes": [{ "k": "con", "name": "Float" }],
"retType": { "k": "con", "name": "Str" },
"body": {
"t": "app",
"fn": { "t": "var", "name": "float_to_str" },
"args": [{ "t": "var", "name": "x" }]
}
}
}
]
},
{
"kind": "fn",
"name": "ne",
"doc": "Polymorphic disequality. `ne x y` ≡ not (eq x y). Ships in milestone 23 as the Eq-class free helper.",
"type": {
"k": "forall",
"vars": ["a"],
"constraints": [{ "class": "Eq", "type": { "k": "var", "name": "a" } }],
"body": {
"k": "fn",
"params": [
{ "k": "var", "name": "a" },
{ "k": "var", "name": "a" }
],
"param_modes": ["borrow", "borrow"],
"ret": { "k": "con", "name": "Bool" },
"effects": []
}
},
"params": ["x", "y"],
"body": {
"t": "app",
"fn": { "t": "var", "name": "not" },
"args": [{
"t": "app",
"fn": { "t": "var", "name": "eq" },
"args": [
{ "t": "var", "name": "x" },
{ "t": "var", "name": "y" }
]
}]
}
},
{
"kind": "fn",
"name": "lt",
"doc": "Polymorphic strict-less-than. `lt x y` ≡ case compare x y of LT -> True; _ -> False. Ships in milestone 23 as the Ord-class free helper.",
"type": {
"k": "forall",
"vars": ["a"],
"constraints": [{ "class": "Ord", "type": { "k": "var", "name": "a" } }],
"body": {
"k": "fn",
"params": [
{ "k": "var", "name": "a" },
{ "k": "var", "name": "a" }
],
"param_modes": ["borrow", "borrow"],
"ret": { "k": "con", "name": "Bool" },
"effects": []
}
},
"params": ["x", "y"],
"body": {
"t": "match",
"scrutinee": {
"t": "app",
"fn": { "t": "var", "name": "compare" },
"args": [
{ "t": "var", "name": "x" },
{ "t": "var", "name": "y" }
]
},
"arms": [
{
"pat": { "p": "ctor", "ctor": "LT", "fields": [] },
"body": { "t": "lit", "lit": { "kind": "bool", "value": true } }
},
{
"pat": { "p": "wild" },
"body": { "t": "lit", "lit": { "kind": "bool", "value": false } }
}
]
}
},
{
"kind": "fn",
"name": "le",
"doc": "Polymorphic less-than-or-equal. `le x y` ≡ case compare x y of GT -> False; _ -> True. Ships in milestone 23 as the Ord-class free helper.",
"type": {
"k": "forall",
"vars": ["a"],
"constraints": [{ "class": "Ord", "type": { "k": "var", "name": "a" } }],
"body": {
"k": "fn",
"params": [
{ "k": "var", "name": "a" },
{ "k": "var", "name": "a" }
],
"param_modes": ["borrow", "borrow"],
"ret": { "k": "con", "name": "Bool" },
"effects": []
}
},
"params": ["x", "y"],
"body": {
"t": "match",
"scrutinee": {
"t": "app",
"fn": { "t": "var", "name": "compare" },
"args": [
{ "t": "var", "name": "x" },
{ "t": "var", "name": "y" }
]
},
"arms": [
{
"pat": { "p": "ctor", "ctor": "GT", "fields": [] },
"body": { "t": "lit", "lit": { "kind": "bool", "value": false } }
},
{
"pat": { "p": "wild" },
"body": { "t": "lit", "lit": { "kind": "bool", "value": true } }
}
]
}
},
{
"kind": "fn",
"name": "gt",
"doc": "Polymorphic strict-greater-than. `gt x y` ≡ case compare x y of GT -> True; _ -> False. Ships in milestone 23 as the Ord-class free helper.",
"type": {
"k": "forall",
"vars": ["a"],
"constraints": [{ "class": "Ord", "type": { "k": "var", "name": "a" } }],
"body": {
"k": "fn",
"params": [
{ "k": "var", "name": "a" },
{ "k": "var", "name": "a" }
],
"param_modes": ["borrow", "borrow"],
"ret": { "k": "con", "name": "Bool" },
"effects": []
}
},
"params": ["x", "y"],
"body": {
"t": "match",
"scrutinee": {
"t": "app",
"fn": { "t": "var", "name": "compare" },
"args": [
{ "t": "var", "name": "x" },
{ "t": "var", "name": "y" }
]
},
"arms": [
{
"pat": { "p": "ctor", "ctor": "GT", "fields": [] },
"body": { "t": "lit", "lit": { "kind": "bool", "value": true } }
},
{
"pat": { "p": "wild" },
"body": { "t": "lit", "lit": { "kind": "bool", "value": false } }
}
]
}
},
{
"kind": "fn",
"name": "ge",
"doc": "Polymorphic greater-than-or-equal. `ge x y` ≡ case compare x y of LT -> False; _ -> True. Ships in milestone 23 as the Ord-class free helper.",
"type": {
"k": "forall",
"vars": ["a"],
"constraints": [{ "class": "Ord", "type": { "k": "var", "name": "a" } }],
"body": {
"k": "fn",
"params": [
{ "k": "var", "name": "a" },
{ "k": "var", "name": "a" }
],
"param_modes": ["borrow", "borrow"],
"ret": { "k": "con", "name": "Bool" },
"effects": []
}
},
"params": ["x", "y"],
"body": {
"t": "match",
"scrutinee": {
"t": "app",
"fn": { "t": "var", "name": "compare" },
"args": [
{ "t": "var", "name": "x" },
{ "t": "var", "name": "y" }
]
},
"arms": [
{
"pat": { "p": "ctor", "ctor": "LT", "fields": [] },
"body": { "t": "lit", "lit": { "kind": "bool", "value": false } }
},
{
"pat": { "p": "wild" },
"body": { "t": "lit", "lit": { "kind": "bool", "value": true } }
}
]
}
},
{
"kind": "fn",
"name": "print",
"doc": "Polymorphic console-print helper. `print x` ≡ `do io/print_str (show x)` with an explicit let-binder around `show x` for heap-Str RC discipline per eob.1 Str carve-out. Ships in milestone 24 as the second half of the Show prelude.",
"type": {
"k": "forall",
"vars": ["a"],
"constraints": [{ "class": "Show", "type": { "k": "var", "name": "a" } }],
"body": {
"k": "fn",
"params": [{ "k": "var", "name": "a" }],
"param_modes": ["borrow"],
"ret": { "k": "con", "name": "Unit" },
"effects": ["IO"]
}
},
"params": ["x"],
"body": {
"t": "let",
"name": "s",
"value": {
"t": "app",
"fn": { "t": "var", "name": "show" },
"args": [{ "t": "var", "name": "x" }]
},
"body": {
"t": "do",
"op": "io/print_str",
"args": [{ "t": "var", "name": "s" }]
}
}
}
]
}