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