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