codegen tidy: extract drop emission into drop.rs
Second slice of the codegen split. Pulls every method that emits
or dispatches drop fns into a dedicated module. No behaviour
change.
Cluster A (per-type drop bodies):
- emit_drop_fn_for_type — recursive cascade, ADT default.
- emit_iterative_drop_fn_for_type — Iter 18e worklist variant.
- field_is_same_type — same-ADT predicate (private to drop.rs).
- field_drop_call — per-field drop-symbol resolver.
Cluster B (per-let-close dispatch):
- is_rc_heap_allocated — Iter 18c.3 trackability gate, widened
in 18g.2 to include Own-returning App.
- synth_callee_ret_mode — ret_mode lookup helper (private).
- drop_symbol_for_binder — picks the symbol Term::Let calls
at scope close.
- emit_inlined_partial_drop — partial-drop emission for
pattern-moved slots.
Cluster C (closure drops):
- build_env_drop_fn — env's drop fn body builder.
- build_pair_drop_fn — pair's drop fn body builder.
Visibility: methods called from lib.rs are pub(crate);
helpers used only inside drop.rs (field_is_same_type,
synth_callee_ret_mode) stay private. Three Emitter helpers
in lib.rs that drop.rs calls back into were upgraded to
pub(crate): synth_arg_type, lookup_ctor_by_type, fresh_ssa.
Field access from drop.rs into the parent's private Emitter
fields uses the standard descendant-module privacy lane.
lib.rs drops from 4800 → 4139 lines. drop.rs is 580 lines.
cargo test --workspace clean: 66/66 e2e + 26/26 codegen +
all sibling crates pass.
This commit is contained in:
@@ -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_<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");
|
||||
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_<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
|
||||
// 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<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(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_<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 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<usize>,
|
||||
) -> 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_<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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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_<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).
|
||||
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_<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.
|
||||
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.
|
||||
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<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
|
||||
/// [`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_<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(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_<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 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<usize>,
|
||||
) -> 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_<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(())
|
||||
}
|
||||
|
||||
/// 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<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.
|
||||
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<Type> {
|
||||
pub(crate) fn synth_arg_type(&self, t: &Term) -> Result<Type> {
|
||||
self.synth_with_extras(t, &[])
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user