Files
AILang/docs/plans/0064-iter-pd.1.md
T
Brummel 832375f2ac convention: counter-prefix file naming across docs/specs/, docs/plans/, design/contracts/, design/models/
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.
2026-05-28 13:31:31 +02:00

28 KiB

pd.1 — core API split (load_modules_with + build_workspace) — 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: 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<F> 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.

#[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):

/// 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<F>(
    entry_path: &Path,
    loader: F,
) -> Result<(String, PathBuf, BTreeMap<String, Module>), WorkspaceLoadError>
where
    F: Fn(&Path) -> Result<Module, WorkspaceLoadError> + 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<String, Module> = BTreeMap::new();
    let mut visiting: Vec<String> = Vec::new();
    let mut visiting_set: HashSet<String> = 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:

#[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:

/// 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<String, Module>,
    implicit_imports: &[&str],
) -> Result<Workspace, WorkspaceLoadError> {
    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.

#[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:

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:

pub(crate) fn validate_canonical_type_names(
    modules: &BTreeMap<String, Module>,
) -> Result<(), WorkspaceLoadError> {

New:

pub(crate) fn validate_canonical_type_names(
    modules: &BTreeMap<String, Module>,
    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:

fn check_class_ref(
    class_ref: &str,
    owning_module: &str,
    local_classes: &BTreeMap<String, BTreeSet<String>>,
    import_names: &[&str],
) -> Result<(), WorkspaceLoadError> {

New:

fn check_class_ref(
    class_ref: &str,
    owning_module: &str,
    local_classes: &BTreeMap<String, BTreeSet<String>>,
    import_names: &[&str],
    implicit_imports: &[&str],
) -> Result<(), WorkspaceLoadError> {

check_type_con_name (signature at lines 1101-1106). Old:

fn check_type_con_name(
    name: &str,
    owning_module: &str,
    local_types: &BTreeMap<String, BTreeSet<String>>,
    import_names: &[&str],
) -> Result<(), WorkspaceLoadError> {

New:

fn check_type_con_name(
    name: &str,
    owning_module: &str,
    local_types: &BTreeMap<String, BTreeSet<String>>,
    import_names: &[&str],
    implicit_imports: &[&str],
) -> Result<(), WorkspaceLoadError> {
  • Step 5: Replace the literal-"prelude" fallback in check_class_ref

At lines 1084-1091, replace:

    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:

    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:

    if !import_names.contains(&"prelude")
        && local_types
            .get("prelude")
            .map(|s| s.contains(name))
            .unwrap_or(false)
    {
        candidates.push(format!("prelude.{name}"));
    }

with:

    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:

check_type_con_name(name, owning, local_types, &import_names)?;

New:

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:

validate_canonical_type_names(&modules)

New:

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:

#[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:

pub fn load_workspace_with<F>(
    entry_path: &Path,
    loader: F,
) -> Result<Workspace, WorkspaceLoadError>
where
    F: Fn(&Path) -> Result<Module, WorkspaceLoadError> + 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:

/// 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:

pub fn load_workspace(entry_path: &Path) -> Result<Workspace, WorkspaceLoadError> {
    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):

//! ```no_run
//! use ailang_core::load_workspace;
//! let ws = load_workspace(...).unwrap();
//! ```

New:

//! ```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/0025-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.