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, 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#[derive(Default)]
25struct ModuleTreeNode {
26    content: Option<String>,
27    children: BTreeMap<String, ModuleTreeNode>,
28}
29
30/// Transpile an Aver program to a Rust project.
31pub fn transpile(ctx: &mut CodegenContext) -> ProjectOutput {
32    let has_embedded_policy = ctx.policy.is_some();
33    let has_runtime_policy = ctx.runtime_policy_from_env;
34    let embedded_independence_cancel = ctx
35        .policy
36        .as_ref()
37        .is_some_and(|config| config.independence_mode == crate::config::IndependenceMode::Cancel);
38    let used_services = detect_used_services(ctx);
39    let needs_http_types = needs_named_type(ctx, "Header")
40        || needs_named_type(ctx, "HttpResponse")
41        || needs_named_type(ctx, "HttpRequest");
42    let needs_tcp_types = needs_named_type(ctx, "Tcp.Connection");
43    let needs_terminal_types = needs_named_type(ctx, "Terminal.Size");
44
45    let has_tcp_runtime = used_services.contains("Tcp");
46    let has_http_runtime = used_services.contains("Http");
47    let has_http_server_runtime = used_services.contains("HttpServer");
48    let has_terminal_runtime = used_services.contains("Terminal");
49
50    let has_tcp_types = has_tcp_runtime || needs_tcp_types;
51    let has_http_types = has_http_runtime || has_http_server_runtime || needs_http_types;
52    let has_http_server_types = has_http_server_runtime || needs_named_type(ctx, "HttpRequest");
53    let has_terminal_types = has_terminal_runtime || needs_terminal_types;
54
55    let main_fn = ctx.fn_defs.iter().find(|fd| fd.name == "main");
56    let top_level_stmts: Vec<_> = ctx
57        .items
58        .iter()
59        .filter_map(|item| {
60            if let TopLevel::Stmt(stmt) = item {
61                Some(stmt)
62            } else {
63                None
64            }
65        })
66        .collect();
67    let verify_blocks: Vec<_> = ctx
68        .items
69        .iter()
70        .filter_map(|item| {
71            if let TopLevel::Verify(vb) = item {
72                Some(vb)
73            } else {
74                None
75            }
76        })
77        .collect();
78
79    let mut files = vec![
80        (
81            "Cargo.toml".to_string(),
82            project::generate_cargo_toml(
83                &ctx.project_name,
84                &used_services,
85                has_embedded_policy,
86                has_runtime_policy,
87                ctx.emit_replay_runtime,
88            ),
89        ),
90        (
91            "src/main.rs".to_string(),
92            render_root_main(
93                main_fn,
94                has_embedded_policy,
95                ctx.emit_replay_runtime,
96                ctx.guest_entry.as_deref(),
97                !verify_blocks.is_empty(),
98                ctx.emit_self_host_support,
99            ),
100        ),
101        (
102            "src/runtime_support.rs".to_string(),
103            render_runtime_support(
104                has_tcp_types,
105                has_http_types,
106                has_http_server_types,
107                ctx.emit_replay_runtime,
108                embedded_independence_cancel,
109            ),
110        ),
111    ];
112
113    if ctx.emit_self_host_support {
114        files.push((
115            "src/self_host_support.rs".to_string(),
116            self_host::generate_self_host_support(),
117        ));
118    }
119
120    if has_embedded_policy && let Some(config) = &ctx.policy {
121        files.push((
122            "src/policy_support.rs".to_string(),
123            format!("{}\n", policy::generate_policy_runtime(config)),
124        ));
125    }
126
127    if ctx.emit_replay_runtime {
128        files.push((
129            "src/replay_support.rs".to_string(),
130            replay::generate_replay_runtime(
131                has_embedded_policy,
132                has_runtime_policy,
133                has_terminal_types,
134                has_tcp_types,
135                has_http_types,
136                has_http_server_types,
137                embedded_independence_cancel,
138            ),
139        ));
140    }
141
142    if !verify_blocks.is_empty() {
143        files.push((
144            "src/verify.rs".to_string(),
145            render_verify_module(&verify_blocks, ctx),
146        ));
147    }
148
149    let mut module_tree = ModuleTreeNode::default();
150    insert_module_content(
151        &mut module_tree,
152        &[String::from("entry")],
153        render_generated_module(
154            root_module_depends(&ctx.items),
155            entry_module_sections(ctx, main_fn, &top_level_stmts),
156        ),
157    );
158
159    for i in 0..ctx.modules.len() {
160        // Set extra_fn_defs so find_fn_def_by_name resolves intra-module
161        // bare-name calls (e.g. buildFibStats calling finalizeFibStats).
162        ctx.extra_fn_defs = ctx.modules[i].fn_defs.clone();
163        let module = &ctx.modules[i];
164        let path = module_prefix_to_rust_segments(&module.prefix);
165        insert_module_content(
166            &mut module_tree,
167            &path,
168            render_generated_module(module.depends.clone(), module_sections(module, ctx)),
169        );
170    }
171    ctx.extra_fn_defs.clear();
172
173    emit_module_tree_files(&module_tree, "src/aver_generated", &mut files);
174    files.sort_by(|left, right| left.0.cmp(&right.0));
175
176    ProjectOutput { files }
177}
178
179fn render_root_main(
180    main_fn: Option<&FnDef>,
181    has_policy: bool,
182    has_replay: bool,
183    guest_entry: Option<&str>,
184    has_verify: bool,
185    has_self_host_support: bool,
186) -> String {
187    let mut sections = vec![
188        "#![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(),
189        "// Aver Rust emission".to_string(),
190        "#[macro_use] extern crate aver_rt;".to_string(),
191        "pub use ::aver_rt::AverMap as HashMap;".to_string(),
192        "pub use ::aver_rt::AverStr;".to_string(),
193        String::new(),
194        "mod runtime_support;".to_string(),
195        "pub use runtime_support::*;".to_string(),
196    ];
197
198    if has_policy {
199        sections.push(String::new());
200        sections.push("mod policy_support;".to_string());
201        sections.push("pub use policy_support::*;".to_string());
202    }
203
204    if has_replay {
205        sections.push(String::new());
206        sections.push("mod replay_support;".to_string());
207        sections.push("pub use replay_support::*;".to_string());
208    }
209
210    if has_self_host_support {
211        sections.push(String::new());
212        sections.push("mod self_host_support;".to_string());
213    }
214
215    sections.push(String::new());
216    sections.push("pub mod aver_generated;".to_string());
217
218    if has_verify {
219        sections.push(String::new());
220        sections.push("#[cfg(test)]".to_string());
221        sections.push("mod verify;".to_string());
222    }
223
224    // Spawn main on a thread with 256 MB stack to avoid overflow in deep recursion.
225    sections.push(String::new());
226    let returns_result = main_fn.is_some_and(|fd| fd.return_type.starts_with("Result<"));
227    let result_unit_string =
228        main_fn.is_some_and(|fd| fd.return_type.replace(' ', "") == "Result<Unit,String>");
229    if returns_result {
230        if result_unit_string {
231            sections.push("fn main() {".to_string());
232            sections.push("    let child = std::thread::Builder::new()".to_string());
233            sections.push("        .stack_size(256 * 1024 * 1024)".to_string());
234            if has_replay && guest_entry.is_none() {
235                sections.push("        .spawn(|| {".to_string());
236                sections.push("            let __result = aver_replay::with_guest_scope(\"main\", serde_json::Value::Null, aver_generated::entry::main);".to_string());
237                sections.push("            __result.map_err(|e| e.to_string())".to_string());
238                sections.push("        })".to_string());
239            } else {
240                sections.push("        .spawn(|| {".to_string());
241                sections
242                    .push("            let __result = aver_generated::entry::main();".to_string());
243                sections.push("            __result.map_err(|e| e.to_string())".to_string());
244                sections.push("        })".to_string());
245            }
246            sections.push("        .expect(\"thread spawn\");".to_string());
247            sections.push("    match child.join().expect(\"thread join\") {".to_string());
248            sections.push("        Ok(()) => {}".to_string());
249            sections.push("        Err(e) => {".to_string());
250            sections.push("            eprintln!(\"{}\", e);".to_string());
251            sections.push("            std::process::exit(1);".to_string());
252            sections.push("        }".to_string());
253            sections.push("    }".to_string());
254        } else {
255            let ret_type = types::type_annotation_to_rust(&main_fn.unwrap().return_type);
256            sections.push(format!("fn main() -> {} {{", ret_type));
257            if has_replay && guest_entry.is_none() {
258                sections.push(
259                    "    aver_replay::with_guest_scope(\"main\", serde_json::Value::Null, aver_generated::entry::main)"
260                        .to_string(),
261                );
262            } else {
263                sections.push("    aver_generated::entry::main()".to_string());
264            }
265        }
266    } else {
267        sections.push("fn main() {".to_string());
268        if main_fn.is_some() {
269            sections.push("    let child = std::thread::Builder::new()".to_string());
270            sections.push("        .stack_size(256 * 1024 * 1024)".to_string());
271            if has_replay && guest_entry.is_none() {
272                sections.push("        .spawn(|| aver_replay::with_guest_scope(\"main\", serde_json::Value::Null, || aver_generated::entry::main()))".to_string());
273            } else {
274                sections.push("        .spawn(|| aver_generated::entry::main())".to_string());
275            }
276            sections.push("        .expect(\"thread spawn\");".to_string());
277            sections.push("    child.join().expect(\"thread join\");".to_string());
278        }
279    }
280    sections.push("}".to_string());
281    sections.push(String::new());
282
283    sections.join("\n")
284}
285
286fn render_runtime_support(
287    has_tcp_types: bool,
288    has_http_types: bool,
289    has_http_server_types: bool,
290    has_replay: bool,
291    embedded_independence_cancel: bool,
292) -> String {
293    let mut sections = vec![runtime::generate_runtime(
294        has_replay,
295        has_http_server_types,
296        embedded_independence_cancel,
297    )];
298    if has_tcp_types {
299        sections.push(runtime::generate_tcp_types());
300    }
301    if has_http_types {
302        sections.push(runtime::generate_http_types());
303    }
304    if has_http_server_types {
305        sections.push(runtime::generate_http_server_types());
306    }
307    format!("{}\n", sections.join("\n\n"))
308}
309
310fn render_verify_module(
311    verify_blocks: &[&crate::ast::VerifyBlock],
312    ctx: &CodegenContext,
313) -> String {
314    [
315        "#[allow(unused_imports)]".to_string(),
316        "use crate::*;".to_string(),
317        "#[allow(unused_imports)]".to_string(),
318        "use crate::aver_generated::entry::*;".to_string(),
319        String::new(),
320        toplevel::emit_verify_blocks(verify_blocks, ctx),
321        String::new(),
322    ]
323    .join("\n")
324}
325
326fn render_generated_module(depends: Vec<String>, sections: Vec<String>) -> String {
327    if sections.is_empty() {
328        String::new()
329    } else {
330        let mut lines = vec![
331            "#[allow(unused_imports)]".to_string(),
332            "use crate::*;".to_string(),
333        ];
334        for dep in depends {
335            let path = module_prefix_to_rust_segments(&dep).join("::");
336            lines.push("#[allow(unused_imports)]".to_string());
337            lines.push(format!("use crate::aver_generated::{}::*;", path));
338        }
339        lines.push(String::new());
340        lines.push(sections.join("\n\n"));
341        lines.push(String::new());
342        lines.join("\n")
343    }
344}
345
346fn entry_module_sections(
347    ctx: &CodegenContext,
348    main_fn: Option<&FnDef>,
349    top_level_stmts: &[&crate::ast::Stmt],
350) -> Vec<String> {
351    let mut sections = Vec::new();
352
353    for td in &ctx.type_defs {
354        if is_shared_runtime_type(td) {
355            continue;
356        }
357        sections.push(toplevel::emit_public_type_def(td, ctx));
358        if ctx.emit_replay_runtime {
359            sections.push(replay::emit_replay_value_impl(td));
360        }
361    }
362
363    // Detect mutual TCO groups among non-main functions.
364    let non_main_fns: Vec<&FnDef> = ctx.fn_defs.iter().filter(|fd| fd.name != "main").collect();
365    let mutual_groups = toplevel::find_mutual_tco_groups(&non_main_fns);
366    let mut mutual_tco_members: HashSet<String> = HashSet::new();
367
368    for (group_id, group_indices) in mutual_groups.iter().enumerate() {
369        let group_fns: Vec<&FnDef> = group_indices.iter().map(|&idx| non_main_fns[idx]).collect();
370        for fd in &group_fns {
371            mutual_tco_members.insert(fd.name.clone());
372        }
373        sections.push(toplevel::emit_mutual_tco_block(
374            group_id + 1,
375            &group_fns,
376            ctx,
377            "pub ",
378        ));
379    }
380
381    for fd in &ctx.fn_defs {
382        if fd.name == "main" || mutual_tco_members.contains(&fd.name) {
383            continue;
384        }
385        let is_memo = ctx.memo_fns.contains(&fd.name);
386        sections.push(toplevel::emit_public_fn_def(fd, is_memo, ctx));
387    }
388
389    if main_fn.is_some() || !top_level_stmts.is_empty() {
390        sections.push(toplevel::emit_public_main(main_fn, top_level_stmts, ctx));
391    }
392
393    sections
394}
395
396fn module_sections(module: &crate::codegen::ModuleInfo, ctx: &CodegenContext) -> Vec<String> {
397    let mut sections = Vec::new();
398
399    for td in &module.type_defs {
400        if is_shared_runtime_type(td) {
401            continue;
402        }
403        sections.push(toplevel::emit_public_type_def(td, ctx));
404        if ctx.emit_replay_runtime {
405            sections.push(replay::emit_replay_value_impl(td));
406        }
407    }
408
409    // Detect mutual TCO groups among module functions.
410    let fn_refs: Vec<&FnDef> = module.fn_defs.iter().collect();
411    let mutual_groups = toplevel::find_mutual_tco_groups(&fn_refs);
412    let mut mutual_tco_members: HashSet<String> = HashSet::new();
413
414    for (group_id, group_indices) in mutual_groups.iter().enumerate() {
415        let group_fns: Vec<&FnDef> = group_indices.iter().map(|&idx| fn_refs[idx]).collect();
416        for fd in &group_fns {
417            mutual_tco_members.insert(fd.name.clone());
418        }
419        sections.push(toplevel::emit_mutual_tco_block(
420            group_id + 1,
421            &group_fns,
422            ctx,
423            "pub ",
424        ));
425    }
426
427    for fd in &module.fn_defs {
428        if mutual_tco_members.contains(&fd.name) {
429            continue;
430        }
431        let is_memo = ctx.memo_fns.contains(&fd.name);
432        sections.push(toplevel::emit_public_fn_def(fd, is_memo, ctx));
433    }
434
435    sections
436}
437
438fn insert_module_content(node: &mut ModuleTreeNode, segments: &[String], content: String) {
439    let child = node.children.entry(segments[0].clone()).or_default();
440    if segments.len() == 1 {
441        child.content = Some(content);
442    } else {
443        insert_module_content(child, &segments[1..], content);
444    }
445}
446
447fn emit_module_tree_files(node: &ModuleTreeNode, rel_dir: &str, files: &mut Vec<(String, String)>) {
448    let mut parts = Vec::new();
449
450    if let Some(content) = &node.content
451        && !content.trim().is_empty()
452    {
453        parts.push(content.trim_end().to_string());
454    }
455
456    for child_name in node.children.keys() {
457        parts.push(format!("pub mod {};", child_name));
458    }
459
460    let mut mod_rs = parts.join("\n\n");
461    if !mod_rs.is_empty() {
462        mod_rs.push('\n');
463    }
464    files.push((format!("{}/mod.rs", rel_dir), mod_rs));
465
466    for (child_name, child) in &node.children {
467        emit_module_tree_files(child, &format!("{}/{}", rel_dir, child_name), files);
468    }
469}
470
471fn root_module_depends(items: &[TopLevel]) -> Vec<String> {
472    items
473        .iter()
474        .find_map(|item| {
475            if let TopLevel::Module(module) = item {
476                Some(module.depends.clone())
477            } else {
478                None
479            }
480        })
481        .unwrap_or_default()
482}
483
484/// Detect which effectful services are used in the program (including modules).
485fn detect_used_services(ctx: &CodegenContext) -> HashSet<String> {
486    let mut services = HashSet::new();
487    for item in &ctx.items {
488        if let TopLevel::FnDef(fd) = item {
489            for eff in &fd.effects {
490                services.insert(eff.node.clone());
491                if let Some((service, _)) = eff.node.split_once('.') {
492                    services.insert(service.to_string());
493                }
494            }
495        }
496    }
497    for module in &ctx.modules {
498        for fd in &module.fn_defs {
499            for eff in &fd.effects {
500                services.insert(eff.node.clone());
501                if let Some((service, _)) = eff.node.split_once('.') {
502                    services.insert(service.to_string());
503                }
504            }
505        }
506    }
507    services
508}
509
510fn is_shared_runtime_type(td: &TypeDef) -> bool {
511    matches!(
512        td,
513        TypeDef::Product { name, .. }
514            if matches!(name.as_str(), "Header" | "HttpResponse" | "HttpRequest")
515    )
516}
517
518fn needs_named_type(ctx: &CodegenContext, wanted: &str) -> bool {
519    ctx.fn_sigs.values().any(|(params, ret, _effects)| {
520        params.iter().any(|p| type_contains_named(p, wanted)) || type_contains_named(ret, wanted)
521    })
522}
523
524fn type_contains_named(ty: &Type, wanted: &str) -> bool {
525    match ty {
526        Type::Named(name) => name == wanted,
527        Type::Result(ok, err) => {
528            type_contains_named(ok, wanted) || type_contains_named(err, wanted)
529        }
530        Type::Option(inner) | Type::List(inner) | Type::Vector(inner) => {
531            type_contains_named(inner, wanted)
532        }
533        Type::Tuple(items) => items.iter().any(|t| type_contains_named(t, wanted)),
534        Type::Map(k, v) => type_contains_named(k, wanted) || type_contains_named(v, wanted),
535        Type::Fn(params, ret, _effects) => {
536            params.iter().any(|t| type_contains_named(t, wanted))
537                || type_contains_named(ret, wanted)
538        }
539        Type::Int | Type::Float | Type::Str | Type::Bool | Type::Unit | Type::Unknown => false,
540    }
541}
542
543#[cfg(test)]
544mod tests {
545    use super::{
546        ModuleTreeNode, emit_module_tree_files, insert_module_content, render_generated_module,
547        transpile,
548    };
549    use crate::codegen::build_context;
550    use crate::source::parse_source;
551    use crate::tco;
552    use crate::types::checker::run_type_check_full;
553    use std::collections::HashSet;
554
555    fn ctx_from_source(source: &str, project_name: &str) -> crate::codegen::CodegenContext {
556        let mut items = parse_source(source).expect("source should parse");
557        tco::transform_program(&mut items);
558        let tc = run_type_check_full(&items, None);
559        assert!(
560            tc.errors.is_empty(),
561            "source should typecheck without errors: {:?}",
562            tc.errors
563        );
564        build_context(items, &tc, HashSet::new(), project_name.to_string(), vec![])
565    }
566
567    fn generated_rust_entry_file(out: &crate::codegen::ProjectOutput) -> &str {
568        out.files
569            .iter()
570            .find_map(|(name, content)| {
571                (name == "src/aver_generated/entry/mod.rs").then_some(content.as_str())
572            })
573            .expect("expected generated Rust entry module")
574    }
575
576    fn generated_file<'a>(out: &'a crate::codegen::ProjectOutput, path: &str) -> &'a str {
577        out.files
578            .iter()
579            .find_map(|(name, content)| (name == path).then_some(content.as_str()))
580            .unwrap_or_else(|| panic!("expected generated file '{}'", path))
581    }
582
583    #[test]
584    fn emission_banner_appears_in_root_main() {
585        let mut ctx = ctx_from_source(
586            r#"
587module Demo
588
589fn main() -> Int
590    1
591"#,
592            "demo",
593        );
594
595        let out = transpile(&mut ctx);
596        let root_main = generated_file(&out, "src/main.rs");
597
598        assert!(root_main.contains("// Aver Rust emission"));
599    }
600
601    #[test]
602    fn generated_module_imports_direct_depends() {
603        let rendered = render_generated_module(
604            vec!["Domain.Types".to_string(), "App.Commands".to_string()],
605            vec!["pub fn demo() {}".to_string()],
606        );
607
608        assert!(rendered.contains("use crate::aver_generated::domain::types::*;"));
609        assert!(rendered.contains("use crate::aver_generated::app::commands::*;"));
610        assert!(rendered.contains("pub fn demo() {}"));
611    }
612
613    #[test]
614    fn module_tree_files_do_not_reexport_children() {
615        let mut tree = ModuleTreeNode::default();
616        insert_module_content(
617            &mut tree,
618            &["app".to_string(), "cli".to_string()],
619            "pub fn run() {}".to_string(),
620        );
621
622        let mut files = Vec::new();
623        emit_module_tree_files(&tree, "src/aver_generated", &mut files);
624
625        let root_mod = files
626            .iter()
627            .find(|(path, _)| path == "src/aver_generated/mod.rs")
628            .map(|(_, content)| content)
629            .expect("root mod.rs should exist");
630
631        assert!(root_mod.contains("pub mod app;"));
632        assert!(!root_mod.contains("pub use app::*;"));
633    }
634
635    #[test]
636    fn list_cons_match_uses_cloned_uncons_fast_path_when_optimized() {
637        let mut ctx = ctx_from_source(
638            r#"
639module Demo
640
641fn headPlusTailLen(xs: List<Int>) -> Int
642    match xs
643        [] -> 0
644        [h, ..t] -> h + List.len(t)
645"#,
646            "demo",
647        );
648
649        let out = transpile(&mut ctx);
650        let entry = generated_rust_entry_file(&out);
651
652        // The common []/[h,..t] pattern uses aver_list_match! macro
653        assert!(entry.contains("aver_list_match!"));
654    }
655
656    #[test]
657    fn list_cons_match_stays_structured_in_semantic_mode() {
658        let mut ctx = ctx_from_source(
659            r#"
660module Demo
661
662fn headPlusTailLen(xs: List<Int>) -> Int
663    match xs
664        [] -> 0
665        [h, ..t] -> h + List.len(t)
666"#,
667            "demo",
668        );
669
670        let out = transpile(&mut ctx);
671        let entry = generated_rust_entry_file(&out);
672
673        // Both modes now use the aver_list_match! macro for []/[h,..t] patterns
674        assert!(entry.contains("aver_list_match!"));
675    }
676
677    #[test]
678    fn list_literal_clones_ident_when_used_afterward() {
679        let mut ctx = ctx_from_source(
680            r#"
681module Demo
682
683record Audit
684    message: String
685
686fn useTwice(audit: Audit) -> List<Audit>
687    first = [audit]
688    [audit]
689"#,
690            "demo",
691        );
692
693        let out = transpile(&mut ctx);
694        let entry = generated_rust_entry_file(&out);
695
696        assert!(entry.contains("let first = aver_rt::AverList::from_vec(vec![audit.clone()]);"));
697        // Borrowed param always needs .clone() when consumed
698        assert!(entry.contains("aver_rt::AverList::from_vec(vec![audit.clone()])"));
699    }
700
701    #[test]
702    fn record_update_clones_base_when_value_is_used_afterward() {
703        let mut ctx = ctx_from_source(
704            r#"
705module Demo
706
707record PaymentState
708    paymentId: String
709    currency: String
710
711fn touch(state: PaymentState) -> String
712    updated = PaymentState.update(state, currency = "EUR")
713    state.paymentId
714"#,
715            "demo",
716        );
717
718        let out = transpile(&mut ctx);
719        let entry = generated_rust_entry_file(&out);
720
721        assert!(entry.contains("..state.clone()"));
722    }
723
724    #[test]
725    fn mutual_tco_generates_trampoline_instead_of_regular_calls() {
726        let mut ctx = ctx_from_source(
727            r#"
728module Demo
729
730fn isEven(n: Int) -> Bool
731    match n == 0
732        true -> true
733        false -> isOdd(n - 1)
734
735fn isOdd(n: Int) -> Bool
736    match n == 0
737        true -> false
738        false -> isEven(n - 1)
739"#,
740            "demo",
741        );
742
743        let out = transpile(&mut ctx);
744        let entry = generated_rust_entry_file(&out);
745
746        // Should generate trampoline enum and dispatch
747        assert!(entry.contains("enum __MutualTco1"));
748        assert!(entry.contains("fn __mutual_tco_trampoline_1"));
749        assert!(entry.contains("loop {"));
750
751        // Wrapper functions delegate to trampoline
752        assert!(entry.contains("pub fn isEven"));
753        assert!(entry.contains("pub fn isOdd"));
754        assert!(entry.contains("__mutual_tco_trampoline_1("));
755
756        // Should NOT contain direct recursive calls between the two
757        assert!(!entry.contains("isOdd((n - 1i64))"));
758    }
759
760    #[test]
761    fn field_access_does_not_double_clone() {
762        let mut ctx = ctx_from_source(
763            r#"
764module Demo
765
766record User
767    name: String
768    age: Int
769
770fn greet(u: User) -> String
771    u.name
772"#,
773            "demo",
774        );
775
776        let out = transpile(&mut ctx);
777        let entry = generated_rust_entry_file(&out);
778
779        // Field access should produce exactly one .clone(), never .clone().clone()
780        assert!(
781            !entry.contains(".clone().clone()"),
782            "double clone detected in generated code:\n{}",
783            entry
784        );
785    }
786
787    #[test]
788    fn borrowed_record_field_return_clones_for_owned_result() {
789        let mut ctx = ctx_from_source(
790            r#"
791module Demo
792
793record User
794    name: String
795
796fn getName(user: User) -> String
797    user.name
798"#,
799            "demo",
800        );
801
802        let out = transpile(&mut ctx);
803        let entry = generated_rust_entry_file(&out);
804
805        assert!(entry.contains("pub fn getName(user: &User) -> AverStr"));
806        assert!(
807            entry.contains("user.name.clone()"),
808            "missing owned clone:\n{}",
809            entry
810        );
811    }
812
813    #[test]
814    fn vector_get_with_literal_default_lowers_to_direct_unwrap_or_code() {
815        let mut ctx = ctx_from_source(
816            r#"
817module Demo
818
819fn cellAt(grid: Vector<Int>, idx: Int) -> Int
820    Option.withDefault(Vector.get(grid, idx), 0)
821"#,
822            "demo",
823        );
824
825        let out = transpile(&mut ctx);
826        let entry = generated_rust_entry_file(&out);
827
828        assert!(entry.contains("grid.get(idx as usize).cloned().unwrap_or(0i64)"));
829    }
830
831    #[test]
832    fn vector_set_default_stays_structured_in_semantic_mode() {
833        let mut ctx = ctx_from_source(
834            r#"
835module Demo
836
837fn updateOrKeep(vec: Vector<Int>, idx: Int, value: Int) -> Vector<Int>
838    Option.withDefault(Vector.set(vec, idx, value), vec)
839"#,
840            "demo",
841        );
842
843        let out = transpile(&mut ctx);
844        let entry = generated_rust_entry_file(&out);
845
846        // Both modes now use the inlined set_unchecked fast path
847        assert!(entry.contains("set_unchecked"));
848        assert!(!entry.contains(".unwrap_or("));
849    }
850
851    #[test]
852    fn vector_set_default_uses_ir_leaf_fast_path_when_optimized() {
853        let mut ctx = ctx_from_source(
854            r#"
855module Demo
856
857fn updateOrKeep(vec: Vector<Int>, idx: Int, value: Int) -> Vector<Int>
858    Option.withDefault(Vector.set(vec, idx, value), vec)
859"#,
860            "demo",
861        );
862
863        let out = transpile(&mut ctx);
864        let entry = generated_rust_entry_file(&out);
865
866        assert!(entry.contains("set_unchecked"));
867        assert!(!entry.contains(".unwrap_or("));
868    }
869
870    #[test]
871    fn vector_set_uses_owned_update_lowering() {
872        let mut ctx = ctx_from_source(
873            r#"
874module Demo
875
876fn update(vec: Vector<Int>, idx: Int, value: Int) -> Option<Vector<Int>>
877    Vector.set(vec, idx, value)
878"#,
879            "demo",
880        );
881
882        let out = transpile(&mut ctx);
883        let entry = generated_rust_entry_file(&out);
884
885        assert!(entry.contains(".set_owned("));
886        assert!(!entry.contains(".set(idx as usize,"));
887    }
888
889    #[test]
890    fn map_remove_uses_owned_update_lowering() {
891        let mut ctx = ctx_from_source(
892            r#"
893module Demo
894
895fn dropKey(m: Map<String, Int>, key: String) -> Map<String, Int>
896    Map.remove(m, key)
897"#,
898            "demo",
899        );
900
901        let out = transpile(&mut ctx);
902        let entry = generated_rust_entry_file(&out);
903
904        assert!(entry.contains(".remove_owned(&"));
905    }
906
907    #[test]
908    fn semantic_keeps_known_leaf_wrapper_call_structured() {
909        let mut ctx = ctx_from_source(
910            r#"
911module Demo
912
913fn cellAt(grid: Vector<Int>, idx: Int) -> Int
914    Option.withDefault(Vector.get(grid, idx), 0)
915
916fn read(grid: Vector<Int>, idx: Int) -> Int
917    cellAt(grid, idx)
918"#,
919            "demo",
920        );
921
922        let out = transpile(&mut ctx);
923        let entry = generated_rust_entry_file(&out);
924
925        assert!(entry.contains("cellAt(grid, idx)"));
926        assert!(!entry.contains("__aver_thin_arg0"));
927    }
928
929    #[test]
930    fn optimized_keeps_known_leaf_wrapper_callsite_and_leaves_absorption_to_rust() {
931        let mut ctx = ctx_from_source(
932            r#"
933module Demo
934
935fn cellAt(grid: Vector<Int>, idx: Int) -> Int
936    Option.withDefault(Vector.get(grid, idx), 0)
937
938fn read(grid: Vector<Int>, idx: Int) -> Int
939    cellAt(grid, idx)
940"#,
941            "demo",
942        );
943
944        let out = transpile(&mut ctx);
945        let entry = generated_rust_entry_file(&out);
946
947        assert!(entry.contains("cellAt(grid, idx)"));
948        assert!(!entry.contains("__aver_thin_arg0"));
949    }
950
951    #[test]
952    fn optimized_keeps_known_dispatch_wrapper_callsite_and_leaves_absorption_to_rust() {
953        let mut ctx = ctx_from_source(
954            r#"
955module Demo
956
957fn bucket(n: Int) -> Int
958    match n == 0
959        true -> 0
960        false -> 1
961
962fn readBucket(n: Int) -> Int
963    bucket(n)
964"#,
965            "demo",
966        );
967
968        let out = transpile(&mut ctx);
969        let entry = generated_rust_entry_file(&out);
970
971        assert!(entry.contains("bucket(n)"));
972        assert!(!entry.contains("__aver_thin_arg0"));
973    }
974
975    #[test]
976    fn bool_match_on_gte_normalizes_to_base_comparison_when_optimized() {
977        let mut ctx = ctx_from_source(
978            r#"
979module Demo
980
981fn bucket(n: Int) -> Int
982    match n >= 10
983        true -> 7
984        false -> 3
985"#,
986            "demo",
987        );
988
989        let out = transpile(&mut ctx);
990        let entry = generated_rust_entry_file(&out);
991
992        assert!(entry.contains("if (n < 10i64) { 3i64 } else { 7i64 }"));
993    }
994
995    #[test]
996    fn bool_match_stays_as_match_in_semantic_mode() {
997        let mut ctx = ctx_from_source(
998            r#"
999module Demo
1000
1001fn bucket(n: Int) -> Int
1002    match n >= 10
1003        true -> 7
1004        false -> 3
1005"#,
1006            "demo",
1007        );
1008
1009        let out = transpile(&mut ctx);
1010        let entry = generated_rust_entry_file(&out);
1011
1012        // Both modes now use the normalized if-else form
1013        assert!(entry.contains("if (n < 10i64) { 3i64 } else { 7i64 }"));
1014    }
1015
1016    #[test]
1017    fn optimized_self_tco_uses_dispatch_table_for_wrapper_match() {
1018        let mut ctx = ctx_from_source(
1019            r#"
1020module Demo
1021
1022fn loop(r: Result<Int, String>) -> Int
1023    match r
1024        Result.Ok(n) -> n
1025        Result.Err(_) -> loop(Result.Ok(1))
1026"#,
1027            "demo",
1028        );
1029
1030        let out = transpile(&mut ctx);
1031        let entry = generated_rust_entry_file(&out);
1032
1033        // Uses native Rust match directly (r cloned since not-Copy Ident without last_use info)
1034        assert!(entry.contains("match r.clone() {"));
1035        assert!(entry.contains("Ok(n)"));
1036        assert!(!entry.contains("__dispatch_subject"));
1037    }
1038
1039    #[test]
1040    fn optimized_mutual_tco_uses_dispatch_table_for_wrapper_match() {
1041        let mut ctx = ctx_from_source(
1042            r#"
1043module Demo
1044
1045fn left(r: Result<Int, String>) -> Int
1046    match r
1047        Result.Ok(n) -> n
1048        Result.Err(_) -> right(Result.Ok(1))
1049
1050fn right(r: Result<Int, String>) -> Int
1051    match r
1052        Result.Ok(n) -> n
1053        Result.Err(_) -> left(Result.Ok(1))
1054"#,
1055            "demo",
1056        );
1057
1058        let out = transpile(&mut ctx);
1059        let entry = generated_rust_entry_file(&out);
1060
1061        // Uses native Rust match directly (r cloned since not-Copy Ident without last_use info)
1062        assert!(entry.contains("match r.clone() {"));
1063        assert!(entry.contains("Ok(n)"));
1064        assert!(!entry.contains("__dispatch_subject"));
1065    }
1066
1067    #[test]
1068    fn single_field_variant_display_avoids_vec_join() {
1069        let mut ctx = ctx_from_source(
1070            r#"
1071module Demo
1072
1073type Wrapper
1074    Wrap(Int)
1075    Pair(Int, Int)
1076    Empty
1077"#,
1078            "demo",
1079        );
1080
1081        let out = transpile(&mut ctx);
1082        let entry = generated_rust_entry_file(&out);
1083
1084        // Single-field variant Wrap(Int): should NOT use vec![].join()
1085        assert!(
1086            !entry.contains("vec![f0.aver_display_inner()].join"),
1087            "single-field variant should use direct format, not vec join:\n{}",
1088            entry
1089        );
1090        // Multi-field variant Pair(Int, Int): SHOULD still use vec![].join()
1091        assert!(
1092            entry.contains("vec![f0.aver_display_inner(), f1.aver_display_inner()].join(\", \")"),
1093            "multi-field variant should use vec join:\n{}",
1094            entry
1095        );
1096    }
1097
1098    #[test]
1099    fn replay_codegen_wraps_guest_entry_in_scoped_runtime() {
1100        let mut ctx = ctx_from_source(
1101            r#"
1102module Demo
1103
1104fn runGuestProgram(path: String) -> Result<String, String>
1105    ! [Disk.readText]
1106    Disk.readText(path)
1107"#,
1108            "demo",
1109        );
1110        ctx.emit_replay_runtime = true;
1111        ctx.guest_entry = Some("runGuestProgram".to_string());
1112
1113        let out = transpile(&mut ctx);
1114        let entry = generated_rust_entry_file(&out);
1115        let replay_support = generated_file(&out, "src/replay_support.rs");
1116        let cargo_toml = generated_file(&out, "Cargo.toml");
1117
1118        assert!(entry.contains("aver_replay::with_guest_scope_result(\"runGuestProgram\""));
1119        assert!(replay_support.contains("pub mod aver_replay"));
1120        assert!(cargo_toml.contains("serde_json = \"1\""));
1121    }
1122
1123    #[test]
1124    fn replay_codegen_uses_guest_args_param_override_when_present() {
1125        let mut ctx = ctx_from_source(
1126            r#"
1127module Demo
1128
1129fn runGuestProgram(path: String, guestArgs: List<String>) -> Result<String, String>
1130    ! [Args.get]
1131    Result.Ok(String.join(Args.get(), ","))
1132"#,
1133            "demo",
1134        );
1135        ctx.emit_replay_runtime = true;
1136        ctx.guest_entry = Some("runGuestProgram".to_string());
1137
1138        let out = transpile(&mut ctx);
1139        let entry = generated_rust_entry_file(&out);
1140        let cargo_toml = generated_file(&out, "Cargo.toml");
1141
1142        assert!(entry.contains("aver_replay::with_guest_scope_args_result(\"runGuestProgram\""));
1143        assert!(entry.contains("guestArgs.clone()"));
1144        assert!(cargo_toml.contains("edition = \"2024\""));
1145    }
1146
1147    #[test]
1148    fn replay_codegen_wraps_root_main_when_no_guest_entry_is_set() {
1149        let mut ctx = ctx_from_source(
1150            r#"
1151module Demo
1152
1153fn main() -> Result<String, String>
1154    ! [Disk.readText]
1155    Disk.readText("demo.av")
1156"#,
1157            "demo",
1158        );
1159        ctx.emit_replay_runtime = true;
1160
1161        let out = transpile(&mut ctx);
1162        let root_main = generated_file(&out, "src/main.rs");
1163
1164        assert!(
1165            root_main.contains("aver_replay::with_guest_scope(\"main\", serde_json::Value::Null")
1166        );
1167    }
1168
1169    #[test]
1170    fn runtime_policy_codegen_uses_runtime_loader() {
1171        let mut ctx = ctx_from_source(
1172            r#"
1173module Demo
1174
1175fn main() -> Result<String, String>
1176    ! [Disk.readText]
1177    Disk.readText("demo.av")
1178"#,
1179            "demo",
1180        );
1181        ctx.emit_replay_runtime = true;
1182        ctx.runtime_policy_from_env = true;
1183
1184        let out = transpile(&mut ctx);
1185        let root_main = generated_file(&out, "src/main.rs");
1186        let replay_support = generated_file(&out, "src/replay_support.rs");
1187        let cargo_toml = generated_file(&out, "Cargo.toml");
1188
1189        assert!(!root_main.contains("mod policy_support;"));
1190        assert!(replay_support.contains("load_runtime_policy_from_env"));
1191        assert!(cargo_toml.contains("url = \"2\""));
1192        assert!(cargo_toml.contains("toml = \"0.8\""));
1193    }
1194
1195    #[test]
1196    fn replay_codegen_can_keep_embedded_policy_when_requested() {
1197        let mut ctx = ctx_from_source(
1198            r#"
1199module Demo
1200
1201fn main() -> Result<String, String>
1202    ! [Disk.readText]
1203    Disk.readText("demo.av")
1204"#,
1205            "demo",
1206        );
1207        ctx.emit_replay_runtime = true;
1208        ctx.policy = Some(crate::config::ProjectConfig {
1209            effect_policies: std::collections::HashMap::new(),
1210            check_suppressions: Vec::new(),
1211            independence_mode: crate::config::IndependenceMode::default(),
1212        });
1213
1214        let out = transpile(&mut ctx);
1215        let root_main = generated_file(&out, "src/main.rs");
1216        let replay_support = generated_file(&out, "src/replay_support.rs");
1217
1218        assert!(root_main.contains("mod policy_support;"));
1219        assert!(replay_support.contains("aver_policy::check_disk"));
1220        assert!(!replay_support.contains("RuntimeEffectPolicy"));
1221    }
1222
1223    #[test]
1224    fn self_host_support_is_emitted_as_separate_module() {
1225        let mut ctx = ctx_from_source(
1226            r#"
1227module Demo
1228
1229fn runGuestProgram(prog: Int, moduleFns: Int) -> Result<String, String>
1230    Result.Ok("ok")
1231"#,
1232            "demo",
1233        );
1234        ctx.emit_self_host_support = true;
1235        ctx.guest_entry = Some("runGuestProgram".to_string());
1236
1237        let out = transpile(&mut ctx);
1238        let root_main = generated_file(&out, "src/main.rs");
1239        let runtime_support = generated_file(&out, "src/runtime_support.rs");
1240        let self_host_support = generated_file(&out, "src/self_host_support.rs");
1241        let entry = generated_rust_entry_file(&out);
1242
1243        assert!(root_main.contains("mod self_host_support;"));
1244        assert!(!runtime_support.contains("with_fn_store"));
1245        assert!(self_host_support.contains("pub fn with_program_fn_store"));
1246        assert!(entry.contains("crate::self_host_support::with_program_fn_store("));
1247    }
1248
1249    #[test]
1250    fn independent_product_codegen_avoids_string_specific_error_type() {
1251        let mut ctx = ctx_from_source(
1252            r#"
1253module Demo
1254
1255fn left() -> Result<Int, Int>
1256    Result.Ok(1)
1257
1258fn right() -> Result<Int, Int>
1259    Result.Ok(2)
1260
1261fn main() -> Result<(Int, Int), Int>
1262    data = (left(), right())?!
1263    Result.Ok(data)
1264"#,
1265            "demo",
1266        );
1267        ctx.emit_replay_runtime = true;
1268
1269        let out = transpile(&mut ctx);
1270        let entry = generated_rust_entry_file(&out);
1271
1272        assert!(!entry.contains("Ok::<_, aver_rt::AverStr>"));
1273        assert!(entry.contains("crate::aver_replay::exit_effect_group();"));
1274        assert!(entry.contains("match (_r0, _r1)"));
1275        assert!(!entry.contains("let _r0 = left()?;"));
1276    }
1277
1278    #[test]
1279    fn independent_product_codegen_emits_cancel_runtime_and_scope_propagation() {
1280        let mut ctx = ctx_from_source(
1281            r#"
1282module Demo
1283
1284fn left() -> Result<Int, String>
1285    Result.Ok(1)
1286
1287fn right() -> Result<Int, String>
1288    Result.Ok(2)
1289
1290fn main() -> Result<(Int, Int), String>
1291    data = (left(), right())?!
1292    Result.Ok(data)
1293"#,
1294            "demo",
1295        );
1296        ctx.emit_replay_runtime = true;
1297        ctx.policy = Some(crate::config::ProjectConfig {
1298            effect_policies: std::collections::HashMap::new(),
1299            check_suppressions: Vec::new(),
1300            independence_mode: crate::config::IndependenceMode::Cancel,
1301        });
1302
1303        let out = transpile(&mut ctx);
1304        let entry = generated_rust_entry_file(&out);
1305        let runtime_support = generated_file(&out, "src/runtime_support.rs");
1306        let replay_support = generated_file(&out, "src/replay_support.rs");
1307
1308        assert!(entry.contains("crate::run_cancelable_branch"));
1309        assert!(entry.contains("capture_parallel_scope_context"));
1310        assert!(entry.contains("_s.spawn(move ||"));
1311        assert!(runtime_support.contains("pub fn run_cancelable_branch"));
1312        assert!(runtime_support.contains("panic_any(AverCancelled)"));
1313        assert!(replay_support.contains("pub fn capture_parallel_scope_context"));
1314        assert!(replay_support.contains("pub fn independence_mode_is_cancel()"));
1315    }
1316
1317    #[test]
1318    fn runtime_policy_codegen_parses_independence_mode() {
1319        let mut ctx = ctx_from_source(
1320            r#"
1321module Demo
1322
1323fn main() -> Result<String, String>
1324    ! [Disk.readText]
1325    Disk.readText("demo.av")
1326"#,
1327            "demo",
1328        );
1329        ctx.emit_replay_runtime = true;
1330        ctx.runtime_policy_from_env = true;
1331
1332        let out = transpile(&mut ctx);
1333        let replay_support = generated_file(&out, "src/replay_support.rs");
1334
1335        assert!(replay_support.contains("independence_mode_cancel"));
1336        assert!(replay_support.contains("[independence].mode must be 'complete' or 'cancel'"));
1337    }
1338
1339    #[test]
1340    fn effectful_codegen_inserts_cancel_checkpoint_before_builtin_calls() {
1341        let mut ctx = ctx_from_source(
1342            r#"
1343module Demo
1344
1345fn main() -> Result<String, String>
1346    ! [Disk.readText]
1347    Disk.readText("demo.av")
1348"#,
1349            "demo",
1350        );
1351
1352        let out = transpile(&mut ctx);
1353        let entry = generated_rust_entry_file(&out);
1354
1355        assert!(entry.contains("crate::cancel_checkpoint(); (aver_rt::read_text"));
1356    }
1357
1358    #[test]
1359    fn replay_support_matches_group_effects_by_occurrence_and_args() {
1360        let mut ctx = ctx_from_source(
1361            r#"
1362module Demo
1363
1364fn left() -> Result<Int, String>
1365    Result.Ok(1)
1366
1367fn right() -> Result<Int, String>
1368    Result.Ok(2)
1369
1370fn main() -> Result<(Int, Int), String>
1371    data = (left(), right())?!
1372    Result.Ok(data)
1373"#,
1374            "demo",
1375        );
1376        ctx.emit_replay_runtime = true;
1377
1378        let out = transpile(&mut ctx);
1379        let replay_support = generated_file(&out, "src/replay_support.rs");
1380
1381        assert!(replay_support.contains("candidate.effect_occurrence"));
1382        assert!(replay_support.contains("candidate.args != args"));
1383    }
1384}