//! 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__(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::{ParamMode, Type, TypeDef}; use ailang_mir::{Callee, MTerm}; use std::collections::{BTreeMap, BTreeSet}; use super::synth::llvm_type; use super::{AllocStrategy, Emitter, FnSig, Result}; impl<'a> Emitter<'a> { /// emit a `void @drop__(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__(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__(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__(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) { // Monomorphic / intrinsic ADTs: one drop fn, empty subst, no // suffix — byte-identical to the pre-leg-C emission. // 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). self.emit_drop_fn_wrapper(td, Self::emit_drop_fn_for_instantiation); } /// Shared wrapper for [`Self::emit_drop_fn_for_type`], /// [`Self::emit_iterative_drop_fn_for_type`], and /// [`Self::emit_partial_drop_fn_for_type`]: the three differ only in /// which `*_for_instantiation` emitter they dispatch to. For a /// monomorphic / intrinsic ADT (`!is_suffixed`) emit exactly one fn /// with an empty subst and empty suffix; for a leg-C polymorphic /// ADT emit one fn per concrete instantiation, in /// `drop_monos.instantiations` order. The set and order of emitted /// functions — hence the emitted IR — is identical to the /// pre-dedup form. The instantiation plan is materialised up front /// so all `self.drop_monos` reads finish before `emit` borrows /// `&mut self`. fn emit_drop_fn_wrapper( &mut self, td: &TypeDef, emit: fn(&mut Self, &TypeDef, &BTreeMap, &str), ) { let key = (self.module_name.to_string(), td.name.clone()); if !self.drop_monos.is_suffixed(&key) { emit(self, td, &BTreeMap::new(), ""); return; } let plan: Vec<(BTreeMap, String)> = self .drop_monos .instantiations(&key) .into_iter() .map(|args| { let subst: BTreeMap = td.vars.iter().cloned().zip(args.iter().cloned()).collect(); let suffix = self .drop_monos .suffix_for(&key, &args) .unwrap_or_default(); (subst, suffix) }) .collect(); for (subst, suffix) in &plan { emit(self, td, subst, suffix); } } /// emit one recursive `drop__` 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, 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}{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). 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"); // FROZEN ABI (embedding boundary) — see design/contracts/0003-embedding-abi.md. 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_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; } // FROZEN ABI (embedding boundary) — see design/contracts/0003-embedding-abi.md. 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); } /// emit `drop__` 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__(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__(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__` 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) { self.emit_drop_fn_wrapper(td, Self::emit_iterative_drop_fn_for_instantiation); } /// emit one iterative `drop__` 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, 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}{sym_suffix}(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_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; } 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); } /// 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, } } /// pick the drop-fn symbol to call for a single /// pointer-typed field. Routes ADT fields to their own /// `drop__` 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, 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 // ABI (len at offset 0, bytes at offset 8): // - heap-Str: malloc'd slab with real rc_header // at `payload - 8`; rc_dec is the correct // refcount-and-free path. // - static-Str: packed-struct LLVM global // <{ i64, [N x i8] }> in .rodata, no rc_header // slot; rc_dec would read undefined bytes at // `payload - 8`. Codegen-level elision // (`emit_inlined_partial_drop` move-tracking // from iter 18d.3 + non-escape lowering from // iter 18b) keeps static-Str pointers out of // this call along every shipping execution // path. The codegen-level invariant is the // protection; no runtime guard backs it up. if matches!(name.as_str(), "Str") { return "ailang_rc_dec".to_string(); } // Resolve owner + per-monomorph suffix in one place // (leg C). A polymorphic non-intrinsic ADT instantiation // (`Box Int`, `Pair Int Int`) gets the same `__` // 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 // 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(), } } /// resolve the `drop__` 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 { self.adt_symbol("drop_", name, args) } /// resolve the `partial_drop__` 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 { self.adt_symbol("partial_drop_", name, args) } /// shared core of [`Self::adt_drop_symbol`] and /// [`Self::adt_partial_drop_symbol`]: build the `_` /// base from the resolved ADT key and append the leg-C per-monomorph /// suffix when one applies. The two public symbols differ only in the /// `prefix` literal (`drop_` vs `partial_drop_`); the emitted string is /// byte-identical to the pre-dedup form, which matters because these are /// IR symbol names (a mismatch is a link error). fn adt_symbol(&self, prefix: &str, name: &str, args: &[Type]) -> String { let key = super::dropmono::resolve_adt_key( name, self.module_name, &self.import_map, ); let base = format!("{prefix}{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. /// /// 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). /// /// The widened input set includes `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`-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 /// be tracked by an earlier let-binder; tracking it again here /// would double-dec. pub(crate) fn is_rc_heap_allocated(&self, value: &MTerm) -> bool { if !matches!(self.alloc, AllocStrategy::Rc) { return false; } match value { MTerm::Ctor { .. } | MTerm::Lam { .. } => { let term_ptr = (value as *const MTerm) as usize; !self.non_escape.contains(&term_ptr) } MTerm::App { callee, .. } => { // a call whose callee carries // `ret_mode == Own` hands a fresh heap allocation to // 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) } MTerm::Loop { .. } => { // Loop result is owned-and-untracked (seeds are moved in). // Track iff its static type is boxed/heap (llvm `ptr`). // mir.4 removed the former `Str` carve-out here: a loop // that returns `Str` now always returns an owned heap // slab, because `lower_to_mir` promotes every `Str` // literal in a loop-carried position — seed inits, recur // args, AND tail (exit-arm) result literals — to // `StrRep::Heap` (codegen `str_clone`s each into a fresh // `rc_header` slab). So `ailang_rc_dec` on a loop-`Str` // result is sound; the static-literal UB that motivated // the carve-out cannot reach a loop result. Unboxed // primitives (Int/Bool/Float/Unit) lower to non-`ptr` and // are correctly excluded by the `ptr` gate. let t = value.ty(); let is_ptr = matches!( crate::synth::llvm_type(&t).as_deref(), Ok("ptr") ); is_ptr } _ => false, } } /// 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: &Callee) -> Option { // A resolved callee carries its fn-type as `sig`; an indirect // one carries it on the boxed sub-term. Both yield the callee // `ret_mode` identically — there is no behaviour change from // mir.1b, only a different place the same fn-type is read from. let cty = match callee { Callee::Indirect(inner) => inner.ty(), Callee::Static { sig, .. } | Callee::Builtin { sig, .. } => sig.clone(), }; match cty { Type::Fn { ret_mode, .. } => Some(ret_mode), _ => None, } } /// 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__` — 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: &MTerm, val_ssa: &str) -> String { match value { MTerm::Ctor { type_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__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 .get(val_ssa) .cloned() .unwrap_or_else(|| "ailang_rc_dec".to_string()), // 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__`). 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). MTerm::App { .. } | MTerm::Loop { .. } => { 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 // (codegen-elision keeps static pointers out of this // path) consume via `ailang_rc_dec`. if name == "Str" { return "ailang_rc_dec".to_string(); } return self.adt_drop_symbol(&name, &args); } "ailang_rc_dec".to_string() } _ => "ailang_rc_dec".to_string(), } } /// tag-conditional partial-drop helper. /// Emits `void @partial_drop__(ptr %p, i64 %mask)` — /// structurally parallel to [`Self::emit_drop_fn_for_type`] but /// gates each ptr-field dec on a bit of `%mask`. If bit j of the /// mask is set, slot j was moved out of the cell (its content /// belongs to a downstream owner) and the dec is skipped; if the /// bit is clear, the field is loaded and routed through /// `field_drop_call` (same dispatch the recursive cascade uses). /// The outer box is always dec'd at `join`. /// /// Closes the three dynamic-tag carve-outs that previously /// fell back to shallow `ailang_rc_dec`: /// 1. `lib.rs` Iter B Own-param dec at fn-return when /// `moved_slots[param]` is non-empty; /// 2. `match_lower.rs` Iter A arm-close pattern-binder dec /// when the binder was itself the scrutinee of an inner /// match that moved out some fields; /// 3. [`Self::emit_inlined_partial_drop`]'s non-Ctor branch /// (let-binder whose value is `Term::App`). /// /// All three share the same shape: a binder whose runtime ctor /// tag is dynamic (not statically known from a `Term::Ctor`) /// and whose `moved_slots` is a strict subset of its ptr fields. /// Pre-fu2 the unmoved fields leaked silently; fu2 routes them /// through this helper. /// /// IR shape (analogous to `emit_drop_fn_for_type`): /// ```text /// define void @partial_drop__(ptr %p, i64 %mask) { /// %is_null = icmp eq ptr %p, null /// br i1 %is_null, label %ret, label %live /// live: /// %tag = load i64, ptr %p /// switch i64 %tag, label %dflt [...] /// arm_i: /// ; for each ptr field f_j of ctor i: /// %b_j = and i64 %mask, (1 << j) /// %s_j = icmp ne i64 %b_j, 0 /// br i1 %s_j, label %after_i_j, label %do_i_j /// do_i_j: /// %a = gep i8, ptr %p, (8 + 8*j) /// %v = load ptr, ptr %a /// call void @(ptr %v) /// br label %after_i_j /// after_i_j: /// ; next field, or br label %join /// dflt: unreachable /// join: /// call void @ailang_rc_dec(ptr %p) /// br label %ret /// ret: /// ret void /// } /// ``` /// /// One helper per ADT, emitted alongside `drop__`. We do /// NOT emit a `(drop-iterative)` partial-drop variant: the /// helper runs once on the binder (carve-out sites are not /// cascade points — the unmoved fields go through their own /// `drop__` which itself decides recursive vs iterative). pub(crate) fn emit_partial_drop_fn_for_type(&mut self, td: &TypeDef) { self.emit_drop_fn_wrapper(td, Self::emit_partial_drop_fn_for_instantiation); } /// emit one `partial_drop__` 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, 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}{sym_suffix}(ptr %p, i64 %mask) {{\n" )); out.push_str("entry:\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"); out.push_str(" %tag = load i64, ptr %p, 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"); let mut local = 0u64; for (i, ctor) in td.ctors.iter().enumerate() { out.push_str(&format!("arm_{i}:\n")); 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; } let bit_id = local; local += 1; let set_id = local; local += 1; let bitmask: u64 = 1u64 << j; out.push_str(&format!( " %b{bit_id} = and i64 %mask, {bitmask}\n" )); out.push_str(&format!( " %s{set_id} = icmp ne i64 %b{bit_id}, 0\n" )); out.push_str(&format!( " br i1 %s{set_id}, label %after_{i}_{j}, label %do_{i}_{j}\n" )); out.push_str(&format!("do_{i}_{j}:\n")); 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); out.push_str(&format!( " call void @{drop_call}(ptr %v{val_id})\n" )); out.push_str(&format!(" br label %after_{i}_{j}\n")); out.push_str(&format!("after_{i}_{j}:\n")); } out.push_str(" br label %join\n"); } out.push_str("dflt:\n"); if n_ctors == 0 { out.push_str(" br label %join\n"); } else { out.push_str(" unreachable\n"); } 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); } /// raw-buf.4: emit the flat `drop__` for an /// intrinsic-storage TypeDef (e.g. RawBuf). The slab is /// `[size:i64][primitive elements]` with no tag at offset 0, so /// the generic tag-switch drop is wrong; the elements are /// primitives carrying no recursive drops. The flat drop is just /// the null-guarded outer rc-dec — same `entry/live/ret` shape /// and `@ailang_rc_dec` call form as the generic drop's `join` /// tail, minus the tag-switch. pub(crate) fn emit_flat_intrinsic_drop_fn(&mut self, td: &TypeDef) { self.emit_flat_intrinsic_drop_fn_inner(td, "drop", "ptr %p"); } /// raw-buf.4: emit the flat `partial_drop__` for an /// intrinsic-storage TypeDef. Emitted for symbol-resolution /// parity with the generic path (every TypeDef gets both a drop /// and a partial-drop). No carve-out site can actually call it — /// the sole ctor field is a primitive, never a moved-out ptr /// field — so the `%mask` arg is ignored and the body is the same /// flat outer rc-dec as the drop fn. pub(crate) fn emit_flat_intrinsic_partial_drop_fn(&mut self, td: &TypeDef) { self.emit_flat_intrinsic_drop_fn_inner(td, "partial_drop", "ptr %p, i64 %mask"); } /// Shared body of [`Self::emit_flat_intrinsic_drop_fn`] and /// [`Self::emit_flat_intrinsic_partial_drop_fn`]. The two differ /// only in the symbol `prefix` (`drop` vs `partial_drop`) and the /// `params` list (`ptr %p` vs `ptr %p, i64 %mask`); everything from /// `entry:` down — the null guard, the single `@ailang_rc_dec`, and /// the `ret` tail — is identical, since the `%mask` is ignored. /// Interpolating `prefix`/`params` reproduces each function's old /// IR byte-for-byte. fn emit_flat_intrinsic_drop_fn_inner( &mut self, td: &TypeDef, prefix: &str, params: &str, ) { let m = self.module_name; let tname = &td.name; let mut out = String::new(); out.push_str(&format!( "define void @{prefix}_{m}_{tname}({params}) {{\n" )); out.push_str("entry:\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"); 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); } /// resolve the `partial_drop__` /// symbol for a type, parallel to the existing per-type drop /// dispatch in `field_drop_call`. Returns `None` for non-ADT /// types (Str, fn-typed, type-vars) — callers fall back to /// shallow `ailang_rc_dec` in those cases (no per-type drop /// fn exists either, and `moved_slots` would not have been /// populated for those shapes anyway). pub(crate) fn partial_drop_symbol_for_type(&self, ty: &Type) -> Option { match ty { Type::Con { name, args } => { if name == "Str" { return None; } Some(self.adt_partial_drop_symbol(name, args)) } _ => None, } } /// build the i64 moved-slots bitmask for a /// `partial_drop__` call. Bit j is set iff slot j is in /// `moved`. We reject slot indices ≥ 64 with `None` — no /// language-level ADT has that many fields, but the cap is /// load-bearing because the helper's mask is a single i64. /// On overflow, callers fall back to shallow dec (correctness- /// safe; carve-out leak path remains as before, but the cap is /// effectively unreachable). pub(crate) fn build_moved_mask(moved: &BTreeSet) -> Option { let mut mask: u64 = 0; for &idx in moved { if idx >= 64 { return None; } mask |= 1u64 << idx; } Some(mask) } /// emit a partial-drop sequence inline at a let-close /// site whose binder has moved-out pattern slots. Replaces the /// uniform `drop__(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. /// /// The widened input set includes `Term::App` (Own-returning /// call). The static per-field emission below is keyed /// against a `Term::Ctor`'s known ctor; a `Term::App` binder /// whose body pattern-matches it has a *dynamic* runtime tag. /// instead of falling back /// to shallow `ailang_rc_dec` (which leaked the unmoved fields), /// we route through the tag-conditional helper /// [`Self::emit_partial_drop_fn_for_type`], which dispatches on /// the runtime tag and dec's only the unmoved fields. pub(crate) fn emit_inlined_partial_drop( &mut self, value: &MTerm, val_ssa: &str, moved: &BTreeSet, ) -> Result<()> { let (type_name, ctor_name) = match value { MTerm::Ctor { type_name, ctor, .. } => (type_name.as_str(), ctor.as_str()), _ => { // dynamic-tag partial-drop via the // per-type helper. `value` is `MTerm::App` (Own- // returning) — the binder's static type is the App's // ret type, read off `value.ty()` and mapped through // `partial_drop_symbol_for_type`. `MTerm::Lam` shapes // never reach here with a non-empty `moved` (you can't // pattern-match a closure-pair); the fallback below // handles them defensively. let sym = self.partial_drop_symbol_for_type(&value.ty()); if let (Some(sym), Some(mask)) = (sym, Self::build_moved_mask(moved)) { self.body.push_str(&format!( " call void @{sym}(ptr {val_ssa}, i64 {mask})\n" )); } else { 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)?; // 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 = 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__` (ADTs cascade) or // `ailang_rc_dec` (Str / closures / vars). 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; } 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(()) } /// 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)], ) -> 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 } /// 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 { // The pair layout is `{ ptr thunk, ptr env }`; env is the // second field. Note the single braces — this is a plain // `push_str`, not a `format!` call, so brace-escaping does // not apply. (Iter 18c.4 originally shipped doubled braces // here; surfaced when the rc backend became the corpus // default and a closure-with-captures escaped.) 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 } }