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