Skip to main content

aver/codegen/rust/
mod.rs

1/// Rust backend for the Aver transpiler.
2///
3/// Transforms Aver AST -> valid Rust source code.
4mod builtins;
5mod expr;
6mod liveness;
7mod pattern;
8mod policy;
9mod project;
10mod replay;
11mod runtime;
12mod self_host;
13mod syntax;
14mod toplevel;
15mod types;
16
17use std::collections::{BTreeMap, HashSet};
18
19use crate::ast::{FnDef, TopLevel, TypeDef};
20use crate::codegen::common::module_prefix_to_rust_segments;
21use crate::codegen::{CodegenContext, ProjectOutput};
22use crate::types::Type;
23
24#[derive(Default)]
25struct ModuleTreeNode {
26    content: Option<String>,
27    children: BTreeMap<String, ModuleTreeNode>,
28}
29
30/// Transpile an Aver program to a Rust project.
31pub fn transpile(ctx: &CodegenContext) -> ProjectOutput {
32    let has_embedded_policy = ctx.policy.is_some();
33    let has_runtime_policy = ctx.runtime_policy_from_env;
34    let 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 vector_get_with_literal_default_lowers_to_direct_unwrap_or_code() {
785        let ctx = ctx_from_source(
786            r#"
787module Demo
788
789fn cellAt(grid: Vector<Int>, idx: Int) -> Int
790    Option.withDefault(Vector.get(grid, idx), 0)
791"#,
792            "demo",
793        );
794
795        let out = transpile(&ctx);
796        let entry = generated_rust_entry_file(&out);
797
798        assert!(entry.contains("grid.get(idx as usize).cloned().unwrap_or(0i64)"));
799    }
800
801    #[test]
802    fn vector_set_default_stays_structured_in_semantic_mode() {
803        let ctx = ctx_from_source(
804            r#"
805module Demo
806
807fn updateOrKeep(vec: Vector<Int>, idx: Int, value: Int) -> Vector<Int>
808    Option.withDefault(Vector.set(vec, idx, value), vec)
809"#,
810            "demo",
811        );
812
813        let out = transpile(&ctx);
814        let entry = generated_rust_entry_file(&out);
815
816        // Both modes now use the inlined set_unchecked fast path
817        assert!(entry.contains("set_unchecked"));
818        assert!(!entry.contains(".unwrap_or("));
819    }
820
821    #[test]
822    fn vector_set_default_uses_ir_leaf_fast_path_when_optimized() {
823        let ctx = ctx_from_source(
824            r#"
825module Demo
826
827fn updateOrKeep(vec: Vector<Int>, idx: Int, value: Int) -> Vector<Int>
828    Option.withDefault(Vector.set(vec, idx, value), vec)
829"#,
830            "demo",
831        );
832
833        let out = transpile(&ctx);
834        let entry = generated_rust_entry_file(&out);
835
836        assert!(entry.contains("set_unchecked"));
837        assert!(!entry.contains(".unwrap_or("));
838    }
839
840    #[test]
841    fn vector_set_uses_owned_update_lowering() {
842        let ctx = ctx_from_source(
843            r#"
844module Demo
845
846fn update(vec: Vector<Int>, idx: Int, value: Int) -> Option<Vector<Int>>
847    Vector.set(vec, idx, value)
848"#,
849            "demo",
850        );
851
852        let out = transpile(&ctx);
853        let entry = generated_rust_entry_file(&out);
854
855        assert!(entry.contains(".set_owned("));
856        assert!(!entry.contains(".set(idx as usize,"));
857    }
858
859    #[test]
860    fn map_remove_uses_owned_update_lowering() {
861        let ctx = ctx_from_source(
862            r#"
863module Demo
864
865fn dropKey(m: Map<String, Int>, key: String) -> Map<String, Int>
866    Map.remove(m, key)
867"#,
868            "demo",
869        );
870
871        let out = transpile(&ctx);
872        let entry = generated_rust_entry_file(&out);
873
874        assert!(entry.contains(".remove_owned(&"));
875    }
876
877    #[test]
878    fn semantic_keeps_known_leaf_wrapper_call_structured() {
879        let ctx = ctx_from_source(
880            r#"
881module Demo
882
883fn cellAt(grid: Vector<Int>, idx: Int) -> Int
884    Option.withDefault(Vector.get(grid, idx), 0)
885
886fn read(grid: Vector<Int>, idx: Int) -> Int
887    cellAt(grid, idx)
888"#,
889            "demo",
890        );
891
892        let out = transpile(&ctx);
893        let entry = generated_rust_entry_file(&out);
894
895        assert!(entry.contains("cellAt(grid, idx)"));
896        assert!(!entry.contains("__aver_thin_arg0"));
897    }
898
899    #[test]
900    fn optimized_keeps_known_leaf_wrapper_callsite_and_leaves_absorption_to_rust() {
901        let ctx = ctx_from_source(
902            r#"
903module Demo
904
905fn cellAt(grid: Vector<Int>, idx: Int) -> Int
906    Option.withDefault(Vector.get(grid, idx), 0)
907
908fn read(grid: Vector<Int>, idx: Int) -> Int
909    cellAt(grid, idx)
910"#,
911            "demo",
912        );
913
914        let out = transpile(&ctx);
915        let entry = generated_rust_entry_file(&out);
916
917        assert!(entry.contains("cellAt(grid, idx)"));
918        assert!(!entry.contains("__aver_thin_arg0"));
919    }
920
921    #[test]
922    fn optimized_keeps_known_dispatch_wrapper_callsite_and_leaves_absorption_to_rust() {
923        let ctx = ctx_from_source(
924            r#"
925module Demo
926
927fn bucket(n: Int) -> Int
928    match n == 0
929        true -> 0
930        false -> 1
931
932fn readBucket(n: Int) -> Int
933    bucket(n)
934"#,
935            "demo",
936        );
937
938        let out = transpile(&ctx);
939        let entry = generated_rust_entry_file(&out);
940
941        assert!(entry.contains("bucket(n)"));
942        assert!(!entry.contains("__aver_thin_arg0"));
943    }
944
945    #[test]
946    fn bool_match_on_gte_normalizes_to_base_comparison_when_optimized() {
947        let ctx = ctx_from_source(
948            r#"
949module Demo
950
951fn bucket(n: Int) -> Int
952    match n >= 10
953        true -> 7
954        false -> 3
955"#,
956            "demo",
957        );
958
959        let out = transpile(&ctx);
960        let entry = generated_rust_entry_file(&out);
961
962        assert!(entry.contains("if (n < 10i64) { 3i64 } else { 7i64 }"));
963    }
964
965    #[test]
966    fn bool_match_stays_as_match_in_semantic_mode() {
967        let ctx = ctx_from_source(
968            r#"
969module Demo
970
971fn bucket(n: Int) -> Int
972    match n >= 10
973        true -> 7
974        false -> 3
975"#,
976            "demo",
977        );
978
979        let out = transpile(&ctx);
980        let entry = generated_rust_entry_file(&out);
981
982        // Both modes now use the normalized if-else form
983        assert!(entry.contains("if (n < 10i64) { 3i64 } else { 7i64 }"));
984    }
985
986    #[test]
987    fn optimized_self_tco_uses_dispatch_table_for_wrapper_match() {
988        let ctx = ctx_from_source(
989            r#"
990module Demo
991
992fn loop(r: Result<Int, String>) -> Int
993    match r
994        Result.Ok(n) -> n
995        Result.Err(_) -> loop(Result.Ok(1))
996"#,
997            "demo",
998        );
999
1000        let out = transpile(&ctx);
1001        let entry = generated_rust_entry_file(&out);
1002
1003        // Now uses native Rust match directly instead of dispatch table
1004        assert!(entry.contains("match r {"));
1005        assert!(entry.contains("Ok(n)"));
1006        assert!(!entry.contains("__dispatch_subject"));
1007    }
1008
1009    #[test]
1010    fn optimized_mutual_tco_uses_dispatch_table_for_wrapper_match() {
1011        let ctx = ctx_from_source(
1012            r#"
1013module Demo
1014
1015fn left(r: Result<Int, String>) -> Int
1016    match r
1017        Result.Ok(n) -> n
1018        Result.Err(_) -> right(Result.Ok(1))
1019
1020fn right(r: Result<Int, String>) -> Int
1021    match r
1022        Result.Ok(n) -> n
1023        Result.Err(_) -> left(Result.Ok(1))
1024"#,
1025            "demo",
1026        );
1027
1028        let out = transpile(&ctx);
1029        let entry = generated_rust_entry_file(&out);
1030
1031        // Now uses native Rust match directly instead of dispatch table
1032        assert!(entry.contains("match r {"));
1033        assert!(entry.contains("Ok(n)"));
1034        assert!(!entry.contains("__dispatch_subject"));
1035    }
1036
1037    #[test]
1038    fn single_field_variant_display_avoids_vec_join() {
1039        let ctx = ctx_from_source(
1040            r#"
1041module Demo
1042
1043type Wrapper
1044    Wrap(Int)
1045    Pair(Int, Int)
1046    Empty
1047"#,
1048            "demo",
1049        );
1050
1051        let out = transpile(&ctx);
1052        let entry = generated_rust_entry_file(&out);
1053
1054        // Single-field variant Wrap(Int): should NOT use vec![].join()
1055        assert!(
1056            !entry.contains("vec![f0.aver_display_inner()].join"),
1057            "single-field variant should use direct format, not vec join:\n{}",
1058            entry
1059        );
1060        // Multi-field variant Pair(Int, Int): SHOULD still use vec![].join()
1061        assert!(
1062            entry.contains("vec![f0.aver_display_inner(), f1.aver_display_inner()].join(\", \")"),
1063            "multi-field variant should use vec join:\n{}",
1064            entry
1065        );
1066    }
1067
1068    #[test]
1069    fn replay_codegen_wraps_guest_entry_in_scoped_runtime() {
1070        let mut ctx = ctx_from_source(
1071            r#"
1072module Demo
1073
1074fn runGuestProgram(path: String) -> Result<String, String>
1075    ! [Disk.readText]
1076    Disk.readText(path)
1077"#,
1078            "demo",
1079        );
1080        ctx.emit_replay_runtime = true;
1081        ctx.guest_entry = Some("runGuestProgram".to_string());
1082
1083        let out = transpile(&ctx);
1084        let entry = generated_rust_entry_file(&out);
1085        let replay_support = generated_file(&out, "src/replay_support.rs");
1086        let cargo_toml = generated_file(&out, "Cargo.toml");
1087
1088        assert!(entry.contains("aver_replay::with_guest_scope_result(\"runGuestProgram\""));
1089        assert!(replay_support.contains("pub mod aver_replay"));
1090        assert!(cargo_toml.contains("serde_json = \"1\""));
1091    }
1092
1093    #[test]
1094    fn replay_codegen_uses_guest_args_param_override_when_present() {
1095        let mut ctx = ctx_from_source(
1096            r#"
1097module Demo
1098
1099fn runGuestProgram(path: String, guestArgs: List<String>) -> Result<String, String>
1100    ! [Args.get]
1101    Result.Ok(String.join(Args.get(), ","))
1102"#,
1103            "demo",
1104        );
1105        ctx.emit_replay_runtime = true;
1106        ctx.guest_entry = Some("runGuestProgram".to_string());
1107
1108        let out = transpile(&ctx);
1109        let entry = generated_rust_entry_file(&out);
1110        let cargo_toml = generated_file(&out, "Cargo.toml");
1111
1112        assert!(entry.contains("aver_replay::with_guest_scope_args_result(\"runGuestProgram\""));
1113        assert!(entry.contains("guestArgs.clone()"));
1114        assert!(cargo_toml.contains("edition = \"2024\""));
1115    }
1116
1117    #[test]
1118    fn replay_codegen_wraps_root_main_when_no_guest_entry_is_set() {
1119        let mut ctx = ctx_from_source(
1120            r#"
1121module Demo
1122
1123fn main() -> Result<String, String>
1124    ! [Disk.readText]
1125    Disk.readText("demo.av")
1126"#,
1127            "demo",
1128        );
1129        ctx.emit_replay_runtime = true;
1130
1131        let out = transpile(&ctx);
1132        let root_main = generated_file(&out, "src/main.rs");
1133
1134        assert!(
1135            root_main.contains("aver_replay::with_guest_scope(\"main\", serde_json::Value::Null")
1136        );
1137    }
1138
1139    #[test]
1140    fn runtime_policy_codegen_uses_runtime_loader() {
1141        let mut ctx = ctx_from_source(
1142            r#"
1143module Demo
1144
1145fn main() -> Result<String, String>
1146    ! [Disk.readText]
1147    Disk.readText("demo.av")
1148"#,
1149            "demo",
1150        );
1151        ctx.emit_replay_runtime = true;
1152        ctx.runtime_policy_from_env = true;
1153
1154        let out = transpile(&ctx);
1155        let root_main = generated_file(&out, "src/main.rs");
1156        let replay_support = generated_file(&out, "src/replay_support.rs");
1157        let cargo_toml = generated_file(&out, "Cargo.toml");
1158
1159        assert!(!root_main.contains("mod policy_support;"));
1160        assert!(replay_support.contains("load_runtime_policy_from_env"));
1161        assert!(cargo_toml.contains("url = \"2\""));
1162        assert!(cargo_toml.contains("toml = \"0.8\""));
1163    }
1164
1165    #[test]
1166    fn replay_codegen_can_keep_embedded_policy_when_requested() {
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.policy = Some(crate::config::ProjectConfig {
1179            effect_policies: std::collections::HashMap::new(),
1180            check_suppressions: Vec::new(),
1181            independence_mode: crate::config::IndependenceMode::default(),
1182        });
1183
1184        let out = transpile(&ctx);
1185        let root_main = generated_file(&out, "src/main.rs");
1186        let replay_support = generated_file(&out, "src/replay_support.rs");
1187
1188        assert!(root_main.contains("mod policy_support;"));
1189        assert!(replay_support.contains("aver_policy::check_disk"));
1190        assert!(!replay_support.contains("RuntimeEffectPolicy"));
1191    }
1192
1193    #[test]
1194    fn self_host_support_is_emitted_as_separate_module() {
1195        let mut ctx = ctx_from_source(
1196            r#"
1197module Demo
1198
1199fn runGuestProgram(prog: Int, moduleFns: Int) -> Result<String, String>
1200    Result.Ok("ok")
1201"#,
1202            "demo",
1203        );
1204        ctx.emit_self_host_support = true;
1205        ctx.guest_entry = Some("runGuestProgram".to_string());
1206
1207        let out = transpile(&ctx);
1208        let root_main = generated_file(&out, "src/main.rs");
1209        let runtime_support = generated_file(&out, "src/runtime_support.rs");
1210        let self_host_support = generated_file(&out, "src/self_host_support.rs");
1211        let entry = generated_rust_entry_file(&out);
1212
1213        assert!(root_main.contains("mod self_host_support;"));
1214        assert!(!runtime_support.contains("with_fn_store"));
1215        assert!(self_host_support.contains("pub fn with_program_fn_store"));
1216        assert!(entry.contains("crate::self_host_support::with_program_fn_store("));
1217    }
1218
1219    #[test]
1220    fn independent_product_codegen_avoids_string_specific_error_type() {
1221        let mut ctx = ctx_from_source(
1222            r#"
1223module Demo
1224
1225fn left() -> Result<Int, Int>
1226    Result.Ok(1)
1227
1228fn right() -> Result<Int, Int>
1229    Result.Ok(2)
1230
1231fn main() -> Result<(Int, Int), Int>
1232    data = (left(), right())?!
1233    Result.Ok(data)
1234"#,
1235            "demo",
1236        );
1237        ctx.emit_replay_runtime = true;
1238
1239        let out = transpile(&ctx);
1240        let entry = generated_rust_entry_file(&out);
1241
1242        assert!(!entry.contains("Ok::<_, aver_rt::AverStr>"));
1243        assert!(entry.contains("crate::aver_replay::exit_effect_group();"));
1244        assert!(entry.contains("match (_r0, _r1)"));
1245        assert!(!entry.contains("let _r0 = left()?;"));
1246    }
1247
1248    #[test]
1249    fn independent_product_codegen_emits_cancel_runtime_and_scope_propagation() {
1250        let mut ctx = ctx_from_source(
1251            r#"
1252module Demo
1253
1254fn left() -> Result<Int, String>
1255    Result.Ok(1)
1256
1257fn right() -> Result<Int, String>
1258    Result.Ok(2)
1259
1260fn main() -> Result<(Int, Int), String>
1261    data = (left(), right())?!
1262    Result.Ok(data)
1263"#,
1264            "demo",
1265        );
1266        ctx.emit_replay_runtime = true;
1267        ctx.policy = Some(crate::config::ProjectConfig {
1268            effect_policies: std::collections::HashMap::new(),
1269            check_suppressions: Vec::new(),
1270            independence_mode: crate::config::IndependenceMode::Cancel,
1271        });
1272
1273        let out = transpile(&ctx);
1274        let entry = generated_rust_entry_file(&out);
1275        let runtime_support = generated_file(&out, "src/runtime_support.rs");
1276        let replay_support = generated_file(&out, "src/replay_support.rs");
1277
1278        assert!(entry.contains("crate::run_cancelable_branch"));
1279        assert!(entry.contains("capture_parallel_scope_context"));
1280        assert!(entry.contains("_s.spawn(move ||"));
1281        assert!(runtime_support.contains("pub fn run_cancelable_branch"));
1282        assert!(runtime_support.contains("panic_any(AverCancelled)"));
1283        assert!(replay_support.contains("pub fn capture_parallel_scope_context"));
1284        assert!(replay_support.contains("pub fn independence_mode_is_cancel()"));
1285    }
1286
1287    #[test]
1288    fn runtime_policy_codegen_parses_independence_mode() {
1289        let mut ctx = ctx_from_source(
1290            r#"
1291module Demo
1292
1293fn main() -> Result<String, String>
1294    ! [Disk.readText]
1295    Disk.readText("demo.av")
1296"#,
1297            "demo",
1298        );
1299        ctx.emit_replay_runtime = true;
1300        ctx.runtime_policy_from_env = true;
1301
1302        let out = transpile(&ctx);
1303        let replay_support = generated_file(&out, "src/replay_support.rs");
1304
1305        assert!(replay_support.contains("independence_mode_cancel"));
1306        assert!(replay_support.contains("[independence].mode must be 'complete' or 'cancel'"));
1307    }
1308
1309    #[test]
1310    fn effectful_codegen_inserts_cancel_checkpoint_before_builtin_calls() {
1311        let ctx = ctx_from_source(
1312            r#"
1313module Demo
1314
1315fn main() -> Result<String, String>
1316    ! [Disk.readText]
1317    Disk.readText("demo.av")
1318"#,
1319            "demo",
1320        );
1321
1322        let out = transpile(&ctx);
1323        let entry = generated_rust_entry_file(&out);
1324
1325        assert!(entry.contains("crate::cancel_checkpoint(); (aver_rt::read_text"));
1326    }
1327
1328    #[test]
1329    fn replay_support_matches_group_effects_by_occurrence_and_args() {
1330        let mut ctx = ctx_from_source(
1331            r#"
1332module Demo
1333
1334fn left() -> Result<Int, String>
1335    Result.Ok(1)
1336
1337fn right() -> Result<Int, String>
1338    Result.Ok(2)
1339
1340fn main() -> Result<(Int, Int), String>
1341    data = (left(), right())?!
1342    Result.Ok(data)
1343"#,
1344            "demo",
1345        );
1346        ctx.emit_replay_runtime = true;
1347
1348        let out = transpile(&ctx);
1349        let replay_support = generated_file(&out, "src/replay_support.rs");
1350
1351        assert!(replay_support.contains("candidate.effect_occurrence"));
1352        assert!(replay_support.contains("candidate.args != args"));
1353    }
1354}