832375f2ac
All 176 files in the four accumulating directories now use a zero-padded 4-digit counter prefix that reflects creation order (`NNNN-slug.md`). The counter is assigned per directory in strict git-log creation order; ties broken alphabetically by original name. The old `YYYY-MM-DD-` prefix on docs/specs/ and docs/plans/ files is dropped — the date is recoverable from git log and the counter carries the ordering. A file's counter is stable for the life of the file: never reassigned, never reused, never compacted. Deleted files retire their counter; subsequent files do not fill the gap. This is the property that lets cross-references stay literal — refs use the full filename including the counter (`design/contracts/0007-honesty-rule.md`) so they grep cleanly and resolve directly without a glob step. 313 cross-references updated across .md/.rs/.toml/.c/.json files (test pins, include_str! paths, design-INDEX entries, baseline notes, runtime C comments, inter-contract markdown links incl. bare basename and `../models/foo.md` forms). CLAUDE.md gets a new "File-naming convention" section spelling out the rule and rationale. skills/brainstorm/SKILL.md and skills/planner/SKILL.md updated so new spec/plan creation produces counter-prefixed names from the start. The full test suite (cargo test --workspace) passes.
803 lines
30 KiB
Markdown
803 lines
30 KiB
Markdown
# pd.2 — surface assumes prelude ownership; core shim retired — Implementation Plan
|
|
|
|
> **Parent spec:** `docs/specs/0028-prelude-decouple.md`
|
|
>
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement`
|
|
> to run this plan. Steps use `- [ ]` checkboxes for tracking.
|
|
|
|
**Goal:** Move the prelude embed (`PRELUDE_JSON` const + `load_prelude` fn) and
|
|
the prelude-injection step (with the `ReservedModuleName` reservation) out of
|
|
`ailang-core` into `ailang-surface`. Rewrite `surface::load_workspace` to call
|
|
`core::load_modules_with` + caller-side prelude inject + `core::build_workspace`
|
|
directly. Retire the pd.1 compatibility shim `core::load_workspace_with` along
|
|
with `load_one` and the `load_module` test-only import. Install the
|
|
cross-form-identity preflight test BEFORE the JSON file gets deleted in pd.3,
|
|
so the embed-source swap is justified by an active green pin and not by
|
|
hope.
|
|
|
|
**Architecture:** post-pd.2 the prelude lives entirely in surface: the bytes
|
|
come from `examples/prelude.ail` via `include_str!`, parsing goes through
|
|
`ailang_surface::parse`, the reserved-name check (`ReservedModuleName`) fires
|
|
from surface, the implicit-imports list `&["prelude"]` is supplied at the
|
|
surface call site. Core's `src/` retains zero literal-`"prelude"` strings;
|
|
the `WorkspaceLoadError::ReservedModuleName` variant declaration stays in
|
|
core's enum (generic shape, no literal in the name) so downstream
|
|
match-arms (notably `crates/ail/src/main.rs:1220-1229`) keep working.
|
|
|
|
**Tech Stack:** `ailang-surface` (loader.rs + new tests/), `ailang-core`
|
|
(workspace.rs deletions + lib.rs rustdoc updates + test-mod re-migration).
|
|
No `ail` CLI changes. No codegen / check changes.
|
|
|
|
---
|
|
|
|
**Files this plan creates or modifies:**
|
|
|
|
- Create: `crates/ailang-surface/tests/prelude_parse_pin.rs` — pins
|
|
`parse_prelude()` succeeds; module name is `"prelude"`; def-count
|
|
meets a minimum bound (guards against empty prelude).
|
|
- Create: `crates/ailang-surface/tests/prelude_module_hash_pin.rs` — two
|
|
`#[test]` fns: (a) `prelude_parse_yields_canonical_hash` pins
|
|
`module_hash(parse_prelude())` against a literal hex captured at iter
|
|
close; (b) `prelude_ail_and_json_parse_to_identical_module` is the
|
|
cross-form-identity preflight that asserts
|
|
`module_hash(serde_json::from_str(prelude.ail.json bytes)) ==
|
|
module_hash(parse(prelude.ail bytes))`. Both are green at pd.2 close;
|
|
pd.3 drops the (b) fn along with the JSON file.
|
|
- Modify: `crates/ailang-surface/src/loader.rs:1-71` — add `PRELUDE_AIL`
|
|
const + `parse_prelude` fn; rewrite `load_workspace` body from the
|
|
current 3-line `load_workspace_with(entry, load_module)` shim-call to
|
|
the 7-line composition `(load_modules_with → reserved-name check →
|
|
inject parse_prelude → build_workspace(&["prelude"]))`. Update import
|
|
line at line 16 (`load_workspace_with` swaps to `load_modules_with,
|
|
build_workspace`). Update module rustdoc at lines 9-13 (the
|
|
`load_workspace_with` cross-reference is stale).
|
|
- Modify: `crates/ailang-core/src/workspace.rs` — delete `PRELUDE_JSON`
|
|
const (~lines 428-430), `load_prelude` fn (~lines 432-439),
|
|
`load_workspace_with` shim (~lines 525-561 including the doc-comment
|
|
at 525-540). Re-migrate 10 in-mod test-fns from
|
|
`load_workspace_with(&entry, load_one)` to `surface::load_workspace`,
|
|
re-migrate 1 in-mod test from `load_workspace_with` to
|
|
`load_modules_with` directly with an inline-closure loader, delete 1
|
|
in-mod test (`load_workspace_with_shim_preserves_today_semantics` —
|
|
subject gone). Delete the now-orphaned `#[cfg(test)] fn load_one` and
|
|
the `#[cfg(test)] use crate::load_module;` import. Update the module
|
|
rustdoc at lines 1-32 (replaces pd.1's "single core entry point" framing
|
|
with "core exposes the two-phase composition; surface owns the public
|
|
entry point").
|
|
- Modify: `crates/ailang-core/src/lib.rs` — three rustdoc sites that
|
|
reference `workspace::load_workspace_with` (lines 16-21, 50-51, 120
|
|
per recon) update to point at `workspace::load_modules_with` +
|
|
`workspace::build_workspace`, with intra-doc cross-references to
|
|
`ailang_surface::load_workspace` for the public composition. The
|
|
`pub use workspace::{module_hash, Workspace, WorkspaceLoadError};`
|
|
re-export at line 73 is unchanged.
|
|
|
|
---
|
|
|
|
### Task 1: Add `PRELUDE_AIL` const + `parse_prelude` fn to surface
|
|
|
|
**Files:**
|
|
- Modify: `crates/ailang-surface/src/loader.rs` — add const + fn near the
|
|
top of the file, above the existing `is_ail_source` helper.
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
Add a new file `crates/ailang-surface/tests/prelude_parse_pin.rs`. The
|
|
test asserts `parse_prelude()` is callable from outside the crate
|
|
(integration-test reach), succeeds, returns a `Module` named `"prelude"`,
|
|
and has a minimum number of defs.
|
|
|
|
```rust
|
|
//! 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()
|
|
);
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Run test to verify it fails**
|
|
|
|
Run: `cargo test -p ailang-surface --test prelude_parse_pin`
|
|
Expected: FAIL with "cannot find function `parse_prelude` in module `ailang_surface`"
|
|
(and "no item named `parse_prelude`").
|
|
|
|
- [ ] **Step 3: Add `PRELUDE_AIL` const + `parse_prelude` fn to `loader.rs`**
|
|
|
|
Edit `crates/ailang-surface/src/loader.rs`. Add immediately above the
|
|
existing `fn is_ail_source` (line 19 area):
|
|
|
|
```rust
|
|
/// 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() -> ailang_core::ast::Module {
|
|
crate::parse(PRELUDE_AIL).expect("examples/prelude.ail must parse as a Module")
|
|
}
|
|
```
|
|
|
|
`pub` is required for `parse_prelude` because the integration test (and
|
|
later, any out-of-crate consumer) needs to call it. `PRELUDE_AIL` is
|
|
also `pub` for the cross-form-identity preflight test to reach the same
|
|
bytes.
|
|
|
|
- [ ] **Step 4: Run test to verify it passes**
|
|
|
|
Run: `cargo test -p ailang-surface --test prelude_parse_pin`
|
|
Expected: PASS.
|
|
|
|
- [ ] **Step 5: Run the surface crate test suite**
|
|
|
|
Run: `cargo test -p ailang-surface`
|
|
Expected: all tests green; the new `parse_prelude` is additive.
|
|
|
|
---
|
|
|
|
### Task 2: Write the cross-form-identity preflight + module-hash pin
|
|
|
|
**Files:**
|
|
- Create: `crates/ailang-surface/tests/prelude_module_hash_pin.rs` — two
|
|
`#[test]` fns (cross-form-identity + hash pin).
|
|
|
|
This task captures the load-bearing assumption that swapping the embed
|
|
source from `prelude.ail.json` to `prelude.ail` does not change the
|
|
canonical `Module`. Both files are still present at pd.2-time, so the
|
|
preflight can compare them directly. pd.3 drops the JSON-side fn along
|
|
with the JSON file.
|
|
|
|
- [ ] **Step 1: Write the failing tests**
|
|
|
|
Create `crates/ailang-surface/tests/prelude_module_hash_pin.rs`:
|
|
|
|
```rust
|
|
//! 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, "PLACEHOLDER_TO_BE_REPLACED_AT_STEP_5",
|
|
"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;
|
|
```
|
|
|
|
NOTE: the literal hex in `prelude_parse_yields_canonical_hash` is a
|
|
placeholder that gets replaced at Step 5 of this task once the test is
|
|
green. The `cross-form-identity` test does NOT contain a literal — it
|
|
asserts equality between two computed values, so it doesn't need a
|
|
captured hex.
|
|
|
|
- [ ] **Step 2: Run tests to observe failure modes**
|
|
|
|
Run: `cargo test -p ailang-surface --test prelude_module_hash_pin`
|
|
Expected:
|
|
- `prelude_ail_and_json_parse_to_identical_module` may PASS or FAIL
|
|
depending on whether the two parse paths actually agree today. If it
|
|
PASSES, we are clear to ship. If it FAILS, **STOP** and report — pd.3
|
|
cannot proceed; the spec's load-bearing assumption is violated and
|
|
the milestone needs a brainstorm-level rethink.
|
|
- `prelude_parse_yields_canonical_hash` FAILS with the placeholder
|
|
comparison (placeholder != real hash).
|
|
|
|
- [ ] **Step 3: If the cross-form-identity test PASSED in Step 2,
|
|
capture the canonical hex hash**
|
|
|
|
Read the failure message of `prelude_parse_yields_canonical_hash` to
|
|
get the actual computed hash; the assert prints both expected and
|
|
actual. Record the actual hex string.
|
|
|
|
If the test message is unclear, run a one-shot helper:
|
|
|
|
```bash
|
|
cargo test -p ailang-surface --test prelude_module_hash_pin prelude_parse_yields_canonical_hash -- --nocapture 2>&1 | grep -E '^( *left:| *right:)'
|
|
```
|
|
|
|
The "right" line carries the actual computed hash.
|
|
|
|
- [ ] **Step 4: If the cross-form-identity test FAILED in Step 2, STOP**
|
|
|
|
Do NOT proceed with Task 3+. Write a per-iter journal entry under
|
|
`docs/journals/2026-05-14-iter-pd.2.md` capturing:
|
|
- The two computed hashes that disagree.
|
|
- Which Module fields differ (run a manual diff via reading both
|
|
Modules into Rust and pretty-printing — or use `git grep` on the
|
|
relevant `serde_json::from_str` Display output to localise the
|
|
divergence).
|
|
- A statement that the milestone is BLOCKED pending a brainstorm-level
|
|
decision on whether to retire the JSON or keep it.
|
|
|
|
This branch is unlikely (the round-trip CI through form-a.1 has been
|
|
exercising parse-then-print idempotency on every `.ail` fixture), but
|
|
the test exists precisely to catch the case where the assumption
|
|
fails.
|
|
|
|
- [ ] **Step 5: Replace the placeholder hex with the real hash**
|
|
|
|
Edit `prelude_module_hash_pin.rs`:
|
|
|
|
```rust
|
|
assert_eq!(
|
|
h, "<actual hex from Step 3>", // captured at pd.2 iter close
|
|
...
|
|
);
|
|
```
|
|
|
|
- [ ] **Step 6: Re-run tests, expect both green**
|
|
|
|
Run: `cargo test -p ailang-surface --test prelude_module_hash_pin`
|
|
Expected: BOTH tests PASS.
|
|
|
|
---
|
|
|
|
### Task 3: Rewrite `surface::load_workspace` body
|
|
|
|
**Files:**
|
|
- Modify: `crates/ailang-surface/src/loader.rs:69-71` — replace the
|
|
3-line shim-call with the 7-line composition.
|
|
|
|
- [ ] **Step 1: Write the regression-pin test for the new composition**
|
|
|
|
Surface's existing `tests/` already contain end-to-end tests of
|
|
`load_workspace`. Add ONE new test specifically pinning that a fresh
|
|
workspace contains the prelude module, and that a user module trying
|
|
to take the name `prelude` is rejected with `ReservedModuleName`.
|
|
|
|
Append to an appropriate existing test file in `crates/ailang-surface/tests/`,
|
|
or create a new file `crates/ailang-surface/tests/prelude_injection_pin.rs`:
|
|
|
|
```rust
|
|
//! 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).
|
|
let entry = Path::new("../../examples/hello.ail.json");
|
|
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#"{"name":"prelude","imports":[],"defs":[]}"#,
|
|
)
|
|
.unwrap();
|
|
// Entry is a separate module named "main" that imports the user's
|
|
// prelude — actually, since prelude is implicit, just point the
|
|
// entry at prelude itself: surface checks the entry-module name
|
|
// against the file stem first, so a 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:?}"),
|
|
}
|
|
}
|
|
```
|
|
|
|
If a similar test already exists in `crates/ailang-surface/tests/`,
|
|
extend it instead of duplicating. The recon did not enumerate the
|
|
existing surface test files; check with `ls crates/ailang-surface/tests/`
|
|
before creating.
|
|
|
|
- [ ] **Step 2: Run the new tests against today's implementation
|
|
(still going through the shim)**
|
|
|
|
Run: `cargo test -p ailang-surface --test prelude_injection_pin`
|
|
Expected: BOTH tests PASS — the shim is still wired today and behaves
|
|
correctly. This is the baseline before the rewrite.
|
|
|
|
- [ ] **Step 3: Rewrite `surface::load_workspace`**
|
|
|
|
Edit `crates/ailang-surface/src/loader.rs`. The current body at lines
|
|
69-71:
|
|
|
|
```rust
|
|
pub fn load_workspace(entry: &Path) -> Result<Workspace, WorkspaceLoadError> {
|
|
load_workspace_with(entry, load_module)
|
|
}
|
|
```
|
|
|
|
becomes:
|
|
|
|
```rust
|
|
pub fn load_workspace(entry: &Path) -> Result<Workspace, WorkspaceLoadError> {
|
|
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"],
|
|
)
|
|
}
|
|
```
|
|
|
|
Update the imports at line 16 from:
|
|
|
|
```rust
|
|
use ailang_core::workspace::{load_workspace_with, Workspace, WorkspaceLoadError};
|
|
```
|
|
|
|
to:
|
|
|
|
```rust
|
|
use ailang_core::workspace::{Workspace, WorkspaceLoadError};
|
|
```
|
|
|
|
(The `load_modules_with` and `build_workspace` calls use the fully-
|
|
qualified path `ailang_core::workspace::*` for clarity at the call
|
|
site — the body is the only consumer in this file.)
|
|
|
|
Update the module rustdoc at lines 9-13:
|
|
|
|
```rust
|
|
//! 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_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).
|
|
```
|
|
|
|
- [ ] **Step 4: Run the regression-pin tests**
|
|
|
|
Run: `cargo test -p ailang-surface --test prelude_injection_pin`
|
|
Expected: BOTH tests still PASS after the rewrite (semantics
|
|
preserved).
|
|
|
|
- [ ] **Step 5: Run the full surface crate test suite**
|
|
|
|
Run: `cargo test -p ailang-surface`
|
|
Expected: all tests green. The rewrite is semantically invariant — the
|
|
shim and the new composition produce identical results.
|
|
|
|
---
|
|
|
|
### Task 4: Re-migrate the 11 in-mod core tests + delete 1
|
|
|
|
**Files:**
|
|
- Modify: `crates/ailang-core/src/workspace.rs`'s `#[cfg(test)] mod tests`
|
|
block — re-migrate 10 tests to `surface::load_workspace`, re-migrate 1
|
|
test to `load_modules_with` directly, delete 1 test.
|
|
|
|
The shim disappears in Task 5; the in-mod test callers must already
|
|
have moved off it before the deletion.
|
|
|
|
- [ ] **Step 1: Re-migrate the 9 fixture-style tests to `surface::load_workspace`**
|
|
|
|
For each of the following 9 tests (lines per recon), replace
|
|
`load_workspace_with(&entry, load_one)` with `ailang_surface::load_workspace(&entry)`:
|
|
|
|
| Line | Test fn |
|
|
|------|---------|
|
|
| 1693 | `detects_import_cycle` |
|
|
| 1709 | `module_not_found_yields_structured_error` |
|
|
| 1791 | `class_param_in_applied_position_fires_canonical_form_rejection` |
|
|
| 1814 | `superclass_with_wrong_param_fires_invalid_superclass_param` |
|
|
| 1854 | `constraint_with_unbound_var_fires_unbound_constraint_type_var` |
|
|
| 2491 | `ct1_fixture_bare_xmod_rejected` |
|
|
| 2507 | `ct1_fixture_bad_qualifier` |
|
|
| 2530 | `ct1_fixture_qualified_class_orphan_post_mq1` |
|
|
|
|
Plus the reservation test at line 1675 (`user_module_named_prelude_is_rejected`)
|
|
— its assertion is unchanged because the variant tag preserves; only
|
|
the call site changes.
|
|
|
|
Add at the top of the `mod tests` block (or near other `use` lines):
|
|
|
|
```rust
|
|
#[cfg(test)]
|
|
use ailang_surface::load_workspace as surface_load_workspace;
|
|
```
|
|
|
|
(Aliasing because `load_workspace` no longer exists in `core::workspace`
|
|
and an unaliased import would shadow nothing useful but reduces clarity
|
|
about which crate is being called.)
|
|
|
|
For each migration, the literal change pattern is:
|
|
|
|
Old:
|
|
```rust
|
|
let result = load_workspace_with(&entry, load_one);
|
|
```
|
|
|
|
New:
|
|
```rust
|
|
let result = surface_load_workspace(&entry);
|
|
```
|
|
|
|
The result type and assertion logic are unchanged.
|
|
|
|
- [ ] **Step 2: Run tests after migration**
|
|
|
|
Run: `cargo test -p ailang-core`
|
|
Expected: all tests green. The 9 + 1 migrated tests still PASS because
|
|
`surface::load_workspace`'s behaviour is identical to the shim's by
|
|
Task 3 Step 4.
|
|
|
|
- [ ] **Step 3: Re-migrate `load_workspace_with_custom_loader_is_called`
|
|
to `load_modules_with` directly**
|
|
|
|
Test at line 2559. Its purpose: verify the loader-injection contract
|
|
(custom `Fn(&Path) -> Result<Module, _>` is invoked). The contract
|
|
post-pd.2 belongs to `load_modules_with`'s loader parameter (not the
|
|
disappearing `load_workspace_with`). Migrate by:
|
|
|
|
Old shape (approximate — verify actual at the line):
|
|
```rust
|
|
let called = std::cell::Cell::new(false);
|
|
let custom = |p: &Path| {
|
|
called.set(true);
|
|
load_one(p)
|
|
};
|
|
let _ = load_workspace_with(&entry, custom);
|
|
assert!(called.get());
|
|
```
|
|
|
|
New shape:
|
|
```rust
|
|
let called = std::cell::Cell::new(false);
|
|
let custom = |p: &Path| {
|
|
called.set(true);
|
|
crate::load_module(p).map_err(|e| match e {
|
|
crate::Error::Io(io) => WorkspaceLoadError::Io {
|
|
path: p.to_path_buf(),
|
|
source: io,
|
|
},
|
|
other => WorkspaceLoadError::Schema {
|
|
path: p.to_path_buf(),
|
|
source: other,
|
|
},
|
|
})
|
|
};
|
|
let _ = load_modules_with(&entry, &custom);
|
|
assert!(called.get());
|
|
```
|
|
|
|
The inline closure replaces `load_one`; the closure body wraps
|
|
`crate::load_module` (the JSON-only single-module loader, still public
|
|
in core) and adapts its error type.
|
|
|
|
Test renames from `load_workspace_with_custom_loader_is_called` to
|
|
`load_modules_with_custom_loader_is_called` and the assertion changes
|
|
target accordingly. Same semantic intent (loader-injection contract is
|
|
honoured).
|
|
|
|
- [ ] **Step 4: Delete `load_workspace_with_shim_preserves_today_semantics`**
|
|
|
|
Test at line 2926. Its subject (the pd.1 shim) is being deleted in
|
|
Task 5. The test has no remaining purpose; delete it outright.
|
|
|
|
If the test is bracketed by `// ---` separators or similar comment
|
|
markers, delete the markers too.
|
|
|
|
- [ ] **Step 5: Run tests after both migrations + deletion**
|
|
|
|
Run: `cargo test -p ailang-core`
|
|
Expected: all tests green. 11 in-mod test callers either moved or
|
|
removed; the rest of the test suite is unaffected.
|
|
|
|
---
|
|
|
|
### Task 5: Delete `PRELUDE_JSON`, `load_prelude`, `load_workspace_with`, `load_one`, `load_module` test-only import
|
|
|
|
**Files:**
|
|
- Modify: `crates/ailang-core/src/workspace.rs` — five deletions
|
|
(constant, fn, fn, fn, import).
|
|
|
|
After Tasks 1-4, the only consumers of these symbols are gone. This
|
|
task is the destructive cleanup.
|
|
|
|
- [ ] **Step 1: Verify no remaining callers**
|
|
|
|
Run each of these greps; each must return nothing:
|
|
|
|
```bash
|
|
# PRELUDE_JSON in production:
|
|
git grep -n 'PRELUDE_JSON' crates/ailang-core/src/
|
|
|
|
# load_prelude callers (production):
|
|
git grep -n 'load_prelude' crates/ailang-core/src/
|
|
|
|
# load_workspace_with callers (production OR test):
|
|
git grep -n 'load_workspace_with' --
|
|
|
|
# load_one callers:
|
|
git grep -n 'load_one' crates/ailang-core/
|
|
|
|
# crate::load_module test-only import (the one gated by cfg(test) in
|
|
# pd.1.5):
|
|
git grep -n '#\[cfg(test)\]' crates/ailang-core/src/workspace.rs | head -20
|
|
```
|
|
|
|
If any production hit appears for the first three (excluding the
|
|
definition site itself in `crates/ailang-core/src/workspace.rs`),
|
|
**STOP** — Task 4 Step 1-2 missed a callsite, find it, migrate, re-run
|
|
the grep.
|
|
|
|
- [ ] **Step 2: Delete `PRELUDE_JSON` constant**
|
|
|
|
Delete the const declaration block in `crates/ailang-core/src/workspace.rs`
|
|
(post-pd.1 location ~lines 425-430 — verify with grep). Includes the
|
|
doc-comment paragraph immediately above.
|
|
|
|
- [ ] **Step 3: Delete `load_prelude` fn**
|
|
|
|
Delete the fn block (post-pd.1 location ~lines 432-439 — verify with
|
|
grep). Includes the doc-comment paragraph immediately above.
|
|
|
|
- [ ] **Step 4: Delete `load_workspace_with` shim fn**
|
|
|
|
Delete the entire fn block (post-pd.1 location ~lines 525-561 — verify
|
|
with grep). Includes the doc-comment block at lines 525-540 (the pd.1
|
|
shim documentation).
|
|
|
|
- [ ] **Step 5: Delete the now-orphaned `load_one` fn**
|
|
|
|
Find `#[cfg(test)] fn load_one` in `workspace.rs` (~line 1598-1611
|
|
per recon). Delete the entire fn including its `#[cfg(test)]`
|
|
attribute and any preceding doc-comment.
|
|
|
|
- [ ] **Step 6: Delete the now-orphaned `crate::load_module` test-import**
|
|
|
|
Find the `#[cfg(test)] use crate::load_module;` line at the top of
|
|
`workspace.rs` (added in pd.1.5). Delete it — it has no remaining
|
|
test consumer because the migrations in Task 4 Step 3 use
|
|
`crate::load_module` via fully-qualified path inside the test body, not
|
|
via top-level import.
|
|
|
|
If a test added in Task 4 Step 3 still uses an unqualified `load_module`,
|
|
either re-add the import (gated `#[cfg(test)]`) or change the test to
|
|
fully-qualified `crate::load_module`. Prefer the latter for
|
|
locality.
|
|
|
|
- [ ] **Step 7: Run the full workspace test suite**
|
|
|
|
Run: `cargo test --workspace`
|
|
Expected: all tests green. If a compile error surfaces a missed
|
|
callsite, find it via the error's file:line, re-migrate.
|
|
|
|
- [ ] **Step 8: Verify zero literal-`"prelude"` strings in `core/src/`**
|
|
|
|
Run: `git grep '"prelude"' crates/ailang-core/src/`
|
|
Expected: NO matches. (The variant declaration at line 321-326 uses
|
|
no literal because the `name` field is `String` — it carries no
|
|
hardcoded string.)
|
|
|
|
If any match appears, identify what it is and decide:
|
|
- Test fixture string (in `mod tests`) — fine to keep, it's not
|
|
production code; verify it's inside `#[cfg(test)]`.
|
|
- Production-code reference — STOP and find why; this violates the
|
|
milestone's primary acceptance criterion.
|
|
|
|
- [ ] **Step 9: Update core's module rustdoc**
|
|
|
|
Edit `crates/ailang-core/src/workspace.rs:1-32`. The pd.1 framing
|
|
("the pd.1 single core entry point is `load_workspace_with`") is now
|
|
stale. Replace with:
|
|
|
|
```rust
|
|
//! Workspace loading and per-workspace validation.
|
|
//!
|
|
//! Two phases, exposed as separate public fns so callers can compose
|
|
//! them around an injection point (e.g. surface's prelude inject):
|
|
//!
|
|
//! - [`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.
|
|
//!
|
|
//! 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.
|
|
//!
|
|
//! 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`.
|
|
//!
|
|
//! [`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.
|
|
```
|
|
|
|
- [ ] **Step 10: Update `lib.rs` rustdoc references**
|
|
|
|
Edit `crates/ailang-core/src/lib.rs`. Three sites (per recon):
|
|
|
|
- Lines 16-21 (module rustdoc preamble) — same shift in framing as
|
|
workspace.rs above. Reword to name the two-phase composition and
|
|
point at `ailang_surface::load_workspace` for the public entry.
|
|
- Lines 50-51 (further module rustdoc) — replace
|
|
`workspace::load_workspace_with` reference with the appropriate
|
|
successor (`load_modules_with` for "transitive loader" semantics or
|
|
`build_workspace` for "workspace assembly" — read the surrounding
|
|
paragraph to pick).
|
|
- Line 120 (doc-comment on `pub fn load_module`) — the
|
|
`for the transitive closure use workspace::load_workspace_with`
|
|
hint becomes
|
|
`for the transitive closure use [`ailang_surface::load_workspace`]
|
|
(which composes `workspace::load_modules_with` + a prelude inject +
|
|
`workspace::build_workspace`)`.
|
|
|
|
These are mechanical reword edits; no semantic change.
|
|
|
|
- [ ] **Step 11: Re-run cargo test --workspace + cargo doc**
|
|
|
|
Run: `cargo test --workspace && cargo doc --no-deps -p ailang-core -p ailang-surface 2>&1 | grep -E '(warning|error)' | head -20`
|
|
Expected: tests green; doc warnings either nil or unchanged from the
|
|
pre-pd.2 baseline (16 pre-existing warnings per the roadmap; pd.2
|
|
should not add new ones).
|
|
|
|
---
|
|
|
|
### Task 6: Bench regressions + final acceptance
|
|
|
|
**Files:** none directly.
|
|
|
|
- [ ] **Step 1: Run regression baselines**
|
|
|
|
Run: `python bench/check.py && python bench/compile_check.py && python bench/cross_lang.py`
|
|
Expected: all three exit 0. pd.2 changes are inside the
|
|
workspace-loader pipeline only; no codegen, no runtime, no
|
|
typecheck-result deltas.
|
|
|
|
If any baseline regresses, investigate before declaring DONE — the
|
|
shape of the change should not move bench numbers.
|
|
|
|
- [ ] **Step 2: Confirm pd.2 acceptance**
|
|
|
|
Per the parent spec (§Acceptance criteria), pd.2's portion is:
|
|
- Item 3 (`git grep '"prelude"' crates/ailang-core/src/` returns no
|
|
matches) — verified at Task 5 Step 8.
|
|
- Item 4 (`cargo test --workspace` green, including the new pins) —
|
|
verified at Task 5 Step 7 + Task 6 Step 1.
|
|
- Item 5 (bench baselines green) — verified at Task 6 Step 1.
|
|
|
|
Items 1, 2, 6, 7 belong to pd.3 (delete `prelude.ail.json`, the
|
|
`include_str!` audit, the carve_out_inventory + form-a §C4 (b)
|
|
spec edits, and the CLAUDE.md / DESIGN.md spot-check).
|
|
|
|
---
|
|
|
|
## Out of scope for pd.2
|
|
|
|
The following spec items are deferred to pd.3 and MUST NOT be touched
|
|
in pd.2:
|
|
|
|
- Deleting `examples/prelude.ail.json`. The cross-form-identity
|
|
preflight test in Task 2 explicitly depends on the JSON file being
|
|
present.
|
|
- Deleting the `prelude_ail_and_json_parse_to_identical_module` test
|
|
fn from `prelude_module_hash_pin.rs` (pd.3 work, after the JSON file
|
|
is gone).
|
|
- Deleting the defensive `include_str!` of `prelude.ail.json` in
|
|
`crates/ail/src/main.rs:472-484` (pd.3 work).
|
|
- Updating `crates/ailang-core/tests/carve_out_inventory.rs` to reflect
|
|
§C4 (b) becoming empty (pd.3 work).
|
|
- Updating `docs/specs/0025-form-a-default-authoring.md` §C4 (b)
|
|
(pd.3 work).
|
|
- Creating `prelude_decouple_carve_out_pin.rs` (pd.3 work — asserts
|
|
the JSON file does not exist; pd.2-time the file IS still present).
|
|
|
|
A pd.2 implementer about to edit any of the above MUST stop and
|
|
re-read the parent spec's iteration sketch.
|