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 syntax;
13mod toplevel;
14mod types;
15
16use std::collections::{BTreeMap, HashSet};
17
18use crate::ast::{FnDef, TopLevel, TypeDef};
19use crate::codegen::common::module_prefix_to_rust_segments;
20use crate::codegen::{CodegenContext, ProjectOutput};
21use crate::types::Type;
22
23#[derive(Default)]
24struct ModuleTreeNode {
25    content: Option<String>,
26    children: BTreeMap<String, ModuleTreeNode>,
27}
28
29/// Transpile an Aver program to a Rust project.
30pub fn transpile(ctx: &CodegenContext) -> ProjectOutput {
31    let has_embedded_policy = ctx.policy.is_some();
32    let has_runtime_policy = ctx.runtime_policy_from_env;
33    let used_services = detect_used_services(ctx);
34    let needs_http_types = needs_named_type(ctx, "Header")
35        || needs_named_type(ctx, "HttpResponse")
36        || needs_named_type(ctx, "HttpRequest");
37    let needs_tcp_types = needs_named_type(ctx, "Tcp.Connection");
38    let needs_terminal_types = needs_named_type(ctx, "Terminal.Size");
39
40    let has_tcp_runtime = used_services.contains("Tcp");
41    let has_http_runtime = used_services.contains("Http");
42    let has_http_server_runtime = used_services.contains("HttpServer");
43    let has_terminal_runtime = used_services.contains("Terminal");
44
45    let has_tcp_types = has_tcp_runtime || needs_tcp_types;
46    let has_http_types = has_http_runtime || has_http_server_runtime || needs_http_types;
47    let has_http_server_types = has_http_server_runtime || needs_named_type(ctx, "HttpRequest");
48    let has_terminal_types = has_terminal_runtime || needs_terminal_types;
49
50    let main_fn = ctx.fn_defs.iter().find(|fd| fd.name == "main");
51    let top_level_stmts: Vec<_> = ctx
52        .items
53        .iter()
54        .filter_map(|item| {
55            if let TopLevel::Stmt(stmt) = item {
56                Some(stmt)
57            } else {
58                None
59            }
60        })
61        .collect();
62    let verify_blocks: Vec<_> = ctx
63        .items
64        .iter()
65        .filter_map(|item| {
66            if let TopLevel::Verify(vb) = item {
67                Some(vb)
68            } else {
69                None
70            }
71        })
72        .collect();
73
74    let mut files = vec![
75        (
76            "Cargo.toml".to_string(),
77            project::generate_cargo_toml(
78                &ctx.project_name,
79                &used_services,
80                has_embedded_policy,
81                has_runtime_policy,
82                ctx.emit_replay_runtime,
83                &std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("aver-rt"),
84            ),
85        ),
86        (
87            "src/main.rs".to_string(),
88            render_root_main(
89                main_fn,
90                has_embedded_policy,
91                ctx.emit_replay_runtime,
92                ctx.guest_entry.as_deref(),
93                !verify_blocks.is_empty(),
94            ),
95        ),
96        (
97            "src/runtime_support.rs".to_string(),
98            render_runtime_support(
99                has_tcp_types,
100                has_http_types,
101                has_http_server_types,
102                ctx.emit_replay_runtime,
103                ctx.emit_self_host_runtime,
104            ),
105        ),
106    ];
107
108    if has_embedded_policy && let Some(config) = &ctx.policy {
109        files.push((
110            "src/policy_support.rs".to_string(),
111            format!("{}\n", policy::generate_policy_runtime(config)),
112        ));
113    }
114
115    if ctx.emit_replay_runtime {
116        files.push((
117            "src/replay_support.rs".to_string(),
118            replay::generate_replay_runtime(
119                has_embedded_policy,
120                has_runtime_policy,
121                has_terminal_types,
122                has_tcp_types,
123                has_http_types,
124                has_http_server_types,
125            ),
126        ));
127    }
128
129    if !verify_blocks.is_empty() {
130        files.push((
131            "src/verify.rs".to_string(),
132            render_verify_module(&verify_blocks, ctx),
133        ));
134    }
135
136    let mut module_tree = ModuleTreeNode::default();
137    insert_module_content(
138        &mut module_tree,
139        &[String::from("entry")],
140        render_generated_module(
141            root_module_depends(&ctx.items),
142            entry_module_sections(ctx, main_fn, &top_level_stmts),
143        ),
144    );
145
146    for module in &ctx.modules {
147        let path = module_prefix_to_rust_segments(&module.prefix);
148        insert_module_content(
149            &mut module_tree,
150            &path,
151            render_generated_module(module.depends.clone(), module_sections(module, ctx)),
152        );
153    }
154
155    emit_module_tree_files(&module_tree, "src/aver_generated", &mut files);
156    files.sort_by(|left, right| left.0.cmp(&right.0));
157
158    ProjectOutput { files }
159}
160
161fn render_root_main(
162    main_fn: Option<&FnDef>,
163    has_policy: bool,
164    has_replay: bool,
165    guest_entry: Option<&str>,
166    has_verify: bool,
167) -> String {
168    let mut sections = vec![
169        "#![allow(unused_variables, unused_mut, dead_code, unused_imports, unused_parens, non_snake_case, non_camel_case_types, unreachable_patterns)]".to_string(),
170        "#[macro_use] extern crate aver_rt;".to_string(),
171        "pub use ::aver_rt::AverMap as HashMap;".to_string(),
172        "pub use ::aver_rt::AverStr;".to_string(),
173        String::new(),
174        "mod runtime_support;".to_string(),
175        "pub use runtime_support::*;".to_string(),
176    ];
177
178    if has_policy {
179        sections.push(String::new());
180        sections.push("mod policy_support;".to_string());
181        sections.push("pub use policy_support::*;".to_string());
182    }
183
184    if has_replay {
185        sections.push(String::new());
186        sections.push("mod replay_support;".to_string());
187        sections.push("pub use replay_support::*;".to_string());
188    }
189
190    sections.push(String::new());
191    sections.push("pub mod aver_generated;".to_string());
192
193    if has_verify {
194        sections.push(String::new());
195        sections.push("#[cfg(test)]".to_string());
196        sections.push("mod verify;".to_string());
197    }
198
199    // Spawn main on a thread with 256 MB stack to avoid overflow in deep recursion.
200    sections.push(String::new());
201    let returns_result = main_fn.is_some_and(|fd| fd.return_type.starts_with("Result<"));
202    let result_unit_string =
203        main_fn.is_some_and(|fd| fd.return_type.replace(' ', "") == "Result<Unit,String>");
204    if returns_result {
205        if result_unit_string {
206            sections.push("fn main() {".to_string());
207            sections.push("    let child = std::thread::Builder::new()".to_string());
208            sections.push("        .stack_size(256 * 1024 * 1024)".to_string());
209            if has_replay && guest_entry.is_none() {
210                sections.push("        .spawn(|| {".to_string());
211                sections.push("            let __result = aver_replay::with_guest_scope(\"main\", serde_json::Value::Null, aver_generated::entry::main);".to_string());
212                sections.push("            __result.map_err(|e| e.to_string())".to_string());
213                sections.push("        })".to_string());
214            } else {
215                sections.push("        .spawn(|| {".to_string());
216                sections
217                    .push("            let __result = aver_generated::entry::main();".to_string());
218                sections.push("            __result.map_err(|e| e.to_string())".to_string());
219                sections.push("        })".to_string());
220            }
221            sections.push("        .expect(\"thread spawn\");".to_string());
222            sections.push("    match child.join().expect(\"thread join\") {".to_string());
223            sections.push("        Ok(()) => {}".to_string());
224            sections.push("        Err(e) => {".to_string());
225            sections.push("            eprintln!(\"{}\", e);".to_string());
226            sections.push("            std::process::exit(1);".to_string());
227            sections.push("        }".to_string());
228            sections.push("    }".to_string());
229        } else {
230            let ret_type = types::type_annotation_to_rust(&main_fn.unwrap().return_type);
231            sections.push(format!("fn main() -> {} {{", ret_type));
232            if has_replay && guest_entry.is_none() {
233                sections.push(
234                    "    aver_replay::with_guest_scope(\"main\", serde_json::Value::Null, aver_generated::entry::main)"
235                        .to_string(),
236                );
237            } else {
238                sections.push("    aver_generated::entry::main()".to_string());
239            }
240        }
241    } else {
242        sections.push("fn main() {".to_string());
243        if main_fn.is_some() {
244            sections.push("    let child = std::thread::Builder::new()".to_string());
245            sections.push("        .stack_size(256 * 1024 * 1024)".to_string());
246            if has_replay && guest_entry.is_none() {
247                sections.push("        .spawn(|| aver_replay::with_guest_scope(\"main\", serde_json::Value::Null, || aver_generated::entry::main()))".to_string());
248            } else {
249                sections.push("        .spawn(|| aver_generated::entry::main())".to_string());
250            }
251            sections.push("        .expect(\"thread spawn\");".to_string());
252            sections.push("    child.join().expect(\"thread join\");".to_string());
253        }
254    }
255    sections.push("}".to_string());
256    sections.push(String::new());
257
258    sections.join("\n")
259}
260
261fn render_runtime_support(
262    has_tcp_types: bool,
263    has_http_types: bool,
264    has_http_server_types: bool,
265    has_replay: bool,
266    emit_self_host_runtime: bool,
267) -> String {
268    let mut sections = vec![runtime::generate_runtime(
269        has_replay,
270        has_http_server_types,
271        emit_self_host_runtime,
272    )];
273    if has_tcp_types {
274        sections.push(runtime::generate_tcp_types());
275    }
276    if has_http_types {
277        sections.push(runtime::generate_http_types());
278    }
279    if has_http_server_types {
280        sections.push(runtime::generate_http_server_types());
281    }
282    format!("{}\n", sections.join("\n\n"))
283}
284
285fn render_verify_module(
286    verify_blocks: &[&crate::ast::VerifyBlock],
287    ctx: &CodegenContext,
288) -> String {
289    [
290        "#[allow(unused_imports)]".to_string(),
291        "use crate::*;".to_string(),
292        "#[allow(unused_imports)]".to_string(),
293        "use crate::aver_generated::entry::*;".to_string(),
294        String::new(),
295        toplevel::emit_verify_blocks(verify_blocks, ctx),
296        String::new(),
297    ]
298    .join("\n")
299}
300
301fn render_generated_module(depends: Vec<String>, sections: Vec<String>) -> String {
302    if sections.is_empty() {
303        String::new()
304    } else {
305        let mut lines = vec![
306            "#[allow(unused_imports)]".to_string(),
307            "use crate::*;".to_string(),
308        ];
309        for dep in depends {
310            let path = module_prefix_to_rust_segments(&dep).join("::");
311            lines.push("#[allow(unused_imports)]".to_string());
312            lines.push(format!("use crate::aver_generated::{}::*;", path));
313        }
314        lines.push(String::new());
315        lines.push(sections.join("\n\n"));
316        lines.push(String::new());
317        lines.join("\n")
318    }
319}
320
321fn entry_module_sections(
322    ctx: &CodegenContext,
323    main_fn: Option<&FnDef>,
324    top_level_stmts: &[&crate::ast::Stmt],
325) -> Vec<String> {
326    let mut sections = Vec::new();
327
328    for td in &ctx.type_defs {
329        if is_shared_runtime_type(td) {
330            continue;
331        }
332        sections.push(toplevel::emit_public_type_def(td, ctx));
333        if ctx.emit_replay_runtime {
334            sections.push(replay::emit_replay_value_impl(td));
335        }
336    }
337
338    // Detect mutual TCO groups among non-main functions.
339    let non_main_fns: Vec<&FnDef> = ctx.fn_defs.iter().filter(|fd| fd.name != "main").collect();
340    let mutual_groups = toplevel::find_mutual_tco_groups(&non_main_fns);
341    let mut mutual_tco_members: HashSet<String> = HashSet::new();
342
343    for (group_id, group_indices) in mutual_groups.iter().enumerate() {
344        let group_fns: Vec<&FnDef> = group_indices.iter().map(|&idx| non_main_fns[idx]).collect();
345        for fd in &group_fns {
346            mutual_tco_members.insert(fd.name.clone());
347        }
348        sections.push(toplevel::emit_mutual_tco_block(
349            group_id + 1,
350            &group_fns,
351            ctx,
352            "pub ",
353        ));
354    }
355
356    for fd in &ctx.fn_defs {
357        if fd.name == "main" || mutual_tco_members.contains(&fd.name) {
358            continue;
359        }
360        let is_memo = ctx.memo_fns.contains(&fd.name);
361        sections.push(toplevel::emit_public_fn_def(fd, is_memo, ctx));
362    }
363
364    if main_fn.is_some() || !top_level_stmts.is_empty() {
365        sections.push(toplevel::emit_public_main(main_fn, top_level_stmts, ctx));
366    }
367
368    sections
369}
370
371fn module_sections(module: &crate::codegen::ModuleInfo, ctx: &CodegenContext) -> Vec<String> {
372    let mut sections = Vec::new();
373
374    for td in &module.type_defs {
375        if is_shared_runtime_type(td) {
376            continue;
377        }
378        sections.push(toplevel::emit_public_type_def(td, ctx));
379        if ctx.emit_replay_runtime {
380            sections.push(replay::emit_replay_value_impl(td));
381        }
382    }
383
384    // Detect mutual TCO groups among module functions.
385    let fn_refs: Vec<&FnDef> = module.fn_defs.iter().collect();
386    let mutual_groups = toplevel::find_mutual_tco_groups(&fn_refs);
387    let mut mutual_tco_members: HashSet<String> = HashSet::new();
388
389    for (group_id, group_indices) in mutual_groups.iter().enumerate() {
390        let group_fns: Vec<&FnDef> = group_indices.iter().map(|&idx| fn_refs[idx]).collect();
391        for fd in &group_fns {
392            mutual_tco_members.insert(fd.name.clone());
393        }
394        sections.push(toplevel::emit_mutual_tco_block(
395            group_id + 1,
396            &group_fns,
397            ctx,
398            "pub ",
399        ));
400    }
401
402    for fd in &module.fn_defs {
403        if mutual_tco_members.contains(&fd.name) {
404            continue;
405        }
406        let is_memo = ctx.memo_fns.contains(&fd.name);
407        sections.push(toplevel::emit_public_fn_def(fd, is_memo, ctx));
408    }
409
410    sections
411}
412
413fn insert_module_content(node: &mut ModuleTreeNode, segments: &[String], content: String) {
414    let child = node.children.entry(segments[0].clone()).or_default();
415    if segments.len() == 1 {
416        child.content = Some(content);
417    } else {
418        insert_module_content(child, &segments[1..], content);
419    }
420}
421
422fn emit_module_tree_files(node: &ModuleTreeNode, rel_dir: &str, files: &mut Vec<(String, String)>) {
423    let mut parts = Vec::new();
424
425    if let Some(content) = &node.content
426        && !content.trim().is_empty()
427    {
428        parts.push(content.trim_end().to_string());
429    }
430
431    for child_name in node.children.keys() {
432        parts.push(format!("pub mod {};", child_name));
433    }
434
435    let mut mod_rs = parts.join("\n\n");
436    if !mod_rs.is_empty() {
437        mod_rs.push('\n');
438    }
439    files.push((format!("{}/mod.rs", rel_dir), mod_rs));
440
441    for (child_name, child) in &node.children {
442        emit_module_tree_files(child, &format!("{}/{}", rel_dir, child_name), files);
443    }
444}
445
446fn root_module_depends(items: &[TopLevel]) -> Vec<String> {
447    items
448        .iter()
449        .find_map(|item| {
450            if let TopLevel::Module(module) = item {
451                Some(module.depends.clone())
452            } else {
453                None
454            }
455        })
456        .unwrap_or_default()
457}
458
459/// Detect which effectful services are used in the program (including modules).
460fn detect_used_services(ctx: &CodegenContext) -> HashSet<String> {
461    let mut services = HashSet::new();
462    for item in &ctx.items {
463        if let TopLevel::FnDef(fd) = item {
464            for eff in &fd.effects {
465                services.insert(eff.clone());
466                if let Some((service, _)) = eff.split_once('.') {
467                    services.insert(service.to_string());
468                }
469            }
470        }
471    }
472    for module in &ctx.modules {
473        for fd in &module.fn_defs {
474            for eff in &fd.effects {
475                services.insert(eff.clone());
476                if let Some((service, _)) = eff.split_once('.') {
477                    services.insert(service.to_string());
478                }
479            }
480        }
481    }
482    services
483}
484
485fn is_shared_runtime_type(td: &TypeDef) -> bool {
486    matches!(
487        td,
488        TypeDef::Product { name, .. }
489            if matches!(name.as_str(), "Header" | "HttpResponse" | "HttpRequest")
490    )
491}
492
493fn needs_named_type(ctx: &CodegenContext, wanted: &str) -> bool {
494    ctx.fn_sigs.values().any(|(params, ret, _effects)| {
495        params.iter().any(|p| type_contains_named(p, wanted)) || type_contains_named(ret, wanted)
496    })
497}
498
499fn type_contains_named(ty: &Type, wanted: &str) -> bool {
500    match ty {
501        Type::Named(name) => name == wanted,
502        Type::Result(ok, err) => {
503            type_contains_named(ok, wanted) || type_contains_named(err, wanted)
504        }
505        Type::Option(inner) | Type::List(inner) | Type::Vector(inner) => {
506            type_contains_named(inner, wanted)
507        }
508        Type::Tuple(items) => items.iter().any(|t| type_contains_named(t, wanted)),
509        Type::Map(k, v) => type_contains_named(k, wanted) || type_contains_named(v, wanted),
510        Type::Fn(params, ret, _effects) => {
511            params.iter().any(|t| type_contains_named(t, wanted))
512                || type_contains_named(ret, wanted)
513        }
514        Type::Int | Type::Float | Type::Str | Type::Bool | Type::Unit | Type::Unknown => false,
515    }
516}
517
518#[cfg(test)]
519mod tests {
520    use super::{
521        ModuleTreeNode, emit_module_tree_files, insert_module_content, render_generated_module,
522        transpile,
523    };
524    use crate::codegen::build_context;
525    use crate::source::parse_source;
526    use crate::tco;
527    use crate::types::checker::run_type_check_full;
528    use std::collections::HashSet;
529
530    fn ctx_from_source(source: &str, project_name: &str) -> crate::codegen::CodegenContext {
531        let mut items = parse_source(source).expect("source should parse");
532        tco::transform_program(&mut items);
533        let tc = run_type_check_full(&items, None);
534        assert!(
535            tc.errors.is_empty(),
536            "source should typecheck without errors: {:?}",
537            tc.errors
538        );
539        build_context(items, &tc, HashSet::new(), project_name.to_string(), vec![])
540    }
541
542    fn generated_rust_entry_file(out: &crate::codegen::ProjectOutput) -> &str {
543        out.files
544            .iter()
545            .find_map(|(name, content)| {
546                (name == "src/aver_generated/entry/mod.rs").then_some(content.as_str())
547            })
548            .expect("expected generated Rust entry module")
549    }
550
551    fn generated_file<'a>(out: &'a crate::codegen::ProjectOutput, path: &str) -> &'a str {
552        out.files
553            .iter()
554            .find_map(|(name, content)| (name == path).then_some(content.as_str()))
555            .unwrap_or_else(|| panic!("expected generated file '{}'", path))
556    }
557
558    #[test]
559    fn generated_module_imports_direct_depends() {
560        let rendered = render_generated_module(
561            vec!["Domain.Types".to_string(), "App.Commands".to_string()],
562            vec!["pub fn demo() {}".to_string()],
563        );
564
565        assert!(rendered.contains("use crate::aver_generated::domain::types::*;"));
566        assert!(rendered.contains("use crate::aver_generated::app::commands::*;"));
567        assert!(rendered.contains("pub fn demo() {}"));
568    }
569
570    #[test]
571    fn module_tree_files_do_not_reexport_children() {
572        let mut tree = ModuleTreeNode::default();
573        insert_module_content(
574            &mut tree,
575            &["app".to_string(), "cli".to_string()],
576            "pub fn run() {}".to_string(),
577        );
578
579        let mut files = Vec::new();
580        emit_module_tree_files(&tree, "src/aver_generated", &mut files);
581
582        let root_mod = files
583            .iter()
584            .find(|(path, _)| path == "src/aver_generated/mod.rs")
585            .map(|(_, content)| content)
586            .expect("root mod.rs should exist");
587
588        assert!(root_mod.contains("pub mod app;"));
589        assert!(!root_mod.contains("pub use app::*;"));
590    }
591
592    #[test]
593    fn list_cons_match_uses_cloned_uncons_fast_path() {
594        let ctx = ctx_from_source(
595            r#"
596module Demo
597
598fn headPlusTailLen(xs: List<Int>) -> Int
599    match xs
600        [] -> 0
601        [h, ..t] -> h + List.len(t)
602"#,
603            "demo",
604        );
605
606        let out = transpile(&ctx);
607        let entry = generated_rust_entry_file(&out);
608
609        // The common []/[h,..t] pattern uses aver_list_match! macro
610        assert!(entry.contains("aver_list_match!"));
611    }
612
613    #[test]
614    fn list_literal_clones_ident_when_used_afterward() {
615        let ctx = ctx_from_source(
616            r#"
617module Demo
618
619record Audit
620    message: String
621
622fn useTwice(audit: Audit) -> List<Audit>
623    first = [audit]
624    [audit]
625"#,
626            "demo",
627        );
628
629        let out = transpile(&ctx);
630        let entry = generated_rust_entry_file(&out);
631
632        assert!(entry.contains("let first = aver_rt::AverList::from_vec(vec![audit.clone()]);"));
633        assert!(entry.contains("aver_rt::AverList::from_vec(vec![audit])"));
634    }
635
636    #[test]
637    fn record_update_clones_base_when_value_is_used_afterward() {
638        let ctx = ctx_from_source(
639            r#"
640module Demo
641
642record PaymentState
643    paymentId: String
644    currency: String
645
646fn touch(state: PaymentState) -> String
647    updated = PaymentState.update(state, currency = "EUR")
648    state.paymentId
649"#,
650            "demo",
651        );
652
653        let out = transpile(&ctx);
654        let entry = generated_rust_entry_file(&out);
655
656        assert!(entry.contains("..state.clone()"));
657    }
658
659    #[test]
660    fn mutual_tco_generates_trampoline_instead_of_regular_calls() {
661        let ctx = ctx_from_source(
662            r#"
663module Demo
664
665fn isEven(n: Int) -> Bool
666    match n == 0
667        true -> true
668        false -> isOdd(n - 1)
669
670fn isOdd(n: Int) -> Bool
671    match n == 0
672        true -> false
673        false -> isEven(n - 1)
674"#,
675            "demo",
676        );
677
678        let out = transpile(&ctx);
679        let entry = generated_rust_entry_file(&out);
680
681        // Should generate trampoline enum and dispatch
682        assert!(entry.contains("enum __MutualTco1"));
683        assert!(entry.contains("fn __mutual_tco_trampoline_1"));
684        assert!(entry.contains("loop {"));
685
686        // Wrapper functions delegate to trampoline
687        assert!(entry.contains("pub fn isEven"));
688        assert!(entry.contains("pub fn isOdd"));
689        assert!(entry.contains("__mutual_tco_trampoline_1("));
690
691        // Should NOT contain direct recursive calls between the two
692        assert!(!entry.contains("isOdd((n - 1i64))"));
693    }
694
695    #[test]
696    fn field_access_does_not_double_clone() {
697        let ctx = ctx_from_source(
698            r#"
699module Demo
700
701record User
702    name: String
703    age: Int
704
705fn greet(u: User) -> String
706    u.name
707"#,
708            "demo",
709        );
710
711        let out = transpile(&ctx);
712        let entry = generated_rust_entry_file(&out);
713
714        // Field access should produce exactly one .clone(), never .clone().clone()
715        assert!(
716            !entry.contains(".clone().clone()"),
717            "double clone detected in generated code:\n{}",
718            entry
719        );
720    }
721
722    #[test]
723    fn single_field_variant_display_avoids_vec_join() {
724        let ctx = ctx_from_source(
725            r#"
726module Demo
727
728type Wrapper
729    Wrap(Int)
730    Pair(Int, Int)
731    Empty
732"#,
733            "demo",
734        );
735
736        let out = transpile(&ctx);
737        let entry = generated_rust_entry_file(&out);
738
739        // Single-field variant Wrap(Int): should NOT use vec![].join()
740        assert!(
741            !entry.contains("vec![f0.aver_display_inner()].join"),
742            "single-field variant should use direct format, not vec join:\n{}",
743            entry
744        );
745        // Multi-field variant Pair(Int, Int): SHOULD still use vec![].join()
746        assert!(
747            entry.contains("vec![f0.aver_display_inner(), f1.aver_display_inner()].join(\", \")"),
748            "multi-field variant should use vec join:\n{}",
749            entry
750        );
751    }
752
753    #[test]
754    fn replay_codegen_wraps_guest_entry_in_scoped_runtime() {
755        let mut ctx = ctx_from_source(
756            r#"
757module Demo
758
759fn runGuestProgram(path: String) -> Result<String, String>
760    ! [Disk.readText]
761    Disk.readText(path)
762"#,
763            "demo",
764        );
765        ctx.emit_replay_runtime = true;
766        ctx.guest_entry = Some("runGuestProgram".to_string());
767
768        let out = transpile(&ctx);
769        let entry = generated_rust_entry_file(&out);
770        let replay_support = generated_file(&out, "src/replay_support.rs");
771        let cargo_toml = generated_file(&out, "Cargo.toml");
772
773        assert!(entry.contains("aver_replay::with_guest_scope_result(\"runGuestProgram\""));
774        assert!(replay_support.contains("pub mod aver_replay"));
775        assert!(cargo_toml.contains("serde_json = \"1\""));
776    }
777
778    #[test]
779    fn replay_codegen_uses_guest_args_param_override_when_present() {
780        let mut ctx = ctx_from_source(
781            r#"
782module Demo
783
784fn runGuestProgram(path: String, guestArgs: List<String>) -> Result<String, String>
785    ! [Args.get]
786    Result.Ok(String.join(Args.get(), ","))
787"#,
788            "demo",
789        );
790        ctx.emit_replay_runtime = true;
791        ctx.guest_entry = Some("runGuestProgram".to_string());
792
793        let out = transpile(&ctx);
794        let entry = generated_rust_entry_file(&out);
795        let cargo_toml = generated_file(&out, "Cargo.toml");
796
797        assert!(entry.contains("aver_replay::with_guest_scope_args_result(\"runGuestProgram\""));
798        assert!(entry.contains("guestArgs.clone()"));
799        assert!(cargo_toml.contains("edition = \"2024\""));
800    }
801
802    #[test]
803    fn replay_codegen_wraps_root_main_when_no_guest_entry_is_set() {
804        let mut ctx = ctx_from_source(
805            r#"
806module Demo
807
808fn main() -> Result<String, String>
809    ! [Disk.readText]
810    Disk.readText("demo.av")
811"#,
812            "demo",
813        );
814        ctx.emit_replay_runtime = true;
815
816        let out = transpile(&ctx);
817        let root_main = generated_file(&out, "src/main.rs");
818
819        assert!(
820            root_main.contains("aver_replay::with_guest_scope(\"main\", serde_json::Value::Null")
821        );
822    }
823
824    #[test]
825    fn runtime_policy_codegen_uses_runtime_loader() {
826        let mut ctx = ctx_from_source(
827            r#"
828module Demo
829
830fn main() -> Result<String, String>
831    ! [Disk.readText]
832    Disk.readText("demo.av")
833"#,
834            "demo",
835        );
836        ctx.emit_replay_runtime = true;
837        ctx.runtime_policy_from_env = true;
838
839        let out = transpile(&ctx);
840        let root_main = generated_file(&out, "src/main.rs");
841        let replay_support = generated_file(&out, "src/replay_support.rs");
842        let cargo_toml = generated_file(&out, "Cargo.toml");
843
844        assert!(!root_main.contains("mod policy_support;"));
845        assert!(replay_support.contains("load_runtime_policy_from_env"));
846        assert!(cargo_toml.contains("url = \"2\""));
847        assert!(cargo_toml.contains("toml = \"0.8\""));
848    }
849
850    #[test]
851    fn replay_codegen_can_keep_embedded_policy_when_requested() {
852        let mut ctx = ctx_from_source(
853            r#"
854module Demo
855
856fn main() -> Result<String, String>
857    ! [Disk.readText]
858    Disk.readText("demo.av")
859"#,
860            "demo",
861        );
862        ctx.emit_replay_runtime = true;
863        ctx.policy = Some(crate::config::ProjectConfig {
864            effect_policies: std::collections::HashMap::new(),
865        });
866
867        let out = transpile(&ctx);
868        let root_main = generated_file(&out, "src/main.rs");
869        let replay_support = generated_file(&out, "src/replay_support.rs");
870
871        assert!(root_main.contains("mod policy_support;"));
872        assert!(replay_support.contains("aver_policy::check_disk"));
873        assert!(!replay_support.contains("RuntimeEffectPolicy"));
874    }
875}