codegen tidy: extract drop emission into drop.rs

Second slice of the codegen split. Pulls every method that emits
or dispatches drop fns into a dedicated module. No behaviour
change.

Cluster A (per-type drop bodies):
  - emit_drop_fn_for_type — recursive cascade, ADT default.
  - emit_iterative_drop_fn_for_type — Iter 18e worklist variant.
  - field_is_same_type — same-ADT predicate (private to drop.rs).
  - field_drop_call — per-field drop-symbol resolver.

Cluster B (per-let-close dispatch):
  - is_rc_heap_allocated — Iter 18c.3 trackability gate, widened
    in 18g.2 to include Own-returning App.
  - synth_callee_ret_mode — ret_mode lookup helper (private).
  - drop_symbol_for_binder — picks the symbol Term::Let calls
    at scope close.
  - emit_inlined_partial_drop — partial-drop emission for
    pattern-moved slots.

Cluster C (closure drops):
  - build_env_drop_fn — env's drop fn body builder.
  - build_pair_drop_fn — pair's drop fn body builder.

Visibility: methods called from lib.rs are pub(crate);
helpers used only inside drop.rs (field_is_same_type,
synth_callee_ret_mode) stay private. Three Emitter helpers
in lib.rs that drop.rs calls back into were upgraded to
pub(crate): synth_arg_type, lookup_ctor_by_type, fresh_ssa.
Field access from drop.rs into the parent's private Emitter
fields uses the standard descendant-module privacy lane.

lib.rs drops from 4800 → 4139 lines. drop.rs is 580 lines.
cargo test --workspace clean: 66/66 e2e + 26/26 codegen +
all sibling crates pass.
This commit is contained in:
2026-05-08 15:47:59 +02:00
parent 84ba83dda3
commit 86989a7cb5
2 changed files with 700 additions and 665 deletions
+696
View File
@@ -0,0 +1,696 @@
//! Per-type drop-fn emission and per-let-close drop dispatch.
//!
//! All RC-allocator drop work lives here:
//! - `emit_drop_fn_for_type` / `emit_iterative_drop_fn_for_type`
//! emit `@drop_<m>_<T>(ptr)` per `Def::Type` under `--alloc=rc`.
//! The recursive variant cascades through `field_drop_call`; the
//! iterative variant uses an explicit worklist (Iter 18e) so
//! `(drop-iterative)` types can free chains of arbitrary length
//! without consuming proportional C stack.
//! - `field_is_same_type` / `field_drop_call` are the internal
//! dispatch helpers consulted by the per-type drop bodies and by
//! match-arm / reuse-as drop emission elsewhere.
//! - `is_rc_heap_allocated` / `synth_callee_ret_mode` /
//! `drop_symbol_for_binder` / `emit_inlined_partial_drop` are the
//! per-let-close cluster: codegen calls them from the
//! `Term::Let` lowering to decide whether to dec, what symbol to
//! dispatch to, and (when pattern destructuring transferred
//! fields out) how to emit a partial-drop sequence inline.
//! - `build_env_drop_fn` / `build_pair_drop_fn` build the per-
//! closure drop fns that `lower_lambda` defers into the IR
//! stream (Iter 18c.4 closure cleanup).
//!
//! Methods called from `lib.rs` are `pub(crate)`; helpers used only
//! among the drop-cluster methods stay private. Field access from
//! this submodule into the parent's private `Emitter` fields works
//! through the standard descendant-module privacy lane.
use ailang_core::ast::*;
use std::collections::BTreeSet;
use super::synth::llvm_type;
use super::{AllocStrategy, Emitter, FnSig, Result};
impl<'a> Emitter<'a> {
/// Iter 18c.4: emit a `void @drop_<module>_<TypeName>(ptr %p)`
/// function that decrements the refcount of every pointer-typed
/// field of every ctor, then frees the outer box.
///
/// Shape:
/// ```text
/// define void @drop_<m>_<T>(ptr %p) {
/// %tag = load i64, ptr %p
/// switch i64 %tag, label %dflt [
/// i64 0, label %arm0
/// ...
/// ]
/// arm_i:
/// for each pointer-typed field f_j:
/// %addr = gep ptr %p, i64 (8 + 8*j)
/// %v = load ptr, ptr %addr
/// call void @drop_<owner>_<FieldT>(ptr %v) ; or @ailang_rc_dec
/// br label %join
/// dflt:
/// unreachable
/// join:
/// call void @ailang_rc_dec(ptr %p)
/// ret void
/// }
/// ```
///
/// For ADTs with no boxed children every arm is empty and falls
/// straight through to `join`, which is just the final dec — see
/// the assignment's "always emit drop_X for every ADT" decision
/// (`rc_box_drop`'s `MkBox(Int)` is the canonical example).
///
/// Recursion: when a ctor field's type is the same ADT (or any
/// ADT in the workspace), the emitted call to
/// `@drop_<owner>_<T>(field)` is recursive at the IR level and
/// will overflow the stack on long lists. The 18e
/// `(drop-iterative)` annotation routes such types through
/// [`Self::emit_iterative_drop_fn_for_type`] instead, which
/// replaces the recursive call with a worklist push. ADTs WITHOUT
/// the annotation continue to use this recursive form — the
/// orchestrator's choice: opt-in iterative drop where the depth
/// 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) {
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("entry:\n");
// Null guard: a null payload is a no-op (matches
// `runtime/rc.c::ailang_rc_dec`'s null guard).
out.push_str(" %is_null = icmp eq ptr %p, null\n");
out.push_str(" br i1 %is_null, label %ret, label %live\n");
out.push_str("live:\n");
out.push_str(" %tag = load i64, ptr %p, align 8\n");
let n_ctors = td.ctors.len();
// Switch over the tag. Each ctor gets one arm.
out.push_str(" switch i64 %tag, label %dflt [\n");
for (i, _) in td.ctors.iter().enumerate() {
out.push_str(&format!(" i64 {i}, label %arm_{i}\n"));
}
out.push_str(" ]\n");
// Per-ctor arm: iterate fields, dec the boxed ones.
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());
if lty != "ptr" {
continue;
}
let off = 8 + (j as i64) * 8;
let addr_id = local;
local += 1;
let val_id = local;
local += 1;
out.push_str(&format!(
" %a{addr_id} = getelementptr inbounds i8, ptr %p, i64 {off}\n"
));
out.push_str(&format!(
" %v{val_id} = load ptr, ptr %a{addr_id}, align 8\n"
));
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
// level. Otherwise this is the unbounded recursive
// cascade; safe only on bounded-depth ADTs (the
// `(drop-iterative)` annotation exists for the
// unbounded ones).
out.push_str(&format!(
" call void @{drop_call}(ptr %v{val_id})\n"
));
}
out.push_str(" br label %join\n");
}
// Default arm: unreachable when the typechecker has accepted
// the input — every legal box has one of the ctor tags.
out.push_str("dflt:\n");
if n_ctors == 0 {
// No ctors at all: a Type with zero ctors cannot be
// instantiated; the drop fn is dead. Still emit a
// br-to-join for IR validity.
out.push_str(" br label %join\n");
} else {
out.push_str(" unreachable\n");
}
// Join: free the outer box.
out.push_str("join:\n");
out.push_str(" call void @ailang_rc_dec(ptr %p)\n");
out.push_str(" br label %ret\n");
out.push_str("ret:\n");
out.push_str(" ret void\n");
out.push_str("}\n\n");
self.body.push_str(&out);
}
/// Iter 18e: emit `drop_<m>_<T>` for a `(drop-iterative)` type.
/// Replaces the recursive cascade in [`Self::emit_drop_fn_for_type`]
/// with an iterative-with-explicit-worklist body so cells of
/// arbitrary chain depth can free without consuming proportional
/// C stack.
///
/// IR shape:
/// ```text
/// define void @drop_<m>_<T>(ptr %p) {
/// entry:
/// %is_null = icmp eq ptr %p, null
/// br i1 %is_null, label %ret, label %init_wl
/// init_wl:
/// %wl = call ptr @ailang_drop_worklist_new()
/// call void @ailang_drop_worklist_push(ptr %wl, ptr %p)
/// br label %loop_head
/// loop_head:
/// %cur = call ptr @ailang_drop_worklist_pop(ptr %wl)
/// %done = icmp eq ptr %cur, null
/// br i1 %done, label %finish, label %dispatch
/// dispatch:
/// %tag = load i64, ptr %cur, align 8
/// switch i64 %tag, label %dflt [
/// i64 0, label %arm_0
/// ...
/// ]
/// arm_i:
/// for each pointer-typed field f_j of ctor i:
/// %addr = gep %cur, 8 + 8*j
/// %v = load ptr, ptr %addr
/// if field type is T (same as the type being dropped):
/// call void @ailang_drop_worklist_push(ptr %wl, ptr %v)
/// else:
/// call void @drop_<owner>_<F>(ptr %v) ; or @ailang_rc_dec
/// call void @ailang_rc_dec(ptr %cur)
/// br label %loop_head
/// dflt:
/// unreachable
/// finish:
/// call void @ailang_drop_worklist_free(ptr %wl)
/// br label %ret
/// ret:
/// ret void
/// }
/// ```
///
/// Mono-typed worklist. Every pointer pushed onto `%wl` is a `T`
/// (the type being dropped). For a field whose type is `T` itself
/// → push (continues the iterative cascade). For any other ADT
/// field type `T'` → call `drop_<m'>_<T'>` directly: if `T'` is
/// also `(drop-iterative)`, that fn allocates its own worklist
/// instance (no nesting); if `T'` is non-iterative, it recurses
/// stack-wise (depth bounded by the number of *distinct* nested
/// ADTs reachable from `T`, which is small in practice).
///
/// This interpretation of the assignment's "should also use the
/// worklist" clause was chosen because a heterogeneously-typed
/// worklist would require storing a (ptr, drop-handler) tuple per
/// entry plus a vtable dispatch on pop — significant complexity
/// for the case where two distinct ADTs are mutually recursive
/// AND both are drop-iterative AND the chain is millions deep.
/// That triple-conjunct is not on the 18-arc's critical path; if
/// it surfaces in practice, a follow-up iter can extend the
/// 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 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("entry:\n");
// Null guard — symmetric with the recursive variant. A null
// payload skips worklist allocation entirely.
out.push_str(" %is_null = icmp eq ptr %p, null\n");
out.push_str(" br i1 %is_null, label %ret, label %init_wl\n");
out.push_str("init_wl:\n");
out.push_str(" %wl = call ptr @ailang_drop_worklist_new()\n");
out.push_str(
" call void @ailang_drop_worklist_push(ptr %wl, ptr %p)\n",
);
out.push_str(" br label %loop_head\n");
out.push_str("loop_head:\n");
out.push_str(
" %cur = call ptr @ailang_drop_worklist_pop(ptr %wl)\n",
);
out.push_str(" %done = icmp eq ptr %cur, null\n");
out.push_str(" br i1 %done, label %finish, label %dispatch\n");
out.push_str("dispatch:\n");
out.push_str(" %tag = load i64, ptr %cur, align 8\n");
let n_ctors = td.ctors.len();
out.push_str(" switch i64 %tag, label %dflt [\n");
for (i, _) in td.ctors.iter().enumerate() {
out.push_str(&format!(" i64 {i}, label %arm_{i}\n"));
}
out.push_str(" ]\n");
// Per-ctor arms. For each pointer-typed field decide push vs
// direct call based on whether the field's type is the same
// as the type being dropped.
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());
if lty != "ptr" {
continue;
}
let off = 8 + (j as i64) * 8;
let addr_id = local;
local += 1;
let val_id = local;
local += 1;
out.push_str(&format!(
" %a{addr_id} = getelementptr inbounds i8, ptr %cur, i64 {off}\n"
));
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) {
// Same-type field: push onto the worklist —
// continues the iterative cascade. Null-guarding
// is handled inside `ailang_drop_worklist_push`
// itself (skips null payloads).
out.push_str(&format!(
" call void @ailang_drop_worklist_push(ptr %wl, ptr %v{val_id})\n"
));
} else {
// Different-type field: dispatch to that type's
// own drop fn (which itself decides recursive vs.
// 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);
out.push_str(&format!(
" call void @{drop_call}(ptr %v{val_id})\n"
));
}
}
// Dec the outer cell. Worklist holds no other reference
// to this pointer (push happened exactly once on the
// parent's cascade, and pop just removed that entry), so
// the cell's refcount drops by exactly one here. Children
// pushed above keep their own refcounts pending until
// their loop iteration.
out.push_str(" call void @ailang_rc_dec(ptr %cur)\n");
out.push_str(" br label %loop_head\n");
}
// Default arm: unreachable when the typechecker has accepted
// the input. Same shape as the recursive variant.
out.push_str("dflt:\n");
if n_ctors == 0 {
out.push_str(" br label %finish\n");
} else {
out.push_str(" unreachable\n");
}
out.push_str("finish:\n");
out.push_str(" call void @ailang_drop_worklist_free(ptr %wl)\n");
out.push_str(" br label %ret\n");
out.push_str("ret:\n");
out.push_str(" ret void\n");
out.push_str("}\n\n");
self.body.push_str(&out);
}
/// Iter 18e helper: is `fty` the same ADT as `td_name` in the
/// current module? Used by the iterative-drop body to decide
/// "push to worklist" (same type) vs. "call its drop fn directly"
/// (different type).
///
/// Returns `true` only when the field type is a `Type::Con`
/// referencing `td_name` AND the reference resolves to the
/// current module (bare or qualified-but-self). Qualified names
/// pointing at *other* modules are different types — even when
/// they spell the same suffix. Type-vars and fn-types are never
/// the same as the ADT being dropped (parametric self-recursion
/// could bind a var to T, but the bound is invisible at codegen
/// since we don't monomorphise drop fns).
fn field_is_same_type(&self, fty: &Type, td_name: &str) -> bool {
match fty {
Type::Con { name, .. } => {
if name.matches('.').count() == 1 {
let (prefix, suffix) = name.split_once('.').expect("checked");
if suffix != td_name {
return false;
}
// Resolve the prefix; same-module iff the resolved
// target equals self.module_name.
let target = self
.import_map
.get(prefix)
.map(|s| s.as_str())
.unwrap_or(prefix);
target == self.module_name
} else {
name == td_name
}
}
_ => false,
}
}
/// Iter 18c.4: pick the drop-fn symbol to call for a single
/// pointer-typed field. Routes ADT fields to their own
/// `drop_<owner>_<T>` symbol so the recursion cascades through
/// recursive types (List, Tree). Falls back to `ailang_rc_dec`
/// for non-ADT pointer types (Str, fn-typed, unresolved Var) —
/// 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, .. } => {
// Built-in pointer-typed cons: Str. No drop fn —
// shallow `ailang_rc_dec` is the right answer (Str
// payloads are NUL-terminated bytes in static
// memory; nothing to recurse into).
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)
}
// Type::Fn (closure-typed field) → no per-type drop fn
// exists for closures (each closure has its own per-pair
// drop fn keyed by the lam-id, not by type). Iter 18c.4
// shallow-frees the closure-pair; the env and any
// captured ADT fields leak. A future iter that types
// closure-typed fields with a runtime descriptor pointer
// would close this. For 18c.4's recursive-ADT story this
// is acceptable — the shipping fixtures don't store
// closures inside ADT fields.
Type::Fn { .. } => "ailang_rc_dec".to_string(),
// Type::Var (parameterised ADT field whose type-arg was
// not pinned at declaration): we don't know the field's
// concrete shape from the static decl alone, and the
// generated drop fn is a single symbol per ADT (no
// monomorphisation). Shallow free is the conservative
// choice — boxed children of polymorphic-typed fields
// leak. Iter 18d/18e will revisit this when reuse hints
// and the worklist allocator land.
Type::Var { .. } | Type::Forall { .. } => "ailang_rc_dec".to_string(),
}
}
/// Iter 18c.3: 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.
///
/// Returns `true` exactly when:
/// - the active allocator is `Rc`,
/// - `value` is a `Term::Ctor` or `Term::Lam` (the two AST shapes
/// that lower through the heap-allocation path), AND
/// - the term is *not* in the current fn's `non_escape` set —
/// escaping ctors/lambdas go through `ailang_rc_alloc`,
/// non-escaping ones become stack `alloca`s and must NOT be
/// `dec`'d (they are freed by LLVM at fn return).
///
/// Iter 18g.2 widens this to include `Term::App` whose callee's
/// 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.
///
/// Other value shapes (vars, literals, matches, …) return `false`
/// here. A `Term::Var` returning an RC-allocated box would already
/// be tracked by an earlier let-binder; tracking it again here
/// would double-dec.
pub(crate) fn is_rc_heap_allocated(&self, value: &Term) -> bool {
if !matches!(self.alloc, AllocStrategy::Rc) {
return false;
}
match value {
Term::Ctor { .. } | Term::Lam { .. } => {
let term_ptr = (value as *const Term) as usize;
!self.non_escape.contains(&term_ptr)
}
Term::App { callee, .. } => {
// Iter 18g.2: 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.
self.synth_callee_ret_mode(callee)
.map(|m| matches!(m, ParamMode::Own))
.unwrap_or(false)
}
_ => false,
}
}
/// Iter 18g.2: lookup helper for a callee's `ret_mode`. Returns
/// `Some(mode)` when the callee resolves to a fn-typed term;
/// `None` for shapes whose type is not a `Type::Fn` (typechecker
/// would already have rejected an App on a non-fn, but the helper
/// is defensive). Used by [`Self::is_rc_heap_allocated`] and the
/// [`Self::drop_symbol_for_binder`] App-arm to decide both
/// trackability and the drop-fn symbol.
fn synth_callee_ret_mode(&self, callee: &Term) -> Option<ParamMode> {
let cty = self.synth_arg_type(callee).ok()?;
match cty {
Type::Fn { ret_mode, .. } => Some(ret_mode),
_ => None,
}
}
/// Iter 18c.4: pick the drop-fn symbol to call at the close of a
/// trackable `Term::Let` scope. For a `Term::Ctor` binder the
/// symbol is `drop_<owner>_<TypeName>` — derived from the ctor's
/// `type_name` (which already encodes the owning module via the
/// `module.T` form when cross-module). For a `Term::Lam` binder
/// the symbol comes from `closure_drops`, populated by
/// [`Self::lower_lambda`] when it emitted the per-pair drop fn.
///
/// Falls back to `ailang_rc_dec` for any other shape — should be
/// unreachable since `is_rc_heap_allocated` only returns `true`
/// for `Term::Ctor` / `Term::Lam`, but a defensive fallback
/// keeps the IR well-formed even if the predicate ever widens.
pub(crate) fn drop_symbol_for_binder(&self, value: &Term, val_ssa: &str) -> String {
match value {
Term::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)
}
Term::Lam { .. } => self
.closure_drops
.get(val_ssa)
.cloned()
.unwrap_or_else(|| "ailang_rc_dec".to_string()),
// Iter 18g.2: an Own-returning call hands a freshly heap-
// allocated cell whose static type is the callee's
// `ret`. Resolve the per-type drop fn from that ret-type
// so the cascade walks the cell's pointer-typed children
// (e.g. an Own-returned `Tree` fans out via
// `drop_<m>_<Tree>`). Falls back to `ailang_rc_dec` if
// the ret-type is not a `Type::Con` (e.g. a bare type
// var on an as-yet-unmonomorphised polymorphic call —
// the monomorphised copies will resolve correctly).
Term::App { .. } => {
if let Ok(ret_ty) = self.synth_arg_type(value) {
if let Type::Con { name, .. } = ret_ty {
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);
}
}
"ailang_rc_dec".to_string()
}
_ => "ailang_rc_dec".to_string(),
}
}
/// Iter 18d.3: emit a partial-drop sequence inline at a let-close
/// site whose binder has moved-out pattern slots. Replaces the
/// uniform `drop_<m>_<T>(ptr)` call: load each pointer-typed
/// field whose slot index is NOT in `moved`, dispatch through
/// `field_drop_call` (the same per-type or shallow drop the
/// recursive cascade picks), then `ailang_rc_dec` the outer
/// box. Skips slots in `moved` entirely — those values were
/// transferred to a pattern-bound binder that owns the dec
/// for them.
///
/// Iter 18g.2 widens the input set: with `Term::App` (Own-
/// returning call) now also trackable, a let-binder whose value is
/// `Term::App` may accumulate moved slots if its body pattern-
/// matches the binder. The per-field static-ctor emission below
/// is not usable in that case (the runtime tag is dynamic, not
/// statically known from `value`'s shape); fall back to shallow
/// `ailang_rc_dec`. The non-moved field cells leak under that
/// path — same dynamic-tag partial-drop debt that 18d.4's match
/// arm already documents.
pub(crate) fn emit_inlined_partial_drop(
&mut self,
value: &Term,
val_ssa: &str,
moved: &BTreeSet<usize>,
) -> Result<()> {
let (type_name, ctor_name) = match value {
Term::Ctor { type_name, ctor, .. } => (type_name.as_str(), ctor.as_str()),
_ => {
// Iter 18g.2 fallback: dynamic-tag partial-drop debt.
// `value` is `Term::App` (Own-returning) or `Term::Lam`
// — `is_rc_heap_allocated` returns true for both, and a
// body that pattern-matches such a binder will populate
// `moved_slots`. We don't know the active ctor
// statically, so we shallow-dec the outer cell. Non-
// moved field cells leak under this path; closing the
// leak requires a tag-conditional partial-drop helper
// (analogous to the per-type drop fn but parameterised
// on the tag). Tracked as future work — same family as
// 18d.4's `(case (LCons h t) ...)` carve-out.
let _ = (val_ssa, moved); // keep compiler quiet about unused
self.body.push_str(&format!(
" call void @ailang_rc_dec(ptr {val_ssa})\n"
));
return Ok(());
}
};
let cref = self.lookup_ctor_by_type(type_name, ctor_name)?;
// 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());
if lty != "ptr" {
continue;
}
if moved.contains(&idx) {
continue;
}
let off = 8 + (idx as i64) * 8;
let addr = self.fresh_ssa();
self.body.push_str(&format!(
" {addr} = getelementptr inbounds i8, ptr {val_ssa}, i64 {off}\n"
));
let v = self.fresh_ssa();
self.body.push_str(&format!(
" {v} = load ptr, ptr {addr}, align 8\n"
));
let drop_call = self.field_drop_call(fty_ail);
self.body.push_str(&format!(
" call void @{drop_call}(ptr {v})\n"
));
}
// Finally dec the outer box. The per-type drop fn would have
// done this in its `join` block; we replicate it here.
self.body.push_str(&format!(
" call void @ailang_rc_dec(ptr {val_ssa})\n"
));
Ok(())
}
/// Iter 18c.4: build the IR text for a closure env's drop fn.
/// Layout: 8 bytes per capture, in declaration order.
/// For each pointer-typed capture, emit a load + drop call;
/// finally `ailang_rc_dec` the env block.
pub(crate) fn build_env_drop_fn(
&self,
sym: &str,
cap_meta: &[(String, String, String, Type, Option<FnSig>)],
) -> String {
let mut out = String::new();
out.push_str(&format!("define void @{sym}(ptr %env) {{\nentry:\n"));
out.push_str(" %is_null = icmp eq ptr %env, null\n");
out.push_str(" br i1 %is_null, label %ret, label %live\n");
out.push_str("live:\n");
let mut local = 0u64;
for (i, (_cname, _outer_ssa, lty, ail_ty, _sig)) in cap_meta.iter().enumerate() {
if lty != "ptr" {
continue;
}
let off = (i as i64) * 8;
let addr_id = local;
local += 1;
let val_id = local;
local += 1;
out.push_str(&format!(
" %a{addr_id} = getelementptr inbounds i8, ptr %env, i64 {off}\n"
));
out.push_str(&format!(
" %v{val_id} = load ptr, ptr %a{addr_id}, align 8\n"
));
let drop_call = self.field_drop_call(ail_ty);
out.push_str(&format!(
" call void @{drop_call}(ptr %v{val_id})\n"
));
}
out.push_str(" call void @ailang_rc_dec(ptr %env)\n");
out.push_str(" br label %ret\n");
out.push_str("ret:\n");
out.push_str(" ret void\n}\n\n");
out
}
/// Iter 18c.4: build the IR text for a closure pair's drop fn.
/// Layout: { ptr thunk, ptr env } — env at offset 8.
/// Loads env, calls the env drop, then decs the pair box.
pub(crate) fn build_pair_drop_fn(
&self,
sym: &str,
env_drop: &str,
has_env: bool,
) -> String {
let mut out = String::new();
out.push_str(&format!("define void @{sym}(ptr %p) {{\nentry:\n"));
out.push_str(" %is_null = icmp eq ptr %p, null\n");
out.push_str(" br i1 %is_null, label %ret, label %live\n");
out.push_str("live:\n");
if has_env {
out.push_str(
" %ea = getelementptr inbounds {{ ptr, ptr }}, ptr %p, i64 0, i32 1\n",
);
out.push_str(" %env = load ptr, ptr %ea, align 8\n");
out.push_str(&format!(" call void @{env_drop}(ptr %env)\n"));
} else {
// No env — the env-drop is still emitted (uniform shape)
// but is a no-op on null. Skip the load and call dec
// directly on the pair.
let _ = env_drop;
}
out.push_str(" call void @ailang_rc_dec(ptr %p)\n");
out.push_str(" br label %ret\n");
out.push_str("ret:\n");
out.push_str(" ret void\n}\n\n");
out
}
}
+4 -665
View File
@@ -40,6 +40,7 @@ use std::collections::{BTreeMap, BTreeSet};
use ailang_check::uniqueness::{infer_module, UniquenessTable};
mod drop;
mod escape;
mod subst;
mod synth;
@@ -803,389 +804,6 @@ impl<'a> Emitter<'a> {
Ok(())
}
/// Iter 18c.4: emit a `void @drop_<module>_<TypeName>(ptr %p)`
/// function that decrements the refcount of every pointer-typed
/// field of every ctor, then frees the outer box.
///
/// Shape:
/// ```text
/// define void @drop_<m>_<T>(ptr %p) {
/// %tag = load i64, ptr %p
/// switch i64 %tag, label %dflt [
/// i64 0, label %arm0
/// ...
/// ]
/// arm_i:
/// for each pointer-typed field f_j:
/// %addr = gep ptr %p, i64 (8 + 8*j)
/// %v = load ptr, ptr %addr
/// call void @drop_<owner>_<FieldT>(ptr %v) ; or @ailang_rc_dec
/// br label %join
/// dflt:
/// unreachable
/// join:
/// call void @ailang_rc_dec(ptr %p)
/// ret void
/// }
/// ```
///
/// For ADTs with no boxed children every arm is empty and falls
/// straight through to `join`, which is just the final dec — see
/// the assignment's "always emit drop_X for every ADT" decision
/// (`rc_box_drop`'s `MkBox(Int)` is the canonical example).
///
/// Recursion: when a ctor field's type is the same ADT (or any
/// ADT in the workspace), the emitted call to
/// `@drop_<owner>_<T>(field)` is recursive at the IR level and
/// will overflow the stack on long lists. The 18e
/// `(drop-iterative)` annotation routes such types through
/// [`Self::emit_iterative_drop_fn_for_type`] instead, which
/// replaces the recursive call with a worklist push. ADTs WITHOUT
/// the annotation continue to use this recursive form — the
/// orchestrator's choice: opt-in iterative drop where the depth
/// is known to grow, recursive cascade everywhere else (cheaper
/// IR, no worklist allocation).
fn emit_drop_fn_for_type(&mut self, td: &TypeDef) {
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("entry:\n");
// Null guard: a null payload is a no-op (matches
// `runtime/rc.c::ailang_rc_dec`'s null guard).
out.push_str(" %is_null = icmp eq ptr %p, null\n");
out.push_str(" br i1 %is_null, label %ret, label %live\n");
out.push_str("live:\n");
out.push_str(" %tag = load i64, ptr %p, align 8\n");
let n_ctors = td.ctors.len();
// Switch over the tag. Each ctor gets one arm.
out.push_str(" switch i64 %tag, label %dflt [\n");
for (i, _) in td.ctors.iter().enumerate() {
out.push_str(&format!(" i64 {i}, label %arm_{i}\n"));
}
out.push_str(" ]\n");
// Per-ctor arm: iterate fields, dec the boxed ones.
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());
if lty != "ptr" {
continue;
}
let off = 8 + (j as i64) * 8;
let addr_id = local;
local += 1;
let val_id = local;
local += 1;
out.push_str(&format!(
" %a{addr_id} = getelementptr inbounds i8, ptr %p, i64 {off}\n"
));
out.push_str(&format!(
" %v{val_id} = load ptr, ptr %a{addr_id}, align 8\n"
));
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
// level. Otherwise this is the unbounded recursive
// cascade; safe only on bounded-depth ADTs (the
// `(drop-iterative)` annotation exists for the
// unbounded ones).
out.push_str(&format!(
" call void @{drop_call}(ptr %v{val_id})\n"
));
}
out.push_str(" br label %join\n");
}
// Default arm: unreachable when the typechecker has accepted
// the input — every legal box has one of the ctor tags.
out.push_str("dflt:\n");
if n_ctors == 0 {
// No ctors at all: a Type with zero ctors cannot be
// instantiated; the drop fn is dead. Still emit a
// br-to-join for IR validity.
out.push_str(" br label %join\n");
} else {
out.push_str(" unreachable\n");
}
// Join: free the outer box.
out.push_str("join:\n");
out.push_str(" call void @ailang_rc_dec(ptr %p)\n");
out.push_str(" br label %ret\n");
out.push_str("ret:\n");
out.push_str(" ret void\n");
out.push_str("}\n\n");
self.body.push_str(&out);
}
/// Iter 18e: emit `drop_<m>_<T>` for a `(drop-iterative)` type.
/// Replaces the recursive cascade in [`Self::emit_drop_fn_for_type`]
/// with an iterative-with-explicit-worklist body so cells of
/// arbitrary chain depth can free without consuming proportional
/// C stack.
///
/// IR shape:
/// ```text
/// define void @drop_<m>_<T>(ptr %p) {
/// entry:
/// %is_null = icmp eq ptr %p, null
/// br i1 %is_null, label %ret, label %init_wl
/// init_wl:
/// %wl = call ptr @ailang_drop_worklist_new()
/// call void @ailang_drop_worklist_push(ptr %wl, ptr %p)
/// br label %loop_head
/// loop_head:
/// %cur = call ptr @ailang_drop_worklist_pop(ptr %wl)
/// %done = icmp eq ptr %cur, null
/// br i1 %done, label %finish, label %dispatch
/// dispatch:
/// %tag = load i64, ptr %cur, align 8
/// switch i64 %tag, label %dflt [
/// i64 0, label %arm_0
/// ...
/// ]
/// arm_i:
/// for each pointer-typed field f_j of ctor i:
/// %addr = gep %cur, 8 + 8*j
/// %v = load ptr, ptr %addr
/// if field type is T (same as the type being dropped):
/// call void @ailang_drop_worklist_push(ptr %wl, ptr %v)
/// else:
/// call void @drop_<owner>_<F>(ptr %v) ; or @ailang_rc_dec
/// call void @ailang_rc_dec(ptr %cur)
/// br label %loop_head
/// dflt:
/// unreachable
/// finish:
/// call void @ailang_drop_worklist_free(ptr %wl)
/// br label %ret
/// ret:
/// ret void
/// }
/// ```
///
/// Mono-typed worklist. Every pointer pushed onto `%wl` is a `T`
/// (the type being dropped). For a field whose type is `T` itself
/// → push (continues the iterative cascade). For any other ADT
/// field type `T'` → call `drop_<m'>_<T'>` directly: if `T'` is
/// also `(drop-iterative)`, that fn allocates its own worklist
/// instance (no nesting); if `T'` is non-iterative, it recurses
/// stack-wise (depth bounded by the number of *distinct* nested
/// ADTs reachable from `T`, which is small in practice).
///
/// This interpretation of the assignment's "should also use the
/// worklist" clause was chosen because a heterogeneously-typed
/// worklist would require storing a (ptr, drop-handler) tuple per
/// entry plus a vtable dispatch on pop — significant complexity
/// for the case where two distinct ADTs are mutually recursive
/// AND both are drop-iterative AND the chain is millions deep.
/// That triple-conjunct is not on the 18-arc's critical path; if
/// it surfaces in practice, a follow-up iter can extend the
/// worklist entry shape. The mono-typed version captures the
/// stack-overflow-on-long-self-chains problem fully.
fn emit_iterative_drop_fn_for_type(&mut self, td: &TypeDef) {
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("entry:\n");
// Null guard — symmetric with the recursive variant. A null
// payload skips worklist allocation entirely.
out.push_str(" %is_null = icmp eq ptr %p, null\n");
out.push_str(" br i1 %is_null, label %ret, label %init_wl\n");
out.push_str("init_wl:\n");
out.push_str(" %wl = call ptr @ailang_drop_worklist_new()\n");
out.push_str(
" call void @ailang_drop_worklist_push(ptr %wl, ptr %p)\n",
);
out.push_str(" br label %loop_head\n");
out.push_str("loop_head:\n");
out.push_str(
" %cur = call ptr @ailang_drop_worklist_pop(ptr %wl)\n",
);
out.push_str(" %done = icmp eq ptr %cur, null\n");
out.push_str(" br i1 %done, label %finish, label %dispatch\n");
out.push_str("dispatch:\n");
out.push_str(" %tag = load i64, ptr %cur, align 8\n");
let n_ctors = td.ctors.len();
out.push_str(" switch i64 %tag, label %dflt [\n");
for (i, _) in td.ctors.iter().enumerate() {
out.push_str(&format!(" i64 {i}, label %arm_{i}\n"));
}
out.push_str(" ]\n");
// Per-ctor arms. For each pointer-typed field decide push vs
// direct call based on whether the field's type is the same
// as the type being dropped.
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());
if lty != "ptr" {
continue;
}
let off = 8 + (j as i64) * 8;
let addr_id = local;
local += 1;
let val_id = local;
local += 1;
out.push_str(&format!(
" %a{addr_id} = getelementptr inbounds i8, ptr %cur, i64 {off}\n"
));
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) {
// Same-type field: push onto the worklist —
// continues the iterative cascade. Null-guarding
// is handled inside `ailang_drop_worklist_push`
// itself (skips null payloads).
out.push_str(&format!(
" call void @ailang_drop_worklist_push(ptr %wl, ptr %v{val_id})\n"
));
} else {
// Different-type field: dispatch to that type's
// own drop fn (which itself decides recursive vs.
// 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);
out.push_str(&format!(
" call void @{drop_call}(ptr %v{val_id})\n"
));
}
}
// Dec the outer cell. Worklist holds no other reference
// to this pointer (push happened exactly once on the
// parent's cascade, and pop just removed that entry), so
// the cell's refcount drops by exactly one here. Children
// pushed above keep their own refcounts pending until
// their loop iteration.
out.push_str(" call void @ailang_rc_dec(ptr %cur)\n");
out.push_str(" br label %loop_head\n");
}
// Default arm: unreachable when the typechecker has accepted
// the input. Same shape as the recursive variant.
out.push_str("dflt:\n");
if n_ctors == 0 {
out.push_str(" br label %finish\n");
} else {
out.push_str(" unreachable\n");
}
out.push_str("finish:\n");
out.push_str(" call void @ailang_drop_worklist_free(ptr %wl)\n");
out.push_str(" br label %ret\n");
out.push_str("ret:\n");
out.push_str(" ret void\n");
out.push_str("}\n\n");
self.body.push_str(&out);
}
/// Iter 18e helper: is `fty` the same ADT as `td_name` in the
/// current module? Used by the iterative-drop body to decide
/// "push to worklist" (same type) vs. "call its drop fn directly"
/// (different type).
///
/// Returns `true` only when the field type is a `Type::Con`
/// referencing `td_name` AND the reference resolves to the
/// current module (bare or qualified-but-self). Qualified names
/// pointing at *other* modules are different types — even when
/// they spell the same suffix. Type-vars and fn-types are never
/// the same as the ADT being dropped (parametric self-recursion
/// could bind a var to T, but the bound is invisible at codegen
/// since we don't monomorphise drop fns).
fn field_is_same_type(&self, fty: &Type, td_name: &str) -> bool {
match fty {
Type::Con { name, .. } => {
if name.matches('.').count() == 1 {
let (prefix, suffix) = name.split_once('.').expect("checked");
if suffix != td_name {
return false;
}
// Resolve the prefix; same-module iff the resolved
// target equals self.module_name.
let target = self
.import_map
.get(prefix)
.map(|s| s.as_str())
.unwrap_or(prefix);
target == self.module_name
} else {
name == td_name
}
}
_ => false,
}
}
/// Iter 18c.4: pick the drop-fn symbol to call for a single
/// pointer-typed field. Routes ADT fields to their own
/// `drop_<owner>_<T>` symbol so the recursion cascades through
/// recursive types (List, Tree). Falls back to `ailang_rc_dec`
/// for non-ADT pointer types (Str, fn-typed, unresolved Var) —
/// those are not user-defined ADTs and have no per-type drop fn.
fn field_drop_call(&self, fty: &Type) -> String {
match fty {
Type::Con { name, .. } => {
// Built-in pointer-typed cons: Str. No drop fn —
// shallow `ailang_rc_dec` is the right answer (Str
// payloads are NUL-terminated bytes in static
// memory; nothing to recurse into).
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)
}
// Type::Fn (closure-typed field) → no per-type drop fn
// exists for closures (each closure has its own per-pair
// drop fn keyed by the lam-id, not by type). Iter 18c.4
// shallow-frees the closure-pair; the env and any
// captured ADT fields leak. A future iter that types
// closure-typed fields with a runtime descriptor pointer
// would close this. For 18c.4's recursive-ADT story this
// is acceptable — the shipping fixtures don't store
// closures inside ADT fields.
Type::Fn { .. } => "ailang_rc_dec".to_string(),
// Type::Var (parameterised ADT field whose type-arg was
// not pinned at declaration): we don't know the field's
// concrete shape from the static decl alone, and the
// generated drop fn is a single symbol per ADT (no
// monomorphisation). Shallow free is the conservative
// choice — boxed children of polymorphic-typed fields
// leak. Iter 18d/18e will revisit this when reuse hints
// and the worklist allocator land.
Type::Var { .. } | Type::Forall { .. } => "ailang_rc_dec".to_string(),
}
}
/// Iter 12b: emit one specialised version of a polymorphic def.
/// Substitutes rigid vars in both the type and the body, then
/// calls `emit_fn` against a synthetic FnDef whose `name` already
@@ -1543,210 +1161,6 @@ impl<'a> Emitter<'a> {
));
}
/// Iter 18c.3: 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.
///
/// Returns `true` exactly when:
/// - the active allocator is `Rc`,
/// - `value` is a `Term::Ctor` or `Term::Lam` (the two AST shapes
/// that lower through the heap-allocation path), AND
/// - the term is *not* in the current fn's `non_escape` set —
/// escaping ctors/lambdas go through `ailang_rc_alloc`,
/// non-escaping ones become stack `alloca`s and must NOT be
/// `dec`'d (they are freed by LLVM at fn return).
///
/// Iter 18g.2 widens this to include `Term::App` whose callee's
/// 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.
///
/// Other value shapes (vars, literals, matches, …) return `false`
/// here. A `Term::Var` returning an RC-allocated box would already
/// be tracked by an earlier let-binder; tracking it again here
/// would double-dec.
fn is_rc_heap_allocated(&self, value: &Term) -> bool {
if !matches!(self.alloc, AllocStrategy::Rc) {
return false;
}
match value {
Term::Ctor { .. } | Term::Lam { .. } => {
let term_ptr = (value as *const Term) as usize;
!self.non_escape.contains(&term_ptr)
}
Term::App { callee, .. } => {
// Iter 18g.2: 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.
self.synth_callee_ret_mode(callee)
.map(|m| matches!(m, ParamMode::Own))
.unwrap_or(false)
}
_ => false,
}
}
/// Iter 18g.2: lookup helper for a callee's `ret_mode`. Returns
/// `Some(mode)` when the callee resolves to a fn-typed term;
/// `None` for shapes whose type is not a `Type::Fn` (typechecker
/// would already have rejected an App on a non-fn, but the helper
/// is defensive). Used by [`Self::is_rc_heap_allocated`] and the
/// [`Self::drop_symbol_for_binder`] App-arm to decide both
/// trackability and the drop-fn symbol.
fn synth_callee_ret_mode(&self, callee: &Term) -> Option<ParamMode> {
let cty = self.synth_arg_type(callee).ok()?;
match cty {
Type::Fn { ret_mode, .. } => Some(ret_mode),
_ => None,
}
}
/// Iter 18c.4: pick the drop-fn symbol to call at the close of a
/// trackable `Term::Let` scope. For a `Term::Ctor` binder the
/// symbol is `drop_<owner>_<TypeName>` — derived from the ctor's
/// `type_name` (which already encodes the owning module via the
/// `module.T` form when cross-module). For a `Term::Lam` binder
/// the symbol comes from `closure_drops`, populated by
/// [`lower_lambda`] when it emitted the per-pair drop fn.
///
/// Falls back to `ailang_rc_dec` for any other shape — should be
/// unreachable since `is_rc_heap_allocated` only returns `true`
/// for `Term::Ctor` / `Term::Lam`, but a defensive fallback
/// keeps the IR well-formed even if the predicate ever widens.
fn drop_symbol_for_binder(&self, value: &Term, val_ssa: &str) -> String {
match value {
Term::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)
}
Term::Lam { .. } => self
.closure_drops
.get(val_ssa)
.cloned()
.unwrap_or_else(|| "ailang_rc_dec".to_string()),
// Iter 18g.2: an Own-returning call hands a freshly heap-
// allocated cell whose static type is the callee's
// `ret`. Resolve the per-type drop fn from that ret-type
// so the cascade walks the cell's pointer-typed children
// (e.g. an Own-returned `Tree` fans out via
// `drop_<m>_<Tree>`). Falls back to `ailang_rc_dec` if
// the ret-type is not a `Type::Con` (e.g. a bare type
// var on an as-yet-unmonomorphised polymorphic call —
// the monomorphised copies will resolve correctly).
Term::App { .. } => {
if let Ok(ret_ty) = self.synth_arg_type(value) {
if let Type::Con { name, .. } = ret_ty {
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);
}
}
"ailang_rc_dec".to_string()
}
_ => "ailang_rc_dec".to_string(),
}
}
/// Iter 18d.3: emit a partial-drop sequence inline at a let-close
/// site whose binder has moved-out pattern slots. Replaces the
/// uniform `drop_<m>_<T>(ptr)` call: load each pointer-typed
/// field whose slot index is NOT in `moved`, dispatch through
/// `field_drop_call` (the same per-type or shallow drop the
/// recursive cascade picks), then `ailang_rc_dec` the outer
/// box. Skips slots in `moved` entirely — those values were
/// transferred to a pattern-bound binder that owns the dec
/// for them.
///
/// Iter 18g.2 widens the input set: with `Term::App` (Own-
/// returning call) now also trackable, a let-binder whose value is
/// `Term::App` may accumulate moved slots if its body pattern-
/// matches the binder. The per-field static-ctor emission below
/// is not usable in that case (the runtime tag is dynamic, not
/// statically known from `value`'s shape); fall back to shallow
/// `ailang_rc_dec`. The non-moved field cells leak under that
/// path — same dynamic-tag partial-drop debt that 18d.4's match
/// arm already documents.
fn emit_inlined_partial_drop(
&mut self,
value: &Term,
val_ssa: &str,
moved: &BTreeSet<usize>,
) -> Result<()> {
let (type_name, ctor_name) = match value {
Term::Ctor { type_name, ctor, .. } => (type_name.as_str(), ctor.as_str()),
_ => {
// Iter 18g.2 fallback: dynamic-tag partial-drop debt.
// `value` is `Term::App` (Own-returning) or `Term::Lam`
// — `is_rc_heap_allocated` returns true for both, and a
// body that pattern-matches such a binder will populate
// `moved_slots`. We don't know the active ctor
// statically, so we shallow-dec the outer cell. Non-
// moved field cells leak under this path; closing the
// leak requires a tag-conditional partial-drop helper
// (analogous to the per-type drop fn but parameterised
// on the tag). Tracked as future work — same family as
// 18d.4's `(case (LCons h t) ...)` carve-out.
let _ = (val_ssa, moved); // keep compiler quiet about unused
self.body.push_str(&format!(
" call void @ailang_rc_dec(ptr {val_ssa})\n"
));
return Ok(());
}
};
let cref = self.lookup_ctor_by_type(type_name, ctor_name)?;
// 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());
if lty != "ptr" {
continue;
}
if moved.contains(&idx) {
continue;
}
let off = 8 + (idx as i64) * 8;
let addr = self.fresh_ssa();
self.body.push_str(&format!(
" {addr} = getelementptr inbounds i8, ptr {val_ssa}, i64 {off}\n"
));
let v = self.fresh_ssa();
self.body.push_str(&format!(
" {v} = load ptr, ptr {addr}, align 8\n"
));
let drop_call = self.field_drop_call(fty_ail);
self.body.push_str(&format!(
" call void @{drop_call}(ptr {v})\n"
));
}
// Finally dec the outer box. The per-type drop fn would have
// done this in its `join` block; we replicate it here.
self.body.push_str(&format!(
" call void @ailang_rc_dec(ptr {val_ssa})\n"
));
Ok(())
}
/// Lowers a term to (SSA value string, LLVM type).
fn lower_term(&mut self, t: &Term) -> Result<(String, String)> {
match t {
@@ -2143,7 +1557,7 @@ impl<'a> Emitter<'a> {
/// under a swapped `module_name` (see `emit_specialised_fn`)
/// resolve its bare ctor references against the *owner* module's
/// ctor table, not the consumer's.
fn lookup_ctor_by_type(
pub(crate) fn lookup_ctor_by_type(
&self,
type_name: &str,
ctor_name: &str,
@@ -3796,81 +3210,6 @@ impl<'a> Emitter<'a> {
Ok((clos, "ptr".into()))
}
/// Iter 18c.4: build the IR text for a closure env's drop fn.
/// Layout: 8 bytes per capture, in declaration order.
/// For each pointer-typed capture, emit a load + drop call;
/// finally `ailang_rc_dec` the env block.
fn build_env_drop_fn(
&self,
sym: &str,
cap_meta: &[(String, String, String, Type, Option<FnSig>)],
) -> String {
let mut out = String::new();
out.push_str(&format!("define void @{sym}(ptr %env) {{\nentry:\n"));
out.push_str(" %is_null = icmp eq ptr %env, null\n");
out.push_str(" br i1 %is_null, label %ret, label %live\n");
out.push_str("live:\n");
let mut local = 0u64;
for (i, (_cname, _outer_ssa, lty, ail_ty, _sig)) in cap_meta.iter().enumerate() {
if lty != "ptr" {
continue;
}
let off = (i as i64) * 8;
let addr_id = local;
local += 1;
let val_id = local;
local += 1;
out.push_str(&format!(
" %a{addr_id} = getelementptr inbounds i8, ptr %env, i64 {off}\n"
));
out.push_str(&format!(
" %v{val_id} = load ptr, ptr %a{addr_id}, align 8\n"
));
let drop_call = self.field_drop_call(ail_ty);
out.push_str(&format!(
" call void @{drop_call}(ptr %v{val_id})\n"
));
}
out.push_str(" call void @ailang_rc_dec(ptr %env)\n");
out.push_str(" br label %ret\n");
out.push_str("ret:\n");
out.push_str(" ret void\n}\n\n");
out
}
/// Iter 18c.4: build the IR text for a closure pair's drop fn.
/// Layout: { ptr thunk, ptr env } — env at offset 8.
/// Loads env, calls the env drop, then decs the pair box.
fn build_pair_drop_fn(
&self,
sym: &str,
env_drop: &str,
has_env: bool,
) -> String {
let mut out = String::new();
out.push_str(&format!("define void @{sym}(ptr %p) {{\nentry:\n"));
out.push_str(" %is_null = icmp eq ptr %p, null\n");
out.push_str(" br i1 %is_null, label %ret, label %live\n");
out.push_str("live:\n");
if has_env {
out.push_str(
" %ea = getelementptr inbounds {{ ptr, ptr }}, ptr %p, i64 0, i32 1\n",
);
out.push_str(" %env = load ptr, ptr %ea, align 8\n");
out.push_str(&format!(" call void @{env_drop}(ptr %env)\n"));
} else {
// No env — the env-drop is still emitted (uniform shape)
// but is a no-op on null. Skip the load and call dec
// directly on the pair.
let _ = env_drop;
}
out.push_str(" call void @ailang_rc_dec(ptr %p)\n");
out.push_str(" br label %ret\n");
out.push_str("ret:\n");
out.push_str(" ret void\n}\n\n");
out
}
/// Iter 8b: walk a Term collecting free-variable names that should
/// be captured by an enclosing lambda. Builtins and top-level fns
/// are excluded — they're globally accessible. Qualified names
@@ -4238,7 +3577,7 @@ impl<'a> Emitter<'a> {
}
}
fn fresh_ssa(&mut self) -> String {
pub(crate) fn fresh_ssa(&mut self) -> String {
self.counter += 1;
format!("%v{}", self.counter)
}
@@ -4277,7 +3616,7 @@ impl<'a> Emitter<'a> {
/// to the return type. The body of a let is walked with the
/// let-bound name added to a small `extras` shadow stack so we
/// don't need `&mut self`.
fn synth_arg_type(&self, t: &Term) -> Result<Type> {
pub(crate) fn synth_arg_type(&self, t: &Term) -> Result<Type> {
self.synth_with_extras(t, &[])
}