Skip to main content

aver/codegen/rust/
mod.rs

1/// Rust backend for the Aver transpiler.
2///
3/// Transforms Aver AST -> valid Rust source code.
4mod builtins;
5pub mod emit_ctx;
6mod expr;
7mod from_mir;
8pub use from_mir::{CoverageReport, MirEmitCtx, coverage_report, coverage_report_with_blockers};
9mod pattern;
10mod policy;
11mod project;
12mod replay;
13mod runtime;
14mod self_host;
15mod syntax;
16mod toplevel;
17mod types;
18
19use std::collections::{BTreeMap, BTreeSet, HashSet};
20
21use crate::ast::{FnDef, TopLevel, TypeDef};
22use crate::codegen::common::module_prefix_to_rust_segments;
23use crate::codegen::{CodegenContext, ProjectOutput};
24use crate::types::Type;
25
26/// Synthesize Rust's `mod.rs` cascade from a flat list of (segments, body)
27/// modules. Every parent directory along each module's path gets a
28/// `mod.rs` that declares `pub mod {child};` for each immediate child;
29/// the leaf node's `mod.rs` carries the body. Backend-local because the
30/// cascade is a Rust/Cargo-specific filesystem convention — Lean and
31/// Dafny just write the leaf path directly.
32fn synthesize_rust_module_cascade(
33    rel_dir: &str,
34    modules: &[(Vec<String>, String)],
35) -> Vec<(String, String)> {
36    let mut by_dir: BTreeMap<Vec<String>, (Option<String>, BTreeSet<String>)> = BTreeMap::new();
37    for (segments, content) in modules {
38        by_dir.entry(segments.clone()).or_default().0 = Some(content.clone());
39        for i in 0..segments.len() {
40            let parent: Vec<String> = segments[..i].to_vec();
41            by_dir
42                .entry(parent)
43                .or_default()
44                .1
45                .insert(segments[i].clone());
46        }
47    }
48    let mut files = Vec::new();
49    for (dir_segs, (content, children)) in by_dir {
50        let dir_path = if dir_segs.is_empty() {
51            rel_dir.to_string()
52        } else {
53            format!("{}/{}", rel_dir, dir_segs.join("/"))
54        };
55        let mut parts: Vec<String> = Vec::new();
56        if let Some(c) = content
57            && !c.trim().is_empty()
58        {
59            parts.push(c.trim_end().to_string());
60        }
61        for child in children {
62            parts.push(format!("pub mod {};", child));
63        }
64        let mut mod_rs = parts.join("\n\n");
65        if !mod_rs.is_empty() {
66            mod_rs.push('\n');
67        }
68        files.push((format!("{}/mod.rs", dir_path), mod_rs));
69    }
70    files
71}
72
73/// Transpile an Aver program to a Rust project.
74pub fn transpile(ctx: &mut CodegenContext) -> ProjectOutput {
75    let has_embedded_policy = ctx.policy.is_some();
76    let has_runtime_policy = ctx.runtime_policy_from_env;
77    let embedded_independence_cancel = ctx
78        .policy
79        .as_ref()
80        .is_some_and(|config| config.independence_mode == crate::config::IndependenceMode::Cancel);
81    let used_services = detect_used_services(ctx);
82    let needs_http_types =
83        needs_named_type(ctx, "HttpResponse") || needs_named_type(ctx, "HttpRequest");
84    let needs_tcp_types = needs_named_type(ctx, "Tcp.Connection");
85    let needs_terminal_types = needs_named_type(ctx, "Terminal.Size");
86    // Oracle-proof stub fns take a leading `BranchPath` param. They are
87    // emitted as dead-at-runtime fns (module-level fns emit regardless of
88    // reachability), so the type must be in scope to compile.
89    let needs_branch_path = needs_named_type(ctx, "BranchPath");
90
91    let has_tcp_runtime = used_services.contains("Tcp");
92    let has_http_runtime = used_services.contains("Http");
93    let has_http_server_runtime = used_services.contains("HttpServer");
94    let has_terminal_runtime = used_services.contains("Terminal");
95
96    let has_tcp_types = has_tcp_runtime || needs_tcp_types;
97    let has_http_types = has_http_runtime || has_http_server_runtime || needs_http_types;
98    let has_http_server_types = has_http_server_runtime || needs_named_type(ctx, "HttpRequest");
99    let has_terminal_types = has_terminal_runtime || needs_terminal_types;
100
101    // `main` fn lookup is identity-safe by parser invariant — only
102    // entry scope can declare it, and at most once. The view's
103    // `entry_fns()` enumerates the same set in the same source order,
104    // so iterating either substrate yields the same answer here.
105    // temporary-migration-bridge: downstream `render_root_main` still
106    // takes `Option<&FnDef>`; signature swap is a PR D follow-up.
107    let main_fn = ctx.fn_defs.iter().find(|fd| fd.name == "main");
108    debug_assert_eq!(
109        main_fn.is_some(),
110        ctx.resolved_program
111            .entry_fns()
112            .any(|rfd| rfd.name == "main"),
113        "ctx.fn_defs and ctx.resolved_program.entry_fns() must agree on \
114         main fn presence (epic #170 Phase 1 invariant)"
115    );
116    let top_level_stmts: Vec<_> = ctx
117        .items
118        .iter()
119        .filter_map(|item| {
120            if let TopLevel::Stmt(stmt) = item {
121                Some(stmt)
122            } else {
123                None
124            }
125        })
126        .collect();
127    let verify_blocks: Vec<_> = ctx
128        .items
129        .iter()
130        .filter_map(|item| {
131            if let TopLevel::Verify(vb) = item {
132                Some(vb)
133            } else {
134                None
135            }
136        })
137        .collect();
138
139    let mut files = vec![
140        (
141            "Cargo.toml".to_string(),
142            project::generate_cargo_toml(
143                &ctx.project_name,
144                &used_services,
145                has_embedded_policy,
146                has_runtime_policy,
147                ctx.emit_replay_runtime,
148            ),
149        ),
150        (
151            "src/main.rs".to_string(),
152            render_root_main(
153                main_fn,
154                has_embedded_policy,
155                ctx.emit_replay_runtime,
156                ctx.guest_entry.as_deref(),
157                !verify_blocks.is_empty(),
158                ctx.emit_self_host_support,
159            ),
160        ),
161        (
162            "src/runtime_support.rs".to_string(),
163            render_runtime_support(
164                has_tcp_types,
165                has_http_types,
166                has_http_server_types,
167                has_terminal_types,
168                needs_branch_path,
169                ctx.emit_replay_runtime,
170                embedded_independence_cancel,
171            ),
172        ),
173    ];
174
175    if ctx.emit_self_host_support {
176        files.push((
177            "src/self_host_support.rs".to_string(),
178            self_host::generate_self_host_support(),
179        ));
180    }
181
182    if has_embedded_policy && let Some(config) = &ctx.policy {
183        files.push((
184            "src/policy_support.rs".to_string(),
185            format!("{}\n", policy::generate_policy_runtime(config)),
186        ));
187    }
188
189    if ctx.emit_replay_runtime {
190        files.push((
191            "src/replay_support.rs".to_string(),
192            replay::generate_replay_runtime(
193                has_embedded_policy,
194                has_runtime_policy,
195                has_terminal_types,
196                has_tcp_types,
197                has_http_types,
198                has_http_server_types,
199                embedded_independence_cancel,
200            ),
201        ));
202    }
203
204    if !verify_blocks.is_empty() {
205        files.push((
206            "src/verify.rs".to_string(),
207            render_verify_module(&verify_blocks, ctx),
208        ));
209    }
210
211    let mut rust_modules: Vec<(Vec<String>, String)> = Vec::new();
212    rust_modules.push((
213        vec!["entry".to_string()],
214        render_generated_module(
215            root_module_depends(&ctx.items),
216            entry_module_sections(ctx, main_fn, &top_level_stmts),
217        ),
218    ));
219
220    for i in 0..ctx.modules.len() {
221        // Set extra_fn_defs so find_fn_def_by_name resolves intra-module
222        // bare-name calls (e.g. buildFibStats calling finalizeFibStats).
223        ctx.extra_fn_defs = ctx.modules[i].fn_defs.clone();
224        let module = &ctx.modules[i];
225        let segments = module_prefix_to_rust_segments(&module.prefix);
226        rust_modules.push((
227            segments,
228            render_generated_module(module.depends.clone(), module_sections(module, ctx)),
229        ));
230    }
231    ctx.extra_fn_defs.clear();
232
233    files.extend(synthesize_rust_module_cascade(
234        "src/aver_generated",
235        &rust_modules,
236    ));
237    files.sort_by(|left, right| left.0.cmp(&right.0));
238
239    ProjectOutput { files }
240}
241
242fn render_root_main(
243    main_fn: Option<&FnDef>,
244    has_policy: bool,
245    has_replay: bool,
246    guest_entry: Option<&str>,
247    has_verify: bool,
248    has_self_host_support: bool,
249) -> String {
250    let mut sections = vec![
251        "#![allow(unused_variables, unused_mut, dead_code, unused_imports, unused_parens, non_snake_case, non_camel_case_types, unreachable_patterns, hidden_glob_reexports)]".to_string(),
252        "// Aver Rust emission".to_string(),
253        "#[macro_use] extern crate aver_rt;".to_string(),
254        "pub use ::aver_rt::AverMap as HashMap;".to_string(),
255        "pub use ::aver_rt::AverStr;".to_string(),
256        "pub use ::aver_rt::Buffer;".to_string(),
257        String::new(),
258        "mod runtime_support;".to_string(),
259        "pub use runtime_support::*;".to_string(),
260    ];
261
262    if has_policy {
263        sections.push(String::new());
264        sections.push("mod policy_support;".to_string());
265        sections.push("pub use policy_support::*;".to_string());
266    }
267
268    if has_replay {
269        sections.push(String::new());
270        sections.push("mod replay_support;".to_string());
271        sections.push("pub use replay_support::*;".to_string());
272    }
273
274    if has_self_host_support {
275        sections.push(String::new());
276        sections.push("mod self_host_support;".to_string());
277    }
278
279    sections.push(String::new());
280    sections.push("pub mod aver_generated;".to_string());
281
282    if has_verify {
283        sections.push(String::new());
284        sections.push("#[cfg(test)]".to_string());
285        sections.push("mod verify;".to_string());
286    }
287
288    // Spawn main on a thread with 256 MB stack to avoid overflow in deep recursion.
289    sections.push(String::new());
290    let returns_result = main_fn.is_some_and(|fd| fd.return_type.starts_with("Result<"));
291    let result_unit_string =
292        main_fn.is_some_and(|fd| fd.return_type.replace(' ', "") == "Result<Unit,String>");
293    // `aver bench --target rust` sets `AVER_BENCH_ITER` (and optionally
294    // `AVER_BENCH_WARMUP`) to drive an in-process benchmark loop that
295    // calls `aver_generated::entry::main` N times under one process.
296    // Spawning the binary per-iter from the host harness floors at
297    // ~2–3 ms macOS process-spawn cost and reports pure noise on any
298    // workload below ~1 ms (`fib`, `factorial`, `record`). One env-var
299    // read on process start when unset; zero generated complexity in
300    // the user's main body.
301    let bench_dispatch_lines = |has_replay: bool| -> Vec<String> {
302        let user_call = if has_replay {
303            "aver_replay::with_guest_scope(\"main\", serde_json::Value::Null, aver_generated::entry::main)"
304        } else {
305            "aver_generated::entry::main()"
306        };
307        vec![
308            "    if let Ok(n_str) = std::env::var(\"AVER_BENCH_ITER\") {".to_string(),
309            "        let n: usize = n_str.parse().unwrap_or(0);".to_string(),
310            "        let warmup: usize = std::env::var(\"AVER_BENCH_WARMUP\").ok().and_then(|s| s.parse().ok()).unwrap_or(0);".to_string(),
311            "        for _ in 0..warmup {".to_string(),
312            format!("            let _ = std::hint::black_box({});", user_call),
313            "        }".to_string(),
314            "        for _ in 0..n {".to_string(),
315            "            let t = std::time::Instant::now();".to_string(),
316            format!("            let _ = std::hint::black_box({});", user_call),
317            "            eprintln!(\"__bench_iter_ms__: {}\", t.elapsed().as_secs_f64() * 1000.0);".to_string(),
318            "        }".to_string(),
319            "        std::process::exit(0);".to_string(),
320            "    }".to_string(),
321        ]
322    };
323    if returns_result {
324        if result_unit_string {
325            sections.push("fn main() {".to_string());
326            sections.extend(bench_dispatch_lines(has_replay && guest_entry.is_none()));
327            sections.push("    let child = std::thread::Builder::new()".to_string());
328            sections.push("        .stack_size(256 * 1024 * 1024)".to_string());
329            if has_replay && guest_entry.is_none() {
330                sections.push("        .spawn(|| {".to_string());
331                sections.push("            let __result = aver_replay::with_guest_scope(\"main\", serde_json::Value::Null, aver_generated::entry::main);".to_string());
332                sections.push("            __result.map_err(|e| e.to_string())".to_string());
333                sections.push("        })".to_string());
334            } else {
335                sections.push("        .spawn(|| {".to_string());
336                sections
337                    .push("            let __result = aver_generated::entry::main();".to_string());
338                sections.push("            __result.map_err(|e| e.to_string())".to_string());
339                sections.push("        })".to_string());
340            }
341            sections.push("        .expect(\"thread spawn\");".to_string());
342            sections.push("    match child.join().expect(\"thread join\") {".to_string());
343            sections.push("        Ok(()) => {}".to_string());
344            sections.push("        Err(e) => {".to_string());
345            sections.push("            eprintln!(\"{}\", e);".to_string());
346            sections.push("            std::process::exit(1);".to_string());
347            sections.push("        }".to_string());
348            sections.push("    }".to_string());
349        } else {
350            let ret_type = types::type_annotation_to_rust(&main_fn.unwrap().return_type);
351            sections.push(format!("fn main() -> {} {{", ret_type));
352            sections.extend(bench_dispatch_lines(has_replay && guest_entry.is_none()));
353            if has_replay && guest_entry.is_none() {
354                sections.push(
355                    "    aver_replay::with_guest_scope(\"main\", serde_json::Value::Null, aver_generated::entry::main)"
356                        .to_string(),
357                );
358            } else {
359                sections.push("    aver_generated::entry::main()".to_string());
360            }
361        }
362    } else {
363        sections.push("fn main() {".to_string());
364        if main_fn.is_some() {
365            sections.extend(bench_dispatch_lines(has_replay && guest_entry.is_none()));
366            sections.push("    let child = std::thread::Builder::new()".to_string());
367            sections.push("        .stack_size(256 * 1024 * 1024)".to_string());
368            if has_replay && guest_entry.is_none() {
369                sections.push("        .spawn(|| aver_replay::with_guest_scope(\"main\", serde_json::Value::Null, || aver_generated::entry::main()))".to_string());
370            } else {
371                sections.push("        .spawn(|| aver_generated::entry::main())".to_string());
372            }
373            sections.push("        .expect(\"thread spawn\");".to_string());
374            sections.push("    child.join().expect(\"thread join\");".to_string());
375        }
376    }
377    sections.push("}".to_string());
378    sections.push(String::new());
379
380    sections.join("\n")
381}
382
383fn render_runtime_support(
384    has_tcp_types: bool,
385    has_http_types: bool,
386    has_http_server_types: bool,
387    has_terminal_types: bool,
388    needs_branch_path: bool,
389    has_replay: bool,
390    embedded_independence_cancel: bool,
391) -> String {
392    let mut sections = vec![runtime::generate_runtime(
393        has_replay,
394        has_http_server_types,
395        embedded_independence_cancel,
396    )];
397    if has_tcp_types {
398        sections.push(runtime::generate_tcp_types());
399    }
400    if has_http_types {
401        sections.push(runtime::generate_http_types());
402    }
403    if has_http_server_types {
404        sections.push(runtime::generate_http_server_types());
405    }
406    if has_terminal_types {
407        sections.push(runtime::generate_terminal_types());
408    }
409    if needs_branch_path {
410        sections.push(runtime::generate_branch_path_types());
411    }
412    format!("{}\n", sections.join("\n\n"))
413}
414
415fn render_verify_module(
416    verify_blocks: &[&crate::ast::VerifyBlock],
417    ctx: &CodegenContext,
418) -> String {
419    [
420        "#[allow(unused_imports)]".to_string(),
421        "use crate::*;".to_string(),
422        "#[allow(unused_imports)]".to_string(),
423        "use crate::aver_generated::entry::*;".to_string(),
424        String::new(),
425        toplevel::emit_verify_blocks(verify_blocks, ctx),
426        String::new(),
427    ]
428    .join("\n")
429}
430
431fn render_generated_module(depends: Vec<String>, sections: Vec<String>) -> String {
432    if sections.is_empty() {
433        String::new()
434    } else {
435        let mut lines = vec![
436            "#[allow(unused_imports)]".to_string(),
437            "use crate::*;".to_string(),
438        ];
439        for dep in depends {
440            let path = module_prefix_to_rust_segments(&dep).join("::");
441            lines.push("#[allow(unused_imports)]".to_string());
442            lines.push(format!("use crate::aver_generated::{}::*;", path));
443        }
444        lines.push(String::new());
445        lines.push(sections.join("\n\n"));
446        lines.push(String::new());
447        lines.join("\n")
448    }
449}
450
451/// Emit one mutual-TCO block (enum + trampoline + wrappers) via the MIR
452/// walker. The HIR emitter is gone (rust-on-MIR W6/Stage-3): every
453/// member resolves to a `(MirFn, ResolvedFnDef)` pair and the trampoline
454/// is synthesized all-or-nothing from MIR. A member missing a `MirFn` /
455/// `ResolvedFnDef`, or a `None` from the walker, is a hard codegen error
456/// (`compile_error!` in the wrapper) — never a panic and never a silent
457/// drop. TCO is verified behaviorally (build + run vs VM + self-host
458/// regen), not by byte-parity.
459fn emit_mutual_tco_block_routed(
460    group_id: usize,
461    group_fns: &[&FnDef],
462    ctx: &CodegenContext,
463    scope: Option<&str>,
464    visibility: &str,
465) -> String {
466    // Resolve every member to its (MirFn, ResolvedFnDef) pair.
467    let resolved: Option<Vec<(&crate::ir::mir::MirFn, &crate::ir::hir::ResolvedFnDef)>> =
468        ctx.mir_program.as_ref().and_then(|prog| {
469            group_fns
470                .iter()
471                .map(|fd| {
472                    let fn_id = crate::codegen::common::fn_id_for_decl(ctx, fd)?;
473                    let mir_fn = prog.fn_by_id(fn_id)?;
474                    let resolved_fd = ctx.resolved_program.fn_by_id(fn_id)?;
475                    Some((mir_fn, resolved_fd))
476                })
477                .collect()
478        });
479    let code = resolved.and_then(|pairs| {
480        let mir_fns: Vec<&crate::ir::mir::MirFn> = pairs.iter().map(|(m, _)| *m).collect();
481        let resolved_fns: Vec<&crate::ir::hir::ResolvedFnDef> =
482            pairs.iter().map(|(_, r)| *r).collect();
483        from_mir::emit_mir_mutual_tco_block(
484            group_id,
485            group_fns,
486            &mir_fns,
487            &resolved_fns,
488            ctx,
489            scope,
490            visibility,
491        )
492    });
493    code.unwrap_or_else(|| {
494        let names = group_fns
495            .iter()
496            .map(|fd| fd.name.as_str())
497            .collect::<Vec<_>>()
498            .join(", ");
499        format!(
500            "{}fn __mutual_tco_block_{}_render_error() {{ compile_error!(\"MIR walker could not render mutual-TCO block [{}]\"); }}",
501            visibility, group_id, names
502        )
503    })
504}
505
506fn entry_module_sections(
507    ctx: &CodegenContext,
508    main_fn: Option<&FnDef>,
509    top_level_stmts: &[&crate::ast::Stmt],
510) -> Vec<String> {
511    let mut sections = Vec::new();
512
513    for td in &ctx.type_defs {
514        if is_shared_runtime_type(td) {
515            continue;
516        }
517        sections.push(toplevel::emit_public_type_def(td, ctx));
518        if ctx.emit_replay_runtime {
519            sections.push(replay::emit_replay_value_impl(td));
520        }
521    }
522
523    // Detect mutual TCO groups among non-main functions. Set-form membership
524    // is read from `ctx.mutual_tco_members` (populated by the analyze stage's
525    // unioned per-module sets); the index-keyed `groups` form still comes
526    // from `find_mutual_tco_groups` because trampoline emission needs the
527    // structural shape, not just the names.
528    // temporary-migration-bridge: `find_mutual_tco_groups` walks
529    // `&[&FnDef]` to detect tail-call SCCs against the raw AST body
530    // shape. Position-aligned with `ctx.resolved_program.entry_fns()`
531    // (both iterate the same source-ordered set). PR D migrates the
532    // SCC analyser to consume `ResolvedFnDef` bodies directly.
533    let non_main_fns: Vec<&FnDef> = ctx.fn_defs.iter().filter(|fd| fd.name != "main").collect();
534    let mutual_groups = toplevel::find_mutual_tco_groups(&non_main_fns);
535
536    for (group_id, group_indices) in mutual_groups.iter().enumerate() {
537        let group_fns: Vec<&FnDef> = group_indices.iter().map(|&idx| non_main_fns[idx]).collect();
538        sections.push(emit_mutual_tco_block_routed(
539            group_id + 1,
540            &group_fns,
541            ctx,
542            None,
543            "pub ",
544        ));
545    }
546
547    // Epic #170 Phase 4: emit each entry fn from a paired
548    // (`&FnDef`, `&ResolvedFnDef`) input. The AST view carries
549    // source-shape metadata the emitter still reads (param
550    // annotations, effects), the resolved view carries the body the
551    // expr emitter walks. `ctx.resolved_program.fn_by_id(fn_id)`
552    // is the identity-keyed lookup — no bare-name walk over the
553    // resolved list.
554    for fd in &ctx.fn_defs {
555        let Some(fn_id) = crate::codegen::common::fn_id_for_decl(ctx, fd) else {
556            continue;
557        };
558        let is_mutual = ctx.mutual_tco_members.contains(&fn_id);
559        if fd.name == "main" || is_mutual {
560            continue;
561        }
562        let Some(resolved_fd) = ctx.resolved_program.fn_by_id(fn_id) else {
563            // Synthetic FnDefs (TCO hoists) inserted post-pipeline don't
564            // have a resolved twin yet — fall back to on-demand resolve
565            // for those. `temporary-migration-bridge`: PR E moves
566            // synthetic-fn resolve into a typed builder.
567            let resolved_owned = ctx.resolve_fn_def(fd, None);
568            sections.push(toplevel::emit_public_fn_def(
569                fd,
570                resolved_owned.as_ref(),
571                ctx,
572                None,
573            ));
574            continue;
575        };
576        sections.push(toplevel::emit_public_fn_def(fd, resolved_fd, ctx, None));
577    }
578
579    if main_fn.is_some() || !top_level_stmts.is_empty() {
580        // rust-on-MIR W6/Stage-0: `main` carries a `ResolvedFnDef` (and so
581        // a lowered `MirFn`), reachable through the same identity-keyed
582        // `fn_id_for_decl` lookup the entry loop above runs for every
583        // other fn. Thread the FnId so the main-body emit can route the
584        // body through the MIR walker behind `AVER_RUST_MIR_MAIN`.
585        let main_fn_id = main_fn.and_then(|fd| crate::codegen::common::fn_id_for_decl(ctx, fd));
586        sections.push(toplevel::emit_public_main(
587            main_fn,
588            top_level_stmts,
589            ctx,
590            main_fn_id,
591        ));
592    }
593
594    sections
595}
596
597fn module_sections(module: &crate::codegen::ModuleInfo, ctx: &CodegenContext) -> Vec<String> {
598    let mut sections = Vec::new();
599
600    for td in &module.type_defs {
601        if is_shared_runtime_type(td) {
602            continue;
603        }
604        sections.push(toplevel::emit_public_type_def(td, ctx));
605        if ctx.emit_replay_runtime {
606            sections.push(replay::emit_replay_value_impl(td));
607        }
608    }
609
610    // Same shape as the entry path: groups (with indices) come from
611    // `find_mutual_tco_groups`, set-form membership reads from
612    // `module.analysis.mutual_tco_members` (bare names, scope-local
613    // per module, DAG invariant keeps them unambiguous) when the
614    // analyze stage ran. Fall back to projecting `ctx.mutual_tco_members`
615    // (FnId set) back to bare names for this module's scope.
616    let fn_refs: Vec<&FnDef> = module.fn_defs.iter().collect();
617    let mutual_groups = toplevel::find_mutual_tco_groups(&fn_refs);
618    let module_mutual_owned: HashSet<String> = match module.analysis.as_ref() {
619        Some(a) => a.mutual_tco_members.clone(),
620        None => ctx
621            .mutual_tco_members
622            .iter()
623            .filter_map(|id| {
624                let entry = ctx.symbol_table.fn_entry(*id);
625                entry
626                    .key
627                    .scope
628                    .as_deref()
629                    .filter(|s| *s == module.prefix)
630                    .map(|_| entry.key.name.clone())
631            })
632            .collect(),
633    };
634    let module_mutual = &module_mutual_owned;
635
636    for (group_id, group_indices) in mutual_groups.iter().enumerate() {
637        let group_fns: Vec<&FnDef> = group_indices.iter().map(|&idx| fn_refs[idx]).collect();
638        sections.push(emit_mutual_tco_block_routed(
639            group_id + 1,
640            &group_fns,
641            ctx,
642            Some(&module.prefix),
643            "pub ",
644        ));
645    }
646
647    for fd in &module.fn_defs {
648        if module_mutual.contains(&fd.name) {
649            continue;
650        }
651        // Same pair-API as the entry loop above. Module fns route
652        // through `fn_id_for_decl` (pointer-eq scope on `&FnDef`) →
653        // `resolved_program.fn_by_id(fn_id)` so a same-bare-name
654        // entry-scope twin never accidentally provides this body.
655        let resolved_fd = crate::codegen::common::fn_id_for_decl(ctx, fd)
656            .and_then(|id| ctx.resolved_program.fn_by_id(id));
657        let resolved_owned = if resolved_fd.is_some() {
658            None
659        } else {
660            // temporary-migration-bridge: synthetic / mid-rewrite fns
661            // fall back to on-demand resolve in the dep scope.
662            Some(ctx.resolve_fn_def(fd, Some(&module.prefix)))
663        };
664        let resolved_ref: &crate::ir::hir::ResolvedFnDef =
665            resolved_fd.unwrap_or_else(|| resolved_owned.as_ref().unwrap().as_ref());
666        sections.push(toplevel::emit_public_fn_def(
667            fd,
668            resolved_ref,
669            ctx,
670            Some(&module.prefix),
671        ));
672    }
673
674    sections
675}
676
677fn root_module_depends(items: &[TopLevel]) -> Vec<String> {
678    items
679        .iter()
680        .find_map(|item| {
681            if let TopLevel::Module(module) = item {
682                Some(module.depends.clone())
683            } else {
684                None
685            }
686        })
687        .unwrap_or_default()
688}
689
690/// Detect which effectful services are used in the program (including modules).
691fn detect_used_services(ctx: &CodegenContext) -> HashSet<String> {
692    let mut services = HashSet::new();
693    for item in &ctx.items {
694        if let TopLevel::FnDef(fd) = item {
695            for eff in &fd.effects {
696                services.insert(eff.node.clone());
697                if let Some((service, _)) = eff.node.split_once('.') {
698                    services.insert(service.to_string());
699                }
700            }
701        }
702    }
703    for module in &ctx.modules {
704        for fd in &module.fn_defs {
705            for eff in &fd.effects {
706                services.insert(eff.node.clone());
707                if let Some((service, _)) = eff.node.split_once('.') {
708                    services.insert(service.to_string());
709                }
710            }
711        }
712    }
713    services
714}
715
716fn is_shared_runtime_type(td: &TypeDef) -> bool {
717    matches!(
718        td,
719        TypeDef::Product { name, .. }
720            if matches!(name.as_str(), "HttpResponse" | "HttpRequest")
721    )
722}
723
724fn needs_named_type(ctx: &CodegenContext, wanted: &str) -> bool {
725    // Walks resolved fn defs (entry + every dep module). The wasm-gc
726    // counterpart `items_reference_name` uses the same shape — a
727    // substring scan over signature surface — so the discovery
728    // semantics stay identical post #180 Phase 7 fn_sigs drop.
729    let scan = |params: &[(String, Type)], ret: &Type| -> bool {
730        params.iter().any(|(_, p)| type_contains_named(p, wanted))
731            || type_contains_named(ret, wanted)
732    };
733    if ctx
734        .resolved_program
735        .entry_fns()
736        .any(|rfd| scan(&rfd.params, &rfd.return_type))
737    {
738        return true;
739    }
740    ctx.resolved_program.modules.iter().any(|m| {
741        m.fn_defs
742            .iter()
743            .any(|rfd| scan(&rfd.params, &rfd.return_type))
744    })
745}
746
747// syntax-discovery-only: walks ALL fn signatures asking "does
748// any param / return type contain a Named ref with this exact
749// bare-string name?" Callers pass builtin record names
750// (`"HttpResponse"`, `"Tcp.Connection"`, `"Terminal.Size"`)
751// whose typed registration carries no `id` — those refs are
752// matched by the name surface they were declared with. The
753// query is a discovery walk, not an identity decision.
754fn type_contains_named(ty: &Type, wanted: &str) -> bool {
755    match ty {
756        Type::Named { name, .. } => name == wanted,
757        Type::Result(ok, err) => {
758            type_contains_named(ok, wanted) || type_contains_named(err, wanted)
759        }
760        Type::Option(inner) | Type::List(inner) | Type::Vector(inner) => {
761            type_contains_named(inner, wanted)
762        }
763        Type::Tuple(items) => items.iter().any(|t| type_contains_named(t, wanted)),
764        Type::Map(k, v) => type_contains_named(k, wanted) || type_contains_named(v, wanted),
765        Type::Fn(params, ret, _effects) => {
766            params.iter().any(|t| type_contains_named(t, wanted))
767                || type_contains_named(ret, wanted)
768        }
769        Type::Int
770        | Type::Float
771        | Type::Str
772        | Type::Bool
773        | Type::Unit
774        | Type::Invalid
775        | Type::Var(_) => false,
776    }
777}
778
779#[cfg(test)]
780mod tests {
781    use super::{render_generated_module, synthesize_rust_module_cascade, transpile};
782    use crate::codegen::build_context;
783    use crate::source::parse_source;
784
785    fn ctx_from_source(source: &str, project_name: &str) -> crate::codegen::CodegenContext {
786        let mut items = parse_source(source).expect("source should parse");
787        // Run the canonical compiler pipeline exactly as the CLI
788        // (`main::commands::compile`) does, so the in-process
789        // `resolved_items` / `symbol_table` / `analysis` — and the MIR
790        // `build_context` lowers from them — match the real pipeline.
791        // The previous hand-rolled tco + typecheck + `resolve_program`
792        // skipped the `last_use` and `analyze` stages, so its resolved
793        // AST carried no `last_use` stamps; the per-fn MIR then diverged
794        // from the CLI's (e.g. the `Option.withDefault(Vector.set(v, …),
795        // v)` fusion's same-vector reads lost their last-use ownership),
796        // silently testing a different MIR shape than production emits.
797        let pipeline_result = crate::ir::pipeline::run(
798            &mut items,
799            crate::ir::PipelineConfig {
800                typecheck: Some(crate::ir::TypecheckMode::Full { base_dir: None }),
801                run_build_symbols: true,
802                ..Default::default()
803            },
804        );
805        let tc = pipeline_result.typecheck.expect("typecheck was requested");
806        assert!(
807            tc.errors.is_empty(),
808            "source should typecheck without errors: {:?}",
809            tc.errors
810        );
811        build_context(
812            items,
813            &tc,
814            pipeline_result.analysis.as_ref(),
815            project_name.to_string(),
816            vec![],
817            pipeline_result.symbol_table,
818            pipeline_result.resolved_items,
819        )
820    }
821
822    /// Multi-module ctx builder for cross-scope regression tests.
823    /// `entry_src` is the entry-module source; `dep_sources` is a
824    /// list of `(prefix, source)` for dep modules. The entry's
825    /// `depends [...]` list must mention every dep prefix.
826    fn ctx_from_multi(
827        entry_src: &str,
828        dep_sources: &[(&str, &str)],
829        project_name: &str,
830    ) -> crate::codegen::CodegenContext {
831        let mut entry_items = parse_source(entry_src).expect("entry source should parse");
832        crate::ir::pipeline::tco(&mut entry_items);
833
834        // Build `LoadedModule` views for the typechecker (`WithLoaded`
835        // mode walks the same shape an on-disk multi-file compile
836        // produces, so the typechecker sees dep symbols without
837        // touching the filesystem).
838        let loaded: Vec<crate::source::LoadedModule> = dep_sources
839            .iter()
840            .map(|(prefix, src)| {
841                let items = parse_source(src).expect("dep source should parse");
842                crate::source::LoadedModule {
843                    dep_name: prefix.to_string(),
844                    items,
845                    path: std::path::PathBuf::from(format!("{}.av", prefix)),
846                }
847            })
848            .collect();
849
850        let modules: Vec<crate::codegen::ModuleInfo> = loaded
851            .iter()
852            .map(|lm| {
853                let depends = lm
854                    .items
855                    .iter()
856                    .find_map(|i| match i {
857                        crate::ast::TopLevel::Module(m) => Some(m.depends.clone()),
858                        _ => None,
859                    })
860                    .unwrap_or_default();
861                let type_defs = lm
862                    .items
863                    .iter()
864                    .filter_map(|i| match i {
865                        crate::ast::TopLevel::TypeDef(td) => Some(td.clone()),
866                        _ => None,
867                    })
868                    .collect();
869                let fn_defs = lm
870                    .items
871                    .iter()
872                    .filter_map(|i| match i {
873                        crate::ast::TopLevel::FnDef(fd) if fd.name != "main" => Some(fd.clone()),
874                        _ => None,
875                    })
876                    .collect();
877                crate::codegen::ModuleInfo {
878                    prefix: lm.dep_name.clone(),
879                    depends,
880                    type_defs,
881                    fn_defs,
882                    analysis: None,
883                }
884            })
885            .collect();
886
887        let tc = crate::ir::pipeline::typecheck(
888            &entry_items,
889            &crate::ir::TypecheckMode::WithLoaded(&loaded),
890        );
891        assert!(
892            tc.errors.is_empty(),
893            "entry+dep source should typecheck without errors: {:?}",
894            tc.errors
895        );
896        let symbol_table = crate::ir::SymbolTable::build(&entry_items, &modules);
897        let resolved_items = crate::ir::hir::resolve_program(&symbol_table, &entry_items);
898        build_context(
899            entry_items,
900            &tc,
901            None,
902            project_name.to_string(),
903            modules,
904            symbol_table,
905            resolved_items,
906        )
907    }
908
909    fn generated_rust_entry_file(out: &crate::codegen::ProjectOutput) -> &str {
910        out.files
911            .iter()
912            .find_map(|(name, content)| {
913                (name == "src/aver_generated/entry/mod.rs").then_some(content.as_str())
914            })
915            .expect("expected generated Rust entry module")
916    }
917
918    fn generated_file<'a>(out: &'a crate::codegen::ProjectOutput, path: &str) -> &'a str {
919        out.files
920            .iter()
921            .find_map(|(name, content)| (name == path).then_some(content.as_str()))
922            .unwrap_or_else(|| panic!("expected generated file '{}'", path))
923    }
924
925    #[test]
926    fn cross_module_same_bare_name_fns_resolve_via_qualified_path() {
927        // Epic #170 Phase 3: pins the architectural invariant that
928        // Rust codegen distinguishes same-bare-name fns across the
929        // entry module and a dep module by using the fully-qualified
930        // path for cross-module calls. The entry's `helper(n)` body
931        // is `n + 1`; the dep `Worker.helper(n)` body is `n + 100`.
932        // The emitted Rust must:
933        //   1. emit BOTH fn defs (one per module file)
934        //   2. emit `Worker.walk` body as a fully-qualified call to
935        //      `crate::aver_generated::worker::helper` — NOT a bare
936        //      `helper(n)` that the entry's `use worker::*` wildcard
937        //      shadow would silently mis-resolve.
938        //   3. emit `main`'s `Worker.walk(20)` as a fully-qualified
939        //      call too — same anti-shadow rule.
940        let mut ctx = ctx_from_multi(
941            r#"
942module Entry
943    depends [Worker]
944    intent = "Entry with own same-bare 'helper'."
945    effects []
946
947fn helper(n: Int) -> Int
948    n + 1
949
950fn main() -> Int
951    helper(10) + Worker.walk(20)
952"#,
953            &[(
954                "Worker",
955                r#"
956module Worker
957    exposes [walk]
958    intent = "Worker module with same-bare 'helper'."
959    effects []
960
961fn helper(n: Int) -> Int
962    n + 100
963
964fn walk(n: Int) -> Int
965    helper(n)
966"#,
967            )],
968            "cross_module_helper",
969        );
970
971        let out = transpile(&mut ctx);
972
973        let worker = generated_file(&out, "src/aver_generated/worker/mod.rs");
974        assert!(
975            worker.contains("pub fn helper"),
976            "Worker module must emit its OWN helper:\n{worker}"
977        );
978        assert!(
979            worker.contains("n + 100"),
980            "Worker.helper body must keep its OWN literal (100):\n{worker}"
981        );
982        // Critical anti-shadow check: Worker.walk calls Worker.helper
983        // through the canonical crate path, never bare `helper(n)`
984        // (which would resolve to whoever's in scope after
985        // `use worker::*`).
986        assert!(
987            worker.contains("crate::aver_generated::worker::helper"),
988            "Worker.walk must call its own helper via the canonical \
989             crate path (not bare-name `helper(n)`):\n{worker}"
990        );
991
992        let entry = generated_rust_entry_file(&out);
993        assert!(
994            entry.contains("pub fn helper"),
995            "Entry module must emit its OWN helper:\n{entry}"
996        );
997        assert!(
998            entry.contains("n + 1i64") || entry.contains("(n + 1i64)"),
999            "Entry.helper body must keep its OWN literal (1):\n{entry}"
1000        );
1001        // Entry's `Worker.walk(20)` must qualify through the crate
1002        // path too — bare `walk(20)` would also be ambiguous against
1003        // a hypothetical entry `walk`.
1004        assert!(
1005            entry.contains("crate::aver_generated::worker::walk"),
1006            "main()'s Worker.walk(20) must qualify through the \
1007             canonical crate path:\n{entry}"
1008        );
1009    }
1010
1011    #[test]
1012    fn emission_banner_appears_in_root_main() {
1013        let mut ctx = ctx_from_source(
1014            r#"
1015module Demo
1016
1017fn main() -> Int
1018    1
1019"#,
1020            "demo",
1021        );
1022
1023        let out = transpile(&mut ctx);
1024        let root_main = generated_file(&out, "src/main.rs");
1025
1026        assert!(root_main.contains("// Aver Rust emission"));
1027    }
1028
1029    #[test]
1030    fn generated_module_imports_direct_depends() {
1031        let rendered = render_generated_module(
1032            vec!["Domain.Types".to_string(), "App.Commands".to_string()],
1033            vec!["pub fn demo() {}".to_string()],
1034        );
1035
1036        assert!(rendered.contains("use crate::aver_generated::domain::types::*;"));
1037        assert!(rendered.contains("use crate::aver_generated::app::commands::*;"));
1038        assert!(rendered.contains("pub fn demo() {}"));
1039    }
1040
1041    #[test]
1042    fn module_tree_files_do_not_reexport_children() {
1043        let modules = vec![(
1044            vec!["app".to_string(), "cli".to_string()],
1045            "pub fn run() {}".to_string(),
1046        )];
1047        let files = synthesize_rust_module_cascade("src/aver_generated", &modules);
1048
1049        let root_mod = files
1050            .iter()
1051            .find(|(path, _)| path == "src/aver_generated/mod.rs")
1052            .map(|(_, content)| content)
1053            .expect("root mod.rs should exist");
1054
1055        assert!(root_mod.contains("pub mod app;"));
1056        assert!(!root_mod.contains("pub use app::*;"));
1057    }
1058
1059    #[test]
1060    fn list_cons_match_uses_cloned_uncons_fast_path_when_optimized() {
1061        let mut ctx = ctx_from_source(
1062            r#"
1063module Demo
1064
1065fn headPlusTailLen(xs: List<Int>) -> Int
1066    match xs
1067        [] -> 0
1068        [h, ..t] -> h + List.len(t)
1069"#,
1070            "demo",
1071        );
1072
1073        let out = transpile(&mut ctx);
1074        let entry = generated_rust_entry_file(&out);
1075
1076        // The common []/[h,..t] pattern uses aver_list_match! macro
1077        assert!(entry.contains("aver_list_match!"));
1078    }
1079
1080    #[test]
1081    fn list_cons_match_stays_structured_in_semantic_mode() {
1082        let mut ctx = ctx_from_source(
1083            r#"
1084module Demo
1085
1086fn headPlusTailLen(xs: List<Int>) -> Int
1087    match xs
1088        [] -> 0
1089        [h, ..t] -> h + List.len(t)
1090"#,
1091            "demo",
1092        );
1093
1094        let out = transpile(&mut ctx);
1095        let entry = generated_rust_entry_file(&out);
1096
1097        // Both modes now use the aver_list_match! macro for []/[h,..t] patterns
1098        assert!(entry.contains("aver_list_match!"));
1099    }
1100
1101    #[test]
1102    fn list_literal_clones_ident_when_used_afterward() {
1103        let mut ctx = ctx_from_source(
1104            r#"
1105module Demo
1106
1107record Audit
1108    message: String
1109
1110fn useTwice(audit: Audit) -> List<Audit>
1111    first = [audit]
1112    List.concat(first, [audit])
1113"#,
1114            "demo",
1115        );
1116
1117        let out = transpile(&mut ctx);
1118        let entry = generated_rust_entry_file(&out);
1119
1120        // `first` is consumed by `List.concat` so it stays live (an unused
1121        // binding is correctly dead-code-eliminated); both `[audit]` literals
1122        // then clone the borrowed `audit` because it is used more than once.
1123        assert!(entry.contains("let first = aver_rt::AverList::from_vec(vec![audit.clone()]);"));
1124        // Borrowed param always needs .clone() when consumed
1125        assert!(entry.contains("aver_rt::AverList::from_vec(vec![audit.clone()])"));
1126    }
1127
1128    #[test]
1129    fn record_update_clones_base_when_value_is_used_afterward() {
1130        let mut ctx = ctx_from_source(
1131            r#"
1132module Demo
1133
1134record PaymentState
1135    paymentId: String
1136    currency: String
1137
1138fn touch(state: PaymentState) -> String
1139    updated = PaymentState.update(state, currency = "EUR")
1140    "{updated.currency}-{state.paymentId}"
1141"#,
1142            "demo",
1143        );
1144
1145        let out = transpile(&mut ctx);
1146        let entry = generated_rust_entry_file(&out);
1147
1148        // `updated` is consumed by the interpolation so it stays live; the
1149        // record update then clones the borrowed `state` because `state` is
1150        // used again afterward (`state.paymentId`).
1151        assert!(entry.contains("..state.clone()"));
1152    }
1153
1154    #[test]
1155    fn mutual_tco_generates_trampoline_instead_of_regular_calls() {
1156        let mut ctx = ctx_from_source(
1157            r#"
1158module Demo
1159
1160fn isEven(n: Int) -> Bool
1161    match n == 0
1162        true -> true
1163        false -> isOdd(n - 1)
1164
1165fn isOdd(n: Int) -> Bool
1166    match n == 0
1167        true -> false
1168        false -> isEven(n - 1)
1169"#,
1170            "demo",
1171        );
1172
1173        let out = transpile(&mut ctx);
1174        let entry = generated_rust_entry_file(&out);
1175
1176        // Should generate trampoline enum and dispatch
1177        assert!(entry.contains("enum __MutualTco1"));
1178        assert!(entry.contains("fn __mutual_tco_trampoline_1"));
1179        assert!(entry.contains("loop {"));
1180
1181        // Wrapper functions delegate to trampoline
1182        assert!(entry.contains("pub fn isEven"));
1183        assert!(entry.contains("pub fn isOdd"));
1184        assert!(entry.contains("__mutual_tco_trampoline_1("));
1185
1186        // Should NOT contain direct recursive calls between the two
1187        assert!(!entry.contains("isOdd((n - 1i64))"));
1188    }
1189
1190    #[test]
1191    fn field_access_does_not_double_clone() {
1192        let mut ctx = ctx_from_source(
1193            r#"
1194module Demo
1195
1196record User
1197    name: String
1198    age: Int
1199
1200fn greet(u: User) -> String
1201    u.name
1202"#,
1203            "demo",
1204        );
1205
1206        let out = transpile(&mut ctx);
1207        let entry = generated_rust_entry_file(&out);
1208
1209        // Field access should produce exactly one .clone(), never .clone().clone()
1210        assert!(
1211            !entry.contains(".clone().clone()"),
1212            "double clone detected in generated code:\n{}",
1213            entry
1214        );
1215    }
1216
1217    #[test]
1218    fn borrowed_record_field_return_clones_for_owned_result() {
1219        let mut ctx = ctx_from_source(
1220            r#"
1221module Demo
1222
1223record User
1224    name: String
1225
1226fn getName(user: User) -> String
1227    user.name
1228"#,
1229            "demo",
1230        );
1231
1232        let out = transpile(&mut ctx);
1233        let entry = generated_rust_entry_file(&out);
1234
1235        assert!(entry.contains("pub fn getName(user: &User) -> AverStr"));
1236        assert!(
1237            entry.contains("user.name.clone()"),
1238            "missing owned clone:\n{}",
1239            entry
1240        );
1241    }
1242
1243    #[test]
1244    fn vector_get_with_literal_default_lowers_to_direct_unwrap_or_code() {
1245        let mut ctx = ctx_from_source(
1246            r#"
1247module Demo
1248
1249fn cellAt(grid: Vector<Int>, idx: Int) -> Int
1250    Option.withDefault(Vector.get(grid, idx), 0)
1251"#,
1252            "demo",
1253        );
1254
1255        let out = transpile(&mut ctx);
1256        let entry = generated_rust_entry_file(&out);
1257
1258        assert!(entry.contains("grid.get(idx as usize).cloned().unwrap_or(0i64)"));
1259    }
1260
1261    #[test]
1262    fn vector_set_default_stays_structured_in_semantic_mode() {
1263        let mut ctx = ctx_from_source(
1264            r#"
1265module Demo
1266
1267fn updateOrKeep(vec: Vector<Int>, idx: Int, value: Int) -> Vector<Int>
1268    Option.withDefault(Vector.set(vec, idx, value), vec)
1269"#,
1270            "demo",
1271        );
1272
1273        let out = transpile(&mut ctx);
1274        let entry = generated_rust_entry_file(&out);
1275
1276        // Both modes now use the inlined set_unchecked fast path
1277        assert!(entry.contains("set_unchecked"));
1278        assert!(!entry.contains(".unwrap_or("));
1279    }
1280
1281    #[test]
1282    fn vector_set_default_uses_ir_leaf_fast_path_when_optimized() {
1283        let mut ctx = ctx_from_source(
1284            r#"
1285module Demo
1286
1287fn updateOrKeep(vec: Vector<Int>, idx: Int, value: Int) -> Vector<Int>
1288    Option.withDefault(Vector.set(vec, idx, value), vec)
1289"#,
1290            "demo",
1291        );
1292
1293        let out = transpile(&mut ctx);
1294        let entry = generated_rust_entry_file(&out);
1295
1296        assert!(entry.contains("set_unchecked"));
1297        assert!(!entry.contains(".unwrap_or("));
1298    }
1299
1300    #[test]
1301    fn vector_set_uses_owned_update_lowering() {
1302        let mut ctx = ctx_from_source(
1303            r#"
1304module Demo
1305
1306fn update(vec: Vector<Int>, idx: Int, value: Int) -> Option<Vector<Int>>
1307    Vector.set(vec, idx, value)
1308"#,
1309            "demo",
1310        );
1311
1312        let out = transpile(&mut ctx);
1313        let entry = generated_rust_entry_file(&out);
1314
1315        assert!(entry.contains(".set_owned("));
1316        assert!(!entry.contains(".set(idx as usize,"));
1317    }
1318
1319    #[test]
1320    fn map_remove_uses_owned_update_lowering() {
1321        let mut ctx = ctx_from_source(
1322            r#"
1323module Demo
1324
1325fn dropKey(m: Map<String, Int>, key: String) -> Map<String, Int>
1326    Map.remove(m, key)
1327"#,
1328            "demo",
1329        );
1330
1331        let out = transpile(&mut ctx);
1332        let entry = generated_rust_entry_file(&out);
1333
1334        assert!(entry.contains(".remove_owned(&"));
1335    }
1336
1337    #[test]
1338    fn semantic_keeps_known_leaf_wrapper_call_structured() {
1339        let mut ctx = ctx_from_source(
1340            r#"
1341module Demo
1342
1343fn cellAt(grid: Vector<Int>, idx: Int) -> Int
1344    Option.withDefault(Vector.get(grid, idx), 0)
1345
1346fn read(grid: Vector<Int>, idx: Int) -> Int
1347    cellAt(grid, idx)
1348"#,
1349            "demo",
1350        );
1351
1352        let out = transpile(&mut ctx);
1353        let entry = generated_rust_entry_file(&out);
1354
1355        assert!(entry.contains("cellAt(grid, idx)"));
1356        assert!(!entry.contains("__aver_thin_arg0"));
1357    }
1358
1359    #[test]
1360    fn optimized_keeps_known_leaf_wrapper_callsite_and_leaves_absorption_to_rust() {
1361        let mut ctx = ctx_from_source(
1362            r#"
1363module Demo
1364
1365fn cellAt(grid: Vector<Int>, idx: Int) -> Int
1366    Option.withDefault(Vector.get(grid, idx), 0)
1367
1368fn read(grid: Vector<Int>, idx: Int) -> Int
1369    cellAt(grid, idx)
1370"#,
1371            "demo",
1372        );
1373
1374        let out = transpile(&mut ctx);
1375        let entry = generated_rust_entry_file(&out);
1376
1377        assert!(entry.contains("cellAt(grid, idx)"));
1378        assert!(!entry.contains("__aver_thin_arg0"));
1379    }
1380
1381    #[test]
1382    fn optimized_keeps_known_dispatch_wrapper_callsite_and_leaves_absorption_to_rust() {
1383        let mut ctx = ctx_from_source(
1384            r#"
1385module Demo
1386
1387fn bucket(n: Int) -> Int
1388    match n == 0
1389        true -> 0
1390        false -> 1
1391
1392fn readBucket(n: Int) -> Int
1393    bucket(n)
1394"#,
1395            "demo",
1396        );
1397
1398        let out = transpile(&mut ctx);
1399        let entry = generated_rust_entry_file(&out);
1400
1401        assert!(entry.contains("bucket(n)"));
1402        assert!(!entry.contains("__aver_thin_arg0"));
1403    }
1404
1405    #[test]
1406    fn bool_match_on_gte_normalizes_to_base_comparison_when_optimized() {
1407        let mut ctx = ctx_from_source(
1408            r#"
1409module Demo
1410
1411fn bucket(n: Int) -> Int
1412    match n >= 10
1413        true -> 7
1414        false -> 3
1415"#,
1416            "demo",
1417        );
1418
1419        let out = transpile(&mut ctx);
1420        let entry = generated_rust_entry_file(&out);
1421
1422        assert!(entry.contains("if (n < 10i64) { 3i64 } else { 7i64 }"));
1423    }
1424
1425    #[test]
1426    fn bool_match_stays_as_match_in_semantic_mode() {
1427        let mut ctx = ctx_from_source(
1428            r#"
1429module Demo
1430
1431fn bucket(n: Int) -> Int
1432    match n >= 10
1433        true -> 7
1434        false -> 3
1435"#,
1436            "demo",
1437        );
1438
1439        let out = transpile(&mut ctx);
1440        let entry = generated_rust_entry_file(&out);
1441
1442        // Both modes now use the normalized if-else form
1443        assert!(entry.contains("if (n < 10i64) { 3i64 } else { 7i64 }"));
1444    }
1445
1446    #[test]
1447    fn wave2_generic_user_sum_match_emits_native_rust_match() {
1448        // rust-on-MIR Wave 2: a generic match over a user sum type (no
1449        // bool / list / dispatch-table shortcut) graduates through
1450        // `emit_mir_match`. The production output is byte-identical to
1451        // the HIR walker by the parity gate, so asserting the shape here
1452        // pins the graduated emit. `Shape.Circle(r)` / `Shape.Square(s)`
1453        // → a native Rust `match` with `Demo_Shape::Circle(r) => { … }`
1454        // arms.
1455        let mut ctx = ctx_from_source(
1456            r#"
1457module Demo
1458
1459type Shape
1460    Circle(Float)
1461    Square(Float)
1462
1463fn area(sh: Shape) -> Float
1464    match sh
1465        Shape.Circle(r) -> r * r * 3.14
1466        Shape.Square(s) -> s * s
1467"#,
1468            "demo",
1469        );
1470
1471        let out = transpile(&mut ctx);
1472        let entry = generated_rust_entry_file(&out);
1473
1474        assert!(
1475            entry.contains("match sh.clone() {"),
1476            "generic user-sum match should emit a native Rust match on the cloned subject: {entry}"
1477        );
1478        assert!(entry.contains("Shape::Circle(r) =>"));
1479        assert!(entry.contains("Shape::Square(s) =>"));
1480    }
1481
1482    #[test]
1483    fn optimized_self_tco_uses_dispatch_table_for_wrapper_match() {
1484        let mut ctx = ctx_from_source(
1485            r#"
1486module Demo
1487
1488fn loop(r: Result<Int, String>) -> Int
1489    match r
1490        Result.Ok(n) -> n
1491        Result.Err(_) -> loop(Result.Ok(1))
1492"#,
1493            "demo",
1494        );
1495
1496        let out = transpile(&mut ctx);
1497        let entry = generated_rust_entry_file(&out);
1498
1499        // Uses native Rust match directly. `r` is the match subject's
1500        // last use, so the `last_use` pass (now run by `ctx_from_source`
1501        // via the full pipeline, matching the CLI) lets it move into the
1502        // match without a `.clone()`.
1503        assert!(entry.contains("match r {"));
1504        assert!(entry.contains("Ok(n)"));
1505        assert!(!entry.contains("__dispatch_subject"));
1506    }
1507
1508    #[test]
1509    fn optimized_mutual_tco_uses_dispatch_table_for_wrapper_match() {
1510        let mut ctx = ctx_from_source(
1511            r#"
1512module Demo
1513
1514fn left(r: Result<Int, String>) -> Int
1515    match r
1516        Result.Ok(n) -> n
1517        Result.Err(_) -> right(Result.Ok(1))
1518
1519fn right(r: Result<Int, String>) -> Int
1520    match r
1521        Result.Ok(n) -> n
1522        Result.Err(_) -> left(Result.Ok(1))
1523"#,
1524            "demo",
1525        );
1526
1527        let out = transpile(&mut ctx);
1528        let entry = generated_rust_entry_file(&out);
1529
1530        // Uses native Rust match directly. `r` is the match subject's
1531        // last use, so the `last_use` pass (now run by `ctx_from_source`
1532        // via the full pipeline, matching the CLI) lets it move into the
1533        // match without a `.clone()`.
1534        assert!(entry.contains("match r {"));
1535        assert!(entry.contains("Ok(n)"));
1536        assert!(!entry.contains("__dispatch_subject"));
1537    }
1538
1539    #[test]
1540    fn single_field_variant_display_avoids_vec_join() {
1541        let mut ctx = ctx_from_source(
1542            r#"
1543module Demo
1544
1545type Wrapper
1546    Wrap(Int)
1547    Pair(Int, Int)
1548    Empty
1549"#,
1550            "demo",
1551        );
1552
1553        let out = transpile(&mut ctx);
1554        let entry = generated_rust_entry_file(&out);
1555
1556        // Single-field variant Wrap(Int): should NOT use vec![].join()
1557        assert!(
1558            !entry.contains("vec![f0.aver_display_inner()].join"),
1559            "single-field variant should use direct format, not vec join:\n{}",
1560            entry
1561        );
1562        // Multi-field variant Pair(Int, Int): SHOULD still use vec![].join()
1563        assert!(
1564            entry.contains("vec![f0.aver_display_inner(), f1.aver_display_inner()].join(\", \")"),
1565            "multi-field variant should use vec join:\n{}",
1566            entry
1567        );
1568    }
1569
1570    #[test]
1571    fn replay_codegen_wraps_guest_entry_in_scoped_runtime() {
1572        let mut ctx = ctx_from_source(
1573            r#"
1574module Demo
1575
1576fn runGuestProgram(path: String) -> Result<String, String>
1577    ! [Disk.readText]
1578    Disk.readText(path)
1579"#,
1580            "demo",
1581        );
1582        ctx.emit_replay_runtime = true;
1583        ctx.guest_entry = Some("runGuestProgram".to_string());
1584
1585        let out = transpile(&mut ctx);
1586        let entry = generated_rust_entry_file(&out);
1587        let replay_support = generated_file(&out, "src/replay_support.rs");
1588        let cargo_toml = generated_file(&out, "Cargo.toml");
1589
1590        assert!(entry.contains("aver_replay::with_guest_scope_result(\"runGuestProgram\""));
1591        assert!(replay_support.contains("pub mod aver_replay"));
1592        assert!(cargo_toml.contains("serde_json = \"1\""));
1593    }
1594
1595    #[test]
1596    fn replay_codegen_uses_guest_args_param_override_when_present() {
1597        let mut ctx = ctx_from_source(
1598            r#"
1599module Demo
1600
1601fn runGuestProgram(path: String, guestArgs: List<String>) -> Result<String, String>
1602    ! [Args.get]
1603    Result.Ok(String.join(Args.get(), ","))
1604"#,
1605            "demo",
1606        );
1607        ctx.emit_replay_runtime = true;
1608        ctx.guest_entry = Some("runGuestProgram".to_string());
1609
1610        let out = transpile(&mut ctx);
1611        let entry = generated_rust_entry_file(&out);
1612        let cargo_toml = generated_file(&out, "Cargo.toml");
1613
1614        assert!(entry.contains("aver_replay::with_guest_scope_args_result(\"runGuestProgram\""));
1615        assert!(entry.contains("guestArgs.clone()"));
1616        assert!(cargo_toml.contains("edition = \"2024\""));
1617    }
1618
1619    #[test]
1620    fn replay_codegen_wraps_root_main_when_no_guest_entry_is_set() {
1621        let mut ctx = ctx_from_source(
1622            r#"
1623module Demo
1624
1625fn main() -> Result<String, String>
1626    ! [Disk.readText]
1627    Disk.readText("demo.av")
1628"#,
1629            "demo",
1630        );
1631        ctx.emit_replay_runtime = true;
1632
1633        let out = transpile(&mut ctx);
1634        let root_main = generated_file(&out, "src/main.rs");
1635
1636        assert!(
1637            root_main.contains("aver_replay::with_guest_scope(\"main\", serde_json::Value::Null")
1638        );
1639    }
1640
1641    #[test]
1642    fn runtime_policy_codegen_uses_runtime_loader() {
1643        let mut ctx = ctx_from_source(
1644            r#"
1645module Demo
1646
1647fn main() -> Result<String, String>
1648    ! [Disk.readText]
1649    Disk.readText("demo.av")
1650"#,
1651            "demo",
1652        );
1653        ctx.emit_replay_runtime = true;
1654        ctx.runtime_policy_from_env = true;
1655
1656        let out = transpile(&mut ctx);
1657        let root_main = generated_file(&out, "src/main.rs");
1658        let replay_support = generated_file(&out, "src/replay_support.rs");
1659        let cargo_toml = generated_file(&out, "Cargo.toml");
1660
1661        assert!(!root_main.contains("mod policy_support;"));
1662        assert!(replay_support.contains("load_runtime_policy_from_env"));
1663        assert!(cargo_toml.contains("url = \"2\""));
1664        assert!(cargo_toml.contains("toml = \"0.8\""));
1665    }
1666
1667    #[test]
1668    fn replay_codegen_can_keep_embedded_policy_when_requested() {
1669        let mut ctx = ctx_from_source(
1670            r#"
1671module Demo
1672
1673fn main() -> Result<String, String>
1674    ! [Disk.readText]
1675    Disk.readText("demo.av")
1676"#,
1677            "demo",
1678        );
1679        ctx.emit_replay_runtime = true;
1680        ctx.policy = Some(crate::config::ProjectConfig {
1681            effect_policies: std::collections::HashMap::new(),
1682            check_suppressions: Vec::new(),
1683            independence_mode: crate::config::IndependenceMode::default(),
1684            shape_layers: Vec::new(),
1685            shape_expected: Vec::new(),
1686        });
1687
1688        let out = transpile(&mut ctx);
1689        let root_main = generated_file(&out, "src/main.rs");
1690        let replay_support = generated_file(&out, "src/replay_support.rs");
1691
1692        assert!(root_main.contains("mod policy_support;"));
1693        assert!(replay_support.contains("aver_policy::check_disk"));
1694        assert!(!replay_support.contains("RuntimeEffectPolicy"));
1695    }
1696
1697    #[test]
1698    fn self_host_support_is_emitted_as_separate_module() {
1699        let mut ctx = ctx_from_source(
1700            r#"
1701module Demo
1702
1703fn runGuestProgram(prog: Int, moduleFns: Int) -> Result<String, String>
1704    Result.Ok("ok")
1705"#,
1706            "demo",
1707        );
1708        ctx.emit_self_host_support = true;
1709        ctx.guest_entry = Some("runGuestProgram".to_string());
1710
1711        let out = transpile(&mut ctx);
1712        let root_main = generated_file(&out, "src/main.rs");
1713        let runtime_support = generated_file(&out, "src/runtime_support.rs");
1714        let self_host_support = generated_file(&out, "src/self_host_support.rs");
1715        let entry = generated_rust_entry_file(&out);
1716
1717        assert!(root_main.contains("mod self_host_support;"));
1718        assert!(!runtime_support.contains("with_fn_store"));
1719        assert!(self_host_support.contains("pub fn with_program_fn_store"));
1720        assert!(entry.contains("crate::self_host_support::with_program_fn_store("));
1721    }
1722
1723    #[test]
1724    fn independent_product_codegen_avoids_string_specific_error_type() {
1725        let mut ctx = ctx_from_source(
1726            r#"
1727module Demo
1728
1729fn left() -> Result<Int, Int>
1730    Result.Ok(1)
1731
1732fn right() -> Result<Int, Int>
1733    Result.Ok(2)
1734
1735fn main() -> Result<Tuple<Int, Int>, Int>
1736    data = (left(), right())?!
1737    Result.Ok(data)
1738"#,
1739            "demo",
1740        );
1741        ctx.emit_replay_runtime = true;
1742
1743        let out = transpile(&mut ctx);
1744        let entry = generated_rust_entry_file(&out);
1745
1746        assert!(!entry.contains("Ok::<_, aver_rt::AverStr>"));
1747        assert!(entry.contains("crate::aver_replay::exit_effect_group();"));
1748        assert!(entry.contains("match (_r0, _r1)"));
1749        assert!(!entry.contains("let _r0 = left()?;"));
1750    }
1751
1752    #[test]
1753    fn independent_product_codegen_emits_cancel_runtime_and_scope_propagation() {
1754        let mut ctx = ctx_from_source(
1755            r#"
1756module Demo
1757
1758fn left() -> Result<Int, String>
1759    Result.Ok(1)
1760
1761fn right() -> Result<Int, String>
1762    Result.Ok(2)
1763
1764fn main() -> Result<Tuple<Int, Int>, String>
1765    data = (left(), right())?!
1766    Result.Ok(data)
1767"#,
1768            "demo",
1769        );
1770        ctx.emit_replay_runtime = true;
1771        ctx.policy = Some(crate::config::ProjectConfig {
1772            effect_policies: std::collections::HashMap::new(),
1773            check_suppressions: Vec::new(),
1774            independence_mode: crate::config::IndependenceMode::Cancel,
1775            shape_layers: Vec::new(),
1776            shape_expected: Vec::new(),
1777        });
1778
1779        let out = transpile(&mut ctx);
1780        let entry = generated_rust_entry_file(&out);
1781        let runtime_support = generated_file(&out, "src/runtime_support.rs");
1782        let replay_support = generated_file(&out, "src/replay_support.rs");
1783
1784        assert!(entry.contains("crate::run_cancelable_branch"));
1785        assert!(entry.contains("capture_parallel_scope_context"));
1786        assert!(entry.contains("_s.spawn(move ||"));
1787        assert!(runtime_support.contains("pub fn run_cancelable_branch"));
1788        assert!(runtime_support.contains("panic_any(AverCancelled)"));
1789        assert!(replay_support.contains("pub fn capture_parallel_scope_context"));
1790        assert!(replay_support.contains("pub fn independence_mode_is_cancel()"));
1791    }
1792
1793    #[test]
1794    fn runtime_policy_codegen_parses_independence_mode() {
1795        let mut ctx = ctx_from_source(
1796            r#"
1797module Demo
1798
1799fn main() -> Result<String, String>
1800    ! [Disk.readText]
1801    Disk.readText("demo.av")
1802"#,
1803            "demo",
1804        );
1805        ctx.emit_replay_runtime = true;
1806        ctx.runtime_policy_from_env = true;
1807
1808        let out = transpile(&mut ctx);
1809        let replay_support = generated_file(&out, "src/replay_support.rs");
1810
1811        assert!(replay_support.contains("independence_mode_cancel"));
1812        assert!(replay_support.contains("[independence].mode must be 'complete' or 'cancel'"));
1813    }
1814
1815    #[test]
1816    fn effectful_codegen_inserts_cancel_checkpoint_before_builtin_calls() {
1817        let mut ctx = ctx_from_source(
1818            r#"
1819module Demo
1820
1821fn main() -> Result<String, String>
1822    ! [Disk.readText]
1823    Disk.readText("demo.av")
1824"#,
1825            "demo",
1826        );
1827
1828        let out = transpile(&mut ctx);
1829        let entry = generated_rust_entry_file(&out);
1830
1831        assert!(entry.contains("crate::cancel_checkpoint(); (aver_rt::read_text"));
1832    }
1833
1834    #[test]
1835    fn replay_support_matches_group_effects_by_occurrence_and_args() {
1836        let mut ctx = ctx_from_source(
1837            r#"
1838module Demo
1839
1840fn left() -> Result<Int, String>
1841    Result.Ok(1)
1842
1843fn right() -> Result<Int, String>
1844    Result.Ok(2)
1845
1846fn main() -> Result<Tuple<Int, Int>, String>
1847    data = (left(), right())?!
1848    Result.Ok(data)
1849"#,
1850            "demo",
1851        );
1852        ctx.emit_replay_runtime = true;
1853
1854        let out = transpile(&mut ctx);
1855        let replay_support = generated_file(&out, "src/replay_support.rs");
1856
1857        assert!(replay_support.contains("candidate.effect_occurrence"));
1858        assert!(replay_support.contains("candidate.args != args"));
1859    }
1860}