eba1be8d9d
Second of two iterations delivering spec 0060's mir.3 row (plan docs/plans/0118-mir.3b-marg-mode-and-table-delete.md). lower_to_mir's Term::App arm fills each MArg.mode from the resolved callee's param_modes (the sig = synth_pure(callee) it already holds); codegen's emit_call anon-temp borrow-slot drop gate reads arg.mode instead of re-looking-up the callee's param_modes from the module_def_ail_types table; and — since that gate was the table's only reader — the module_def_ail_types field plus all its construction and threading are deleted. Two refinements to the spec's mir.3b sketch, settled in planning from a focused recon and recorded in spec 0060: 1. module_def_ail_types is deleted in mir.3b, not mir.5. A grep proved self.module_def_ail_types had exactly ONE reader (the anon-temp gate); the own-param drop reads param_modes from the def's own f.ty, not this table. Once the gate moves onto MArg.mode the table is dead, so it is removed now rather than carried as dead code to mir.5. The mir.5 row's "last re-derivation residue" shrinks to the element-type / Term::New (New.elem) work. 2. MArg.mode is filled for App args only; MTerm::Let.mode is not filled. Only App args have a real per-arg mode source (the callee Type::Fn.param_modes). Do args (EffectOpSig has no param modes), Ctor args (no per-field mode), and Recur args (loop binders carry no mode) stay Mode::Owned. Let.mode has no source (ast::Term::Let has no mode field) and no consumer (the let-drop gate reads consume, not Let.mode), so it stays Owned — filling it would invent a value nobody reads. Safety property held: MArg.mode is filled from the same callee fn-type the gate read from the table, so the drop fires identically. For (app RawBuf.get (app RawBuf.set …) 0), RawBuf.get's param 0 is borrow, so args[0].mode = Borrow — exactly the value module_def_ail_types yielded. Confirmed by the anon-temp witness staying green. ParamMode -> Mode conversion: Borrow -> Mode::Borrow; Own and Implicit (the Implicit ≡ Own contract) -> Mode::Owned. The own-param drop keeps reading ParamMode off f.ty (Type/ParamMode imports retained, live readers at lib.rs:1356/1507). Also retires three now-stale doc comments that named the deleted module_def_ail_types field (ailang-check/src/lib.rs, uniqueness.rs, and the codegen_import_map_fallback_pin test doc) — a direct consequence of the deletion, reworded to the current architecture. Verification (orchestrator, post-implement inspect): diff matches the plan (App-arg m_args built before m_callee consumes sig — the benign borrow-order deviation the plan's note flagged); module_def_ail_types grep-clean across all of crates/; cargo build --workspace clean (no unused/dead_code); cargo test --workspace 702 passed / 0 failed / 3 ignored (+1 = the new producer pin app_arg_carries_callee_borrow_mode; no #[ignore] added). The anon-temp drop-correctness witness raw_buf_owned_drop_balances_rc_stats stays live=0, now driven by MArg.mode; the other RC-stats leak pins green; #49 stays #[ignore] (mir.4); #51/#53 build guards green. mir.3 is now complete (3a relocated consume_count + deleted the second uniqueness run; 3b filled the mode annotation + deleted module_def_ail_types). Remaining: mir.4 (#49 StrRep RED->GREEN), mir.5 (element-type/Term::New + ledger).
140 lines
5.7 KiB
Rust
140 lines
5.7 KiB
Rust
//! Pin for the iter-24.3 codegen `import_map`-fallback path
|
|
//! (design/contracts/0013-typeclasses.md, "Cross-module references in
|
|
//! synthesised bodies" invariant 2).
|
|
//!
|
|
//! Property protected: post-mono synthesised body cross-module
|
|
//! references resolve at codegen via the fallback to
|
|
//! `module_user_fns` when the prefix is
|
|
//! NOT in the current module's `import_map`. Specifically, the
|
|
//! synthesised `prelude.print__<UserType>` body references
|
|
//! `<user_module>.show__<UserType>` even though `prelude` does not
|
|
//! import user modules.
|
|
//!
|
|
//! Failure mode this pin catches: a future codegen refactor
|
|
//! tightens `resolve_top_level_fn` or `lower_app`'s cross-module
|
|
//! arm or `synth_with_extras`'s Var arm back to `import_map`-only.
|
|
//! Without this pin, the regression surfaces only at the
|
|
//! `show_user_adt` E2E (which builds + runs a binary, slow to
|
|
//! bisect).
|
|
|
|
use ailang_check::{check_workspace, monomorphise_workspace};
|
|
use ailang_core::ast::{Def, Term};
|
|
use ailang_surface::load_workspace;
|
|
use std::path::PathBuf;
|
|
|
|
fn fixture_path() -> PathBuf {
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
.join("../../examples")
|
|
.join("show_user_adt.ail")
|
|
}
|
|
|
|
#[test]
|
|
fn synthesised_print_uses_user_module_show_via_fallback() {
|
|
// Step 1: workspace loads + typechecks clean.
|
|
let ws = load_workspace(&fixture_path()).expect("workspace loads");
|
|
let diags = check_workspace(&ws);
|
|
assert!(
|
|
diags.is_empty(),
|
|
"typecheck diagnostics in show_user_adt fixture: {diags:?}"
|
|
);
|
|
|
|
// Step 2: mono synthesis produces `prelude.print__<IntBox>` whose
|
|
// body references `show_user_adt.show__<IntBox>` (cross-module).
|
|
let post_mono = monomorphise_workspace(&ws).expect("mono green");
|
|
let prelude_mod = post_mono
|
|
.modules
|
|
.get("prelude")
|
|
.expect("prelude post-mono module present");
|
|
|
|
let print_def = prelude_mod
|
|
.defs
|
|
.iter()
|
|
.find_map(|d| match d {
|
|
Def::Fn(f) if f.name.starts_with("print__") => Some(f),
|
|
_ => None,
|
|
})
|
|
.expect("synthesised print__<UserType> not found in prelude post-mono module");
|
|
|
|
// Step 3: recursively walk `print_def.body` looking for a Var
|
|
// whose name carries the `show_user_adt.` prefix (the cross-
|
|
// module reference invariant 2 protects).
|
|
fn contains_xmod_show_var(t: &Term) -> bool {
|
|
match t {
|
|
Term::Var { name } => {
|
|
name.starts_with("show_user_adt.") && name.contains("show__")
|
|
}
|
|
Term::Let { value, body, .. } => {
|
|
contains_xmod_show_var(value) || contains_xmod_show_var(body)
|
|
}
|
|
Term::LetRec { body, in_term, .. } => {
|
|
contains_xmod_show_var(body) || contains_xmod_show_var(in_term)
|
|
}
|
|
Term::App { callee, args, .. } => {
|
|
contains_xmod_show_var(callee) || args.iter().any(contains_xmod_show_var)
|
|
}
|
|
Term::Do { args, .. } => args.iter().any(contains_xmod_show_var),
|
|
Term::Lam { body, .. } => contains_xmod_show_var(body),
|
|
Term::If { cond, then, else_ } => {
|
|
contains_xmod_show_var(cond)
|
|
|| contains_xmod_show_var(then)
|
|
|| contains_xmod_show_var(else_)
|
|
}
|
|
Term::Match { scrutinee, arms } => {
|
|
contains_xmod_show_var(scrutinee)
|
|
|| arms.iter().any(|a| contains_xmod_show_var(&a.body))
|
|
}
|
|
Term::Ctor { args, .. } => args.iter().any(contains_xmod_show_var),
|
|
Term::Seq { lhs, rhs } => {
|
|
contains_xmod_show_var(lhs) || contains_xmod_show_var(rhs)
|
|
}
|
|
Term::Clone { value } => contains_xmod_show_var(value),
|
|
Term::ReuseAs { source, body } => {
|
|
contains_xmod_show_var(source) || contains_xmod_show_var(body)
|
|
}
|
|
// loop-recur iter 1: a `Term::Loop` cannot itself host a
|
|
// synthesised cross-module reference, but recurse
|
|
// defensively through binder inits / body / recur args.
|
|
Term::Loop { binders, body } => {
|
|
binders.iter().any(|b| contains_xmod_show_var(&b.init))
|
|
|| contains_xmod_show_var(body)
|
|
}
|
|
Term::Recur { args } => args.iter().any(contains_xmod_show_var),
|
|
// prep.2 (kernel-extension-mechanics): recurse through
|
|
// NewArg::Value subterms; type-args do not carry a Var.
|
|
Term::New { args, .. } => args.iter().any(|arg| match arg {
|
|
ailang_core::ast::NewArg::Value(v) => contains_xmod_show_var(v),
|
|
ailang_core::ast::NewArg::Type(_) => false,
|
|
}),
|
|
Term::Lit { .. } => false,
|
|
Term::Intrinsic => false,
|
|
}
|
|
}
|
|
|
|
assert!(
|
|
contains_xmod_show_var(&print_def.body),
|
|
"synthesised print body should contain a `show_user_adt.<suffix>` Var \
|
|
referencing the user-module's show__<IntBox> mono symbol — \
|
|
this is the cross-module reference codegen resolves via the \
|
|
import_map-fallback path. Body: {:?}",
|
|
print_def.body
|
|
);
|
|
|
|
// Step 4: confirm prelude module's `imports` does NOT contain
|
|
// `show_user_adt` — the resolution at codegen time genuinely
|
|
// bypasses the source template's import_map.
|
|
let prelude_src = ws
|
|
.modules
|
|
.get("prelude")
|
|
.expect("prelude source module present");
|
|
assert!(
|
|
prelude_src
|
|
.imports
|
|
.iter()
|
|
.all(|imp| imp.module != "show_user_adt"),
|
|
"prelude must not import show_user_adt (the invariant is that \
|
|
codegen resolves the cross-module ref WITHOUT going through \
|
|
import_map). Got imports: {:?}",
|
|
prelude_src.imports
|
|
);
|
|
}
|