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