1mod builtins;
5mod expr;
6mod liveness;
7mod pattern;
8mod policy;
9mod project;
10mod replay;
11mod runtime;
12mod syntax;
13mod toplevel;
14mod types;
15
16use std::collections::{BTreeMap, HashSet};
17
18use crate::ast::{FnDef, TopLevel, TypeDef};
19use crate::codegen::common::module_prefix_to_rust_segments;
20use crate::codegen::{CodegenContext, ProjectOutput};
21use crate::types::Type;
22
23#[derive(Default)]
24struct ModuleTreeNode {
25 content: Option<String>,
26 children: BTreeMap<String, ModuleTreeNode>,
27}
28
29pub fn transpile(ctx: &CodegenContext) -> ProjectOutput {
31 let used_services = detect_used_services(ctx);
32 let needs_http_types = needs_named_type(ctx, "Header")
33 || needs_named_type(ctx, "HttpResponse")
34 || needs_named_type(ctx, "HttpRequest");
35 let needs_tcp_types = needs_named_type(ctx, "Tcp.Connection");
36 let needs_terminal_types = needs_named_type(ctx, "Terminal.Size");
37
38 let has_tcp_runtime = used_services.contains("Tcp");
39 let has_http_runtime = used_services.contains("Http");
40 let has_http_server_runtime = used_services.contains("HttpServer");
41 let has_terminal_runtime = used_services.contains("Terminal");
42
43 let has_tcp_types = has_tcp_runtime || needs_tcp_types;
44 let has_http_types = has_http_runtime || has_http_server_runtime || needs_http_types;
45 let has_http_server_types = has_http_server_runtime || needs_named_type(ctx, "HttpRequest");
46 let has_terminal_types = has_terminal_runtime || needs_terminal_types;
47
48 let main_fn = ctx.fn_defs.iter().find(|fd| fd.name == "main");
49 let top_level_stmts: Vec<_> = ctx
50 .items
51 .iter()
52 .filter_map(|item| {
53 if let TopLevel::Stmt(stmt) = item {
54 Some(stmt)
55 } else {
56 None
57 }
58 })
59 .collect();
60 let verify_blocks: Vec<_> = ctx
61 .items
62 .iter()
63 .filter_map(|item| {
64 if let TopLevel::Verify(vb) = item {
65 Some(vb)
66 } else {
67 None
68 }
69 })
70 .collect();
71
72 let mut files = vec![
73 (
74 "Cargo.toml".to_string(),
75 project::generate_cargo_toml(
76 &ctx.project_name,
77 &used_services,
78 ctx.policy.is_some(),
79 ctx.emit_replay_runtime,
80 &std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("aver-rt"),
81 ),
82 ),
83 (
84 "src/main.rs".to_string(),
85 render_root_main(
86 main_fn,
87 ctx.policy.is_some(),
88 ctx.emit_replay_runtime,
89 ctx.guest_entry.as_deref(),
90 !verify_blocks.is_empty(),
91 ),
92 ),
93 (
94 "src/runtime_support.rs".to_string(),
95 render_runtime_support(
96 has_tcp_types,
97 has_http_types,
98 has_http_server_types,
99 ctx.emit_replay_runtime,
100 ctx.emit_self_host_runtime,
101 ),
102 ),
103 ];
104
105 if let Some(config) = &ctx.policy {
106 files.push((
107 "src/policy_support.rs".to_string(),
108 format!("{}\n", policy::generate_policy_runtime(config)),
109 ));
110 }
111
112 if ctx.emit_replay_runtime {
113 files.push((
114 "src/replay_support.rs".to_string(),
115 replay::generate_replay_runtime(
116 ctx.policy.is_some(),
117 has_terminal_types,
118 has_tcp_types,
119 has_http_types,
120 has_http_server_types,
121 ),
122 ));
123 }
124
125 if !verify_blocks.is_empty() {
126 files.push((
127 "src/verify.rs".to_string(),
128 render_verify_module(&verify_blocks, ctx),
129 ));
130 }
131
132 let mut module_tree = ModuleTreeNode::default();
133 insert_module_content(
134 &mut module_tree,
135 &[String::from("entry")],
136 render_generated_module(
137 root_module_depends(&ctx.items),
138 entry_module_sections(ctx, main_fn, &top_level_stmts),
139 ),
140 );
141
142 for module in &ctx.modules {
143 let path = module_prefix_to_rust_segments(&module.prefix);
144 insert_module_content(
145 &mut module_tree,
146 &path,
147 render_generated_module(module.depends.clone(), module_sections(module, ctx)),
148 );
149 }
150
151 emit_module_tree_files(&module_tree, "src/aver_generated", &mut files);
152 files.sort_by(|left, right| left.0.cmp(&right.0));
153
154 ProjectOutput { files }
155}
156
157fn render_root_main(
158 main_fn: Option<&FnDef>,
159 has_policy: bool,
160 has_replay: bool,
161 guest_entry: Option<&str>,
162 has_verify: bool,
163) -> String {
164 let mut sections = vec![
165 "#![allow(unused_variables, unused_mut, dead_code, unused_imports, unused_parens, non_snake_case, non_camel_case_types, unreachable_patterns)]".to_string(),
166 "#[macro_use] extern crate aver_rt;".to_string(),
167 "pub use ::aver_rt::AverMap as HashMap;".to_string(),
168 "pub use ::aver_rt::AverStr;".to_string(),
169 String::new(),
170 "mod runtime_support;".to_string(),
171 "pub use runtime_support::*;".to_string(),
172 ];
173
174 if has_policy {
175 sections.push(String::new());
176 sections.push("mod policy_support;".to_string());
177 sections.push("pub use policy_support::*;".to_string());
178 }
179
180 if has_replay {
181 sections.push(String::new());
182 sections.push("mod replay_support;".to_string());
183 sections.push("pub use replay_support::*;".to_string());
184 }
185
186 sections.push(String::new());
187 sections.push("pub mod aver_generated;".to_string());
188
189 if has_verify {
190 sections.push(String::new());
191 sections.push("#[cfg(test)]".to_string());
192 sections.push("mod verify;".to_string());
193 }
194
195 sections.push(String::new());
197 let returns_result = main_fn.is_some_and(|fd| fd.return_type.starts_with("Result<"));
198 let result_unit_string =
199 main_fn.is_some_and(|fd| fd.return_type.replace(' ', "") == "Result<Unit,String>");
200 if returns_result {
201 if result_unit_string {
202 sections.push("fn main() {".to_string());
203 sections.push(" let child = std::thread::Builder::new()".to_string());
204 sections.push(" .stack_size(256 * 1024 * 1024)".to_string());
205 if has_replay && guest_entry.is_none() {
206 sections.push(" .spawn(|| {".to_string());
207 sections.push(" let __result = aver_replay::with_guest_scope(\"main\", serde_json::Value::Null, aver_generated::entry::main);".to_string());
208 sections.push(" __result.map_err(|e| e.to_string())".to_string());
209 sections.push(" })".to_string());
210 } else {
211 sections.push(" .spawn(|| {".to_string());
212 sections
213 .push(" let __result = aver_generated::entry::main();".to_string());
214 sections.push(" __result.map_err(|e| e.to_string())".to_string());
215 sections.push(" })".to_string());
216 }
217 sections.push(" .expect(\"thread spawn\");".to_string());
218 sections.push(" match child.join().expect(\"thread join\") {".to_string());
219 sections.push(" Ok(()) => {}".to_string());
220 sections.push(" Err(e) => {".to_string());
221 sections.push(" eprintln!(\"{}\", e);".to_string());
222 sections.push(" std::process::exit(1);".to_string());
223 sections.push(" }".to_string());
224 sections.push(" }".to_string());
225 } else {
226 let ret_type = types::type_annotation_to_rust(&main_fn.unwrap().return_type);
227 sections.push(format!("fn main() -> {} {{", ret_type));
228 if has_replay && guest_entry.is_none() {
229 sections.push(
230 " aver_replay::with_guest_scope(\"main\", serde_json::Value::Null, aver_generated::entry::main)"
231 .to_string(),
232 );
233 } else {
234 sections.push(" aver_generated::entry::main()".to_string());
235 }
236 }
237 } else {
238 sections.push("fn main() {".to_string());
239 if main_fn.is_some() {
240 sections.push(" let child = std::thread::Builder::new()".to_string());
241 sections.push(" .stack_size(256 * 1024 * 1024)".to_string());
242 if has_replay && guest_entry.is_none() {
243 sections.push(" .spawn(|| aver_replay::with_guest_scope(\"main\", serde_json::Value::Null, || aver_generated::entry::main()))".to_string());
244 } else {
245 sections.push(" .spawn(|| aver_generated::entry::main())".to_string());
246 }
247 sections.push(" .expect(\"thread spawn\");".to_string());
248 sections.push(" child.join().expect(\"thread join\");".to_string());
249 }
250 }
251 sections.push("}".to_string());
252 sections.push(String::new());
253
254 sections.join("\n")
255}
256
257fn render_runtime_support(
258 has_tcp_types: bool,
259 has_http_types: bool,
260 has_http_server_types: bool,
261 has_replay: bool,
262 emit_self_host_runtime: bool,
263) -> String {
264 let mut sections = vec![runtime::generate_runtime(
265 has_replay,
266 has_http_server_types,
267 emit_self_host_runtime,
268 )];
269 if has_tcp_types {
270 sections.push(runtime::generate_tcp_types());
271 }
272 if has_http_types {
273 sections.push(runtime::generate_http_types());
274 }
275 if has_http_server_types {
276 sections.push(runtime::generate_http_server_types());
277 }
278 format!("{}\n", sections.join("\n\n"))
279}
280
281fn render_verify_module(
282 verify_blocks: &[&crate::ast::VerifyBlock],
283 ctx: &CodegenContext,
284) -> String {
285 [
286 "#[allow(unused_imports)]".to_string(),
287 "use crate::*;".to_string(),
288 "#[allow(unused_imports)]".to_string(),
289 "use crate::aver_generated::entry::*;".to_string(),
290 String::new(),
291 toplevel::emit_verify_blocks(verify_blocks, ctx),
292 String::new(),
293 ]
294 .join("\n")
295}
296
297fn render_generated_module(depends: Vec<String>, sections: Vec<String>) -> String {
298 if sections.is_empty() {
299 String::new()
300 } else {
301 let mut lines = vec![
302 "#[allow(unused_imports)]".to_string(),
303 "use crate::*;".to_string(),
304 ];
305 for dep in depends {
306 let path = module_prefix_to_rust_segments(&dep).join("::");
307 lines.push("#[allow(unused_imports)]".to_string());
308 lines.push(format!("use crate::aver_generated::{}::*;", path));
309 }
310 lines.push(String::new());
311 lines.push(sections.join("\n\n"));
312 lines.push(String::new());
313 lines.join("\n")
314 }
315}
316
317fn entry_module_sections(
318 ctx: &CodegenContext,
319 main_fn: Option<&FnDef>,
320 top_level_stmts: &[&crate::ast::Stmt],
321) -> Vec<String> {
322 let mut sections = Vec::new();
323
324 for td in &ctx.type_defs {
325 if is_shared_runtime_type(td) {
326 continue;
327 }
328 sections.push(toplevel::emit_public_type_def(td, ctx));
329 if ctx.emit_replay_runtime {
330 sections.push(replay::emit_replay_value_impl(td));
331 }
332 }
333
334 let non_main_fns: Vec<&FnDef> = ctx.fn_defs.iter().filter(|fd| fd.name != "main").collect();
336 let mutual_groups = toplevel::find_mutual_tco_groups(&non_main_fns);
337 let mut mutual_tco_members: HashSet<String> = HashSet::new();
338
339 for (group_id, group_indices) in mutual_groups.iter().enumerate() {
340 let group_fns: Vec<&FnDef> = group_indices.iter().map(|&idx| non_main_fns[idx]).collect();
341 for fd in &group_fns {
342 mutual_tco_members.insert(fd.name.clone());
343 }
344 sections.push(toplevel::emit_mutual_tco_block(
345 group_id + 1,
346 &group_fns,
347 ctx,
348 "pub ",
349 ));
350 }
351
352 for fd in &ctx.fn_defs {
353 if fd.name == "main" || mutual_tco_members.contains(&fd.name) {
354 continue;
355 }
356 let is_memo = ctx.memo_fns.contains(&fd.name);
357 sections.push(toplevel::emit_public_fn_def(fd, is_memo, ctx));
358 }
359
360 if main_fn.is_some() || !top_level_stmts.is_empty() {
361 sections.push(toplevel::emit_public_main(main_fn, top_level_stmts, ctx));
362 }
363
364 sections
365}
366
367fn module_sections(module: &crate::codegen::ModuleInfo, ctx: &CodegenContext) -> Vec<String> {
368 let mut sections = Vec::new();
369
370 for td in &module.type_defs {
371 if is_shared_runtime_type(td) {
372 continue;
373 }
374 sections.push(toplevel::emit_public_type_def(td, ctx));
375 if ctx.emit_replay_runtime {
376 sections.push(replay::emit_replay_value_impl(td));
377 }
378 }
379
380 let fn_refs: Vec<&FnDef> = module.fn_defs.iter().collect();
382 let mutual_groups = toplevel::find_mutual_tco_groups(&fn_refs);
383 let mut mutual_tco_members: HashSet<String> = HashSet::new();
384
385 for (group_id, group_indices) in mutual_groups.iter().enumerate() {
386 let group_fns: Vec<&FnDef> = group_indices.iter().map(|&idx| fn_refs[idx]).collect();
387 for fd in &group_fns {
388 mutual_tco_members.insert(fd.name.clone());
389 }
390 sections.push(toplevel::emit_mutual_tco_block(
391 group_id + 1,
392 &group_fns,
393 ctx,
394 "pub ",
395 ));
396 }
397
398 for fd in &module.fn_defs {
399 if mutual_tco_members.contains(&fd.name) {
400 continue;
401 }
402 let is_memo = ctx.memo_fns.contains(&fd.name);
403 sections.push(toplevel::emit_public_fn_def(fd, is_memo, ctx));
404 }
405
406 sections
407}
408
409fn insert_module_content(node: &mut ModuleTreeNode, segments: &[String], content: String) {
410 let child = node.children.entry(segments[0].clone()).or_default();
411 if segments.len() == 1 {
412 child.content = Some(content);
413 } else {
414 insert_module_content(child, &segments[1..], content);
415 }
416}
417
418fn emit_module_tree_files(node: &ModuleTreeNode, rel_dir: &str, files: &mut Vec<(String, String)>) {
419 let mut parts = Vec::new();
420
421 if let Some(content) = &node.content
422 && !content.trim().is_empty()
423 {
424 parts.push(content.trim_end().to_string());
425 }
426
427 for child_name in node.children.keys() {
428 parts.push(format!("pub mod {};", child_name));
429 }
430
431 let mut mod_rs = parts.join("\n\n");
432 if !mod_rs.is_empty() {
433 mod_rs.push('\n');
434 }
435 files.push((format!("{}/mod.rs", rel_dir), mod_rs));
436
437 for (child_name, child) in &node.children {
438 emit_module_tree_files(child, &format!("{}/{}", rel_dir, child_name), files);
439 }
440}
441
442fn root_module_depends(items: &[TopLevel]) -> Vec<String> {
443 items
444 .iter()
445 .find_map(|item| {
446 if let TopLevel::Module(module) = item {
447 Some(module.depends.clone())
448 } else {
449 None
450 }
451 })
452 .unwrap_or_default()
453}
454
455fn detect_used_services(ctx: &CodegenContext) -> HashSet<String> {
457 let mut services = HashSet::new();
458 for item in &ctx.items {
459 if let TopLevel::FnDef(fd) = item {
460 for eff in &fd.effects {
461 services.insert(eff.clone());
462 if let Some((service, _)) = eff.split_once('.') {
463 services.insert(service.to_string());
464 }
465 }
466 }
467 }
468 for module in &ctx.modules {
469 for fd in &module.fn_defs {
470 for eff in &fd.effects {
471 services.insert(eff.clone());
472 if let Some((service, _)) = eff.split_once('.') {
473 services.insert(service.to_string());
474 }
475 }
476 }
477 }
478 services
479}
480
481fn is_shared_runtime_type(td: &TypeDef) -> bool {
482 matches!(
483 td,
484 TypeDef::Product { name, .. }
485 if matches!(name.as_str(), "Header" | "HttpResponse" | "HttpRequest")
486 )
487}
488
489fn needs_named_type(ctx: &CodegenContext, wanted: &str) -> bool {
490 ctx.fn_sigs.values().any(|(params, ret, _effects)| {
491 params.iter().any(|p| type_contains_named(p, wanted)) || type_contains_named(ret, wanted)
492 })
493}
494
495fn type_contains_named(ty: &Type, wanted: &str) -> bool {
496 match ty {
497 Type::Named(name) => name == wanted,
498 Type::Result(ok, err) => {
499 type_contains_named(ok, wanted) || type_contains_named(err, wanted)
500 }
501 Type::Option(inner) | Type::List(inner) | Type::Vector(inner) => {
502 type_contains_named(inner, wanted)
503 }
504 Type::Tuple(items) => items.iter().any(|t| type_contains_named(t, wanted)),
505 Type::Map(k, v) => type_contains_named(k, wanted) || type_contains_named(v, wanted),
506 Type::Fn(params, ret, _effects) => {
507 params.iter().any(|t| type_contains_named(t, wanted))
508 || type_contains_named(ret, wanted)
509 }
510 Type::Int | Type::Float | Type::Str | Type::Bool | Type::Unit | Type::Unknown => false,
511 }
512}
513
514#[cfg(test)]
515mod tests {
516 use super::{
517 ModuleTreeNode, emit_module_tree_files, insert_module_content, render_generated_module,
518 transpile,
519 };
520 use crate::codegen::build_context;
521 use crate::source::parse_source;
522 use crate::tco;
523 use crate::types::checker::run_type_check_full;
524 use std::collections::HashSet;
525
526 fn ctx_from_source(source: &str, project_name: &str) -> crate::codegen::CodegenContext {
527 let mut items = parse_source(source).expect("source should parse");
528 tco::transform_program(&mut items);
529 let tc = run_type_check_full(&items, None);
530 assert!(
531 tc.errors.is_empty(),
532 "source should typecheck without errors: {:?}",
533 tc.errors
534 );
535 build_context(items, &tc, HashSet::new(), project_name.to_string(), vec![])
536 }
537
538 fn generated_rust_entry_file(out: &crate::codegen::ProjectOutput) -> &str {
539 out.files
540 .iter()
541 .find_map(|(name, content)| {
542 (name == "src/aver_generated/entry/mod.rs").then_some(content.as_str())
543 })
544 .expect("expected generated Rust entry module")
545 }
546
547 fn generated_file<'a>(out: &'a crate::codegen::ProjectOutput, path: &str) -> &'a str {
548 out.files
549 .iter()
550 .find_map(|(name, content)| (name == path).then_some(content.as_str()))
551 .unwrap_or_else(|| panic!("expected generated file '{}'", path))
552 }
553
554 #[test]
555 fn generated_module_imports_direct_depends() {
556 let rendered = render_generated_module(
557 vec!["Domain.Types".to_string(), "App.Commands".to_string()],
558 vec!["pub fn demo() {}".to_string()],
559 );
560
561 assert!(rendered.contains("use crate::aver_generated::domain::types::*;"));
562 assert!(rendered.contains("use crate::aver_generated::app::commands::*;"));
563 assert!(rendered.contains("pub fn demo() {}"));
564 }
565
566 #[test]
567 fn module_tree_files_do_not_reexport_children() {
568 let mut tree = ModuleTreeNode::default();
569 insert_module_content(
570 &mut tree,
571 &["app".to_string(), "cli".to_string()],
572 "pub fn run() {}".to_string(),
573 );
574
575 let mut files = Vec::new();
576 emit_module_tree_files(&tree, "src/aver_generated", &mut files);
577
578 let root_mod = files
579 .iter()
580 .find(|(path, _)| path == "src/aver_generated/mod.rs")
581 .map(|(_, content)| content)
582 .expect("root mod.rs should exist");
583
584 assert!(root_mod.contains("pub mod app;"));
585 assert!(!root_mod.contains("pub use app::*;"));
586 }
587
588 #[test]
589 fn list_cons_match_uses_cloned_uncons_fast_path() {
590 let ctx = ctx_from_source(
591 r#"
592module Demo
593
594fn headPlusTailLen(xs: List<Int>) -> Int
595 match xs
596 [] -> 0
597 [h, ..t] -> h + List.len(t)
598"#,
599 "demo",
600 );
601
602 let out = transpile(&ctx);
603 let entry = generated_rust_entry_file(&out);
604
605 assert!(entry.contains("aver_list_match!"));
607 }
608
609 #[test]
610 fn list_literal_clones_ident_when_used_afterward() {
611 let ctx = ctx_from_source(
612 r#"
613module Demo
614
615record Audit
616 message: String
617
618fn useTwice(audit: Audit) -> List<Audit>
619 first = [audit]
620 [audit]
621"#,
622 "demo",
623 );
624
625 let out = transpile(&ctx);
626 let entry = generated_rust_entry_file(&out);
627
628 assert!(entry.contains("let first = aver_rt::AverList::from_vec(vec![audit.clone()]);"));
629 assert!(entry.contains("aver_rt::AverList::from_vec(vec![audit])"));
630 }
631
632 #[test]
633 fn record_update_clones_base_when_value_is_used_afterward() {
634 let ctx = ctx_from_source(
635 r#"
636module Demo
637
638record PaymentState
639 paymentId: String
640 currency: String
641
642fn touch(state: PaymentState) -> String
643 updated = PaymentState.update(state, currency = "EUR")
644 state.paymentId
645"#,
646 "demo",
647 );
648
649 let out = transpile(&ctx);
650 let entry = generated_rust_entry_file(&out);
651
652 assert!(entry.contains("..state.clone()"));
653 }
654
655 #[test]
656 fn mutual_tco_generates_trampoline_instead_of_regular_calls() {
657 let ctx = ctx_from_source(
658 r#"
659module Demo
660
661fn isEven(n: Int) -> Bool
662 match n == 0
663 true -> true
664 false -> isOdd(n - 1)
665
666fn isOdd(n: Int) -> Bool
667 match n == 0
668 true -> false
669 false -> isEven(n - 1)
670"#,
671 "demo",
672 );
673
674 let out = transpile(&ctx);
675 let entry = generated_rust_entry_file(&out);
676
677 assert!(entry.contains("enum __MutualTco1"));
679 assert!(entry.contains("fn __mutual_tco_trampoline_1"));
680 assert!(entry.contains("loop {"));
681
682 assert!(entry.contains("pub fn isEven"));
684 assert!(entry.contains("pub fn isOdd"));
685 assert!(entry.contains("__mutual_tco_trampoline_1("));
686
687 assert!(!entry.contains("isOdd((n - 1i64))"));
689 }
690
691 #[test]
692 fn field_access_does_not_double_clone() {
693 let ctx = ctx_from_source(
694 r#"
695module Demo
696
697record User
698 name: String
699 age: Int
700
701fn greet(u: User) -> String
702 u.name
703"#,
704 "demo",
705 );
706
707 let out = transpile(&ctx);
708 let entry = generated_rust_entry_file(&out);
709
710 assert!(
712 !entry.contains(".clone().clone()"),
713 "double clone detected in generated code:\n{}",
714 entry
715 );
716 }
717
718 #[test]
719 fn single_field_variant_display_avoids_vec_join() {
720 let ctx = ctx_from_source(
721 r#"
722module Demo
723
724type Wrapper
725 Wrap(Int)
726 Pair(Int, Int)
727 Empty
728"#,
729 "demo",
730 );
731
732 let out = transpile(&ctx);
733 let entry = generated_rust_entry_file(&out);
734
735 assert!(
737 !entry.contains("vec![f0.aver_display_inner()].join"),
738 "single-field variant should use direct format, not vec join:\n{}",
739 entry
740 );
741 assert!(
743 entry.contains("vec![f0.aver_display_inner(), f1.aver_display_inner()].join(\", \")"),
744 "multi-field variant should use vec join:\n{}",
745 entry
746 );
747 }
748
749 #[test]
750 fn replay_codegen_wraps_guest_entry_in_scoped_runtime() {
751 let mut ctx = ctx_from_source(
752 r#"
753module Demo
754
755fn runGuestProgram(path: String) -> Result<String, String>
756 ! [Disk.readText]
757 Disk.readText(path)
758"#,
759 "demo",
760 );
761 ctx.emit_replay_runtime = true;
762 ctx.guest_entry = Some("runGuestProgram".to_string());
763
764 let out = transpile(&ctx);
765 let entry = generated_rust_entry_file(&out);
766 let replay_support = generated_file(&out, "src/replay_support.rs");
767 let cargo_toml = generated_file(&out, "Cargo.toml");
768
769 assert!(entry.contains("aver_replay::with_guest_scope_result(\"runGuestProgram\""));
770 assert!(replay_support.contains("pub mod aver_replay"));
771 assert!(cargo_toml.contains("serde_json = \"1\""));
772 }
773
774 #[test]
775 fn replay_codegen_uses_guest_args_param_override_when_present() {
776 let mut ctx = ctx_from_source(
777 r#"
778module Demo
779
780fn runGuestProgram(path: String, guestArgs: List<String>) -> Result<String, String>
781 ! [Args.get]
782 Result.Ok(String.join(Args.get(), ","))
783"#,
784 "demo",
785 );
786 ctx.emit_replay_runtime = true;
787 ctx.guest_entry = Some("runGuestProgram".to_string());
788
789 let out = transpile(&ctx);
790 let entry = generated_rust_entry_file(&out);
791 let cargo_toml = generated_file(&out, "Cargo.toml");
792
793 assert!(entry.contains("aver_replay::with_guest_scope_args_result(\"runGuestProgram\""));
794 assert!(entry.contains("guestArgs.clone()"));
795 assert!(cargo_toml.contains("edition = \"2024\""));
796 }
797
798 #[test]
799 fn replay_codegen_wraps_root_main_when_no_guest_entry_is_set() {
800 let mut ctx = ctx_from_source(
801 r#"
802module Demo
803
804fn main() -> Result<String, String>
805 ! [Disk.readText]
806 Disk.readText("demo.av")
807"#,
808 "demo",
809 );
810 ctx.emit_replay_runtime = true;
811
812 let out = transpile(&ctx);
813 let root_main = generated_file(&out, "src/main.rs");
814
815 assert!(
816 root_main.contains("aver_replay::with_guest_scope(\"main\", serde_json::Value::Null")
817 );
818 }
819}