4ea8bc5faf
Tasks 6-7 of the Boss-repaired split dispatch (Tasks 1-5 committed d5c565d).
Task 6: DESIGN.md §"Embedding ABI" gains the `### Frozen value layout
(M3 — one-way commitment)` SSOT subsection; both "provisional until M3"
sentences rewritten to one-way-freeze wording (docs_honesty_pin-safe — the
pinned "written **bare**" sentence stayed byte-verbatim, :2297->:2299
content-asserted); // FROZEN ABI lockstep pointers at runtime/rc.c +
match_lower.rs lower_ctor + drop.rs; 3 plan-named stale rustdocs fixed +
the Boss-caught stale // gate-comment block (check/lib.rs:1917-1922,
M1/scalar-only -> M1/M2/M3/C-ABI-permitted-incl-record); byte-pin
enforceability demonstrated (RED on a local offset perturbation, GREEN on
git-checkout restore — the only legitimate git op, working-tree only).
Task 7: milestone-close verification gate.
Boss-verified independently (agent report = intent, not outcome): workspace
639 passed / 0 failed; embed_record_e2e 2/2 (own+borrow, global
leak-freedom model); byte-pin 1/1; gate 10/10; forwarder 3/3;
docs_honesty_pin 5/5 pin-safe; design_schema_drift + embed_export_hash_stable
+ embed_e2e + M2 swarm regression-green-unmodified.
INDEX.md line added (deferred from the PARTIAL — one line per iter, final
title). Milestone-close audit next (architect Invariant 1 + bench trio,
spec Testing items 8/9 — audit-owned, not the implement run's job).
920 lines
41 KiB
Rust
920 lines
41 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::*;
|
|
use std::collections::BTreeSet;
|
|
|
|
use super::synth::llvm_type;
|
|
use super::{AllocStrategy, Emitter, FnSig, Result};
|
|
|
|
impl<'a> Emitter<'a> {
|
|
/// Iter 18c.4: emit a `void @drop_<module>_<TypeName>(ptr %p)`
|
|
/// function that decrements the refcount of every pointer-typed
|
|
/// field of every ctor, then frees the outer box.
|
|
///
|
|
/// Shape:
|
|
/// ```text
|
|
/// define void @drop_<m>_<T>(ptr %p) {
|
|
/// %tag = load i64, ptr %p
|
|
/// switch i64 %tag, label %dflt [
|
|
/// i64 0, label %arm0
|
|
/// ...
|
|
/// ]
|
|
/// arm_i:
|
|
/// for each pointer-typed field f_j:
|
|
/// %addr = gep ptr %p, i64 (8 + 8*j)
|
|
/// %v = load ptr, ptr %addr
|
|
/// call void @drop_<owner>_<FieldT>(ptr %v) ; or @ailang_rc_dec
|
|
/// br label %join
|
|
/// dflt:
|
|
/// unreachable
|
|
/// join:
|
|
/// call void @ailang_rc_dec(ptr %p)
|
|
/// ret void
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// For ADTs with no boxed children every arm is empty and falls
|
|
/// straight through to `join`, which is just the final dec — see
|
|
/// the assignment's "always emit drop_X for every ADT" decision
|
|
/// (`rc_box_drop`'s `MkBox(Int)` is the canonical example).
|
|
///
|
|
/// Recursion: when a ctor field's type is the same ADT (or any
|
|
/// ADT in the workspace), the emitted call to
|
|
/// `@drop_<owner>_<T>(field)` is recursive at the IR level and
|
|
/// will overflow the stack on long lists. The 18e
|
|
/// `(drop-iterative)` annotation routes such types through
|
|
/// [`Self::emit_iterative_drop_fn_for_type`] instead, which
|
|
/// replaces the recursive call with a worklist push. ADTs WITHOUT
|
|
/// the annotation continue to use this recursive form — the
|
|
/// orchestrator's choice: opt-in iterative drop where the depth
|
|
/// is known to grow, recursive cascade everywhere else (cheaper
|
|
/// IR, no worklist allocation).
|
|
pub(crate) fn emit_drop_fn_for_type(&mut self, td: &TypeDef) {
|
|
let m = self.module_name;
|
|
let tname = &td.name;
|
|
let mut out = String::new();
|
|
out.push_str(&format!("define void @drop_{m}_{tname}(ptr %p) {{\n"));
|
|
out.push_str("entry:\n");
|
|
// Null guard: a null payload is a no-op (matches
|
|
// `runtime/rc.c::ailang_rc_dec`'s null guard).
|
|
out.push_str(" %is_null = icmp eq ptr %p, null\n");
|
|
out.push_str(" br i1 %is_null, label %ret, label %live\n");
|
|
out.push_str("live:\n");
|
|
// FROZEN ABI (embedding boundary) — see DESIGN.md §"Embedding ABI".
|
|
out.push_str(" %tag = load i64, ptr %p, align 8\n");
|
|
|
|
let n_ctors = td.ctors.len();
|
|
// Switch over the tag. Each ctor gets one arm.
|
|
out.push_str(" switch i64 %tag, label %dflt [\n");
|
|
for (i, _) in td.ctors.iter().enumerate() {
|
|
out.push_str(&format!(" i64 {i}, label %arm_{i}\n"));
|
|
}
|
|
out.push_str(" ]\n");
|
|
|
|
// Per-ctor arm: iterate fields, dec the boxed ones.
|
|
let mut local = 0u64;
|
|
for (i, ctor) in td.ctors.iter().enumerate() {
|
|
out.push_str(&format!("arm_{i}:\n"));
|
|
for (j, fty) in ctor.fields.iter().enumerate() {
|
|
// Decide what to call for this field. If the field
|
|
// lowers to `ptr` (boxed), we issue a `dec` call.
|
|
// For known ADT field types we route through that
|
|
// ADT's own drop fn so the recursion cascades; for
|
|
// anything else that lowers to `ptr` (Str, fn-typed,
|
|
// unresolved Var), fall back to plain `ailang_rc_dec`.
|
|
let lty = llvm_type(fty).unwrap_or_else(|_| "ptr".into());
|
|
if lty != "ptr" {
|
|
continue;
|
|
}
|
|
// FROZEN ABI (embedding boundary) — see DESIGN.md §"Embedding ABI".
|
|
let off = 8 + (j as i64) * 8;
|
|
let addr_id = local;
|
|
local += 1;
|
|
let val_id = local;
|
|
local += 1;
|
|
out.push_str(&format!(
|
|
" %a{addr_id} = getelementptr inbounds i8, ptr %p, i64 {off}\n"
|
|
));
|
|
out.push_str(&format!(
|
|
" %v{val_id} = load ptr, ptr %a{addr_id}, align 8\n"
|
|
));
|
|
let drop_call = self.field_drop_call(fty);
|
|
// Recursive call into the field's drop fn. If the
|
|
// field's type is itself `(drop-iterative)`, that drop
|
|
// fn is the worklist variant — recursion stops at one
|
|
// level. Otherwise this is the unbounded recursive
|
|
// cascade; safe only on bounded-depth ADTs (the
|
|
// `(drop-iterative)` annotation exists for the
|
|
// unbounded ones).
|
|
out.push_str(&format!(
|
|
" call void @{drop_call}(ptr %v{val_id})\n"
|
|
));
|
|
}
|
|
out.push_str(" br label %join\n");
|
|
}
|
|
|
|
// Default arm: unreachable when the typechecker has accepted
|
|
// the input — every legal box has one of the ctor tags.
|
|
out.push_str("dflt:\n");
|
|
if n_ctors == 0 {
|
|
// No ctors at all: a Type with zero ctors cannot be
|
|
// instantiated; the drop fn is dead. Still emit a
|
|
// br-to-join for IR validity.
|
|
out.push_str(" br label %join\n");
|
|
} else {
|
|
out.push_str(" unreachable\n");
|
|
}
|
|
|
|
// Join: free the outer box.
|
|
out.push_str("join:\n");
|
|
out.push_str(" call void @ailang_rc_dec(ptr %p)\n");
|
|
out.push_str(" br label %ret\n");
|
|
out.push_str("ret:\n");
|
|
out.push_str(" ret void\n");
|
|
out.push_str("}\n\n");
|
|
self.body.push_str(&out);
|
|
}
|
|
|
|
/// Iter 18e: emit `drop_<m>_<T>` for a `(drop-iterative)` type.
|
|
/// Replaces the recursive cascade in [`Self::emit_drop_fn_for_type`]
|
|
/// with an iterative-with-explicit-worklist body so cells of
|
|
/// arbitrary chain depth can free without consuming proportional
|
|
/// C stack.
|
|
///
|
|
/// IR shape:
|
|
/// ```text
|
|
/// define void @drop_<m>_<T>(ptr %p) {
|
|
/// entry:
|
|
/// %is_null = icmp eq ptr %p, null
|
|
/// br i1 %is_null, label %ret, label %init_wl
|
|
/// init_wl:
|
|
/// %wl = call ptr @ailang_drop_worklist_new()
|
|
/// call void @ailang_drop_worklist_push(ptr %wl, ptr %p)
|
|
/// br label %loop_head
|
|
/// loop_head:
|
|
/// %cur = call ptr @ailang_drop_worklist_pop(ptr %wl)
|
|
/// %done = icmp eq ptr %cur, null
|
|
/// br i1 %done, label %finish, label %dispatch
|
|
/// dispatch:
|
|
/// %tag = load i64, ptr %cur, align 8
|
|
/// switch i64 %tag, label %dflt [
|
|
/// i64 0, label %arm_0
|
|
/// ...
|
|
/// ]
|
|
/// arm_i:
|
|
/// for each pointer-typed field f_j of ctor i:
|
|
/// %addr = gep %cur, 8 + 8*j
|
|
/// %v = load ptr, ptr %addr
|
|
/// if field type is T (same as the type being dropped):
|
|
/// call void @ailang_drop_worklist_push(ptr %wl, ptr %v)
|
|
/// else:
|
|
/// call void @drop_<owner>_<F>(ptr %v) ; or @ailang_rc_dec
|
|
/// call void @ailang_rc_dec(ptr %cur)
|
|
/// br label %loop_head
|
|
/// dflt:
|
|
/// unreachable
|
|
/// finish:
|
|
/// call void @ailang_drop_worklist_free(ptr %wl)
|
|
/// br label %ret
|
|
/// ret:
|
|
/// ret void
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// Mono-typed worklist. Every pointer pushed onto `%wl` is a `T`
|
|
/// (the type being dropped). For a field whose type is `T` itself
|
|
/// → push (continues the iterative cascade). For any other ADT
|
|
/// field type `T'` → call `drop_<m'>_<T'>` directly: if `T'` is
|
|
/// also `(drop-iterative)`, that fn allocates its own worklist
|
|
/// instance (no nesting); if `T'` is non-iterative, it recurses
|
|
/// stack-wise (depth bounded by the number of *distinct* nested
|
|
/// ADTs reachable from `T`, which is small in practice).
|
|
///
|
|
/// This interpretation of the assignment's "should also use the
|
|
/// worklist" clause was chosen because a heterogeneously-typed
|
|
/// worklist would require storing a (ptr, drop-handler) tuple per
|
|
/// entry plus a vtable dispatch on pop — significant complexity
|
|
/// for the case where two distinct ADTs are mutually recursive
|
|
/// AND both are drop-iterative AND the chain is millions deep.
|
|
/// That triple-conjunct is not on the 18-arc's critical path; if
|
|
/// it surfaces in practice, a follow-up iter can extend the
|
|
/// worklist entry shape. The mono-typed version captures the
|
|
/// stack-overflow-on-long-self-chains problem fully.
|
|
pub(crate) fn emit_iterative_drop_fn_for_type(&mut self, td: &TypeDef) {
|
|
let m = self.module_name;
|
|
let tname = &td.name;
|
|
let mut out = String::new();
|
|
out.push_str(&format!("define void @drop_{m}_{tname}(ptr %p) {{\n"));
|
|
out.push_str("entry:\n");
|
|
// Null guard — symmetric with the recursive variant. A null
|
|
// payload skips worklist allocation entirely.
|
|
out.push_str(" %is_null = icmp eq ptr %p, null\n");
|
|
out.push_str(" br i1 %is_null, label %ret, label %init_wl\n");
|
|
out.push_str("init_wl:\n");
|
|
out.push_str(" %wl = call ptr @ailang_drop_worklist_new()\n");
|
|
out.push_str(
|
|
" call void @ailang_drop_worklist_push(ptr %wl, ptr %p)\n",
|
|
);
|
|
out.push_str(" br label %loop_head\n");
|
|
|
|
out.push_str("loop_head:\n");
|
|
out.push_str(
|
|
" %cur = call ptr @ailang_drop_worklist_pop(ptr %wl)\n",
|
|
);
|
|
out.push_str(" %done = icmp eq ptr %cur, null\n");
|
|
out.push_str(" br i1 %done, label %finish, label %dispatch\n");
|
|
|
|
out.push_str("dispatch:\n");
|
|
out.push_str(" %tag = load i64, ptr %cur, align 8\n");
|
|
let n_ctors = td.ctors.len();
|
|
out.push_str(" switch i64 %tag, label %dflt [\n");
|
|
for (i, _) in td.ctors.iter().enumerate() {
|
|
out.push_str(&format!(" i64 {i}, label %arm_{i}\n"));
|
|
}
|
|
out.push_str(" ]\n");
|
|
|
|
// Per-ctor arms. For each pointer-typed field decide push vs
|
|
// direct call based on whether the field's type is the same
|
|
// as the type being dropped.
|
|
let mut local = 0u64;
|
|
for (i, ctor) in td.ctors.iter().enumerate() {
|
|
out.push_str(&format!("arm_{i}:\n"));
|
|
for (j, fty) in ctor.fields.iter().enumerate() {
|
|
let lty = llvm_type(fty).unwrap_or_else(|_| "ptr".into());
|
|
if lty != "ptr" {
|
|
continue;
|
|
}
|
|
let off = 8 + (j as i64) * 8;
|
|
let addr_id = local;
|
|
local += 1;
|
|
let val_id = local;
|
|
local += 1;
|
|
out.push_str(&format!(
|
|
" %a{addr_id} = getelementptr inbounds i8, ptr %cur, i64 {off}\n"
|
|
));
|
|
out.push_str(&format!(
|
|
" %v{val_id} = load ptr, ptr %a{addr_id}, align 8\n"
|
|
));
|
|
if self.field_is_same_type(fty, &td.name) {
|
|
// Same-type field: push onto the worklist —
|
|
// continues the iterative cascade. Null-guarding
|
|
// is handled inside `ailang_drop_worklist_push`
|
|
// itself (skips null payloads).
|
|
out.push_str(&format!(
|
|
" call void @ailang_drop_worklist_push(ptr %wl, ptr %v{val_id})\n"
|
|
));
|
|
} else {
|
|
// Different-type field: dispatch to that type's
|
|
// own drop fn (which itself decides recursive vs.
|
|
// iterative). `field_drop_call` resolves the
|
|
// symbol; its null-guard semantics are the same
|
|
// as the recursive variant.
|
|
let drop_call = self.field_drop_call(fty);
|
|
out.push_str(&format!(
|
|
" call void @{drop_call}(ptr %v{val_id})\n"
|
|
));
|
|
}
|
|
}
|
|
// Dec the outer cell. Worklist holds no other reference
|
|
// to this pointer (push happened exactly once on the
|
|
// parent's cascade, and pop just removed that entry), so
|
|
// the cell's refcount drops by exactly one here. Children
|
|
// pushed above keep their own refcounts pending until
|
|
// their loop iteration.
|
|
out.push_str(" call void @ailang_rc_dec(ptr %cur)\n");
|
|
out.push_str(" br label %loop_head\n");
|
|
}
|
|
|
|
// Default arm: unreachable when the typechecker has accepted
|
|
// the input. Same shape as the recursive variant.
|
|
out.push_str("dflt:\n");
|
|
if n_ctors == 0 {
|
|
out.push_str(" br label %finish\n");
|
|
} else {
|
|
out.push_str(" unreachable\n");
|
|
}
|
|
|
|
out.push_str("finish:\n");
|
|
out.push_str(" call void @ailang_drop_worklist_free(ptr %wl)\n");
|
|
out.push_str(" br label %ret\n");
|
|
out.push_str("ret:\n");
|
|
out.push_str(" ret void\n");
|
|
out.push_str("}\n\n");
|
|
self.body.push_str(&out);
|
|
}
|
|
|
|
/// Iter 18e helper: is `fty` the same ADT as `td_name` in the
|
|
/// current module? Used by the iterative-drop body to decide
|
|
/// "push to worklist" (same type) vs. "call its drop fn directly"
|
|
/// (different type).
|
|
///
|
|
/// Returns `true` only when the field type is a `Type::Con`
|
|
/// referencing `td_name` AND the reference resolves to the
|
|
/// current module (bare or qualified-but-self). Qualified names
|
|
/// pointing at *other* modules are different types — even when
|
|
/// they spell the same suffix. Type-vars and fn-types are never
|
|
/// the same as the ADT being dropped (parametric self-recursion
|
|
/// could bind a var to T, but the bound is invisible at codegen
|
|
/// since we don't monomorphise drop fns).
|
|
fn field_is_same_type(&self, fty: &Type, td_name: &str) -> bool {
|
|
match fty {
|
|
Type::Con { name, .. } => {
|
|
if name.matches('.').count() == 1 {
|
|
let (prefix, suffix) = name.split_once('.').expect("checked");
|
|
if suffix != td_name {
|
|
return false;
|
|
}
|
|
// Resolve the prefix; same-module iff the resolved
|
|
// target equals self.module_name.
|
|
let target = self
|
|
.import_map
|
|
.get(prefix)
|
|
.map(|s| s.as_str())
|
|
.unwrap_or(prefix);
|
|
target == self.module_name
|
|
} else {
|
|
name == td_name
|
|
}
|
|
}
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// Iter 18c.4: pick the drop-fn symbol to call for a single
|
|
/// pointer-typed field. Routes ADT fields to their own
|
|
/// `drop_<owner>_<T>` symbol so the recursion cascades through
|
|
/// recursive types (List, Tree). Falls back to `ailang_rc_dec`
|
|
/// for non-ADT pointer types (Str, fn-typed, unresolved Var) —
|
|
/// those are not user-defined ADTs and have no per-type drop fn.
|
|
pub(crate) fn field_drop_call(&self, fty: &Type) -> String {
|
|
match fty {
|
|
Type::Con { name, .. } => {
|
|
// Built-in pointer-typed cons: Str. No drop fn —
|
|
// shallow `ailang_rc_dec` is the right answer.
|
|
// Str has two realisations sharing the consumer
|
|
// ABI (len at offset 0, bytes at offset 8):
|
|
// - heap-Str: malloc'd slab with real rc_header
|
|
// at `payload - 8`; rc_dec is the correct
|
|
// refcount-and-free path.
|
|
// - static-Str: packed-struct LLVM global
|
|
// <{ i64, [N x i8] }> in .rodata, no rc_header
|
|
// slot; rc_dec would read undefined bytes at
|
|
// `payload - 8`. Codegen-level elision
|
|
// (`emit_inlined_partial_drop` move-tracking
|
|
// from iter 18d.3 + non-escape lowering from
|
|
// iter 18b) keeps static-Str pointers out of
|
|
// this call along every shipping execution
|
|
// path. The codegen-level invariant is the
|
|
// protection; no runtime guard backs it up.
|
|
if matches!(name.as_str(), "Str") {
|
|
return "ailang_rc_dec".to_string();
|
|
}
|
|
// Qualified `module.T` → drop fn lives in `module`.
|
|
if name.matches('.').count() == 1 {
|
|
let (prefix, suffix) = name.split_once('.').expect("checked");
|
|
if let Some(target) = self.import_map.get(prefix) {
|
|
return format!("drop_{target}_{suffix}");
|
|
}
|
|
// Fallback: treat the prefix itself as the owner
|
|
// module (typechecker would have rejected an
|
|
// unimported prefix earlier).
|
|
return format!("drop_{prefix}_{suffix}");
|
|
}
|
|
// Bare name: declared in the current module.
|
|
format!("drop_{m}_{name}", m = self.module_name)
|
|
}
|
|
// Type::Fn (closure-typed field) → no per-type drop fn
|
|
// exists for closures (each closure has its own per-pair
|
|
// drop fn keyed by the lam-id, not by type). Iter 18c.4
|
|
// shallow-frees the closure-pair; the env and any
|
|
// captured ADT fields leak. A future iter that types
|
|
// closure-typed fields with a runtime descriptor pointer
|
|
// would close this. For 18c.4's recursive-ADT story this
|
|
// is acceptable — the shipping fixtures don't store
|
|
// closures inside ADT fields.
|
|
Type::Fn { .. } => "ailang_rc_dec".to_string(),
|
|
// Type::Var (parameterised ADT field whose type-arg was
|
|
// not pinned at declaration): we don't know the field's
|
|
// concrete shape from the static decl alone, and the
|
|
// generated drop fn is a single symbol per ADT (no
|
|
// monomorphisation). Shallow free is the conservative
|
|
// choice — boxed children of polymorphic-typed fields
|
|
// leak. Iter 18d/18e will revisit this when reuse hints
|
|
// and the worklist allocator land.
|
|
Type::Var { .. } | Type::Forall { .. } => "ailang_rc_dec".to_string(),
|
|
}
|
|
}
|
|
|
|
/// Iter 18c.3: predicate the `Term::Let` lowering uses to decide
|
|
/// whether a let-binder owns a fresh RC-heap allocation that
|
|
/// codegen should `dec` at scope close.
|
|
///
|
|
/// Returns `true` exactly when:
|
|
/// - the active allocator is `Rc`,
|
|
/// - `value` is a `Term::Ctor` or `Term::Lam` (the two AST shapes
|
|
/// that lower through the heap-allocation path), AND
|
|
/// - the term is *not* in the current fn's `non_escape` set —
|
|
/// escaping ctors/lambdas go through `ailang_rc_alloc`,
|
|
/// non-escaping ones become stack `alloca`s and must NOT be
|
|
/// `dec`'d (they are freed by LLVM at fn return).
|
|
///
|
|
/// Iter 18g.2 widens this to include `Term::App` whose callee's
|
|
/// fn-type carries `ret_mode == Own`. The mode contract states that
|
|
/// the callee hands the returned cell's ownership to the caller's
|
|
/// frame; the let-scope close is the right place for the caller's
|
|
/// dec. Calls whose callee is `Borrow`/`Implicit`-returning are
|
|
/// still not trackable — they don't carry that signal.
|
|
///
|
|
/// Other value shapes (vars, literals, matches, …) return `false`
|
|
/// here. A `Term::Var` returning an RC-allocated box would already
|
|
/// be tracked by an earlier let-binder; tracking it again here
|
|
/// would double-dec.
|
|
pub(crate) fn is_rc_heap_allocated(&self, value: &Term) -> bool {
|
|
if !matches!(self.alloc, AllocStrategy::Rc) {
|
|
return false;
|
|
}
|
|
match value {
|
|
Term::Ctor { .. } | Term::Lam { .. } => {
|
|
let term_ptr = (value as *const Term) as usize;
|
|
!self.non_escape.contains(&term_ptr)
|
|
}
|
|
Term::App { callee, .. } => {
|
|
// Iter 18g.2: a call whose callee carries
|
|
// `ret_mode == Own` hands a fresh heap allocation to
|
|
// the caller's frame. Trackable. `Borrow` and
|
|
// `Implicit` ret-modes do not carry that signal —
|
|
// returning by Borrow is a view into the callee's
|
|
// owned data (caller does not own it), and Implicit
|
|
// is the back-compat lane that 18c.3's debt covers.
|
|
self.synth_callee_ret_mode(callee)
|
|
.map(|m| matches!(m, ParamMode::Own))
|
|
.unwrap_or(false)
|
|
}
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// Iter 18g.2: lookup helper for a callee's `ret_mode`. Returns
|
|
/// `Some(mode)` when the callee resolves to a fn-typed term;
|
|
/// `None` for shapes whose type is not a `Type::Fn` (typechecker
|
|
/// would already have rejected an App on a non-fn, but the helper
|
|
/// is defensive). Used by [`Self::is_rc_heap_allocated`] and the
|
|
/// [`Self::drop_symbol_for_binder`] App-arm to decide both
|
|
/// trackability and the drop-fn symbol.
|
|
fn synth_callee_ret_mode(&self, callee: &Term) -> Option<ParamMode> {
|
|
let cty = self.synth_arg_type(callee).ok()?;
|
|
match cty {
|
|
Type::Fn { ret_mode, .. } => Some(ret_mode),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Iter 18c.4: pick the drop-fn symbol to call at the close of a
|
|
/// trackable `Term::Let` scope. For a `Term::Ctor` binder the
|
|
/// symbol is `drop_<owner>_<TypeName>` — derived from the ctor's
|
|
/// `type_name` (which already encodes the owning module via the
|
|
/// `module.T` form when cross-module). For a `Term::Lam` binder
|
|
/// the symbol comes from `closure_drops`, populated by
|
|
/// [`Self::lower_lambda`] when it emitted the per-pair drop fn.
|
|
///
|
|
/// Falls back to `ailang_rc_dec` for any other shape — should be
|
|
/// unreachable since `is_rc_heap_allocated` only returns `true`
|
|
/// for `Term::Ctor` / `Term::Lam`, but a defensive fallback
|
|
/// keeps the IR well-formed even if the predicate ever widens.
|
|
pub(crate) fn drop_symbol_for_binder(&self, value: &Term, val_ssa: &str) -> String {
|
|
match value {
|
|
Term::Ctor { type_name, .. } => {
|
|
if type_name.matches('.').count() == 1 {
|
|
let (prefix, suffix) =
|
|
type_name.split_once('.').expect("checked");
|
|
if let Some(target) = self.import_map.get(prefix) {
|
|
return format!("drop_{target}_{suffix}");
|
|
}
|
|
return format!("drop_{prefix}_{suffix}");
|
|
}
|
|
format!("drop_{m}_{type_name}", m = self.module_name)
|
|
}
|
|
Term::Lam { .. } => self
|
|
.closure_drops
|
|
.get(val_ssa)
|
|
.cloned()
|
|
.unwrap_or_else(|| "ailang_rc_dec".to_string()),
|
|
// Iter 18g.2: an Own-returning call hands a freshly heap-
|
|
// allocated cell whose static type is the callee's
|
|
// `ret`. Resolve the per-type drop fn from that ret-type
|
|
// so the cascade walks the cell's pointer-typed children
|
|
// (e.g. an Own-returned `Tree` fans out via
|
|
// `drop_<m>_<Tree>`). Falls back to `ailang_rc_dec` if
|
|
// the ret-type is not a `Type::Con` (e.g. a bare type
|
|
// var on an as-yet-unmonomorphised polymorphic call —
|
|
// the monomorphised copies will resolve correctly).
|
|
Term::App { .. } => {
|
|
if let Ok(Type::Con { name, .. }) = self.synth_arg_type(value) {
|
|
// Symmetric to `field_drop_call`'s Str arm: Str is a
|
|
// built-in pointer type with no per-type drop fn. Both
|
|
// heap-Str (rc_header at payload-8) and static-Str
|
|
// (codegen-elision keeps static pointers out of this
|
|
// path) consume via `ailang_rc_dec`.
|
|
if name == "Str" {
|
|
return "ailang_rc_dec".to_string();
|
|
}
|
|
if name.matches('.').count() == 1 {
|
|
let (prefix, suffix) =
|
|
name.split_once('.').expect("checked");
|
|
if let Some(target) = self.import_map.get(prefix) {
|
|
return format!("drop_{target}_{suffix}");
|
|
}
|
|
return format!("drop_{prefix}_{suffix}");
|
|
}
|
|
return format!("drop_{m}_{name}", m = self.module_name);
|
|
}
|
|
"ailang_rc_dec".to_string()
|
|
}
|
|
_ => "ailang_rc_dec".to_string(),
|
|
}
|
|
}
|
|
|
|
/// Iter 18g.tidy.fu2: 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) {
|
|
let m = self.module_name;
|
|
let tname = &td.name;
|
|
let mut out = String::new();
|
|
out.push_str(&format!(
|
|
"define void @partial_drop_{m}_{tname}(ptr %p, i64 %mask) {{\n"
|
|
));
|
|
out.push_str("entry:\n");
|
|
out.push_str(" %is_null = icmp eq ptr %p, null\n");
|
|
out.push_str(" br i1 %is_null, label %ret, label %live\n");
|
|
out.push_str("live:\n");
|
|
out.push_str(" %tag = load i64, ptr %p, align 8\n");
|
|
|
|
let n_ctors = td.ctors.len();
|
|
out.push_str(" switch i64 %tag, label %dflt [\n");
|
|
for (i, _) in td.ctors.iter().enumerate() {
|
|
out.push_str(&format!(" i64 {i}, label %arm_{i}\n"));
|
|
}
|
|
out.push_str(" ]\n");
|
|
|
|
let mut local = 0u64;
|
|
for (i, ctor) in td.ctors.iter().enumerate() {
|
|
out.push_str(&format!("arm_{i}:\n"));
|
|
for (j, fty) in ctor.fields.iter().enumerate() {
|
|
let lty = llvm_type(fty).unwrap_or_else(|_| "ptr".into());
|
|
if lty != "ptr" {
|
|
continue;
|
|
}
|
|
let bit_id = local;
|
|
local += 1;
|
|
let set_id = local;
|
|
local += 1;
|
|
let bitmask: u64 = 1u64 << j;
|
|
out.push_str(&format!(
|
|
" %b{bit_id} = and i64 %mask, {bitmask}\n"
|
|
));
|
|
out.push_str(&format!(
|
|
" %s{set_id} = icmp ne i64 %b{bit_id}, 0\n"
|
|
));
|
|
out.push_str(&format!(
|
|
" br i1 %s{set_id}, label %after_{i}_{j}, label %do_{i}_{j}\n"
|
|
));
|
|
out.push_str(&format!("do_{i}_{j}:\n"));
|
|
let off = 8 + (j as i64) * 8;
|
|
let addr_id = local;
|
|
local += 1;
|
|
let val_id = local;
|
|
local += 1;
|
|
out.push_str(&format!(
|
|
" %a{addr_id} = getelementptr inbounds i8, ptr %p, i64 {off}\n"
|
|
));
|
|
out.push_str(&format!(
|
|
" %v{val_id} = load ptr, ptr %a{addr_id}, align 8\n"
|
|
));
|
|
let drop_call = self.field_drop_call(fty);
|
|
out.push_str(&format!(
|
|
" call void @{drop_call}(ptr %v{val_id})\n"
|
|
));
|
|
out.push_str(&format!(" br label %after_{i}_{j}\n"));
|
|
out.push_str(&format!("after_{i}_{j}:\n"));
|
|
}
|
|
out.push_str(" br label %join\n");
|
|
}
|
|
|
|
out.push_str("dflt:\n");
|
|
if n_ctors == 0 {
|
|
out.push_str(" br label %join\n");
|
|
} else {
|
|
out.push_str(" unreachable\n");
|
|
}
|
|
|
|
out.push_str("join:\n");
|
|
out.push_str(" call void @ailang_rc_dec(ptr %p)\n");
|
|
out.push_str(" br label %ret\n");
|
|
out.push_str("ret:\n");
|
|
out.push_str(" ret void\n");
|
|
out.push_str("}\n\n");
|
|
self.body.push_str(&out);
|
|
}
|
|
|
|
/// Iter 18g.tidy.fu2: 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, .. } => {
|
|
if name == "Str" {
|
|
return None;
|
|
}
|
|
if name.matches('.').count() == 1 {
|
|
let (prefix, suffix) = name.split_once('.').expect("checked");
|
|
if let Some(target) = self.import_map.get(prefix) {
|
|
return Some(format!("partial_drop_{target}_{suffix}"));
|
|
}
|
|
return Some(format!("partial_drop_{prefix}_{suffix}"));
|
|
}
|
|
Some(format!(
|
|
"partial_drop_{m}_{name}",
|
|
m = self.module_name
|
|
))
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Iter 18g.tidy.fu2: 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)
|
|
}
|
|
|
|
/// Iter 18d.3: emit a partial-drop sequence inline at a let-close
|
|
/// site whose binder has moved-out pattern slots. Replaces the
|
|
/// uniform `drop_<m>_<T>(ptr)` call: load each pointer-typed
|
|
/// field whose slot index is NOT in `moved`, dispatch through
|
|
/// `field_drop_call` (the same per-type or shallow drop the
|
|
/// recursive cascade picks), then `ailang_rc_dec` the outer
|
|
/// box. Skips slots in `moved` entirely — those values were
|
|
/// transferred to a pattern-bound binder that owns the dec
|
|
/// for them.
|
|
///
|
|
/// Iter 18g.2 widened the input set to `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.
|
|
/// Iter 18g.tidy.fu2 closes that path: instead of falling back
|
|
/// to shallow `ailang_rc_dec` (which leaked the unmoved fields),
|
|
/// we route through the tag-conditional helper
|
|
/// [`Self::emit_partial_drop_fn_for_type`], which dispatches on
|
|
/// the runtime tag and dec's only the unmoved fields.
|
|
pub(crate) fn emit_inlined_partial_drop(
|
|
&mut self,
|
|
value: &Term,
|
|
val_ssa: &str,
|
|
moved: &BTreeSet<usize>,
|
|
) -> Result<()> {
|
|
let (type_name, ctor_name) = match value {
|
|
Term::Ctor { type_name, ctor, .. } => (type_name.as_str(), ctor.as_str()),
|
|
_ => {
|
|
// Iter 18g.tidy.fu2: dynamic-tag partial-drop via the
|
|
// per-type helper. `value` is `Term::App` (Own-
|
|
// returning) — the binder's static type is the App's
|
|
// ret type, recovered through `synth_arg_type` /
|
|
// `partial_drop_symbol_for_type`. `Term::Lam` shapes
|
|
// never reach here with a non-empty `moved` (you can't
|
|
// pattern-match a closure-pair); the fallback below
|
|
// handles them defensively.
|
|
let sym = self
|
|
.synth_arg_type(value)
|
|
.ok()
|
|
.and_then(|ty| self.partial_drop_symbol_for_type(&ty));
|
|
if let (Some(sym), Some(mask)) =
|
|
(sym, Self::build_moved_mask(moved))
|
|
{
|
|
self.body.push_str(&format!(
|
|
" call void @{sym}(ptr {val_ssa}, i64 {mask})\n"
|
|
));
|
|
} else {
|
|
self.body.push_str(&format!(
|
|
" call void @ailang_rc_dec(ptr {val_ssa})\n"
|
|
));
|
|
}
|
|
return Ok(());
|
|
}
|
|
};
|
|
let cref = self.lookup_ctor_by_type(type_name, ctor_name)?;
|
|
// Per-field dec for non-moved pointer-typed slots. ail_fields
|
|
// are the AILang-level field types; field_drop_call resolves
|
|
// them to either `drop_<owner>_<T>` (ADTs cascade) or
|
|
// `ailang_rc_dec` (Str / closures / vars).
|
|
for (idx, fty_ail) in cref.ail_fields.iter().enumerate() {
|
|
let lty = llvm_type(fty_ail).unwrap_or_else(|_| "ptr".into());
|
|
if lty != "ptr" {
|
|
continue;
|
|
}
|
|
if moved.contains(&idx) {
|
|
continue;
|
|
}
|
|
let off = 8 + (idx as i64) * 8;
|
|
let addr = self.fresh_ssa();
|
|
self.body.push_str(&format!(
|
|
" {addr} = getelementptr inbounds i8, ptr {val_ssa}, i64 {off}\n"
|
|
));
|
|
let v = self.fresh_ssa();
|
|
self.body.push_str(&format!(
|
|
" {v} = load ptr, ptr {addr}, align 8\n"
|
|
));
|
|
let drop_call = self.field_drop_call(fty_ail);
|
|
self.body.push_str(&format!(
|
|
" call void @{drop_call}(ptr {v})\n"
|
|
));
|
|
}
|
|
// Finally dec the outer box. The per-type drop fn would have
|
|
// done this in its `join` block; we replicate it here.
|
|
self.body.push_str(&format!(
|
|
" call void @ailang_rc_dec(ptr {val_ssa})\n"
|
|
));
|
|
Ok(())
|
|
}
|
|
|
|
/// Iter 18c.4: build the IR text for a closure env's drop fn.
|
|
/// Layout: 8 bytes per capture, in declaration order.
|
|
/// For each pointer-typed capture, emit a load + drop call;
|
|
/// finally `ailang_rc_dec` the env block.
|
|
pub(crate) fn build_env_drop_fn(
|
|
&self,
|
|
sym: &str,
|
|
cap_meta: &[(String, String, String, Type, Option<FnSig>)],
|
|
) -> String {
|
|
let mut out = String::new();
|
|
out.push_str(&format!("define void @{sym}(ptr %env) {{\nentry:\n"));
|
|
out.push_str(" %is_null = icmp eq ptr %env, null\n");
|
|
out.push_str(" br i1 %is_null, label %ret, label %live\n");
|
|
out.push_str("live:\n");
|
|
let mut local = 0u64;
|
|
for (i, (_cname, _outer_ssa, lty, ail_ty, _sig)) in cap_meta.iter().enumerate() {
|
|
if lty != "ptr" {
|
|
continue;
|
|
}
|
|
let off = (i as i64) * 8;
|
|
let addr_id = local;
|
|
local += 1;
|
|
let val_id = local;
|
|
local += 1;
|
|
out.push_str(&format!(
|
|
" %a{addr_id} = getelementptr inbounds i8, ptr %env, i64 {off}\n"
|
|
));
|
|
out.push_str(&format!(
|
|
" %v{val_id} = load ptr, ptr %a{addr_id}, align 8\n"
|
|
));
|
|
let drop_call = self.field_drop_call(ail_ty);
|
|
out.push_str(&format!(
|
|
" call void @{drop_call}(ptr %v{val_id})\n"
|
|
));
|
|
}
|
|
out.push_str(" call void @ailang_rc_dec(ptr %env)\n");
|
|
out.push_str(" br label %ret\n");
|
|
out.push_str("ret:\n");
|
|
out.push_str(" ret void\n}\n\n");
|
|
out
|
|
}
|
|
|
|
/// Iter 18c.4: build the IR text for a closure pair's drop fn.
|
|
/// Layout: { ptr thunk, ptr env } — env at offset 8.
|
|
/// Loads env, calls the env drop, then decs the pair box.
|
|
pub(crate) fn build_pair_drop_fn(
|
|
&self,
|
|
sym: &str,
|
|
env_drop: &str,
|
|
has_env: bool,
|
|
) -> String {
|
|
let mut out = String::new();
|
|
out.push_str(&format!("define void @{sym}(ptr %p) {{\nentry:\n"));
|
|
out.push_str(" %is_null = icmp eq ptr %p, null\n");
|
|
out.push_str(" br i1 %is_null, label %ret, label %live\n");
|
|
out.push_str("live:\n");
|
|
if has_env {
|
|
// 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
|
|
}
|
|
}
|