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