Files
AILang/crates/ailang-mir/src/lib.rs
T
Brummel d72fe0c2e6 fix(codegen): drop branch-consume-split owned params (#63 leg 3)
The last leg of the #63 drop-soundness cluster: an (own ...) heap param
consumed on one match-arm / if-branch but live on a sibling was never
dropped on the live path, leaking one slab per call. This was the
residual live=2 leak in series_sma that kept the series headline from
being leak-clean.

Root: the per-fn aggregate consume_count (the worst-case max over all
branches from uniqueness::merge_states) was codegen's only consume
signal, and every owned-param drop site gated on it. A param consumed
on SOME branch makes the aggregate >= 1, so every site skipped it —
including the branch where it stays live. match and if share this root
(if is first-class MTerm::If, not desugared to match).

Mechanism (spec 0068):
- CAPTURE: uniqueness retains the per-branch consume snapshots it
  already computes per branch and discarded at the max merge — new
  BranchConsume + infer_module_with_cross_branches (the aggregate-only
  infer_module_with_cross now delegates and drops the channel).
- CARRY: additive MArm.consume / MTerm::If.{then,else}_consume, attached
  by lower_to_mir via a traversal-order cursor (post-order pop, kind- and
  exhaustion-asserts make a desync a loud panic; neutral for const bodies
  which uniqueness does not walk). The AST carries no node id, so the
  correspondence is the structural pre-order both walks share — pinned by
  branch_consume_maps_attach_to_matching_arms.
- GATE: emit_leakclass_branch_param_drops fires a fall-through drop only
  for the leak class (branch_consume==0 AND aggregate>=1), in match arms
  + both if branches; the pre-tail-call dec switches its gate source from
  the aggregate to per-arm. The fn-return dec and arm-close pattern-binder
  dec are UNCHANGED.

Safety:
- Double-free: the drop sites partition by aggregate (==0 -> existing
  fn-return/pre-tail-call; >=1 -> new per-branch). Disjoint, so no param
  is dropped twice — no fn-return disable, no tail-position analysis.
- Use-after-free: the checker rejects use-after-consume, so an
  aggregate>=1 param is provably dead past the construct (path-terminal
  drop, tail position irrelevant).
- Type eligibility (found by the full-suite gate during implement; spec
  0068 refined): the new agg>=1 site is the FIRST drop site that can
  reach a STATIC closure-pair param (a top-level fn ref like inc in
  Either.either, consumed on one arm, live on the other) — a .rodata
  constant with no rc-header. field_drop_call routes Type::Fn / static-Str
  / Type::Var to the bare ailang_rc_dec, which underflowed on it. The
  helper now drops only params with a real per-type heap-ADT drop fn
  (field_drop_call != "ailang_rc_dec"). Sound (no underflow) and
  leak-correct (a static closure/Str allocates nothing). Witnessed green
  by std_either_demo / std_list_demo / poly_rec_capture_demo.

Verification: full workspace suite green (116 binaries); both new leak
pins (if + match) and series_sma_no_leak_pin green at live=0; legs 1/2
pins still green; INTERCEPTS<->(intrinsic) bijection intact; no
Pattern::Lit reject path added.

The leg-3 helper's type precondition was applied inline by the
orchestrator (a narrowing guard clause in an already-reviewed Task-4
helper, full context loaded) after the implement-orchestrator correctly
surfaced the spec gap rather than papering over the regression.

This clears the series_sma leak tail; the series milestone (#61) close
stays a separate deliberate step (its end-to-end milestone fieldtest).

closes #63
2026-06-02 15:47:01 +02:00

215 lines
7.8 KiB
Rust

//! Typed mid-level IR — the single artefact that crosses the
//! `check` → `codegen` boundary. Produced by
//! `ailang_check::lower_to_mir` from a monomorphised, typechecked
//! `Workspace`; consumed by `ailang_codegen`. Every node carries the
//! facts codegen used to re-derive: its type (`ty`), the resolved
//! callee (`Callee`, filled mir.2), parameter modes / consume counts
//! (`Mode` / `consume_count`, filled mir.3), and string representation
//! (`StrRep`, filled mir.4). MIR is never authored, never hashed,
//! never round-tripped — it is a compiler-internal derived form.
use ailang_core::ast::{Literal, Module, Pattern, Type};
use std::collections::BTreeMap;
/// A whole monomorphised program, lowered to MIR.
#[derive(Debug, Clone)]
pub struct MirWorkspace {
pub entry: String,
pub modules: BTreeMap<String, MirModule>,
}
#[derive(Debug, Clone)]
pub struct MirModule {
pub name: String,
/// The post-mono AST module. codegen reads structural data
/// (Def::Type / Def::Const / ctor layouts / intrinsic-fn markers /
/// imports) from here; the typed fn bodies come from `defs`. This
/// keeps `MirWorkspace` the single artefact codegen consumes —
/// structural reads were never a re-derivation, so carrying the AST
/// alongside the typed bodies does not reintroduce one.
pub ast: Module,
pub defs: Vec<MirDef>,
/// Typed bodies of the module's non-literal `Def::Const`s. A const
/// is inlined at every `Var` reference site (codegen reads the
/// body from here, keyed by name); literal consts emit a global
/// instead and are not carried. Same role as `defs` for fns — the
/// body is the checker-typed `MTerm` so codegen re-derives no
/// type to lower it.
pub consts: Vec<MirConst>,
}
/// One lowered non-literal const. `body` is the typed `MTerm` codegen
/// inlines at each reference site.
#[derive(Debug, Clone)]
pub struct MirConst {
pub name: String,
pub ty: Type,
pub body: MTerm,
}
/// One lowered top-level fn. `sig` is the declared signature; `body`
/// is the lowered fn body with `ty` on every node.
#[derive(Debug, Clone)]
pub struct MirDef {
pub name: String,
pub sig: Type,
pub params: Vec<String>,
pub body: MTerm,
/// Per-binder consume count for this def's body, keyed by binder
/// name (let-binders, params, pattern-binders). `0` means the
/// binder is exclusively borrowed/unused in its scope and codegen
/// must emit the scope-close `dec` under `--alloc=rc`. Filled by
/// `lower_to_mir` from `ailang_check::uniqueness::infer_module_with_cross`
/// (mir.3a) — the single uniqueness engine, run once on the
/// post-mono body. Codegen reads it instead of re-running the pass.
pub consume: BTreeMap<String, u32>,
}
/// One MIR node — structural counterpart of `ast::Term`, plus the
/// `Str` split. `ty` is the checker-proved type at this node.
#[derive(Debug, Clone)]
pub enum MTerm {
Lit { lit: Literal, ty: Type },
/// String literal — the `Str`-representation carrier. `rep` is
/// `Static` for every literal at mir.1; mir.4 flips loop-carried
/// seeds to `Heap`. Split out of `Lit` so the rep annotation has a
/// home (spec §"Str split").
Str { lit: String, rep: StrRep },
Var { name: String, ty: Type },
/// `callee` is `Indirect(<lowered callee>)` at mir.1; mir.2
/// resolves static targets to `Callee::Static`.
App { callee: Callee, args: Vec<MArg>, tail: bool, ty: Type },
Let { name: String, mode: Mode, init: Box<MTerm>, body: Box<MTerm>, ty: Type },
LetRec {
name: String,
sig: Type,
params: Vec<String>,
body: Box<MTerm>,
in_term: Box<MTerm>,
ty: Type,
},
If {
cond: Box<MTerm>,
then: Box<MTerm>,
else_: Box<MTerm>,
/// #63 leg 3: per-branch consume of each owned binder on the
/// `then` / `else` branch (absolute count at the branch end).
/// Codegen drops a param live here (`0`) but consumed on the
/// sibling (`MirDef.consume >= 1`). Empty for const bodies.
then_consume: BTreeMap<String, u32>,
else_consume: BTreeMap<String, u32>,
ty: Type,
},
Do { op: String, args: Vec<MArg>, tail: bool, ty: Type },
Ctor { type_name: String, ctor: String, args: Vec<MArg>, ty: Type },
Match { scrutinee: Box<MTerm>, arms: Vec<MArm>, ty: Type },
Lam {
params: Vec<String>,
param_tys: Vec<Type>,
ret_ty: Type,
effects: Vec<String>,
body: Box<MTerm>,
ty: Type,
},
Seq { lhs: Box<MTerm>, rhs: Box<MTerm>, ty: Type },
Clone { value: Box<MTerm>, ty: Type },
ReuseAs { source: Box<MTerm>, body: Box<MTerm>, ty: Type },
Loop { binders: Vec<MLoopBinder>, body: Box<MTerm>, ty: Type },
Recur { args: Vec<MArg>, ty: Type },
/// `elem` is `None` at mir.1; mir.5 carries the element type.
New { type_name: String, elem: Option<Type>, args: Vec<MNewArg>, ty: Type },
Intrinsic { ty: Type },
}
/// A match arm. Patterns stay structural (`ast::Pattern`) — they carry
/// no type annotation the boundary re-derives.
#[derive(Debug, Clone)]
pub struct MArm {
pub pat: Pattern,
pub body: MTerm,
/// #63 leg 3: per-arm consume of each owned binder (absolute count at
/// this arm's end). See `MTerm::If.then_consume`. Empty for const bodies.
pub consume: BTreeMap<String, u32>,
}
#[derive(Debug, Clone)]
pub struct MLoopBinder {
pub name: String,
pub ty: Type,
pub init: MTerm,
}
#[derive(Debug, Clone)]
pub enum MNewArg {
Type(Type),
Value(MTerm),
}
/// A call/ctor/recur argument with its mode and consume count. Both
/// fields are mir.1 defaults (`Owned` / `1`); mir.3 fills them.
#[derive(Debug, Clone)]
pub struct MArg {
pub term: MTerm,
pub mode: Mode,
pub consume_count: u32,
}
/// A resolved App callee. `Static`/`Builtin` are filled by mir.2's
/// `lower_to_mir::classify_callee`, which mirrors check's `synth`
/// `Term::Var` ladder; `Indirect` is a genuinely dynamic callee
/// (fn-typed local/param, closure value, or a name shadowed by a
/// local). `sig` is the callee's fn-type — the lossless replacement
/// for the sub-term's `ty()` that codegen's drop path reads for the
/// callee `ret_mode`.
#[derive(Debug, Clone)]
pub enum Callee {
/// A statically-resolved user/prelude fn: codegen routes it to
/// `emit_call(module, fn_name, …)` with `module` pre-resolved
/// (replacing codegen's `type_home_module` ladder).
Static { module: String, fn_name: String, sig: Type },
/// An operator / intrinsic builtin (`+`, `not`, `str_concat`, …):
/// codegen lowers it inline by `name` (opcode selection), never
/// via `emit_call`.
Builtin { name: String, sig: Type },
/// A dynamic callee — lower the boxed term to a fn-pointer.
Indirect(Box<MTerm>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StrRep {
Heap,
Static,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Owned,
Borrow,
}
impl MTerm {
/// The checker-proved type at this node. `Str` is always `Str`.
pub fn ty(&self) -> Type {
match self {
MTerm::Lit { ty, .. }
| MTerm::Var { ty, .. }
| MTerm::App { ty, .. }
| MTerm::Let { ty, .. }
| MTerm::LetRec { ty, .. }
| MTerm::If { ty, .. }
| MTerm::Do { ty, .. }
| MTerm::Ctor { ty, .. }
| MTerm::Match { ty, .. }
| MTerm::Lam { ty, .. }
| MTerm::Seq { ty, .. }
| MTerm::Clone { ty, .. }
| MTerm::ReuseAs { ty, .. }
| MTerm::Loop { ty, .. }
| MTerm::Recur { ty, .. }
| MTerm::New { ty, .. }
| MTerm::Intrinsic { ty } => ty.clone(),
MTerm::Str { .. } => Type::str_(),
}
}
}