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