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.
805 lines
28 KiB
Markdown
805 lines
28 KiB
Markdown
# 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`.
|
|
|
|
```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<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:
|
|
|
|
```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<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.
|
|
|
|
```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<String, Module>,
|
|
) -> Result<(), WorkspaceLoadError> {
|
|
```
|
|
|
|
New:
|
|
|
|
```rust
|
|
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:
|
|
|
|
```rust
|
|
fn check_class_ref(
|
|
class_ref: &str,
|
|
owning_module: &str,
|
|
local_classes: &BTreeMap<String, BTreeSet<String>>,
|
|
import_names: &[&str],
|
|
) -> Result<(), WorkspaceLoadError> {
|
|
```
|
|
|
|
New:
|
|
|
|
```rust
|
|
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:
|
|
|
|
```rust
|
|
fn check_type_con_name(
|
|
name: &str,
|
|
owning_module: &str,
|
|
local_types: &BTreeMap<String, BTreeSet<String>>,
|
|
import_names: &[&str],
|
|
) -> Result<(), WorkspaceLoadError> {
|
|
```
|
|
|
|
New:
|
|
|
|
```rust
|
|
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:
|
|
|
|
```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<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:
|
|
|
|
```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<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):
|
|
```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/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.
|