From 61167a4ef0ade5dd65dadfbdde1a44b65c6693ca Mon Sep 17 00:00:00 2001 From: Brummel Date: Thu, 14 May 2026 12:05:56 +0200 Subject: [PATCH] =?UTF-8?q?plan:=20pd.1=20core=20API=20split=20=E2=80=94?= =?UTF-8?q?=20load=5Fmodules=5Fwith=20+=20build=5Fworkspace=20+=20implicit?= =?UTF-8?q?=5Fimports=20threading?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 6 tasks, scoped to crates/ailang-core/src/workspace.rs only. Surface unchanged in pd.1 (load_workspace_with survives as a thin shim composed of the new fns + prelude inject). pd.2 will rewire surface to call the new API directly and retire the shim. Plan-recon flagged two ambiguities in the spec's iter-sketch vs. Components wording; resolved here in favour of the shim approach so all existing tests stay green without migration. ~12 in-tree validate_canonical_type_names callers add &["prelude"] as the new second arg. --- docs/plans/2026-05-14-iter-pd.1.md | 804 +++++++++++++++++++++++++++++ 1 file changed, 804 insertions(+) create mode 100644 docs/plans/2026-05-14-iter-pd.1.md diff --git a/docs/plans/2026-05-14-iter-pd.1.md b/docs/plans/2026-05-14-iter-pd.1.md new file mode 100644 index 0000000..86e5b5c --- /dev/null +++ b/docs/plans/2026-05-14-iter-pd.1.md @@ -0,0 +1,804 @@ +# pd.1 — core API split (load_modules_with + build_workspace) — Implementation Plan + +> **Parent spec:** `docs/specs/2026-05-14-prelude-decouple.md` +> +> **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement` +> to run this plan. Steps use `- [ ]` checkboxes for tracking. + +**Goal:** Introduce two new public fns in `crates/ailang-core/src/workspace.rs` +(`load_modules_with` + `build_workspace`), thread `implicit_imports: &[&str]` +through `validate_canonical_type_names`, and replace the hardcoded `"prelude"` +fallbacks in `check_class_ref` and `check_type_con_name` with the parameterised +walk. `load_workspace_with` survives pd.1 as a thin compatibility shim composed +of the new fns + the still-in-place prelude inject; pd.2 rewires surface to use +the new API directly and removes the shim. + +**Architecture:** core's existing `load_workspace_with` is the single block +that today does DFS + reserved-name + inject + 3-stage validation. pd.1 carves +out `load_modules_with` (DFS only) and `build_workspace` (validation + +registry) as new public fns; the surviving `load_workspace_with` becomes a +3-line composition `load_modules_with → reserved-name + inject → build_workspace`. +core's `src/` literal-`"prelude"` strings drop from 8 to 4 (the inject step's +two and `WorkspaceLoadError::ReservedModuleName`'s string-arg site stay +because the shim still holds them; the four diagnostic-fallback occurrences +on lines 1084 / 1086 / 1148 / 1150 are removed by the threading). Surface +and downstream tests are unchanged in pd.1. + +**Tech Stack:** `ailang-core` (workspace.rs only). No surface, codegen, check, +or CLI changes. + +--- + +**Files this plan creates or modifies:** + +- Modify: `crates/ailang-core/src/workspace.rs:413-528` — extract DFS pre-amble + into `load_modules_with` (lines 462-491), extract validation pipeline into + `build_workspace` (lines 506-528), refit `load_workspace_with` as a 3-line + shim. `load_prelude` (425-428) and `PRELUDE_JSON` (417-419) stay in pd.1 + (still consumed by the shim's inject step); they leave in pd.2. +- Modify: `crates/ailang-core/src/workspace.rs:925-1097` — add + `implicit_imports: &[&str]` parameter to `validate_canonical_type_names` + (signature at line 938-940); thread to `check_class_ref` (line 1041-1046) + and `check_type_con_name` (line 1101-1106); replace literal-`"prelude"` + fallback blocks at lines 1084-1091 and 1148-1155 with the iter form. +- Modify: `crates/ailang-core/src/workspace.rs:437-439` — retire `pub fn + load_workspace`. Zero production callers; the rustdoc example at lines + 22-31 referencing it gets rewritten to use the shim. +- Modify: `crates/ailang-core/src/workspace.rs:1-32` — module rustdoc. + Update the example block (lines 22-31) and the "single entry point" + wording (line 8) to reflect the shim shape. +- Modify: `crates/ailang-core/src/workspace.rs` `#[cfg(test)] mod tests` + block (1583-2749) — direct callers of `validate_canonical_type_names` + add `&["prelude"]` as second arg (~12 tests). Direct callers of + `load_workspace`, `load_workspace_with`, `build_registry` are + unchanged (the shim + the unchanged validators preserve the public + signature for the latter two; the legacy `load_workspace` wrapper has + no in-tree test caller per recon). +- Test: `crates/ailang-core/src/workspace.rs` mod tests — three new + `#[test]` fns to pin the new API: (a) `load_modules_with` returns + modules WITHOUT the prelude (proves it is loader-only), (b) + `build_workspace` accepts a pre-assembled modules-map and runs the + three-stage validation, (c) the shim composition `load_workspace_with` + produces an output byte-identical to today (regression pin against + the refactor). + +--- + +### Task 1: Add `pub fn load_modules_with` (DFS extraction) + +**Files:** +- Modify: `crates/ailang-core/src/workspace.rs` — add new fn between current + `load_workspace_with` and the prelude-related helpers (anywhere in lines + 430-530 is fine; suggest just above `load_workspace_with`). + +- [ ] **Step 1: Write the failing test** in the `#[cfg(test)] mod tests` + block at the end of `workspace.rs`. + +```rust +#[test] +fn load_modules_with_returns_modules_without_prelude() { + let dir = tempfile::tempdir().unwrap(); + write_module(dir.path(), "main", &[]); + let entry = dir.path().join("main.ail.json"); + + let (entry_name, root_dir, modules) = + load_modules_with(&entry, load_one).expect("load"); + + assert_eq!(entry_name, "main"); + assert_eq!(root_dir, dir.path().to_path_buf()); + assert!( + !modules.contains_key("prelude"), + "load_modules_with must not auto-inject prelude (that is build_workspace's caller's job)" + ); + assert!(modules.contains_key("main")); +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cargo test -p ailang-core load_modules_with_returns_modules_without_prelude` +Expected: FAIL with "cannot find function `load_modules_with` in this scope" + +- [ ] **Step 3: Add the new fn** + +Place this above `load_workspace_with` (anywhere in the 430-530 range works; +keep `PRELUDE_JSON` and `load_prelude` adjacent to `load_workspace_with` for +locality): + +```rust +/// pd.1: extract the DFS pre-amble of `load_workspace_with` into a public +/// loader-only fn. Returns `(entry_name, root_dir, modules)` with NO prelude +/// injected and NO validation run. The caller is responsible for injecting +/// any implicit modules (e.g. prelude) and then handing the result to +/// `build_workspace` for validation + registry construction. +/// +/// Surface composes this with a prelude-injection step in pd.2; pd.1 keeps +/// the shim `load_workspace_with` callable so surface is unchanged. +pub fn load_modules_with( + entry_path: &Path, + loader: F, +) -> Result<(String, PathBuf, BTreeMap), WorkspaceLoadError> +where + F: Fn(&Path) -> Result + Copy, +{ + let entry_path = entry_path.to_path_buf(); + let root_dir = entry_path + .parent() + .map(Path::to_path_buf) + .unwrap_or_else(|| PathBuf::from(".")); + + let entry_module = loader(&entry_path)?; + let expected_entry_name = module_name_from_path(&entry_path); + if entry_module.name != expected_entry_name { + return Err(WorkspaceLoadError::ModuleNameMismatch { + name_in_file: entry_module.name.clone(), + name_from_path: expected_entry_name, + }); + } + + let entry_name = entry_module.name.clone(); + + let mut modules: BTreeMap = BTreeMap::new(); + let mut visiting: Vec = Vec::new(); + let mut visiting_set: HashSet = HashSet::new(); + + visit( + entry_module, + &root_dir, + &mut modules, + &mut visiting, + &mut visiting_set, + loader, + )?; + + Ok((entry_name, root_dir, modules)) +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cargo test -p ailang-core load_modules_with_returns_modules_without_prelude` +Expected: PASS. + +- [ ] **Step 5: Run full core test suite to verify no regression** + +Run: `cargo test -p ailang-core` +Expected: all existing tests pass (the new fn is additive; nothing else +calls it yet). + +--- + +### Task 2: Add `pub fn build_workspace` (validation pipeline extraction) + +**Files:** +- Modify: `crates/ailang-core/src/workspace.rs` — add new fn next to + `load_modules_with`. The `implicit_imports: &[&str]` parameter is + declared on `build_workspace` AND threaded into the call to + `validate_canonical_type_names` (which gets the parameter in Task 3). + +- [ ] **Step 1: Write the failing test** + +Place in the `#[cfg(test)] mod tests` block: + +```rust +#[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 (mirrors what surface will do in pd.2). + let prelude = load_prelude(); + 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")); +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cargo test -p ailang-core build_workspace_accepts_assembled_modules_and_runs_validation` +Expected: FAIL with "cannot find function `build_workspace` in this scope". + +- [ ] **Step 3: Add the new fn** + +Place adjacent to `load_modules_with`: + +```rust +/// pd.1: extract the validation + registry-construction tail of +/// `load_workspace_with` into a public fn that operates on a pre-assembled +/// modules map. The caller (typically `surface::load_workspace`, post-pd.2) +/// is responsible for injecting any implicit modules (e.g. prelude) into +/// `modules` BEFORE calling this fn, AND for naming those implicit modules +/// in `implicit_imports` so the diagnostic helpers (`check_class_ref`, +/// `check_type_con_name`) include them as fallback candidates. +/// +/// Runs the three-stage pipeline: +/// 1. `validate_canonical_type_names` (with `implicit_imports`) +/// 2. `validate_classdefs` (no `implicit_imports` — does not consume it) +/// 3. `build_registry` +pub fn build_workspace( + entry_name: String, + root_dir: PathBuf, + modules: BTreeMap, + implicit_imports: &[&str], +) -> Result { + validate_canonical_type_names(&modules, implicit_imports)?; + validate_classdefs(&modules)?; + let registry = build_registry(&modules)?; + + Ok(Workspace { + entry: entry_name, + modules, + root_dir, + registry, + }) +} +``` + +- [ ] **Step 4: Run test to verify it fails (with a different error)** + +Run: `cargo test -p ailang-core build_workspace_accepts_assembled_modules_and_runs_validation` +Expected: FAIL with "this function takes 1 argument but 2 arguments were +supplied" (because `validate_canonical_type_names` still has the old +single-arg signature — Task 3 adds the parameter). The test compiles +through `build_workspace` but fails at the inner call site. + +If the failure mode is different (e.g. trait bound), inspect — the +compile-error message must point at `validate_canonical_type_names`'s +arity, not at anything else. + +- [ ] **Step 5: Verify Task 2 is wired (deferring green to Task 3)** + +This task ends RED-on-purpose. Task 3 lands the parameter on +`validate_canonical_type_names` and turns Task 2's test green at the +end of Task 3. + +--- + +### Task 3: Thread `implicit_imports: &[&str]` through `validate_canonical_type_names`, `check_class_ref`, `check_type_con_name` + +**Files:** +- Modify: `crates/ailang-core/src/workspace.rs:925-1097` — three signatures + + ~6 internal call sites in `validate_canonical_type_names` + two + fallback-iter rewrites in the diagnostic helpers. + +- [ ] **Step 1: Write the failing test for the new threading semantics** + +Place in `#[cfg(test)] mod tests`. The test exercises the parameterisation +end-to-end: with `implicit_imports = &["prelude"]` the fallback fires +(today's behaviour); with `implicit_imports = &[]` the same fixture +returns `BareCrossModuleClassRef` with NO `prelude.X` candidate. + +```rust +#[test] +fn implicit_imports_arg_drives_prelude_fallback_in_diagnostics() { + // Build a 2-module workspace where module "main" has a bare class-ref + // "Eq" that resolves only via prelude. + let mut modules = BTreeMap::new(); + modules.insert("main".to_string(), module_with_bare_classref("main", "Eq")); + modules.insert( + "prelude".to_string(), + module_with_class_def("prelude", "Eq", "a"), + ); + + // With implicit_imports = &["prelude"] — prelude.Eq appears in candidates. + let err_with = validate_canonical_type_names(&modules, &["prelude"]).unwrap_err(); + match err_with { + WorkspaceLoadError::BareCrossModuleClassRef { ref candidates, .. } => { + assert!( + candidates.iter().any(|c| c == "prelude.Eq"), + "expected prelude.Eq in candidates, got {:?}", candidates + ); + } + other => panic!("expected BareCrossModuleClassRef, got {other:?}"), + } + + // With implicit_imports = &[] — prelude.Eq does NOT appear. + let err_without = validate_canonical_type_names(&modules, &[]).unwrap_err(); + match err_without { + WorkspaceLoadError::BareCrossModuleClassRef { ref candidates, .. } => { + assert!( + !candidates.iter().any(|c| c == "prelude.Eq"), + "expected NO prelude.Eq in candidates, got {:?}", candidates + ); + } + other => panic!("expected BareCrossModuleClassRef, got {other:?}"), + } +} +``` + +The helpers `module_with_bare_classref` and `module_with_class_def` may +need to be added if no in-tree analogue exists. Keep them in the same +`mod tests` block. Suggest: + +```rust +fn module_with_bare_classref(name: &str, class_ref: &str) -> Module { + Module { + name: name.to_string(), + imports: vec![], + defs: vec![Def::Instance(InstanceDef { + class: class_ref.to_string(), + ty: Type::Con { name: format!("{name}.Foo"), args: vec![] }, + methods: vec![], + doc: None, + })], + } +} + +fn module_with_class_def(name: &str, class_name: &str, param: &str) -> Module { + Module { + name: name.to_string(), + imports: vec![], + defs: vec![Def::Class(ClassDef { + name: class_name.to_string(), + param: param.to_string(), + superclasses: vec![], + methods: vec![], + doc: None, + })], + } +} +``` + +If similar helpers already exist in the test block, use them and skip the +helper additions. + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cargo test -p ailang-core implicit_imports_arg_drives_prelude_fallback_in_diagnostics` +Expected: FAIL with "this function takes 1 argument but 2 arguments were +supplied" — `validate_canonical_type_names` still has the old single-arg +signature. + +- [ ] **Step 3: Add the parameter to `validate_canonical_type_names`** + +Edit the signature at line 938-940. Old: + +```rust +pub(crate) fn validate_canonical_type_names( + modules: &BTreeMap, +) -> Result<(), WorkspaceLoadError> { +``` + +New: + +```rust +pub(crate) fn validate_canonical_type_names( + modules: &BTreeMap, + implicit_imports: &[&str], +) -> Result<(), WorkspaceLoadError> { +``` + +- [ ] **Step 4: Add the parameter to `check_class_ref` and `check_type_con_name`** + +`check_class_ref` (signature at lines 1041-1046). Old: + +```rust +fn check_class_ref( + class_ref: &str, + owning_module: &str, + local_classes: &BTreeMap>, + import_names: &[&str], +) -> Result<(), WorkspaceLoadError> { +``` + +New: + +```rust +fn check_class_ref( + class_ref: &str, + owning_module: &str, + local_classes: &BTreeMap>, + import_names: &[&str], + implicit_imports: &[&str], +) -> Result<(), WorkspaceLoadError> { +``` + +`check_type_con_name` (signature at lines 1101-1106). Old: + +```rust +fn check_type_con_name( + name: &str, + owning_module: &str, + local_types: &BTreeMap>, + import_names: &[&str], +) -> Result<(), WorkspaceLoadError> { +``` + +New: + +```rust +fn check_type_con_name( + name: &str, + owning_module: &str, + local_types: &BTreeMap>, + import_names: &[&str], + implicit_imports: &[&str], +) -> Result<(), WorkspaceLoadError> { +``` + +- [ ] **Step 5: Replace the literal-`"prelude"` fallback in `check_class_ref`** + +At lines 1084-1091, replace: + +```rust + if !import_names.contains(&"prelude") + && local_classes + .get("prelude") + .map(|s| s.contains(class_ref)) + .unwrap_or(false) + { + candidates.push(format!("prelude.{class_ref}")); + } +``` + +with: + +```rust + for implicit in implicit_imports { + if import_names.contains(implicit) { + continue; // already added above + } + if local_classes + .get(*implicit) + .map(|s| s.contains(class_ref)) + .unwrap_or(false) + { + candidates.push(format!("{implicit}.{class_ref}")); + } + } +``` + +- [ ] **Step 6: Replace the literal-`"prelude"` fallback in `check_type_con_name`** + +At lines 1148-1155, replace: + +```rust + if !import_names.contains(&"prelude") + && local_types + .get("prelude") + .map(|s| s.contains(name)) + .unwrap_or(false) + { + candidates.push(format!("prelude.{name}")); + } +``` + +with: + +```rust + for implicit in implicit_imports { + if import_names.contains(implicit) { + continue; // already added above + } + if local_types + .get(*implicit) + .map(|s| s.contains(name)) + .unwrap_or(false) + { + candidates.push(format!("{implicit}.{name}")); + } + } +``` + +- [ ] **Step 7: Update the call sites of `check_class_ref` and `check_type_con_name` inside `validate_canonical_type_names`** + +Six internal call sites per recon (lines 981, 988, 998, 1002, 1007, 1015). +Each gets `implicit_imports` forwarded as the new last arg. The exact +shape of each call site varies — the change is mechanical: add +`implicit_imports` as the trailing positional arg. + +Example pattern (the actual lines may differ slightly; replicate for all +six call sites): + +Old: +```rust +check_type_con_name(name, owning, local_types, &import_names)?; +``` + +New: +```rust +check_type_con_name(name, owning, local_types, &import_names, implicit_imports)?; +``` + +- [ ] **Step 8: Run the new test from Step 1** + +Run: `cargo test -p ailang-core implicit_imports_arg_drives_prelude_fallback_in_diagnostics` +Expected: PASS — both the with-prelude and without-prelude branches. + +- [ ] **Step 9: Run Task 2's test** + +Run: `cargo test -p ailang-core build_workspace_accepts_assembled_modules_and_runs_validation` +Expected: PASS — the inner `validate_canonical_type_names(&modules, implicit_imports)` call now compiles. + +- [ ] **Step 10: Migrate existing direct callers of `validate_canonical_type_names`** + +Recon listed ~12 tests in the `mod tests` block that call +`validate_canonical_type_names(&modules)` directly (lines 1981, 2000, +2036, 2076, 2108, 2143, 2179, 2584, 2625, 2678, 2720; plus any siblings +in 2393-2483 not enumerated). Each gets a mechanical rewrite: + +Old: +```rust +validate_canonical_type_names(&modules) +``` + +New: +```rust +validate_canonical_type_names(&modules, &["prelude"]) +``` + +The `&["prelude"]` matches today's hardcoded behaviour; assertions on +`prelude.X` candidate strings continue to pass. + +- [ ] **Step 11: Run the full core test suite** + +Run: `cargo test -p ailang-core` +Expected: all tests pass. If a missed call site breaks compilation, find it, +add `&["prelude"]`, re-run. + +--- + +### Task 4: Refit `load_workspace_with` as a thin compatibility shim + +**Files:** +- Modify: `crates/ailang-core/src/workspace.rs:441-529` — replace the body + of `load_workspace_with` with a 3-line composition. The doc-comment + updates to reflect the shim role. + +- [ ] **Step 1: Write the regression-pin test for the shim** + +The shim must produce a `Workspace` byte-identical to what today's +`load_workspace_with` produces for the same inputs. Pin one +representative case in the `#[cfg(test)] mod tests` block: + +```rust +#[test] +fn load_workspace_with_shim_preserves_today_semantics() { + let dir = tempfile::tempdir().unwrap(); + write_module(dir.path(), "main", &[]); + let entry = dir.path().join("main.ail.json"); + + let ws = load_workspace_with(&entry, load_one).expect("load"); + + assert_eq!(ws.entry, "main"); + assert!(ws.modules.contains_key("main")); + assert!( + ws.modules.contains_key("prelude"), + "shim must still inject prelude" + ); + // Module hash stability: the prelude module hash must equal what + // today's load_prelude() produces. + assert_eq!( + module_hash(&ws.modules["prelude"]), + module_hash(&load_prelude()), + ); +} +``` + +- [ ] **Step 2: Run test to verify it passes against today's + implementation** + +Run: `cargo test -p ailang-core load_workspace_with_shim_preserves_today_semantics` +Expected: PASS — this is the baseline before the shim refit, asserting the +test is correctly written against today's behaviour. + +- [ ] **Step 3: Refit `load_workspace_with`** + +Replace the body at lines 461-528 (everything inside the fn after the +signature). Old shape (~70 lines): the inline DFS + inject + validation +block. New shape: + +```rust +pub fn load_workspace_with( + entry_path: &Path, + loader: F, +) -> Result +where + F: Fn(&Path) -> Result + Copy, +{ + // pd.1: thin compatibility shim composed of the new + // `load_modules_with` + `build_workspace` plus the prelude inject. + // pd.2 will rewire `surface::load_workspace` to call the new fns + // directly and remove this shim. + let (entry_name, root_dir, mut modules) = load_modules_with(entry_path, loader)?; + + // Iter 23.1 (preserved through the shim): the prelude module is + // implicitly part of every workspace. A user module of the same name + // would shadow; reject explicitly. + if modules.contains_key("prelude") { + return Err(WorkspaceLoadError::ReservedModuleName { + name: "prelude".to_string(), + }); + } + modules.insert("prelude".to_string(), load_prelude()); + + build_workspace(entry_name, root_dir, modules, &["prelude"]) +} +``` + +The doc-comment above the fn (lines 441-454) updates to reflect the shim +role: + +```rust +/// pd.1: thin compatibility shim composed of `load_modules_with` + +/// `build_workspace` plus the prelude inject. Preserves today's +/// `load_workspace_with` semantics so surface (`crates/ailang-surface/src/loader.rs`) +/// stays unchanged in pd.1. pd.2 will rewire surface to call +/// `load_modules_with` and `build_workspace` directly and retire this +/// shim along with the embedded `PRELUDE_JSON` constant. +/// +/// Algorithm: DFS over `imports` via `load_modules_with`, then inject +/// the prelude (rejecting collision with a user module of the same +/// name), then validate + build the typeclass registry via +/// `build_workspace`. +``` + +- [ ] **Step 4: Run the regression-pin test** + +Run: `cargo test -p ailang-core load_workspace_with_shim_preserves_today_semantics` +Expected: PASS — the shim produces the same `Workspace`. + +- [ ] **Step 5: Run the full core test suite** + +Run: `cargo test -p ailang-core` +Expected: all tests pass. The shim is a pure refactor; every existing +test that called `load_workspace_with` continues to see the same +behaviour. + +--- + +### Task 5: Retire `pub fn load_workspace` (the JSON-only legacy wrapper) + +**Files:** +- Modify: `crates/ailang-core/src/workspace.rs:437-439` — delete the fn. +- Modify: `crates/ailang-core/src/workspace.rs:1-32` — update the + module-level rustdoc. +- Modify: `crates/ailang-core/src/lib.rs` — remove the re-export of + `load_workspace` if present. + +- [ ] **Step 1: Verify no in-tree caller** + +Run: `git grep -n 'ailang_core::load_workspace\b' -- '*.rs' '*.toml' | grep -v 'load_workspace_with'` +Expected: no production hits. Doc-comment hits in `crates/ailang-core/src/workspace.rs:1-32` +and other `//!` / `///` references are acceptable; they'll be cleaned up +in Step 4. + +If a real caller appears, STOP and report — the spec assumed zero +callers per the grounding-check report. + +- [ ] **Step 2: Delete the fn** + +Remove lines 437-439: + +```rust +pub fn load_workspace(entry_path: &Path) -> Result { + load_workspace_with(entry_path, load_one) +} +``` + +- [ ] **Step 3: Remove the re-export from `lib.rs`** + +Inspect `crates/ailang-core/src/lib.rs`. If it contains +`pub use workspace::load_workspace;` or similar, delete that line. If +the re-export is `pub use workspace::*;`, no edit needed; if it names +`load_workspace` explicitly, delete the entry. + +- [ ] **Step 4: Update the module rustdoc** + +`crates/ailang-core/src/workspace.rs:1-32` — the example block calls +`ailang_core::load_workspace(...)`. Rewrite the example to use the +shim: + +Old (approximate — verify with the file): +```rust +//! ```no_run +//! use ailang_core::load_workspace; +//! let ws = load_workspace(...).unwrap(); +//! ``` +``` + +New: +```rust +//! ```no_run +//! // pd.2 will move the public entry point to ailang-surface; +//! // pd.1 keeps `load_workspace_with` as the single core entry point. +//! use ailang_core::workspace::{load_workspace_with, load_one}; +//! let ws = load_workspace_with(/* path */&std::path::PathBuf::from("examples/foo.ail.json"), load_one).unwrap(); +//! ``` +``` + +The "single entry point" wording at line 8 (if present) updates +correspondingly. + +- [ ] **Step 5: Run cargo check + tests** + +Run: `cargo check -p ailang-core && cargo test -p ailang-core` +Expected: clean compile, all tests green. The doc-test in the rustdoc +example may or may not run depending on the `no_run` attribute; if it +runs and fails, mark it `no_run` or `ignore`. + +--- + +### Task 6: Full workspace test suite + bench regressions + +**Files:** none directly — verification step. + +- [ ] **Step 1: Run the full workspace test suite** + +Run: `cargo test --workspace` +Expected: all tests green. `ailang-surface`, `ailang-check`, `ailang-codegen`, +and `ail` tests are unaffected because the shim preserves the public +behaviour. + +If a test fails, the most likely cause is a missed call site for +`validate_canonical_type_names` outside the `mod tests` block (Task 3 +Step 10). Find it via `git grep -n 'validate_canonical_type_names('` +and migrate. + +- [ ] **Step 2: Run the regression baselines** + +Run: `python bench/check.py && python bench/compile_check.py && python bench/cross_lang.py` +Expected: all three exit code 0. None of pd.1's changes alter compiled +binary output or check semantics; baselines should be unchanged. + +- [ ] **Step 3: Confirm core's `src/` literal-`"prelude"` count dropped from 8 to 4** + +Run: `git grep -c '"prelude"' crates/ailang-core/src/workspace.rs` +Expected: count drops from the pre-pd.1 baseline (8) to 4. The remaining +4 are: line 498 (`if modules.contains_key("prelude")` in the shim), +line 500 (`name: "prelude".to_string()` in the shim's err return), line +504 (`modules.insert("prelude".to_string(), load_prelude())` in the +shim), and any survivor in test code. The 4 diagnostic-fallback +occurrences (1084 / 1086 / 1148 / 1150) MUST be gone. + +If any of the four diagnostic occurrences survives, Task 3 Steps 5-6 +were incomplete — find and remove. + +- [ ] **Step 4: Confirm pd.1 acceptance criterion** + +Per the spec (§Acceptance criteria, items 4 + 5 are pd.1's portion; +items 1, 2, 3, 6, 7 belong to pd.2/pd.3): + +- `cargo test --workspace` green ✓ (Step 1) +- `bench/check.py`, `bench/compile_check.py`, `bench/cross_lang.py` + green at unchanged baselines ✓ (Step 2) + +The remaining acceptance items (delete `prelude.ail.json`, zero +`include_str!`-of-prelude.ail.json, zero literal-`"prelude"` in +`core/src/`, carve_out_inventory update, form-a §C4 (b) update) +land in pd.2 + pd.3. + +--- + +## Out of scope for pd.1 + +The following spec items are deferred to pd.2 / pd.3 and MUST NOT be +touched in pd.1: + +- Adding `PRELUDE_AIL: &str = include_str!("../../../examples/prelude.ail")` + or `parse_prelude` to `crates/ailang-surface/src/loader.rs` (pd.2). +- Rewriting `surface::load_workspace` to call the new core fns directly + (pd.2) — surface stays calling `load_workspace_with` via the shim in + pd.1. +- Removing the prelude embed (`PRELUDE_JSON` const + `load_prelude` fn) + from core (pd.2 — once surface owns the embed, the shim can drop them). +- The cross-form-identity preflight test (pd.2 — gates the embed swap). +- Deleting `examples/prelude.ail.json` (pd.3). +- Deleting the defensive include in `crates/ail/src/main.rs:472-484` (pd.3). +- Updating `crates/ailang-core/tests/carve_out_inventory.rs` (pd.3). +- Updating `docs/specs/2026-05-13-form-a-default-authoring.md` §C4 (b) + (pd.3). + +A pd.1 implementer who finds themselves about to edit any of the above +files should STOP and re-read the parent spec's iteration sketch — the +work belongs to a later iter.