7d9ab4d29d
Emitted IR is byte-identical for every case — proven by the IR-pin
suites (eq_primitives_pin, ord_int_intercept_ir_pin, the raw_buf_*
and drop/leak pins) and the full e2e suite, all green.
intercepts.rs:
- The twelve emit_rawbuf_{new,get,set,size}_{int,float,bool} functions
were copy-paste across three element types, differing only in the
element width (8/8/1) and the LLVM load/store type (i64/double/i1).
Collapsed to four parameterised cores; the element variants are thin
call sites passing the (width, type) pair.
- Replaced the repeated, off-by-one-prone parameter-SSA extraction
idiom (`locals[n-2]`, `locals[n-1]`) with an Emitter::last_param_ssas
helper.
- Factored the shared IR-Str bytes GEP out of emit_eq_str /
emit_compare_str into emit_str_bytes_gep.
drop.rs:
- The three emit_*_drop_fn_for_type methods shared one wrapping shape
(monomorphic short-circuit, else loop over instantiations dispatching
to a *_for_instantiation helper); hoisted into emit_drop_fn_wrapper
taking the per-instantiation emitter as a fn pointer.
- emit_flat_intrinsic_drop_fn / _partial_drop_fn differed only in the
symbol prefix and an ignored mask param; merged into one inner
emitter.
No INTERCEPTS-registry or (intrinsic)-marker changes.
1128 lines
51 KiB
Rust
1128 lines
51 KiB
Rust
//! Per-type drop-fn emission and per-let-close drop dispatch.
|
|
//!
|
|
//! All RC-allocator drop work lives here:
|
|
//! - `emit_drop_fn_for_type` / `emit_iterative_drop_fn_for_type`
|
|
//! emit `@drop_<m>_<T>(ptr)` per `Def::Type` under `--alloc=rc`.
|
|
//! The recursive variant cascades through `field_drop_call`; the
|
|
//! iterative variant uses an explicit worklist (Iter 18e) so
|
|
//! `(drop-iterative)` types can free chains of arbitrary length
|
|
//! without consuming proportional C stack.
|
|
//! - `field_is_same_type` / `field_drop_call` are the internal
|
|
//! dispatch helpers consulted by the per-type drop bodies and by
|
|
//! match-arm / reuse-as drop emission elsewhere.
|
|
//! - `is_rc_heap_allocated` / `synth_callee_ret_mode` /
|
|
//! `drop_symbol_for_binder` / `emit_inlined_partial_drop` are the
|
|
//! per-let-close cluster: codegen calls them from the
|
|
//! `Term::Let` lowering to decide whether to dec, what symbol to
|
|
//! dispatch to, and (when pattern destructuring transferred
|
|
//! fields out) how to emit a partial-drop sequence inline.
|
|
//! - `build_env_drop_fn` / `build_pair_drop_fn` build the per-
|
|
//! closure drop fns that `lower_lambda` defers into the IR
|
|
//! stream (Iter 18c.4 closure cleanup).
|
|
//!
|
|
//! Methods called from `lib.rs` are `pub(crate)`; helpers used only
|
|
//! among the drop-cluster methods stay private. Field access from
|
|
//! this submodule into the parent's private `Emitter` fields works
|
|
//! through the standard descendant-module privacy lane.
|
|
|
|
use ailang_core::ast::{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_<module>_<TypeName>(ptr %p)`
|
|
/// function that decrements the refcount of every pointer-typed
|
|
/// field of every ctor, then frees the outer box.
|
|
///
|
|
/// Shape:
|
|
/// ```text
|
|
/// define void @drop_<m>_<T>(ptr %p) {
|
|
/// %tag = load i64, ptr %p
|
|
/// switch i64 %tag, label %dflt [
|
|
/// i64 0, label %arm0
|
|
/// ...
|
|
/// ]
|
|
/// arm_i:
|
|
/// for each pointer-typed field f_j:
|
|
/// %addr = gep ptr %p, i64 (8 + 8*j)
|
|
/// %v = load ptr, ptr %addr
|
|
/// call void @drop_<owner>_<FieldT>(ptr %v) ; or @ailang_rc_dec
|
|
/// br label %join
|
|
/// dflt:
|
|
/// unreachable
|
|
/// join:
|
|
/// call void @ailang_rc_dec(ptr %p)
|
|
/// ret void
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// For ADTs with no boxed children every arm is empty and falls
|
|
/// straight through to `join`, which is just the final dec — see
|
|
/// the assignment's "always emit drop_X for every ADT" decision
|
|
/// (`rc_box_drop`'s `MkBox(Int)` is the canonical example).
|
|
///
|
|
/// Recursion: when a ctor field's type is the same ADT (or any
|
|
/// ADT in the workspace), the emitted call to
|
|
/// `@drop_<owner>_<T>(field)` is recursive at the IR level and
|
|
/// will overflow the stack on long lists. The 18e
|
|
/// `(drop-iterative)` annotation routes such types through
|
|
/// [`Self::emit_iterative_drop_fn_for_type`] instead, which
|
|
/// replaces the recursive call with a worklist push. ADTs WITHOUT
|
|
/// the annotation continue to use this recursive form — the
|
|
/// orchestrator's choice: opt-in iterative drop where the depth
|
|
/// is known to grow, recursive cascade everywhere else (cheaper
|
|
/// IR, no worklist allocation).
|
|
pub(crate) fn emit_drop_fn_for_type(&mut self, td: &TypeDef) {
|
|
// 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<String, Type>, &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, Type>, String)> = self
|
|
.drop_monos
|
|
.instantiations(&key)
|
|
.into_iter()
|
|
.map(|args| {
|
|
let subst: BTreeMap<String, Type> =
|
|
td.vars.iter().cloned().zip(args.iter().cloned()).collect();
|
|
let suffix = self
|
|
.drop_monos
|
|
.suffix_for(&key, &args)
|
|
.unwrap_or_default();
|
|
(subst, suffix)
|
|
})
|
|
.collect();
|
|
for (subst, suffix) in &plan {
|
|
emit(self, td, subst, suffix);
|
|
}
|
|
}
|
|
|
|
/// emit one recursive `drop_<m>_<T><suffix>` body. `subst` maps the
|
|
/// declared type-vars to the concrete instantiation args (empty for
|
|
/// monomorphic ADTs); `sym_suffix` is the `__<...>` mono suffix
|
|
/// (empty for monomorphic ADTs).
|
|
fn emit_drop_fn_for_instantiation(
|
|
&mut self,
|
|
td: &TypeDef,
|
|
subst: &BTreeMap<String, Type>,
|
|
sym_suffix: &str,
|
|
) {
|
|
let m = self.module_name;
|
|
let tname = &td.name;
|
|
let mut out = String::new();
|
|
out.push_str(&format!("define void @drop_{m}_{tname}{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_<m>_<T>` for a `(drop-iterative)` type.
|
|
/// Replaces the recursive cascade in [`Self::emit_drop_fn_for_type`]
|
|
/// with an iterative-with-explicit-worklist body so cells of
|
|
/// arbitrary chain depth can free without consuming proportional
|
|
/// C stack.
|
|
///
|
|
/// IR shape:
|
|
/// ```text
|
|
/// define void @drop_<m>_<T>(ptr %p) {
|
|
/// entry:
|
|
/// %is_null = icmp eq ptr %p, null
|
|
/// br i1 %is_null, label %ret, label %init_wl
|
|
/// init_wl:
|
|
/// %wl = call ptr @ailang_drop_worklist_new()
|
|
/// call void @ailang_drop_worklist_push(ptr %wl, ptr %p)
|
|
/// br label %loop_head
|
|
/// loop_head:
|
|
/// %cur = call ptr @ailang_drop_worklist_pop(ptr %wl)
|
|
/// %done = icmp eq ptr %cur, null
|
|
/// br i1 %done, label %finish, label %dispatch
|
|
/// dispatch:
|
|
/// %tag = load i64, ptr %cur, align 8
|
|
/// switch i64 %tag, label %dflt [
|
|
/// i64 0, label %arm_0
|
|
/// ...
|
|
/// ]
|
|
/// arm_i:
|
|
/// for each pointer-typed field f_j of ctor i:
|
|
/// %addr = gep %cur, 8 + 8*j
|
|
/// %v = load ptr, ptr %addr
|
|
/// if field type is T (same as the type being dropped):
|
|
/// call void @ailang_drop_worklist_push(ptr %wl, ptr %v)
|
|
/// else:
|
|
/// call void @drop_<owner>_<F>(ptr %v) ; or @ailang_rc_dec
|
|
/// call void @ailang_rc_dec(ptr %cur)
|
|
/// br label %loop_head
|
|
/// dflt:
|
|
/// unreachable
|
|
/// finish:
|
|
/// call void @ailang_drop_worklist_free(ptr %wl)
|
|
/// br label %ret
|
|
/// ret:
|
|
/// ret void
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// Mono-typed worklist. Every pointer pushed onto `%wl` is a `T`
|
|
/// (the type being dropped). For a field whose type is `T` itself
|
|
/// → push (continues the iterative cascade). For any other ADT
|
|
/// field type `T'` → call `drop_<m'>_<T'>` directly: if `T'` is
|
|
/// also `(drop-iterative)`, that fn allocates its own worklist
|
|
/// instance (no nesting); if `T'` is non-iterative, it recurses
|
|
/// stack-wise (depth bounded by the number of *distinct* nested
|
|
/// ADTs reachable from `T`, which is small in practice).
|
|
///
|
|
/// This interpretation of the assignment's "should also use the
|
|
/// worklist" clause was chosen because a heterogeneously-typed
|
|
/// worklist would require storing a (ptr, drop-handler) tuple per
|
|
/// entry plus a vtable dispatch on pop — significant complexity
|
|
/// for the case where two distinct ADTs are mutually recursive
|
|
/// AND both are drop-iterative AND the chain is millions deep.
|
|
/// That triple-conjunct is not on the 18-arc's critical path; if
|
|
/// it surfaces in practice, a follow-up iter can extend the
|
|
/// worklist entry shape. The mono-typed version captures the
|
|
/// stack-overflow-on-long-self-chains problem fully.
|
|
pub(crate) fn emit_iterative_drop_fn_for_type(&mut self, td: &TypeDef) {
|
|
self.emit_drop_fn_wrapper(td, Self::emit_iterative_drop_fn_for_instantiation);
|
|
}
|
|
|
|
/// emit one iterative `drop_<m>_<T><suffix>` body (worklist variant).
|
|
/// `subst` / `sym_suffix` carry the leg-C per-monomorph
|
|
/// instantiation, identical in role to
|
|
/// [`Self::emit_drop_fn_for_instantiation`].
|
|
fn emit_iterative_drop_fn_for_instantiation(
|
|
&mut self,
|
|
td: &TypeDef,
|
|
subst: &BTreeMap<String, Type>,
|
|
sym_suffix: &str,
|
|
) {
|
|
let m = self.module_name;
|
|
let tname = &td.name;
|
|
let mut out = String::new();
|
|
out.push_str(&format!("define void @drop_{m}_{tname}{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_<owner>_<T>` symbol so the recursion cascades through
|
|
/// recursive types (List, Tree). Falls back to `ailang_rc_dec`
|
|
/// for non-ADT pointer types (Str, fn-typed, unresolved Var) —
|
|
/// those are not user-defined ADTs and have no per-type drop fn.
|
|
pub(crate) fn field_drop_call(&self, fty: &Type) -> String {
|
|
match fty {
|
|
Type::Con { name, 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 `__<suffix>`
|
|
// the emission loop minted for that instantiation;
|
|
// monomorphic / intrinsic ADTs keep the un-suffixed
|
|
// symbol byte-for-byte.
|
|
self.adt_drop_symbol(name, args)
|
|
}
|
|
// Type::Fn (closure-typed field) → no per-type drop fn
|
|
// exists for closures (each closure has its own per-pair
|
|
// 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_<owner>_<T>` symbol for an ADT `Type::Con`,
|
|
/// applying the leg-C per-monomorph suffix when the resolved ADT is
|
|
/// polymorphic + non-intrinsic. The caller has already excluded
|
|
/// `Str` and non-`Con` shapes. `name` is the (possibly qualified)
|
|
/// type name; `args` are its concrete instantiation arguments.
|
|
///
|
|
/// The suffix decision lives in [`dropmono::DropAdtMeta`], shared
|
|
/// with the emission loop, so a call symbol matches its definition
|
|
/// byte-for-byte (a mismatch is an IR link error).
|
|
pub(crate) fn adt_drop_symbol(&self, name: &str, args: &[Type]) -> String {
|
|
self.adt_symbol("drop_", name, args)
|
|
}
|
|
|
|
/// resolve the `partial_drop_<owner>_<T>` symbol for an ADT
|
|
/// `Type::Con`, applying the leg-C per-monomorph suffix. Parallel
|
|
/// to [`Self::adt_drop_symbol`]. The caller has already excluded
|
|
/// `Str` / non-`Con` shapes.
|
|
pub(crate) fn adt_partial_drop_symbol(&self, name: &str, args: &[Type]) -> String {
|
|
self.adt_symbol("partial_drop_", name, args)
|
|
}
|
|
|
|
/// shared core of [`Self::adt_drop_symbol`] and
|
|
/// [`Self::adt_partial_drop_symbol`]: build the `<prefix><owner>_<bare>`
|
|
/// 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<ParamMode> {
|
|
// 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_<owner>_<TypeName>` — derived from the ctor's
|
|
/// `type_name` (which already encodes the owning module via the
|
|
/// `module.T` form when cross-module). For a `Term::Lam` binder
|
|
/// the symbol comes from `closure_drops`, populated by
|
|
/// [`Self::lower_lambda`] when it emitted the per-pair drop fn.
|
|
///
|
|
/// Falls back to `ailang_rc_dec` for any other shape — should be
|
|
/// unreachable since `is_rc_heap_allocated` only returns `true`
|
|
/// for `Term::Ctor` / `Term::Lam`, but a defensive fallback
|
|
/// keeps the IR well-formed even if the predicate ever widens.
|
|
pub(crate) fn drop_symbol_for_binder(&self, value: &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_<m>_Box__Int`. `type_name` alone
|
|
// would lose the instantiation.
|
|
let args = match value.ty() {
|
|
Type::Con { args, .. } => args,
|
|
_ => Vec::new(),
|
|
};
|
|
self.adt_drop_symbol(type_name, &args)
|
|
}
|
|
MTerm::Lam { .. } => self
|
|
.closure_drops
|
|
.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_<m>_<Tree>`). Falls back to `ailang_rc_dec` if
|
|
// the ret-type is not a `Type::Con` (e.g. a bare type
|
|
// var on an as-yet-unmonomorphised polymorphic call —
|
|
// the monomorphised copies will resolve correctly).
|
|
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_<m>_<T>(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_<m>_<T>(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 @<field_drop_call>(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_<m>_<T>`. 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_<m>_<F>` 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_<m>_<T><suffix>` body. `subst` /
|
|
/// `sym_suffix` carry the leg-C per-monomorph instantiation,
|
|
/// identical in role to [`Self::emit_drop_fn_for_instantiation`].
|
|
fn emit_partial_drop_fn_for_instantiation(
|
|
&mut self,
|
|
td: &TypeDef,
|
|
subst: &BTreeMap<String, Type>,
|
|
sym_suffix: &str,
|
|
) {
|
|
let m = self.module_name;
|
|
let tname = &td.name;
|
|
let mut out = String::new();
|
|
out.push_str(&format!(
|
|
"define void @partial_drop_{m}_{tname}{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_<m>_<T>` 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_<m>_<T>` 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_<owner>_<T>`
|
|
/// 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<String> {
|
|
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_<m>_<T>` 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<usize>) -> Option<u64> {
|
|
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_<m>_<T>(ptr)` call: load each pointer-typed
|
|
/// field whose slot index is NOT in `moved`, dispatch through
|
|
/// `field_drop_call` (the same per-type or shallow drop the
|
|
/// recursive cascade picks), then `ailang_rc_dec` the outer
|
|
/// box. Skips slots in `moved` entirely — those values were
|
|
/// transferred to a pattern-bound binder that owns the dec
|
|
/// for them.
|
|
///
|
|
/// 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<usize>,
|
|
) -> 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<String, Type> =
|
|
match (&cref.type_vars, value.ty()) {
|
|
(vars, Type::Con { args, .. })
|
|
if !vars.is_empty() && vars.len() == args.len() =>
|
|
{
|
|
vars.iter().cloned().zip(args.into_iter()).collect()
|
|
}
|
|
_ => BTreeMap::new(),
|
|
};
|
|
// Per-field dec for non-moved pointer-typed slots. ail_fields
|
|
// are the AILang-level field types; field_drop_call resolves
|
|
// them to either `drop_<owner>_<T>` (ADTs cascade) or
|
|
// `ailang_rc_dec` (Str / closures / vars).
|
|
for (idx, fty_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<FnSig>)],
|
|
) -> String {
|
|
let mut out = String::new();
|
|
out.push_str(&format!("define void @{sym}(ptr %env) {{\nentry:\n"));
|
|
out.push_str(" %is_null = icmp eq ptr %env, null\n");
|
|
out.push_str(" br i1 %is_null, label %ret, label %live\n");
|
|
out.push_str("live:\n");
|
|
let mut local = 0u64;
|
|
for (i, (_cname, _outer_ssa, lty, ail_ty, _sig)) in cap_meta.iter().enumerate() {
|
|
if lty != "ptr" {
|
|
continue;
|
|
}
|
|
let off = (i as i64) * 8;
|
|
let addr_id = local;
|
|
local += 1;
|
|
let val_id = local;
|
|
local += 1;
|
|
out.push_str(&format!(
|
|
" %a{addr_id} = getelementptr inbounds i8, ptr %env, i64 {off}\n"
|
|
));
|
|
out.push_str(&format!(
|
|
" %v{val_id} = load ptr, ptr %a{addr_id}, align 8\n"
|
|
));
|
|
let drop_call = self.field_drop_call(ail_ty);
|
|
out.push_str(&format!(
|
|
" call void @{drop_call}(ptr %v{val_id})\n"
|
|
));
|
|
}
|
|
out.push_str(" call void @ailang_rc_dec(ptr %env)\n");
|
|
out.push_str(" br label %ret\n");
|
|
out.push_str("ret:\n");
|
|
out.push_str(" ret void\n}\n\n");
|
|
out
|
|
}
|
|
|
|
/// 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
|
|
}
|
|
}
|