diff --git a/crates/ailang-codegen/src/drop.rs b/crates/ailang-codegen/src/drop.rs new file mode 100644 index 0000000..6c99353 --- /dev/null +++ b/crates/ailang-codegen/src/drop.rs @@ -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__(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__(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"); + 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__` 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); + } + + /// 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__` 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 { + 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__` — 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__`). 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__(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, + ) -> 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__` (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)], + ) -> 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 + } +} diff --git a/crates/ailang-codegen/src/lib.rs b/crates/ailang-codegen/src/lib.rs index 0891409..2daa300 100644 --- a/crates/ailang-codegen/src/lib.rs +++ b/crates/ailang-codegen/src/lib.rs @@ -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__(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). - 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__` 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. - 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__` 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 { - 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__` — 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__`). 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__(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, - ) -> 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__` (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)], - ) -> 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 { + pub(crate) fn synth_arg_type(&self, t: &Term) -> Result { self.synth_with_extras(t, &[]) }