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