Files
AILang/crates/ailang-codegen/src/subst.rs
T
Brummel 76b21c00eb feat(lang): eliminate the Implicit ownership default — totality + the drop-soundness it demasks (#55)
Deletes `ParamMode::Implicit`. `ParamMode` is now `{Own, Borrow}`:
every fn-type slot on every signature carries an explicit `own` or
`borrow`, no defaulted position survives anywhere (model 0008 §2,
spec 0062). The parser rejects a bare fn-type slot; `borrow-return`
and `borrow-over-value` reject at the signature; the corpus is
migrated to minimal-ownership modes (consumed ⇒ own, read-only-heap
⇒ borrow, value ⇒ trivial-own). The documented `Implicit`-ret-mode
leak is fixed: an owned heap return now drops exactly once (live=0,
acceptance criterion 5).

This was the easy half. Removing the default ACTIVATED a family of
drop paths that `Implicit` had silently skipped — the pre-cutover
language was leaking (and in places mis-dropping) here rather than
crashing, because an Implicit scrutinee turned the drop off. Making
the modes explicit (Own) turned those paths on and exposed two
latent-bug clusters, all fixed RED-first as part of this cutover:

Drop-soundness family (four legs):
  A. lit-sub-pattern double-free — the desugar re-matched the same
     owned scrutinee in the lit fall-through; fixed by grouping
     consecutive same-ctor arms into one match (bind fields once),
     in ailang-core desugar.
  B. Cons-husk leak on non-tail arm bodies — the lit-sub-pattern
     desugar rebound the owned scrutinee via `Let $mp = xs`, which
     bumped consume_count and suppressed the existing fn-return
     partial_drop. Fixed by not rebinding a bare-Var scrutinee
     (one husk-freeing mechanism, not two).
  C. polymorphic `drop_<T>` rc_dec'd monomorphised value fields —
     the per-ADT drop fn was emitted once from the polymorphic
     TypeDef, defaulting type-var fields to ptr and rc_dec'ing
     inline Ints (segfault). Fixed with per-monomorph drop
     functions (new ailang-codegen::dropmono): the drop set is
     collected from the lowered MIR, value-type fields are skipped,
     heap fields still freed once; monomorphic-concrete ADTs keep
     their byte-identical un-suffixed drop symbol.
  D. static Str literal passed to an `(own Str)` param — the
     literal lowers to a header-less rodata constant; the callee's
     now-active rc_dec read its length field as a refcount and
     freed a static address (segfault). Fixed with the missing
     fourth StrRep::Static→Heap promotion in lower_to_mir's App arm,
     gated on Own mode (borrow args stay static, no regression).

over-strict-mode lint over-fired: it suggested `(borrow V)` for
value-typed params (which `borrow-over-value` rejects — own is the
only legal mode there) and fired on `(intrinsic)` bodies (whose
consumption the linearity walk cannot observe). Tightened to skip
both; contract 0008 updated to the narrowed firing scope.

Irreversible step — canonical-form hash reset (model 0008 §6,
acceptance criterion 6). Every signature now carries explicit modes,
so the hashable canonical JSON changed for every module. RATIFY:
the corpus-wide hash-pin reset (hash_pin, prelude_module_hash_pin,
mono_hash_stability, eq_ord_e2e, embed_export_hash_stable, the
ct4/iter*/loop_recur schema-extension pins) and the list ir_snapshot
golden were regenerated once, deliberately, as the intended one-time
consequence of removing the mode elision from the canonical form —
not a regression. Each regenerated hash verified deterministic across
two runs.

Also fixes a pre-existing latent failure surfaced by the verification
gate, unrelated to this cutover: the `every_contract_names_a_resolvable_
ratifying_test` resolver (design_index_pin) could not resolve the
" + " dual-link ratifying-test form (`uniqueness.rs + linearity.rs`)
that the #57 audit-close (dfdc65f) introduced — it shipped red on that
commit. Resolver taught the dual-link form, mirroring its sibling.

Verification: cargo test --workspace = 731 passed, 0 failed (twice,
stable); e2e 102 passed, no binary exits non-zero (corpus crash-free);
grep-clean for Implicit/fn_implicit/mode_eq across crates; every drop
fix confirmed via emitted IR + AILANG_RC_STATS balance on the head==K,
head!=K, and Nil paths. Three BLOCKEDs en route (the unsound first
husk-dec attempt, the over-strict derivation premise, the leg-B fix
direction) were each treated as a real design/spec gap and rediagnosed,
not patched over.

Supersedes #54 (return-position-only leak patch). Precondition #57
(linearity hardening) was already met. Spec docs/specs/0062, plan
docs/plans/0121.

closes #55
2026-06-02 00:03:46 +02:00

226 lines
9.6 KiB
Rust

//! Type substitution + unification helpers for monomorphisation.
//!
//! Free functions extracted from `lib.rs` during the 18g tidy split.
//! `unify_for_subst` walks declared params against actual arg types and
//! binds `Type::Var`s; `apply_subst_to_type` specialises a polymorphic
//! type under that binding; `qualify_local_types_codegen` rewrites bare
//! ADT names into `module.Type` form when a sig crosses an import
//! boundary. (The mir.1b switch deleted the codegen-side type-mirror,
//! the only caller of the former `derive_substitution` entry; its
//! callers in `match_lower` build the substitution directly via
//! `unify_for_subst`.)
use ailang_core::ast::*;
use std::collections::{BTreeMap, BTreeSet};
use super::{CodegenError, Result};
/// Walks `param` and `arg` in parallel, treating any `Type::Var { name }`
/// on the param side whose name is in `vars` as an unknown to be bound
/// in `subst`. Identical concrete shapes pass through; structural
/// mismatches yield an internal error.
pub(crate) fn unify_for_subst(
param: &Type,
arg: &Type,
vars: &BTreeSet<&str>,
subst: &mut BTreeMap<String, Type>,
) -> Result<()> {
// A `$u`-prefixed var is a
// synth-only wildcard the checker emits for nullary
// ctors of a parameterised ADT (e.g. `Nil : List<$u>`). It
// carries no real constraint — accept without binding so a
// sibling arg can pin the type var instead. Without this,
// `Cons(Int, Nil)` synth would unify `a = Int` (from head) and
// then `a = $u` (from tail's recursive `List<a>` slot) and
// falsely error.
//
// 15g-aux: the early-return must accept `$u` on **either** side.
// `$u` enters in arg position from synth, but the prev-binding
// recursion below (`unify_for_subst(&prev, arg, ...)`) can swap
// a `$u` onto the param side when a previously-bound type is
// unified against a fresher arg whose roles differ. Reduced
// repro: `length [Left 1, Right 10]` — `a` first binds to
// `Either<Int, $u>` from `Left 1`, then a recursive unification
// against `Either<$u, Int>` from `Right 10` lands `$u` in the
// param-pos[1] slot. Symmetric early-return is correct because
// `$u` is a synth-only wildcard regardless of which side carries
// it after the prev-binding swap.
if let Type::Var { name } = arg {
if name.starts_with("$u") {
return Ok(());
}
}
if let Type::Var { name } = param {
if name.starts_with("$u") {
return Ok(());
}
}
match (param, arg) {
(Type::Var { name }, _) if vars.contains(name.as_str()) => {
if let Some(prev) = subst.get(name).cloned() {
// the previously-bound type may be more
// concrete than `arg` (e.g. `prev = List<Int>` from a
// sibling binding, `arg = List<$u>` from a synth-
// wildcard nullary ctor). Use recursive unification
// instead of strict equality so the inner `$u`
// wildcard matches `Int`. The previous strict-
// equality check rejected such overlaps as bogus
// duplicate bindings.
return unify_for_subst(&prev, arg, vars, subst);
}
subst.insert(name.clone(), arg.clone());
Ok(())
}
(
Type::Con { name: pn, args: pa },
Type::Con { name: an, args: aa },
) if pn == an && pa.len() == aa.len() => {
for (p, a) in pa.iter().zip(aa.iter()) {
unify_for_subst(p, a, vars, subst)?;
}
Ok(())
}
(
Type::Fn { params: pp, ret: pr, .. },
Type::Fn { params: ap, ret: ar, .. },
) => {
if pp.len() != ap.len() {
return Err(CodegenError::Internal(
"monomorphisation: fn arity mismatch in arg".into(),
));
}
for (p, a) in pp.iter().zip(ap.iter()) {
unify_for_subst(p, a, vars, subst)?;
}
unify_for_subst(pr, ar, vars, subst)
}
(Type::Var { name: pn }, Type::Var { name: an }) if pn == an => Ok(()),
_ => Err(CodegenError::Internal(format!(
"monomorphisation: cannot match param `{}` to arg `{}`",
ailang_core::pretty::type_to_string(param),
ailang_core::pretty::type_to_string(arg),
))),
}
}
/// rewrites bare `Type::Con` references that resolve against
/// `owner_local_types` into qualified `module.Type` form. Mirrors
/// `ailang_check::qualify_local_types` and complements prep.1's
/// `qualify_workspace_types` (which qualifies consumer-side bare
/// cross-module refs). Used when the codegen pulls a polymorphic
/// fn signature across the import boundary; without this the
/// substitution derived from the call site's qualified args
/// (e.g. `std_maybe.Maybe<Int>`) would fail to unify against the
/// owner-local signature (`Maybe<a>`).
pub(crate) fn qualify_local_types_codegen(
t: &Type,
owner_module: &str,
owner_local_types: &BTreeSet<String>,
) -> Type {
match t {
Type::Con { name, args } => {
// The first two branches share the body `name.clone()` but
// express semantically distinct reasons (already qualified;
// primitive needs no qualification). Combining them with
// `||` would obscure why each disqualifies the name.
#[allow(clippy::if_same_then_else)]
let qualified = if name.contains('.') {
name.clone()
} else if ailang_core::primitives::is_primitive_name(name) {
name.clone()
} else if owner_local_types.contains(name) {
format!("{owner_module}.{name}")
} else {
name.clone()
};
Type::Con {
name: qualified,
args: args
.iter()
.map(|a| qualify_local_types_codegen(a, owner_module, owner_local_types))
.collect(),
}
}
Type::Fn { params, ret, effects, param_modes, ret_mode } => Type::Fn {
params: params
.iter()
.map(|p| qualify_local_types_codegen(p, owner_module, owner_local_types))
.collect(),
ret: Box::new(qualify_local_types_codegen(ret, owner_module, owner_local_types)),
effects: effects.clone(),
param_modes: param_modes.clone(),
ret_mode: *ret_mode,
},
Type::Forall { vars, constraints, body } => Type::Forall {
vars: vars.clone(),
constraints: constraints.clone(),
body: Box::new(qualify_local_types_codegen(body, owner_module, owner_local_types)),
},
Type::Var { .. } => t.clone(),
}
}
/// substitute rigid type vars in `t` according to `subst`.
/// Used to specialise the type of a polymorphic def for a given
/// instantiation.
pub(crate) fn apply_subst_to_type(t: &Type, subst: &BTreeMap<String, Type>) -> Type {
match t {
Type::Var { name } => subst.get(name).cloned().unwrap_or_else(|| t.clone()),
Type::Con { name, args } => Type::Con {
name: name.clone(),
args: args.iter().map(|a| apply_subst_to_type(a, subst)).collect(),
},
Type::Fn { params, ret, effects, param_modes, ret_mode } => {
let new_params: Vec<Type> =
params.iter().map(|p| apply_subst_to_type(p, subst)).collect();
let new_ret = apply_subst_to_type(ret, subst);
// spec 0062: a polymorphic (borrow a) specialised onto a
// value type becomes (own value-type) — borrow-over-value
// is forbidden and is a no-op for unboxed types (no RC).
let coerce = |ty: &Type, m: &ParamMode| -> ParamMode {
if matches!(m, ParamMode::Borrow) {
if let Type::Con { name, args } = ty {
if args.is_empty() && ailang_core::primitives::is_value_type(name) {
return ParamMode::Own;
}
}
}
*m
};
let new_param_modes: Vec<ParamMode> = new_params
.iter()
.zip(param_modes.iter())
.map(|(t, m)| coerce(t, m))
.collect();
let new_ret_mode = coerce(&new_ret, ret_mode);
Type::Fn {
params: new_params,
ret: Box::new(new_ret),
effects: effects.clone(),
param_modes: new_param_modes,
ret_mode: new_ret_mode,
}
}
Type::Forall { vars, constraints, body } => {
// Inner forall shadows: don't substitute re-bound names.
let inner: BTreeMap<String, Type> = subst
.iter()
.filter(|(k, _)| !vars.contains(k))
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
Type::Forall {
vars: vars.clone(),
constraints: constraints.clone(),
body: Box::new(apply_subst_to_type(body, &inner)),
}
}
}
}
// iter 23.4: `apply_subst_to_term` and `descriptor_for_subst` were
// the codegen-side body-substitution and symbol-mangling helpers used
// by `lower_polymorphic_call` / `emit_specialised_fn`. Both are gone:
// the typecheck-time mono pass synthesises every monomorphic body
// (via `ailang_check::substitute_rigids_in_term`) and produces
// surface-named mono symbols (via `ailang_check::mono::mono_symbol_n`).