//! 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::*; use std::collections::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) { 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"); // FROZEN ABI (embedding boundary) — see design/contracts/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) 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; } // FROZEN ABI (embedding boundary) — see design/contracts/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) { 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); } /// 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, .. } => { // 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(); } // 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(), } } /// 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`/`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, .. } => { // 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, } } /// 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 { let cty = self.synth_arg_type(callee).ok()?; 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: &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()), // 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). Term::App { .. } => { if let Ok(Type::Con { name, .. }) = self.synth_arg_type(value) { // 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(); } 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(), } } /// 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) { let m = self.module_name; let tname = &td.name; let mut out = String::new(); out.push_str(&format!( "define void @partial_drop_{m}_{tname}(ptr %p, i64 %mask) {{\n" )); 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) in ctor.fields.iter().enumerate() { 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); } /// 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, .. } => { if name == "Str" { return None; } if name.matches('.').count() == 1 { let (prefix, suffix) = name.split_once('.').expect("checked"); if let Some(target) = self.import_map.get(prefix) { return Some(format!("partial_drop_{target}_{suffix}")); } return Some(format!("partial_drop_{prefix}_{suffix}")); } Some(format!( "partial_drop_{m}_{name}", m = self.module_name )) } _ => 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: &Term, val_ssa: &str, moved: &BTreeSet, ) -> Result<()> { let (type_name, ctor_name) = match value { Term::Ctor { type_name, ctor, .. } => (type_name.as_str(), ctor.as_str()), _ => { // dynamic-tag partial-drop via the // per-type helper. `value` is `Term::App` (Own- // returning) — the binder's static type is the App's // ret type, recovered through `synth_arg_type` / // `partial_drop_symbol_for_type`. `Term::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 .synth_arg_type(value) .ok() .and_then(|ty| self.partial_drop_symbol_for_type(&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)?; // 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_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(()) } /// 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 } }