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