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
This commit is contained in:
2026-06-02 00:03:46 +02:00
parent 05c3c018de
commit 76b21c00eb
342 changed files with 3196 additions and 1503 deletions
+199 -75
View File
@@ -27,7 +27,7 @@
use ailang_core::ast::{ParamMode, Type, TypeDef};
use ailang_mir::{Callee, MTerm};
use std::collections::BTreeSet;
use std::collections::{BTreeMap, BTreeSet};
use super::synth::llvm_type;
use super::{AllocStrategy, Emitter, FnSig, Result};
@@ -76,10 +76,44 @@ impl<'a> Emitter<'a> {
/// is known to grow, recursive cascade everywhere else (cheaper
/// IR, no worklist allocation).
pub(crate) fn emit_drop_fn_for_type(&mut self, td: &TypeDef) {
// Monomorphic / intrinsic ADTs: one drop fn, empty subst, no
// suffix — byte-identical to the pre-leg-C emission.
let key = (self.module_name.to_string(), td.name.clone());
if !self.drop_monos.is_suffixed(&key) {
self.emit_drop_fn_for_instantiation(td, &BTreeMap::new(), "");
return;
}
// Polymorphic ADTs: one drop fn per concrete instantiation
// collected workspace-wide (leg C). Each fn substitutes the
// declared type-vars to the instantiation's concrete args so
// the dec-vs-skip decision is made on the *monomorph* field
// type (a value-type field is an inline scalar → skipped; a
// heap field is dec'd via its own per-monomorph drop symbol).
for args in self.drop_monos.instantiations(&key) {
let subst: BTreeMap<String, Type> =
td.vars.iter().cloned().zip(args.iter().cloned()).collect();
let suffix = self
.drop_monos
.suffix_for(&key, &args)
.unwrap_or_default();
self.emit_drop_fn_for_instantiation(td, &subst, &suffix);
}
}
/// emit one recursive `drop_<m>_<T><suffix>` body. `subst` maps the
/// declared type-vars to the concrete instantiation args (empty for
/// monomorphic ADTs); `sym_suffix` is the `__<...>` mono suffix
/// (empty for monomorphic ADTs).
fn emit_drop_fn_for_instantiation(
&mut self,
td: &TypeDef,
subst: &BTreeMap<String, Type>,
sym_suffix: &str,
) {
let m = self.module_name;
let tname = &td.name;
let mut out = String::new();
out.push_str(&format!("define void @drop_{m}_{tname}(ptr %p) {{\n"));
out.push_str(&format!("define void @drop_{m}_{tname}{sym_suffix}(ptr %p) {{\n"));
out.push_str("entry:\n");
// Null guard: a null payload is a no-op (matches
// `runtime/rc.c::ailang_rc_dec`'s null guard).
@@ -101,14 +135,16 @@ impl<'a> Emitter<'a> {
let mut local = 0u64;
for (i, ctor) in td.ctors.iter().enumerate() {
out.push_str(&format!("arm_{i}:\n"));
for (j, fty) in ctor.fields.iter().enumerate() {
// Decide what to call for this field. If the field
// lowers to `ptr` (boxed), we issue a `dec` call.
// For known ADT field types we route through that
// ADT's own drop fn so the recursion cascades; for
// anything else that lowers to `ptr` (Str, fn-typed,
// unresolved Var), fall back to plain `ailang_rc_dec`.
let lty = llvm_type(fty).unwrap_or_else(|_| "ptr".into());
for (j, fty_decl) in ctor.fields.iter().enumerate() {
// Substitute the declared field type to its monomorph.
// A value-type field (`Int`/`Bool`/`Float`/`Unit`)
// lowers to a non-`ptr` scalar → skip the dec (it is
// stored inline, NOT a heap pointer — `rc_dec` on it
// would SIGSEGV). A heap field lowers to `ptr` → dec via
// `field_drop_call` on the *concrete* type, so the
// cascade targets the field's own per-monomorph symbol.
let fty = super::subst::apply_subst_to_type(fty_decl, subst);
let lty = llvm_type(&fty).unwrap_or_else(|_| "ptr".into());
if lty != "ptr" {
continue;
}
@@ -124,7 +160,7 @@ impl<'a> Emitter<'a> {
out.push_str(&format!(
" %v{val_id} = load ptr, ptr %a{addr_id}, align 8\n"
));
let drop_call = self.field_drop_call(fty);
let drop_call = self.field_drop_call(&fty);
// Recursive call into the field's drop fn. If the
// field's type is itself `(drop-iterative)`, that drop
// fn is the worklist variant — recursion stops at one
@@ -227,10 +263,36 @@ impl<'a> Emitter<'a> {
/// worklist entry shape. The mono-typed version captures the
/// stack-overflow-on-long-self-chains problem fully.
pub(crate) fn emit_iterative_drop_fn_for_type(&mut self, td: &TypeDef) {
let key = (self.module_name.to_string(), td.name.clone());
if !self.drop_monos.is_suffixed(&key) {
self.emit_iterative_drop_fn_for_instantiation(td, &BTreeMap::new(), "");
return;
}
for args in self.drop_monos.instantiations(&key) {
let subst: BTreeMap<String, Type> =
td.vars.iter().cloned().zip(args.iter().cloned()).collect();
let suffix = self
.drop_monos
.suffix_for(&key, &args)
.unwrap_or_default();
self.emit_iterative_drop_fn_for_instantiation(td, &subst, &suffix);
}
}
/// emit one iterative `drop_<m>_<T><suffix>` body (worklist variant).
/// `subst` / `sym_suffix` carry the leg-C per-monomorph
/// instantiation, identical in role to
/// [`Self::emit_drop_fn_for_instantiation`].
fn emit_iterative_drop_fn_for_instantiation(
&mut self,
td: &TypeDef,
subst: &BTreeMap<String, Type>,
sym_suffix: &str,
) {
let m = self.module_name;
let tname = &td.name;
let mut out = String::new();
out.push_str(&format!("define void @drop_{m}_{tname}(ptr %p) {{\n"));
out.push_str(&format!("define void @drop_{m}_{tname}{sym_suffix}(ptr %p) {{\n"));
out.push_str("entry:\n");
// Null guard — symmetric with the recursive variant. A null
// payload skips worklist allocation entirely.
@@ -265,8 +327,12 @@ impl<'a> Emitter<'a> {
let mut local = 0u64;
for (i, ctor) in td.ctors.iter().enumerate() {
out.push_str(&format!("arm_{i}:\n"));
for (j, fty) in ctor.fields.iter().enumerate() {
let lty = llvm_type(fty).unwrap_or_else(|_| "ptr".into());
for (j, fty_decl) in ctor.fields.iter().enumerate() {
// Substitute to the monomorph (same reasoning as the
// recursive variant): a value-type field is inline and
// must not be dec'd/pushed.
let fty = super::subst::apply_subst_to_type(fty_decl, subst);
let lty = llvm_type(&fty).unwrap_or_else(|_| "ptr".into());
if lty != "ptr" {
continue;
}
@@ -281,7 +347,7 @@ impl<'a> Emitter<'a> {
out.push_str(&format!(
" %v{val_id} = load ptr, ptr %a{addr_id}, align 8\n"
));
if self.field_is_same_type(fty, &td.name) {
if self.field_is_same_type(&fty, &td.name) {
// Same-type field: push onto the worklist —
// continues the iterative cascade. Null-guarding
// is handled inside `ailang_drop_worklist_push`
@@ -295,7 +361,7 @@ impl<'a> Emitter<'a> {
// iterative). `field_drop_call` resolves the
// symbol; its null-guard semantics are the same
// as the recursive variant.
let drop_call = self.field_drop_call(fty);
let drop_call = self.field_drop_call(&fty);
out.push_str(&format!(
" call void @{drop_call}(ptr %v{val_id})\n"
));
@@ -374,7 +440,7 @@ impl<'a> Emitter<'a> {
/// those are not user-defined ADTs and have no per-type drop fn.
pub(crate) fn field_drop_call(&self, fty: &Type) -> String {
match fty {
Type::Con { name, .. } => {
Type::Con { name, args } => {
// Built-in pointer-typed cons: Str. No drop fn —
// shallow `ailang_rc_dec` is the right answer.
// Str has two realisations sharing the consumer
@@ -395,19 +461,13 @@ impl<'a> Emitter<'a> {
if matches!(name.as_str(), "Str") {
return "ailang_rc_dec".to_string();
}
// Qualified `module.T` → drop fn lives in `module`.
if name.matches('.').count() == 1 {
let (prefix, suffix) = name.split_once('.').expect("checked");
if let Some(target) = self.import_map.get(prefix) {
return format!("drop_{target}_{suffix}");
}
// Fallback: treat the prefix itself as the owner
// module (typechecker would have rejected an
// unimported prefix earlier).
return format!("drop_{prefix}_{suffix}");
}
// Bare name: declared in the current module.
format!("drop_{m}_{name}", m = self.module_name)
// Resolve owner + per-monomorph suffix in one place
// (leg C). A polymorphic non-intrinsic ADT instantiation
// (`Box Int`, `Pair Int Int`) gets the same `__<suffix>`
// the emission loop minted for that instantiation;
// monomorphic / intrinsic ADTs keep the un-suffixed
// symbol byte-for-byte.
self.adt_drop_symbol(name, args)
}
// Type::Fn (closure-typed field) → no per-type drop fn
// exists for closures (each closure has its own per-pair
@@ -431,6 +491,46 @@ impl<'a> Emitter<'a> {
}
}
/// resolve the `drop_<owner>_<T>` symbol for an ADT `Type::Con`,
/// applying the leg-C per-monomorph suffix when the resolved ADT is
/// polymorphic + non-intrinsic. The caller has already excluded
/// `Str` and non-`Con` shapes. `name` is the (possibly qualified)
/// type name; `args` are its concrete instantiation arguments.
///
/// The suffix decision lives in [`dropmono::DropAdtMeta`], shared
/// with the emission loop, so a call symbol matches its definition
/// byte-for-byte (a mismatch is an IR link error).
pub(crate) fn adt_drop_symbol(&self, name: &str, args: &[Type]) -> String {
let key = super::dropmono::resolve_adt_key(
name,
self.module_name,
&self.import_map,
);
let base = format!("drop_{owner}_{bare}", owner = key.0, bare = key.1);
match self.drop_monos.suffix_for(&key, args) {
Some(suffix) => format!("{base}{suffix}"),
None => base,
}
}
/// resolve the `partial_drop_<owner>_<T>` symbol for an ADT
/// `Type::Con`, applying the leg-C per-monomorph suffix. Parallel
/// to [`Self::adt_drop_symbol`]. The caller has already excluded
/// `Str` / non-`Con` shapes.
pub(crate) fn adt_partial_drop_symbol(&self, name: &str, args: &[Type]) -> String {
let key = super::dropmono::resolve_adt_key(
name,
self.module_name,
&self.import_map,
);
let base =
format!("partial_drop_{owner}_{bare}", owner = key.0, bare = key.1);
match self.drop_monos.suffix_for(&key, args) {
Some(suffix) => format!("{base}{suffix}"),
None => base,
}
}
/// predicate the `Term::Let` lowering uses to decide
/// whether a let-binder owns a fresh RC-heap allocation that
/// codegen should `dec` at scope close.
@@ -448,8 +548,8 @@ impl<'a> Emitter<'a> {
/// fn-type carries `ret_mode == Own`. The mode contract states that
/// the callee hands the returned cell's ownership to the caller's
/// frame; the let-scope close is the right place for the caller's
/// dec. Calls whose callee is `Borrow`/`Implicit`-returning are
/// still not trackable — they don't carry that signal.
/// dec. Calls whose callee is `Borrow`-returning are still not
/// trackable — a borrow-return is a view, not an owned ref.
///
/// Other value shapes (vars, literals, matches, …) return `false`
/// here. A `Term::Var` returning an RC-allocated box would already
@@ -467,11 +567,10 @@ impl<'a> Emitter<'a> {
MTerm::App { callee, .. } => {
// a call whose callee carries
// `ret_mode == Own` hands a fresh heap allocation to
// the caller's frame. Trackable. `Borrow` and
// `Implicit` ret-modes do not carry that signal —
// returning by Borrow is a view into the callee's
// owned data (caller does not own it), and Implicit
// is the back-compat lane that 18c.3's debt covers.
// the caller's frame. Trackable. A `Borrow` ret-mode
// does not carry that signal — returning by Borrow is a
// view into the callee's owned data (caller does not
// own it).
self.synth_callee_ret_mode(callee)
.map(|m| matches!(m, ParamMode::Own))
.unwrap_or(false)
@@ -538,15 +637,16 @@ impl<'a> Emitter<'a> {
pub(crate) fn drop_symbol_for_binder(&self, value: &MTerm, val_ssa: &str) -> String {
match value {
MTerm::Ctor { type_name, .. } => {
if type_name.matches('.').count() == 1 {
let (prefix, suffix) =
type_name.split_once('.').expect("checked");
if let Some(target) = self.import_map.get(prefix) {
return format!("drop_{target}_{suffix}");
}
return format!("drop_{prefix}_{suffix}");
}
format!("drop_{m}_{type_name}", m = self.module_name)
// The binder's instantiated result type carries the
// concrete args (`Box Int` → args `[Int]`); read them off
// `value.ty()` so the leg-C per-monomorph suffix matches
// the emitted `drop_<m>_Box__Int`. `type_name` alone
// would lose the instantiation.
let args = match value.ty() {
Type::Con { args, .. } => args,
_ => Vec::new(),
};
self.adt_drop_symbol(type_name, &args)
}
MTerm::Lam { .. } => self
.closure_drops
@@ -563,7 +663,7 @@ impl<'a> Emitter<'a> {
// var on an as-yet-unmonomorphised polymorphic call —
// the monomorphised copies will resolve correctly).
MTerm::App { .. } | MTerm::Loop { .. } => {
if let Type::Con { name, .. } = value.ty() {
if let Type::Con { name, args } = value.ty() {
// Symmetric to `field_drop_call`'s Str arm: Str is a
// built-in pointer type with no per-type drop fn. Both
// heap-Str (rc_header at payload-8) and static-Str
@@ -572,15 +672,7 @@ impl<'a> Emitter<'a> {
if name == "Str" {
return "ailang_rc_dec".to_string();
}
if name.matches('.').count() == 1 {
let (prefix, suffix) =
name.split_once('.').expect("checked");
if let Some(target) = self.import_map.get(prefix) {
return format!("drop_{target}_{suffix}");
}
return format!("drop_{prefix}_{suffix}");
}
return format!("drop_{m}_{name}", m = self.module_name);
return self.adt_drop_symbol(&name, &args);
}
"ailang_rc_dec".to_string()
}
@@ -649,11 +741,36 @@ impl<'a> Emitter<'a> {
/// cascade points — the unmoved fields go through their own
/// `drop_<m>_<F>` which itself decides recursive vs iterative).
pub(crate) fn emit_partial_drop_fn_for_type(&mut self, td: &TypeDef) {
let key = (self.module_name.to_string(), td.name.clone());
if !self.drop_monos.is_suffixed(&key) {
self.emit_partial_drop_fn_for_instantiation(td, &BTreeMap::new(), "");
return;
}
for args in self.drop_monos.instantiations(&key) {
let subst: BTreeMap<String, Type> =
td.vars.iter().cloned().zip(args.iter().cloned()).collect();
let suffix = self
.drop_monos
.suffix_for(&key, &args)
.unwrap_or_default();
self.emit_partial_drop_fn_for_instantiation(td, &subst, &suffix);
}
}
/// emit one `partial_drop_<m>_<T><suffix>` body. `subst` /
/// `sym_suffix` carry the leg-C per-monomorph instantiation,
/// identical in role to [`Self::emit_drop_fn_for_instantiation`].
fn emit_partial_drop_fn_for_instantiation(
&mut self,
td: &TypeDef,
subst: &BTreeMap<String, Type>,
sym_suffix: &str,
) {
let m = self.module_name;
let tname = &td.name;
let mut out = String::new();
out.push_str(&format!(
"define void @partial_drop_{m}_{tname}(ptr %p, i64 %mask) {{\n"
"define void @partial_drop_{m}_{tname}{sym_suffix}(ptr %p, i64 %mask) {{\n"
));
out.push_str("entry:\n");
out.push_str(" %is_null = icmp eq ptr %p, null\n");
@@ -671,8 +788,9 @@ impl<'a> Emitter<'a> {
let mut local = 0u64;
for (i, ctor) in td.ctors.iter().enumerate() {
out.push_str(&format!("arm_{i}:\n"));
for (j, fty) in ctor.fields.iter().enumerate() {
let lty = llvm_type(fty).unwrap_or_else(|_| "ptr".into());
for (j, fty_decl) in ctor.fields.iter().enumerate() {
let fty = super::subst::apply_subst_to_type(fty_decl, subst);
let lty = llvm_type(&fty).unwrap_or_else(|_| "ptr".into());
if lty != "ptr" {
continue;
}
@@ -702,7 +820,7 @@ impl<'a> Emitter<'a> {
out.push_str(&format!(
" %v{val_id} = load ptr, ptr %a{addr_id}, align 8\n"
));
let drop_call = self.field_drop_call(fty);
let drop_call = self.field_drop_call(&fty);
out.push_str(&format!(
" call void @{drop_call}(ptr %v{val_id})\n"
));
@@ -788,21 +906,11 @@ impl<'a> Emitter<'a> {
/// populated for those shapes anyway).
pub(crate) fn partial_drop_symbol_for_type(&self, ty: &Type) -> Option<String> {
match ty {
Type::Con { name, .. } => {
Type::Con { name, args } => {
if name == "Str" {
return None;
}
if name.matches('.').count() == 1 {
let (prefix, suffix) = name.split_once('.').expect("checked");
if let Some(target) = self.import_map.get(prefix) {
return Some(format!("partial_drop_{target}_{suffix}"));
}
return Some(format!("partial_drop_{prefix}_{suffix}"));
}
Some(format!(
"partial_drop_{m}_{name}",
m = self.module_name
))
Some(self.adt_partial_drop_symbol(name, args))
}
_ => None,
}
@@ -879,12 +987,28 @@ impl<'a> Emitter<'a> {
}
};
let cref = self.lookup_ctor_by_type(type_name, ctor_name)?;
// leg C: substitute the declared field types to the binder's
// concrete instantiation (read off `value.ty()`'s args against
// the ADT's declared type-vars), so a value-type field is
// recognised as inline (non-`ptr`, skipped) and a heap field
// routes through its own per-monomorph drop symbol. For a
// monomorphic ADT the subst is empty and this is a no-op.
let inst_subst: BTreeMap<String, Type> =
match (&cref.type_vars, value.ty()) {
(vars, Type::Con { args, .. })
if !vars.is_empty() && vars.len() == args.len() =>
{
vars.iter().cloned().zip(args.into_iter()).collect()
}
_ => BTreeMap::new(),
};
// Per-field dec for non-moved pointer-typed slots. ail_fields
// are the AILang-level field types; field_drop_call resolves
// them to either `drop_<owner>_<T>` (ADTs cascade) or
// `ailang_rc_dec` (Str / closures / vars).
for (idx, fty_ail) in cref.ail_fields.iter().enumerate() {
let lty = llvm_type(fty_ail).unwrap_or_else(|_| "ptr".into());
for (idx, fty_decl) in cref.ail_fields.iter().enumerate() {
let fty_ail = super::subst::apply_subst_to_type(fty_decl, &inst_subst);
let lty = llvm_type(&fty_ail).unwrap_or_else(|_| "ptr".into());
if lty != "ptr" {
continue;
}
@@ -900,7 +1024,7 @@ impl<'a> Emitter<'a> {
self.body.push_str(&format!(
" {v} = load ptr, ptr {addr}, align 8\n"
));
let drop_call = self.field_drop_call(fty_ail);
let drop_call = self.field_drop_call(&fty_ail);
self.body.push_str(&format!(
" call void @{drop_call}(ptr {v})\n"
));
+437
View File
@@ -0,0 +1,437 @@
//! 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),
}
}
+4 -6
View File
@@ -153,15 +153,13 @@ impl<'a> Emitter<'a> {
let saved_entry_marker = self.entry_block_end_marker.take();
// A lambda thunk is its own fn frame for
// param-mode lookup. The outer fn's params are not in scope
// inside the thunk; the thunk's own params are pushed below
// and (currently) carry no mode annotation, so they default
// to `Implicit` — `lower_match`'s Iter A gate will skip arm-
// close pattern-binder dec for matches on lambda params,
// mirroring the fn-level Implicit-param treatment.
// inside the thunk; the thunk's own params carry no surface
// annotation, so they are synthesised `Own` — the consume-side
// default that owns its argument for the thunk body.
let saved_param_modes = std::mem::take(&mut self.current_param_modes);
for pname in lam_params.iter() {
self.current_param_modes
.insert(pname.clone(), ParamMode::Implicit);
.insert(pname.clone(), ParamMode::Own);
}
// Lambdas inside lambdas are fine: they get their own counter
// namespace within the enclosing thunk. They share the
+72 -50
View File
@@ -40,6 +40,7 @@ use ailang_mir::{Callee, MArg, MTerm, MirDef, MirWorkspace, Mode, StrRep};
use std::collections::{BTreeMap, BTreeSet};
mod drop;
mod dropmono;
mod escape;
mod intercepts;
mod lambda;
@@ -396,6 +397,14 @@ fn lower_workspace_inner(mir: &MirWorkspace, alloc: AllocStrategy, target: Targe
module_consts.insert(mname.clone(), consts);
}
// Leg C: workspace-global per-monomorph drop metadata. Built once
// here (after the symbol-table pass, before lowering) and shared by
// reference with every `Emitter`. Computes which polymorphic ADTs
// take a per-instantiation drop fn and the concrete arg-tuples each
// is used at, so the emission loop and the call-site manglers agree
// on every `drop_<m>_<T>__<suffix>` symbol.
let drop_monos = dropmono::collect_drop_monos(mir);
// Pass 2: lower per module. Globals/strings are accumulated per module,
// because they are mangled per module.
for (mname, mir_module) in &mir.modules {
@@ -432,6 +441,7 @@ fn lower_workspace_inner(mir: &MirWorkspace, alloc: AllocStrategy, target: Targe
&module_ctor_index,
&module_consts,
import_map,
&drop_monos,
alloc,
);
emitter
@@ -770,6 +780,13 @@ struct Emitter<'a> {
module_user_fns: &'a BTreeMap<String, BTreeMap<String, FnSig>>,
/// Import map of the current module (alias/module name → actual module name).
import_map: BTreeMap<String, String>,
/// workspace-global per-monomorph drop metadata (leg C). Tells the
/// drop-fn emission loop which polymorphic ADTs to emit one drop fn
/// per instantiation for, and tells the call-site manglers whether
/// a `Type::Con` takes a per-monomorph `__<suffix>` on its
/// `drop_`/`partial_drop_` symbol. Shared by reference with every
/// `Emitter`.
drop_monos: &'a dropmono::DropAdtMeta,
/// ADT table: type_name -> list of ctors in definition order.
/// Tag of a ctor = index in this list.
/// Kept around for future tools (pretty-printer for ADT values,
@@ -874,15 +891,15 @@ struct Emitter<'a> {
/// entry) from the fn type's `param_modes`. Consulted by
/// `lower_match`'s arm-close pattern-binder dec emission (Iter A) to
/// decide whether the scrutinee was statically owned: if the
/// scrutinee resolves to a fn-param whose mode is `Borrow` or
/// `Implicit`, the pattern-binder dec must NOT fire — the caller
/// still holds a reference and dec'ing the pattern-binder would
/// fragment the caller's structure.
/// scrutinee resolves to a fn-param whose mode is `Borrow`, the
/// pattern-binder dec must NOT fire — the caller still holds a
/// reference and dec'ing the pattern-binder would fragment the
/// caller's structure.
///
/// Symmetric with the Iter B gate at fn return (`emit_fn`'s Own-
/// param dec): both sites must check the param-mode signal before
/// dec'ing, because Implicit and Borrow do not carry the "caller
/// handed off ownership" signal that makes the dec safe.
/// param dec): both sites check the param-mode signal before
/// dec'ing, because `Borrow` does not carry the "caller handed off
/// ownership" signal that makes the dec safe.
current_param_modes: BTreeMap<String, ParamMode>,
/// Per-fn map of name → (alloca SSA name, AIL element type) for
/// alloca-resident loop binders. Populated on entry to a
@@ -973,6 +990,7 @@ impl<'a> Emitter<'a> {
module_ctor_index: &'a BTreeMap<String, BTreeMap<String, CtorRef>>,
module_consts: &'a BTreeMap<String, BTreeMap<String, ConstDef>>,
import_map: BTreeMap<String, String>,
drop_monos: &'a dropmono::DropAdtMeta,
alloc: AllocStrategy,
) -> Self {
let mut types: BTreeMap<String, Vec<CtorInfo>> = BTreeMap::new();
@@ -1030,6 +1048,7 @@ impl<'a> Emitter<'a> {
str_counter: 0,
module_user_fns,
import_map,
drop_monos,
types,
module_ctor_index,
module_consts,
@@ -1293,10 +1312,9 @@ impl<'a> Emitter<'a> {
fn emit_fn(&mut self, f: &FnDef, body: Option<&MTerm>) -> Result<()> {
// also lift `param_modes` out of the fn type. The
// fn-return Own-param dec emission below consults it to decide
// which params get a drop call before `ret`. `Implicit`
// entries (legacy / unannotated) and `Borrow` entries are
// skipped — only `Own` carries the static "caller handed off
// ownership" signal.
// which params get a drop call before `ret`. `Borrow` entries
// are skipped — only `Own` carries the static "caller handed
// off ownership" signal.
let (param_tys, ret_ty, param_modes) = match &f.ty {
Type::Fn {
params,
@@ -1339,7 +1357,10 @@ impl<'a> Emitter<'a> {
self.pending_entry_allocas.clear();
self.entry_block_end_marker = None;
for (i, pname) in f.params.iter().enumerate() {
let mode = param_modes.get(i).copied().unwrap_or(ParamMode::Implicit);
// Codegen-synthesised fn-defs (lambda thunks, local-rec
// lifts) may carry an empty `param_modes`; fall back to
// `Own` (the synthesis default) rather than index-panic.
let mode = param_modes.get(i).copied().unwrap_or(ParamMode::Own);
self.current_param_modes.insert(pname.clone(), mode);
}
// run escape analysis over the fn body. The result
@@ -1469,11 +1490,10 @@ impl<'a> Emitter<'a> {
// the caller's frame; caller dec's, not us),
// - the current block is still open.
//
// `Implicit`-mode params do NOT get this dec: they have
// no static "caller handed off ownership" signal —
// emitting a dec here might double-dec a value the caller
// also dec's. `Borrow`-mode params definitely don't get
// dec'd (the caller still owns them).
// `Borrow`-mode params do NOT get dec'd: the caller still
// owns them, so there is no caller-handed-off-ownership
// signal and a dec here would fragment the caller's
// structure.
//
// Closes the 18c.3/18c.4 carve-out: "fn parameters still
// don't get dec'd at fn return — the caller-handed-off-
@@ -1490,7 +1510,7 @@ impl<'a> Emitter<'a> {
if plty != "ptr" {
continue;
}
let mode = param_modes.get(i).copied().unwrap_or(ParamMode::Implicit);
let mode = param_modes.get(i).copied().unwrap_or(ParamMode::Own);
if !matches!(mode, ParamMode::Own) {
continue;
}
@@ -1812,8 +1832,8 @@ impl<'a> Emitter<'a> {
// If `value` is a `Term::Var` referencing a name in
// `current_param_modes`, the let-binder inherits that
// mode for the duration of the body. Without this,
// `(let a t (match a ...))` where `t` is an Implicit
// / Borrow-mode param defeats the
// `(let a t (match a ...))` where `t` is a
// `Borrow`-mode param defeats the
// `scrutinee_is_owned` gate in `lower_match` (the
// gate looks up `a` in `current_param_modes`, misses,
// and defaults to "owned" — Iter A then dec's
@@ -2690,8 +2710,7 @@ impl<'a> Emitter<'a> {
// alias whose owner is some other binder and is dropped
// there, never here;
// - the matching callee param mode is `Borrow` — `Own` slots
// consume the arg (the callee dec's it), `Implicit` is the
// back-compat lane that carries no transfer signal;
// consume the arg (the callee dec's it);
// - the dropped SSA is never the call result `dst` (an input
// argument SSA is always distinct from the freshly-minted
// result SSA), so this can never dec a value that flows out
@@ -2702,7 +2721,7 @@ impl<'a> Emitter<'a> {
// `param_modes`), so the borrow-slot test reads it directly
// — no re-lookup of the callee's signature from a codegen
// sig table. `Borrow` slots borrow the arg, so an Own-ret
// heap temp landing in one is dropped here; `Own`/`Implicit`
// heap temp landing in one is dropped here; `Own`
// slots consume the arg (the callee dec's it).
for (arg, (arg_ssa, arg_ty)) in args.iter().zip(compiled_args.iter()) {
let is_borrow_slot = matches!(arg.mode, Mode::Borrow);
@@ -3168,7 +3187,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
params: vec!["a".into(), "b".into()],
body: Term::App {
@@ -3192,7 +3211,7 @@ mod tests {
ret: Box::new(Type::unit()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
params: vec![],
body: Term::Lit { lit: Literal::Unit },
@@ -3238,7 +3257,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
params: vec![],
body: Term::Lit {
@@ -3313,7 +3332,7 @@ mod tests {
ret: Box::new(Type::unit()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
params: vec![],
body: Term::Lit { lit: Literal::Unit },
@@ -3386,7 +3405,7 @@ mod tests {
ret: Box::new(Type::unit()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
params: vec![],
body: Term::Lit { lit: Literal::Unit },
@@ -3418,7 +3437,7 @@ mod tests {
ret: Box::new(ret_ty),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
params: vec![],
body,
@@ -3491,7 +3510,7 @@ mod tests {
name: name.into(),
ty: Type::Fn {
params: vec![], ret: Box::new(ret_ty), effects: vec![],
param_modes: vec![], ret_mode: ParamMode::Implicit,
param_modes: vec![], ret_mode: ParamMode::Own,
},
params: vec![], body, suppress: vec![], doc: None,
export: None,
@@ -3543,7 +3562,7 @@ mod tests {
name: name.into(),
ty: Type::Fn {
params: vec![], ret: Box::new(Type::float()), effects: vec![],
param_modes: vec![], ret_mode: ParamMode::Implicit,
param_modes: vec![], ret_mode: ParamMode::Own,
},
params: vec![], body, suppress: vec![], doc: None,
export: None,
@@ -3553,7 +3572,7 @@ mod tests {
name: "main".into(),
ty: Type::Fn {
params: vec![], ret: Box::new(Type::unit()), effects: vec![],
param_modes: vec![], ret_mode: ParamMode::Implicit,
param_modes: vec![], ret_mode: ParamMode::Own,
},
params: vec![], body: Term::Lit { lit: Literal::Unit },
suppress: vec![], doc: None,
@@ -3598,7 +3617,7 @@ mod tests {
ret: Box::new(Type::bool_()),
effects: vec![],
param_modes: vec![ParamMode::Borrow, ParamMode::Borrow],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
params: vec!["x".into(), "y".into()],
// Body is a placeholder — the intercept must
@@ -3616,7 +3635,7 @@ mod tests {
ret: Box::new(Type::unit()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
params: vec![],
body: Term::Lit { lit: Literal::Unit },
@@ -3661,7 +3680,7 @@ mod tests {
ret: Box::new(Type::bool_()),
effects: vec![],
param_modes: vec![ParamMode::Borrow, ParamMode::Borrow],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
params: vec!["x".into(), "y".into()],
body: Term::Lit { lit: Literal::Bool { value: false } },
@@ -3676,7 +3695,7 @@ mod tests {
ret: Box::new(Type::unit()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
params: vec![],
body: Term::Lit { lit: Literal::Unit },
@@ -3725,7 +3744,7 @@ mod tests {
ret: Box::new(Type::unit()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
params: vec![],
body: Term::Lit { lit: Literal::Unit },
@@ -3824,8 +3843,11 @@ mod tests {
params: vec![param_ail_ty.clone(), param_ail_ty.clone()],
ret: Box::new(Type::Con { name: "Ordering".into(), args: vec![] }),
effects: vec![],
param_modes: vec![ParamMode::Borrow, ParamMode::Borrow],
ret_mode: ParamMode::Implicit,
// value-typed params (Int/Bool) cannot be `(borrow V)` —
// the cutover's `borrow-over-value` reject forbids it; a
// value type is copied, so `own` is the only legal mode.
param_modes: vec![ParamMode::Own, ParamMode::Own],
ret_mode: ParamMode::Own,
},
params: vec!["x".into(), "y".into()],
// placeholder body; the `compare__<T>` intercept overrides
@@ -3849,7 +3871,7 @@ mod tests {
ret: Box::new(Type::unit()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
params: vec![],
body: Term::Lit { lit: Literal::Unit },
@@ -3894,7 +3916,7 @@ mod tests {
ret: Box::new(Type::unit()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
params: vec![],
body: Term::Lit { lit: Literal::Unit },
@@ -3930,7 +3952,7 @@ mod tests {
ret: Box::new(Type::unit()),
effects: vec!["IO".into()],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
params: vec![],
body: Term::Do {
@@ -3972,7 +3994,7 @@ mod tests {
ret: Box::new(Type::unit()),
effects: vec!["IO".into()],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
params: vec![],
body: Term::Do {
@@ -4033,7 +4055,7 @@ mod tests {
ret: Box::new(Type::bool_()),
effects: vec![],
param_modes: vec![ParamMode::Borrow, ParamMode::Borrow],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
params: vec!["x".into(), "y".into()],
body: Term::Lit { lit: Literal::Bool { value: false } },
@@ -4048,7 +4070,7 @@ mod tests {
ret: Box::new(Type::unit()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
params: vec![],
body: Term::Lit { lit: Literal::Unit },
@@ -4129,7 +4151,7 @@ mod tests {
ret: Box::new(Type::unit()),
effects: vec!["IO".into()],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
params: vec![],
body: Term::Do {
@@ -4170,7 +4192,7 @@ mod tests {
ret: Box::new(Type::unit()),
effects: vec!["IO".into()],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
params: vec![],
body: Term::Do {
@@ -4212,7 +4234,7 @@ mod tests {
ret: Box::new(Type::unit()),
effects: vec!["IO".into()],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
params: vec![],
body: Term::Do {
@@ -4254,7 +4276,7 @@ mod tests {
ret: Box::new(Type::unit()),
effects: vec!["IO".into()],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
params: vec![],
body: Term::Do {
@@ -4296,7 +4318,7 @@ mod tests {
ret: Box::new(Type::unit()),
effects: vec!["IO".into()],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
params: vec![],
body: Term::Do {
+23 -6
View File
@@ -256,8 +256,16 @@ impl<'a> Emitter<'a> {
} else {
cref.ail_fields.clone()
};
let expected_llvm_tys: Vec<String> = if cref.type_vars.is_empty() {
cref.fields.clone()
// leg C: the body-ctor field substitution (var -> concrete
// arg). Empty for monomorphic ADTs; for a polymorphic body ctor
// it pins each declared field var to the instantiation arg
// derived from the body args' types. Used both for the expected
// LLVM field types AND (further down) for the reuse-arm per-field
// dec, so a value-type field is recognised as inline (skipped)
// and a heap field routes through its own per-monomorph drop
// symbol.
let field_subst: BTreeMap<String, Type> = if cref.type_vars.is_empty() {
BTreeMap::new()
} else {
let arg_ail_tys: Vec<Type> = body_args
.iter()
@@ -269,9 +277,14 @@ impl<'a> Emitter<'a> {
for (exp, actual) in qualified_ail_fields.iter().zip(arg_ail_tys.iter()) {
unify_for_subst(exp, actual, &var_set, &mut subst)?;
}
subst
};
let expected_llvm_tys: Vec<String> = if cref.type_vars.is_empty() {
cref.fields.clone()
} else {
qualified_ail_fields
.iter()
.map(|f| llvm_type(&apply_subst_to_type(f, &subst)))
.map(|f| llvm_type(&apply_subst_to_type(f, &field_subst)))
.collect::<Result<_>>()?
};
// (18d.2 currently does not dec old fields in the reuse
@@ -353,8 +366,12 @@ impl<'a> Emitter<'a> {
.as_ref()
.and_then(|sb| self.moved_slots.get(sb).cloned())
.unwrap_or_default();
for (idx, fty_ail) in qualified_ail_fields.iter().enumerate() {
let lty = llvm_type(fty_ail).unwrap_or_else(|_| "ptr".into());
for (idx, fty_decl) in qualified_ail_fields.iter().enumerate() {
// leg C: substitute to the body-ctor's monomorph so a
// value-type slot is skipped (inline scalar) and a heap slot
// dec's through its own per-monomorph drop symbol.
let fty_ail = apply_subst_to_type(fty_decl, &field_subst);
let lty = llvm_type(&fty_ail).unwrap_or_else(|_| "ptr".into());
if lty != "ptr" {
continue;
}
@@ -370,7 +387,7 @@ impl<'a> Emitter<'a> {
self.body.push_str(&format!(
" {v} = load ptr, ptr {addr}, align 8\n"
));
let drop_call = self.field_drop_call(fty_ail);
let drop_call = self.field_drop_call(&fty_ail);
self.body.push_str(&format!(
" call void @{drop_call}(ptr {v})\n"
));
+31 -7
View File
@@ -170,13 +170,37 @@ pub(crate) fn apply_subst_to_type(t: &Type, subst: &BTreeMap<String, Type>) -> T
name: name.clone(),
args: args.iter().map(|a| apply_subst_to_type(a, subst)).collect(),
},
Type::Fn { params, ret, effects, param_modes, ret_mode } => Type::Fn {
params: params.iter().map(|p| apply_subst_to_type(p, subst)).collect(),
ret: Box::new(apply_subst_to_type(ret, subst)),
effects: effects.clone(),
param_modes: param_modes.clone(),
ret_mode: *ret_mode,
},
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