Skip to main content

aver/codegen/rust/
from_mir.rs

1//! Rust backend: emit expressions from Core MIR.
2//!
3//! This is the SOLE Rust runtime codegen path. The HIR `ResolvedExpr`
4//! walker was deleted in rust-on-MIR W6/Stage-3; the MIR walker here
5//! owns all runtime codegen, the same deduplication MIR brought to the
6//! VM (#339) and wasm-gc (#384): one semantic walker per construct lives
7//! in MIR, and every backend reads from it instead of forking
8//! `ResolvedExpr`.
9//!
10//! [`emit_mir_expr`] is the dispatcher; [`coverage_report`] measures how
11//! much of a program it can render standalone. [`emit_mir_fn_body`] wraps
12//! it into the full single-expr-plan body format, and
13//! [`emit_mir_fn_body_routed`] is the production wire-up: it builds the
14//! per-fn borrow policy and renders the body. A construct the walker
15//! returns `None` for surfaces as a hard codegen diagnostic at the call
16//! site (the only residual is the verify-only Oracle/trace shapes that
17//! never built on the Rust backend).
18//!
19//! ## Covered constructs
20//!
21//! `Literal`, `Local`, `Neg`, `BinOp` (numeric ops, plus `Str` `+`
22//! concat — the right side borrowed for `AverStr`'s `Add<&AverStr>` —
23//! disambiguated from numeric add by the operands' type stamps),
24//! `Call` (`Fn` / `Builtin` / `Intrinsic` / `LocalSlot` — the last a
25//! first-class fn-pointer call `name(args…)`, post-#379 always a plain
26//! fn-pointer since `Type::Fn` is param-only), `Return`, `TailCall`
27//! (emitted as a plain call; the HIR self-TCO `continue` rewrite needs
28//! `ectx`, so the wire-up's parity check is the safety net), `Try` (`?`),
29//! `Tuple`, `List`, `MapLiteral`, `Let` (block-expr `{ let x = …; … }`),
30//! `Project`, `RecordCreate` / `RecordUpdate`, `Construct` (built-in and
31//! user ctors, including dep-module records resolved through
32//! `module_prefixes`), `IfThenElse`, `IndependentProduct`, and `FnValue`
33//! (a fn referenced as a value — the `StaticRef` shape).
34//!
35//! `Match` (Wave 2) — `MirExpr::Match` emits through [`emit_mir_match`],
36//! mirroring HIR's `emit_match` / `emit_dispatch_table_match` /
37//! `emit_list_match` selection byte-for-byte. The shared classifiers
38//! (`classify_match_dispatch_plan_resolved` etc.) + `emit_pattern` +
39//! the dispatch/list emitters are reused directly by translating each
40//! `MirPattern` → `ResolvedPattern` and feeding a `body_for_arm`
41//! closure that renders the matching arm's MIR body. Bool two-arm
42//! matches never reach this arm — the MIR optimizer's `bool_match_to_if`
43//! pass already rewrote them to `IfThenElse` (handled above).
44//!
45//! `InterpolatedStr` never reaches the walker — `interp_lower` lowers it
46//! away before codegen runs. Every reachable MIR construct has a walker
47//! arm; the only `None` cases are the verify-only Oracle/trace shapes
48//! that never built on the Rust backend (they hard-error at the call
49//! site).
50
51use std::collections::{HashMap, HashSet};
52
53use crate::ast::{BinOp, Spanned, Type};
54use crate::codegen::CodegenContext;
55use crate::codegen::common::module_prefix_to_rust_path;
56use crate::ir::hir::{
57    BuiltinCtor, BuiltinIntrinsic, ResolvedCtor, ResolvedMatchArm, ResolvedPattern,
58    classify_match_dispatch_plan_resolved,
59};
60use crate::ir::mir::{MirCallee, MirCtor, MirExpr, MirLocal, MirMatch, MirPattern, MirProgram};
61use crate::ir::{MatchDispatchPlan, SymbolTable};
62
63use super::emit_ctx::{is_copy_type, should_borrow_param};
64use super::expr::{
65    callee_borrow_mask, constructor_boxed_positions, emit_dispatch_table_match, emit_list_match,
66    emit_literal, emit_parallel_result_tuple_unwrap, emit_pattern_rebindings,
67    emit_ref_match_rebindings, emit_result_tuple_unwrap, emit_tuple_from_vars, has_list_patterns,
68    has_string_literal_patterns,
69};
70use super::pattern::emit_pattern;
71use super::syntax::aver_name_to_rust;
72
73/// Walker-side emit context. Holds the slice of the
74/// `CodegenContext` the MIR-to-Rust walker reads — kept explicit
75/// so future `CodegenContext` refactors don't ripple through the
76/// walker, and so other backends (wasm-gc, wasip2) can introduce
77/// their own emit-ctx structs without inheriting Rust-specific
78/// fields.
79///
80/// Two distinct shapes share this struct:
81///
82/// - **coverage / test** (`for_test`): only `symbol_table` +
83///   `module_prefixes` are populated; `codegen` is `None` and the
84///   borrow fields are empty. The coverage walk only asks "does
85///   this fn emit `Some`", so it never needs the borrow machinery
86///   or the full `CodegenContext`.
87/// - **production parity gate** (`for_fn`): carries the full
88///   `&CodegenContext` plus the per-fn borrow policy
89///   (`local_types` / `rc_wrapped` / `borrowed_params` /
90///   `current_module_scope`), recomputed from the `ResolvedFnDef`
91///   the HIR walker uses. This is the slice of
92///   [`super::emit_ctx::EmitCtx`] the covered arms need so their
93///   clone / borrow / `Arc::new` decisions match HIR byte-for-byte.
94#[derive(Clone, Copy)]
95pub struct MirEmitCtx<'a> {
96    pub symbol_table: &'a SymbolTable,
97    pub module_prefixes: &'a HashSet<String>,
98    /// Full codegen context — `Some` only on the production parity
99    /// gate path. `constructor_boxed_positions` /
100    /// `callee_borrow_mask` need it; the coverage walk leaves it
101    /// `None` (no borrow decisions, just structural reach).
102    pub codegen: Option<&'a CodegenContext>,
103    /// Local variable types (fn params + let bindings) for
104    /// copy-type elision. Empty on the coverage path.
105    pub local_types: &'a HashMap<String, Type>,
106    /// Params passed as `Rc<T>` (self-TCO) / `&T` (mutual-TCO).
107    pub rc_wrapped: &'a HashSet<String>,
108    /// Params emitted as `&T` (borrow-by-default for non-Copy,
109    /// non-Str params).
110    pub borrowed_params: &'a HashSet<String>,
111    /// Collection (`Vector`/`Map`) params that the `own_param` MIR pass
112    /// PROVED uniquely owned (cleared their `aliased_slots` bit). These
113    /// are emitted owned-by-value (`mut p: T`, NOT `&T`) and, at a
114    /// last-use read, skip the `.clone()` so an in-place `Rc::make_mut`
115    /// runs on a refcount-1 backing (native O(n) mutate instead of the
116    /// O(n²) borrow+clone COW). SOUNDNESS: a name is in this set only
117    /// when `own_param` cleared its bit — never broadened past what the
118    /// pass proved. Disjoint from `borrowed_params` by construction.
119    pub owned_params: &'a HashSet<String>,
120    /// Owning module prefix for the fn whose body this ctx emits.
121    pub current_module_scope: Option<&'a str>,
122    /// Interned built-in fn names, indexed by `BuiltinId`
123    /// (`MirProgram.builtins`). The `Call(Builtin(id))` arm resolves
124    /// `id` → dotted name through this slice, mirroring wasm-gc's
125    /// `ctx.mir_builtins`. Empty on the coverage / test path — a
126    /// `BuiltinId` then resolves to nothing (`None` → HIR fallback),
127    /// which is fine because that path only inspects `Some` vs `None`.
128    pub mir_builtins: &'a [String],
129    /// Per-`LocalId` bare-`i64` representation facts for the fn whose body
130    /// this ctx emits — the Int "unboxing" analysis output. A slot proven
131    /// `Bare` emits native `i64` (raw literal / raw arithmetic / `i64`
132    /// param-return signature); every other slot keeps `aver_rt::AverInt`.
133    /// Empty (all-`Boxed`) on the coverage / test / free-standing paths.
134    /// SOUNDNESS: a slot is read here as `Bare` only when the analysis
135    /// proved `raw_i64_eligible && !escapes`; a missing fact is `Boxed`.
136    pub bare: &'a crate::ir::mir::FnBareFacts,
137}
138
139impl<'a> MirEmitCtx<'a> {
140    /// Construct a minimal walker ctx for the coverage walk /
141    /// tests. Caller supplies a hand-built symbol table;
142    /// `module_prefixes` defaults to the caller's owned empty set
143    /// (or a populated one when the test needs to exercise
144    /// module-scoped name resolution). No `CodegenContext`, no
145    /// borrow policy — the covered arms emit conservative output
146    /// (no clone / borrow / `Arc::new`), which is fine because the
147    /// coverage walk only inspects `Some` vs `None`.
148    pub fn for_test(symbol_table: &'a SymbolTable, module_prefixes: &'a HashSet<String>) -> Self {
149        static EMPTY_TYPES: std::sync::OnceLock<HashMap<String, Type>> = std::sync::OnceLock::new();
150        static EMPTY_SET: std::sync::OnceLock<HashSet<String>> = std::sync::OnceLock::new();
151        Self {
152            symbol_table,
153            module_prefixes,
154            codegen: None,
155            local_types: EMPTY_TYPES.get_or_init(HashMap::new),
156            rc_wrapped: EMPTY_SET.get_or_init(HashSet::new),
157            borrowed_params: EMPTY_SET.get_or_init(HashSet::new),
158            owned_params: EMPTY_SET.get_or_init(HashSet::new),
159            current_module_scope: None,
160            // No builtin table on the coverage path: `Call(Builtin)`
161            // resolves to `None` and the fn reports as HIR-fallback,
162            // matching the pre-Wave-3a coverage walk's reach.
163            mir_builtins: &[],
164            bare: empty_bare_facts(),
165        }
166    }
167
168    /// Construct a **program-level** walker ctx for free-standing
169    /// expressions that belong to no `ResolvedFnDef` — verify cases
170    /// (this wave) and, next wave, `main` / top-level statements. The
171    /// MIR mirror of `EmitCtx::empty()`: carries the full
172    /// `&CodegenContext` (so ctor boxing / `callee_borrow_mask` / match
173    /// emission work, unlike the coverage `for_test` path which leaves
174    /// `codegen` `None`), but with an **empty per-fn policy** — no
175    /// params, no locals, nothing borrowed-by-default. Every name a
176    /// covered arm sees is treated owned / non-Copy, exactly as
177    /// `EmitCtx::empty()` does for the HIR walker on these same
178    /// free-standing exprs.
179    ///
180    /// Shared infra: both the verify wire-up and the next-wave
181    /// main/top-stmt wire-up build their `MirEmitCtx` from here, so the
182    /// "no-anchor" emit policy lives in one place.
183    ///
184    /// `mir_builtins` is passed explicitly rather than read off
185    /// `ctx.mir_program`: free-standing exprs are lowered against a
186    /// *clone* of the entry program (so builtin / instantiation table
187    /// growth stays local), and `Call(Builtin(id))` must resolve `id`
188    /// through that grown clone's table — not the entry program's,
189    /// which may lack a builtin the lowering just interned. The caller
190    /// owns the clone and lends its `builtins` slice here.
191    pub(super) fn program_level(
192        ctx: &'a CodegenContext,
193        policy: &'a MirFnEmitPolicy,
194        mir_builtins: &'a [String],
195    ) -> Self {
196        Self {
197            symbol_table: &ctx.symbol_table,
198            module_prefixes: &ctx.module_prefixes,
199            codegen: Some(ctx),
200            local_types: &policy.local_types,
201            rc_wrapped: &policy.rc_wrapped,
202            borrowed_params: &policy.borrowed_params,
203            owned_params: &policy.owned_params,
204            current_module_scope: policy.current_module_scope.as_deref(),
205            mir_builtins,
206            bare: &policy.bare,
207        }
208    }
209
210    /// Construct a borrow-aware walker ctx for the production
211    /// parity gate. `policy` is the [`MirFnEmitPolicy`] recomputed
212    /// per-fn from the `ResolvedFnDef` (the same inputs
213    /// `build_fn_ectx_from_resolved` feeds the HIR walker), and
214    /// `ctx` is the full codegen context the borrow helpers query.
215    pub(super) fn for_fn(ctx: &'a CodegenContext, policy: &'a MirFnEmitPolicy) -> Self {
216        Self {
217            symbol_table: &ctx.symbol_table,
218            module_prefixes: &ctx.module_prefixes,
219            codegen: Some(ctx),
220            local_types: &policy.local_types,
221            rc_wrapped: &policy.rc_wrapped,
222            borrowed_params: &policy.borrowed_params,
223            owned_params: &policy.owned_params,
224            current_module_scope: policy.current_module_scope.as_deref(),
225            // The builtin table the parity gate already built into the
226            // `CodegenContext`. `Call(Builtin(id))` resolves `id`
227            // through it; if the ctx carries no MIR program (it always
228            // does on the gate path, but be defensive) builtins just
229            // won't resolve → HIR fallback.
230            mir_builtins: ctx
231                .mir_program
232                .as_ref()
233                .map(|p| p.builtins.as_slice())
234                .unwrap_or(&[]),
235            bare: &policy.bare,
236        }
237    }
238
239    /// Is this local a Copy type in Rust (i64 / f64 / bool / ())?
240    fn is_copy(&self, name: &str) -> bool {
241        self.local_types.get(name).is_some_and(is_copy_type)
242    }
243
244    fn is_rc_wrapped(&self, name: &str) -> bool {
245        self.rc_wrapped.contains(name)
246    }
247
248    fn is_borrowed_param(&self, name: &str) -> bool {
249        self.borrowed_params.contains(name)
250    }
251}
252
253/// A shared empty `FnBareFacts` (all-`Boxed`) for the coverage / test /
254/// free-standing emit paths that have no per-fn unboxing facts. Returning
255/// a `'static` reference keeps `MirEmitCtx` `Copy`.
256fn empty_bare_facts() -> &'static crate::ir::mir::FnBareFacts {
257    static EMPTY: std::sync::OnceLock<crate::ir::mir::FnBareFacts> = std::sync::OnceLock::new();
258    EMPTY.get_or_init(crate::ir::mir::FnBareFacts::default)
259}
260
261/// Does `expr` evaluate to a provably-bare `i64` value (so it may be
262/// emitted with native integer arithmetic rather than `AverInt` methods)?
263///
264/// - A `Local` whose slot the analysis proved `Bare`.
265/// - An `Int` literal (an exact constant — always representable as `i64`
266///   in a bare arithmetic context).
267/// - A `Neg` / `Add` / `Sub` / `Mul` over bare operands WHOSE RESULT
268///   INTERVAL provably fits `i64` (a tree like `n + i64::MAX` whose operands
269///   are each bare but whose result leaves `i64` is NOT bare — emitting raw
270///   `i64` there would silently wrap with `overflow-checks = false`).
271///
272/// SINGLE SOURCE OF TRUTH: this is a thin delegate to
273/// [`crate::ir::mir::FnBareFacts::expr_is_bare_i64`] — the SAME interval-
274/// checked predicate the analysis's `tail_value_is_bare` uses — so codegen
275/// never re-decides bareness structurally and can never disagree with the
276/// analysis that drove the signature.
277///
278/// Fail-closed: any other shape (a boxed `Local`, a call result, a
279/// non-Int operand, an overflowing compound) is NOT bare, so the emit
280/// keeps `AverInt`.
281fn mir_expr_is_bare_i64(expr: &Spanned<MirExpr>, ctx: &MirEmitCtx<'_>) -> bool {
282    ctx.bare.expr_is_bare_i64(&expr.node)
283}
284
285/// Emit `expr` as a raw `i64` expression, assuming [`mir_expr_is_bare_i64`]
286/// already returned `true`. Literals become the bare `{N}i64` form,
287/// arithmetic uses native operators, and a bare `Local` is its plain
288/// ident. `None` only if a nested operand can't render (e.g. a synthetic
289/// unnamed local) — the caller then falls back to the boxed path. Takes no
290/// `ctx`: the bare gate (`mir_expr_is_bare_i64`) already consulted it, and
291/// the emit itself is purely structural.
292fn emit_bare_i64(expr: &Spanned<MirExpr>) -> Option<String> {
293    match &expr.node {
294        MirExpr::Literal(l) => match l.node {
295            crate::ast::Literal::Int(k) => Some(format!("{}i64", k)),
296            _ => None,
297        },
298        MirExpr::Local(local) => {
299            let name = &local.node.name;
300            if name.is_empty() {
301                return None;
302            }
303            Some(aver_name_to_rust(name))
304        }
305        MirExpr::Neg(_) => {
306            // Sound only when the interval excludes `i64::MIN`; the
307            // analysis proves the operand fits `i64`, but `-i64::MIN`
308            // wraps. We bail to the boxed path for a bare `Neg` (the
309            // const-fold pass collapses `Neg(Literal)` before here, so a
310            // real bare `Neg` is rare) — keeping `AverInt` is always sound.
311            None
312        }
313        MirExpr::BinOp(b) => {
314            let op = match b.node.op {
315                BinOp::Add => "+",
316                BinOp::Sub => "-",
317                BinOp::Mul => "*",
318                _ => return None,
319            };
320            let l = emit_bare_i64(&b.node.lhs)?;
321            let r = emit_bare_i64(&b.node.rhs)?;
322            Some(format!("({} {} {})", l, op, r))
323        }
324        _ => None,
325    }
326}
327
328/// Per-fn borrow policy for the MIR walker — the slice of
329/// [`super::emit_ctx::EmitCtx`] the covered arms read, owned so a
330/// borrowing [`MirEmitCtx`] can be built from it. Recomputed per
331/// fn from the `ResolvedFnDef`, mirroring `for_fn` /
332/// `for_fn_no_borrow` on `EmitCtx`.
333pub(super) struct MirFnEmitPolicy {
334    pub local_types: HashMap<String, Type>,
335    pub rc_wrapped: HashSet<String>,
336    pub borrowed_params: HashSet<String>,
337    /// Collection params `own_param` proved uniquely owned — see the
338    /// `MirEmitCtx::owned_params` doc. Default empty (no own_param facts
339    /// applied); populated by [`Self::apply_own_param`].
340    pub owned_params: HashSet<String>,
341    /// Per-`LocalId` bare-`i64` facts for this fn (the Int unboxing
342    /// analysis output), owned so the borrowing `MirEmitCtx` can reference
343    /// it. Default empty (all-`Boxed`); populated by
344    /// [`Self::apply_bare_i64`] from the `CodegenContext`'s program-wide
345    /// `BareI64Facts`.
346    pub bare: crate::ir::mir::FnBareFacts,
347    pub current_module_scope: Option<String>,
348}
349
350impl MirFnEmitPolicy {
351    /// The empty / no-anchor borrow policy — no params, no locals,
352    /// nothing borrowed-by-default. Feeds [`MirEmitCtx::program_level`]
353    /// for free-standing expressions (verify cases, main / top-level
354    /// statements). The MIR mirror of `EmitCtx::empty()`.
355    pub(super) fn empty() -> Self {
356        Self {
357            local_types: HashMap::new(),
358            rc_wrapped: HashSet::new(),
359            borrowed_params: HashSet::new(),
360            owned_params: HashSet::new(),
361            bare: crate::ir::mir::FnBareFacts::default(),
362            current_module_scope: None,
363        }
364    }
365
366    /// Build the borrow policy from a `ResolvedFnDef`'s param
367    /// types. `borrow_by_default` mirrors `EmitCtx::for_fn` (true)
368    /// vs `EmitCtx::for_fn_no_borrow` (false, the TCO path):
369    /// when false, no param is borrowed-by-default. `rc_wrapped`
370    /// starts empty (set later for TCO pass-through, which the
371    /// covered subset doesn't graduate).
372    pub(super) fn from_resolved(
373        resolved: &crate::ir::hir::ResolvedFnDef,
374        scope: Option<&str>,
375        borrow_by_default: bool,
376    ) -> Self {
377        let local_types: HashMap<String, Type> = resolved
378            .params
379            .iter()
380            .map(|(name, ty)| (name.clone(), ty.clone()))
381            .collect();
382        let borrowed_params = if borrow_by_default {
383            local_types
384                .iter()
385                .filter(|(_, ty)| should_borrow_param(ty))
386                .map(|(name, _)| name.clone())
387                .collect()
388        } else {
389            HashSet::new()
390        };
391        Self {
392            local_types,
393            rc_wrapped: HashSet::new(),
394            borrowed_params,
395            owned_params: HashSet::new(),
396            bare: crate::ir::mir::FnBareFacts::default(),
397            current_module_scope: scope.map(String::from),
398        }
399    }
400
401    /// Apply the Int "unboxing" facts to this policy: clone the per-fn
402    /// `FnBareFacts` slice out of the program-wide `BareI64Facts` so the
403    /// body emit and the signature emit read the SAME per-`LocalId`
404    /// representation decisions. A bare `Int` param is ALSO dropped from
405    /// `borrowed_params` — a bare `i64` is `Copy`, taken by value, never
406    /// borrowed. Fail-closed: a fn with no facts keeps the default empty
407    /// (all-`Boxed`).
408    pub(super) fn apply_bare_i64(&mut self, fn_id: crate::ir::FnId, ctx: &CodegenContext) {
409        if let Some(facts) = ctx.bare_i64.for_fn(fn_id) {
410            self.bare = facts.clone();
411        }
412    }
413
414    /// Apply the `own_param` MIR pass's ownership facts to this policy:
415    /// every `Vector`/`Map` param whose `MirFn.aliased_slots` bit was
416    /// CLEARED (proven uniquely owned) graduates from borrow-by-default
417    /// to **owned-by-value** — moved OUT of `borrowed_params` and INTO
418    /// `owned_params` so the signature emits `mut p: T` and the body
419    /// skips the `.clone()` at a last-use mutation site (native in-place
420    /// `Rc::make_mut`, refcount-1).
421    ///
422    /// SOUNDNESS (the #383 corruption class): a collection param is
423    /// graduated ONLY when `own_param` cleared its bit. `own_param`'s
424    /// RULE 1 flags EVERY `Vector`/`Map` param `true` up front and only
425    /// clears the bit on a whole-program proof of unique ownership
426    /// (every visible call site passes a fresh / linearly-threaded
427    /// value, captured-into-aggregate slots stay flagged, multi-module
428    /// returns early leaving every bit set). So a cleared bit on a
429    /// collection param is exactly the pass's proof — never a heuristic.
430    /// A missing bit defaults to flagged (`true`) → not graduated
431    /// (conservative). Params still flagged keep borrow-by-default.
432    pub(super) fn apply_own_param(&mut self, mir_fn: &crate::ir::mir::MirFn) {
433        for (i, param) in mir_fn.params.iter().enumerate() {
434            // Only collection params are candidates (the only thing
435            // `own_param`'s RULE 1 ever flags). A non-collection param is
436            // never owned-graduated by this pass, so leave it untouched.
437            // Check the REAL `Type` (from the policy's `local_types`,
438            // sourced from `ResolvedFnDef`) — the `MirParam.ty` is a
439            // `format!("{ty:?}")` Debug string (`Vector(Int)`), fragile to
440            // parse.
441            let rust_name = aver_name_to_rust(&param.name);
442            let Some(ty) = self.local_types.get(&rust_name) else {
443                continue;
444            };
445            if !is_owned_collection_candidate(ty) {
446                continue;
447            }
448            // `own_param`'s `prone`/clearing both index `aliased_slots`
449            // by PARAM POSITION `i` (its `(0..nparams).filter(|&i| …)`),
450            // matching `MirParam.local = LocalId(i)`; match that exactly.
451            //
452            // Cleared bit ⟺ own_param proved unique ownership. Missing →
453            // treat as flagged (conservative). Still-flagged → keep the
454            // existing borrow-by-default decision (do not graduate).
455            let flagged = mir_fn.aliased_slots.get(i).copied().unwrap_or(true);
456            if flagged {
457                continue;
458            }
459            // Graduate: owned-by-value. On the borrow-by-default path the
460            // param was in `borrowed_params`; remove it. On the TCO
461            // no-borrow path it was never borrowed (already `mut`-owned),
462            // but it still needs to land in `owned_params` so the body's
463            // clone-skip fires.
464            self.borrowed_params.remove(&rust_name);
465            self.owned_params.insert(rust_name);
466        }
467    }
468}
469
470/// Is this the type of a param `own_param` can prove owned — a `Vector`
471/// or `Map`? These are the only param shapes `alias.rs` RULE 1 flags and
472/// thus the only ones `own_param` ever clears; nothing else is a sound
473/// clone-skip candidate. (`List` is an `Rc`-COW persistent list whose
474/// clone is cheap and is NOT flagged by RULE 1, so it stays borrowed.)
475fn is_owned_collection_candidate(ty: &Type) -> bool {
476    matches!(ty, Type::Vector(_) | Type::Map(_, _))
477}
478
479/// The Rust-mangled names of a fn's `Vector`/`Map` params that
480/// `own_param` PROVED uniquely owned (cleared `aliased_slots` bit) — the
481/// set the non-TCO SIGNATURE emits owned-by-value (`mut p: T`). The
482/// `param_types` are the `ResolvedFnDef` param `(name, Type)` pairs (real
483/// `Type`, not the `MirParam.ty` Debug string); the `mir_fn` supplies the
484/// optimized `aliased_slots`. Computed exactly as
485/// [`MirFnEmitPolicy::apply_own_param`] (same param-position indexing,
486/// same RULE-1 candidate filter, same missing-bit-is-flagged default) so
487/// signature and body never disagree.
488pub(super) fn owned_collection_param_names(
489    mir_fn: &crate::ir::mir::MirFn,
490    param_types: &[(String, Type)],
491) -> HashSet<String> {
492    let mut out = HashSet::new();
493    for (i, (name, ty)) in param_types.iter().enumerate() {
494        if !is_owned_collection_candidate(ty) {
495            continue;
496        }
497        let flagged = mir_fn.aliased_slots.get(i).copied().unwrap_or(true);
498        if flagged {
499            continue;
500        }
501        out.insert(aver_name_to_rust(name));
502    }
503    out
504}
505
506/// Dotted built-in record/service types whose source `type_name`
507/// (e.g. `Tcp.Connection`, `Terminal.Size`) maps to a re-exported
508/// flat-named Rust struct (`Tcp_Connection`, `Terminal_Size`) brought
509/// in by the matching `generate_*_types()` `pub use` alias. Returns the
510/// Rust name on a hit, `None` for ordinary user records (which keep
511/// their verbatim `type_name`).
512fn builtin_dotted_record_rename(type_name: &str) -> Option<&'static str> {
513    match type_name {
514        "Tcp.Connection" => Some("Tcp_Connection"),
515        "Terminal.Size" => Some("Terminal_Size"),
516        _ => None,
517    }
518}
519
520/// Mirror of `RustSourceCallCtx::resolve_module_call` in
521/// `toplevel.rs`: find the longest registered module prefix
522/// inside a dotted name. Returns `(prefix, suffix)` on hit,
523/// `None` when no registered prefix matches.
524fn resolve_module_call<'a>(
525    dotted: &'a str,
526    module_prefixes: &HashSet<String>,
527) -> Option<(&'a str, &'a str)> {
528    let mut best: Option<(&str, &str)> = None;
529    for (dot_idx, _) in dotted.match_indices('.') {
530        let prefix = &dotted[..dot_idx];
531        let suffix = &dotted[dot_idx + 1..];
532        if module_prefixes.contains(prefix)
533            && best.is_none_or(|existing| prefix.len() > existing.0.len())
534        {
535            best = Some((prefix, suffix));
536        }
537    }
538    best
539}
540
541/// Resolve a bare record `type_name` (`"Note"`) to the Rust path that
542/// names its struct. For a type defined in a `depends`-ed module the
543/// symbol table carries a scoped [`TypeKey`] (`scope = "Apps.Notepad.
544/// Store"`), so the canonical name is dotted and routes through
545/// [`resolve_module_call`] to the module-mangled path
546/// (`crate::aver_generated::apps::notepad::store::Note`). For an
547/// entry-scope type (`scope = None`) the canonical name is bare and
548/// no qualification is needed, so this returns `None` and the caller
549/// keeps the verbatim name (resolved in scope by the entry module's
550/// own `use`).
551///
552/// This is the verify-test-path sibling of the `Construct(User)`
553/// emit's module-path mangling: a `RecordCreate`/`RecordUpdate` of a
554/// cross-module type inside a `#[cfg(test)]` verify module has no
555/// glob `use` bringing the dep type into scope, so the reference must
556/// be fully qualified. Reuses [`resolve_module_call`] +
557/// [`module_prefix_to_rust_path`], the same helpers the runtime
558/// cross-module ctor / fn-ref emit uses.
559///
560/// Identity comes from the `MirRecordCreate.type_id` when present (the
561/// resolver's precise handle — robust against two dep modules sharing a
562/// bare type name); a `None` `type_id` falls back to the first
563/// symbol-table entry whose bare name matches.
564fn qualify_record_type(
565    type_id: Option<crate::ir::TypeId>,
566    type_name: &str,
567    ctx: &MirEmitCtx<'_>,
568) -> Option<String> {
569    let entry = match type_id {
570        Some(id) => ctx.symbol_table.type_entry(id),
571        None => ctx
572            .symbol_table
573            .types
574            .iter()
575            .find(|e| e.key.name == type_name)?,
576    };
577    let canonical = entry.key.canonical();
578    let (prefix, suffix) = resolve_module_call(&canonical, ctx.module_prefixes)?;
579    Some(format!(
580        "{}::{}",
581        module_prefix_to_rust_path(prefix),
582        suffix
583    ))
584}
585
586/// Pick the Rust type name for a `RecordCreate` / `RecordUpdate`.
587/// Precedence, mirroring HIR's verbatim-type-name shape plus the
588/// new cross-module qualification:
589///  1. built-in dotted record rename (`Tcp.Connection` →
590///     `Tcp_Connection`),
591///  2. module-qualified path for a `depends`-ed user record (so a
592///     verify-test reference compiles without a glob `use`),
593///  3. the verbatim source `type_name` (entry-scope records, in
594///     scope via the entry module's own `use`).
595fn mir_record_rust_type(
596    type_id: Option<crate::ir::TypeId>,
597    type_name: &str,
598    ctx: &MirEmitCtx<'_>,
599) -> String {
600    if let Some(renamed) = builtin_dotted_record_rename(type_name) {
601        return renamed.to_string();
602    }
603    if let Some(qualified) = qualify_record_type(type_id, type_name, ctx) {
604        return qualified;
605    }
606    type_name.to_string()
607}
608
609/// How many fns the MIR walker can emit
610/// standalone vs how many need HIR fallback. Pre-wire-up signal
611/// so callers can track walker reach across the shipped corpus
612/// without altering the codegen path.
613#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
614pub struct CoverageReport {
615    /// Total fn count in the lowered program.
616    pub total: usize,
617    /// Fns whose entire body the walker emits standalone
618    /// (no `None` anywhere in the recursive walk).
619    pub mir_covered: usize,
620    /// Fns the walker can't emit — the recursive walk hit at
621    /// least one variant that returned `None`. Caller would
622    /// fall back to the HIR walker in a wire-up.
623    pub hir_fallback: usize,
624}
625
626impl CoverageReport {
627    /// Walker reach as a percentage of total fns. `0.0` when
628    /// the program is empty (no fns lowered).
629    pub fn ratio(&self) -> f64 {
630        if self.total == 0 {
631            0.0
632        } else {
633            self.mir_covered as f64 / self.total as f64
634        }
635    }
636}
637
638/// Walk every fn in `program` and report walker reach. For each
639/// fn, calls [`emit_mir_expr`] on the body and counts
640/// `Some` / `None`. Suitable for `--explain-mir-coverage`–style
641/// diagnostics; the codegen path itself is untouched.
642pub fn coverage_report(program: &MirProgram, emit_ctx: &MirEmitCtx<'_>) -> CoverageReport {
643    coverage_report_with_blockers(program, emit_ctx).0
644}
645
646/// Same reach measurement as [`coverage_report`], plus a histogram
647/// of the *first* construct that blocked each HIR-fallback fn.
648///
649/// For every fn the walker can't emit, `first_blocker` does the same
650/// recursive `emit_mir_expr`-shaped walk but, instead of building a
651/// string, returns a stable label for the first `MirExpr` variant /
652/// `MirCallee` kind that would have returned `None`. Counting those
653/// labels gives a per-wave roadmap: "lower `Match` next" reads
654/// straight off the dominant bucket. The returned map is keyed by
655/// label and ordered (BTreeMap) for deterministic report output.
656///
657/// This is diagnostic-only — it does not touch the production emit
658/// path, and the walk is the exact mirror of [`emit_mir_expr`] so the
659/// blocker it names is the one the wired-up backend would hit.
660pub fn coverage_report_with_blockers(
661    program: &MirProgram,
662    emit_ctx: &MirEmitCtx<'_>,
663) -> (
664    CoverageReport,
665    std::collections::BTreeMap<&'static str, usize>,
666) {
667    let mut report = CoverageReport::default();
668    let mut blockers: std::collections::BTreeMap<&'static str, usize> =
669        std::collections::BTreeMap::new();
670    for (_, mir_fn) in program.iter() {
671        report.total += 1;
672        if emit_mir_expr(&mir_fn.body, emit_ctx).is_some() {
673            report.mir_covered += 1;
674        } else {
675            report.hir_fallback += 1;
676            let label = first_blocker(&mir_fn.body, emit_ctx).unwrap_or("Unknown");
677            *blockers.entry(label).or_insert(0) += 1;
678        }
679    }
680    (report, blockers)
681}
682
683/// Recursively find the first construct that makes [`emit_mir_expr`]
684/// return `None` for `expr`, and name it with a stable label. Returns
685/// `None` only when the whole subtree emits cleanly (the caller treats
686/// that as "no blocker"). The traversal order matches
687/// `emit_mir_expr`'s argument-evaluation order exactly so the label
688/// pins the *same* node the emit walk would have bailed on.
689fn first_blocker(expr: &Spanned<MirExpr>, emit_ctx: &MirEmitCtx<'_>) -> Option<&'static str> {
690    // Leaf check: if this node emits cleanly on its own, no blocker
691    // lives at-or-below it.
692    if emit_mir_expr(expr, emit_ctx).is_some() {
693        return None;
694    }
695    // The node (or one of its children) blocks. Recurse into children
696    // first so we report the deepest / leftmost actual blocker, not the
697    // wrapper that merely propagated a child's `None`.
698    match &expr.node {
699        MirExpr::Neg(inner) | MirExpr::Return(inner) | MirExpr::Try(inner) => {
700            first_blocker(inner, emit_ctx).or(Some(label_for(&expr.node)))
701        }
702        MirExpr::BinOp(b) => first_blocker(&b.node.lhs, emit_ctx)
703            .or_else(|| first_blocker(&b.node.rhs, emit_ctx))
704            .or(Some("BinOp")),
705        MirExpr::Call(c) => {
706            // `Fn`, `Builtin`, `Intrinsic` and `LocalSlot` callees can all
707            // emit cleanly (Wave 3a graduated the pure builtins +
708            // intrinsics; W6/Stage-0 graduated the first-class `LocalSlot`
709            // fn-pointer call), so recurse into the args first and only
710            // report the callee kind when every arg emits but the call as a
711            // whole still returned `None` (an effectful / unresolved
712            // builtin, or a shape the walker can't render).
713            for a in &c.node.args {
714                if let Some(b) = first_blocker(a, emit_ctx) {
715                    return Some(b);
716                }
717            }
718            match &c.node.callee {
719                MirCallee::Builtin(_) => Some("Call(Builtin)"),
720                MirCallee::Intrinsic(_) => Some("Call(Intrinsic)"),
721                MirCallee::Fn(_) => Some("Call(Fn)"),
722                MirCallee::LocalSlot { .. } => Some("Call(LocalSlot)"),
723            }
724        }
725        MirExpr::TailCall(tc) => {
726            for a in &tc.node.args {
727                if let Some(b) = first_blocker(a, emit_ctx) {
728                    return Some(b);
729                }
730            }
731            Some("TailCall")
732        }
733        MirExpr::Tuple(items) | MirExpr::List(items) => {
734            for item in items {
735                if let Some(b) = first_blocker(item, emit_ctx) {
736                    return Some(b);
737                }
738            }
739            Some(label_for(&expr.node))
740        }
741        MirExpr::MapLiteral(entries) => {
742            for (k, v) in entries {
743                if let Some(b) = first_blocker(k, emit_ctx) {
744                    return Some(b);
745                }
746                if let Some(b) = first_blocker(v, emit_ctx) {
747                    return Some(b);
748                }
749            }
750            Some("MapLiteral")
751        }
752        MirExpr::Let(l) => first_blocker(&l.node.value, emit_ctx)
753            .or_else(|| first_blocker(&l.node.body, emit_ctx))
754            .or(Some("Let(synthetic)")),
755        MirExpr::Project(p) => first_blocker(&p.node.base, emit_ctx).or(Some("Project")),
756        MirExpr::RecordCreate(r) => {
757            for f in &r.node.fields {
758                if let Some(b) = first_blocker(&f.value, emit_ctx) {
759                    return Some(b);
760                }
761            }
762            Some("RecordCreate(builtin/Tcp)")
763        }
764        MirExpr::RecordUpdate(u) => {
765            if let Some(b) = first_blocker(&u.node.base, emit_ctx) {
766                return Some(b);
767            }
768            for f in &u.node.updates {
769                if let Some(b) = first_blocker(&f.value, emit_ctx) {
770                    return Some(b);
771                }
772            }
773            Some("RecordUpdate(builtin/Tcp)")
774        }
775        MirExpr::Construct(c) => {
776            for a in &c.node.args {
777                if let Some(b) = first_blocker(a, emit_ctx) {
778                    return Some(b);
779                }
780            }
781            Some("Construct")
782        }
783        MirExpr::IfThenElse(ite) => first_blocker(&ite.node.cond, emit_ctx)
784            .or_else(|| first_blocker(&ite.node.then_branch, emit_ctx))
785            .or_else(|| first_blocker(&ite.node.else_branch, emit_ctx))
786            .or(Some("IfThenElse")),
787        MirExpr::Match(m) => {
788            if let Some(b) = first_blocker(&m.node.subject, emit_ctx) {
789                return Some(b);
790            }
791            for arm in &m.node.arms {
792                if let Some(b) = first_blocker(&arm.body, emit_ctx) {
793                    return Some(b);
794                }
795            }
796            // Subject + every arm body emit cleanly, yet the Match as a
797            // whole returned `None` — the blocker is the match shape
798            // itself (an untranslatable pattern, or a dispatch shape the
799            // walker can't reproduce byte-identically yet).
800            Some("Match")
801        }
802        // Variants `emit_mir_expr` never recurses into (it returns
803        // `None` immediately): they are themselves the blocker.
804        other => Some(label_for(other)),
805    }
806}
807
808/// Stable histogram label for a `MirExpr` variant. Kept short and
809/// variant-named so the report reads as a worklist.
810fn label_for(expr: &MirExpr) -> &'static str {
811    match expr {
812        MirExpr::Literal(_) => "Literal",
813        MirExpr::Local(_) => "Local(synthetic)",
814        MirExpr::Let(_) => "Let(synthetic)",
815        MirExpr::Call(_) => "Call",
816        MirExpr::TailCall(_) => "TailCall",
817        MirExpr::BinOp(_) => "BinOp",
818        MirExpr::Neg(_) => "Neg",
819        MirExpr::Match(_) => "Match",
820        MirExpr::Construct(_) => "Construct",
821        MirExpr::RecordCreate(_) => "RecordCreate",
822        MirExpr::RecordUpdate(_) => "RecordUpdate",
823        MirExpr::Project(_) => "Project",
824        MirExpr::IfThenElse(_) => "IfThenElse",
825        MirExpr::Try(_) => "Try",
826        MirExpr::List(_) => "List",
827        MirExpr::Tuple(_) => "Tuple",
828        MirExpr::MapLiteral(_) => "MapLiteral",
829        MirExpr::InterpolatedStr(_) => "InterpolatedStr",
830        MirExpr::IndependentProduct(_) => "IndependentProduct",
831        MirExpr::Return(_) => "Return",
832        MirExpr::FnValue(_) => "FnValue",
833        MirExpr::Box(_) => "Box",
834        MirExpr::Unbox(_) => "Unbox",
835    }
836}
837
838/// Try to emit Rust source for `expr` directly from MIR.
839/// Returns `None` for any variant outside the renderable subset — the
840/// signal for the caller to emit a hard codegen diagnostic (the
841/// verify-only Oracle/trace residual).
842///
843/// The per-fn borrow policy (`local_types` / `rc_wrapped` /
844/// `borrowed_params`) is threaded through the [`MirEmitCtx`] so the
845/// clone / borrow / `Arc::new` decisions are correct for the fn whose
846/// body this renders. This is the sole Rust runtime codegen walker.
847pub(super) fn emit_mir_expr(expr: &Spanned<MirExpr>, emit_ctx: &MirEmitCtx<'_>) -> Option<String> {
848    match &expr.node {
849        MirExpr::Literal(lit) => {
850            // The MIR const-fold pass collapses `Neg(Literal(273.15))`
851            // → `Literal(-273.15)`. HIR never folds — it keeps the
852            // `Neg` node and emits `(-273.15f64)` (the `Neg` arm's
853            // `(-{inner})` wrapper). Re-introduce that wrapper for a
854            // negative numeric literal at expression position so the
855            // folded form matches HIR byte-for-byte. (Literal *patterns*
856            // don't reach here — they translate to `ResolvedPattern` and
857            // emit through the shared `emit_pattern` / dispatch path.)
858            match &lit.node {
859                // A negative folded Int literal emits the already-signed
860                // value directly: `emit_literal` produces
861                // `AverInt::from_i64(-N)` (a `const fn`), and `AverInt` has
862                // no std `Neg`, so the old `(-{lit})` re-wrap would not
863                // compile. `from_i64` accepts `i64::MIN` verbatim, so no
864                // `checked_neg` guard is needed.
865                crate::ast::Literal::Int(_) => Some(emit_literal(&lit.node)),
866                crate::ast::Literal::Float(f) if f.is_sign_negative() => Some(format!(
867                    "(-{})",
868                    emit_literal(&crate::ast::Literal::Float(-f))
869                )),
870                _ => Some(emit_literal(&lit.node)),
871            }
872        }
873        MirExpr::Local(spanned_local) => {
874            let name = &spanned_local.node.name;
875            if name.is_empty() {
876                // Synthetic locals (intermediate stmt-chain
877                // effectful expressions) carry no source name —
878                // the Rust backend can't emit them as idents.
879                // Caller falls back to HIR.
880                return None;
881            }
882            Some(aver_name_to_rust(name))
883        }
884        MirExpr::Neg(inner) => {
885            // `Int` negation is `AverInt::neg()` (non-wrapping; `-i64::MIN`
886            // promotes to `Big`). `Float` negation keeps the raw `-`. The
887            // const-fold pass usually collapses `Neg(Literal)` to a folded
888            // literal (handled in the `Literal` arm), so a real `Neg` here is
889            // over a non-literal operand carrying an `Int` stamp; an Int
890            // literal inner (no stamp) emits an `AverInt`, which also needs
891            // `.neg()` (there is no `-` on `AverInt`).
892            let inner_is_int = ty_is_int(inner.ty())
893                || matches!(
894                    &inner.node,
895                    MirExpr::Literal(l)
896                        if matches!(
897                            l.node,
898                            crate::ast::Literal::Int(_) | crate::ast::Literal::BigInt(_)
899                        )
900                );
901            let code = emit_mir_expr(inner, emit_ctx)?;
902            if inner_is_int {
903                Some(format!("{}.neg()", code))
904            } else {
905                Some(format!("(-{})", code))
906            }
907        }
908        MirExpr::BinOp(spanned_binop) => {
909            let bop = &spanned_binop.node;
910            let l = emit_mir_expr(&bop.lhs, emit_ctx)?;
911            let r = emit_mir_expr(&bop.rhs, emit_ctx)?;
912            // `Int` arithmetic lowers to the non-wrapping `AverInt` methods
913            // (`+ - *` → `.add/.sub/.mul(&rhs)`, `/` → `.div_trunc(&rhs)`),
914            // since `AverInt` has no operator-trait impls. Comparisons stay
915            // raw operators (`AverInt` derives `PartialEq`/`PartialOrd`).
916            // Detected from the operand stamps: arithmetic over `Int`
917            // operands carries an `Int` stamp on both sides.
918            let int_arith = matches!(bop.op, BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div)
919                && (ty_is_int(bop.lhs.ty()) || ty_is_int(bop.rhs.ty()));
920            if int_arith {
921                // Int unboxing: emit native integer arithmetic ONLY when the
922                // WHOLE result is proven bare `i64`. SOUNDNESS (BUG 2): the
923                // gate is `mir_expr_is_bare_i64(expr)` — the interval-checked
924                // predicate over the ENTIRE `Add`/`Sub`/`Mul` tree, NOT a
925                // per-operand check. `n + i64::MAX` has each operand bare but
926                // a result interval OUTSIDE `i64`, so it fails this gate and
927                // falls through to the boxed `AverInt` path below (which
928                // converts each bare operand via `from_i64`). A missing fact
929                // also keeps the boxed path. For `Add`/`Sub`/`Mul` we check
930                // the whole expression; `Div` is not modelled by the interval
931                // analysis (raw `/` over `Int` is a source type error — the
932                // builtin is `Int.div`), so it keeps the per-operand check
933                // plus the zero-divisor trap.
934                let whole_bare = match bop.op {
935                    BinOp::Add | BinOp::Sub | BinOp::Mul => mir_expr_is_bare_i64(expr, emit_ctx),
936                    BinOp::Div => {
937                        mir_expr_is_bare_i64(&bop.lhs, emit_ctx)
938                            && mir_expr_is_bare_i64(&bop.rhs, emit_ctx)
939                    }
940                    _ => false,
941                };
942                if whole_bare
943                    && let Some(lb) = emit_bare_i64(&bop.lhs)
944                    && let Some(rb) = emit_bare_i64(&bop.rhs)
945                {
946                    match bop.op {
947                        BinOp::Add => return Some(format!("({} + {})", lb, rb)),
948                        BinOp::Sub => return Some(format!("({} - {})", lb, rb)),
949                        BinOp::Mul => return Some(format!("({} * {})", lb, rb)),
950                        BinOp::Div => {
951                            // Truncating `/` over `i64`, keeping the
952                            // zero-divisor trap (the `AverInt::div_trunc`
953                            // parity). Bind the divisor once so it is
954                            // evaluated a single time.
955                            return Some(format!(
956                                "{{ let __d = {}; if __d == 0i64 {{ panic!(\"divide by zero\") }} else {{ ({}) / __d }} }}",
957                                rb, lb
958                            ));
959                        }
960                        _ => {}
961                    }
962                }
963                // Mixed-representation boundary: this is the boxed
964                // `AverInt` arithmetic path, but ONE operand may be a bare
965                // `i64` (e.g. `acc * n` where `acc` stays boxed and `n`
966                // went bare). ETAP-2 SLICE 1: the bare→`AverInt` boundary is
967                // now an EXPLICIT `Box(n)` node the rewrite inserted, so
968                // `emit_mir_expr` already produced the `from_i64(n)` text for
969                // `l` / `r` — no codegen-side coercion here.
970                let method = match bop.op {
971                    BinOp::Add => "add",
972                    BinOp::Sub => "sub",
973                    BinOp::Mul => "mul",
974                    // Raw `/` over `Int` is a type error in source (Int.div
975                    // is the builtin), so this is normally unreachable; if a
976                    // bare `Div` does reach here it takes the truncating
977                    // semantics of the `/` operator. `div_trunc` returns
978                    // `Option` (None only on zero divisor); a bare `/` over
979                    // ℤ otherwise can't fail, so `.expect` mirrors the
980                    // runtime trap a zero divisor would raise.
981                    BinOp::Div => {
982                        return Some(format!(
983                            "{}.div_trunc(&{}).expect(\"divide by zero\")",
984                            l, r
985                        ));
986                    }
987                    _ => unreachable!("int_arith only set for Add/Sub/Mul/Div"),
988                };
989                return Some(format!("{}.{}(&{})", l, method, r));
990            }
991            // Int unboxing: a comparison (`==`, `<`, `<=`, `>`, `>=`, `!=`)
992            // between two proven-bare `i64` operands emits a raw operator
993            // over `i64` (rather than `&AverInt == &AverInt`). `AverInt`
994            // derives `PartialEq`/`PartialOrd`, and so does `i64`, so the
995            // boxed path below also works — but on a bare subject the boxed
996            // path would compare a bare `i64` against an `AverInt`, a type
997            // error. So when both sides are bare we MUST take the raw path.
998            if matches!(
999                bop.op,
1000                BinOp::Eq | BinOp::Neq | BinOp::Lt | BinOp::Gt | BinOp::Lte | BinOp::Gte
1001            ) && mir_expr_is_bare_i64(&bop.lhs, emit_ctx)
1002                && mir_expr_is_bare_i64(&bop.rhs, emit_ctx)
1003                && let Some(lb) = emit_bare_i64(&bop.lhs)
1004                && let Some(rb) = emit_bare_i64(&bop.rhs)
1005            {
1006                let op = match bop.op {
1007                    BinOp::Eq => "==",
1008                    BinOp::Neq => "!=",
1009                    BinOp::Lt => "<",
1010                    BinOp::Gt => ">",
1011                    BinOp::Lte => "<=",
1012                    BinOp::Gte => ">=",
1013                    _ => unreachable!(),
1014                };
1015                return Some(format!("({} {} {})", lb, op, rb));
1016            }
1017            let op_str = match bop.op {
1018                BinOp::Add => "+",
1019                BinOp::Sub => "-",
1020                BinOp::Mul => "*",
1021                BinOp::Div => "/",
1022                BinOp::Eq => "==",
1023                BinOp::Neq => "!=",
1024                BinOp::Lt => "<",
1025                BinOp::Gt => ">",
1026                BinOp::Lte => "<=",
1027                BinOp::Gte => ">=",
1028            };
1029            // Read type stamps to disambiguate
1030            // numeric `+` from `AverStr` concat. Same shape HIR
1031            // walker takes via `ectx.expr_is_numeric`. HIR's
1032            // disambiguation is `expr_is_numeric(lhs) ||
1033            // expr_is_numeric(rhs)` → plain add; otherwise the
1034            // `AverStr` concat path, where the LHS is run through
1035            // `maybe_clone` (it's consumed by `Add`, the RHS is
1036            // borrowed via `&` for `Add<&AverStr>`). Mirror that
1037            // exactly so Str + Str matches byte-for-byte.
1038            //
1039            // GENUINE DIVERGENCE (Wave 4 boundary — left on HIR
1040            // fallback by design): the MIR walker reads the operand's
1041            // *type stamp* (correct for let-bound locals + match
1042            // bindings + user-fn-call returns), while HIR's
1043            // `expr_is_numeric` reads `ectx.local_types`, which only
1044            // carries *params*. So for `left + right` where `left` /
1045            // `right` are `Int`s bound by `let left = leftRes?` (not
1046            // params), HIR misclassifies them as non-numeric and emits
1047            // the concat-shaped `(left + &right)`; MIR correctly emits
1048            // `(left + right)`. Both COMPILE and produce identical
1049            // results (`i64: Add<&i64>` exists in std), so neither is
1050            // unsound — MIR is just cleaner. Matching HIR here would
1051            // mean deliberately ignoring MIR's correct stamps, so these
1052            // fns (`applyEvalOp`, `validateAndCombine[NoOp]`, `size`,
1053            // `sumDirect`, `countS`'s `&str` deref, …) stay on HIR
1054            // fallback. The eventual HIR retirement fixes HIR (give it
1055            // let-local types), not MIR.
1056            if matches!(bop.op, BinOp::Add)
1057                && !ty_is_numeric(bop.lhs.ty())
1058                && !ty_is_numeric(bop.rhs.ty())
1059            {
1060                let l = mir_maybe_clone(l, &bop.lhs.node, emit_ctx);
1061                Some(format!("({} + &{})", l, r))
1062            } else if matches!(bop.op, BinOp::Eq | BinOp::Neq) {
1063                // HIR derefs `AverStr` (Rc<str>) to `&str` when one
1064                // side is a string literal, since `Rc<str>` doesn't
1065                // impl `PartialEq<&str>`. Mirror that so string
1066                // equality matches.
1067                if let MirExpr::Literal(lit) = &bop.rhs.node
1068                    && let crate::ast::Literal::Str(s) = &lit.node
1069                {
1070                    return Some(format!("(&*{} {} {:?})", l, op_str, s));
1071                }
1072                if let MirExpr::Literal(lit) = &bop.lhs.node
1073                    && let crate::ast::Literal::Str(s) = &lit.node
1074                {
1075                    return Some(format!("({:?} {} &*{})", s, op_str, r));
1076                }
1077                Some(format!("({} {} {})", l, op_str, r))
1078            } else {
1079                Some(format!("({} {} {})", l, op_str, r))
1080            }
1081        }
1082        MirExpr::Call(spanned_call) => {
1083            let call = &spanned_call.node;
1084            match &call.callee {
1085                MirCallee::Fn(fn_id) => {
1086                    // Resolve canonical name through the same
1087                    // symbol table the HIR walker uses, then emit
1088                    // the call exactly as HIR's
1089                    // `emit_named_function_call` does: each arg goes
1090                    // through `borrow_arg` (when the callee's i-th
1091                    // param is borrowed-by-default `&T`) or
1092                    // `clone_arg` (owned), and the module-qualified
1093                    // head is path-mangled via `resolve_module_call`.
1094                    let name = emit_ctx.symbol_table.fn_entry(*fn_id).key.canonical();
1095                    emit_named_call_to(&name, Some(*fn_id), &call.args, emit_ctx)
1096                }
1097                // Resolve the interned dotted name and dispatch:
1098                //   - EFFECTFUL builtins (Wave 3b) →
1099                //     `emit_mir_effectful_builtin_call`, which mirrors
1100                //     HIR's `emit_builtin_call` replay-reroute / policy-
1101                //     wrap / bare-frame machinery byte-for-byte.
1102                //   - PURE builtins (Wave 3a) → `emit_mir_builtin_call`.
1103                // An out-of-range id (a lowering-invariant violation we
1104                // tolerate defensively) returns `None` → HIR fallback.
1105                MirCallee::Builtin(id) => {
1106                    let name = emit_ctx.mir_builtins.get(id.0 as usize)?.as_str();
1107                    if super::builtins::builtin_is_effectful(name) {
1108                        emit_mir_effectful_builtin_call(name, &call.args, emit_ctx)
1109                    } else {
1110                        emit_mir_builtin_call(name, &call.args, emit_ctx)
1111                    }
1112                }
1113                // Wave 3a: the 5 deforestation intrinsics (buffer build
1114                // + `__to_str`). Args are by-value (no clone / borrow),
1115                // mirroring `emit_builtin_call_inner`'s intrinsic arms.
1116                // The Rust backend deforests differently, so a buffered
1117                // fn's MIR shape may not byte-match HIR — the parity
1118                // gate then falls back safely.
1119                MirCallee::Intrinsic(intrinsic) => {
1120                    emit_mir_intrinsic_call(*intrinsic, &call.args, emit_ctx)
1121                }
1122                // First-class fn value held in a slot — calling a `Fn(..)`
1123                // param. Post-#379 the slot holds a plain fn-pointer (no
1124                // closures / `dyn Fn` — `Type::Fn` is param-only), so this
1125                // emits the direct call-by-name `name(args…)`. Mirror of
1126                // HIR's `CallPlan::Dynamic` (`emit_fn_call_with_options`):
1127                // the head is `aver_name_to_rust(name)` and every arg goes
1128                // through `clone_arg`.
1129                MirCallee::LocalSlot { name, .. } => {
1130                    let func = aver_name_to_rust(name);
1131                    let mut arg_strs = Vec::with_capacity(call.args.len());
1132                    for a in &call.args {
1133                        arg_strs.push(mir_clone_arg(
1134                            emit_mir_expr(a, emit_ctx)?,
1135                            &a.node,
1136                            emit_ctx,
1137                        ));
1138                    }
1139                    Some(format!("{}({})", func, arg_strs.join(", ")))
1140                }
1141            }
1142        }
1143        MirExpr::Return(inner) => Some(format!("return {}", emit_mir_expr(inner, emit_ctx)?)),
1144        MirExpr::TailCall(spanned_tc) => {
1145            // Tail call outside a self-TCO loop
1146            // emits as a regular function call — mirror of HIR's
1147            // `ResolvedExpr::TailCall` outside-loop branch
1148            // (which the resolver leaves intact and the emitter
1149            // routes through `emit_named_function_call`). When
1150            // the surrounding fn IS in a TCO loop, HIR rewrites
1151            // it to `continue` + param assigns — the walker
1152            // can't see that without `ectx`, so the wire-up
1153            // layer's parity check is the safety net (mismatch
1154            // → fall back to HIR).
1155            let tc = &spanned_tc.node;
1156            let name = emit_ctx.symbol_table.fn_entry(tc.target).key.canonical();
1157            emit_named_call(&name, &tc.args, emit_ctx)
1158        }
1159        MirExpr::Try(inner) => {
1160            // `value?` propagation. `?` (the `Try` trait) is implemented
1161            // for an owned `Result<T, E>`, not a borrowed `&Result`. When
1162            // the inner is a borrowed-by-default `Result`-typed param
1163            // (`fn foldNote(acc: &Result<…>, …)` then `acc?`), append `?`
1164            // to a *cloned* owned value rather than the `&Result` read —
1165            // otherwise rustc rejects `&Result` as not implementing
1166            // `Try`. `mir_clone_arg` produces the right owning shape
1167            // (`.clone()` for a borrowed param, `(*x).clone()` for an
1168            // rc-wrapped pass-through), and leaves an owned last-use local
1169            // a bare move — exactly what `?` consumes. Mirror of HIR's
1170            // `ErrorProp` once the inner is in an owning position.
1171            let inner_code = emit_mir_expr(inner, emit_ctx)?;
1172            let owned = mir_clone_arg(inner_code, &inner.node, emit_ctx);
1173            Some(format!("{}?", owned))
1174        }
1175        MirExpr::Tuple(items) => {
1176            // `(a, b, c)` tuple literal. Mirror
1177            // of HIR's `ResolvedExpr::Tuple` emit — each element
1178            // routed through `clone_arg` for ownership.
1179            let mut parts = Vec::with_capacity(items.len());
1180            for item in items {
1181                parts.push(mir_clone_arg(
1182                    emit_mir_expr(item, emit_ctx)?,
1183                    &item.node,
1184                    emit_ctx,
1185                ));
1186            }
1187            Some(format!("({})", parts.join(", ")))
1188        }
1189        MirExpr::List(items) => {
1190            // `[a, b, c]` list literal. Mirror
1191            // of HIR's `ResolvedExpr::List` — empty case folds
1192            // to `aver_rt::AverList::empty()`, non-empty to
1193            // `from_vec(vec![...])` with `clone_arg` elements.
1194            if items.is_empty() {
1195                return Some("aver_rt::AverList::empty()".to_string());
1196            }
1197            let mut parts = Vec::with_capacity(items.len());
1198            for item in items {
1199                parts.push(mir_clone_arg(
1200                    emit_mir_expr(item, emit_ctx)?,
1201                    &item.node,
1202                    emit_ctx,
1203                ));
1204            }
1205            Some(format!(
1206                "aver_rt::AverList::from_vec(vec![{}])",
1207                parts.join(", ")
1208            ))
1209        }
1210        MirExpr::MapLiteral(entries) => {
1211            // `{"k" => v, …}` map literal.
1212            // Mirror of HIR's `ResolvedExpr::MapLiteral` — empty
1213            // → `HashMap::new()`, non-empty →
1214            // `vec![(k, v), …].into_iter().collect::<HashMap<_, _>>()`,
1215            // keys + values routed through `clone_arg`.
1216            if entries.is_empty() {
1217                return Some("HashMap::new()".to_string());
1218            }
1219            let mut parts = Vec::with_capacity(entries.len());
1220            for (k, v) in entries {
1221                let key_str = mir_clone_arg(emit_mir_expr(k, emit_ctx)?, &k.node, emit_ctx);
1222                let val_str = mir_clone_arg(emit_mir_expr(v, emit_ctx)?, &v.node, emit_ctx);
1223                parts.push(format!("({}, {})", key_str, val_str));
1224            }
1225            Some(format!(
1226                "vec![{}].into_iter().collect::<HashMap<_, _>>()",
1227                parts.join(", ")
1228            ))
1229        }
1230        MirExpr::Let(spanned_let) => {
1231            // `let binding = value; body` →
1232            // Rust block-expression `{ let x = value; body }`.
1233            // A discarded intermediate (an effectful `Stmt::Expr` at
1234            // non-tail position, or a `_ = effect()` discard) carries
1235            // `binding_name.is_empty()` — there's no source ident to
1236            // bind, so the value is emitted as a bare statement
1237            // (`{ value; body }`), evaluated for its effect with the
1238            // result dropped. Mirror of HIR's discarded-`Stmt::Expr`
1239            // shape.
1240            let let_node = &spanned_let.node;
1241            // ETAP-2 SLICE 1: the binding-crossing representation BOUNDARY
1242            // (defect esc_match — a raw value bound into a boxed slot) is now
1243            // an EXPLICIT `Box`/`Unbox` node the `bare_i64_rewrite` pass
1244            // wrapped the value in. Codegen lowers it via the `Box`/`Unbox`
1245            // arms — no `from_i64` decision here. It still RENDERS a bare
1246            // binding's value raw (representation): a `i64` binding takes a
1247            // native `i64` literal / leaf / arithmetic.
1248            let value = if emit_ctx.bare.is_bare(let_node.binding) {
1249                emit_bare_i64(&let_node.value)
1250                    .or_else(|| emit_mir_expr(&let_node.value, emit_ctx))?
1251            } else {
1252                emit_mir_expr(&let_node.value, emit_ctx)?
1253            };
1254            let body = emit_mir_expr(&let_node.body, emit_ctx)?;
1255            if let_node.binding_name.is_empty() {
1256                Some(format!("{{ {}; {} }}", value, body))
1257            } else {
1258                let name = aver_name_to_rust(&let_node.binding_name);
1259                Some(format!("{{ let {} = {}; {} }}", name, value, body))
1260            }
1261        }
1262        MirExpr::Project(spanned_proj) => {
1263            // `base.field` projection. Mirror of
1264            // HIR's `ResolvedLeafOp::FieldAccess` emit shape —
1265            // emit_expr(base) + "." + aver_name_to_rust(field).
1266            // No clone insertion here; the HIR walker handles
1267            // that via `maybe_clone` at outer call sites.
1268            let proj = &spanned_proj.node;
1269            // A cross-module first-class fn reference used as a value
1270            // (`HttpServer.listen(port, Apps.Notepad.Routes.handleRequest)`)
1271            // lowers to a `Project` chain over a `FnValue` head
1272            // (`Project(Project(FnValue("Apps"), "Notepad"), "Routes"), "handleRequest")`)
1273            // because the resolver leaves the leading segment an `Ident`
1274            // and the rest dotted `Attr`. Collapse such a chain back to
1275            // the canonical dotted name and, when it names a registered
1276            // module-qualified symbol, emit the path-mangled static ref
1277            // (`crate::aver_generated::apps::notepad::routes::handleRequest`)
1278            // exactly as the `MirCallee::Fn` call path does — instead of
1279            // the verbatim `Apps.Notepad.Routes.handleRequest` field
1280            // access, which is not a valid Rust path. The HIR walker saw
1281            // this as a single `StaticRef(full_name)`; on MIR the chain is
1282            // re-flattened here.
1283            if let Some(dotted) = collapse_fnvalue_projection(&expr.node)
1284                && resolve_module_call(&dotted, emit_ctx.module_prefixes).is_some()
1285            {
1286                return Some(emit_mir_static_ref(&dotted, emit_ctx));
1287            }
1288            let base = emit_mir_expr(&proj.base, emit_ctx)?;
1289            Some(format!("{}.{}", base, aver_name_to_rust(&proj.field)))
1290        }
1291        MirExpr::RecordCreate(spanned_rec) => {
1292            // `T { field = v, … }` record literal.
1293            // Mirror of HIR's `ResolvedExpr::RecordCreate` emit
1294            // shape exactly — HIR reads the source-level
1295            // `type_name` string (verbatim on `MirRecordCreate`)
1296            // and only special-cases `Tcp.Connection` → the
1297            // re-exported `Tcp_Connection` struct. Fields route
1298            // through `clone_arg`.
1299            let rec = &spanned_rec.node;
1300            let rust_type = mir_record_rust_type(rec.type_id, &rec.type_name, emit_ctx);
1301            let mut parts = Vec::with_capacity(rec.fields.len());
1302            for f in &rec.fields {
1303                let val =
1304                    mir_clone_arg(emit_mir_expr(&f.value, emit_ctx)?, &f.value.node, emit_ctx);
1305                parts.push(format!("{}: {}", aver_name_to_rust(&f.name), val));
1306            }
1307            Some(format!("{} {{ {} }}", rust_type, parts.join(", ")))
1308        }
1309        MirExpr::RecordUpdate(spanned_upd) => {
1310            // `T.update(base, field = v, …)` →
1311            // `{type_name} { field: value, …, ..base }`. Same
1312            // verbatim-type-name + Tcp.Connection rename as
1313            // RecordCreate; base + updates route through
1314            // `clone_arg`.
1315            let upd = &spanned_upd.node;
1316            let rust_type = mir_record_rust_type(upd.type_id, &upd.type_name, emit_ctx);
1317            let base = mir_clone_arg(
1318                emit_mir_expr(&upd.base, emit_ctx)?,
1319                &upd.base.node,
1320                emit_ctx,
1321            );
1322            let mut parts = Vec::with_capacity(upd.updates.len());
1323            for f in &upd.updates {
1324                let val =
1325                    mir_clone_arg(emit_mir_expr(&f.value, emit_ctx)?, &f.value.node, emit_ctx);
1326                parts.push(format!("{}: {}", aver_name_to_rust(&f.name), val));
1327            }
1328            Some(format!(
1329                "{} {{ {}, ..{} }}",
1330                rust_type,
1331                parts.join(", "),
1332                base
1333            ))
1334        }
1335        MirExpr::Construct(spanned_ctor) => {
1336            // Built-in ctors emit Result / Option wrappers; user
1337            // ctors resolve through the symbol table for
1338            // module-qualified path mangling. Both mirror HIR's
1339            // `clone_arg` on every arg; the User-ctor path also
1340            // wraps recursive (self-typed) fields in
1341            // `std::sync::Arc::new(...)` via
1342            // `constructor_boxed_positions` so recursive types
1343            // (`Tree.Node(left: Tree, …)`) emit byte-identical to
1344            // HIR's `emit_type_constructor_call`.
1345            let con = &spanned_ctor.node;
1346            match con.ctor {
1347                MirCtor::Builtin(builtin) => {
1348                    let (name, takes_arg) = match builtin {
1349                        BuiltinCtor::ResultOk => ("Ok", true),
1350                        BuiltinCtor::ResultErr => ("Err", true),
1351                        BuiltinCtor::OptionSome => ("Some", true),
1352                        BuiltinCtor::OptionNone => ("None", false),
1353                    };
1354                    if !takes_arg {
1355                        // `Option.None` — no args, no parens.
1356                        return Some(name.to_string());
1357                    }
1358                    let mut args = Vec::with_capacity(con.args.len());
1359                    for a in &con.args {
1360                        args.push(mir_clone_arg(
1361                            emit_mir_expr(a, emit_ctx)?,
1362                            &a.node,
1363                            emit_ctx,
1364                        ));
1365                    }
1366                    Some(format!("{}({})", name, args.join(", ")))
1367                }
1368                MirCtor::User(ctor_id) => {
1369                    // Resolve `CtorId` → owning type → variant name
1370                    // via the symbol table, then route the
1371                    // qualified type name through
1372                    // `resolve_module_call` for module-path
1373                    // mangling. Mirror of HIR's
1374                    // `emit_type_constructor_call`, including the
1375                    // boxed-position `Arc::new` on recursive fields
1376                    // (queried via `constructor_boxed_positions`,
1377                    // keyed by the `Type.Variant` name).
1378                    let ctor_entry = emit_ctx.symbol_table.ctor_entry(ctor_id);
1379                    let variant_name = ctor_entry.name.clone();
1380                    let type_entry = emit_ctx.symbol_table.type_entry(ctor_entry.owning_type);
1381                    let qualified = type_entry.key.canonical();
1382                    let boxed_positions = match emit_ctx.codegen {
1383                        Some(cg) => {
1384                            let ctor_name = format!("{}.{}", qualified, variant_name);
1385                            constructor_boxed_positions(&ctor_name, cg)
1386                        }
1387                        // Coverage path: no ctx → no boxed-position
1388                        // info. The parity gate isn't active here
1389                        // (coverage only reads Some/None), so an
1390                        // empty set is fine.
1391                        None => HashSet::new(),
1392                    };
1393                    let mut args = Vec::with_capacity(con.args.len());
1394                    for (idx, a) in con.args.iter().enumerate() {
1395                        let arg = mir_clone_arg(emit_mir_expr(a, emit_ctx)?, &a.node, emit_ctx);
1396                        if boxed_positions.contains(&idx) {
1397                            args.push(format!("std::sync::Arc::new({})", arg));
1398                        } else {
1399                            args.push(arg);
1400                        }
1401                    }
1402                    let args_str = args.join(", ");
1403                    // HIR emits a nullary variant as a unit variant
1404                    // (`E::Point`, no parens). Mirror that so
1405                    // zero-arg ctors match.
1406                    let head = if let Some((prefix, suffix)) =
1407                        resolve_module_call(&qualified, emit_ctx.module_prefixes)
1408                    {
1409                        format!("{}::{}", module_prefix_to_rust_path(prefix), suffix)
1410                    } else {
1411                        qualified
1412                    };
1413                    if con.args.is_empty() {
1414                        Some(format!("{}::{}", head, variant_name))
1415                    } else {
1416                        Some(format!("{}::{}({})", head, variant_name, args_str))
1417                    }
1418                }
1419            }
1420        }
1421        MirExpr::IfThenElse(spanned_ite) => emit_mir_if_then_else(&spanned_ite.node, emit_ctx),
1422        MirExpr::Match(spanned_match) => emit_mir_match(&spanned_match.node, emit_ctx),
1423        MirExpr::IndependentProduct(spanned_ip) => {
1424            emit_mir_independent_product(&spanned_ip.node, emit_ctx)
1425        }
1426        // A fn referenced as a *value* (`callWith(dbl)` passes `dbl`).
1427        // Post-#379, a fn value only ever enters through a `Fn(..)` param,
1428        // so the name is always a plain fn name — but mirror HIR's
1429        // `ResolvedLeafOp::StaticRef` in full (incl. the variant-vs-fn
1430        // refinement + module-path mangling) so the emit is byte-identical.
1431        // The VM does the same (`compile_ident` → `symbol_ref`).
1432        MirExpr::FnValue(name) => Some(emit_mir_static_ref(name, emit_ctx)),
1433        // ETAP-2 representation boundaries (inserted by the
1434        // `bare_i64::rewrite_for_rust` MIR->MIR pass). Codegen no longer
1435        // DECIDES where a boundary goes — the rewrite already inserted it —
1436        // it just lowers the node:
1437        //   Box(x)   — x evaluates to a raw `i64`; box it into `AverInt`.
1438        //   Unbox(x) — x evaluates to an `AverInt`; narrow to raw `i64`
1439        //              via the checked `to_i64()` (the analysis proved the
1440        //              value fits, so the `expect` never fires).
1441        MirExpr::Box(inner) => {
1442            let raw = emit_mir_expr(inner, emit_ctx)?;
1443            Some(format!("aver_rt::AverInt::from_i64({})", raw))
1444        }
1445        MirExpr::Unbox(inner) => {
1446            let boxed = emit_mir_expr(inner, emit_ctx)?;
1447            Some(format!(
1448                "{}.to_i64().expect(\"Int out of i64 range\")",
1449                boxed
1450            ))
1451        }
1452        _ => None,
1453    }
1454}
1455
1456/// Reconstruct the dotted source name of a `Project` chain whose head
1457/// is a `FnValue` — e.g. `Project(Project(FnValue("Apps"), "Notepad"),
1458/// "Routes")` → `"Apps.Notepad.Routes"`. Returns `None` for any chain
1459/// whose head is not a `FnValue` (a genuine record-field access). Used
1460/// to recover a cross-module first-class fn reference that the resolver
1461/// split into an `Ident` head plus dotted `Attr` tail.
1462fn collapse_fnvalue_projection(expr: &MirExpr) -> Option<String> {
1463    match expr {
1464        MirExpr::FnValue(name) => Some(name.clone()),
1465        MirExpr::Project(p) => {
1466            let base = collapse_fnvalue_projection(&p.node.base.node)?;
1467            Some(format!("{}.{}", base, p.node.field))
1468        }
1469        _ => None,
1470    }
1471}
1472
1473/// Mirror of HIR's `ResolvedLeafOp::StaticRef` emit
1474/// (`src/codegen/rust/expr.rs`): a fn / variant referenced as a value.
1475/// Refines a dotted name that resolves to a known user-defined variant to
1476/// the Rust enum-variant form (`Shape::Point`); otherwise emits the
1477/// module-mangled fn reference (`Fibonacci::fib`) or the bare
1478/// `aver_name_to_rust(name)`. `Option.None` / `None` collapse to `None`.
1479///
1480/// `is_user_type` needs the full `CodegenContext`; on the coverage /
1481/// test path (`codegen` is `None`) the variant refinement is skipped —
1482/// the parity gate isn't active there, so the conservative fn-reference
1483/// shape is fine (coverage only inspects `Some` vs `None`).
1484fn emit_mir_static_ref(name: &str, ctx: &MirEmitCtx<'_>) -> String {
1485    if name == "Option.None" || name == "None" {
1486        return "None".to_string();
1487    }
1488    // `BranchPath.Root` is the canonical-root nullary value (Oracle
1489    // structural addressing). It lowers to a `FnValue` rather than a
1490    // call, so it surfaces here — emit the `aver_rt` root constructor.
1491    if name == "BranchPath.Root" {
1492        return "aver_rt::BranchPath::root()".to_string();
1493    }
1494    if let Some((type_name, variant_name)) = name.rsplit_once('.')
1495        && let Some(cg) = ctx.codegen
1496    {
1497        let is_user = |n: &str| crate::codegen::common::is_user_type(n, cg);
1498        if is_user(type_name) {
1499            return if let Some((prefix, _)) = resolve_module_call(name, ctx.module_prefixes) {
1500                let module_path = module_prefix_to_rust_path(prefix);
1501                let bare_type = type_name
1502                    .rsplit_once('.')
1503                    .map(|(_, t)| t)
1504                    .unwrap_or(type_name);
1505                format!("{}::{}::{}", module_path, bare_type, variant_name)
1506            } else {
1507                format!("{}::{}", type_name, variant_name)
1508            };
1509        }
1510        if let Some((_, bare_type)) = type_name.rsplit_once('.')
1511            && is_user(bare_type)
1512        {
1513            return if let Some((prefix, _)) = resolve_module_call(name, ctx.module_prefixes) {
1514                let module_path = module_prefix_to_rust_path(prefix);
1515                format!("{}::{}::{}", module_path, bare_type, variant_name)
1516            } else {
1517                format!("{}::{}", bare_type, variant_name)
1518            };
1519        }
1520    }
1521    if let Some((prefix, bare)) = resolve_module_call(name, ctx.module_prefixes) {
1522        let module_path = module_prefix_to_rust_path(prefix);
1523        format!("{}::{}", module_path, aver_name_to_rust(bare))
1524    } else {
1525        aver_name_to_rust(name)
1526    }
1527}
1528
1529/// Render one free-standing `verify`-case expression through the MIR
1530/// walker. `resolved` is the already-lifted `ResolvedExpr` (the caller
1531/// does the on-demand `ctx.resolve_expr`). Lowers it via
1532/// `lower_top_level_value` against a clone of the entry `MirProgram` (the
1533/// same isolation the VM uses for top-level statements: builtin /
1534/// instantiation table growth stays local to the clone), then emits it
1535/// with a **program-level** [`MirEmitCtx`] (no params / locals — verify
1536/// exprs have no fn anchor).
1537///
1538/// Returns `None` when the expr is outside the lowerable subset OR the
1539/// walker can't render it — the per-expr signal for the caller to emit a
1540/// hard codegen diagnostic (the verify-only Oracle/trace residual that
1541/// never built on the Rust backend). The `#[test]` / `assert_eq!` /
1542/// Result-`?` scaffolding is unaffected; only the expression string
1543/// changes.
1544pub(super) fn emit_mir_verify_expr(
1545    resolved: &Spanned<crate::ir::hir::ResolvedExpr>,
1546    ctx: &CodegenContext,
1547) -> Option<String> {
1548    let base = ctx.mir_program.as_ref()?;
1549    // Clone so the lowerer's builtin / instantiation table growth stays
1550    // local to this expression (mirrors the VM top-level path #338).
1551    let mut prog = base.clone();
1552    let lowered = crate::ir::mir::lower_top_level_value(resolved, &mut prog).ok()?;
1553    let policy = MirFnEmitPolicy::empty();
1554    // Lend the grown clone's builtin table (it backs `Call(Builtin(id))`
1555    // resolution and may carry a builtin the lowering just interned) plus
1556    // the full `ctx` for the borrow / ctor helpers.
1557    let emit_ctx = MirEmitCtx::program_level(ctx, &policy, &prog.builtins);
1558    emit_mir_expr(&lowered, &emit_ctx)
1559}
1560
1561/// Render the **`main` fn body** through the MIR walker. `main` is the
1562/// one entry-point that DOES carry a `ResolvedFnDef` (reachable via
1563/// `fn_id_for_decl` → `resolved_program.fn_by_id` → `mir_program.fn_by_id`),
1564/// so — unlike the free-standing verify / top-stmt exprs — its body has a
1565/// real fn anchor: we build the borrow policy from the resolved main
1566/// (`from_resolved`, borrow-by-default, the non-TCO shape) and emit via
1567/// the same `for_fn` ctx + `emit_mir_fn_body` every other fn uses.
1568///
1569/// `fn_id` is the resolved-main FnId the caller already computed
1570/// (`entry_module_sections` runs `fn_id_for_decl` for every fn). Returns
1571/// `None` when there's no MIR program, the main FnId has no lowered
1572/// `MirFn`, or the walker can't render the body — the signal for the
1573/// caller to emit a hard codegen diagnostic. The `fn main()` /
1574/// `-> Result<…>` signature and the guest/replay wrappers are unaffected;
1575/// only the body string moves onto MIR.
1576pub(super) fn emit_mir_main_body(fn_id: crate::ir::FnId, ctx: &CodegenContext) -> Option<String> {
1577    let mir_fn = ctx.mir_program.as_ref()?.fn_by_id(fn_id)?;
1578    let resolved = ctx.resolved_program.fn_by_id(fn_id)?;
1579    // Main lives in the entry module → no module scope. Borrow-by-default
1580    // matches the non-TCO shape the main body uses.
1581    let mut policy =
1582        MirFnEmitPolicy::from_resolved(resolved, None, /* borrow_by_default */ true);
1583    policy.apply_bare_i64(fn_id, ctx);
1584    let emit_ctx = MirEmitCtx::for_fn(ctx, &policy);
1585    emit_mir_fn_body(&mir_fn.body, &emit_ctx)
1586}
1587
1588/// Render a **guest-entry fn's inner body** through the MIR walker. The
1589/// guest-entry fn (the self-host's `runGuestCliProgram`) has its body
1590/// wrapped in the `aver_replay::with_guest_scope[_args][_result]` (replay
1591/// scope) and `crate::self_host_support::with_program_fn_store` (self-host
1592/// state) templates — pure string wrappers the caller keeps unchanged —
1593/// while the INNER body string is rendered here.
1594///
1595/// Unlike `main`, the caller already holds the `&ResolvedFnDef` (and its
1596/// `fn_id`), so this takes the resolved fn directly rather than looking it
1597/// up by `FnId`. The borrow policy is rebuilt with
1598/// `build_fn_ectx_from_resolved`'s rules (borrow-by-default, the non-TCO
1599/// shape; guest-entry returns before the `has_tco` branch). `scope` is the
1600/// owning module prefix (`None` for the entry-module guest-entry).
1601///
1602/// Returns `None` when there's no MIR program, the guest-entry FnId has
1603/// no lowered `MirFn`, or the walker can't render the body — the signal
1604/// for the caller to emit a hard codegen diagnostic. Only the body
1605/// string moves onto MIR; the replay / self-host-state wrappers stay
1606/// template text.
1607pub(super) fn emit_mir_guest_entry_body(
1608    resolved_fd: &crate::ir::hir::ResolvedFnDef,
1609    scope: Option<&str>,
1610    ctx: &CodegenContext,
1611) -> Option<String> {
1612    let mir_fn = ctx.mir_program.as_ref()?.fn_by_id(resolved_fd.fn_id)?;
1613    let mut policy =
1614        MirFnEmitPolicy::from_resolved(resolved_fd, scope, /* borrow_by_default */ true);
1615    policy.apply_bare_i64(resolved_fd.fn_id, ctx);
1616    let emit_ctx = MirEmitCtx::for_fn(ctx, &policy);
1617    emit_mir_fn_body(&mir_fn.body, &emit_ctx)
1618}
1619
1620/// Render every **top-level statement value** through the MIR walker,
1621/// all-or-nothing. Free-standing module-scope statements (`x = expr` / a
1622/// bare `expr`) belong to no `ResolvedFnDef`, so this mirrors the VM
1623/// top-level path (#338): clone the entry `MirProgram` ONCE (so the
1624/// lowerer's builtin / instantiation table growth stays consistent
1625/// across all the statements that share it), lower each statement's
1626/// already-resolved value via `lower_top_level_value`, and **pre-check**
1627/// that every value both lowers AND the walker renders it — deciding
1628/// before emitting anything so a mid-walk reject never leaves a
1629/// half-written main body (exactly what the VM `compile_top_level` does
1630/// with `mir_expr_compilable`).
1631///
1632/// Returns the rendered value strings in statement order on full success
1633/// (the caller wraps each in the `let {name} = …;` / bare-expr-discard
1634/// `…;` templating), or `None` if there's no MIR program or ANY
1635/// statement falls outside the lowerable / renderable subset — the
1636/// signal for the caller to emit a hard codegen diagnostic for the block
1637/// (the verify-only Oracle/trace residual).
1638pub(super) fn emit_mir_top_stmt_values(
1639    resolved_values: &[&Spanned<crate::ir::hir::ResolvedExpr>],
1640    ctx: &CodegenContext,
1641) -> Option<Vec<String>> {
1642    let base = ctx.mir_program.as_ref()?;
1643    // One clone shared across every statement: the lowerer grows its
1644    // builtin / instantiation tables in place, so all the `Call(Builtin)`
1645    // ids the walker resolves key off the same grown table (mirrors the
1646    // VM lowering one `prog` for the whole `__top_level__` chunk).
1647    let mut prog = base.clone();
1648    let lowered: Vec<Spanned<MirExpr>> = resolved_values
1649        .iter()
1650        .map(|value| crate::ir::mir::lower_top_level_value(value, &mut prog).ok())
1651        .collect::<Option<_>>()?;
1652    let policy = MirFnEmitPolicy::empty();
1653    let emit_ctx = MirEmitCtx::program_level(ctx, &policy, &prog.builtins);
1654    // All-or-nothing: render every value before returning any, so a
1655    // single un-renderable statement falls the WHOLE block back to HIR
1656    // rather than leaving a half-MIR / half-HIR main body.
1657    lowered
1658        .iter()
1659        .map(|low| emit_mir_expr(low, &emit_ctx))
1660        .collect::<Option<Vec<_>>>()
1661}
1662
1663/// Emit `MirExpr::IndependentProduct` (`(a, b, c)!` / `(a, b, c)?!`)
1664/// byte-identical to HIR's `ResolvedExpr::IndependentProduct` arm
1665/// (`super::expr`). The Rust backend is the one target that truly
1666/// PARALLELIZES the product (the VM and wasm-gc lower it sequentially):
1667/// each element runs on its own `std::thread::scope` thread.
1668///
1669/// Mirror notes (the three behaviors this arm must preserve to stay
1670/// byte-equal under the parity gate):
1671///
1672/// 1. **`?!` (`unwrap_results == true`).** A shared `__cancel_flag`
1673///    (`Arc<AtomicBool>`) is threaded into every branch via
1674///    `run_cancelable_branch`; a branch that produces `Err` sets the
1675///    flag so siblings can short-circuit (the *cancel* independence
1676///    mode — `complete` ignores the flag, but the emitted shape is the
1677///    same; the runtime decides). Joined branches are folded by
1678///    `emit_parallel_result_tuple_unwrap` (which unwraps the
1679///    `ParallelBranch::Completed` wrapper, then propagates the first
1680///    `Err` with `?`).
1681/// 2. **`!` (`unwrap_results == false`).** Same `thread::scope`/`spawn`,
1682///    but no cancel flag and no unwrap — joined branch values fold
1683///    straight into a tuple via `emit_tuple_from_vars` (a bare product
1684///    of `Result`s, preserved positionally).
1685/// 3. **Replay sequential fallback.** When `emit_replay_runtime` is on,
1686///    the parallel body is wrapped in
1687///    `if is_effect_tracking_active() { <sequential replay groups> }
1688///    else { <parallel> }`. The sequential arm uses
1689///    `enter_effect_group` / `set_effect_branch(i)` / `exit_effect_group`
1690///    so per-branch effects record/replay deterministically on one
1691///    thread; the parallel arm additionally captures + re-installs the
1692///    parallel scope context per spawned branch.
1693///
1694/// Each element is rendered through `mir_clone_arg` (the byte-identical
1695/// mirror of HIR's `clone_arg`). The `run_cancelable_branch` /
1696/// `ParallelBranch` / parallel-scope runtime is emitted UNCONDITIONALLY
1697/// by `super::runtime`, so no new runtime is needed.
1698fn emit_mir_independent_product(
1699    ip: &crate::ir::mir::MirIndependentProduct,
1700    emit_ctx: &MirEmitCtx<'_>,
1701) -> Option<String> {
1702    let mut parts: Vec<String> = Vec::with_capacity(ip.items.len());
1703    for it in &ip.items {
1704        parts.push(mir_clone_arg(
1705            emit_mir_expr(it, emit_ctx)?,
1706            &it.node,
1707            emit_ctx,
1708        ));
1709    }
1710
1711    let n = parts.len();
1712    // The replay flag lives on the full `CodegenContext`; the coverage /
1713    // test path has none → treat as no replay (mirror of HIR's
1714    // `ctx.emit_replay_runtime`, conservative on the coverage walk).
1715    let has_replay = emit_ctx.codegen.is_some_and(|c| c.emit_replay_runtime);
1716    let unwrap = ip.unwrap_results;
1717
1718    let mut code = String::new();
1719    if has_replay {
1720        // Runtime branch: if recording/replaying, execute sequentially
1721        // with replay groups (thread_local state stays on one thread).
1722        code.push_str("if crate::aver_replay::is_effect_tracking_active() { ");
1723        code.push_str("crate::aver_replay::enter_effect_group(); ");
1724        for (i, part) in parts.iter().enumerate() {
1725            code.push_str(&format!(
1726                "crate::aver_replay::set_effect_branch({i}); let _r{i} = {part}; "
1727            ));
1728        }
1729        code.push_str("crate::aver_replay::exit_effect_group(); ");
1730        if unwrap {
1731            code.push_str(&emit_result_tuple_unwrap("_r", "__v", n));
1732            code.push('?');
1733        } else {
1734            code.push_str(&emit_tuple_from_vars("_r", n));
1735        }
1736        code.push_str(" } else { ");
1737    }
1738
1739    if unwrap {
1740        code.push_str("{ ");
1741        if has_replay {
1742            code.push_str(
1743                "let __parallel_scope = crate::aver_replay::capture_parallel_scope_context(); ",
1744            );
1745        }
1746        code.push_str(
1747            "let __cancel_flag = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); ",
1748        );
1749        code.push_str("std::thread::scope(|_s| { ");
1750        for (i, part) in parts.iter().enumerate() {
1751            if has_replay {
1752                code.push_str(&format!(
1753                    "let __parallel_scope{i} = __parallel_scope.clone(); "
1754                ));
1755            }
1756            code.push_str(&format!("let __cancel_flag{i} = __cancel_flag.clone(); "));
1757            code.push_str(&format!("let _h{i} = _s.spawn(move || "));
1758            if has_replay {
1759                code.push_str(&format!(
1760                    "crate::aver_replay::with_parallel_scope_context(__parallel_scope{i}.clone(), move || "
1761                ));
1762            }
1763            code.push_str("{ crate::run_cancelable_branch(__cancel_flag");
1764            code.push_str(&i.to_string());
1765            code.push_str(".clone(), move || { let __result = ");
1766            code.push_str(part);
1767            code.push_str("; if let Err(_) = &__result { __cancel_flag");
1768            code.push_str(&i.to_string());
1769            code.push_str(".store(true, std::sync::atomic::Ordering::Relaxed); } __result }) }");
1770            if has_replay {
1771                code.push(')');
1772            }
1773            code.push_str("); ");
1774        }
1775        for i in 0..n {
1776            code.push_str(&format!("let _b{i} = _h{i}.join().unwrap(); "));
1777        }
1778        code.push_str(&emit_parallel_result_tuple_unwrap("_b", "_r", "__v", n));
1779        code.push_str(" })? }");
1780    } else {
1781        if has_replay {
1782            code.push_str(
1783                "let __parallel_scope = crate::aver_replay::capture_parallel_scope_context(); ",
1784            );
1785        }
1786        code.push_str("std::thread::scope(|_s| { ");
1787        for (i, part) in parts.iter().enumerate() {
1788            if has_replay {
1789                code.push_str(&format!(
1790                    "let __parallel_scope{i} = __parallel_scope.clone(); "
1791                ));
1792                code.push_str(&format!(
1793                    "let _h{i} = _s.spawn(move || crate::aver_replay::with_parallel_scope_context(__parallel_scope{i}.clone(), move || {part})); "
1794                ));
1795            } else {
1796                code.push_str(&format!("let _h{i} = _s.spawn(move || {part}); "));
1797            }
1798        }
1799        for i in 0..n {
1800            code.push_str(&format!("let _r{i} = _h{i}.join().unwrap(); "));
1801        }
1802        code.push_str(&emit_tuple_from_vars("_r", n));
1803        code.push_str(" }) ");
1804    }
1805
1806    if has_replay {
1807        code.push('}');
1808    }
1809    Some(code)
1810}
1811
1812/// Emit `MirExpr::IfThenElse` byte-identical to HIR's
1813/// `try_emit_bool_if_else` (the only producer of `IfThenElse` is the
1814/// MIR `bool_match_to_if` pass, which rewrites the exact two-arm bool
1815/// matches HIR routes through `try_emit_bool_if_else`).
1816///
1817/// Two HIR behaviors are mirrored here that the naive `if cond { then }
1818/// else { else }` emit misses:
1819///
1820/// 1. **Condition canonicalization.** HIR's
1821///    `classify_bool_subject_plan_resolved` never emits `>=` / `<=` /
1822///    `!=` in the condition: it rewrites `>=`→`<`, `<=`→`>`, `!=`→`==`
1823///    and *swaps* the then/else branches (`invert`). The MIR pass keeps
1824///    the source operator + branch order, so a `code >= 48` subject
1825///    renders as `if (code >= 48) { then } else { else }` where HIR
1826///    renders `if (code < 48) { else } else { then }`. Re-apply HIR's
1827///    rewrite so the two match.
1828/// 2. **Branch clone.** HIR runs each branch through `maybe_clone`
1829///    (owning position). Mirror with `mir_maybe_clone` (a no-op for the
1830///    already-graduated cases, exact for the rest).
1831fn emit_mir_if_then_else(
1832    ite: &crate::ir::mir::MirIfThenElse,
1833    emit_ctx: &MirEmitCtx<'_>,
1834) -> Option<String> {
1835    // HIR's `classify_bool_subject_plan_resolved` maps a comparison
1836    // subject to a canonical operator + an `invert` flag:
1837    //   ==  →  "==", keep ;  !=  →  "==", invert
1838    //   <   →  "<",  keep ;  >=  →  "<",  invert
1839    //   >   →  ">",  keep ;  <=  →  ">",  invert
1840    // `invert == true` swaps the then/else branches. Crucially, HIR's
1841    // `try_emit_bool_if_else` renders the condition operands with a
1842    // *plain* `emit_expr` — it does NOT apply the `BinOp` arm's
1843    // string-literal `&*x == "lit"` deref. So a `match name == "_"`
1844    // subject emits `name == AverStr::from("_")` in the condition, not
1845    // `&*name == "_"`. Mirror that by emitting the comparison cond
1846    // directly here from the raw operand renders, bypassing the
1847    // deref-applying `BinOp` arm.
1848    let (cond, then_src, else_src) = mir_if_cond_and_branches(ite, emit_ctx)?;
1849
1850    let then_branch = mir_maybe_clone(emit_mir_expr(then_src, emit_ctx)?, &then_src.node, emit_ctx);
1851    let else_branch = mir_maybe_clone(emit_mir_expr(else_src, emit_ctx)?, &else_src.node, emit_ctx);
1852    Some(format!(
1853        "if {} {{ {} }} else {{ {} }}",
1854        cond, then_branch, else_branch
1855    ))
1856}
1857
1858// ── Match (Wave 2) ──────────────────────────────────────────────────────
1859//
1860// `MirExpr::Match` → Rust source byte-identical to HIR's `emit_match`
1861// (`src/codegen/rust/expr.rs`). The strategy is to reuse the *shared*
1862// recognition + emit machinery the HIR walker already routes through:
1863//
1864//   1. Translate each `MirPattern` → `ResolvedPattern` (resolving ctor
1865//      identity through the symbol table, exactly as the resolver
1866//      stamped it). Build synthetic `ResolvedMatchArm`s carrying those
1867//      patterns + neutral bodies.
1868//   2. Pre-render every arm body via the MIR walker (`emit_mir_expr` +
1869//      `mir_maybe_clone`). If any arm body can't render, the whole
1870//      match falls back to HIR. The dispatch/list emitters take a
1871//      `body_for_arm` closure; we map each synthetic arm back to its
1872//      pre-rendered MIR body by pointer offset into the synthetic slice.
1873//   3. Drive the SAME selection ladder `emit_match` uses (single-arm
1874//      irrefutable → `let`; borrowed-param `match_on_ref`; list match;
1875//      dispatch table; generic `match`) using the SAME shared
1876//      classifier (`classify_match_dispatch_plan_resolved`) and the
1877//      SAME `emit_dispatch_table_match` / `emit_list_match` /
1878//      `emit_pattern` / `emit_pattern_rebindings` functions.
1879//
1880// Bool two-arm matches never reach here — the MIR optimizer's
1881// `bool_match_to_if` already rewrote them to `MirExpr::IfThenElse`
1882// (handled by the dedicated arm in `emit_mir_expr`). So this arm only
1883// ever sees list / dispatch-table / generic shapes, exactly the
1884// non-bool subset HIR's `emit_match` reaches after its own bool short
1885// circuit. Any shape the walker can't reproduce byte-identically
1886// returns `None` and the parity gate falls back safely.
1887
1888/// Mirror of HIR's `is_irrefutable_pattern` over `ResolvedPattern`.
1889fn resolved_pattern_is_irrefutable(pat: &ResolvedPattern) -> bool {
1890    match pat {
1891        ResolvedPattern::Wildcard | ResolvedPattern::Ident(_) => true,
1892        ResolvedPattern::Tuple(pats) => pats.iter().all(resolved_pattern_is_irrefutable),
1893        _ => false,
1894    }
1895}
1896
1897/// Translate a `MirPattern` → `ResolvedPattern`, resolving ctor
1898/// identity through the symbol table the same way the resolver pass
1899/// stamped it (so `emit_pattern` / `emit_pattern_rebindings` /
1900/// `classify_*` see the exact `ResolvedPattern` shape the HIR walker
1901/// would have). Returns `None` for any pattern shape the walker can't
1902/// translate yet (none currently — every `MirPattern` maps).
1903fn mir_pattern_to_resolved(pat: &MirPattern, ctx: &MirEmitCtx<'_>) -> Option<ResolvedPattern> {
1904    Some(match pat {
1905        MirPattern::Wildcard => ResolvedPattern::Wildcard,
1906        MirPattern::Literal(lit) => ResolvedPattern::Literal(lit.clone()),
1907        // A `Bind` is HIR's `Ident` binding (`x -> …`). The source
1908        // binder name is what HIR emits.
1909        MirPattern::Bind(_, name) => ResolvedPattern::Ident(name.clone()),
1910        MirPattern::EmptyList => ResolvedPattern::EmptyList,
1911        MirPattern::Cons {
1912            head_name,
1913            tail_name,
1914            ..
1915        } => ResolvedPattern::Cons(head_name.clone(), tail_name.clone()),
1916        MirPattern::Tuple(sub) => {
1917            let mut parts = Vec::with_capacity(sub.len());
1918            for p in sub {
1919                parts.push(mir_pattern_to_resolved(p, ctx)?);
1920            }
1921            ResolvedPattern::Tuple(parts)
1922        }
1923        MirPattern::Ctor {
1924            ctor,
1925            binding_names,
1926            ..
1927        } => {
1928            let resolved_ctor = match ctor {
1929                MirCtor::Builtin(b) => ResolvedCtor::Builtin(*b),
1930                MirCtor::User(ctor_id) => {
1931                    // Resolve `CtorId` → owning type + variant name,
1932                    // exactly as the resolver stamped a user
1933                    // `ResolvedCtor::User`. `semantic_constructor_from_resolved_ctor`
1934                    // (used downstream by `emit_pattern` /
1935                    // `emit_pattern_rebindings`) reads `type_id` + `name`.
1936                    let entry = ctx.symbol_table.ctor_entry(*ctor_id);
1937                    ResolvedCtor::User {
1938                        ctor_id: *ctor_id,
1939                        type_id: entry.owning_type,
1940                        name: entry.name.clone(),
1941                    }
1942                }
1943            };
1944            ResolvedPattern::Ctor(resolved_ctor, binding_names.clone())
1945        }
1946    })
1947}
1948
1949/// Build a neutral-bodied [`ResolvedMatchArm`] carrying just `pattern`.
1950/// The dispatch/list emitters only read `arm.pattern` + call the
1951/// `body_for_arm` closure; they never touch `arm.body`, so a `Unit`
1952/// literal placeholder is safe and the real MIR-rendered body is
1953/// supplied through the closure.
1954fn synthetic_arm(pattern: ResolvedPattern) -> ResolvedMatchArm {
1955    ResolvedMatchArm {
1956        pattern,
1957        body: Box::new(Spanned {
1958            node: crate::ir::hir::ResolvedExpr::Literal(crate::ast::Literal::Unit),
1959            line: 0,
1960            ty: std::sync::OnceLock::new(),
1961        }),
1962        binding_slots: std::sync::OnceLock::new(),
1963    }
1964}
1965
1966/// Emit Rust for a `MirExpr::Match`, byte-identical to HIR's
1967/// `emit_match`. Returns `None` (→ HIR fallback) when the subject or
1968/// any arm body can't render, when a pattern can't translate, or when
1969/// the match shape isn't one the walker reproduces yet.
1970fn emit_mir_match(m: &MirMatch, emit_ctx: &MirEmitCtx<'_>) -> Option<String> {
1971    // Default (non-TCO) arm-body renderer: emit the arm body through
1972    // the MIR walker, then `maybe_clone` for the owning position —
1973    // exactly HIR's per-arm
1974    // `maybe_clone(emit_expr(&arm.body.node, …), &arm.body.node, …)`.
1975    emit_mir_match_with(m, emit_ctx, &|arm_body, ctx| {
1976        let body = emit_mir_expr(arm_body, ctx)?;
1977        Some(mir_maybe_clone(body, &arm_body.node, ctx))
1978    })
1979}
1980
1981/// Core of [`emit_mir_match`], parameterized over how each arm body is
1982/// rendered. `render_arm` turns one arm's `Spanned<MirExpr>` body into
1983/// Rust source (or `None` → fall back). The default path renders bodies
1984/// as values (`maybe_clone`); the Wave-5 self-TCO loop path renders them
1985/// in tail position (self-`TailCall` → rebind + `continue`, value arm →
1986/// `return <expr>;`), so the same dispatch/list/generic machinery is
1987/// reused for TCO matches instead of forking the recognition.
1988fn emit_mir_match_with(
1989    m: &MirMatch,
1990    emit_ctx: &MirEmitCtx<'_>,
1991    render_arm: &dyn Fn(&Spanned<MirExpr>, &MirEmitCtx<'_>) -> Option<String>,
1992) -> Option<String> {
1993    // Translate patterns up front — bail if any pattern can't map.
1994    let mut arms: Vec<ResolvedMatchArm> = Vec::with_capacity(m.arms.len());
1995    for arm in &m.arms {
1996        arms.push(synthetic_arm(mir_pattern_to_resolved(
1997            &arm.pattern,
1998            emit_ctx,
1999        )?));
2000    }
2001
2002    // Pre-render every arm body, in arm order. `body_for_arm` (below)
2003    // maps a `&ResolvedMatchArm` back to its index by pointer offset
2004    // into `arms`, then reads the matching pre-rendered string.
2005    let mut arm_bodies: Vec<String> = Vec::with_capacity(m.arms.len());
2006    for arm in &m.arms {
2007        arm_bodies.push(render_arm(&arm.body, emit_ctx)?);
2008    }
2009
2010    let body_for_arm = |arm: &ResolvedMatchArm| -> String {
2011        // The dispatch/list emitters always hand back a reference to an
2012        // element of `arms` (they index `&arms[i]`), so identity match
2013        // by address recovers the arm's position → its pre-rendered MIR
2014        // body. Falls back to an empty body only if an emitter ever
2015        // passed a foreign reference (it doesn't), which the parity
2016        // gate would then reject as a mismatch.
2017        arms.iter()
2018            .position(|candidate| std::ptr::eq(candidate, arm))
2019            .map(|idx| arm_bodies[idx].clone())
2020            .unwrap_or_default()
2021    };
2022
2023    // ── 1. Single-arm irrefutable → `let` destructuring. ──
2024    // Mirror of `emit_match`'s first branch.
2025    if arms.len() == 1 && resolved_pattern_is_irrefutable(&arms[0].pattern) {
2026        let subj_code = emit_mir_expr(&m.subject, emit_ctx)?;
2027        // Int unboxing boundary (defect esc_match): a `match (n - 1) { x -> … }`
2028        // binds the subject to `x`. When the analysis marked the bound slot
2029        // BOXED (because `x` later escapes — `[x, x]`) but the subject renders
2030        // bare `i64`, box it at the binding crossing with `from_i64`, so the
2031        // bound `x` is the `AverInt` its later uses expect. A bare bound slot
2032        // keeps the raw subject. (Only the `Bind` arm carries a slot; a
2033        // wildcard / destructuring pattern is unaffected.)
2034        // ETAP-2 SLICE 1: the bind-crossing boundary is now an EXPLICIT
2035        // `Box`/`Unbox` node the rewrite wrapped the subject in (it rewrites
2036        // the subject in the binding's representation context). `subj_code`
2037        // already carries the right representation — no codegen-side coercion.
2038        let _ = &m.arms[0].pattern;
2039        let subj = mir_clone_arg(subj_code, &m.subject.node, emit_ctx);
2040        let codegen = emit_ctx.codegen?;
2041        let pat = emit_pattern(&arms[0].pattern, false, codegen);
2042        let body = arm_bodies[0].clone();
2043        return Some(match &arms[0].pattern {
2044            ResolvedPattern::Wildcard => body,
2045            ResolvedPattern::Ident(name) => {
2046                let name = aver_name_to_rust(name);
2047                format!("{{ let {} = {}; {} }}", name, subj, body)
2048            }
2049            _ => format!("{{ let {} = {}; {} }}", pat, subj, body),
2050        });
2051    }
2052
2053    // The shared dispatch/list/pattern emitters all need a real
2054    // `CodegenContext` (boxed-field lookup, module-prefix mangling).
2055    // The coverage walk runs without one — there the match only needs
2056    // to report "would emit", so we still translate + recurse but bail
2057    // before the ctx-dependent emit. (Production parity always has a
2058    // ctx; coverage only reads Some/None and matches will fall into the
2059    // None bucket on the coverage path, which is conservative + fine.)
2060    let codegen = emit_ctx.codegen?;
2061
2062    // An `Int`-literal pattern (top-level or nested in a tuple) cannot be a
2063    // Rust `match` pattern — `AverInt` is not a literal. Such matches lower
2064    // to an if/else-if equality-guard chain (`try_emit_int_literal_match`),
2065    // mirroring the dispatch-table guard path. They must NOT take the
2066    // borrow-by-reference path below: a guard `&AverInt == AverInt` does not
2067    // typecheck. So the subject is always cloned by VALUE for these.
2068    let any_int_literal_pattern = arms.iter().any(|arm| pattern_has_int_literal(&arm.pattern));
2069
2070    // ── 2. Borrowed-param subject → match on the reference. ──
2071    // Mirror of `emit_match`'s `match_on_ref` special case: only when
2072    // no arm has pattern bindings AND no arm has an Int-literal subpattern.
2073    let no_bindings = arms
2074        .iter()
2075        .all(|arm| crate::ir::vars::resolved_pattern_bindings(&arm.pattern).is_empty());
2076    let match_on_ref = no_bindings
2077        && !any_int_literal_pattern
2078        && mir_subject_is_borrowed_param(&m.subject.node, emit_ctx);
2079
2080    // Int unboxing: a bare-`i64` subject (a proven-bare counter) is `Copy`,
2081    // so it is read by value WITHOUT `.clone()` and the Int-literal guards
2082    // compare against raw `{N}i64` rather than `AverInt::from_i64(N)`.
2083    let subject_is_bare = any_int_literal_pattern && mir_expr_is_bare_i64(&m.subject, emit_ctx);
2084
2085    let subj = if subject_is_bare {
2086        // A bare i64 subject: emit the plain value (no clone — Copy).
2087        emit_bare_i64(&m.subject).or_else(|| emit_mir_expr(&m.subject, emit_ctx))?
2088    } else if match_on_ref {
2089        emit_mir_expr(&m.subject, emit_ctx)?
2090    } else {
2091        mir_clone_arg(
2092            emit_mir_expr(&m.subject, emit_ctx)?,
2093            &m.subject.node,
2094            emit_ctx,
2095        )
2096    };
2097
2098    let dispatch_plan = classify_match_dispatch_plan_resolved(&arms);
2099
2100    // Bool match → if/else is unreachable here: the MIR optimizer
2101    // already rewrote two-arm bool matches into `IfThenElse`. If a
2102    // `Bool` plan somehow survived (hand-built MIR in a test), fall
2103    // back rather than re-implement `try_emit_bool_if_else` (which
2104    // needs the subject's `ResolvedExpr` form for the compare-invert
2105    // rewrite the MIR walker can't reproduce).
2106    if matches!(dispatch_plan.as_ref(), Some(MatchDispatchPlan::Bool(_))) {
2107        return None;
2108    }
2109
2110    // ── 3. List match. ──
2111    if has_list_patterns(&arms) {
2112        let list_shape = match dispatch_plan.as_ref() {
2113            Some(MatchDispatchPlan::List(shape)) => Some(*shape),
2114            _ => None,
2115        };
2116        return Some(emit_list_match(
2117            subj,
2118            &arms,
2119            list_shape,
2120            true,
2121            codegen,
2122            body_for_arm,
2123        ));
2124    }
2125
2126    // ── 4. Dispatch table (literals / wrapper tags). ──
2127    if let Some(MatchDispatchPlan::Table(shape)) = dispatch_plan.as_ref() {
2128        return Some(emit_dispatch_table_match(
2129            subj,
2130            &arms,
2131            shape,
2132            subject_is_bare,
2133            body_for_arm,
2134        ));
2135    }
2136
2137    // ── 4b. Int-literal match → if/else-if equality-guard chain. ──
2138    // Any match that reaches here with an Int-literal subpattern (a single
2139    // top-level Int literal that didn't form a ≥2-entry dispatch table, or a
2140    // tuple carrying Int literals) can't be a Rust `match` — `AverInt` is not
2141    // a pattern literal. Lower it to guards instead. When such a pattern is
2142    // present the generic `match` path below is NOT a valid fallback (it
2143    // would emit `AverInt::from_i64(N)` as a pattern), so the guard emitter
2144    // is REQUIRED to render; if it can't, return `None` (hard diagnostic).
2145    if any_int_literal_pattern {
2146        return try_emit_int_literal_match(&subj, &arms, &arm_bodies, subject_is_bare, codegen);
2147    }
2148
2149    // ── 5. Generic `match`. ──
2150    // Mirror of `emit_match`'s tail. `needs_as_str` is always `true`
2151    // in HIR (`subject_might_be_string` is a `true` stub), so the
2152    // string-literal-pattern case derefs the subject to `&str`.
2153    let needs_as_str = true;
2154    let match_expr = if needs_as_str && has_string_literal_patterns(&arms) {
2155        format!("&*{}", subj)
2156    } else {
2157        subj
2158    };
2159
2160    let mut arm_strs = Vec::with_capacity(arms.len());
2161    for (idx, arm) in arms.iter().enumerate() {
2162        let pat = emit_pattern(&arm.pattern, needs_as_str, codegen);
2163        let body = arm_bodies[idx].clone();
2164        let mut rebindings = emit_pattern_rebindings(&arm.pattern, codegen);
2165        if match_on_ref {
2166            let ref_rebinds = emit_ref_match_rebindings(&arm.pattern);
2167            if !ref_rebinds.is_empty() {
2168                rebindings = format!("{}{}", ref_rebinds, rebindings);
2169            }
2170        }
2171        arm_strs.push(format!(
2172            "        {} => {{\n            {}{}\n        }}",
2173            pat, rebindings, body
2174        ));
2175    }
2176
2177    Some(format!(
2178        "match {} {{\n{}\n    }}",
2179        match_expr,
2180        arm_strs.join(",\n")
2181    ))
2182}
2183
2184/// Does this pattern contain an `Int`-literal subpattern (top-level or
2185/// nested inside a tuple)? Such a pattern cannot become a Rust `match`
2186/// arm — `AverInt` is not a literal — so it forces the equality-guard
2187/// lowering in [`try_emit_int_literal_match`] and excludes the by-reference
2188/// `match_on_ref` path (where the guard would compare `&AverInt`).
2189fn pattern_has_int_literal(pat: &ResolvedPattern) -> bool {
2190    match pat {
2191        // A big-int literal pattern is also routed through the equality-guard
2192        // chain (an `AverInt` cannot be a Rust `match` literal) — it compares via
2193        // `AverInt: PartialEq`, just like the i64 case but parsed from digits.
2194        ResolvedPattern::Literal(crate::ast::Literal::Int(_) | crate::ast::Literal::BigInt(_)) => {
2195            true
2196        }
2197        ResolvedPattern::Tuple(pats) => pats.iter().any(pattern_has_int_literal),
2198        _ => false,
2199    }
2200}
2201
2202/// Lower a match that carries `Int`-literal patterns into an if/else-if
2203/// equality-guard chain (`AverInt: PartialEq`), since `AverInt` cannot be a
2204/// Rust `match` pattern literal. Mirrors the dispatch-table guard path, but
2205/// also handles a single Int-literal arm (too few for a dispatch table) and
2206/// tuple subjects with Int-literal subpatterns.
2207///
2208/// `subj` is the already-emitted, by-VALUE subject expression. The supported
2209/// arm shapes (after list / table / bool matches are peeled off upstream):
2210/// top-level `Literal(Int)` / `Wildcard` / `Ident`, and `Tuple(..)` whose
2211/// elements are `Literal(Int)` / `Wildcard` / `Ident` (or nested tuples of
2212/// the same). Returns `None` for any other shape (e.g. a non-Int literal or
2213/// a ctor mixed in) — the caller then emits a hard diagnostic.
2214fn try_emit_int_literal_match(
2215    subj: &str,
2216    arms: &[ResolvedMatchArm],
2217    arm_bodies: &[String],
2218    subject_is_bare: bool,
2219    codegen: &CodegenContext,
2220) -> Option<String> {
2221    // Bind the subject once so a non-trivial expr is evaluated a single time
2222    // and the guards / bindings reference the temp.
2223    let subject_name = "__int_match_subject";
2224
2225    // Collect every binding name used anywhere in the match, so the fresh
2226    // tuple-element temporaries (`__litN`) can be chosen to never collide
2227    // with a real pattern binding (hygiene — fix #4).
2228    let mut used_names: HashSet<String> = HashSet::new();
2229    for arm in arms {
2230        for b in crate::ir::vars::resolved_pattern_bindings(&arm.pattern) {
2231            used_names.insert(aver_name_to_rust(&b));
2232        }
2233    }
2234    used_names.insert(subject_name.to_string());
2235
2236    // Pre-render the per-element tuple temp names, fresh + non-colliding.
2237    // The same N temps serve every tuple arm (the subject is one tuple), so
2238    // pick them once up front.
2239    let tuple_arity = arms.iter().find_map(|arm| match &arm.pattern {
2240        ResolvedPattern::Tuple(pats) => Some(pats.len()),
2241        _ => None,
2242    });
2243    let tuple_temps: Vec<String> = match tuple_arity {
2244        Some(n) => {
2245            let mut temps = Vec::with_capacity(n);
2246            let mut counter = 0usize;
2247            for _ in 0..n {
2248                let name = loop {
2249                    let candidate = format!("__lit{}", counter);
2250                    counter += 1;
2251                    if !used_names.contains(&candidate) {
2252                        break candidate;
2253                    }
2254                };
2255                used_names.insert(name.clone());
2256                temps.push(name);
2257            }
2258            temps
2259        }
2260        None => Vec::new(),
2261    };
2262
2263    // For each arm, build (Option<condition>, bindings-prelude). A `None`
2264    // condition is the catch-all (`else`). The last such arm closes the
2265    // chain; any arm after it is dead but harmless.
2266    enum ArmPlan {
2267        Guard { cond: String, prelude: String },
2268        Default { prelude: String },
2269    }
2270
2271    let mut plans: Vec<(ArmPlan, &str)> = Vec::with_capacity(arms.len());
2272    for (idx, arm) in arms.iter().enumerate() {
2273        let body = arm_bodies[idx].as_str();
2274        match &arm.pattern {
2275            ResolvedPattern::Wildcard => {
2276                plans.push((
2277                    ArmPlan::Default {
2278                        prelude: String::new(),
2279                    },
2280                    body,
2281                ));
2282            }
2283            ResolvedPattern::Ident(name) => {
2284                // Catch-all binding: bind the whole subject by value. A bare
2285                // `i64` subject is `Copy` (no `.clone()` needed and binding
2286                // it keeps the bare repr the body's arithmetic reads).
2287                let rust = aver_name_to_rust(name);
2288                let prelude = if subject_is_bare {
2289                    format!("let {} = {}; ", rust, subject_name)
2290                } else {
2291                    format!("let {} = {}.clone(); ", rust, subject_name)
2292                };
2293                plans.push((ArmPlan::Default { prelude }, body));
2294            }
2295            ResolvedPattern::Literal(crate::ast::Literal::Int(n)) => {
2296                // Int unboxing: a bare subject compares against a raw `i64`
2297                // literal; a boxed subject compares against `AverInt`.
2298                let cond = if subject_is_bare {
2299                    format!("{} == {}i64", subject_name, n)
2300                } else {
2301                    format!("{} == aver_rt::AverInt::from_i64({})", subject_name, n)
2302                };
2303                plans.push((
2304                    ArmPlan::Guard {
2305                        cond,
2306                        prelude: String::new(),
2307                    },
2308                    body,
2309                ));
2310            }
2311            ResolvedPattern::Literal(crate::ast::Literal::BigInt(s)) => {
2312                // A big-int literal pattern: compare the subject against the
2313                // exact `AverInt` parsed from the digits. A bare i64 subject can
2314                // never equal a `>i64` value, but boxing it keeps the comparison
2315                // well-typed (and correctly false at runtime).
2316                let lhs = if subject_is_bare {
2317                    format!("aver_rt::AverInt::from_i64({})", subject_name)
2318                } else {
2319                    subject_name.to_string()
2320                };
2321                let cond = format!("{} == {:?}.parse::<aver_rt::AverInt>().unwrap()", lhs, s);
2322                plans.push((
2323                    ArmPlan::Guard {
2324                        cond,
2325                        prelude: String::new(),
2326                    },
2327                    body,
2328                ));
2329            }
2330            ResolvedPattern::Tuple(pats) => {
2331                // Tuple subjects are never bare in this slice (only scalar
2332                // counters go bare); the bare guard machinery does not
2333                // model tuple-element reps. Bail rather than mis-emit.
2334                if subject_is_bare {
2335                    return None;
2336                }
2337                if pats.len() != tuple_temps.len() {
2338                    return None;
2339                }
2340                let mut conds: Vec<String> = Vec::new();
2341                let mut prelude = String::new();
2342                for (pat, temp) in pats.iter().zip(tuple_temps.iter()) {
2343                    // Each top-level temp `__litN` is a reference to the
2344                    // tuple element (`&Elem`), so its value place is `*temp`.
2345                    // Recurse into the element, destructuring nested tuples
2346                    // to arbitrary depth via field-index access expressions.
2347                    let place = format!("(*{})", temp);
2348                    if !lower_int_literal_subpatterns(pat, &place, &mut conds, &mut prelude) {
2349                        // A non-Int literal, ctor, or other shape nested
2350                        // anywhere in the tuple is out of scope here.
2351                        return None;
2352                    }
2353                }
2354                if conds.is_empty() {
2355                    // No literal constraints: this tuple arm is a catch-all
2356                    // (pure bindings / wildcards) — it closes the chain.
2357                    plans.push((ArmPlan::Default { prelude }, body));
2358                } else {
2359                    let cond = conds.join(" && ");
2360                    plans.push((ArmPlan::Guard { cond, prelude }, body));
2361                }
2362            }
2363            // Any other top-level shape is out of scope for this lowering.
2364            _ => return None,
2365        }
2366    }
2367
2368    // Build the if/else-if chain from the back. The chain MUST end in a
2369    // default arm (the Aver typechecker rejects a non-exhaustive Int match,
2370    // so a `_`/binding arm is always present); if none was found, emit an
2371    // `unreachable!` tail so the generated `match` is still total.
2372    let mut chain =
2373        String::from("unreachable!(\"Aver Rust codegen: non-exhaustive Int-literal match\")");
2374    for (plan, body) in plans.into_iter().rev() {
2375        match plan {
2376            ArmPlan::Default { prelude } => {
2377                chain = format!("{{ {}{} }}", prelude, body);
2378            }
2379            ArmPlan::Guard { cond, prelude } => {
2380                chain = format!("if {} {{ {}{} }} else {}", cond, prelude, body, chain);
2381            }
2382        }
2383    }
2384
2385    // Tuple subjects need the element temps bound (by reference, so the
2386    // guards compare `&AverInt == &AverInt`). A non-tuple subject is used
2387    // directly.
2388    let setup = if tuple_temps.is_empty() {
2389        format!("let {} = {};", subject_name, subj)
2390    } else {
2391        // Destructure `&(AverInt, …)`: match ergonomics bind each element as
2392        // `&AverInt`, so the guards compare `&AverInt == &AverInt` and a
2393        // binding element clones the `&AverInt` to an owned value. A
2394        // single-element tuple needs the trailing comma (`(x,)`).
2395        let elems = if tuple_temps.len() == 1 {
2396            format!("{},", tuple_temps[0])
2397        } else {
2398            tuple_temps.join(", ")
2399        };
2400        format!(
2401            "let {} = {}; let ({}) = &{};",
2402            subject_name, subj, elems, subject_name
2403        )
2404    };
2405
2406    // `codegen` is threaded only to keep the signature uniform with the
2407    // other emitters; the guard lowering needs no ctx lookups.
2408    let _ = codegen;
2409    Some(format!("{{ {} {} }}", setup, chain))
2410}
2411
2412/// Recursively lower one tuple-subpattern of an Int-literal match against a
2413/// `place` expression (a Rust expression denoting the *value place* of the
2414/// element, e.g. `(*__lit0)` or `(*__lit1).0`). Appends an equality guard for
2415/// every Int-literal LEAF (at any depth), binds identifier leaves into
2416/// `prelude`, and ignores wildcards. Nested tuples destructure via field-index
2417/// access (`{place}.{i}`) — no fresh `match` bindings, so hygiene is automatic.
2418///
2419/// Returns `false` if any leaf is an unsupported shape (a non-Int literal, a
2420/// ctor, …); the caller then bails to the hard codegen diagnostic. This is the
2421/// arbitrarily-nested generalization of the one-level element loop above.
2422fn lower_int_literal_subpatterns(
2423    pat: &ResolvedPattern,
2424    place: &str,
2425    conds: &mut Vec<String>,
2426    prelude: &mut String,
2427) -> bool {
2428    match pat {
2429        ResolvedPattern::Literal(crate::ast::Literal::Int(n)) => {
2430            // `place` is a value place; `&{place}` is `&AverInt`, comparable to
2431            // the literal reference.
2432            conds.push(format!("&{} == &aver_rt::AverInt::from_i64({})", place, n));
2433            true
2434        }
2435        ResolvedPattern::Literal(crate::ast::Literal::BigInt(s)) => {
2436            // Nested tuple elements are always boxed `AverInt`s; compare against
2437            // the exact value parsed from the digits.
2438            conds.push(format!(
2439                "&{} == &{:?}.parse::<aver_rt::AverInt>().unwrap()",
2440                place, s
2441            ));
2442            true
2443        }
2444        ResolvedPattern::Wildcard => true,
2445        ResolvedPattern::Ident(name) if name == "_" => true,
2446        ResolvedPattern::Ident(name) => {
2447            let rust = aver_name_to_rust(name);
2448            // Clone the value place into an owned binding.
2449            prelude.push_str(&format!("let {} = {}.clone(); ", rust, place));
2450            true
2451        }
2452        ResolvedPattern::Tuple(pats) => {
2453            for (i, sub) in pats.iter().enumerate() {
2454                // Field `i` of the tuple at `place` is itself a value place.
2455                let sub_place = format!("{}.{}", place, i);
2456                if !lower_int_literal_subpatterns(sub, &sub_place, conds, prelude) {
2457                    return false;
2458                }
2459            }
2460            true
2461        }
2462        // A non-Int literal, ctor, or any other shape is out of scope.
2463        _ => false,
2464    }
2465}
2466
2467/// Is the match subject a read of a borrowed-param local? Mirror of
2468/// `emit_match`'s `match_on_ref` subject check
2469/// (`ResolvedExpr::Ident | Resolved` whose name `is_borrowed_param`).
2470fn mir_subject_is_borrowed_param(subject: &MirExpr, emit_ctx: &MirEmitCtx<'_>) -> bool {
2471    local_of(subject).is_some_and(|local| emit_ctx.is_borrowed_param(&local.name))
2472}
2473
2474/// Emit the FULL function body the MIR walker produces, in the
2475/// `emit_fn_body` format — the leading
2476/// `    crate::cancel_checkpoint();\n    ` then the body expression.
2477/// Returns `None` when the walker can't render the body (any uncovered
2478/// construct anywhere in the tree), the signal for the caller to emit a
2479/// hard codegen diagnostic.
2480///
2481/// One return-position detail: a field access (`Project`) on a borrowed
2482/// param in tail/return position needs `.clone()` to produce an owned
2483/// value (`emit_mir_expr` emits `obj.field` without it).
2484///
2485/// A top-level `Let` chain (the MIR shape a `Block` body with `let`
2486/// bindings lowers to) is emitted as flat statement lines —
2487/// `    let a = …;\n    let b = …;\n    <final-expr>` — instead of the
2488/// nested block-expr `{ let a = …; { let b = …; … } }` `emit_mir_expr`
2489/// renders for an inline `Let`. See [`emit_mir_let_chain_flat`].
2490pub(super) fn emit_mir_fn_body(
2491    body: &Spanned<MirExpr>,
2492    emit_ctx: &MirEmitCtx<'_>,
2493) -> Option<String> {
2494    // A top-level `Let` is a multi-statement body, emitted as flat
2495    // statement lines (named binding → `let …;`, discarded intermediate
2496    // `Stmt::Expr` → bare `…;`) then the final expression on its own
2497    // line — never a nested block-expr. The chain handles both named and
2498    // empty-`binding_name` (discarded) bindings, so no first-binding
2499    // guard is needed.
2500    if let MirExpr::Let(spanned_let) = &body.node
2501        && let Some(lines) = emit_mir_let_chain_flat(&spanned_let.node, emit_ctx)
2502    {
2503        return Some(format!("    crate::cancel_checkpoint();\n    {}", lines));
2504    }
2505
2506    // Int unboxing: a non-TCO fn whose return is bare `i64` must emit its
2507    // tail value bare. The tail is the whole body (or, for a `Match` /
2508    // `IfThenElse`, every arm value). `emit_bare_return_tail` handles those
2509    // shapes; a `None` means the tail isn't a renderable-bare shape, in
2510    // which case we fall through to the boxed emit (which would be a type
2511    // error — but `bare_return` was only set when `tail_value_is_bare`
2512    // proved every leaf bare, so this renders for the shapes that earned
2513    // the bare return).
2514    if emit_ctx.bare.bare_return
2515        && let Some(tail) = emit_bare_return_tail(body, emit_ctx)
2516    {
2517        return Some(format!("    crate::cancel_checkpoint();\n    {}", tail));
2518    }
2519
2520    // ETAP-2 SLICE 1: the boxed-return tail boundary (defect Q5 / subj_ret)
2521    // is now EXPLICIT — the `bare_i64_rewrite` pass already `Box`ed every
2522    // bare leaf reaching a boxed return, and `emit_mir_expr` lowers those
2523    // `Box` nodes. So the default emit below renders the boxed-return tail
2524    // correctly without a codegen-side boxing pass.
2525
2526    let mut code = emit_mir_expr(body, emit_ctx)?;
2527    // Return-position field access on a borrowed param → clone for
2528    // an owned result. Mirror of HIR's
2529    // `emit_body_expr_plan_with_options` `Leaf`/`Expr` arms.
2530    if let MirExpr::Project(p) = &body.node
2531        && let Some(local) = local_of(&p.node.base.node)
2532        && emit_ctx.is_borrowed_param(&local.name)
2533    {
2534        code = format!("{}.clone()", code);
2535    }
2536    Some(format!("    crate::cancel_checkpoint();\n    {}", code))
2537}
2538
2539/// Emit a non-TCO body's tail value as a bare `i64` expression. The tail
2540/// is the value the fn returns; for `Match` / `IfThenElse` it is every arm
2541/// value (each rendered bare). A bare leaf (`Local` / literal / arithmetic)
2542/// renders via [`emit_bare_i64`]. Returns `None` for any shape the bare
2543/// path can't render (the caller then falls back to the boxed emit). Only
2544/// invoked when the analysis proved `bare_return` (every tail leaf
2545/// bare-eligible), so the supported shapes cover the bare-return cases.
2546fn emit_bare_return_tail(expr: &Spanned<MirExpr>, ctx: &MirEmitCtx<'_>) -> Option<String> {
2547    match &expr.node {
2548        // A directly bare-eligible leaf / arithmetic tail.
2549        _ if mir_expr_is_bare_i64(expr, ctx) => emit_bare_i64(expr),
2550        // A `Match` over Int literals whose arm bodies are bare tails. Reuse
2551        // the Int-literal guard chain but render each arm's tail bare.
2552        MirExpr::Match(m) => emit_mir_match_with(&m.node, ctx, &|arm_body, ctx| {
2553            emit_bare_return_tail(arm_body, ctx)
2554        }),
2555        MirExpr::IfThenElse(ite) => {
2556            let (cond, then_src, else_src) = mir_if_cond_and_branches(&ite.node, ctx)?;
2557            let then_b = emit_bare_return_tail(then_src, ctx)?;
2558            let else_b = emit_bare_return_tail(else_src, ctx)?;
2559            Some(format!(
2560                "if {} {{ {} }} else {{ {} }}",
2561                cond, then_b, else_b
2562            ))
2563        }
2564        MirExpr::Let(l) => {
2565            // A let-chain ending in a bare tail: render the bindings via the
2566            // flat emitter, then the bare final. Fall back to None if the
2567            // chain isn't flat-renderable.
2568            let _ = l;
2569            None
2570        }
2571        _ => None,
2572    }
2573}
2574
2575/// Emit a top-level `Let` chain as flat Rust statement lines: each
2576/// binding becomes `let {name} = {value};` (value rendered raw, no clone
2577/// wrapper), one per line, 4-space indented and `\n`-joined, terminated
2578/// by the chain's final expression rendered raw on its own line.
2579///
2580/// The chain is the run of directly-nested `Let` nodes: each one emits
2581/// its statement line and continues into its body until a body that
2582/// isn't a `Let` becomes the final expression. A named binding emits
2583/// `let {name} = {value};`; an empty-`binding_name` binding (a
2584/// discarded intermediate `Stmt::Expr` or a `_ = effect()` discard)
2585/// emits a bare `{value};` statement (the value evaluated for its
2586/// effects, result dropped). Returns `None` only when a binding value or
2587/// the final expression can't render.
2588fn emit_mir_let_chain_flat(
2589    let_node: &crate::ir::mir::MirLet,
2590    ctx: &MirEmitCtx<'_>,
2591) -> Option<String> {
2592    let mut lines: Vec<String> = Vec::new();
2593    let mut current = let_node;
2594    loop {
2595        let value = emit_mir_expr(&current.value, ctx)?;
2596        if current.binding_name.is_empty() {
2597            // Discarded intermediate (`Stmt::Expr` at non-tail position,
2598            // or a `_ = effect()` discard binding). No source ident to
2599            // bind — emit the value as a bare statement and drop it, the
2600            // exact mirror of HIR's non-last `ResolvedStmt::Expr` arm
2601            // (`{expr};`). Typically an effectful builtin call
2602            // (`Console.print(…)`) evaluated for its effect.
2603            lines.push(format!("{};", value));
2604        } else {
2605            let name = aver_name_to_rust(&current.binding_name);
2606            lines.push(format!("let {} = {};", name, value));
2607        }
2608
2609        // Continue the chain when the body is another `Let` (named or a
2610        // discarded intermediate); the first non-`Let` body is the final
2611        // expression. Both binder shapes lower to flat statement lines,
2612        // so the nested-block shape never needs to appear.
2613        match &current.body.node {
2614            MirExpr::Let(next) => {
2615                current = &next.node;
2616            }
2617            _ => {
2618                // Int unboxing: a bare-return fn's let-chain tail emits the
2619                // final value bare so the returned expression's type is
2620                // `i64`.
2621                let final_expr = if ctx.bare.bare_return
2622                    && let Some(bare) = emit_bare_return_tail(&current.body, ctx)
2623                {
2624                    bare
2625                } else {
2626                    emit_mir_expr(&current.body, ctx)?
2627                };
2628                lines.push(final_expr);
2629                break;
2630            }
2631        }
2632    }
2633    Some(lines.join("\n    "))
2634}
2635
2636// ── Production body emit (MIR is the sole codegen path) ─────────────────
2637//
2638// The HIR walker was deleted in rust-on-MIR W6/Stage-3, so there is no
2639// byte-parity gate left: the MIR walker OWNS all runtime codegen. This
2640// helper builds the per-fn `MirFnEmitPolicy` (param types /
2641// borrow-by-default) exactly as the HIR `build_fn_ectx_from_resolved`
2642// did, wraps it in a `MirEmitCtx`, and renders the body. A `None`
2643// propagates to the caller, which emits a hard codegen diagnostic — the
2644// only constructs that hit it are the verify-only Oracle/trace residual
2645// that never built on the Rust backend.
2646
2647/// Render a non-TCO fn body via the MIR walker. `resolved` supplies the
2648/// borrow policy (param types / borrow-by-default), recomputed exactly
2649/// as `build_fn_ectx_from_resolved` does. Returns the body string in the
2650/// `emit_fn_body` format (`    crate::cancel_checkpoint();\n    …`), or
2651/// `None` when the walker can't render the body.
2652pub(super) fn emit_mir_fn_body_routed(
2653    mir_fn: &crate::ir::mir::MirFn,
2654    resolved: &crate::ir::hir::ResolvedFnDef,
2655    scope: Option<&str>,
2656    borrow_by_default: bool,
2657    ctx: &CodegenContext,
2658) -> Option<String> {
2659    let mut policy = MirFnEmitPolicy::from_resolved(resolved, scope, borrow_by_default);
2660    // Graduate own_param-proven collection params to owned-by-value so
2661    // the body skips the `.clone()` at last-use mutation sites. The
2662    // SIGNATURE (`emit_fn_def_with_visibility`) computes the SAME owned
2663    // set from the same `mir_fn.aliased_slots` and emits `mut p: T`, so
2664    // body and signature agree on which params are owned.
2665    policy.apply_own_param(mir_fn);
2666    // Apply the Int unboxing facts so a proven-bare slot emits native
2667    // `i64`. Same per-fn slice the signature emit reads (via
2668    // `bare_fn_facts`), so body and signature agree on which params /
2669    // return are bare.
2670    policy.apply_bare_i64(mir_fn.fn_id, ctx);
2671    let emit_ctx = MirEmitCtx::for_fn(ctx, &policy);
2672    emit_mir_fn_body(&mir_fn.body, &emit_ctx)
2673}
2674
2675/// Is the type stamp a primitive numeric?
2676/// `Int` / `Float` / `Byte` count; everything else (incl. `Str`)
2677/// doesn't. Mirror of HIR's `EmitCtx::expr_is_numeric` for the
2678/// MIR walker's `+` dispatch.
2679fn ty_is_numeric(ty: Option<&Type>) -> bool {
2680    matches!(ty, Some(Type::Int | Type::Float))
2681}
2682
2683/// Is the type stamp exactly `Int`? Drives the `AverInt`-method lowering
2684/// of arithmetic (`+ - * /`, unary `-`) — `Int` operands take the
2685/// non-wrapping method calls, everything else (notably `Float`) keeps the
2686/// raw Rust operator.
2687fn ty_is_int(ty: Option<&Type>) -> bool {
2688    matches!(ty, Some(Type::Int))
2689}
2690
2691// ── TCO loop / trampoline synthesis from MIR ────────────────────────────
2692//
2693// Rust has no TCO primitive — the VM emits a `TAIL_CALL` opcode and
2694// wasm-gc a `return_call`, both flat instructions. In generated Rust the
2695// loop (self-recursive) and the trampoline (mutual-recursive) STRUCTURE
2696// is synthesized in source from `MirExpr::TailCall` (a self-`TailCall`
2697// arm becomes `continue` after rebinding the loop's mutable params; a
2698// value arm becomes `return`).
2699//
2700// The MIR walker emits its OWN correct loop / trampoline, verified
2701// BEHAVIORALLY (build + run vs VM + self-host regen):
2702//
2703//   * **Always-snapshot param rebind.** For every rebound param, emit
2704//     `let __tcoN = <arg>;` for ALL of them first, then
2705//     `param = __tcoN;` in order, then `continue;`. Strictly correct
2706//     (no read-after-write clobber), no substring heuristic. Identity
2707//     rebinds (`arg == param`) and pass-through (rc) params are skipped.
2708//   * **No loop-invariant hoisting** (correctness needs none).
2709//
2710// The ownership / borrow facts (rc pass-through params Arc-wrapped on
2711// the self-loop / `&T` extra trampoline args; non-rc owned params `mut`
2712// with NO borrow-by-default) are re-derived from the AST `FnDef` via
2713// `compute_rc_params` / `compute_self_passthrough_params`; those are
2714// name/structure based and SCC discovery reuses `find_mutual_tco_groups`.
2715// Get the ownership wrong → rustc rejects, which the build gate catches.
2716
2717/// Emit a self-TCO fn entirely from MIR: the public signature
2718/// (`mut`-owned params, rc params Arc-wrapped before the loop) + the
2719/// `loop { cancel_checkpoint(); <tco-body> }` wrapper, where the body
2720/// renders self-`TailCall` arms as `{ rebind; continue }` and value arms
2721/// as `return <expr>;`.
2722///
2723/// `fd` supplies param names/types + drives the AST-based rc /
2724/// pass-through computation (mirroring `emit_tco_fn`); `mir_fn.body` is
2725/// the MIR body walked in tail position. Returns `None` (→ HIR fallback)
2726/// when any sub-expression can't render.
2727#[allow(clippy::too_many_arguments)]
2728pub(super) fn emit_mir_tco_fn(
2729    fd: &crate::ast::FnDef,
2730    resolved_fd: &crate::ir::hir::ResolvedFnDef,
2731    mir_fn: &crate::ir::mir::MirFn,
2732    fn_name: &str,
2733    ret_type: &str,
2734    visibility: &str,
2735    scope: Option<&str>,
2736    ctx: &CodegenContext,
2737) -> Option<String> {
2738    use super::toplevel::{compute_rc_params, compute_self_passthrough_params, rc_param_names};
2739
2740    let passthrough_indices = compute_self_passthrough_params(fd);
2741    let rc_indices = compute_rc_params(std::slice::from_ref(&fd), ctx);
2742    let rc_names = rc_param_names(&fd.params, &rc_indices);
2743
2744    // Borrow policy: no borrow-by-default (owned `mut` params), rc
2745    // params wrapped (`(*x).clone()` on read). Mirror of
2746    // `emit_tco_fn`'s `build_fn_ectx_no_borrow_from_resolved` +
2747    // `with_rc_wrapped`.
2748    let mut policy = MirFnEmitPolicy::from_resolved(resolved_fd, scope, /* borrow */ false);
2749    policy.rc_wrapped = rc_names.clone();
2750    // own_param-proven collection params are already `mut`-owned in the
2751    // TCO signature (`emit_tco_params_mir`); graduating them only flips
2752    // the body's clone-skip on. A pass-through param Arc-wrapped via
2753    // `rc_wrapped` keeps its `&T` / `(*x).clone()` shape — rc-wrapping is
2754    // a structural TCO decision that takes precedence, so drop any
2755    // rc-wrapped name back out of `owned_params` to keep signature and
2756    // body consistent.
2757    policy.apply_own_param(mir_fn);
2758    // Int unboxing: a bare `i64` counter param is `Copy`-by-value, so it
2759    // is never rc-wrapped — a param bare in the summary is disjoint from
2760    // `rc_wrapped` by construction (rc only wraps non-Copy pass-through
2761    // collection params). The signature emit (`emit_tco_params_mir`)
2762    // reads the same per-fn facts so both agree.
2763    policy.apply_bare_i64(mir_fn.fn_id, ctx);
2764    for n in &rc_names {
2765        policy.owned_params.remove(n);
2766    }
2767    let emit_ctx = MirEmitCtx::for_fn(ctx, &policy);
2768
2769    // Render the body in tail position FIRST — bail before emitting any
2770    // signature if the walker can't render it.
2771    let body_code = emit_mir_tco_body(
2772        &mir_fn.body,
2773        mir_fn.fn_id,
2774        &fd.params,
2775        &passthrough_indices,
2776        &emit_ctx,
2777    )?;
2778
2779    // Int unboxing: a bare-`i64` param/return emits `i64` in the signature
2780    // (the load-bearing cross-frame change), read off the SAME per-fn facts
2781    // the body emit applied (`apply_bare_i64`). Every caller converts at the
2782    // boundary, so the ABI stays self-consistent.
2783    let bare_facts = ctx.bare_i64.for_fn(mir_fn.fn_id);
2784    let params = emit_tco_params_mir(&fd.params, &rc_indices, bare_facts);
2785    let ret_type = bare_return_type(ret_type, bare_facts);
2786    let mut lines = Vec::new();
2787    lines.push(format!(
2788        "{}fn {}({}) -> {} {{",
2789        visibility, fn_name, params, ret_type
2790    ));
2791    // Wrap pass-through params in Arc before the loop (shadowing the
2792    // original binding). Mirror of `emit_tco_fn`.
2793    for &i in &rc_indices {
2794        let rust_name = aver_name_to_rust(&fd.params[i].0);
2795        lines.push(format!(
2796            "    let {} = std::sync::Arc::new({});",
2797            rust_name, rust_name
2798        ));
2799    }
2800    lines.push("    loop {".to_string());
2801    lines.push(body_code);
2802    lines.push("    }".to_string());
2803    lines.push("}".to_string());
2804    Some(lines.join("\n"))
2805}
2806
2807/// Self-TCO param signature: non-rc params are `mut T` (rebound in the
2808/// loop), rc params are plain `T` (shadowed by the Arc::new binding).
2809/// Mirror of `emit_fn_params_tco`. A param the unboxing analysis proved
2810/// bare (`bare_facts.param_is_bare(i)`) emits `mut p: i64` instead of
2811/// `mut p: aver_rt::AverInt`.
2812fn emit_tco_params_mir(
2813    params: &[(String, String)],
2814    rc_indices: &std::collections::HashSet<usize>,
2815    bare_facts: Option<&crate::ir::mir::FnBareFacts>,
2816) -> String {
2817    params
2818        .iter()
2819        .enumerate()
2820        .map(|(i, (name, type_ann))| {
2821            let is_bare = bare_facts.is_some_and(|f| f.param_is_bare(i));
2822            let rust_type = if is_bare {
2823                "i64".to_string()
2824            } else {
2825                super::types::type_annotation_to_rust(type_ann)
2826            };
2827            let rust_name = aver_name_to_rust(name);
2828            if rc_indices.contains(&i) {
2829                format!("{}: {}", rust_name, rust_type)
2830            } else {
2831                format!("mut {}: {}", rust_name, rust_type)
2832            }
2833        })
2834        .collect::<Vec<_>>()
2835        .join(", ")
2836}
2837
2838/// The return type for a fn whose return the unboxing analysis proved bare:
2839/// `i64`, else the original `ret_type` string. Used by the self-TCO and
2840/// non-TCO signature emit.
2841fn bare_return_type(ret_type: &str, bare_facts: Option<&crate::ir::mir::FnBareFacts>) -> String {
2842    if bare_facts.is_some_and(|f| f.bare_return) {
2843        "i64".to_string()
2844    } else {
2845        ret_type.to_string()
2846    }
2847}
2848
2849/// Emit the self-TCO loop body (inside `loop { … }`). Leads with
2850/// `cancel_checkpoint();`, then renders the MIR body in tail position. A
2851/// top-level `Let` chain (leading bindings) emits flat `let x = v;` lines
2852/// then recurses into the chain's final expression as a tail expr.
2853fn emit_mir_tco_body(
2854    body: &Spanned<MirExpr>,
2855    self_fn: crate::ir::FnId,
2856    params: &[(String, String)],
2857    passthrough: &std::collections::HashSet<usize>,
2858    ctx: &MirEmitCtx<'_>,
2859) -> Option<String> {
2860    let mut lines = Vec::new();
2861    lines.push("        crate::cancel_checkpoint();".to_string());
2862
2863    // Walk the leading `Let` chain as plain statements, then the final
2864    // expression as a tail expr. A named binding emits `let x = v;`; an
2865    // empty-`binding_name` binding (a discarded intermediate `Stmt::Expr`
2866    // or a `_ = effect()` discard) emits a bare `v;` statement (the value
2867    // evaluated for its effect, result dropped) — the mirror of HIR's
2868    // non-last `Stmt::Expr` arm.
2869    let mut current = body;
2870    while let MirExpr::Let(spanned_let) = &current.node {
2871        let let_node = &spanned_let.node;
2872        let value = emit_mir_expr(&let_node.value, ctx)?;
2873        if let_node.binding_name.is_empty() {
2874            lines.push(format!("        {};", value));
2875        } else {
2876            let name = aver_name_to_rust(&let_node.binding_name);
2877            lines.push(format!("        let {} = {};", name, value));
2878        }
2879        current = &let_node.body;
2880    }
2881
2882    let tail = emit_mir_tco_tail_expr(current, self_fn, params, passthrough, ctx)?;
2883    lines.push(format!("        {}", tail));
2884    Some(lines.join("\n"))
2885}
2886
2887/// Emit a MIR expression in self-TCO tail position. Self-`TailCall` →
2888/// `{ rebind; continue }`; `Match` / `IfThenElse` recurse into arms
2889/// (still tail position); anything else is a base-case value → `return
2890/// <expr>;`.
2891fn emit_mir_tco_tail_expr(
2892    expr: &Spanned<MirExpr>,
2893    self_fn: crate::ir::FnId,
2894    params: &[(String, String)],
2895    passthrough: &std::collections::HashSet<usize>,
2896    ctx: &MirEmitCtx<'_>,
2897) -> Option<String> {
2898    match &expr.node {
2899        MirExpr::TailCall(spanned_tc) => {
2900            let tc = &spanned_tc.node;
2901            if tc.target == self_fn && tc.args.len() == params.len() {
2902                emit_mir_self_tco_continue(&tc.args, params, passthrough, ctx)
2903            } else {
2904                // Tail call to a DIFFERENT fn (out of this self-loop):
2905                // emit a plain call + return. The leverage note's
2906                // module-DAG invariant means a self-TCO body's tail
2907                // calls target itself; a foreign target here is rare but
2908                // handled correctly.
2909                let name = ctx.symbol_table.fn_entry(tc.target).key.canonical();
2910                Some(format!(
2911                    "return {};",
2912                    emit_named_call(&name, &tc.args, ctx)?
2913                ))
2914            }
2915        }
2916        MirExpr::Match(spanned_match) => {
2917            emit_mir_match_with(&spanned_match.node, ctx, &|arm_body, ctx| {
2918                emit_mir_tco_tail_expr(arm_body, self_fn, params, passthrough, ctx)
2919            })
2920        }
2921        MirExpr::IfThenElse(spanned_ite) => {
2922            emit_mir_tco_if_then_else(&spanned_ite.node, self_fn, params, passthrough, ctx)
2923        }
2924        // Base-case value (or `?` / let-bound value): `return <expr>;`.
2925        _ => Some(format!("return {};", emit_mir_value_return(expr, ctx)?)),
2926    }
2927}
2928
2929/// Render a MIR `IfThenElse` in TCO tail position — both branches stay
2930/// in tail position (recurse). Reuses the condition canonicalization
2931/// from [`emit_mir_if_then_else`] would be ideal, but that helper
2932/// renders branches as values; here branches are tail exprs, so we
2933/// re-derive the condition the same way (the MIR `bool_match_to_if` pass
2934/// is the only producer).
2935fn emit_mir_tco_if_then_else(
2936    ite: &crate::ir::mir::MirIfThenElse,
2937    self_fn: crate::ir::FnId,
2938    params: &[(String, String)],
2939    passthrough: &std::collections::HashSet<usize>,
2940    ctx: &MirEmitCtx<'_>,
2941) -> Option<String> {
2942    let (cond, then_src, else_src) = mir_if_cond_and_branches(ite, ctx)?;
2943    let then_branch = emit_mir_tco_tail_expr(then_src, self_fn, params, passthrough, ctx)?;
2944    let else_branch = emit_mir_tco_tail_expr(else_src, self_fn, params, passthrough, ctx)?;
2945    Some(format!(
2946        "if {} {{ {} }} else {{ {} }}",
2947        cond, then_branch, else_branch
2948    ))
2949}
2950
2951/// Render a value expression for a `return` in a TCO / trampoline base
2952/// case. Mirror of `emit_mir_expr` + the owning-position `maybe_clone`,
2953/// plus the HIR `emit_tco_expr` `_` arm's bare-rc-ident deref-clone:
2954/// returning a pass-through param (Arc<T> / &T) needs `(*x).clone()` to
2955/// yield an owned `T`.
2956fn emit_mir_value_return(expr: &Spanned<MirExpr>, ctx: &MirEmitCtx<'_>) -> Option<String> {
2957    // Int unboxing: when the fn's return type is bare `i64`, the base-case
2958    // value must be emitted bare too (a bare `Local`, a bare literal, or
2959    // bare arithmetic) so the returned expression's type is `i64`. The
2960    // analysis only set `bare_return` when EVERY tail leaf is bare-eligible
2961    // (`tail_value_is_bare`), so this path renders.
2962    if ctx.bare.bare_return
2963        && mir_expr_is_bare_i64(expr, ctx)
2964        && let Some(bare) = emit_bare_i64(expr)
2965    {
2966        return Some(bare);
2967    }
2968    // ETAP-2 SLICE 1: the boxed-return tail boundary (defects Q5 / subj_ret)
2969    // is now an EXPLICIT `Box` node the `bare_i64_rewrite` pass inserted
2970    // around each bare leaf reaching a boxed return. `emit_mir_expr` lowers
2971    // those `Box` nodes, so the default path renders the boxed return
2972    // correctly without a codegen-side boxing pass.
2973    let code = emit_mir_expr(expr, ctx)?;
2974    Some(mir_maybe_clone(code, &expr.node, ctx))
2975}
2976
2977/// Emit the self-TCO `{ rebind; continue }` block from the tail-call
2978/// args, using the always-snapshot rule. Pass-through (rc) params and
2979/// identity rebinds (`arg == param`) are skipped; every other rebound
2980/// param gets a `let __tcoN = <arg>;` snapshot first (avoiding
2981/// read-after-write clobber), then `param = __tcoN;` in order, then
2982/// `continue;`.
2983fn emit_mir_self_tco_continue(
2984    args: &[Spanned<MirExpr>],
2985    params: &[(String, String)],
2986    passthrough: &std::collections::HashSet<usize>,
2987    ctx: &MirEmitCtx<'_>,
2988) -> Option<String> {
2989    let mut arg_strs = Vec::with_capacity(args.len());
2990    for (i, a) in args.iter().enumerate() {
2991        // ETAP-2 SLICE 1: the rebind-value representation boundary is now
2992        // EXPLICIT — the `bare_i64_rewrite` pass already wrapped each
2993        // self-tail-call arg for the self-fn's i-th param representation
2994        // (`Box` for a raw arg into a boxed loop var, `Unbox` for a boxed
2995        // arg into a bare `i64` loop var). Codegen renders the arg:
2996        //   - bare param (`mut p: i64`): a raw leaf / compound / literal
2997        //     renders bare via `emit_bare_i64`; an `Unbox`-wrapped boxed arg
2998        //     falls through to `emit_mir_expr` (lowering `Unbox`).
2999        //   - boxed param: `emit_mir_expr` (lowering any `Box` node), then
3000        //     the clone policy.
3001        if ctx.bare.param_is_bare(i) {
3002            let rendered = emit_bare_i64(a).or_else(|| emit_mir_expr(a, ctx));
3003            arg_strs.push(rendered?);
3004        } else {
3005            let code = emit_mir_expr(a, ctx)?;
3006            arg_strs.push(mir_clone_arg(code, &a.node, ctx));
3007        }
3008    }
3009
3010    // Which positions are actually rebound (non-passthrough, non-identity)?
3011    let mut rebind: Vec<bool> = vec![false; params.len()];
3012    for (i, (name, _)) in params.iter().enumerate() {
3013        if passthrough.contains(&i) {
3014            continue;
3015        }
3016        if arg_strs[i] == aver_name_to_rust(name) {
3017            continue; // identity — no-op
3018        }
3019        rebind[i] = true;
3020    }
3021
3022    let mut lines = Vec::new();
3023    lines.push("{".to_string());
3024    // Phase 1: snapshot ALL rebound args into temps (always-snapshot).
3025    for (i, arg_str) in arg_strs.iter().enumerate() {
3026        if rebind[i] {
3027            lines.push(format!("            let __tco{} = {};", i, arg_str));
3028        }
3029    }
3030    // Phase 2: assign temps back to params, in order.
3031    for (i, (name, _)) in params.iter().enumerate() {
3032        if rebind[i] {
3033            lines.push(format!(
3034                "            {} = __tco{};",
3035                aver_name_to_rust(name),
3036                i
3037            ));
3038        }
3039    }
3040    lines.push("            continue;".to_string());
3041    lines.push("        }".to_string());
3042    Some(lines.join("\n"))
3043}
3044
3045/// Recompute the canonicalized condition + the (possibly swapped) tail
3046/// branches for a MIR `IfThenElse`. Shared by the value emitter
3047/// ([`emit_mir_if_then_else`]) and the TCO emitter — extracted so the
3048/// condition-rewrite logic lives in one place.
3049fn mir_if_cond_and_branches<'a>(
3050    ite: &'a crate::ir::mir::MirIfThenElse,
3051    ctx: &MirEmitCtx<'_>,
3052) -> Option<(String, &'a Spanned<MirExpr>, &'a Spanned<MirExpr>)> {
3053    let canonical_compare = |op: BinOp| -> Option<(&'static str, bool)> {
3054        match op {
3055            BinOp::Eq => Some(("==", false)),
3056            BinOp::Neq => Some(("==", true)),
3057            BinOp::Lt => Some(("<", false)),
3058            BinOp::Gte => Some(("<", true)),
3059            BinOp::Gt => Some((">", false)),
3060            BinOp::Lte => Some((">", true)),
3061            BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div => None,
3062        }
3063    };
3064    match &ite.cond.node {
3065        MirExpr::BinOp(spanned_binop) if canonical_compare(spanned_binop.node.op).is_some() => {
3066            let bop = &spanned_binop.node;
3067            let (op_str, invert) = canonical_compare(bop.op).expect("checked by guard");
3068            // Int unboxing: a comparison between two bare `i64` operands
3069            // emits raw `i64` operands (a bare counter `i == 0` compares
3070            // `i64 == 0i64`, not `i64 == AverInt`). Mirror of the
3071            // comparison bare path in `emit_mir_expr`.
3072            let (l, r) = if mir_expr_is_bare_i64(&bop.lhs, ctx)
3073                && mir_expr_is_bare_i64(&bop.rhs, ctx)
3074                && let Some(lb) = emit_bare_i64(&bop.lhs)
3075                && let Some(rb) = emit_bare_i64(&bop.rhs)
3076            {
3077                (lb, rb)
3078            } else {
3079                (emit_mir_expr(&bop.lhs, ctx)?, emit_mir_expr(&bop.rhs, ctx)?)
3080            };
3081            let cond = format!("({} {} {})", l, op_str, r);
3082            if invert {
3083                Some((cond, &ite.else_branch, &ite.then_branch))
3084            } else {
3085                Some((cond, &ite.then_branch, &ite.else_branch))
3086            }
3087        }
3088        _ => {
3089            let cond = emit_mir_expr(&ite.cond, ctx)?;
3090            Some((cond, &ite.then_branch, &ite.else_branch))
3091        }
3092    }
3093}
3094
3095// ── mutual-recursion trampoline from MIR ────────────────────────────────
3096
3097/// Emit a mutual-TCO block from MIR: a state enum (one variant per
3098/// member, payload = non-rc param values), a trampoline dispatch loop
3099/// (member-`TailCall` bounces to a new enum variant, a value `return`s),
3100/// and thin wrapper fns. The member bodies are walked from MIR
3101/// (`MirFn.body`).
3102///
3103/// `group_fns` is the SCC (from the AST-based `find_mutual_tco_groups`);
3104/// `mir_fns` are the matching `MirFn`s in the same order. Returns `None`
3105/// (→ the caller emits a hard codegen diagnostic for the whole block)
3106/// when any member body can't render — the block is all-or-nothing
3107/// because the members share one trampoline.
3108#[allow(clippy::too_many_arguments)]
3109pub(super) fn emit_mir_mutual_tco_block(
3110    group_id: usize,
3111    group_fns: &[&crate::ast::FnDef],
3112    mir_fns: &[&crate::ir::mir::MirFn],
3113    resolved_fns: &[&crate::ir::hir::ResolvedFnDef],
3114    ctx: &CodegenContext,
3115    scope: Option<&str>,
3116    visibility: &str,
3117) -> Option<String> {
3118    use super::toplevel::{compute_rc_params, fn_name_to_variant, rc_param_names};
3119
3120    if group_fns.is_empty() {
3121        return None;
3122    }
3123    let enum_name = format!("__MutualTco{}", group_id);
3124    let trampoline_name = format!("__mutual_tco_trampoline_{}", group_id);
3125    let ret_type = if group_fns[0].return_type.is_empty() {
3126        "()".to_string()
3127    } else {
3128        super::types::type_annotation_to_rust(&group_fns[0].return_type)
3129    };
3130
3131    let member_fn_ids: HashSet<crate::ir::FnId> = mir_fns.iter().map(|m| m.fn_id).collect();
3132    let rc_indices = compute_rc_params(group_fns, ctx);
3133    let rc_names = rc_param_names(&group_fns[0].params, &rc_indices);
3134
3135    // Render every member's trampoline-arm body FIRST — bail before
3136    // emitting anything if a member can't render (all-or-nothing block).
3137    let mut arm_bodies: Vec<String> = Vec::with_capacity(group_fns.len());
3138    for (i, mir_fn) in mir_fns.iter().enumerate() {
3139        // Trampoline arm policy: no borrow-by-default, rc params wrapped.
3140        // NB: own_param graduation is deliberately NOT applied to the
3141        // mutual-TCO path — graduating a collection param to owned here
3142        // would require coordinating the trampoline enum payload type, the
3143        // wrapper signatures, and the arg passing across every member, a
3144        // far larger and riskier change for no measured win (the perf
3145        // flagship `vector_ops` is self-TCO, handled in `emit_mir_tco_fn`).
3146        // Keeping borrow-by-default is always sound — not graduating never
3147        // skips a clone.
3148        let mut policy = MirFnEmitPolicy::from_resolved(resolved_fns[i], scope, false);
3149        policy.rc_wrapped = rc_names.clone();
3150        let arm_ctx = MirEmitCtx::for_fn(ctx, &policy);
3151        let body = emit_mir_trampoline_body(
3152            &mir_fn.body,
3153            &member_fn_ids,
3154            &enum_name,
3155            &rc_names,
3156            &arm_ctx,
3157        )?;
3158        arm_bodies.push(body);
3159    }
3160
3161    let mut sections = Vec::new();
3162
3163    // 1. Enum — one variant per member, payload = non-rc param types.
3164    let mut enum_lines = Vec::new();
3165    enum_lines.push("#[allow(non_camel_case_types)]".to_string());
3166    enum_lines.push(format!("enum {} {{", enum_name));
3167    for fd in group_fns {
3168        let variant = fn_name_to_variant(&fd.name);
3169        let param_types: Vec<String> = fd
3170            .params
3171            .iter()
3172            .filter(|(name, _)| !rc_names.contains(name))
3173            .map(|(_, ty)| super::types::type_annotation_to_rust(ty))
3174            .collect();
3175        if param_types.is_empty() {
3176            enum_lines.push(format!("    {},", variant));
3177        } else {
3178            enum_lines.push(format!("    {}({}),", variant, param_types.join(", ")));
3179        }
3180    }
3181    enum_lines.push("}".to_string());
3182    sections.push(enum_lines.join("\n"));
3183
3184    // 2. Trampoline fn — rc params are extra `&T` args.
3185    let rc_extra_params: String = mutual_rc_param_sig(group_fns[0], &rc_names);
3186    let mut tramp_lines = Vec::new();
3187    tramp_lines.push(format!(
3188        "fn {}(mut __state: {}{}) -> {} {{",
3189        trampoline_name, enum_name, rc_extra_params, ret_type
3190    ));
3191    tramp_lines.push("    loop {".to_string());
3192    tramp_lines.push("        __state = match __state {".to_string());
3193    for (fd, arm_body) in group_fns.iter().zip(&arm_bodies) {
3194        let variant = fn_name_to_variant(&fd.name);
3195        let param_bindings: Vec<String> = fd
3196            .params
3197            .iter()
3198            .filter(|(name, _)| !rc_names.contains(name))
3199            .map(|(name, _)| format!("mut {}", aver_name_to_rust(name)))
3200            .collect();
3201        let binding = if param_bindings.is_empty() {
3202            format!("{}::{}", enum_name, variant)
3203        } else {
3204            format!("{}::{}({})", enum_name, variant, param_bindings.join(", "))
3205        };
3206        tramp_lines.push(format!("            {} => {{", binding));
3207        tramp_lines.push(arm_body.clone());
3208        tramp_lines.push("            }".to_string());
3209    }
3210    tramp_lines.push("        };".to_string());
3211    tramp_lines.push("    }".to_string());
3212    tramp_lines.push("}".to_string());
3213    sections.push(tramp_lines.join("\n"));
3214
3215    // 3. Wrapper fns — borrow-by-default params, clone borrowed into the
3216    //    enum variant, pass rc params as `&T` extra trampoline args.
3217    for fd in group_fns {
3218        let fn_name = aver_name_to_rust(&fd.name);
3219        let variant = fn_name_to_variant(&fd.name);
3220        let params = super::toplevel::emit_fn_params_pub(&fd.params, false);
3221        let variant_arg_names: Vec<String> = fd
3222            .params
3223            .iter()
3224            .filter(|(name, _)| !rc_names.contains(name))
3225            .map(|(name, type_ann)| {
3226                let rust_name = aver_name_to_rust(name);
3227                let ty = crate::types::parse_type_str(type_ann);
3228                if should_borrow_param(&ty) {
3229                    format!("{}.clone()", rust_name)
3230                } else {
3231                    rust_name
3232                }
3233            })
3234            .collect();
3235        let variant_call = if variant_arg_names.is_empty() {
3236            format!("{}::{}", enum_name, variant)
3237        } else {
3238            format!(
3239                "{}::{}({})",
3240                enum_name,
3241                variant,
3242                variant_arg_names.join(", ")
3243            )
3244        };
3245        let rc_extra_args: String = {
3246            let parts: Vec<String> = fd
3247                .params
3248                .iter()
3249                .filter(|(name, _)| rc_names.contains(name))
3250                .map(|(name, _)| format!("&{}", aver_name_to_rust(name)))
3251                .collect();
3252            if parts.is_empty() {
3253                String::new()
3254            } else {
3255                format!(", {}", parts.join(", "))
3256            }
3257        };
3258        let mut wrapper = Vec::new();
3259        if let Some(desc) = &fd.desc {
3260            wrapper.push(format!("/// {}", desc));
3261        }
3262        wrapper.push(format!(
3263            "{}fn {}({}) -> {} {{",
3264            visibility, fn_name, params, ret_type
3265        ));
3266        wrapper.push(format!(
3267            "    {}({}{})",
3268            trampoline_name, variant_call, rc_extra_args
3269        ));
3270        wrapper.push("}".to_string());
3271        sections.push(wrapper.join("\n"));
3272    }
3273
3274    Some(sections.join("\n\n"))
3275}
3276
3277/// Build the rc-param extra `&T` argument list for the mutual
3278/// trampoline signature (`, x: &T, y: &U`), or empty when no rc params.
3279fn mutual_rc_param_sig(fd: &crate::ast::FnDef, rc_names: &HashSet<String>) -> String {
3280    if rc_names.is_empty() {
3281        return String::new();
3282    }
3283    let parts: Vec<String> = fd
3284        .params
3285        .iter()
3286        .filter(|(name, _)| rc_names.contains(name))
3287        .map(|(name, ty)| {
3288            format!(
3289                "{}: &{}",
3290                aver_name_to_rust(name),
3291                super::types::type_annotation_to_rust(ty)
3292            )
3293        })
3294        .collect();
3295    if parts.is_empty() {
3296        String::new()
3297    } else {
3298        format!(", {}", parts.join(", "))
3299    }
3300}
3301
3302/// Emit one trampoline arm body from MIR: leads with
3303/// `cancel_checkpoint();`, walks the leading `Let` chain as plain `let`
3304/// statements, then renders the final expression in trampoline tail
3305/// position (member-`TailCall` → enum variant bounce, value → `return`).
3306fn emit_mir_trampoline_body(
3307    body: &Spanned<MirExpr>,
3308    members: &HashSet<crate::ir::FnId>,
3309    enum_name: &str,
3310    rc_names: &HashSet<String>,
3311    ctx: &MirEmitCtx<'_>,
3312) -> Option<String> {
3313    let mut lines = Vec::new();
3314    lines.push("                crate::cancel_checkpoint();".to_string());
3315
3316    let mut current = body;
3317    while let MirExpr::Let(spanned_let) = &current.node {
3318        let let_node = &spanned_let.node;
3319        let value = emit_mir_expr(&let_node.value, ctx)?;
3320        if let_node.binding_name.is_empty() {
3321            // Discarded intermediate (`Stmt::Expr` / `_ = effect()`)
3322            // — bare statement, result dropped.
3323            lines.push(format!("                {};", value));
3324        } else {
3325            let name = aver_name_to_rust(&let_node.binding_name);
3326            lines.push(format!("                let {} = {};", name, value));
3327        }
3328        current = &let_node.body;
3329    }
3330
3331    let tail = emit_mir_trampoline_tail_expr(current, members, enum_name, rc_names, ctx)?;
3332    lines.push(format!("                {}", tail));
3333    Some(lines.join("\n"))
3334}
3335
3336/// Render a MIR expression in trampoline tail position. A `TailCall` to
3337/// a group member becomes an enum-variant bounce (excluding rc args); a
3338/// `TailCall` to a non-member, or any base-case value, becomes a
3339/// `return`. `Match` / `IfThenElse` recurse (still tail position).
3340fn emit_mir_trampoline_tail_expr(
3341    expr: &Spanned<MirExpr>,
3342    members: &HashSet<crate::ir::FnId>,
3343    enum_name: &str,
3344    rc_names: &HashSet<String>,
3345    ctx: &MirEmitCtx<'_>,
3346) -> Option<String> {
3347    match &expr.node {
3348        MirExpr::TailCall(spanned_tc) => {
3349            let tc = &spanned_tc.node;
3350            if members.contains(&tc.target) {
3351                // Bounce → enum variant for the TARGET member, excluding
3352                // its rc (pass-through) args. The target's param names
3353                // drive which positional args are rc — read them off the
3354                // target fn entry's source-level signature so the rc
3355                // filter matches the target, not the caller.
3356                let target_name = ctx.symbol_table.fn_entry(tc.target).key.name.clone();
3357                let variant = super::toplevel::fn_name_to_variant(&target_name);
3358                let mut arg_strs = Vec::new();
3359                for a in &tc.args {
3360                    // Skip rc args by the arg's source-level name: a
3361                    // pass-through arg is a bare local read whose name is
3362                    // in `rc_names` (shared across the SCC by name+type).
3363                    if let Some(local) = local_of(&a.node)
3364                        && rc_names.contains(&local.name)
3365                    {
3366                        continue;
3367                    }
3368                    arg_strs.push(mir_clone_arg(emit_mir_expr(a, ctx)?, &a.node, ctx));
3369                }
3370                if arg_strs.is_empty() {
3371                    Some(format!("{}::{}", enum_name, variant))
3372                } else {
3373                    Some(format!(
3374                        "{}::{}({})",
3375                        enum_name,
3376                        variant,
3377                        arg_strs.join(", ")
3378                    ))
3379                }
3380            } else {
3381                let name = ctx.symbol_table.fn_entry(tc.target).key.canonical();
3382                Some(format!("return {}", emit_named_call(&name, &tc.args, ctx)?))
3383            }
3384        }
3385        MirExpr::Match(spanned_match) => {
3386            emit_mir_match_with(&spanned_match.node, ctx, &|arm_body, ctx| {
3387                emit_mir_trampoline_tail_expr(arm_body, members, enum_name, rc_names, ctx)
3388            })
3389        }
3390        MirExpr::IfThenElse(spanned_ite) => {
3391            let (cond, then_src, else_src) = mir_if_cond_and_branches(&spanned_ite.node, ctx)?;
3392            let t = emit_mir_trampoline_tail_expr(then_src, members, enum_name, rc_names, ctx)?;
3393            let e = emit_mir_trampoline_tail_expr(else_src, members, enum_name, rc_names, ctx)?;
3394            Some(format!("if {} {{ {} }} else {{ {} }}", cond, t, e))
3395        }
3396        _ => Some(format!("return {}", emit_mir_value_return(expr, ctx)?)),
3397    }
3398}
3399
3400// ── MIR-side borrow / clone machinery ───────────────────────────────────
3401//
3402// Mirror of the HIR walker's `expr_skip_clone` / `maybe_clone` /
3403// `clone_arg` / `borrow_arg` (emit_ctx.rs + expr.rs), keyed off
3404// `MirLocal` (slot + `last_use` + source `name`) instead of
3405// `ResolvedExpr::Resolved`. The covered arms route every arg /
3406// field / element / base through these so their output matches HIR
3407// byte-for-byte on the borrow decisions. When the walker has no
3408// `CodegenContext` (coverage path), the local-name lookups still
3409// work off the (empty) policy fields and degrade to the
3410// conservative `last_use ? move : clone` shape — which is fine
3411// because the coverage walk only inspects `Some` vs `None`.
3412
3413/// `&MirExpr` reference to a source-named local, if any. Synthetic
3414/// locals (empty name) are excluded — the walker already bails on
3415/// them upstream.
3416fn local_of(expr: &MirExpr) -> Option<&MirLocal> {
3417    match expr {
3418        MirExpr::Local(l) if !l.node.name.is_empty() => Some(&l.node),
3419        _ => None,
3420    }
3421}
3422
3423/// Should `.clone()` be skipped for this MIR expr? Mirror of HIR's
3424/// `expr_skip_clone`. A local read skips clone on its last use or
3425/// when Copy; `rc_wrapped` / `borrowed_params` never skip (they
3426/// need the special clone paths in `mir_maybe_clone`). A name that
3427/// isn't a known local is treated as a global / namespace and
3428/// always skips. Non-locals (literals, nested exprs) never need a
3429/// clone wrapper here.
3430fn mir_expr_skip_clone(expr: &MirExpr, ctx: &MirEmitCtx<'_>) -> bool {
3431    match local_of(expr) {
3432        Some(local) => {
3433            let name = local.name.as_str();
3434            if ctx.is_rc_wrapped(name) || ctx.is_borrowed_param(name) {
3435                return false;
3436            }
3437            local.last_use || ctx.is_copy(name)
3438        }
3439        None => true,
3440    }
3441}
3442
3443/// Mirror of HIR's `maybe_clone`: wrap a local read in the right
3444/// clone shape for an owning position (arg, return, ctor field,
3445/// tuple / list / map element). `code` is the already-emitted
3446/// expression text for `expr`.
3447fn mir_maybe_clone(code: String, expr: &MirExpr, ctx: &MirEmitCtx<'_>) -> String {
3448    if let Some(local) = local_of(expr) {
3449        let name = local.name.as_str();
3450        return if mir_expr_skip_clone(expr, ctx) {
3451            code
3452        } else if ctx.is_rc_wrapped(name) {
3453            // Pass-through param (Rc<T> / &T): deref then clone.
3454            format!("(*{}).clone()", code)
3455        } else {
3456            // Borrowed param or plain owned local: clone to own.
3457            format!("{}.clone()", code)
3458        };
3459    }
3460    // Field access (`Project`): emit_mir_expr produces `base.field`
3461    // without clone; clone here for ownership. Matches HIR's
3462    // `maybe_clone` `Attr` arm — builtin namespace access never
3463    // reaches the MIR walker (it lowers to a `Call`), so no
3464    // namespace special-case is needed.
3465    if matches!(expr, MirExpr::Project(_)) {
3466        return format!("{}.clone()", code);
3467    }
3468    code
3469}
3470
3471/// Mirror of HIR's `clone_arg` (`clone_arg_with_options`): emit an
3472/// expression as an owning argument. HIR elides the `.clone()` on a
3473/// record field access whose field type is Copy
3474/// (`attr_result_is_copy`); Wave 4 ports that elision here via
3475/// [`mir_attr_result_is_copy`], reading the base local's stamped type.
3476/// For the common case (non-`Project` args) this delegates to
3477/// `mir_maybe_clone`, matching HIR exactly.
3478fn mir_clone_arg(code: String, expr: &MirExpr, ctx: &MirEmitCtx<'_>) -> String {
3479    if let MirExpr::Project(p) = expr
3480        && mir_attr_result_is_copy(&p.node, ctx)
3481    {
3482        // Copy-typed record field: HIR returns the bare field access
3483        // (no `.clone()`). Mirror that.
3484        return code;
3485    }
3486    mir_maybe_clone(code, expr, ctx)
3487}
3488
3489/// Mirror of HIR's `attr_result_is_copy` over a `MirProject`: the
3490/// field access result is Copy iff the projection base is a
3491/// `Type::Named` local and the projected field's declared type is a
3492/// Copy type. Reads the base's type from `local_types` (params + let
3493/// bindings — the MIR walker has richer coverage than HIR here, but the
3494/// guard `obj is a Named local` is the same), then defers to the shared
3495/// `record_field_is_copy` for the field-type lookup. Returns `false`
3496/// (HIR's conservative "needs a clone") when there's no `CodegenContext`
3497/// (coverage path) or the base isn't a Named local.
3498fn mir_attr_result_is_copy(proj: &crate::ir::mir::MirProject, ctx: &MirEmitCtx<'_>) -> bool {
3499    let Some(cg) = ctx.codegen else {
3500        return false;
3501    };
3502    let Some(local) = local_of(&proj.base.node) else {
3503        return false;
3504    };
3505    let Some(named_ty) = ctx
3506        .local_types
3507        .get(&local.name)
3508        .filter(|t| matches!(t, Type::Named { .. }))
3509    else {
3510        return false;
3511    };
3512    super::expr::record_field_is_copy(named_ty, &proj.field, cg)
3513}
3514
3515/// Emit a named user-function call (`Call(Fn)` /
3516/// outside-loop `TailCall`). Mirror of HIR's
3517/// `emit_named_function_call`: per-arg `borrow_arg` (when the
3518/// callee's i-th param is borrowed-by-default `&T`) or `clone_arg`
3519/// (owned), and `resolve_module_call` head path-mangling.
3520///
3521/// `callee_borrow_mask` needs the full `CodegenContext`; on the
3522/// coverage path (`codegen == None`) there's no mask, so every arg
3523/// rides `clone_arg` (conservative — coverage only reads Some/None,
3524/// and the production parity gate never runs without a ctx).
3525fn emit_named_call(name: &str, args: &[Spanned<MirExpr>], ctx: &MirEmitCtx<'_>) -> Option<String> {
3526    emit_named_call_to(name, None, args, ctx)
3527}
3528
3529/// Like [`emit_named_call`] but threads the callee's `FnId` so the Int
3530/// unboxing facts can convert each arg at the call boundary: when the
3531/// callee's i-th param is bare `i64`, the arg is emitted as a raw `i64`
3532/// (an already-bare arg directly; a boxed `AverInt` arg narrowed via a
3533/// checked `to_i64`). A `None` `callee` (a foreign tail-call helper with
3534/// no facts) keeps every arg boxed.
3535fn emit_named_call_to(
3536    name: &str,
3537    callee: Option<crate::ir::FnId>,
3538    args: &[Spanned<MirExpr>],
3539    ctx: &MirEmitCtx<'_>,
3540) -> Option<String> {
3541    let borrow_mask = match ctx.codegen {
3542        Some(cg) => callee_borrow_mask(name, args.len(), cg),
3543        None => vec![false; args.len()],
3544    };
3545    // ETAP-2 SLICE 1: the call-arg representation BOUNDARY (the `from_i64` /
3546    // `to_i64` CONVERSION for a value whose representation differs from the
3547    // param's) is now an EXPLICIT `Box`/`Unbox` node the `bare_i64_rewrite`
3548    // pass inserted, lowered by `emit_mir_expr`. Codegen no longer decides
3549    // those conversions here. It still RENDERS a bare-param arg raw — that
3550    // is representation, not a boundary: a literal / bare leaf / bare
3551    // arithmetic tree at a `i64` param emits its native `i64` form
3552    // (`emit_bare_i64`), and an `Unbox`-wrapped boxed arg falls through to
3553    // `emit_mir_expr` (which lowers the `Unbox`).
3554    let callee_bare = callee.and_then(|id| ctx.codegen.and_then(|cg| cg.bare_i64.for_fn(id)));
3555    let mut arg_strs = Vec::with_capacity(args.len());
3556    for (i, a) in args.iter().enumerate() {
3557        if callee_bare.is_some_and(|f| f.param_is_bare(i)) {
3558            // Bare `i64` param: render the arg in its raw form. A raw leaf /
3559            // compound / literal renders via `emit_bare_i64`; anything else
3560            // (an `Unbox` node the rewrite inserted for a genuinely-boxed
3561            // arg) falls through to `emit_mir_expr`.
3562            let rendered = emit_bare_i64(a).or_else(|| emit_mir_expr(a, ctx));
3563            arg_strs.push(rendered?);
3564            continue;
3565        }
3566        let code = emit_mir_expr(a, ctx)?;
3567        let s = if borrow_mask.get(i).copied().unwrap_or(false) {
3568            mir_borrow_arg(code, &a.node, ctx)
3569        } else {
3570            mir_clone_arg(code, &a.node, ctx)
3571        };
3572        arg_strs.push(s);
3573    }
3574    if let Some((prefix, suffix)) = resolve_module_call(name, ctx.module_prefixes) {
3575        Some(format!(
3576            "{}::{}({})",
3577            module_prefix_to_rust_path(prefix),
3578            aver_name_to_rust(suffix),
3579            arg_strs.join(", ")
3580        ))
3581    } else {
3582        Some(format!(
3583            "{}({})",
3584            aver_name_to_rust(name),
3585            arg_strs.join(", ")
3586        ))
3587    }
3588}
3589
3590/// Mirror of HIR's `borrow_arg`: emit an expression for passing to
3591/// a user fn whose param is `&T`. `code` is the already-emitted
3592/// text for `expr`.
3593fn mir_borrow_arg(code: String, expr: &MirExpr, ctx: &MirEmitCtx<'_>) -> String {
3594    let Some(local) = local_of(expr) else {
3595        // Complex expression: borrow the temporary.
3596        return format!("&{}", code);
3597    };
3598    let name = local.name.as_str();
3599    if ctx.is_copy(name) {
3600        // Copy type: by value.
3601        code
3602    } else if matches!(ctx.local_types.get(name), Some(Type::Str)) {
3603        // AverStr (Rc<str>): by value; last-use moves, else clone.
3604        if local.last_use {
3605            code
3606        } else if ctx.is_rc_wrapped(name) {
3607            format!("(*{}).clone()", code)
3608        } else {
3609            format!("{}.clone()", code)
3610        }
3611    } else if ctx.is_borrowed_param(name) {
3612        // Already `&T` — pass directly.
3613        code
3614    } else if ctx.is_rc_wrapped(name) {
3615        // Pass-through TCO param: deref to `&T`.
3616        format!("&*{}", code)
3617    } else {
3618        // Owned local: borrow it (last-use and non-last-use both
3619        // emit `&code` in the HIR walker).
3620        format!("&{}", code)
3621    }
3622}
3623
3624// ── Wave 3a: PURE builtin calls + deforestation intrinsics ──────────────
3625//
3626// Mirror of the HIR oracle `emit_builtin_call` / `emit_builtin_call_inner`
3627// (`builtins.rs`) for the ~88 PURE builtins (Result / Option / Int /
3628// Float / String / List / Map / Vector / Bool / Char / Byte). The
3629// EFFECTFUL families (Args / Console / Http / HttpServer / Disk / Env /
3630// Random / SelfHostRuntime / Tcp / Terminal / Time) are split off at the
3631// `Call(Builtin)` arm to `emit_mir_effectful_builtin_call` (Wave 3b,
3632// below) — they are NOT handled here.
3633//
3634// Each arm copies its HIR sibling's shape verbatim, substituting:
3635//   `emit_arg(i)`                  → `emit_mir_expr(&args[i], ctx)?`
3636//   `clone_arg(&args[i].node, …)`  → `mir_clone_arg(emit_mir_expr(…)?, …)`
3637//   `emit_str_arg_or_deref(…)`     → `mir_str_arg_or_deref(&args[i], ctx)?`
3638// then runs the `builtin_needs_str_conversion` `.into_aver()` post-step
3639// that `emit_builtin_call` applies (Int.mod, Int/Float.fromString,
3640// String.* returning String, Char.fromCode, Byte.*). The byte-parity
3641// gate is the safety net: any arm whose output diverges from HIR blocks
3642// graduation and the fn falls back to HIR.
3643
3644/// Mirror of HIR's `emit_str_arg_or_deref`: emit a string-accepting
3645/// argument (`String.contains` / `startsWith` / `endsWith`) as a bare
3646/// `"foo"` literal (no allocation) or, for any other expression, the
3647/// deref form `&*code`. Returns `None` when the inner expr can't emit.
3648fn mir_str_arg_or_deref(expr: &Spanned<MirExpr>, ctx: &MirEmitCtx<'_>) -> Option<String> {
3649    if let MirExpr::Literal(lit) = &expr.node
3650        && let crate::ast::Literal::Str(s) = &lit.node
3651    {
3652        return Some(format!("{:?}", s));
3653    }
3654    let code = emit_mir_expr(expr, ctx)?;
3655    Some(format!("&*{}", code))
3656}
3657
3658/// Resolve a nested expression that is itself a `Call(Builtin(id))` to
3659/// its canonical dotted name + arg slice. MIR lowering wipes the
3660/// syntactic shape the HIR `ResolvedLeafOp` classifiers key off
3661/// (`Option.withDefault` / `Result.withDefault` / `Vector.get` over a
3662/// nested builtin), so the fusion recognizers
3663/// ([`try_emit_mir_fusion`]) re-match the pattern over this resolved
3664/// `(name, args)` form instead. Returns `None` for any non-`Call`, a
3665/// non-`Builtin` callee, or an out-of-range / unresolved `BuiltinId`
3666/// (the same defensive fallthrough the `Call(Builtin)` arm takes).
3667fn mir_builtin_call_parts<'a, 'c>(
3668    expr: &'a MirExpr,
3669    ctx: &MirEmitCtx<'c>,
3670) -> Option<(&'c str, &'a [Spanned<MirExpr>])> {
3671    let MirExpr::Call(spanned_call) = expr else {
3672        return None;
3673    };
3674    let call = &spanned_call.node;
3675    let MirCallee::Builtin(id) = &call.callee else {
3676        return None;
3677    };
3678    let name = ctx.mir_builtins.get(id.0 as usize)?.as_str();
3679    Some((name, &call.args))
3680}
3681
3682/// Two MIR exprs that name the SAME source local. Used by the
3683/// `VectorSetOrDefaultSameVector` fusion's same-vector guard (HIR's
3684/// `default_expr.node != inner_args[0].node` check). Compares by slot,
3685/// not the whole `MirLocal`, because the two reads can carry different
3686/// `last_use` flags (the outer default read is typically the last use
3687/// of the slot, the inner `Vector.set` read is not) yet still denote
3688/// the same vector. Synthetic / unnamed locals never match.
3689fn mir_same_local(a: &MirExpr, b: &MirExpr) -> bool {
3690    match (local_of(a), local_of(b)) {
3691        (Some(la), Some(lb)) => la.slot == lb.slot,
3692        _ => false,
3693    }
3694}
3695
3696/// Re-recognize the three codegen FUSIONS the HIR walker performs over
3697/// pre-lowering `ResolvedLeafOp` shapes but MIR lowering flattens into
3698/// nested builtin `Call`s. The HIR classifiers
3699/// (`classify_vector_set_or_default` / `classify_int_mod_or_default` /
3700/// `classify_list_index_get` in `ir::hir::classify`) match the
3701/// syntactic AST; here we re-match the equivalent `MirExpr::Call`
3702/// nesting and emit the EXACT fused Rust form the HIR `ResolvedLeafOp`
3703/// emitter (`emit_leaf_op_with_options`, `expr.rs`) produces, so the
3704/// byte-parity gate graduates these fns instead of falling back to the
3705/// (un-fused, slower) generic builtin emit. Returns `None` when the
3706/// outer call isn't one of the three fusion heads or the nested shape
3707/// doesn't match — the caller then emits the generic builtin form.
3708fn try_emit_mir_fusion(
3709    name: &str,
3710    args: &[Spanned<MirExpr>],
3711    ctx: &MirEmitCtx<'_>,
3712) -> Option<String> {
3713    match name {
3714        // Fusion #1: `Option.withDefault(Vector.set(v, i, x), v)` where
3715        // both `v` are the SAME local → in-place bounds-checked set.
3716        // HIR: `ResolvedLeafOp::VectorSetOrDefaultSameVector`.
3717        "Option.withDefault" if args.len() == 2 => {
3718            let (inner_name, inner_args) = mir_builtin_call_parts(&args[0].node, ctx)?;
3719            if inner_name != "Vector.set" || inner_args.len() != 3 {
3720                return None;
3721            }
3722            // Same-vector guard: the default arm (`args[1]`) must be the
3723            // same local as the vector being set (`inner_args[0]`).
3724            if !mir_same_local(&args[1].node, &inner_args[0].node) {
3725                return None;
3726            }
3727            // own_param self-keep collapse (the perf flagship): when the
3728            // set-target is an OWNED collection param (its `aliased_slots`
3729            // bit was cleared by `own_param` → in `ctx.owned_params`) and
3730            // the slot is dead after this fusion, MOVE it into `__vec`
3731            // instead of cloning. The fusion consumes the slot exactly
3732            // once (it returns either the mutated handle or the same
3733            // handle), so liveness is the OR of the two `v` occurrences'
3734            // `last_use` bits — the inner `Vector.set` read carries
3735            // `last_use=false` (the textually-last read is the default
3736            // arm), so without the OR we would wrongly clone and the
3737            // refcount-2 `Rc::make_mut` would deep-copy every iteration
3738            // (the O(n²) the VM/own_param fix already eliminated). This is
3739            // the exact mirror of own_param's `Option.withDefault` self-
3740            // keep shape + the VM fusion-collapse in `vm/compiler/mir.rs`.
3741            let set_local = local_of(&inner_args[0].node);
3742            let default_local = local_of(&args[1].node);
3743            let move_vec = match (set_local, default_local) {
3744                (Some(sv), Some(dv)) => {
3745                    ctx.owned_params.contains(sv.name.as_str())
3746                        && !ctx.is_borrowed_param(&sv.name)
3747                        && !ctx.is_rc_wrapped(&sv.name)
3748                        && (sv.last_use || dv.last_use)
3749                }
3750                _ => false,
3751            };
3752            let vector = if move_vec {
3753                // Owned + dead-after: move (no `.clone()`) → in-place
3754                // `set_unchecked` on a refcount-1 `Rc`.
3755                emit_mir_expr(&inner_args[0], ctx)?
3756            } else {
3757                // HIR: vector via `clone_arg` (borrowed / not-proven-owned).
3758                mir_clone_arg(
3759                    emit_mir_expr(&inner_args[0], ctx)?,
3760                    &inner_args[0].node,
3761                    ctx,
3762                )
3763            };
3764            let index = emit_mir_expr(&inner_args[1], ctx)?;
3765            let value = mir_clone_arg(
3766                emit_mir_expr(&inner_args[2], ctx)?,
3767                &inner_args[2].node,
3768                ctx,
3769            );
3770            Some(format!(
3771                "{{ let __vec = {}; match ({}).to_usize() {{ Some(__idx) if __idx < __vec.len() => __vec.set_unchecked(__idx, {}), _ => __vec }} }}",
3772                vector, index, value
3773            ))
3774        }
3775        // Fusion #2: `Result.withDefault(Int.mod(a, b), default)` and the
3776        // parallel `Result.withDefault(Int.div(a, b), default)` → skip the
3777        // `Result` allocation. HIR:
3778        // `ResolvedLeafOp::IntModOrDefaultLiteral` /
3779        // `ResolvedLeafOp::IntDivOrDefaultLiteral`.
3780        "Result.withDefault" if args.len() == 2 => {
3781            let (inner_name, inner_args) = mir_builtin_call_parts(&args[0].node, ctx)?;
3782            // `Int.mod` fuses to `rem_euclid`; `Int.div` to truncating `/`.
3783            let op = match inner_name {
3784                "Int.mod" => "rem_euclid",
3785                "Int.div" => "div",
3786                _ => return None,
3787            };
3788            if inner_args.len() != 2 {
3789                return None;
3790            }
3791            // The default arm must be a literal (HIR's
3792            // `classify_int_mod_or_default` requires a literal default).
3793            let MirExpr::Literal(default_lit) = &args[1].node else {
3794                return None;
3795            };
3796            let a = &inner_args[0];
3797            let b = &inner_args[1];
3798            let a_str = emit_mir_expr(a, ctx)?;
3799            let default = emit_literal(&default_lit.node);
3800            match op {
3801                // Euclidean division (partner of Euclidean `Int.mod`).
3802                // `AverInt::div_euclid` is `None` ONLY on a zero divisor
3803                // (the `i64::MIN / -1` edge promotes to `Big` over ℤ), so
3804                // `.unwrap_or(default)` yields the default exactly when the
3805                // divisor is zero — matching the VM.
3806                "div" => {
3807                    let b_str = emit_mir_expr(b, ctx)?;
3808                    Some(format!(
3809                        "({}).div_euclid(&({})).unwrap_or({})",
3810                        a_str, b_str, default
3811                    ))
3812                }
3813                // `rem_euclid` is total except on a zero divisor, so a
3814                // non-zero literal divisor can skip the runtime zero check —
3815                // the `.unwrap()` is total there (divisor proven non-zero).
3816                _ => {
3817                    if let MirExpr::Literal(b_lit) = &b.node
3818                        && let crate::ast::Literal::Int(n) = &b_lit.node
3819                        && *n != 0
3820                    {
3821                        let b_str = emit_literal(&crate::ast::Literal::Int(*n));
3822                        Some(format!("({}).rem_euclid(&({})).unwrap()", a_str, b_str))
3823                    } else {
3824                        let b_str = emit_mir_expr(b, ctx)?;
3825                        Some(format!(
3826                            "{{ let __b = {}; if __b.is_zero() {{ {} }} else {{ ({}).rem_euclid(&__b).unwrap() }} }}",
3827                            b_str, default, a_str
3828                        ))
3829                    }
3830                }
3831            }
3832        }
3833        // Fusion #3: `Vector.get(Vector.fromList(list), index)` → index
3834        // the materialized `Vec` directly, skipping the intermediate
3835        // `AverVector::from_vec` (an extra `Rc::new`). HIR:
3836        // `ResolvedLeafOp::ListIndexGet`.
3837        "Vector.get" if args.len() == 2 => {
3838            let (inner_name, inner_args) = mir_builtin_call_parts(&args[0].node, ctx)?;
3839            if inner_name != "Vector.fromList" || inner_args.len() != 1 {
3840                return None;
3841            }
3842            let list = emit_mir_expr(&inner_args[0], ctx)?;
3843            let index = emit_mir_expr(&args[1], ctx)?;
3844            // Index lookup: out-of-`usize` index → `None` (matches the
3845            // unfused `Vector.get`).
3846            Some(format!(
3847                "({}).to_usize().and_then(|__i| {}.to_vec().get(__i).cloned())",
3848                index, list
3849            ))
3850        }
3851        _ => None,
3852    }
3853}
3854
3855/// Emit a PURE builtin call from MIR, byte-identical to the HIR
3856/// oracle's `emit_builtin_call` (minus the effectful / replay / policy
3857/// branches, which never reach here). Returns `None` for any builtin
3858/// the oracle doesn't cover here (→ HIR fallback). `name` is already
3859/// known non-effectful (the `Call(Builtin)` arm gated it).
3860fn emit_mir_builtin_call(
3861    name: &str,
3862    args: &[Spanned<MirExpr>],
3863    ctx: &MirEmitCtx<'_>,
3864) -> Option<String> {
3865    // FUSIONS first: the HIR walker recognizes these
3866    // `Option.withDefault` / `Result.withDefault` / `Vector.get` over a
3867    // nested builtin shapes PRE-lowering and emits a fused form. MIR
3868    // lowering flattens the shape, so re-recognize it here before the
3869    // generic per-builtin arms below produce the un-fused (slower)
3870    // output. Anything that doesn't match falls through to the generic
3871    // emit, byte-identical to HIR's non-fused path.
3872    if let Some(fused) = try_emit_mir_fusion(name, args, ctx) {
3873        return Some(fused);
3874    }
3875
3876    // `emit_arg(i)`: raw emit (HIR's `emit_expr(&args[i].node, …)`).
3877    macro_rules! arg {
3878        ($i:expr) => {
3879            emit_mir_expr(&args[$i], ctx)?
3880        };
3881    }
3882    // `clone_arg(&args[i].node, …)`: owning clone.
3883    macro_rules! clone {
3884        ($i:expr) => {
3885            mir_clone_arg(emit_mir_expr(&args[$i], ctx)?, &args[$i].node, ctx)
3886        };
3887    }
3888
3889    let result = match name {
3890        // ---- Result ----
3891        "Result.Ok" => format!("Ok({})", clone!(0)),
3892        "Result.Err" => format!("Err({})", clone!(0)),
3893        "Result.withDefault" => format!("{}.unwrap_or({})", clone!(0), clone!(1)),
3894
3895        // ---- Option ----
3896        "Option.Some" => format!("Some({})", clone!(0)),
3897        "Option.withDefault" => format!("{}.unwrap_or({})", clone!(0), clone!(1)),
3898        "Option.toResult" => format!("{}.ok_or({})", clone!(0), clone!(1)),
3899
3900        // ---- Int ----
3901        // `AverInt::abs` promotes `|i64::MIN|` to `Big` (no wrap/panic).
3902        "Int.abs" => format!("{}.abs()", arg!(0)),
3903        // Float→Int truncation MUST funnel through `from_f64_trunc` (NOT
3904        // `from_i64(f as i64)`, which saturates a huge finite float to
3905        // `i64::MAX`). Mirrors the VM's `float_to_aver_int`.
3906        "Int.fromFloat" => format!("aver_rt::AverInt::from_f64_trunc({})", arg!(0)),
3907        "Int.fromString" => {
3908            // Match the VM's Err message BYTE-FOR-BYTE: `Int.fromString`
3909            // in `src/types/int.rs` returns `Cannot parse '{input}' as
3910            // Int`, not rustc's native `parse` error ("invalid digit
3911            // found in string"). A program that reads the `Result.Err`
3912            // string (and verify cases asserting it) must see identical
3913            // bytes on rust and the VM. Bind a *reference* to the input
3914            // (parse + the message both borrow), so a non-trivial arg
3915            // expr is evaluated once and the original owned value stays
3916            // available to surrounding code. `AverInt::from_str` parses
3917            // arbitrary-length integers (the no-wrap guarantee).
3918            let s = arg!(0);
3919            format!(
3920                "{{ let __s = &({s}); __s.parse::<aver_rt::AverInt>().map_err(|_| format!(\"Cannot parse '{{}}' as Int\", __s)) }}"
3921            )
3922        }
3923        // `AverInt` has no by-value `Ord::min`/`max`; use the borrowing
3924        // `min_ref`/`max_ref` (which keep the small-int clone cheap).
3925        "Int.min" => format!("{}.min_ref(&{})", arg!(0), arg!(1)),
3926        "Int.max" => format!("{}.max_ref(&{})", arg!(0), arg!(1)),
3927        "Int.mod" => {
3928            let a = arg!(0);
3929            let b = arg!(1);
3930            // Euclidean remainder over ℤ. `rem_euclid` returns `None` only
3931            // on a zero divisor; the error string is verbatim from the VM
3932            // (`src/types/int.rs`) so the boxed `Result.Err` is
3933            // byte-identical across backends.
3934            format!(
3935                "match ({a}).rem_euclid(&({b})) {{ Some(__r) => Ok(__r), None => Err(\"division by zero\".to_string()) }}"
3936            )
3937        }
3938        "Int.div" => {
3939            let a = arg!(0);
3940            let b = arg!(1);
3941            // Euclidean division over ℤ (partner of Euclidean `Int.mod`).
3942            // `div_euclid` is `None` ONLY for a zero divisor — the
3943            // `i64::MIN / -1` "overflow" edge promotes to `Big` (it is just
3944            // `i64::MAX + 1`), so the VM's old "division overflow" branch is
3945            // DEAD here (VM agrees: both are ℤ now). Keep the
3946            // "division by zero" string byte-identical to the VM.
3947            format!(
3948                "match ({a}).div_euclid(&({b})) {{ Some(__q) => Ok(__q), None => Err(\"division by zero\".to_string()) }}"
3949            )
3950        }
3951
3952        // ---- Float ----
3953        "Float.abs" => format!("{}.abs()", arg!(0)),
3954        // The VM applies `float_to_aver_int(f.round())` etc. — round/floor/
3955        // ceil the f64, then truncate into ℤ via `from_f64_trunc` (NOT
3956        // `as i64`, which saturates huge finite floats to `i64::MAX`).
3957        "Float.round" => format!("aver_rt::AverInt::from_f64_trunc({}.round())", arg!(0)),
3958        "Float.floor" => format!("aver_rt::AverInt::from_f64_trunc({}.floor())", arg!(0)),
3959        "Float.ceil" => format!("aver_rt::AverInt::from_f64_trunc({}.ceil())", arg!(0)),
3960        "Float.fromString" => {
3961            // Match the VM's Err message BYTE-FOR-BYTE: `Float.fromString`
3962            // in `src/types/float.rs` returns `Cannot parse '{input}' as
3963            // Float`, not rustc's native `parse` error.
3964            let s = arg!(0);
3965            format!(
3966                "{{ let __s = &({s}); __s.parse::<f64>().map_err(|_| format!(\"Cannot parse '{{}}' as Float\", __s)) }}"
3967            )
3968        }
3969        "Float.sqrt" => format!("{}.sqrt()", arg!(0)),
3970        "Float.pow" => format!("{}.powf({})", arg!(0), arg!(1)),
3971        "Float.min" => format!("{}.min({})", arg!(0), arg!(1)),
3972        "Float.max" => format!("{}.max({})", arg!(0), arg!(1)),
3973        "Float.sin" => format!("{}.sin()", arg!(0)),
3974        "Float.cos" => format!("{}.cos()", arg!(0)),
3975        "Float.atan2" => format!("{}.atan2({})", arg!(0), arg!(1)),
3976        "Float.pi" => "std::f64::consts::PI".to_string(),
3977        // The arg is now `AverInt`; `to_f64` saturates huge magnitudes to
3978        // `±∞` (never `NaN`), mirroring the VM's `AverInt::to_f64`.
3979        "Float.fromInt" => format!("{}.to_f64()", arg!(0)),
3980
3981        // ---- String ----
3982        // `AverInt: Display` renders Small and Big byte-identically to the
3983        // VM's `Value::Int` formatting.
3984        "String.fromInt" => format!("{}.to_string()", arg!(0)),
3985        "String.fromFloat" => format!("{}.to_string()", arg!(0)),
3986        "String.fromBool" => format!("{}.to_string()", arg!(0)),
3987        "String.charAt" => {
3988            let s = arg!(0);
3989            let idx = arg!(1);
3990            // Index lookup: an out-of-`usize` index is out of range → `None`
3991            // (charAt already returns `Option<String>`), never a wrapped
3992            // index. `to_usize()` is the checked conversion.
3993            format!(
3994                "({}).to_usize().and_then(|__i| {}.chars().nth(__i).map(|c| c.to_string()))",
3995                idx, s
3996            )
3997        }
3998        // Producer: wrap the `usize` length in `AverInt`.
3999        "String.len" => format!(
4000            "aver_rt::AverInt::from_i64({}.chars().count() as i64)",
4001            arg!(0)
4002        ),
4003        "String.slice" => {
4004            let s = arg!(0);
4005            let from = arg!(1);
4006            let to = arg!(2);
4007            // `string_slice` clamps internally (`from.max(0)`, end past the
4008            // length saturates) and takes `i64` bounds. A Big bound is out of
4009            // any string's range, so saturate it sign-aware to `i64::MIN`/
4010            // `i64::MAX` (`aver_int_clamp_i64`): a huge positive clamps to the
4011            // string end, a huge negative clamps to 0 — behaviorally identical
4012            // to the VM.
4013            format!(
4014                "aver_rt::string_slice(&{}, crate::aver_int_clamp_i64(&{}), crate::aver_int_clamp_i64(&{}))",
4015                s, from, to
4016            )
4017        }
4018        "String.contains" => {
4019            let s = arg!(0);
4020            let sub = mir_str_arg_or_deref(&args[1], ctx)?;
4021            format!("{}.contains({})", s, sub)
4022        }
4023        "String.startsWith" => {
4024            let s = arg!(0);
4025            let prefix = mir_str_arg_or_deref(&args[1], ctx)?;
4026            format!("{}.starts_with({})", s, prefix)
4027        }
4028        "String.endsWith" => {
4029            let s = arg!(0);
4030            let suffix = mir_str_arg_or_deref(&args[1], ctx)?;
4031            format!("{}.ends_with({})", s, suffix)
4032        }
4033        "String.trim" => format!("{}.trim().to_string()", arg!(0)),
4034        "String.toUpper" => format!("{}.to_uppercase()", arg!(0)),
4035        "String.toLower" => format!("{}.to_lowercase()", arg!(0)),
4036        "String.split" => {
4037            let s = arg!(0);
4038            let delim = arg!(1);
4039            format!(
4040                "aver_rt::AverList::from_vec({}.split(&*{}).map(|s| s.to_string()).collect::<Vec<_>>())",
4041                s, delim
4042            )
4043        }
4044        "String.join" => {
4045            let parts = arg!(0);
4046            let delim = arg!(1);
4047            format!("aver_rt::string_join(&{}, &{})", parts, delim)
4048        }
4049        "String.replace" => {
4050            let s = arg!(0);
4051            let from = arg!(1);
4052            let to = arg!(2);
4053            format!("{}.replace(&*{}, &*{})", s, from, to)
4054        }
4055        "String.chars" => format!(
4056            "aver_rt::AverList::from_vec({}.chars().map(|c| c.to_string()).collect::<Vec<_>>())",
4057            arg!(0)
4058        ),
4059        "String.repeat" => {
4060            let s = arg!(0);
4061            let n = arg!(1);
4062            // A negative or out-of-`usize` count yields the empty string
4063            // (matching a 0-repeat); `to_usize()` is `None` for both.
4064            format!("{}.repeat(({}).to_usize().unwrap_or(0))", s, n)
4065        }
4066        "String.indexOf" => {
4067            let s = arg!(0);
4068            let sub = arg!(1);
4069            // Producer: the found byte index wraps in `AverInt`; not-found
4070            // is `-1`.
4071            format!(
4072                "{}.find(&*{}).map(|i| aver_rt::AverInt::from_i64(i as i64)).unwrap_or(aver_rt::AverInt::from_i64(-1))",
4073                s, sub
4074            )
4075        }
4076        "String.byteLength" => {
4077            format!("aver_rt::AverInt::from_i64({}.len() as i64)", arg!(0))
4078        }
4079
4080        // ---- List ----
4081        "List.len" => {
4082            if let MirExpr::List(items) = &args[0].node
4083                && items.is_empty()
4084            {
4085                "aver_rt::AverInt::from_i64(0)".to_string()
4086            } else {
4087                // Producer: wrap the `usize` length in `AverInt`.
4088                format!("aver_rt::AverInt::from_i64({}.len() as i64)", arg!(0))
4089            }
4090        }
4091        "List.prepend" => format!("aver_rt::AverList::prepend({}, &{})", clone!(0), clone!(1)),
4092        "List.take" => {
4093            let list = arg!(0);
4094            let count = arg!(1);
4095            // A negative count → 0; a huge (Big) count → take all
4096            // (`to_usize()` is `None`, so `usize::MAX`). Semantics preserved
4097            // from the old `usize::try_from`-based clamp.
4098            format!(
4099                "{{ let __n = ({count}).to_usize().unwrap_or(usize::MAX); aver_rt::AverList::from_vec(({list}).iter().take(__n).cloned().collect::<Vec<_>>()) }}"
4100            )
4101        }
4102        "List.drop" => {
4103            let list = arg!(0);
4104            let count = arg!(1);
4105            format!(
4106                "{{ let __n = ({count}).to_usize().unwrap_or(usize::MAX); aver_rt::AverList::from_vec(({list}).iter().skip(__n).cloned().collect::<Vec<_>>()) }}"
4107            )
4108        }
4109        "List.concat" => format!("aver_rt::AverList::concat(&{}, &{})", clone!(0), clone!(1)),
4110        "List.reverse" => format!("{}.reverse()", arg!(0)),
4111        "List.contains" => {
4112            let list = arg!(0);
4113            let item = arg!(1);
4114            format!("{}.contains(&{})", list, item)
4115        }
4116        "List.zip" => {
4117            let a = arg!(0);
4118            let b = arg!(1);
4119            format!(
4120                "aver_rt::AverList::from_vec({}.iter().zip({}.iter()).map(|(a, b)| (a.clone(), b.clone())).collect::<Vec<_>>())",
4121                a, b
4122            )
4123        }
4124        "List.fromVector" => format!("{}.to_list()", arg!(0)),
4125
4126        // ---- Map ----
4127        "Map.fromList" => format!(
4128            "{{ let mut m = HashMap::new(); for (k, v) in {}.iter().cloned() {{ m = m.insert_owned(k, v); }} m }}",
4129            clone!(0)
4130        ),
4131        "Map.entries" => format!(
4132            "{{ let mut es: Vec<_> = {}.iter().map(|(k, v)| (k.clone(), v.clone())).collect(); es.sort_by(|a, b| a.0.cmp(&b.0)); aver_rt::AverList::from_vec(es) }}",
4133            arg!(0)
4134        ),
4135        "Map.get" => {
4136            let map = arg!(0);
4137            let key = arg!(1);
4138            format!("{}.get(&{}).cloned()", map, key)
4139        }
4140        "Map.set" => format!("{}.insert_owned({}, {})", clone!(0), clone!(1), clone!(2)),
4141        "Map.has" => {
4142            let map = arg!(0);
4143            let key = arg!(1);
4144            format!("{}.contains_key(&{})", map, key)
4145        }
4146        "Map.remove" => {
4147            let map = clone!(0);
4148            let key = arg!(1);
4149            format!("{}.remove_owned(&{})", map, key)
4150        }
4151        "Map.keys" => format!(
4152            "{{ let mut ks: Vec<_> = {}.keys().cloned().collect(); ks.sort(); aver_rt::AverList::from_vec(ks) }}",
4153            arg!(0)
4154        ),
4155        "Map.values" => format!(
4156            "aver_rt::AverList::from_vec({}.values().cloned().collect::<Vec<_>>())",
4157            arg!(0)
4158        ),
4159        "Map.len" => format!("aver_rt::AverInt::from_i64({}.len() as i64)", arg!(0)),
4160
4161        // ---- Bool ----
4162        "Bool.or" => format!("({} || {})", arg!(0), arg!(1)),
4163        "Bool.and" => format!("({} && {})", arg!(0), arg!(1)),
4164        "Bool.not" => format!("(!{})", arg!(0)),
4165
4166        // ---- Char ----
4167        // Producer: the code point wraps in `AverInt`.
4168        "Char.toCode" => format!(
4169            "aver_rt::AverInt::from_i64({}.chars().next().map(|c| c as i64).unwrap_or(0))",
4170            arg!(0)
4171        ),
4172        // Index-like lookup: an out-of-`u32` (or invalid) code → `None`.
4173        "Char.fromCode" => format!(
4174            "({}).to_u32().and_then(char::from_u32).map(|c| c.to_string())",
4175            arg!(0)
4176        ),
4177
4178        // ---- Byte ----
4179        // Range check over ℤ (no `as u8` truncation): compare the `AverInt`
4180        // against the 0–255 bounds, then convert the in-range value.
4181        "Byte.toHex" => format!(
4182            "{{ let __n = {}; match __n.to_u16() {{ Some(__b @ 0..=255) => Ok(format!(\"{{:02x}}\", __b as u8)), _ => Err(format!(\"Byte.toHex: {{}} is out of range 0–255\", __n)) }} }}",
4183            arg!(0)
4184        ),
4185        "Byte.fromHex" => format!(
4186            "{{ let __s = {}; if __s.len() != 2 {{ Err(format!(\"Byte.fromHex: expected exactly 2 hex chars, got '{{}}'\", __s)) }} else {{ u8::from_str_radix(&__s, 16).map(|n| aver_rt::AverInt::from_i64(n as i64)).map_err(|_| format!(\"Byte.fromHex: invalid hex '{{}}'\", __s)) }} }}",
4187            arg!(0)
4188        ),
4189
4190        // ---- Vector ----
4191        "Vector.new" => {
4192            let size = arg!(0);
4193            let default = clone!(1);
4194            // Capacity site: the VM ERRORS on a negative / non-machine-sized
4195            // size. Generated Rust has no Result channel here (Vector.new
4196            // returns a Vector), so a bad size ABORTS via `.expect` with the
4197            // VM's exact message — NEVER a silent `unwrap_or(0)` empty vector.
4198            format!(
4199                "aver_rt::AverVector::new(({}).to_usize().expect(\"Vector.new: size must be a non-negative, machine-sized Int\"), {})",
4200                size, default
4201            )
4202        }
4203        "Vector.get" => {
4204            let vec = arg!(0);
4205            let idx = arg!(1);
4206            // Index lookup: an out-of-`usize` index → `None` (Vector.get
4207            // already returns `Option`).
4208            format!(
4209                "({}).to_usize().and_then(|__i| {}.get(__i).cloned())",
4210                idx, vec
4211            )
4212        }
4213        "Vector.set" => {
4214            let vec = clone!(0);
4215            let idx = arg!(1);
4216            let val = clone!(2);
4217            // `set_owned` returns `Option<Vector>` (None on out-of-range),
4218            // mirroring the VM. An out-of-`usize` index is likewise `None`,
4219            // via the checked conversion — never a wrapped index.
4220            format!(
4221                "({}).to_usize().and_then(|__i| {}.set_owned(__i, {}))",
4222                idx, vec, val
4223            )
4224        }
4225        "Vector.len" => format!("aver_rt::AverInt::from_i64({}.len() as i64)", arg!(0)),
4226        "Vector.fromList" => format!("aver_rt::AverVector::from_vec({}.to_vec())", arg!(0)),
4227
4228        // ---- BranchPath ----
4229        // Oracle structural-addressing constructors. The `aver_rt`
4230        // `BranchPath` struct (+ `root`/`child`/`parse` impls) is
4231        // re-exported into the generated crate. `BranchPath.Root` is a
4232        // nullary value, not a call — it lowers to a `FnValue` and is
4233        // handled in `emit_mir_static_ref`. `.child` / `.parse` are
4234        // builtin method calls and land here.
4235        //
4236        // `child(path: &BranchPath, idx: i64)`: the path arg goes
4237        // through `mir_borrow_arg` so a borrowed-param `&BranchPath` is
4238        // passed directly while a fresh owned value (e.g. a nested
4239        // `BranchPath.child(...)` or `BranchPath.Root`) gets a `&`.
4240        "BranchPath.child" => {
4241            let path = mir_borrow_arg(emit_mir_expr(&args[0], ctx)?, &args[0].node, ctx);
4242            let idx = arg!(1);
4243            // `child` takes a host `i64` index; convert the `AverInt` at the
4244            // boundary, ERRORING on an out-of-range index like the VM (never
4245            // a silent truncation).
4246            format!(
4247                "aver_rt::BranchPath::child({}, crate::to_host_i64(&({}), \"BranchPath.child: `idx` must be a non-negative, machine-sized Int\"))",
4248                path, idx
4249            )
4250        }
4251        // `parse(raw: &str)`: `mir_str_arg_or_deref` yields a bare
4252        // string literal or the `&*` deref form, both `&str`.
4253        "BranchPath.parse" => {
4254            let raw = mir_str_arg_or_deref(&args[0], ctx)?;
4255            format!("aver_rt::BranchPath::parse({})", raw)
4256        }
4257
4258        // Not a covered pure builtin (effectful builtins never reach
4259        // here — gated at the call arm). HIR fallback.
4260        _ => return None,
4261    };
4262
4263    // Mirror of `emit_builtin_call`'s `.into_aver()` post-step for
4264    // String-returning pure builtins (and Int.mod / Int.fromString /
4265    // Float.fromString / Char.fromCode / Byte.*).
4266    if super::builtins::builtin_needs_str_conversion(name) {
4267        Some(format!("({}).into_aver()", result))
4268    } else {
4269        Some(result)
4270    }
4271}
4272
4273// ── Wave 3b: EFFECTFUL builtin calls (replay / policy / bare framing) ───
4274//
4275// SECURITY-SENSITIVE. Mirror of the HIR oracle `emit_builtin_call`
4276// (`builtins.rs`) for the 11 EFFECTFUL families (Args / Console / Http /
4277// HttpServer / Disk / Env / Random / SelfHostRuntime / Tcp / Terminal /
4278// Time). Wave 3a gated these out (`builtin_is_effectful` → `None` → HIR
4279// fallback); Wave 3b emits them, threading `ctx.policy` +
4280// `ctx.emit_replay_runtime` (reachable through `ctx.codegen`).
4281//
4282// The three wrappers HIR applies are reproduced by the SAME shared
4283// composers `emit_builtin_call` calls — `compose_replay_effect_call`
4284// (replay reroute), `compose_effectful_builtin_raw` (the raw `aver_rt::*`
4285// body), and `compose_effect_wrap` (policy `check_*` + bare
4286// `cancel_checkpoint` framing) — so the MIR output is byte-identical to
4287// HIR by construction. The only walker-specific inputs are the per-arg
4288// renders: `mir_clone_arg` (the replay temps, HIR's `clone_arg`) and the
4289// raw `emit_mir_expr` (the non-replay args + the policy first arg, HIR's
4290// `emit_expr`).
4291//
4292// A dropped composer here silently disables aver.toml DENY enforcement
4293// or record/replay capture (invisible to rustc + coverage + happy-path
4294// stdout) — the differential security test under `AVER_RUST_MIR_ONLY=1`
4295// forces this path and is revert-proofed against exactly that drop.
4296
4297/// Emit an EFFECTFUL builtin call from MIR, byte-identical to the HIR
4298/// oracle's `emit_builtin_call`. `name` is already known effectful (the
4299/// `Call(Builtin)` arm routed it here). Returns `None` (→ HIR fallback)
4300/// when an arg can't render, when the production `CodegenContext` is
4301/// absent (coverage path — no policy/replay info), or when the raw
4302/// effect body isn't one the oracle covers.
4303fn emit_mir_effectful_builtin_call(
4304    name: &str,
4305    args: &[Spanned<MirExpr>],
4306    ctx: &MirEmitCtx<'_>,
4307) -> Option<String> {
4308    // The policy / replay flags live on the full `CodegenContext`. The
4309    // coverage / test path has none → fall back to HIR (which the
4310    // coverage walk reads as a `None`, conservative + fine). The
4311    // production parity gate always carries a ctx.
4312    let codegen = ctx.codegen?;
4313
4314    // (1) Replay reroute — mirror of `emit_builtin_call`'s
4315    //     `if ctx.emit_replay_runtime && builtin_is_effectful(name)`.
4316    //     Each arg is bound to `__effect_argN` via the `clone_arg`
4317    //     mirror; the shared composer emits the
4318    //     `cancel_checkpoint` + `invoke_effect(<effect>, vec![json], || raw)`
4319    //     block from the temp names.
4320    if codegen.emit_replay_runtime {
4321        let mut arg_clones = Vec::with_capacity(args.len());
4322        for a in args {
4323            arg_clones.push(mir_clone_arg(emit_mir_expr(a, ctx)?, &a.node, ctx));
4324        }
4325        return super::builtins::compose_replay_effect_call(name, &arg_clones);
4326    }
4327
4328    // (2) Raw effect body — mirror of `emit_builtin_call_inner`'s
4329    //     effectful arms, every arg by-value (raw `emit_mir_expr`, HIR's
4330    //     `emit_arg`). The shared composer renders the `aver_rt::*` call.
4331    let mut arg_strs = Vec::with_capacity(args.len());
4332    for a in args {
4333        arg_strs.push(emit_mir_expr(a, ctx)?);
4334    }
4335    let result = super::builtins::compose_effectful_builtin_raw(name, &arg_strs)?;
4336
4337    // `.into_aver()` post-step for String-returning effectful builtins
4338    // (mirror of `emit_builtin_call`'s `builtin_needs_str_conversion`).
4339    let result = if super::builtins::builtin_needs_str_conversion(name) {
4340        format!("({}).into_aver()", result)
4341    } else {
4342        result
4343    };
4344
4345    // (3) Policy wrap (Http/Disk/Env) + bare `cancel_checkpoint` framing
4346    //     — mirror of `emit_builtin_call`'s tail. The first arg for the
4347    //     `check_*` call is rendered raw (HIR's `emit_expr`).
4348    let policy_active = codegen.policy.is_some() && !codegen.emit_replay_runtime;
4349    let first_arg = if policy_active && !args.is_empty() {
4350        Some(emit_mir_expr(&args[0], ctx)?)
4351    } else {
4352        None
4353    };
4354    Some(super::builtins::compose_effect_wrap(
4355        name,
4356        result,
4357        policy_active,
4358        first_arg,
4359    ))
4360}
4361
4362/// Emit one of the 5 deforestation intrinsics from MIR, byte-identical
4363/// to the HIR oracle's `emit_builtin_call_inner` intrinsic arms. Args
4364/// are by-value (raw `emit_mir_expr`, no clone / borrow), matching the
4365/// loop-rebind shape the deforestation synthesizer emits. The Rust
4366/// backend deforests differently, so a buffered fn's MIR shape may not
4367/// byte-match HIR — the parity gate then falls back safely.
4368fn emit_mir_intrinsic_call(
4369    intrinsic: BuiltinIntrinsic,
4370    args: &[Spanned<MirExpr>],
4371    ctx: &MirEmitCtx<'_>,
4372) -> Option<String> {
4373    match intrinsic {
4374        BuiltinIntrinsic::BufNew => {
4375            let cap = emit_mir_expr(&args[0], ctx)?;
4376            // The capacity is a pure allocation HINT (no semantic effect): a
4377            // Big / out-of-`usize` value just falls back to 0 (the Vec grows
4378            // on demand). This is the one site where `unwrap_or(0)` is sound
4379            // because the value never reaches output — not a silent wrong
4380            // value, a sizing hint.
4381            Some(format!(
4382                "aver_rt::Buffer::with_capacity(({}).to_usize().unwrap_or(0))",
4383                cap
4384            ))
4385        }
4386        BuiltinIntrinsic::BufAppend => {
4387            let buf = emit_mir_expr(&args[0], ctx)?;
4388            let s = emit_mir_expr(&args[1], ctx)?;
4389            Some(format!(
4390                "{{ let mut __b = {}; __b.push_str(&{}); __b }}",
4391                buf, s
4392            ))
4393        }
4394        BuiltinIntrinsic::BufAppendSepUnlessFirst => {
4395            let buf = emit_mir_expr(&args[0], ctx)?;
4396            let sep = emit_mir_expr(&args[1], ctx)?;
4397            Some(format!(
4398                "{{ let mut __b = {}; if !__b.is_empty() {{ __b.push_str(&{}); }} __b }}",
4399                buf, sep
4400            ))
4401        }
4402        BuiltinIntrinsic::BufFinalize => {
4403            let buf = emit_mir_expr(&args[0], ctx)?;
4404            Some(format!("aver_rt::AverStr::from({})", buf))
4405        }
4406        BuiltinIntrinsic::ToStr => {
4407            let arg = emit_mir_expr(&args[0], ctx)?;
4408            Some(format!(
4409                "aver_rt::AverStr::from(aver_rt::aver_display(&({})))",
4410                arg
4411            ))
4412        }
4413        // Const-divisor Euclidean div/mod (0.24 "Divide"). The MIR
4414        // const-fold pass only emits these for a literal NON-ZERO divisor,
4415        // so `AverInt::div_euclid` / `rem_euclid` (which are `None` only on a
4416        // zero divisor) are always `Some` here — the `.unwrap()` is total.
4417        // Same routines `Int.div` / `Int.mod` use in `src/types/int.rs`.
4418        BuiltinIntrinsic::IntDivEuclid => {
4419            let a = emit_mir_expr(&args[0], ctx)?;
4420            let b = emit_mir_expr(&args[1], ctx)?;
4421            Some(format!("({}).div_euclid(&({})).unwrap()", a, b))
4422        }
4423        BuiltinIntrinsic::IntModEuclid => {
4424            let a = emit_mir_expr(&args[0], ctx)?;
4425            let b = emit_mir_expr(&args[1], ctx)?;
4426            Some(format!("({}).rem_euclid(&({})).unwrap()", a, b))
4427        }
4428    }
4429}
4430
4431#[cfg(test)]
4432mod tests {
4433    use super::*;
4434    use crate::ir::SymbolTable;
4435    use crate::ir::mir::{LocalId, MirBinOp, MirCall, MirExpr, MirLocal};
4436    use std::sync::OnceLock;
4437
4438    fn span<T>(node: T) -> Spanned<T> {
4439        Spanned {
4440            node,
4441            line: 0,
4442            ty: OnceLock::new(),
4443        }
4444    }
4445
4446    fn span_ty<T>(node: T, ty: Type) -> Spanned<T> {
4447        let stamp = OnceLock::new();
4448        let _ = stamp.set(ty);
4449        Spanned {
4450            node,
4451            line: 0,
4452            ty: stamp,
4453        }
4454    }
4455
4456    /// Empty `MirEmitCtx` with statically-borrowed empty symbol
4457    /// table + empty module-prefix set. `OnceLock`s give us a
4458    /// `'static` lifetime so tests can pass `&empty_ctx()`
4459    /// inline without juggling local owners.
4460    fn empty_ctx() -> MirEmitCtx<'static> {
4461        use std::sync::OnceLock;
4462        static SYMBOLS: OnceLock<SymbolTable> = OnceLock::new();
4463        static PREFIXES: OnceLock<HashSet<String>> = OnceLock::new();
4464        MirEmitCtx::for_test(
4465            SYMBOLS.get_or_init(SymbolTable::default),
4466            PREFIXES.get_or_init(HashSet::new),
4467        )
4468    }
4469
4470    #[test]
4471    fn emits_int_literal_as_i64_suffix() {
4472        let lit = span(MirExpr::Literal(span(crate::ast::Literal::Int(42))));
4473        assert_eq!(
4474            emit_mir_expr(&lit, &empty_ctx()).as_deref(),
4475            Some("aver_rt::AverInt::from_i64(42)")
4476        );
4477    }
4478
4479    #[test]
4480    fn emits_local_via_aver_name_to_rust() {
4481        let local = MirLocal {
4482            slot: LocalId(0),
4483            last_use: false,
4484            name: "x".to_string(),
4485        };
4486        let expr = span(MirExpr::Local(span(local)));
4487        let emit = emit_mir_expr(&expr, &empty_ctx()).expect("local should emit");
4488        assert!(
4489            emit.contains("x"),
4490            "local emit should reference `x`: {emit}"
4491        );
4492    }
4493
4494    #[test]
4495    fn returns_none_for_synthetic_local() {
4496        let local = MirLocal {
4497            slot: LocalId(7),
4498            last_use: false,
4499            name: String::new(),
4500        };
4501        let expr = span(MirExpr::Local(span(local)));
4502        assert!(emit_mir_expr(&expr, &empty_ctx()).is_none());
4503    }
4504
4505    #[test]
4506    fn empty_fn_policy_has_no_anchor() {
4507        // The shared no-anchor policy: no params/locals, nothing
4508        // borrowed-by-default — the MIR mirror of `EmitCtx::empty()`.
4509        let policy = MirFnEmitPolicy::empty();
4510        assert!(policy.local_types.is_empty());
4511        assert!(policy.rc_wrapped.is_empty());
4512        assert!(policy.borrowed_params.is_empty());
4513        assert!(policy.current_module_scope.is_none());
4514    }
4515
4516    #[test]
4517    fn program_level_ctx_renders_free_expr() {
4518        // A program-level ctx (empty policy + a real symbol table /
4519        // codegen) renders a free-standing literal — the verify-case
4520        // shape (no fn anchor). We can't build a full `CodegenContext`
4521        // cheaply here, so assert the policy/ctx wiring via the
4522        // walker on a literal that needs no `codegen`.
4523        let policy = MirFnEmitPolicy::empty();
4524        use std::sync::OnceLock;
4525        static SYMBOLS: OnceLock<SymbolTable> = OnceLock::new();
4526        static PREFIXES: OnceLock<HashSet<String>> = OnceLock::new();
4527        static BUILTINS: OnceLock<Vec<String>> = OnceLock::new();
4528        // `program_level` needs a `&CodegenContext`; the literal arm
4529        // never reads it, so exercise the borrow-field plumbing via
4530        // `for_test` + the empty policy's slices instead (same shapes).
4531        let ctx = MirEmitCtx {
4532            symbol_table: SYMBOLS.get_or_init(SymbolTable::default),
4533            module_prefixes: PREFIXES.get_or_init(HashSet::new),
4534            codegen: None,
4535            local_types: &policy.local_types,
4536            rc_wrapped: &policy.rc_wrapped,
4537            borrowed_params: &policy.borrowed_params,
4538            owned_params: &policy.owned_params,
4539            current_module_scope: policy.current_module_scope.as_deref(),
4540            mir_builtins: BUILTINS.get_or_init(Vec::new),
4541            bare: &policy.bare,
4542        };
4543        let lit = span(MirExpr::Literal(span(crate::ast::Literal::Int(7))));
4544        assert_eq!(
4545            emit_mir_expr(&lit, &ctx).as_deref(),
4546            Some("aver_rt::AverInt::from_i64(7)")
4547        );
4548    }
4549
4550    #[test]
4551    fn main_body_policy_borrows_by_default_like_hir() {
4552        // `emit_mir_main_body` builds its policy from the resolved-main
4553        // via `from_resolved(.., borrow_by_default = true)` — the same
4554        // non-TCO borrow rules the HIR main body uses (`build_fn_ectx`).
4555        // A `List<Int>` param borrows; an `Int` param does not. (Main
4556        // usually has no params, but the policy must honour the same
4557        // rule so a `main(args: List<String>)`-style entry borrows
4558        // identically to every other fn.)
4559        let resolved = crate::ir::hir::ResolvedFnDef {
4560            fn_id: crate::ir::FnId(0),
4561            name: "main".to_string(),
4562            line: 1,
4563            params: vec![
4564                ("xs".to_string(), Type::List(Box::new(Type::Int))),
4565                ("n".to_string(), Type::Int),
4566            ],
4567            return_type: Type::Unit,
4568            effects: vec![],
4569            desc: None,
4570            body: std::sync::Arc::new(crate::ir::hir::ResolvedFnBody::Block(vec![])),
4571            resolution: None,
4572        };
4573        let policy = MirFnEmitPolicy::from_resolved(&resolved, None, true);
4574        assert!(policy.borrowed_params.contains("xs"));
4575        assert!(!policy.borrowed_params.contains("n"));
4576        assert!(policy.current_module_scope.is_none());
4577    }
4578
4579    #[test]
4580    fn emits_int_binop_add_as_averint_method() {
4581        let x = MirLocal {
4582            slot: LocalId(0),
4583            last_use: false,
4584            name: "x".to_string(),
4585        };
4586        let bop = MirBinOp {
4587            op: BinOp::Add,
4588            lhs: Box::new(span_ty(MirExpr::Local(span(x.clone())), Type::Int)),
4589            rhs: Box::new(span_ty(MirExpr::Local(span(x)), Type::Int)),
4590        };
4591        let expr = span(MirExpr::BinOp(span(bop)));
4592        let emit = emit_mir_expr(&expr, &empty_ctx()).expect("binop should emit");
4593        // Int arithmetic lowers to the non-wrapping `AverInt::add(&rhs)`.
4594        assert_eq!(emit, "x.add(&x)");
4595    }
4596
4597    #[test]
4598    fn emits_str_binop_add_as_concat() {
4599        // When both operands are stamped `Str`,
4600        // the BinOp::Add path emits `(l + &r)` to match HIR's
4601        // `AverStr` concat shape.
4602        let s = MirLocal {
4603            slot: LocalId(0),
4604            last_use: false,
4605            name: "s".to_string(),
4606        };
4607        let bop = MirBinOp {
4608            op: BinOp::Add,
4609            lhs: Box::new(span_ty(MirExpr::Local(span(s.clone())), Type::Str)),
4610            rhs: Box::new(span_ty(MirExpr::Local(span(s)), Type::Str)),
4611        };
4612        let expr = span(MirExpr::BinOp(span(bop)));
4613        let emit = emit_mir_expr(&expr, &empty_ctx()).expect("binop should emit");
4614        assert!(
4615            emit.contains(" + &"),
4616            "Str+Str should emit `+ &` for AverStr concat: {emit}"
4617        );
4618    }
4619
4620    #[test]
4621    fn emits_neg_as_paren_minus_inner() {
4622        // An Int operand negates via `AverInt::neg()` (no std `-`).
4623        let inner = span(MirExpr::Literal(span(crate::ast::Literal::Int(7))));
4624        let expr = span(MirExpr::Neg(Box::new(inner)));
4625        let emit = emit_mir_expr(&expr, &empty_ctx()).expect("neg should emit");
4626        assert_eq!(emit, "aver_rt::AverInt::from_i64(7).neg()");
4627    }
4628
4629    #[test]
4630    fn returns_none_for_builtin_call_without_table() {
4631        // On the coverage / test path the `mir_builtins` table is
4632        // empty, so a `BuiltinId` resolves to nothing → HIR fallback.
4633        let call = MirCall {
4634            callee: MirCallee::Builtin(crate::ir::BuiltinId(0)),
4635            args: vec![span(MirExpr::Literal(span(crate::ast::Literal::Str(
4636                "hello".to_string(),
4637            ))))],
4638        };
4639        let expr = span(MirExpr::Call(span(call)));
4640        assert!(emit_mir_expr(&expr, &empty_ctx()).is_none());
4641    }
4642
4643    /// `MirEmitCtx` carrying a one-entry builtin table so `Call(Builtin)`
4644    /// resolves `BuiltinId(0)` → `name`. Leaks the backing `Vec` to give
4645    /// it a `'static` lifetime (test-only).
4646    fn ctx_with_builtin(name: &str) -> MirEmitCtx<'static> {
4647        use std::sync::OnceLock;
4648        static SYMBOLS: OnceLock<SymbolTable> = OnceLock::new();
4649        static PREFIXES: OnceLock<HashSet<String>> = OnceLock::new();
4650        let builtins: &'static [String] = Box::leak(vec![name.to_string()].into_boxed_slice());
4651        let mut ctx = MirEmitCtx::for_test(
4652            SYMBOLS.get_or_init(SymbolTable::default),
4653            PREFIXES.get_or_init(HashSet::new),
4654        );
4655        ctx.mir_builtins = builtins;
4656        ctx
4657    }
4658
4659    fn int_lit(n: i64) -> Spanned<MirExpr> {
4660        span_ty(
4661            MirExpr::Literal(span(crate::ast::Literal::Int(n))),
4662            Type::Int,
4663        )
4664    }
4665
4666    #[test]
4667    fn emits_pure_builtin_int_mod_with_into_aver() {
4668        // `Int.mod` is a covered PURE builtin; it carries the
4669        // `.into_aver()` post-step (`builtin_needs_str_conversion`).
4670        let call = MirCall {
4671            callee: MirCallee::Builtin(crate::ir::BuiltinId(0)),
4672            args: vec![int_lit(7), int_lit(3)],
4673        };
4674        let expr = span(MirExpr::Call(span(call)));
4675        let emit = emit_mir_expr(&expr, &ctx_with_builtin("Int.mod")).expect("Int.mod emits");
4676        assert_eq!(
4677            emit,
4678            "(match (aver_rt::AverInt::from_i64(7)).rem_euclid(&(aver_rt::AverInt::from_i64(3))) { Some(__r) => Ok(__r), None => Err(\"division by zero\".to_string()) }).into_aver()"
4679        );
4680    }
4681
4682    #[test]
4683    fn emits_pure_builtin_bool_or() {
4684        let call = MirCall {
4685            callee: MirCallee::Builtin(crate::ir::BuiltinId(0)),
4686            args: vec![
4687                span_ty(
4688                    MirExpr::Literal(span(crate::ast::Literal::Bool(true))),
4689                    Type::Bool,
4690                ),
4691                span_ty(
4692                    MirExpr::Literal(span(crate::ast::Literal::Bool(false))),
4693                    Type::Bool,
4694                ),
4695            ],
4696        };
4697        let expr = span(MirExpr::Call(span(call)));
4698        let emit = emit_mir_expr(&expr, &ctx_with_builtin("Bool.or")).expect("Bool.or emits");
4699        assert_eq!(emit, "(true || false)");
4700    }
4701
4702    #[test]
4703    fn effectful_builtin_returns_none_without_codegen_ctx() {
4704        // Wave 3b: effectful builtins DO emit on the production path, but
4705        // they need the `CodegenContext` (for `ctx.policy` /
4706        // `ctx.emit_replay_runtime`). The coverage / test path carries no
4707        // ctx, so `Console.print` returns `None` → HIR fallback there,
4708        // which the coverage walk reads conservatively. (Production emit
4709        // is exercised by the differential security test.)
4710        let call = MirCall {
4711            callee: MirCallee::Builtin(crate::ir::BuiltinId(0)),
4712            args: vec![span(MirExpr::Literal(span(crate::ast::Literal::Str(
4713                "hi".to_string(),
4714            ))))],
4715        };
4716        let expr = span(MirExpr::Call(span(call)));
4717        assert!(
4718            emit_mir_expr(&expr, &ctx_with_builtin("Console.print")).is_none(),
4719            "effectful Console.print needs a CodegenContext; without one it \
4720             falls back to HIR"
4721        );
4722    }
4723
4724    #[test]
4725    fn emits_buf_finalize_intrinsic() {
4726        // `__buf_finalize(buf)` → `aver_rt::AverStr::from(buf)`.
4727        let buf = MirLocal {
4728            slot: LocalId(0),
4729            last_use: true,
4730            name: "b".to_string(),
4731        };
4732        let call = MirCall {
4733            callee: MirCallee::Intrinsic(BuiltinIntrinsic::BufFinalize),
4734            args: vec![span(MirExpr::Local(span(buf)))],
4735        };
4736        let expr = span(MirExpr::Call(span(call)));
4737        let emit = emit_mir_expr(&expr, &empty_ctx()).expect("__buf_finalize emits");
4738        assert_eq!(emit, "aver_rt::AverStr::from(b)");
4739    }
4740
4741    #[test]
4742    fn emits_return_keyword() {
4743        let inner = span(MirExpr::Literal(span(crate::ast::Literal::Int(7))));
4744        let expr = span(MirExpr::Return(Box::new(inner)));
4745        let emit = emit_mir_expr(&expr, &empty_ctx()).expect("return should emit");
4746        assert_eq!(emit, "return aver_rt::AverInt::from_i64(7)");
4747    }
4748
4749    fn symbols_with_one_type(name: &str, scoped: bool) -> SymbolTable {
4750        use crate::ir::ModuleId;
4751        use crate::ir::identity::TypeKey;
4752        use crate::ir::symbol_table::{ModuleEntry, TypeEntry};
4753        let mut st = SymbolTable::default();
4754        st.modules.push(ModuleEntry { prefix: None });
4755        let key = if scoped {
4756            TypeKey::in_module("Tcp", name)
4757        } else {
4758            TypeKey::entry(name)
4759        };
4760        st.types.push(TypeEntry {
4761            key,
4762            module: ModuleId(0),
4763            index_in_module: 0,
4764            variants: vec![],
4765            is_product: true,
4766        });
4767        st
4768    }
4769
4770    #[test]
4771    fn emits_record_create_unscoped() {
4772        // `Point { x: 1, y: 2 }`. HIR-parity: the walker emits the
4773        // verbatim source-level `type_name` (`MirRecordCreate.type_name`),
4774        // the same string the HIR walker reads — not a symbol-table
4775        // lookup. The resolver leaves the user-typed name bare.
4776        let field_x = crate::ir::mir::MirRecordField {
4777            name: "x".to_string(),
4778            value: span(MirExpr::Literal(span(crate::ast::Literal::Int(1)))),
4779        };
4780        let field_y = crate::ir::mir::MirRecordField {
4781            name: "y".to_string(),
4782            value: span(MirExpr::Literal(span(crate::ast::Literal::Int(2)))),
4783        };
4784        let rec = crate::ir::mir::MirRecordCreate {
4785            type_id: Some(crate::ir::TypeId(0)),
4786            type_name: "Point".to_string(),
4787            fields: vec![field_x, field_y],
4788        };
4789        let expr = span(MirExpr::RecordCreate(span(rec)));
4790        let st = symbols_with_one_type("Point", false);
4791        let prefixes = HashSet::new();
4792        let ctx = MirEmitCtx::for_test(&st, &prefixes);
4793        let emit = emit_mir_expr(&expr, &ctx).expect("record create should emit");
4794        assert_eq!(
4795            emit,
4796            "Point { x: aver_rt::AverInt::from_i64(1), y: aver_rt::AverInt::from_i64(2) }"
4797        );
4798    }
4799
4800    #[test]
4801    fn emits_tcp_connection_record_with_rename() {
4802        // `Tcp.Connection` is the lone hardcoded special-case: HIR
4803        // renames it to the re-exported `Tcp_Connection` struct
4804        // inline. The MIR walker mirrors that exactly (no bounce) so
4805        // the output is byte-identical to HIR.
4806        let rec = crate::ir::mir::MirRecordCreate {
4807            type_id: Some(crate::ir::TypeId(0)),
4808            type_name: "Tcp.Connection".to_string(),
4809            fields: vec![],
4810        };
4811        let expr = span(MirExpr::RecordCreate(span(rec)));
4812        let st = symbols_with_one_type("Connection", true);
4813        let prefixes = HashSet::new();
4814        let ctx = MirEmitCtx::for_test(&st, &prefixes);
4815        let emit = emit_mir_expr(&expr, &ctx).expect("tcp connection record should emit");
4816        assert_eq!(emit, "Tcp_Connection {  }");
4817    }
4818
4819    #[test]
4820    fn emits_terminal_size_record_with_rename() {
4821        // `Terminal.Size` is renamed to the re-exported `Terminal_Size`
4822        // struct (alias `pub use aver_rt::TerminalSize as Terminal_Size`),
4823        // mirroring the `Tcp.Connection` special-case — so the dotted
4824        // ctor `Terminal.Size(width = .., height = ..)` emits a valid Rust
4825        // struct literal instead of the malformed `Terminal.Size { .. }`.
4826        let field_w = crate::ir::mir::MirRecordField {
4827            name: "width".to_string(),
4828            value: span(MirExpr::Literal(span(crate::ast::Literal::Int(80)))),
4829        };
4830        let field_h = crate::ir::mir::MirRecordField {
4831            name: "height".to_string(),
4832            value: span(MirExpr::Literal(span(crate::ast::Literal::Int(24)))),
4833        };
4834        let rec = crate::ir::mir::MirRecordCreate {
4835            type_id: Some(crate::ir::TypeId(0)),
4836            type_name: "Terminal.Size".to_string(),
4837            fields: vec![field_w, field_h],
4838        };
4839        let expr = span(MirExpr::RecordCreate(span(rec)));
4840        let st = symbols_with_one_type("Size", true);
4841        let prefixes = HashSet::new();
4842        let ctx = MirEmitCtx::for_test(&st, &prefixes);
4843        let emit = emit_mir_expr(&expr, &ctx).expect("terminal size record should emit");
4844        assert_eq!(
4845            emit,
4846            "Terminal_Size { width: aver_rt::AverInt::from_i64(80), height: aver_rt::AverInt::from_i64(24) }"
4847        );
4848    }
4849
4850    #[test]
4851    fn emits_record_create_dep_module_as_bare_name() {
4852        // A dep-module record emits the bare type name the user
4853        // typed (`Expr { … }`) — the resolver doesn't dot-prefix
4854        // `RecordCreate.type_name`, and the consumer module's import
4855        // makes `Expr` resolve. HIR-parity via the verbatim
4856        // `type_name` string.
4857        let field = crate::ir::mir::MirRecordField {
4858            name: "tag".to_string(),
4859            value: span(MirExpr::Literal(span(crate::ast::Literal::Int(1)))),
4860        };
4861        let rec = crate::ir::mir::MirRecordCreate {
4862            type_id: Some(crate::ir::TypeId(0)),
4863            type_name: "Expr".to_string(),
4864            fields: vec![field],
4865        };
4866        let expr = span(MirExpr::RecordCreate(span(rec)));
4867        use crate::ir::ModuleId;
4868        use crate::ir::identity::TypeKey;
4869        use crate::ir::symbol_table::{ModuleEntry, TypeEntry};
4870        let mut st = SymbolTable::default();
4871        st.modules.push(ModuleEntry { prefix: None });
4872        st.types.push(TypeEntry {
4873            key: TypeKey::in_module("ast", "Expr"),
4874            module: ModuleId(0),
4875            index_in_module: 0,
4876            variants: vec![],
4877            is_product: true,
4878        });
4879        let prefixes = HashSet::new();
4880        let ctx = MirEmitCtx::for_test(&st, &prefixes);
4881        let emit = emit_mir_expr(&expr, &ctx).expect("dep-module record should emit");
4882        assert_eq!(emit, "Expr { tag: aver_rt::AverInt::from_i64(1) }");
4883    }
4884
4885    #[test]
4886    fn emits_record_create_dep_module_qualified_when_prefix_registered() {
4887        // Residual-2 fix: when the owning module's prefix IS registered
4888        // in `module_prefixes` (the verify-test codegen path, where the
4889        // `#[cfg(test)]` module has no glob `use` bringing the dep type
4890        // into scope), a cross-module `RecordCreate` must emit the
4891        // module-mangled Rust path — not the bare name. This is the
4892        // sibling of the `Construct(User)` ctor mangling.
4893        let field = crate::ir::mir::MirRecordField {
4894            name: "id".to_string(),
4895            value: span(MirExpr::Literal(span(crate::ast::Literal::Int(1)))),
4896        };
4897        let rec = crate::ir::mir::MirRecordCreate {
4898            type_id: Some(crate::ir::TypeId(0)),
4899            type_name: "Note".to_string(),
4900            fields: vec![field],
4901        };
4902        let expr = span(MirExpr::RecordCreate(span(rec)));
4903        use crate::ir::ModuleId;
4904        use crate::ir::identity::TypeKey;
4905        use crate::ir::symbol_table::{ModuleEntry, TypeEntry};
4906        let mut st = SymbolTable::default();
4907        st.modules.push(ModuleEntry { prefix: None });
4908        st.types.push(TypeEntry {
4909            key: TypeKey::in_module("Apps.Notepad.Store", "Note"),
4910            module: ModuleId(0),
4911            index_in_module: 0,
4912            variants: vec![],
4913            is_product: true,
4914        });
4915        let mut prefixes = HashSet::new();
4916        prefixes.insert("Apps.Notepad.Store".to_string());
4917        let ctx = MirEmitCtx::for_test(&st, &prefixes);
4918        let emit = emit_mir_expr(&expr, &ctx).expect("qualified dep-module record should emit");
4919        assert_eq!(
4920            emit,
4921            "crate::aver_generated::apps::notepad::store::Note { id: aver_rt::AverInt::from_i64(1) }"
4922        );
4923    }
4924
4925    #[test]
4926    fn emits_record_update_unscoped() {
4927        // `T { field: v, ..base }`. Verbatim `type_name`; `base`
4928        // routed through `clone_arg` (here the empty borrow policy
4929        // means a non-last-use local clones).
4930        let base = MirLocal {
4931            slot: LocalId(0),
4932            last_use: false,
4933            name: "base".to_string(),
4934        };
4935        let update = crate::ir::mir::MirRecordField {
4936            name: "x".to_string(),
4937            value: span(MirExpr::Literal(span(crate::ast::Literal::Int(9)))),
4938        };
4939        let upd = crate::ir::mir::MirRecordUpdate {
4940            base: Box::new(span(MirExpr::Local(span(base)))),
4941            type_id: Some(crate::ir::TypeId(0)),
4942            type_name: "Point".to_string(),
4943            updates: vec![update],
4944        };
4945        let expr = span(MirExpr::RecordUpdate(span(upd)));
4946        let st = symbols_with_one_type("Point", false);
4947        let prefixes = HashSet::new();
4948        let ctx = MirEmitCtx::for_test(&st, &prefixes);
4949        let emit = emit_mir_expr(&expr, &ctx).expect("record update should emit");
4950        // `base` is a non-last-use, non-Copy local → `clone_arg`
4951        // clones it, exactly as HIR's `maybe_clone` does for a
4952        // `Resolved { last_use: false }` non-Copy local. (A
4953        // `MirLocal` is always a local read — the resolver's
4954        // global-Ident passthrough doesn't apply.)
4955        assert_eq!(
4956            emit,
4957            "Point { x: aver_rt::AverInt::from_i64(9), ..base.clone() }"
4958        );
4959    }
4960
4961    fn symbols_with_one_fn(name: &str) -> SymbolTable {
4962        use crate::ir::ModuleId;
4963        use crate::ir::identity::FnKey;
4964        use crate::ir::symbol_table::{FnEntry, ModuleEntry};
4965        let mut st = SymbolTable::default();
4966        st.modules.push(ModuleEntry { prefix: None });
4967        st.fns.push(FnEntry {
4968            key: FnKey::entry(name),
4969            module: ModuleId(0),
4970            index_in_module: 0,
4971        });
4972        st
4973    }
4974
4975    #[test]
4976    fn emits_tail_call_as_regular_call() {
4977        // Outside-loop `TailCall` mirrors HIR's
4978        // regular-call emit shape — `name(args)`.
4979        let arg = span(MirExpr::Literal(span(crate::ast::Literal::Int(7))));
4980        let tc = span(MirExpr::TailCall(span(crate::ir::mir::MirTailCall {
4981            target: crate::ir::FnId(0),
4982            args: vec![arg],
4983        })));
4984        let st = symbols_with_one_fn("loop_step");
4985        let prefixes = HashSet::new();
4986        let ctx = MirEmitCtx::for_test(&st, &prefixes);
4987        let emit = emit_mir_expr(&tc, &ctx).expect("tail call should emit");
4988        assert_eq!(emit, "loop_step(aver_rt::AverInt::from_i64(7))");
4989    }
4990
4991    #[test]
4992    fn returns_none_for_unsupported_variant() {
4993        // Pick a variant the walker doesn't cover — `InterpolatedStr`.
4994        // (The pipeline contract guarantees `ir::interp_lower` rewrites it
4995        // away before Rust codegen, so the walker deliberately leaves it in
4996        // the `_ => None` catch-all; reaching it raw signals fall back to
4997        // HIR.)
4998        let interp = span(MirExpr::InterpolatedStr(vec![
4999            crate::ir::mir::MirStrPart::Literal("x".to_string()),
5000        ]));
5001        assert!(emit_mir_expr(&interp, &empty_ctx()).is_none());
5002    }
5003
5004    #[test]
5005    fn emits_empty_map_as_hashmap_new() {
5006        // Empty map literal.
5007        let expr = span(MirExpr::MapLiteral(vec![]));
5008        let emit = emit_mir_expr(&expr, &empty_ctx()).expect("map should emit");
5009        assert_eq!(emit, "HashMap::new()");
5010    }
5011
5012    #[test]
5013    fn emits_nonempty_map_as_vec_into_iter_collect() {
5014        // Non-empty map literal.
5015        let k1 = span(MirExpr::Literal(span(crate::ast::Literal::Int(1))));
5016        let v1 = span(MirExpr::Literal(span(crate::ast::Literal::Int(10))));
5017        let k2 = span(MirExpr::Literal(span(crate::ast::Literal::Int(2))));
5018        let v2 = span(MirExpr::Literal(span(crate::ast::Literal::Int(20))));
5019        let expr = span(MirExpr::MapLiteral(vec![(k1, v1), (k2, v2)]));
5020        let emit = emit_mir_expr(&expr, &empty_ctx()).expect("map should emit");
5021        assert_eq!(
5022            emit,
5023            "vec![(aver_rt::AverInt::from_i64(1), aver_rt::AverInt::from_i64(10)), (aver_rt::AverInt::from_i64(2), aver_rt::AverInt::from_i64(20))].into_iter().collect::<HashMap<_, _>>()"
5024        );
5025    }
5026
5027    #[test]
5028    fn emits_try_as_question_mark() {
5029        // `Try(inner)` → `inner?`.
5030        let inner = span(MirExpr::Literal(span(crate::ast::Literal::Int(7))));
5031        let expr = span(MirExpr::Try(Box::new(inner)));
5032        let emit = emit_mir_expr(&expr, &empty_ctx()).expect("try should emit");
5033        assert_eq!(emit, "aver_rt::AverInt::from_i64(7)?");
5034    }
5035
5036    #[test]
5037    fn emits_tuple_literal_as_paren_list() {
5038        // `(7, 9)` tuple.
5039        let a = span(MirExpr::Literal(span(crate::ast::Literal::Int(7))));
5040        let b = span(MirExpr::Literal(span(crate::ast::Literal::Int(9))));
5041        let expr = span(MirExpr::Tuple(vec![a, b]));
5042        let emit = emit_mir_expr(&expr, &empty_ctx()).expect("tuple should emit");
5043        assert_eq!(
5044            emit,
5045            "(aver_rt::AverInt::from_i64(7), aver_rt::AverInt::from_i64(9))"
5046        );
5047    }
5048
5049    #[test]
5050    fn emits_bare_independent_product_as_parallel_tuple() {
5051        // `(7, 9)!` — bare product (no unwrap). No replay (empty ctx),
5052        // so the parallel `thread::scope` body folds straight into a
5053        // tuple via `emit_tuple_from_vars`. Byte-identical to HIR's
5054        // `ResolvedExpr::IndependentProduct` `!` arm.
5055        let a = span(MirExpr::Literal(span(crate::ast::Literal::Int(7))));
5056        let b = span(MirExpr::Literal(span(crate::ast::Literal::Int(9))));
5057        let expr = span(MirExpr::IndependentProduct(span(
5058            crate::ir::mir::MirIndependentProduct {
5059                items: vec![a, b],
5060                unwrap_results: false,
5061            },
5062        )));
5063        let emit = emit_mir_expr(&expr, &empty_ctx()).expect("bare product should emit");
5064        assert_eq!(
5065            emit,
5066            "std::thread::scope(|_s| { let _h0 = _s.spawn(move || aver_rt::AverInt::from_i64(7)); \
5067             let _h1 = _s.spawn(move || aver_rt::AverInt::from_i64(9)); let _r0 = _h0.join().unwrap(); \
5068             let _r1 = _h1.join().unwrap(); (_r0, _r1) }) "
5069        );
5070    }
5071
5072    #[test]
5073    fn emits_unwrap_independent_product_with_cancel_flag() {
5074        // `(7, 9)?!` — unwrap product. No replay (empty ctx), so the
5075        // `?!` path emits the shared `__cancel_flag`, one
5076        // `run_cancelable_branch` spawn per element, joins, then the
5077        // `emit_parallel_result_tuple_unwrap` fold + trailing `?`.
5078        // Byte-identical to HIR's `ResolvedExpr::IndependentProduct`
5079        // `?!` arm.
5080        let a = span(MirExpr::Literal(span(crate::ast::Literal::Int(7))));
5081        let b = span(MirExpr::Literal(span(crate::ast::Literal::Int(9))));
5082        let expr = span(MirExpr::IndependentProduct(span(
5083            crate::ir::mir::MirIndependentProduct {
5084                items: vec![a, b],
5085                unwrap_results: true,
5086            },
5087        )));
5088        let emit = emit_mir_expr(&expr, &empty_ctx()).expect("unwrap product should emit");
5089        assert!(
5090            emit.starts_with(
5091                "{ let __cancel_flag = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); \
5092                 std::thread::scope(|_s| { "
5093            ),
5094            "got: {emit}"
5095        );
5096        assert!(
5097            emit.contains("crate::run_cancelable_branch(__cancel_flag0"),
5098            "got: {emit}"
5099        );
5100        assert!(
5101            emit.contains("crate::run_cancelable_branch(__cancel_flag1"),
5102            "got: {emit}"
5103        );
5104        assert!(
5105            emit.contains("crate::ParallelBranch::Completed"),
5106            "got: {emit}"
5107        );
5108        assert!(emit.trim_end().ends_with("})? }"), "got: {emit}");
5109    }
5110
5111    #[test]
5112    fn emits_empty_list_as_averlist_empty() {
5113        let expr = span(MirExpr::List(vec![]));
5114        let emit = emit_mir_expr(&expr, &empty_ctx()).expect("list should emit");
5115        assert_eq!(emit, "aver_rt::AverList::empty()");
5116    }
5117
5118    #[test]
5119    fn emits_nonempty_list_as_from_vec() {
5120        let a = span(MirExpr::Literal(span(crate::ast::Literal::Int(1))));
5121        let b = span(MirExpr::Literal(span(crate::ast::Literal::Int(2))));
5122        let expr = span(MirExpr::List(vec![a, b]));
5123        let emit = emit_mir_expr(&expr, &empty_ctx()).expect("list should emit");
5124        assert_eq!(
5125            emit,
5126            "aver_rt::AverList::from_vec(vec![aver_rt::AverInt::from_i64(1), aver_rt::AverInt::from_i64(2)])"
5127        );
5128    }
5129
5130    #[test]
5131    fn emits_project_as_dotted_field() {
5132        // `base.field` projection.
5133        let local = MirLocal {
5134            slot: LocalId(0),
5135            last_use: false,
5136            name: "user".to_string(),
5137        };
5138        let base = span(MirExpr::Local(span(local)));
5139        let proj = crate::ir::mir::MirProject {
5140            base: Box::new(base),
5141            field: "name".to_string(),
5142        };
5143        let expr = span(MirExpr::Project(span(proj)));
5144        let emit = emit_mir_expr(&expr, &empty_ctx()).expect("project should emit");
5145        assert!(
5146            emit.ends_with(".name"),
5147            "project should end with `.name`, got: {emit}"
5148        );
5149    }
5150
5151    #[test]
5152    fn emits_result_ok_as_ok_call() {
5153        // BuiltinCtor::ResultOk → `Ok(arg)`.
5154        let arg = span(MirExpr::Literal(span(crate::ast::Literal::Int(42))));
5155        let con = crate::ir::mir::MirConstruct {
5156            ctor: MirCtor::Builtin(BuiltinCtor::ResultOk),
5157            args: vec![arg],
5158        };
5159        let expr = span(MirExpr::Construct(span(con)));
5160        let emit = emit_mir_expr(&expr, &empty_ctx()).expect("construct should emit");
5161        assert_eq!(emit, "Ok(aver_rt::AverInt::from_i64(42))");
5162    }
5163
5164    #[test]
5165    fn emits_option_none_as_bare_none() {
5166        // BuiltinCtor::OptionNone has no args
5167        // and emits `None` without parens.
5168        let con = crate::ir::mir::MirConstruct {
5169            ctor: MirCtor::Builtin(BuiltinCtor::OptionNone),
5170            args: vec![],
5171        };
5172        let expr = span(MirExpr::Construct(span(con)));
5173        let emit = emit_mir_expr(&expr, &empty_ctx()).expect("construct should emit");
5174        assert_eq!(emit, "None");
5175    }
5176
5177    #[test]
5178    fn emits_let_as_block_expr() {
5179        // `let x = 7; x` → `{ let x = aver_rt::AverInt::from_i64(7); x }`.
5180        let value = span(MirExpr::Literal(span(crate::ast::Literal::Int(7))));
5181        let body_local = MirLocal {
5182            slot: LocalId(0),
5183            last_use: false,
5184            name: "x".to_string(),
5185        };
5186        let body = span(MirExpr::Local(span(body_local)));
5187        let let_node = crate::ir::mir::MirLet {
5188            binding: LocalId(0),
5189            binding_name: "x".to_string(),
5190            value: Box::new(value),
5191            body: Box::new(body),
5192        };
5193        let expr = span(MirExpr::Let(span(let_node)));
5194        let emit = emit_mir_expr(&expr, &empty_ctx()).expect("let should emit");
5195        assert_eq!(emit, "{ let x = aver_rt::AverInt::from_i64(7); x }");
5196    }
5197
5198    #[test]
5199    fn synthetic_let_emits_bare_statement_not_none() {
5200        // A synthetic Let (intermediate effectful `Stmt::Expr` at non-tail
5201        // position, or a `_ = effect()` discard) carries an empty
5202        // `binding_name`. Stage-3 closes the former None gap: the walker
5203        // now emits the value as a bare statement (`{ value; body }`,
5204        // result dropped) instead of bailing to HIR.
5205        let value = span(MirExpr::Literal(span(crate::ast::Literal::Int(7))));
5206        let body = span(MirExpr::Literal(span(crate::ast::Literal::Int(0))));
5207        let let_node = crate::ir::mir::MirLet {
5208            binding: LocalId(7),
5209            binding_name: String::new(),
5210            value: Box::new(value),
5211            body: Box::new(body),
5212        };
5213        let expr = span(MirExpr::Let(span(let_node)));
5214        assert_eq!(
5215            emit_mir_expr(&expr, &empty_ctx()).as_deref(),
5216            Some("{ aver_rt::AverInt::from_i64(7); aver_rt::AverInt::from_i64(0) }")
5217        );
5218    }
5219
5220    /// Build a symbol table holding one type + one variant ctor.
5221    /// `scope_prefix == Some("foo")` for module-scoped types.
5222    fn symbols_with_one_user_ctor(
5223        scope_prefix: Option<&str>,
5224        type_name: &str,
5225        variant_name: &str,
5226    ) -> SymbolTable {
5227        use crate::ir::ModuleId;
5228        use crate::ir::identity::TypeKey;
5229        use crate::ir::symbol_table::{CtorEntry, ModuleEntry, TypeEntry};
5230        let mut st = SymbolTable::default();
5231        st.modules.push(ModuleEntry { prefix: None });
5232        let key = match scope_prefix {
5233            Some(p) => TypeKey::in_module(p, type_name),
5234            None => TypeKey::entry(type_name),
5235        };
5236        st.types.push(TypeEntry {
5237            key,
5238            module: ModuleId(0),
5239            index_in_module: 0,
5240            variants: vec![crate::ir::CtorId(0)],
5241            is_product: false,
5242        });
5243        st.ctors.push(CtorEntry {
5244            owning_type: crate::ir::TypeId(0),
5245            name: variant_name.to_string(),
5246        });
5247        st
5248    }
5249
5250    #[test]
5251    fn emits_user_ctor_unscoped() {
5252        // `Shape.Circle(r)` (bare type) →
5253        // `Shape::Circle(r)`.
5254        let arg = span(MirExpr::Literal(span(crate::ast::Literal::Int(7))));
5255        let con = crate::ir::mir::MirConstruct {
5256            ctor: MirCtor::User(crate::ir::CtorId(0)),
5257            args: vec![arg],
5258        };
5259        let expr = span(MirExpr::Construct(span(con)));
5260        let st = symbols_with_one_user_ctor(None, "Shape", "Circle");
5261        let prefixes = HashSet::new();
5262        let ctx = MirEmitCtx::for_test(&st, &prefixes);
5263        let emit = emit_mir_expr(&expr, &ctx).expect("user ctor should emit");
5264        assert_eq!(emit, "Shape::Circle(aver_rt::AverInt::from_i64(7))");
5265    }
5266
5267    #[test]
5268    fn emits_user_ctor_scoped_via_module_prefix() {
5269        // Dep-module ctor resolved through
5270        // `module_prefixes` + `module_prefix_to_rust_path`.
5271        // `ast.Expr.App(x)` → `crate::aver_generated::ast::Expr::App(x)`.
5272        let arg = span(MirExpr::Literal(span(crate::ast::Literal::Int(1))));
5273        let con = crate::ir::mir::MirConstruct {
5274            ctor: MirCtor::User(crate::ir::CtorId(0)),
5275            args: vec![arg],
5276        };
5277        let expr = span(MirExpr::Construct(span(con)));
5278        let st = symbols_with_one_user_ctor(Some("ast"), "Expr", "App");
5279        let mut prefixes = HashSet::new();
5280        prefixes.insert("ast".to_string());
5281        let ctx = MirEmitCtx::for_test(&st, &prefixes);
5282        let emit = emit_mir_expr(&expr, &ctx).expect("scoped user ctor should emit");
5283        assert_eq!(
5284            emit,
5285            "crate::aver_generated::ast::Expr::App(aver_rt::AverInt::from_i64(1))"
5286        );
5287    }
5288
5289    #[test]
5290    fn first_blocker_names_a_top_level_match() {
5291        // A bare `Match` is an uncovered variant — `first_blocker`
5292        // must name it "Match" so the coverage histogram reads as a
5293        // worklist.
5294        let m = span(MirExpr::Match(span(crate::ir::mir::MirMatch {
5295            subject: Box::new(span(MirExpr::Literal(span(crate::ast::Literal::Int(0))))),
5296            arms: vec![],
5297        })));
5298        assert!(emit_mir_expr(&m, &empty_ctx()).is_none());
5299        assert_eq!(first_blocker(&m, &empty_ctx()), Some("Match"));
5300    }
5301
5302    #[test]
5303    fn first_blocker_recurses_to_deepest_builtin_call() {
5304        // `return (builtinCall(...))` — the outer Return emits cleanly
5305        // over a covered child, so the blocker the histogram reports
5306        // must be the *builtin call kind*, not the Return wrapper.
5307        let call = span(MirExpr::Call(span(MirCall {
5308            callee: MirCallee::Builtin(crate::ir::BuiltinId(0)),
5309            args: vec![span(MirExpr::Literal(span(crate::ast::Literal::Int(1))))],
5310        })));
5311        let ret = span(MirExpr::Return(Box::new(call)));
5312        assert!(emit_mir_expr(&ret, &empty_ctx()).is_none());
5313        assert_eq!(first_blocker(&ret, &empty_ctx()), Some("Call(Builtin)"));
5314    }
5315
5316    #[test]
5317    fn first_blocker_is_none_for_fully_covered_body() {
5318        // A clean integer literal has no blocker.
5319        let lit = span(MirExpr::Literal(span(crate::ast::Literal::Int(42))));
5320        assert!(first_blocker(&lit, &empty_ctx()).is_none());
5321    }
5322
5323    /// Minimal `MirFn` carrying just a body — every other field is a
5324    /// neutral default so the coverage walk (which only reads `body`)
5325    /// has something well-formed to traverse.
5326    fn fn_with_body(fn_id: crate::ir::FnId, body: Spanned<MirExpr>) -> crate::ir::mir::MirFn {
5327        crate::ir::mir::MirFn {
5328            fn_id,
5329            name: String::new(),
5330            params: vec![],
5331            return_type: String::new(),
5332            effects: vec![],
5333            body,
5334            local_count: 0,
5335            aliased_slots: std::sync::Arc::new(vec![]),
5336            repr: crate::ir::mir::MirFnRepr::default(),
5337        }
5338    }
5339
5340    #[test]
5341    fn coverage_with_blockers_counts_and_buckets() {
5342        // Build a two-fn program: one emits (a literal), one blocks on
5343        // Match. The report must read 1 covered / 1 fallback with a
5344        // single "Match" bucket of count 1.
5345        let mut program = MirProgram::default();
5346        let covered_body = span(MirExpr::Literal(span(crate::ast::Literal::Int(7))));
5347        let blocked_body = span(MirExpr::Match(span(crate::ir::mir::MirMatch {
5348            subject: Box::new(span(MirExpr::Literal(span(crate::ast::Literal::Int(0))))),
5349            arms: vec![],
5350        })));
5351        program.fns.insert(
5352            crate::ir::FnId(0),
5353            fn_with_body(crate::ir::FnId(0), covered_body),
5354        );
5355        program.fns.insert(
5356            crate::ir::FnId(1),
5357            fn_with_body(crate::ir::FnId(1), blocked_body),
5358        );
5359
5360        let (report, blockers) = coverage_report_with_blockers(&program, &empty_ctx());
5361        assert_eq!(report.total, 2);
5362        assert_eq!(report.mir_covered, 1);
5363        assert_eq!(report.hir_fallback, 1);
5364        assert_eq!(blockers.get("Match"), Some(&1));
5365    }
5366
5367    // ── Wave 4 ──────────────────────────────────────────────────────
5368
5369    /// Build `let a = <a_val>; let b = <b_val>; <body>` as a nested
5370    /// MIR `Let` chain.
5371    fn let_chain(
5372        a: (&str, Spanned<MirExpr>),
5373        b: (&str, Spanned<MirExpr>),
5374        body: Spanned<MirExpr>,
5375    ) -> Spanned<MirExpr> {
5376        let inner = MirExpr::Let(span(crate::ir::mir::MirLet {
5377            binding: LocalId(1),
5378            binding_name: b.0.to_string(),
5379            value: Box::new(b.1),
5380            body: Box::new(body),
5381        }));
5382        span(MirExpr::Let(span(crate::ir::mir::MirLet {
5383            binding: LocalId(0),
5384            binding_name: a.0.to_string(),
5385            value: Box::new(a.1),
5386            body: Box::new(span(inner)),
5387        })))
5388    }
5389
5390    #[test]
5391    fn fn_body_emits_let_chain_as_flat_statement_lines() {
5392        // A top-level `Let` chain must render as flat `let …;`-lines —
5393        // the format HIR's `Block` body arm produces — NOT the nested
5394        // block-expr `{ let a = …; { let b = …; … } }` that an inline
5395        // `Let` renders. This is the Wave-4 multi-statement boundary.
5396        let a_local = MirLocal {
5397            slot: LocalId(0),
5398            last_use: true,
5399            name: "a".to_string(),
5400        };
5401        let body = let_chain(
5402            ("a", int_lit(1)),
5403            ("b", int_lit(2)),
5404            span(MirExpr::Local(span(a_local))),
5405        );
5406        let emit = emit_mir_fn_body(&body, &empty_ctx()).expect("let chain emits");
5407        assert_eq!(
5408            emit,
5409            "    crate::cancel_checkpoint();\n    let a = aver_rt::AverInt::from_i64(1);\n    let b = aver_rt::AverInt::from_i64(2);\n    a"
5410        );
5411    }
5412
5413    #[test]
5414    fn fn_body_emits_discarded_intermediate_as_bare_statement() {
5415        // A discarded intermediate (`Stmt::Expr` at non-tail position, or
5416        // a `_ = effect()` discard) is modeled as a `Let` with an EMPTY
5417        // `binding_name`. It must render as a bare `value;` statement (the
5418        // value evaluated, result dropped) — the mirror of HIR's non-last
5419        // `ResolvedStmt::Expr` arm — NOT fall back to HIR. This is the
5420        // dominant Stage-3 None gap.
5421        //
5422        // Shape: `g = <1>; <2 discarded>; g`
5423        let g_local = MirLocal {
5424            slot: LocalId(0),
5425            last_use: true,
5426            name: "g".to_string(),
5427        };
5428        let body = let_chain(
5429            ("g", int_lit(1)),
5430            ("", int_lit(2)), // discarded intermediate — empty binding_name
5431            span(MirExpr::Local(span(g_local))),
5432        );
5433        let emit = emit_mir_fn_body(&body, &empty_ctx()).expect("discarded stmt emits");
5434        assert_eq!(
5435            emit,
5436            "    crate::cancel_checkpoint();\n    let g = aver_rt::AverInt::from_i64(1);\n    aver_rt::AverInt::from_i64(2);\n    g"
5437        );
5438    }
5439
5440    #[test]
5441    fn fn_body_emits_leading_discarded_statement() {
5442        // A body whose FIRST statement is a discard (empty binding_name)
5443        // must still take the flat path (no first-binding guard) and emit
5444        // the leading bare statement.
5445        //
5446        // Shape: `<1 discarded>; g = <2>; g`
5447        let g_local = MirLocal {
5448            slot: LocalId(1),
5449            last_use: true,
5450            name: "g".to_string(),
5451        };
5452        let body = let_chain(
5453            ("", int_lit(1)), // leading discard
5454            ("g", int_lit(2)),
5455            span(MirExpr::Local(span(g_local))),
5456        );
5457        let emit = emit_mir_fn_body(&body, &empty_ctx()).expect("leading discard emits");
5458        assert_eq!(
5459            emit,
5460            "    crate::cancel_checkpoint();\n    aver_rt::AverInt::from_i64(1);\n    let g = aver_rt::AverInt::from_i64(2);\n    g"
5461        );
5462    }
5463
5464    #[test]
5465    fn inline_discarded_let_renders_as_bare_block_statement() {
5466        // An inline `Let` with an empty binding_name (discard not at the
5467        // body top-level) renders as `{ value; body }` — bare statement,
5468        // result dropped — not a `let _ = …`.
5469        let value = int_lit(7);
5470        let body_local = MirLocal {
5471            slot: LocalId(0),
5472            last_use: true,
5473            name: "x".to_string(),
5474        };
5475        let let_node = crate::ir::mir::MirLet {
5476            binding: LocalId(0),
5477            binding_name: String::new(),
5478            value: Box::new(value),
5479            body: Box::new(span(MirExpr::Local(span(body_local)))),
5480        };
5481        let expr = span(MirExpr::Let(span(let_node)));
5482        let emit = emit_mir_expr(&expr, &empty_ctx()).expect("inline discard emits");
5483        assert_eq!(emit, "{ aver_rt::AverInt::from_i64(7); x }");
5484    }
5485
5486    #[test]
5487    fn inline_let_still_renders_as_block_expr() {
5488        // An inline `Let` (NOT at top-level body position) still renders
5489        // as a nested block-expr — only the fn-body path flattens.
5490        let value = int_lit(7);
5491        let body_local = MirLocal {
5492            slot: LocalId(0),
5493            last_use: true,
5494            name: "x".to_string(),
5495        };
5496        let let_node = crate::ir::mir::MirLet {
5497            binding: LocalId(0),
5498            binding_name: "x".to_string(),
5499            value: Box::new(value),
5500            body: Box::new(span(MirExpr::Local(span(body_local)))),
5501        };
5502        let expr = span(MirExpr::Let(span(let_node)));
5503        let emit = emit_mir_expr(&expr, &empty_ctx()).expect("inline let emits");
5504        assert_eq!(emit, "{ let x = aver_rt::AverInt::from_i64(7); x }");
5505    }
5506
5507    #[test]
5508    fn neg_folded_int_literal_emits_signed_from_i64() {
5509        // `const_fold` collapses `Neg(Int(5))` → `Literal(-5)`; the walker
5510        // emits the already-signed `AverInt::from_i64(-5)` directly — no
5511        // `(-…)` re-wrap, since `AverInt` has no std `Neg`.
5512        let expr = span(MirExpr::Literal(span(crate::ast::Literal::Int(-5))));
5513        let emit = emit_mir_expr(&expr, &empty_ctx()).expect("neg int literal emits");
5514        assert_eq!(emit, "aver_rt::AverInt::from_i64(-5)");
5515    }
5516
5517    #[test]
5518    fn neg_folded_float_literal_re_wraps_like_hir_neg() {
5519        // `Neg(Float(273.15))` folds to `Literal(-273.15)`; re-wrap to
5520        // `(-273.15f64)` to match HIR's `(-273.15f64)`.
5521        let expr = span(MirExpr::Literal(span(crate::ast::Literal::Float(-273.15))));
5522        let emit = emit_mir_expr(&expr, &empty_ctx()).expect("neg float literal emits");
5523        assert_eq!(emit, "(-273.15f64)");
5524    }
5525
5526    #[test]
5527    fn positive_literals_unchanged_by_neg_rewrap() {
5528        // Positive literals are never wrapped.
5529        let i = span(MirExpr::Literal(span(crate::ast::Literal::Int(5))));
5530        assert_eq!(
5531            emit_mir_expr(&i, &empty_ctx()).as_deref(),
5532            Some("aver_rt::AverInt::from_i64(5)")
5533        );
5534        let f = span(MirExpr::Literal(span(crate::ast::Literal::Float(1.5))));
5535        assert_eq!(emit_mir_expr(&f, &empty_ctx()).as_deref(), Some("1.5f64"));
5536    }
5537
5538    /// Build an `IfThenElse` with a comparison `cond` of the given op
5539    /// over two named Int locals, and `Int` literal branches.
5540    fn if_compare(op: BinOp) -> Spanned<MirExpr> {
5541        let lhs = MirLocal {
5542            slot: LocalId(0),
5543            last_use: false,
5544            name: "code".to_string(),
5545        };
5546        let cond = MirExpr::BinOp(span(crate::ir::mir::MirBinOp {
5547            op,
5548            lhs: Box::new(span_ty(MirExpr::Local(span(lhs)), Type::Int)),
5549            rhs: Box::new(int_lit(48)),
5550        }));
5551        span(MirExpr::IfThenElse(span(crate::ir::mir::MirIfThenElse {
5552            cond: Box::new(span(cond)),
5553            then_branch: Box::new(int_lit(1)),
5554            else_branch: Box::new(int_lit(0)),
5555        })))
5556    }
5557
5558    #[test]
5559    fn if_then_else_keeps_lt_canonical_no_swap() {
5560        // `<` is canonical (invert=false): keep operator, branches in
5561        // source order.
5562        let emit = emit_mir_expr(&if_compare(BinOp::Lt), &empty_ctx()).expect("if emits");
5563        assert_eq!(
5564            emit,
5565            "if (code < aver_rt::AverInt::from_i64(48)) { aver_rt::AverInt::from_i64(1) } else { aver_rt::AverInt::from_i64(0) }"
5566        );
5567    }
5568
5569    #[test]
5570    fn if_then_else_inverts_gte_to_lt_and_swaps_branches() {
5571        // `>=` → HIR canonicalizes to `<` + invert (swap branches):
5572        // `if (code < 48) { else_branch } else { then_branch }`.
5573        let emit = emit_mir_expr(&if_compare(BinOp::Gte), &empty_ctx()).expect("if emits");
5574        assert_eq!(
5575            emit,
5576            "if (code < aver_rt::AverInt::from_i64(48)) { aver_rt::AverInt::from_i64(0) } else { aver_rt::AverInt::from_i64(1) }"
5577        );
5578    }
5579
5580    #[test]
5581    fn if_then_else_inverts_lte_to_gt_and_swaps_branches() {
5582        let emit = emit_mir_expr(&if_compare(BinOp::Lte), &empty_ctx()).expect("if emits");
5583        assert_eq!(
5584            emit,
5585            "if (code > aver_rt::AverInt::from_i64(48)) { aver_rt::AverInt::from_i64(0) } else { aver_rt::AverInt::from_i64(1) }"
5586        );
5587    }
5588
5589    #[test]
5590    fn if_then_else_inverts_neq_to_eq_and_swaps_branches() {
5591        let emit = emit_mir_expr(&if_compare(BinOp::Neq), &empty_ctx()).expect("if emits");
5592        assert_eq!(
5593            emit,
5594            "if (code == aver_rt::AverInt::from_i64(48)) { aver_rt::AverInt::from_i64(0) } else { aver_rt::AverInt::from_i64(1) }"
5595        );
5596    }
5597
5598    #[test]
5599    fn if_then_else_cond_does_not_deref_string_literal() {
5600        // HIR's bool-if-else condition uses a plain `emit_expr` — it
5601        // does NOT apply the `BinOp` arm's `&*name == "lit"` deref. So
5602        // `match name == "_"` emits `name == AverStr::from("_")` in the
5603        // cond, matching HIR byte-for-byte.
5604        let name = MirLocal {
5605            slot: LocalId(0),
5606            last_use: false,
5607            name: "name".to_string(),
5608        };
5609        let cond = MirExpr::BinOp(span(crate::ir::mir::MirBinOp {
5610            op: BinOp::Eq,
5611            lhs: Box::new(span_ty(MirExpr::Local(span(name)), Type::Str)),
5612            rhs: Box::new(span_ty(
5613                MirExpr::Literal(span(crate::ast::Literal::Str("_".to_string()))),
5614                Type::Str,
5615            )),
5616        }));
5617        let expr = span(MirExpr::IfThenElse(span(crate::ir::mir::MirIfThenElse {
5618            cond: Box::new(span(cond)),
5619            then_branch: Box::new(int_lit(1)),
5620            else_branch: Box::new(int_lit(0)),
5621        })));
5622        let emit = emit_mir_expr(&expr, &empty_ctx()).expect("if emits");
5623        assert_eq!(
5624            emit,
5625            "if (name == AverStr::from(\"_\")) { aver_rt::AverInt::from_i64(1) } else { aver_rt::AverInt::from_i64(0) }"
5626        );
5627    }
5628}