Files
AILang/crates/ailang-codegen/src/dropmono.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

438 lines
17 KiB
Rust

//! Per-monomorph drop-fn collection for polymorphic ADTs (leg C of
//! the #55-cutover drop-soundness family).
//!
//! Background. Pre-cutover, the per-ADT drop fn was emitted exactly
//! once per `Def::Type`, keyed by the *declared* field types. For a
//! polymorphic ADT (`Box a`, `Pair a b`) a ctor field typed at a
//! type-var `a` failed `synth::llvm_type` and fell back to `ptr`,
//! emitting an `ailang_rc_dec` on it. At a value-type monomorph
//! (`Box Int`) that field is an inline `i64`, so `rc_dec(7)`
//! dereferences the scalar and SIGSEGVs.
//!
//! Fix. Emit one drop fn per (polymorphic-ADT, concrete instantiation)
//! and decide dec-vs-skip on the *substituted* field type. A
//! value-type field (`Int`/`Bool`/`Float`/`Unit`) is skipped (inline
//! scalar, no RC); a heap field is dec'd through its own per-monomorph
//! drop symbol so the cascade composes.
//!
//! Symbol naming. A concrete-monomorphic ADT (`vars` empty: IntList,
//! Ordering) keeps its un-suffixed `drop_<m>_<T>` symbol byte-for-byte
//! (the `ir_snapshot` goldens pin these). A polymorphic ADT
//! instantiation gets a mono suffix mangled the same way
//! `ailang_check::mono::mono_symbol_n` mangles fn symbols
//! (`drop_<m>_Pair__Int_Int`; compound args hash-route to stay
//! bounded). Intrinsic-storage types (RawBuf) are polymorphic but use
//! the flat intrinsic drop path — they are NOT suffixed (their drop is
//! element-type independent and the golden pins `drop_<m>_RawBuf`).
//!
//! This module owns the workspace-global collection
//! ([`collect_drop_monos`]) and the shared suffix decision
//! ([`DropAdtMeta`]); the emission and call-site manglers in `drop.rs`
//! / `match_lower.rs` consult the same [`DropAdtMeta`] so a definition
//! and its callers always agree on the symbol (a mismatch is a link
//! error).
use ailang_core::ast::{Def, Module, Term, Type, TypeDef};
use ailang_mir::{Callee, MArg, MNewArg, MTerm, MirWorkspace};
use std::collections::{BTreeMap, BTreeSet};
use super::subst::{apply_subst_to_type, qualify_local_types_codegen};
/// A canonical key for an ADT: `(owner_module, bare_type_name)`.
pub(crate) type AdtKey = (String, String);
/// Workspace-global drop-monomorphisation metadata, built once before
/// the per-module codegen loop and shared (by reference) with every
/// `Emitter`.
#[derive(Debug, Default)]
pub(crate) struct DropAdtMeta {
/// ADTs that take a per-monomorph drop-symbol suffix: declared
/// `vars` non-empty AND not intrinsic-storage. A `Type::Con` whose
/// resolved key is in this set, and whose `args` are non-empty,
/// gets a `__<suffix>` appended to its `drop_`/`partial_drop_`
/// symbol.
suffixed: BTreeSet<AdtKey>,
/// For each suffixed ADT, the concrete arg-tuples it is
/// instantiated at across the whole workspace, keyed by a canonical
/// hash of the arg-tuple (`Type` is not `Ord`, so the tuple itself
/// cannot key a `BTreeSet`; the hash gives a deterministic,
/// dedup-stable key). The emission loop reads the values to emit one
/// drop fn per instantiation in the owning module.
monos: BTreeMap<AdtKey, BTreeMap<String, Vec<Type>>>,
}
/// Canonical, deterministic key for an arg-tuple (used to dedup
/// instantiations that `Type`'s non-`Ord`-ness blocks from a set key).
fn args_key(args: &[Type]) -> String {
args.iter()
.map(ailang_core::canonical::type_hash)
.collect::<Vec<_>>()
.join("|")
}
impl DropAdtMeta {
/// Is this ADT (resolved to `key`) suffixed (poly + non-intrinsic)?
pub(crate) fn is_suffixed(&self, key: &AdtKey) -> bool {
self.suffixed.contains(key)
}
/// The concrete instantiations of `key` to emit in its owner
/// module. Empty for monomorphic / intrinsic / never-instantiated
/// ADTs.
pub(crate) fn instantiations(&self, key: &AdtKey) -> Vec<Vec<Type>> {
self.monos
.get(key)
.map(|s| s.values().cloned().collect())
.unwrap_or_default()
}
/// The mono symbol suffix for an instantiation `args`, mangled the
/// same way `mono_symbol_n` mangles fn type-args. `None` when the
/// type is not suffixed (monomorphic / intrinsic) or has no args —
/// caller keeps the un-suffixed base symbol. The leading `__`
/// joiner is included so callers concatenate directly.
pub(crate) fn suffix_for(&self, key: &AdtKey, args: &[Type]) -> Option<String> {
if args.is_empty() || !self.is_suffixed(key) {
return None;
}
// Reuse the fn-symbol mangler: `mono_symbol_n("", &args)` would
// prepend an empty base + joiner. We want only the per-arg
// suffix joined by `__`, prefixed with the `__` that separates
// it from the drop base. Mirror `mono_symbol_n`'s join exactly.
let parts: Vec<String> = args
.iter()
.map(ailang_check::mono::type_mono_suffix)
.collect();
Some(format!("__{}", parts.join("__")))
}
}
/// Resolve a (possibly qualified) `Type::Con` name to its canonical
/// `(owner_module, bare_name)` key, using `import_map` for the
/// qualified case and `current_module` for the bare case.
pub(crate) fn resolve_adt_key(
name: &str,
current_module: &str,
import_map: &BTreeMap<String, String>,
) -> AdtKey {
if name.matches('.').count() == 1 {
let (prefix, suffix) = name.split_once('.').expect("checked");
let owner = import_map
.get(prefix)
.map(|s| s.as_str())
.unwrap_or(prefix);
(owner.to_string(), suffix.to_string())
} else {
(current_module.to_string(), name.to_string())
}
}
/// Does this `Def::Type` use the flat intrinsic-storage drop path
/// (its `new` op is an `(intrinsic)`, not a real term-ctor body)?
/// Mirror of the gate in `lib.rs`'s drop-fn emission loop.
fn is_intrinsic_storage(module: &Module, td: &TypeDef) -> bool {
module.defs.iter().any(|d| {
matches!(d, Def::Fn(f)
if f.name == "new"
&& matches!(f.body, Term::Intrinsic)
&& fn_returns_type_name(f, &td.name))
})
}
/// Does fn `f` return a `Type::Con` named `tname` (outermost)? Mirror
/// of `lib.rs::fn_returns_type` (kept local to avoid widening that
/// fn's visibility).
fn fn_returns_type_name(f: &ailang_core::ast::FnDef, tname: &str) -> bool {
fn outer(t: &Type, tname: &str) -> bool {
match t {
Type::Con { name, .. } => name == tname,
Type::Forall { body, .. } => outer(body, tname),
_ => false,
}
}
if let Type::Fn { ret, .. } = &f.ty {
outer(ret, tname)
} else if let Type::Forall { body, .. } = &f.ty {
if let Type::Fn { ret, .. } = body.as_ref() {
outer(ret, tname)
} else {
false
}
} else {
false
}
}
/// Build the workspace-global drop-monomorphisation table: which ADTs
/// are suffixed, and which concrete instantiations each is used at.
pub(crate) fn collect_drop_monos(mir: &MirWorkspace) -> DropAdtMeta {
let mut meta = DropAdtMeta::default();
// Per-module set of locally-declared ADT names. Used to qualify a
// bare type-con reference (`List` in `std_list`'s body) to its
// canonical `module.Type` form *at the module where it appears*, so
// a stored arg / field resolves to the same owner regardless of
// which module later emits a drop fn referencing it. Without this,
// a `Maybe (List Int)` collected in `std_list` would store a bare
// `List`, and the drop fn emitted in `std_maybe` (Maybe's owner)
// would resolve that bare `List` to `drop_std_maybe_List` (wrong
// owner) — an undefined-symbol link error.
let mut local_types: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
// 1. Determine the suffixed set + record each ADT's declared
// type-vars and ctor field types (under their owner module),
// so the fixpoint below can expand nested field instantiations.
// `adt_decl[(m, T)] = (vars, ctor_field_types_flattened)`.
let mut adt_decl: BTreeMap<AdtKey, (Vec<String>, Vec<Type>)> = BTreeMap::new();
for (mname, mir_module) in &mir.modules {
let m = &mir_module.ast;
for def in &m.defs {
if let Def::Type(td) = def {
let key = (mname.clone(), td.name.clone());
let fields: Vec<Type> =
td.ctors.iter().flat_map(|c| c.fields.clone()).collect();
adt_decl.insert(key.clone(), (td.vars.clone(), fields));
local_types
.entry(mname.clone())
.or_default()
.insert(td.name.clone());
if !td.vars.is_empty() && !is_intrinsic_storage(m, td) {
meta.suffixed.insert(key);
}
}
}
}
// 2. Seed the instantiation set by walking every MIR body / const
// and collecting every `Type` that appears. Each walked type is
// qualified against its module's local types before collection,
// so stored args carry canonical `module.Type` names.
let mut seeds: Vec<(AdtKey, Vec<Type>)> = Vec::new();
for (mname, mir_module) in &mir.modules {
let m = &mir_module.ast;
let import_map = build_import_map(m);
let owner_local = local_types.get(mname).cloned().unwrap_or_default();
let mut sink = |t: &Type| {
let q = qualify_local_types_codegen(t, mname, &owner_local);
collect_instantiations(&q, mname, &import_map, &meta.suffixed, &mut seeds);
};
for d in &mir_module.defs {
walk_term_types(&d.body, &mut sink);
}
for c in &mir_module.consts {
walk_term_types(&c.body, &mut sink);
}
// Also walk every fn signature type appearing in the AST defs —
// an Own-returned polymorphic ADT whose ctor is built in a
// callee still needs its drop emitted in the caller's module
// when the let-close decs it. The body walk catches the ctor
// site; the signature walk is belt-and-braces for ret types
// that surface only through a call.
for def in &m.defs {
if let Def::Fn(f) = def {
walk_type(&f.ty, &mut sink);
}
}
}
// 3. Fixpoint: an instantiation `T<args>` needs each of its ctor
// fields, substituted by {var -> arg}, to also be emitted (a
// `Pair (Box Int) Int` field-drops `Box Int`). Expand until no
// new (key, args) pairs appear. Dedup on `(AdtKey, args-hash)`
// since `Type` is not `Ord`.
let mut worklist: Vec<(AdtKey, Vec<Type>)> = seeds;
let mut seen: BTreeSet<(AdtKey, String)> = BTreeSet::new();
while let Some((key, args)) = worklist.pop() {
let ak = args_key(&args);
if !seen.insert((key.clone(), ak.clone())) {
continue;
}
meta.monos
.entry(key.clone())
.or_default()
.insert(ak, args.clone());
let Some((vars, fields)) = adt_decl.get(&key) else {
continue;
};
if vars.len() != args.len() {
// Defensive: arity mismatch means our resolution missed;
// skip rather than mis-substitute.
continue;
}
let subst: BTreeMap<String, Type> =
vars.iter().cloned().zip(args.iter().cloned()).collect();
// Field types are written in the owner module's local
// namespace; resolve against the owner's import map and qualify
// the owner's own bare type-cons (e.g. a `List a` self-field, or
// a sibling ADT) so the nested instantiation keys on the
// canonical owner. The substituted-in args are already
// qualified from the seed pass.
let owner = key.0.clone();
let import_map = mir
.modules
.get(&owner)
.map(|mm| build_import_map(&mm.ast))
.unwrap_or_default();
let owner_local = local_types.get(&owner).cloned().unwrap_or_default();
for f in fields {
let concrete = apply_subst_to_type(f, &subst);
let concrete = qualify_local_types_codegen(&concrete, &owner, &owner_local);
collect_instantiations(
&concrete,
&owner,
&import_map,
&meta.suffixed,
&mut worklist,
);
}
}
meta
}
/// Build a module's import map (alias|name -> actual module), mirroring
/// the per-module loop in `lower_workspace_inner`.
fn build_import_map(m: &Module) -> BTreeMap<String, String> {
let mut import_map: BTreeMap<String, String> = BTreeMap::new();
for imp in &m.imports {
let key = imp.alias.clone().unwrap_or_else(|| imp.module.clone());
import_map.insert(key, imp.module.clone());
}
if m.name != "prelude" {
import_map
.entry("prelude".to_string())
.or_insert_with(|| "prelude".to_string());
}
import_map
}
/// Collect, from a single `Type`, every suffixed-ADT instantiation
/// (recursing into args).
fn collect_instantiations(
t: &Type,
current_module: &str,
import_map: &BTreeMap<String, String>,
suffixed: &BTreeSet<AdtKey>,
out: &mut Vec<(AdtKey, Vec<Type>)>,
) {
match t {
Type::Con { name, args } => {
if !args.is_empty() {
let key = resolve_adt_key(name, current_module, import_map);
if suffixed.contains(&key) {
out.push((key, args.clone()));
}
}
for a in args {
collect_instantiations(a, current_module, import_map, suffixed, out);
}
}
Type::Fn { params, ret, .. } => {
for p in params {
collect_instantiations(p, current_module, import_map, suffixed, out);
}
collect_instantiations(ret, current_module, import_map, suffixed, out);
}
Type::Forall { body, .. } => {
collect_instantiations(body, current_module, import_map, suffixed, out)
}
Type::Var { .. } => {}
}
}
/// Apply `sink` to every `Type` reachable from a `Type`.
fn walk_type<F: FnMut(&Type)>(t: &Type, sink: &mut F) {
sink(t);
}
/// Apply `sink` to every `Type` carried by `term` and its sub-terms.
/// `sink` itself recurses into the type structure
/// ([`collect_instantiations`]), so this walker only has to surface
/// every type-bearing node once.
fn walk_term_types<F: FnMut(&Type)>(term: &MTerm, sink: &mut F) {
// The node's own static type.
let ty = term.ty();
sink(&ty);
match term {
MTerm::Lit { .. }
| MTerm::Var { .. }
| MTerm::Str { .. }
| MTerm::Intrinsic { .. }
| MTerm::Recur { .. } => {}
MTerm::App { callee, args, .. } => {
walk_callee_types(callee, sink);
walk_args(args, sink);
}
MTerm::Do { args, .. } => walk_args(args, sink),
MTerm::Ctor { args, .. } => walk_args(args, sink),
MTerm::New { elem, args, .. } => {
if let Some(e) = elem {
sink(e);
}
for a in args {
if let MNewArg::Value(v) = a {
walk_term_types(v, sink);
} else if let MNewArg::Type(t) = a {
sink(t);
}
}
}
MTerm::Let { init, body, .. } => {
walk_term_types(init, sink);
walk_term_types(body, sink);
}
MTerm::LetRec { sig, body, in_term, .. } => {
sink(sig);
walk_term_types(body, sink);
walk_term_types(in_term, sink);
}
MTerm::If { cond, then, else_, .. } => {
walk_term_types(cond, sink);
walk_term_types(then, sink);
walk_term_types(else_, sink);
}
MTerm::Match { scrutinee, arms, .. } => {
walk_term_types(scrutinee, sink);
for arm in arms {
walk_term_types(&arm.body, sink);
}
}
MTerm::Lam { param_tys, ret_ty, body, .. } => {
for p in param_tys {
sink(p);
}
sink(ret_ty);
walk_term_types(body, sink);
}
MTerm::Seq { lhs, rhs, .. } => {
walk_term_types(lhs, sink);
walk_term_types(rhs, sink);
}
MTerm::Clone { value, .. } => walk_term_types(value, sink),
MTerm::ReuseAs { source, body, .. } => {
walk_term_types(source, sink);
walk_term_types(body, sink);
}
MTerm::Loop { binders, body, .. } => {
for b in binders {
sink(&b.ty);
walk_term_types(&b.init, sink);
}
walk_term_types(body, sink);
}
}
}
fn walk_args<F: FnMut(&Type)>(args: &[MArg], sink: &mut F) {
for a in args {
walk_term_types(&a.term, sink);
}
}
fn walk_callee_types<F: FnMut(&Type)>(callee: &Callee, sink: &mut F) {
match callee {
Callee::Static { sig, .. } | Callee::Builtin { sig, .. } => sink(sig),
Callee::Indirect(inner) => walk_term_types(inner, sink),
}
}