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