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        "pub use ::aver_rt::Buffer;".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. Set-form membership
406    // is read from `ctx.mutual_tco_members` (populated by the analyze stage's
407    // unioned per-module sets); the index-keyed `groups` form still comes
408    // from `find_mutual_tco_groups` because trampoline emission needs the
409    // structural shape, not just the names.
410    let non_main_fns: Vec<&FnDef> = ctx.fn_defs.iter().filter(|fd| fd.name != "main").collect();
411    let mutual_groups = toplevel::find_mutual_tco_groups(&non_main_fns);
412
413    for (group_id, group_indices) in mutual_groups.iter().enumerate() {
414        let group_fns: Vec<&FnDef> = group_indices.iter().map(|&idx| non_main_fns[idx]).collect();
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" || ctx.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    // Same shape as the entry path: groups (with indices) come from
452    // `find_mutual_tco_groups`, set-form membership reads from
453    // `module.analysis.mutual_tco_members` when the analyze stage ran.
454    let fn_refs: Vec<&FnDef> = module.fn_defs.iter().collect();
455    let mutual_groups = toplevel::find_mutual_tco_groups(&fn_refs);
456    let module_mutual: &HashSet<String> = match module.analysis.as_ref() {
457        Some(a) => &a.mutual_tco_members,
458        None => &ctx.mutual_tco_members,
459    };
460
461    for (group_id, group_indices) in mutual_groups.iter().enumerate() {
462        let group_fns: Vec<&FnDef> = group_indices.iter().map(|&idx| fn_refs[idx]).collect();
463        sections.push(toplevel::emit_mutual_tco_block(
464            group_id + 1,
465            &group_fns,
466            ctx,
467            "pub ",
468        ));
469    }
470
471    for fd in &module.fn_defs {
472        if module_mutual.contains(&fd.name) {
473            continue;
474        }
475        let is_memo = ctx.memo_fns.contains(&fd.name);
476        sections.push(toplevel::emit_public_fn_def(fd, is_memo, ctx));
477    }
478
479    sections
480}
481
482fn root_module_depends(items: &[TopLevel]) -> Vec<String> {
483    items
484        .iter()
485        .find_map(|item| {
486            if let TopLevel::Module(module) = item {
487                Some(module.depends.clone())
488            } else {
489                None
490            }
491        })
492        .unwrap_or_default()
493}
494
495/// Detect which effectful services are used in the program (including modules).
496fn detect_used_services(ctx: &CodegenContext) -> HashSet<String> {
497    let mut services = HashSet::new();
498    for item in &ctx.items {
499        if let TopLevel::FnDef(fd) = item {
500            for eff in &fd.effects {
501                services.insert(eff.node.clone());
502                if let Some((service, _)) = eff.node.split_once('.') {
503                    services.insert(service.to_string());
504                }
505            }
506        }
507    }
508    for module in &ctx.modules {
509        for fd in &module.fn_defs {
510            for eff in &fd.effects {
511                services.insert(eff.node.clone());
512                if let Some((service, _)) = eff.node.split_once('.') {
513                    services.insert(service.to_string());
514                }
515            }
516        }
517    }
518    services
519}
520
521fn is_shared_runtime_type(td: &TypeDef) -> bool {
522    matches!(
523        td,
524        TypeDef::Product { name, .. }
525            if matches!(name.as_str(), "HttpResponse" | "HttpRequest")
526    )
527}
528
529fn needs_named_type(ctx: &CodegenContext, wanted: &str) -> bool {
530    ctx.fn_sigs.values().any(|(params, ret, _effects)| {
531        params.iter().any(|p| type_contains_named(p, wanted)) || type_contains_named(ret, wanted)
532    })
533}
534
535fn type_contains_named(ty: &Type, wanted: &str) -> bool {
536    match ty {
537        Type::Named(name) => name == wanted,
538        Type::Result(ok, err) => {
539            type_contains_named(ok, wanted) || type_contains_named(err, wanted)
540        }
541        Type::Option(inner) | Type::List(inner) | Type::Vector(inner) => {
542            type_contains_named(inner, wanted)
543        }
544        Type::Tuple(items) => items.iter().any(|t| type_contains_named(t, wanted)),
545        Type::Map(k, v) => type_contains_named(k, wanted) || type_contains_named(v, wanted),
546        Type::Fn(params, ret, _effects) => {
547            params.iter().any(|t| type_contains_named(t, wanted))
548                || type_contains_named(ret, wanted)
549        }
550        Type::Int
551        | Type::Float
552        | Type::Str
553        | Type::Bool
554        | Type::Unit
555        | Type::Invalid
556        | Type::Var(_) => false,
557    }
558}
559
560#[cfg(test)]
561mod tests {
562    use super::{render_generated_module, synthesize_rust_module_cascade, transpile};
563    use crate::codegen::build_context;
564    use crate::source::parse_source;
565    use crate::tco;
566    use std::collections::HashSet;
567
568    fn ctx_from_source(source: &str, project_name: &str) -> crate::codegen::CodegenContext {
569        let mut items = parse_source(source).expect("source should parse");
570        crate::ir::pipeline::tco(&mut items);
571        let tc = crate::ir::pipeline::typecheck(
572            &items,
573            &crate::ir::TypecheckMode::Full { base_dir: None },
574        );
575        assert!(
576            tc.errors.is_empty(),
577            "source should typecheck without errors: {:?}",
578            tc.errors
579        );
580        build_context(
581            items,
582            &tc,
583            None,
584            HashSet::new(),
585            project_name.to_string(),
586            vec![],
587        )
588    }
589
590    fn generated_rust_entry_file(out: &crate::codegen::ProjectOutput) -> &str {
591        out.files
592            .iter()
593            .find_map(|(name, content)| {
594                (name == "src/aver_generated/entry/mod.rs").then_some(content.as_str())
595            })
596            .expect("expected generated Rust entry module")
597    }
598
599    fn generated_file<'a>(out: &'a crate::codegen::ProjectOutput, path: &str) -> &'a str {
600        out.files
601            .iter()
602            .find_map(|(name, content)| (name == path).then_some(content.as_str()))
603            .unwrap_or_else(|| panic!("expected generated file '{}'", path))
604    }
605
606    #[test]
607    fn emission_banner_appears_in_root_main() {
608        let mut ctx = ctx_from_source(
609            r#"
610module Demo
611
612fn main() -> Int
613    1
614"#,
615            "demo",
616        );
617
618        let out = transpile(&mut ctx);
619        let root_main = generated_file(&out, "src/main.rs");
620
621        assert!(root_main.contains("// Aver Rust emission"));
622    }
623
624    #[test]
625    fn generated_module_imports_direct_depends() {
626        let rendered = render_generated_module(
627            vec!["Domain.Types".to_string(), "App.Commands".to_string()],
628            vec!["pub fn demo() {}".to_string()],
629        );
630
631        assert!(rendered.contains("use crate::aver_generated::domain::types::*;"));
632        assert!(rendered.contains("use crate::aver_generated::app::commands::*;"));
633        assert!(rendered.contains("pub fn demo() {}"));
634    }
635
636    #[test]
637    fn module_tree_files_do_not_reexport_children() {
638        let modules = vec![(
639            vec!["app".to_string(), "cli".to_string()],
640            "pub fn run() {}".to_string(),
641        )];
642        let files = synthesize_rust_module_cascade("src/aver_generated", &modules);
643
644        let root_mod = files
645            .iter()
646            .find(|(path, _)| path == "src/aver_generated/mod.rs")
647            .map(|(_, content)| content)
648            .expect("root mod.rs should exist");
649
650        assert!(root_mod.contains("pub mod app;"));
651        assert!(!root_mod.contains("pub use app::*;"));
652    }
653
654    #[test]
655    fn list_cons_match_uses_cloned_uncons_fast_path_when_optimized() {
656        let mut ctx = ctx_from_source(
657            r#"
658module Demo
659
660fn headPlusTailLen(xs: List<Int>) -> Int
661    match xs
662        [] -> 0
663        [h, ..t] -> h + List.len(t)
664"#,
665            "demo",
666        );
667
668        let out = transpile(&mut ctx);
669        let entry = generated_rust_entry_file(&out);
670
671        // The common []/[h,..t] pattern uses aver_list_match! macro
672        assert!(entry.contains("aver_list_match!"));
673    }
674
675    #[test]
676    fn list_cons_match_stays_structured_in_semantic_mode() {
677        let mut ctx = ctx_from_source(
678            r#"
679module Demo
680
681fn headPlusTailLen(xs: List<Int>) -> Int
682    match xs
683        [] -> 0
684        [h, ..t] -> h + List.len(t)
685"#,
686            "demo",
687        );
688
689        let out = transpile(&mut ctx);
690        let entry = generated_rust_entry_file(&out);
691
692        // Both modes now use the aver_list_match! macro for []/[h,..t] patterns
693        assert!(entry.contains("aver_list_match!"));
694    }
695
696    #[test]
697    fn list_literal_clones_ident_when_used_afterward() {
698        let mut ctx = ctx_from_source(
699            r#"
700module Demo
701
702record Audit
703    message: String
704
705fn useTwice(audit: Audit) -> List<Audit>
706    first = [audit]
707    [audit]
708"#,
709            "demo",
710        );
711
712        let out = transpile(&mut ctx);
713        let entry = generated_rust_entry_file(&out);
714
715        assert!(entry.contains("let first = aver_rt::AverList::from_vec(vec![audit.clone()]);"));
716        // Borrowed param always needs .clone() when consumed
717        assert!(entry.contains("aver_rt::AverList::from_vec(vec![audit.clone()])"));
718    }
719
720    #[test]
721    fn record_update_clones_base_when_value_is_used_afterward() {
722        let mut ctx = ctx_from_source(
723            r#"
724module Demo
725
726record PaymentState
727    paymentId: String
728    currency: String
729
730fn touch(state: PaymentState) -> String
731    updated = PaymentState.update(state, currency = "EUR")
732    state.paymentId
733"#,
734            "demo",
735        );
736
737        let out = transpile(&mut ctx);
738        let entry = generated_rust_entry_file(&out);
739
740        assert!(entry.contains("..state.clone()"));
741    }
742
743    #[test]
744    fn mutual_tco_generates_trampoline_instead_of_regular_calls() {
745        let mut ctx = ctx_from_source(
746            r#"
747module Demo
748
749fn isEven(n: Int) -> Bool
750    match n == 0
751        true -> true
752        false -> isOdd(n - 1)
753
754fn isOdd(n: Int) -> Bool
755    match n == 0
756        true -> false
757        false -> isEven(n - 1)
758"#,
759            "demo",
760        );
761
762        let out = transpile(&mut ctx);
763        let entry = generated_rust_entry_file(&out);
764
765        // Should generate trampoline enum and dispatch
766        assert!(entry.contains("enum __MutualTco1"));
767        assert!(entry.contains("fn __mutual_tco_trampoline_1"));
768        assert!(entry.contains("loop {"));
769
770        // Wrapper functions delegate to trampoline
771        assert!(entry.contains("pub fn isEven"));
772        assert!(entry.contains("pub fn isOdd"));
773        assert!(entry.contains("__mutual_tco_trampoline_1("));
774
775        // Should NOT contain direct recursive calls between the two
776        assert!(!entry.contains("isOdd((n - 1i64))"));
777    }
778
779    #[test]
780    fn field_access_does_not_double_clone() {
781        let mut ctx = ctx_from_source(
782            r#"
783module Demo
784
785record User
786    name: String
787    age: Int
788
789fn greet(u: User) -> String
790    u.name
791"#,
792            "demo",
793        );
794
795        let out = transpile(&mut ctx);
796        let entry = generated_rust_entry_file(&out);
797
798        // Field access should produce exactly one .clone(), never .clone().clone()
799        assert!(
800            !entry.contains(".clone().clone()"),
801            "double clone detected in generated code:\n{}",
802            entry
803        );
804    }
805
806    #[test]
807    fn borrowed_record_field_return_clones_for_owned_result() {
808        let mut ctx = ctx_from_source(
809            r#"
810module Demo
811
812record User
813    name: String
814
815fn getName(user: User) -> String
816    user.name
817"#,
818            "demo",
819        );
820
821        let out = transpile(&mut ctx);
822        let entry = generated_rust_entry_file(&out);
823
824        assert!(entry.contains("pub fn getName(user: &User) -> AverStr"));
825        assert!(
826            entry.contains("user.name.clone()"),
827            "missing owned clone:\n{}",
828            entry
829        );
830    }
831
832    #[test]
833    fn vector_get_with_literal_default_lowers_to_direct_unwrap_or_code() {
834        let mut ctx = ctx_from_source(
835            r#"
836module Demo
837
838fn cellAt(grid: Vector<Int>, idx: Int) -> Int
839    Option.withDefault(Vector.get(grid, idx), 0)
840"#,
841            "demo",
842        );
843
844        let out = transpile(&mut ctx);
845        let entry = generated_rust_entry_file(&out);
846
847        assert!(entry.contains("grid.get(idx as usize).cloned().unwrap_or(0i64)"));
848    }
849
850    #[test]
851    fn vector_set_default_stays_structured_in_semantic_mode() {
852        let mut ctx = ctx_from_source(
853            r#"
854module Demo
855
856fn updateOrKeep(vec: Vector<Int>, idx: Int, value: Int) -> Vector<Int>
857    Option.withDefault(Vector.set(vec, idx, value), vec)
858"#,
859            "demo",
860        );
861
862        let out = transpile(&mut ctx);
863        let entry = generated_rust_entry_file(&out);
864
865        // Both modes now use the inlined set_unchecked fast path
866        assert!(entry.contains("set_unchecked"));
867        assert!(!entry.contains(".unwrap_or("));
868    }
869
870    #[test]
871    fn vector_set_default_uses_ir_leaf_fast_path_when_optimized() {
872        let mut ctx = ctx_from_source(
873            r#"
874module Demo
875
876fn updateOrKeep(vec: Vector<Int>, idx: Int, value: Int) -> Vector<Int>
877    Option.withDefault(Vector.set(vec, idx, value), vec)
878"#,
879            "demo",
880        );
881
882        let out = transpile(&mut ctx);
883        let entry = generated_rust_entry_file(&out);
884
885        assert!(entry.contains("set_unchecked"));
886        assert!(!entry.contains(".unwrap_or("));
887    }
888
889    #[test]
890    fn vector_set_uses_owned_update_lowering() {
891        let mut ctx = ctx_from_source(
892            r#"
893module Demo
894
895fn update(vec: Vector<Int>, idx: Int, value: Int) -> Option<Vector<Int>>
896    Vector.set(vec, idx, value)
897"#,
898            "demo",
899        );
900
901        let out = transpile(&mut ctx);
902        let entry = generated_rust_entry_file(&out);
903
904        assert!(entry.contains(".set_owned("));
905        assert!(!entry.contains(".set(idx as usize,"));
906    }
907
908    #[test]
909    fn map_remove_uses_owned_update_lowering() {
910        let mut ctx = ctx_from_source(
911            r#"
912module Demo
913
914fn dropKey(m: Map<String, Int>, key: String) -> Map<String, Int>
915    Map.remove(m, key)
916"#,
917            "demo",
918        );
919
920        let out = transpile(&mut ctx);
921        let entry = generated_rust_entry_file(&out);
922
923        assert!(entry.contains(".remove_owned(&"));
924    }
925
926    #[test]
927    fn semantic_keeps_known_leaf_wrapper_call_structured() {
928        let mut ctx = ctx_from_source(
929            r#"
930module Demo
931
932fn cellAt(grid: Vector<Int>, idx: Int) -> Int
933    Option.withDefault(Vector.get(grid, idx), 0)
934
935fn read(grid: Vector<Int>, idx: Int) -> Int
936    cellAt(grid, idx)
937"#,
938            "demo",
939        );
940
941        let out = transpile(&mut ctx);
942        let entry = generated_rust_entry_file(&out);
943
944        assert!(entry.contains("cellAt(grid, idx)"));
945        assert!(!entry.contains("__aver_thin_arg0"));
946    }
947
948    #[test]
949    fn optimized_keeps_known_leaf_wrapper_callsite_and_leaves_absorption_to_rust() {
950        let mut ctx = ctx_from_source(
951            r#"
952module Demo
953
954fn cellAt(grid: Vector<Int>, idx: Int) -> Int
955    Option.withDefault(Vector.get(grid, idx), 0)
956
957fn read(grid: Vector<Int>, idx: Int) -> Int
958    cellAt(grid, idx)
959"#,
960            "demo",
961        );
962
963        let out = transpile(&mut ctx);
964        let entry = generated_rust_entry_file(&out);
965
966        assert!(entry.contains("cellAt(grid, idx)"));
967        assert!(!entry.contains("__aver_thin_arg0"));
968    }
969
970    #[test]
971    fn optimized_keeps_known_dispatch_wrapper_callsite_and_leaves_absorption_to_rust() {
972        let mut ctx = ctx_from_source(
973            r#"
974module Demo
975
976fn bucket(n: Int) -> Int
977    match n == 0
978        true -> 0
979        false -> 1
980
981fn readBucket(n: Int) -> Int
982    bucket(n)
983"#,
984            "demo",
985        );
986
987        let out = transpile(&mut ctx);
988        let entry = generated_rust_entry_file(&out);
989
990        assert!(entry.contains("bucket(n)"));
991        assert!(!entry.contains("__aver_thin_arg0"));
992    }
993
994    #[test]
995    fn bool_match_on_gte_normalizes_to_base_comparison_when_optimized() {
996        let mut ctx = ctx_from_source(
997            r#"
998module Demo
999
1000fn bucket(n: Int) -> Int
1001    match n >= 10
1002        true -> 7
1003        false -> 3
1004"#,
1005            "demo",
1006        );
1007
1008        let out = transpile(&mut ctx);
1009        let entry = generated_rust_entry_file(&out);
1010
1011        assert!(entry.contains("if (n < 10i64) { 3i64 } else { 7i64 }"));
1012    }
1013
1014    #[test]
1015    fn bool_match_stays_as_match_in_semantic_mode() {
1016        let mut ctx = ctx_from_source(
1017            r#"
1018module Demo
1019
1020fn bucket(n: Int) -> Int
1021    match n >= 10
1022        true -> 7
1023        false -> 3
1024"#,
1025            "demo",
1026        );
1027
1028        let out = transpile(&mut ctx);
1029        let entry = generated_rust_entry_file(&out);
1030
1031        // Both modes now use the normalized if-else form
1032        assert!(entry.contains("if (n < 10i64) { 3i64 } else { 7i64 }"));
1033    }
1034
1035    #[test]
1036    fn optimized_self_tco_uses_dispatch_table_for_wrapper_match() {
1037        let mut ctx = ctx_from_source(
1038            r#"
1039module Demo
1040
1041fn loop(r: Result<Int, String>) -> Int
1042    match r
1043        Result.Ok(n) -> n
1044        Result.Err(_) -> loop(Result.Ok(1))
1045"#,
1046            "demo",
1047        );
1048
1049        let out = transpile(&mut ctx);
1050        let entry = generated_rust_entry_file(&out);
1051
1052        // Uses native Rust match directly (r cloned since not-Copy Ident without last_use info)
1053        assert!(entry.contains("match r.clone() {"));
1054        assert!(entry.contains("Ok(n)"));
1055        assert!(!entry.contains("__dispatch_subject"));
1056    }
1057
1058    #[test]
1059    fn optimized_mutual_tco_uses_dispatch_table_for_wrapper_match() {
1060        let mut ctx = ctx_from_source(
1061            r#"
1062module Demo
1063
1064fn left(r: Result<Int, String>) -> Int
1065    match r
1066        Result.Ok(n) -> n
1067        Result.Err(_) -> right(Result.Ok(1))
1068
1069fn right(r: Result<Int, String>) -> Int
1070    match r
1071        Result.Ok(n) -> n
1072        Result.Err(_) -> left(Result.Ok(1))
1073"#,
1074            "demo",
1075        );
1076
1077        let out = transpile(&mut ctx);
1078        let entry = generated_rust_entry_file(&out);
1079
1080        // Uses native Rust match directly (r cloned since not-Copy Ident without last_use info)
1081        assert!(entry.contains("match r.clone() {"));
1082        assert!(entry.contains("Ok(n)"));
1083        assert!(!entry.contains("__dispatch_subject"));
1084    }
1085
1086    #[test]
1087    fn single_field_variant_display_avoids_vec_join() {
1088        let mut ctx = ctx_from_source(
1089            r#"
1090module Demo
1091
1092type Wrapper
1093    Wrap(Int)
1094    Pair(Int, Int)
1095    Empty
1096"#,
1097            "demo",
1098        );
1099
1100        let out = transpile(&mut ctx);
1101        let entry = generated_rust_entry_file(&out);
1102
1103        // Single-field variant Wrap(Int): should NOT use vec![].join()
1104        assert!(
1105            !entry.contains("vec![f0.aver_display_inner()].join"),
1106            "single-field variant should use direct format, not vec join:\n{}",
1107            entry
1108        );
1109        // Multi-field variant Pair(Int, Int): SHOULD still use vec![].join()
1110        assert!(
1111            entry.contains("vec![f0.aver_display_inner(), f1.aver_display_inner()].join(\", \")"),
1112            "multi-field variant should use vec join:\n{}",
1113            entry
1114        );
1115    }
1116
1117    #[test]
1118    fn replay_codegen_wraps_guest_entry_in_scoped_runtime() {
1119        let mut ctx = ctx_from_source(
1120            r#"
1121module Demo
1122
1123fn runGuestProgram(path: String) -> Result<String, String>
1124    ! [Disk.readText]
1125    Disk.readText(path)
1126"#,
1127            "demo",
1128        );
1129        ctx.emit_replay_runtime = true;
1130        ctx.guest_entry = Some("runGuestProgram".to_string());
1131
1132        let out = transpile(&mut ctx);
1133        let entry = generated_rust_entry_file(&out);
1134        let replay_support = generated_file(&out, "src/replay_support.rs");
1135        let cargo_toml = generated_file(&out, "Cargo.toml");
1136
1137        assert!(entry.contains("aver_replay::with_guest_scope_result(\"runGuestProgram\""));
1138        assert!(replay_support.contains("pub mod aver_replay"));
1139        assert!(cargo_toml.contains("serde_json = \"1\""));
1140    }
1141
1142    #[test]
1143    fn replay_codegen_uses_guest_args_param_override_when_present() {
1144        let mut ctx = ctx_from_source(
1145            r#"
1146module Demo
1147
1148fn runGuestProgram(path: String, guestArgs: List<String>) -> Result<String, String>
1149    ! [Args.get]
1150    Result.Ok(String.join(Args.get(), ","))
1151"#,
1152            "demo",
1153        );
1154        ctx.emit_replay_runtime = true;
1155        ctx.guest_entry = Some("runGuestProgram".to_string());
1156
1157        let out = transpile(&mut ctx);
1158        let entry = generated_rust_entry_file(&out);
1159        let cargo_toml = generated_file(&out, "Cargo.toml");
1160
1161        assert!(entry.contains("aver_replay::with_guest_scope_args_result(\"runGuestProgram\""));
1162        assert!(entry.contains("guestArgs.clone()"));
1163        assert!(cargo_toml.contains("edition = \"2024\""));
1164    }
1165
1166    #[test]
1167    fn replay_codegen_wraps_root_main_when_no_guest_entry_is_set() {
1168        let mut ctx = ctx_from_source(
1169            r#"
1170module Demo
1171
1172fn main() -> Result<String, String>
1173    ! [Disk.readText]
1174    Disk.readText("demo.av")
1175"#,
1176            "demo",
1177        );
1178        ctx.emit_replay_runtime = true;
1179
1180        let out = transpile(&mut ctx);
1181        let root_main = generated_file(&out, "src/main.rs");
1182
1183        assert!(
1184            root_main.contains("aver_replay::with_guest_scope(\"main\", serde_json::Value::Null")
1185        );
1186    }
1187
1188    #[test]
1189    fn runtime_policy_codegen_uses_runtime_loader() {
1190        let mut ctx = ctx_from_source(
1191            r#"
1192module Demo
1193
1194fn main() -> Result<String, String>
1195    ! [Disk.readText]
1196    Disk.readText("demo.av")
1197"#,
1198            "demo",
1199        );
1200        ctx.emit_replay_runtime = true;
1201        ctx.runtime_policy_from_env = true;
1202
1203        let out = transpile(&mut ctx);
1204        let root_main = generated_file(&out, "src/main.rs");
1205        let replay_support = generated_file(&out, "src/replay_support.rs");
1206        let cargo_toml = generated_file(&out, "Cargo.toml");
1207
1208        assert!(!root_main.contains("mod policy_support;"));
1209        assert!(replay_support.contains("load_runtime_policy_from_env"));
1210        assert!(cargo_toml.contains("url = \"2\""));
1211        assert!(cargo_toml.contains("toml = \"0.8\""));
1212    }
1213
1214    #[test]
1215    fn replay_codegen_can_keep_embedded_policy_when_requested() {
1216        let mut ctx = ctx_from_source(
1217            r#"
1218module Demo
1219
1220fn main() -> Result<String, String>
1221    ! [Disk.readText]
1222    Disk.readText("demo.av")
1223"#,
1224            "demo",
1225        );
1226        ctx.emit_replay_runtime = true;
1227        ctx.policy = Some(crate::config::ProjectConfig {
1228            effect_policies: std::collections::HashMap::new(),
1229            check_suppressions: Vec::new(),
1230            independence_mode: crate::config::IndependenceMode::default(),
1231        });
1232
1233        let out = transpile(&mut ctx);
1234        let root_main = generated_file(&out, "src/main.rs");
1235        let replay_support = generated_file(&out, "src/replay_support.rs");
1236
1237        assert!(root_main.contains("mod policy_support;"));
1238        assert!(replay_support.contains("aver_policy::check_disk"));
1239        assert!(!replay_support.contains("RuntimeEffectPolicy"));
1240    }
1241
1242    #[test]
1243    fn self_host_support_is_emitted_as_separate_module() {
1244        let mut ctx = ctx_from_source(
1245            r#"
1246module Demo
1247
1248fn runGuestProgram(prog: Int, moduleFns: Int) -> Result<String, String>
1249    Result.Ok("ok")
1250"#,
1251            "demo",
1252        );
1253        ctx.emit_self_host_support = true;
1254        ctx.guest_entry = Some("runGuestProgram".to_string());
1255
1256        let out = transpile(&mut ctx);
1257        let root_main = generated_file(&out, "src/main.rs");
1258        let runtime_support = generated_file(&out, "src/runtime_support.rs");
1259        let self_host_support = generated_file(&out, "src/self_host_support.rs");
1260        let entry = generated_rust_entry_file(&out);
1261
1262        assert!(root_main.contains("mod self_host_support;"));
1263        assert!(!runtime_support.contains("with_fn_store"));
1264        assert!(self_host_support.contains("pub fn with_program_fn_store"));
1265        assert!(entry.contains("crate::self_host_support::with_program_fn_store("));
1266    }
1267
1268    #[test]
1269    fn independent_product_codegen_avoids_string_specific_error_type() {
1270        let mut ctx = ctx_from_source(
1271            r#"
1272module Demo
1273
1274fn left() -> Result<Int, Int>
1275    Result.Ok(1)
1276
1277fn right() -> Result<Int, Int>
1278    Result.Ok(2)
1279
1280fn main() -> Result<(Int, Int), Int>
1281    data = (left(), right())?!
1282    Result.Ok(data)
1283"#,
1284            "demo",
1285        );
1286        ctx.emit_replay_runtime = true;
1287
1288        let out = transpile(&mut ctx);
1289        let entry = generated_rust_entry_file(&out);
1290
1291        assert!(!entry.contains("Ok::<_, aver_rt::AverStr>"));
1292        assert!(entry.contains("crate::aver_replay::exit_effect_group();"));
1293        assert!(entry.contains("match (_r0, _r1)"));
1294        assert!(!entry.contains("let _r0 = left()?;"));
1295    }
1296
1297    #[test]
1298    fn independent_product_codegen_emits_cancel_runtime_and_scope_propagation() {
1299        let mut ctx = ctx_from_source(
1300            r#"
1301module Demo
1302
1303fn left() -> Result<Int, String>
1304    Result.Ok(1)
1305
1306fn right() -> Result<Int, String>
1307    Result.Ok(2)
1308
1309fn main() -> Result<(Int, Int), String>
1310    data = (left(), right())?!
1311    Result.Ok(data)
1312"#,
1313            "demo",
1314        );
1315        ctx.emit_replay_runtime = true;
1316        ctx.policy = Some(crate::config::ProjectConfig {
1317            effect_policies: std::collections::HashMap::new(),
1318            check_suppressions: Vec::new(),
1319            independence_mode: crate::config::IndependenceMode::Cancel,
1320        });
1321
1322        let out = transpile(&mut ctx);
1323        let entry = generated_rust_entry_file(&out);
1324        let runtime_support = generated_file(&out, "src/runtime_support.rs");
1325        let replay_support = generated_file(&out, "src/replay_support.rs");
1326
1327        assert!(entry.contains("crate::run_cancelable_branch"));
1328        assert!(entry.contains("capture_parallel_scope_context"));
1329        assert!(entry.contains("_s.spawn(move ||"));
1330        assert!(runtime_support.contains("pub fn run_cancelable_branch"));
1331        assert!(runtime_support.contains("panic_any(AverCancelled)"));
1332        assert!(replay_support.contains("pub fn capture_parallel_scope_context"));
1333        assert!(replay_support.contains("pub fn independence_mode_is_cancel()"));
1334    }
1335
1336    #[test]
1337    fn runtime_policy_codegen_parses_independence_mode() {
1338        let mut ctx = ctx_from_source(
1339            r#"
1340module Demo
1341
1342fn main() -> Result<String, String>
1343    ! [Disk.readText]
1344    Disk.readText("demo.av")
1345"#,
1346            "demo",
1347        );
1348        ctx.emit_replay_runtime = true;
1349        ctx.runtime_policy_from_env = true;
1350
1351        let out = transpile(&mut ctx);
1352        let replay_support = generated_file(&out, "src/replay_support.rs");
1353
1354        assert!(replay_support.contains("independence_mode_cancel"));
1355        assert!(replay_support.contains("[independence].mode must be 'complete' or 'cancel'"));
1356    }
1357
1358    #[test]
1359    fn effectful_codegen_inserts_cancel_checkpoint_before_builtin_calls() {
1360        let mut ctx = ctx_from_source(
1361            r#"
1362module Demo
1363
1364fn main() -> Result<String, String>
1365    ! [Disk.readText]
1366    Disk.readText("demo.av")
1367"#,
1368            "demo",
1369        );
1370
1371        let out = transpile(&mut ctx);
1372        let entry = generated_rust_entry_file(&out);
1373
1374        assert!(entry.contains("crate::cancel_checkpoint(); (aver_rt::read_text"));
1375    }
1376
1377    #[test]
1378    fn replay_support_matches_group_effects_by_occurrence_and_args() {
1379        let mut ctx = ctx_from_source(
1380            r#"
1381module Demo
1382
1383fn left() -> Result<Int, String>
1384    Result.Ok(1)
1385
1386fn right() -> Result<Int, String>
1387    Result.Ok(2)
1388
1389fn main() -> Result<(Int, Int), String>
1390    data = (left(), right())?!
1391    Result.Ok(data)
1392"#,
1393            "demo",
1394        );
1395        ctx.emit_replay_runtime = true;
1396
1397        let out = transpile(&mut ctx);
1398        let replay_support = generated_file(&out, "src/replay_support.rs");
1399
1400        assert!(replay_support.contains("candidate.effect_occurrence"));
1401        assert!(replay_support.contains("candidate.args != args"));
1402    }
1403}