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