76b21c00eb
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
226 lines
9.6 KiB
Rust
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`).
|