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