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