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