1mod 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
24fn 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
71pub 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");
102 debug_assert_eq!(
103 main_fn.is_some(),
104 ctx.resolved_program
105 .entry_fns()
106 .any(|rfd| rfd.name == "main"),
107 "ctx.fn_defs and ctx.resolved_program.entry_fns() must agree on \
108 main fn presence (epic #170 Phase 1 invariant)"
109 );
110 let top_level_stmts: Vec<_> = ctx
111 .items
112 .iter()
113 .filter_map(|item| {
114 if let TopLevel::Stmt(stmt) = item {
115 Some(stmt)
116 } else {
117 None
118 }
119 })
120 .collect();
121 let verify_blocks: Vec<_> = ctx
122 .items
123 .iter()
124 .filter_map(|item| {
125 if let TopLevel::Verify(vb) = item {
126 Some(vb)
127 } else {
128 None
129 }
130 })
131 .collect();
132
133 let mut files = vec![
134 (
135 "Cargo.toml".to_string(),
136 project::generate_cargo_toml(
137 &ctx.project_name,
138 &used_services,
139 has_embedded_policy,
140 has_runtime_policy,
141 ctx.emit_replay_runtime,
142 ),
143 ),
144 (
145 "src/main.rs".to_string(),
146 render_root_main(
147 main_fn,
148 has_embedded_policy,
149 ctx.emit_replay_runtime,
150 ctx.guest_entry.as_deref(),
151 !verify_blocks.is_empty(),
152 ctx.emit_self_host_support,
153 ),
154 ),
155 (
156 "src/runtime_support.rs".to_string(),
157 render_runtime_support(
158 has_tcp_types,
159 has_http_types,
160 has_http_server_types,
161 ctx.emit_replay_runtime,
162 embedded_independence_cancel,
163 ),
164 ),
165 ];
166
167 if ctx.emit_self_host_support {
168 files.push((
169 "src/self_host_support.rs".to_string(),
170 self_host::generate_self_host_support(),
171 ));
172 }
173
174 if has_embedded_policy && let Some(config) = &ctx.policy {
175 files.push((
176 "src/policy_support.rs".to_string(),
177 format!("{}\n", policy::generate_policy_runtime(config)),
178 ));
179 }
180
181 if ctx.emit_replay_runtime {
182 files.push((
183 "src/replay_support.rs".to_string(),
184 replay::generate_replay_runtime(
185 has_embedded_policy,
186 has_runtime_policy,
187 has_terminal_types,
188 has_tcp_types,
189 has_http_types,
190 has_http_server_types,
191 embedded_independence_cancel,
192 ),
193 ));
194 }
195
196 if !verify_blocks.is_empty() {
197 files.push((
198 "src/verify.rs".to_string(),
199 render_verify_module(&verify_blocks, ctx),
200 ));
201 }
202
203 let mut rust_modules: Vec<(Vec<String>, String)> = Vec::new();
204 rust_modules.push((
205 vec!["entry".to_string()],
206 render_generated_module(
207 root_module_depends(&ctx.items),
208 entry_module_sections(ctx, main_fn, &top_level_stmts),
209 ),
210 ));
211
212 for i in 0..ctx.modules.len() {
213 ctx.extra_fn_defs = ctx.modules[i].fn_defs.clone();
216 let module = &ctx.modules[i];
217 let segments = module_prefix_to_rust_segments(&module.prefix);
218 rust_modules.push((
219 segments,
220 render_generated_module(module.depends.clone(), module_sections(module, ctx)),
221 ));
222 }
223 ctx.extra_fn_defs.clear();
224
225 files.extend(synthesize_rust_module_cascade(
226 "src/aver_generated",
227 &rust_modules,
228 ));
229 files.sort_by(|left, right| left.0.cmp(&right.0));
230
231 ProjectOutput { files }
232}
233
234fn render_root_main(
235 main_fn: Option<&FnDef>,
236 has_policy: bool,
237 has_replay: bool,
238 guest_entry: Option<&str>,
239 has_verify: bool,
240 has_self_host_support: bool,
241) -> String {
242 let mut sections = vec![
243 "#![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(),
244 "// Aver Rust emission".to_string(),
245 "#[macro_use] extern crate aver_rt;".to_string(),
246 "pub use ::aver_rt::AverMap as HashMap;".to_string(),
247 "pub use ::aver_rt::AverStr;".to_string(),
248 "pub use ::aver_rt::Buffer;".to_string(),
249 String::new(),
250 "mod runtime_support;".to_string(),
251 "pub use runtime_support::*;".to_string(),
252 ];
253
254 if has_policy {
255 sections.push(String::new());
256 sections.push("mod policy_support;".to_string());
257 sections.push("pub use policy_support::*;".to_string());
258 }
259
260 if has_replay {
261 sections.push(String::new());
262 sections.push("mod replay_support;".to_string());
263 sections.push("pub use replay_support::*;".to_string());
264 }
265
266 if has_self_host_support {
267 sections.push(String::new());
268 sections.push("mod self_host_support;".to_string());
269 }
270
271 sections.push(String::new());
272 sections.push("pub mod aver_generated;".to_string());
273
274 if has_verify {
275 sections.push(String::new());
276 sections.push("#[cfg(test)]".to_string());
277 sections.push("mod verify;".to_string());
278 }
279
280 sections.push(String::new());
282 let returns_result = main_fn.is_some_and(|fd| fd.return_type.starts_with("Result<"));
283 let result_unit_string =
284 main_fn.is_some_and(|fd| fd.return_type.replace(' ', "") == "Result<Unit,String>");
285 let bench_dispatch_lines = |has_replay: bool| -> Vec<String> {
294 let user_call = if has_replay {
295 "aver_replay::with_guest_scope(\"main\", serde_json::Value::Null, aver_generated::entry::main)"
296 } else {
297 "aver_generated::entry::main()"
298 };
299 vec![
300 " if let Ok(n_str) = std::env::var(\"AVER_BENCH_ITER\") {".to_string(),
301 " let n: usize = n_str.parse().unwrap_or(0);".to_string(),
302 " let warmup: usize = std::env::var(\"AVER_BENCH_WARMUP\").ok().and_then(|s| s.parse().ok()).unwrap_or(0);".to_string(),
303 " for _ in 0..warmup {".to_string(),
304 format!(" let _ = std::hint::black_box({});", user_call),
305 " }".to_string(),
306 " for _ in 0..n {".to_string(),
307 " let t = std::time::Instant::now();".to_string(),
308 format!(" let _ = std::hint::black_box({});", user_call),
309 " eprintln!(\"__bench_iter_ms__: {}\", t.elapsed().as_secs_f64() * 1000.0);".to_string(),
310 " }".to_string(),
311 " std::process::exit(0);".to_string(),
312 " }".to_string(),
313 ]
314 };
315 if returns_result {
316 if result_unit_string {
317 sections.push("fn main() {".to_string());
318 sections.extend(bench_dispatch_lines(has_replay && guest_entry.is_none()));
319 sections.push(" let child = std::thread::Builder::new()".to_string());
320 sections.push(" .stack_size(256 * 1024 * 1024)".to_string());
321 if has_replay && guest_entry.is_none() {
322 sections.push(" .spawn(|| {".to_string());
323 sections.push(" let __result = aver_replay::with_guest_scope(\"main\", serde_json::Value::Null, aver_generated::entry::main);".to_string());
324 sections.push(" __result.map_err(|e| e.to_string())".to_string());
325 sections.push(" })".to_string());
326 } else {
327 sections.push(" .spawn(|| {".to_string());
328 sections
329 .push(" let __result = aver_generated::entry::main();".to_string());
330 sections.push(" __result.map_err(|e| e.to_string())".to_string());
331 sections.push(" })".to_string());
332 }
333 sections.push(" .expect(\"thread spawn\");".to_string());
334 sections.push(" match child.join().expect(\"thread join\") {".to_string());
335 sections.push(" Ok(()) => {}".to_string());
336 sections.push(" Err(e) => {".to_string());
337 sections.push(" eprintln!(\"{}\", e);".to_string());
338 sections.push(" std::process::exit(1);".to_string());
339 sections.push(" }".to_string());
340 sections.push(" }".to_string());
341 } else {
342 let ret_type = types::type_annotation_to_rust(&main_fn.unwrap().return_type);
343 sections.push(format!("fn main() -> {} {{", ret_type));
344 sections.extend(bench_dispatch_lines(has_replay && guest_entry.is_none()));
345 if has_replay && guest_entry.is_none() {
346 sections.push(
347 " aver_replay::with_guest_scope(\"main\", serde_json::Value::Null, aver_generated::entry::main)"
348 .to_string(),
349 );
350 } else {
351 sections.push(" aver_generated::entry::main()".to_string());
352 }
353 }
354 } else {
355 sections.push("fn main() {".to_string());
356 if main_fn.is_some() {
357 sections.extend(bench_dispatch_lines(has_replay && guest_entry.is_none()));
358 sections.push(" let child = std::thread::Builder::new()".to_string());
359 sections.push(" .stack_size(256 * 1024 * 1024)".to_string());
360 if has_replay && guest_entry.is_none() {
361 sections.push(" .spawn(|| aver_replay::with_guest_scope(\"main\", serde_json::Value::Null, || aver_generated::entry::main()))".to_string());
362 } else {
363 sections.push(" .spawn(|| aver_generated::entry::main())".to_string());
364 }
365 sections.push(" .expect(\"thread spawn\");".to_string());
366 sections.push(" child.join().expect(\"thread join\");".to_string());
367 }
368 }
369 sections.push("}".to_string());
370 sections.push(String::new());
371
372 sections.join("\n")
373}
374
375fn render_runtime_support(
376 has_tcp_types: bool,
377 has_http_types: bool,
378 has_http_server_types: bool,
379 has_replay: bool,
380 embedded_independence_cancel: bool,
381) -> String {
382 let mut sections = vec![runtime::generate_runtime(
383 has_replay,
384 has_http_server_types,
385 embedded_independence_cancel,
386 )];
387 if has_tcp_types {
388 sections.push(runtime::generate_tcp_types());
389 }
390 if has_http_types {
391 sections.push(runtime::generate_http_types());
392 }
393 if has_http_server_types {
394 sections.push(runtime::generate_http_server_types());
395 }
396 format!("{}\n", sections.join("\n\n"))
397}
398
399fn render_verify_module(
400 verify_blocks: &[&crate::ast::VerifyBlock],
401 ctx: &CodegenContext,
402) -> String {
403 [
404 "#[allow(unused_imports)]".to_string(),
405 "use crate::*;".to_string(),
406 "#[allow(unused_imports)]".to_string(),
407 "use crate::aver_generated::entry::*;".to_string(),
408 String::new(),
409 toplevel::emit_verify_blocks(verify_blocks, ctx),
410 String::new(),
411 ]
412 .join("\n")
413}
414
415fn render_generated_module(depends: Vec<String>, sections: Vec<String>) -> String {
416 if sections.is_empty() {
417 String::new()
418 } else {
419 let mut lines = vec![
420 "#[allow(unused_imports)]".to_string(),
421 "use crate::*;".to_string(),
422 ];
423 for dep in depends {
424 let path = module_prefix_to_rust_segments(&dep).join("::");
425 lines.push("#[allow(unused_imports)]".to_string());
426 lines.push(format!("use crate::aver_generated::{}::*;", path));
427 }
428 lines.push(String::new());
429 lines.push(sections.join("\n\n"));
430 lines.push(String::new());
431 lines.join("\n")
432 }
433}
434
435fn entry_module_sections(
436 ctx: &CodegenContext,
437 main_fn: Option<&FnDef>,
438 top_level_stmts: &[&crate::ast::Stmt],
439) -> Vec<String> {
440 let mut sections = Vec::new();
441
442 for td in &ctx.type_defs {
443 if is_shared_runtime_type(td) {
444 continue;
445 }
446 sections.push(toplevel::emit_public_type_def(td, ctx));
447 if ctx.emit_replay_runtime {
448 sections.push(replay::emit_replay_value_impl(td));
449 }
450 }
451
452 let non_main_fns: Vec<&FnDef> = ctx.fn_defs.iter().filter(|fd| fd.name != "main").collect();
463 let mutual_groups = toplevel::find_mutual_tco_groups(&non_main_fns);
464
465 for (group_id, group_indices) in mutual_groups.iter().enumerate() {
466 let group_fns: Vec<&FnDef> = group_indices.iter().map(|&idx| non_main_fns[idx]).collect();
467 sections.push(toplevel::emit_mutual_tco_block(
468 group_id + 1,
469 &group_fns,
470 ctx,
471 None,
472 "pub ",
473 ));
474 }
475
476 for fd in &ctx.fn_defs {
484 let Some(fn_id) = crate::codegen::common::fn_id_for_decl(ctx, fd) else {
485 continue;
486 };
487 let is_mutual = ctx.mutual_tco_members.contains(&fn_id);
488 if fd.name == "main" || is_mutual {
489 continue;
490 }
491 let Some(resolved_fd) = ctx.resolved_program.fn_by_id(fn_id) else {
492 let resolved_owned = ctx.resolve_fn_def(fd, None);
497 let is_memo = ctx.memo_fns.contains(&fd.name);
498 sections.push(toplevel::emit_public_fn_def(
499 fd,
500 resolved_owned.as_ref(),
501 is_memo,
502 ctx,
503 None,
504 ));
505 continue;
506 };
507 let is_memo = ctx.memo_fns.contains(&fd.name);
508 sections.push(toplevel::emit_public_fn_def(
509 fd,
510 resolved_fd,
511 is_memo,
512 ctx,
513 None,
514 ));
515 }
516
517 if main_fn.is_some() || !top_level_stmts.is_empty() {
518 sections.push(toplevel::emit_public_main(main_fn, top_level_stmts, ctx));
519 }
520
521 sections
522}
523
524fn module_sections(module: &crate::codegen::ModuleInfo, ctx: &CodegenContext) -> Vec<String> {
525 let mut sections = Vec::new();
526
527 for td in &module.type_defs {
528 if is_shared_runtime_type(td) {
529 continue;
530 }
531 sections.push(toplevel::emit_public_type_def(td, ctx));
532 if ctx.emit_replay_runtime {
533 sections.push(replay::emit_replay_value_impl(td));
534 }
535 }
536
537 let fn_refs: Vec<&FnDef> = module.fn_defs.iter().collect();
544 let mutual_groups = toplevel::find_mutual_tco_groups(&fn_refs);
545 let module_mutual_owned: HashSet<String> = match module.analysis.as_ref() {
546 Some(a) => a.mutual_tco_members.clone(),
547 None => ctx
548 .mutual_tco_members
549 .iter()
550 .filter_map(|id| {
551 let entry = ctx.symbol_table.fn_entry(*id);
552 entry
553 .key
554 .scope
555 .as_deref()
556 .filter(|s| *s == module.prefix)
557 .map(|_| entry.key.name.clone())
558 })
559 .collect(),
560 };
561 let module_mutual = &module_mutual_owned;
562
563 for (group_id, group_indices) in mutual_groups.iter().enumerate() {
564 let group_fns: Vec<&FnDef> = group_indices.iter().map(|&idx| fn_refs[idx]).collect();
565 sections.push(toplevel::emit_mutual_tco_block(
566 group_id + 1,
567 &group_fns,
568 ctx,
569 Some(&module.prefix),
570 "pub ",
571 ));
572 }
573
574 for fd in &module.fn_defs {
575 if module_mutual.contains(&fd.name) {
576 continue;
577 }
578 let is_memo = ctx.memo_fns.contains(&fd.name);
579 let resolved_fd = crate::codegen::common::fn_id_for_decl(ctx, fd)
584 .and_then(|id| ctx.resolved_program.fn_by_id(id));
585 let resolved_owned = if resolved_fd.is_some() {
586 None
587 } else {
588 Some(ctx.resolve_fn_def(fd, Some(&module.prefix)))
591 };
592 let resolved_ref: &crate::ir::hir::ResolvedFnDef =
593 resolved_fd.unwrap_or_else(|| resolved_owned.as_ref().unwrap().as_ref());
594 sections.push(toplevel::emit_public_fn_def(
595 fd,
596 resolved_ref,
597 is_memo,
598 ctx,
599 Some(&module.prefix),
600 ));
601 }
602
603 sections
604}
605
606fn root_module_depends(items: &[TopLevel]) -> Vec<String> {
607 items
608 .iter()
609 .find_map(|item| {
610 if let TopLevel::Module(module) = item {
611 Some(module.depends.clone())
612 } else {
613 None
614 }
615 })
616 .unwrap_or_default()
617}
618
619fn detect_used_services(ctx: &CodegenContext) -> HashSet<String> {
621 let mut services = HashSet::new();
622 for item in &ctx.items {
623 if let TopLevel::FnDef(fd) = item {
624 for eff in &fd.effects {
625 services.insert(eff.node.clone());
626 if let Some((service, _)) = eff.node.split_once('.') {
627 services.insert(service.to_string());
628 }
629 }
630 }
631 }
632 for module in &ctx.modules {
633 for fd in &module.fn_defs {
634 for eff in &fd.effects {
635 services.insert(eff.node.clone());
636 if let Some((service, _)) = eff.node.split_once('.') {
637 services.insert(service.to_string());
638 }
639 }
640 }
641 }
642 services
643}
644
645fn is_shared_runtime_type(td: &TypeDef) -> bool {
646 matches!(
647 td,
648 TypeDef::Product { name, .. }
649 if matches!(name.as_str(), "HttpResponse" | "HttpRequest")
650 )
651}
652
653fn needs_named_type(ctx: &CodegenContext, wanted: &str) -> bool {
654 let scan = |params: &[(String, Type)], ret: &Type| -> bool {
659 params.iter().any(|(_, p)| type_contains_named(p, wanted))
660 || type_contains_named(ret, wanted)
661 };
662 if ctx
663 .resolved_program
664 .entry_fns()
665 .any(|rfd| scan(&rfd.params, &rfd.return_type))
666 {
667 return true;
668 }
669 ctx.resolved_program.modules.iter().any(|m| {
670 m.fn_defs
671 .iter()
672 .any(|rfd| scan(&rfd.params, &rfd.return_type))
673 })
674}
675
676fn type_contains_named(ty: &Type, wanted: &str) -> bool {
684 match ty {
685 Type::Named { name, .. } => name == wanted,
686 Type::Result(ok, err) => {
687 type_contains_named(ok, wanted) || type_contains_named(err, wanted)
688 }
689 Type::Option(inner) | Type::List(inner) | Type::Vector(inner) => {
690 type_contains_named(inner, wanted)
691 }
692 Type::Tuple(items) => items.iter().any(|t| type_contains_named(t, wanted)),
693 Type::Map(k, v) => type_contains_named(k, wanted) || type_contains_named(v, wanted),
694 Type::Fn(params, ret, _effects) => {
695 params.iter().any(|t| type_contains_named(t, wanted))
696 || type_contains_named(ret, wanted)
697 }
698 Type::Int
699 | Type::Float
700 | Type::Str
701 | Type::Bool
702 | Type::Unit
703 | Type::Invalid
704 | Type::Var(_) => false,
705 }
706}
707
708#[cfg(test)]
709mod tests {
710 use super::{render_generated_module, synthesize_rust_module_cascade, transpile};
711 use crate::codegen::build_context;
712 use crate::source::parse_source;
713
714 use std::collections::HashSet;
715
716 fn ctx_from_source(source: &str, project_name: &str) -> crate::codegen::CodegenContext {
717 let mut items = parse_source(source).expect("source should parse");
718 crate::ir::pipeline::tco(&mut items);
719 let tc = crate::ir::pipeline::typecheck(
720 &items,
721 &crate::ir::TypecheckMode::Full { base_dir: None },
722 );
723 assert!(
724 tc.errors.is_empty(),
725 "source should typecheck without errors: {:?}",
726 tc.errors
727 );
728 let symbol_table = crate::ir::SymbolTable::build(&items, &[]);
732 let resolved_items = crate::ir::hir::resolve_program(&symbol_table, &items);
733 build_context(
734 items,
735 &tc,
736 None,
737 HashSet::new(),
738 project_name.to_string(),
739 vec![],
740 symbol_table,
741 resolved_items,
742 )
743 }
744
745 fn ctx_from_multi(
750 entry_src: &str,
751 dep_sources: &[(&str, &str)],
752 project_name: &str,
753 ) -> crate::codegen::CodegenContext {
754 let mut entry_items = parse_source(entry_src).expect("entry source should parse");
755 crate::ir::pipeline::tco(&mut entry_items);
756
757 let loaded: Vec<crate::source::LoadedModule> = dep_sources
762 .iter()
763 .map(|(prefix, src)| {
764 let items = parse_source(src).expect("dep source should parse");
765 crate::source::LoadedModule {
766 dep_name: prefix.to_string(),
767 items,
768 path: std::path::PathBuf::from(format!("{}.av", prefix)),
769 }
770 })
771 .collect();
772
773 let modules: Vec<crate::codegen::ModuleInfo> = loaded
774 .iter()
775 .map(|lm| {
776 let depends = lm
777 .items
778 .iter()
779 .find_map(|i| match i {
780 crate::ast::TopLevel::Module(m) => Some(m.depends.clone()),
781 _ => None,
782 })
783 .unwrap_or_default();
784 let type_defs = lm
785 .items
786 .iter()
787 .filter_map(|i| match i {
788 crate::ast::TopLevel::TypeDef(td) => Some(td.clone()),
789 _ => None,
790 })
791 .collect();
792 let fn_defs = lm
793 .items
794 .iter()
795 .filter_map(|i| match i {
796 crate::ast::TopLevel::FnDef(fd) if fd.name != "main" => Some(fd.clone()),
797 _ => None,
798 })
799 .collect();
800 crate::codegen::ModuleInfo {
801 prefix: lm.dep_name.clone(),
802 depends,
803 type_defs,
804 fn_defs,
805 analysis: None,
806 }
807 })
808 .collect();
809
810 let tc = crate::ir::pipeline::typecheck(
811 &entry_items,
812 &crate::ir::TypecheckMode::WithLoaded(&loaded),
813 );
814 assert!(
815 tc.errors.is_empty(),
816 "entry+dep source should typecheck without errors: {:?}",
817 tc.errors
818 );
819 let symbol_table = crate::ir::SymbolTable::build(&entry_items, &modules);
820 let resolved_items = crate::ir::hir::resolve_program(&symbol_table, &entry_items);
821 build_context(
822 entry_items,
823 &tc,
824 None,
825 HashSet::new(),
826 project_name.to_string(),
827 modules,
828 symbol_table,
829 resolved_items,
830 )
831 }
832
833 fn generated_rust_entry_file(out: &crate::codegen::ProjectOutput) -> &str {
834 out.files
835 .iter()
836 .find_map(|(name, content)| {
837 (name == "src/aver_generated/entry/mod.rs").then_some(content.as_str())
838 })
839 .expect("expected generated Rust entry module")
840 }
841
842 fn generated_file<'a>(out: &'a crate::codegen::ProjectOutput, path: &str) -> &'a str {
843 out.files
844 .iter()
845 .find_map(|(name, content)| (name == path).then_some(content.as_str()))
846 .unwrap_or_else(|| panic!("expected generated file '{}'", path))
847 }
848
849 #[test]
850 fn cross_module_same_bare_name_fns_resolve_via_qualified_path() {
851 let mut ctx = ctx_from_multi(
865 r#"
866module Entry
867 depends [Worker]
868 intent = "Entry with own same-bare 'helper'."
869 effects []
870
871fn helper(n: Int) -> Int
872 n + 1
873
874fn main() -> Int
875 helper(10) + Worker.walk(20)
876"#,
877 &[(
878 "Worker",
879 r#"
880module Worker
881 exposes [walk]
882 intent = "Worker module with same-bare 'helper'."
883 effects []
884
885fn helper(n: Int) -> Int
886 n + 100
887
888fn walk(n: Int) -> Int
889 helper(n)
890"#,
891 )],
892 "cross_module_helper",
893 );
894
895 let out = transpile(&mut ctx);
896
897 let worker = generated_file(&out, "src/aver_generated/worker/mod.rs");
898 assert!(
899 worker.contains("pub fn helper"),
900 "Worker module must emit its OWN helper:\n{worker}"
901 );
902 assert!(
903 worker.contains("n + 100"),
904 "Worker.helper body must keep its OWN literal (100):\n{worker}"
905 );
906 assert!(
911 worker.contains("crate::aver_generated::worker::helper"),
912 "Worker.walk must call its own helper via the canonical \
913 crate path (not bare-name `helper(n)`):\n{worker}"
914 );
915
916 let entry = generated_rust_entry_file(&out);
917 assert!(
918 entry.contains("pub fn helper"),
919 "Entry module must emit its OWN helper:\n{entry}"
920 );
921 assert!(
922 entry.contains("n + 1i64") || entry.contains("(n + 1i64)"),
923 "Entry.helper body must keep its OWN literal (1):\n{entry}"
924 );
925 assert!(
929 entry.contains("crate::aver_generated::worker::walk"),
930 "main()'s Worker.walk(20) must qualify through the \
931 canonical crate path:\n{entry}"
932 );
933 }
934
935 #[test]
936 fn emission_banner_appears_in_root_main() {
937 let mut ctx = ctx_from_source(
938 r#"
939module Demo
940
941fn main() -> Int
942 1
943"#,
944 "demo",
945 );
946
947 let out = transpile(&mut ctx);
948 let root_main = generated_file(&out, "src/main.rs");
949
950 assert!(root_main.contains("// Aver Rust emission"));
951 }
952
953 #[test]
954 fn generated_module_imports_direct_depends() {
955 let rendered = render_generated_module(
956 vec!["Domain.Types".to_string(), "App.Commands".to_string()],
957 vec!["pub fn demo() {}".to_string()],
958 );
959
960 assert!(rendered.contains("use crate::aver_generated::domain::types::*;"));
961 assert!(rendered.contains("use crate::aver_generated::app::commands::*;"));
962 assert!(rendered.contains("pub fn demo() {}"));
963 }
964
965 #[test]
966 fn module_tree_files_do_not_reexport_children() {
967 let modules = vec![(
968 vec!["app".to_string(), "cli".to_string()],
969 "pub fn run() {}".to_string(),
970 )];
971 let files = synthesize_rust_module_cascade("src/aver_generated", &modules);
972
973 let root_mod = files
974 .iter()
975 .find(|(path, _)| path == "src/aver_generated/mod.rs")
976 .map(|(_, content)| content)
977 .expect("root mod.rs should exist");
978
979 assert!(root_mod.contains("pub mod app;"));
980 assert!(!root_mod.contains("pub use app::*;"));
981 }
982
983 #[test]
984 fn list_cons_match_uses_cloned_uncons_fast_path_when_optimized() {
985 let mut ctx = ctx_from_source(
986 r#"
987module Demo
988
989fn headPlusTailLen(xs: List<Int>) -> Int
990 match xs
991 [] -> 0
992 [h, ..t] -> h + List.len(t)
993"#,
994 "demo",
995 );
996
997 let out = transpile(&mut ctx);
998 let entry = generated_rust_entry_file(&out);
999
1000 assert!(entry.contains("aver_list_match!"));
1002 }
1003
1004 #[test]
1005 fn list_cons_match_stays_structured_in_semantic_mode() {
1006 let mut ctx = ctx_from_source(
1007 r#"
1008module Demo
1009
1010fn headPlusTailLen(xs: List<Int>) -> Int
1011 match xs
1012 [] -> 0
1013 [h, ..t] -> h + List.len(t)
1014"#,
1015 "demo",
1016 );
1017
1018 let out = transpile(&mut ctx);
1019 let entry = generated_rust_entry_file(&out);
1020
1021 assert!(entry.contains("aver_list_match!"));
1023 }
1024
1025 #[test]
1026 fn list_literal_clones_ident_when_used_afterward() {
1027 let mut ctx = ctx_from_source(
1028 r#"
1029module Demo
1030
1031record Audit
1032 message: String
1033
1034fn useTwice(audit: Audit) -> List<Audit>
1035 first = [audit]
1036 [audit]
1037"#,
1038 "demo",
1039 );
1040
1041 let out = transpile(&mut ctx);
1042 let entry = generated_rust_entry_file(&out);
1043
1044 assert!(entry.contains("let first = aver_rt::AverList::from_vec(vec![audit.clone()]);"));
1045 assert!(entry.contains("aver_rt::AverList::from_vec(vec![audit.clone()])"));
1047 }
1048
1049 #[test]
1050 fn record_update_clones_base_when_value_is_used_afterward() {
1051 let mut ctx = ctx_from_source(
1052 r#"
1053module Demo
1054
1055record PaymentState
1056 paymentId: String
1057 currency: String
1058
1059fn touch(state: PaymentState) -> String
1060 updated = PaymentState.update(state, currency = "EUR")
1061 state.paymentId
1062"#,
1063 "demo",
1064 );
1065
1066 let out = transpile(&mut ctx);
1067 let entry = generated_rust_entry_file(&out);
1068
1069 assert!(entry.contains("..state.clone()"));
1070 }
1071
1072 #[test]
1073 fn mutual_tco_generates_trampoline_instead_of_regular_calls() {
1074 let mut ctx = ctx_from_source(
1075 r#"
1076module Demo
1077
1078fn isEven(n: Int) -> Bool
1079 match n == 0
1080 true -> true
1081 false -> isOdd(n - 1)
1082
1083fn isOdd(n: Int) -> Bool
1084 match n == 0
1085 true -> false
1086 false -> isEven(n - 1)
1087"#,
1088 "demo",
1089 );
1090
1091 let out = transpile(&mut ctx);
1092 let entry = generated_rust_entry_file(&out);
1093
1094 assert!(entry.contains("enum __MutualTco1"));
1096 assert!(entry.contains("fn __mutual_tco_trampoline_1"));
1097 assert!(entry.contains("loop {"));
1098
1099 assert!(entry.contains("pub fn isEven"));
1101 assert!(entry.contains("pub fn isOdd"));
1102 assert!(entry.contains("__mutual_tco_trampoline_1("));
1103
1104 assert!(!entry.contains("isOdd((n - 1i64))"));
1106 }
1107
1108 #[test]
1109 fn field_access_does_not_double_clone() {
1110 let mut ctx = ctx_from_source(
1111 r#"
1112module Demo
1113
1114record User
1115 name: String
1116 age: Int
1117
1118fn greet(u: User) -> String
1119 u.name
1120"#,
1121 "demo",
1122 );
1123
1124 let out = transpile(&mut ctx);
1125 let entry = generated_rust_entry_file(&out);
1126
1127 assert!(
1129 !entry.contains(".clone().clone()"),
1130 "double clone detected in generated code:\n{}",
1131 entry
1132 );
1133 }
1134
1135 #[test]
1136 fn borrowed_record_field_return_clones_for_owned_result() {
1137 let mut ctx = ctx_from_source(
1138 r#"
1139module Demo
1140
1141record User
1142 name: String
1143
1144fn getName(user: User) -> String
1145 user.name
1146"#,
1147 "demo",
1148 );
1149
1150 let out = transpile(&mut ctx);
1151 let entry = generated_rust_entry_file(&out);
1152
1153 assert!(entry.contains("pub fn getName(user: &User) -> AverStr"));
1154 assert!(
1155 entry.contains("user.name.clone()"),
1156 "missing owned clone:\n{}",
1157 entry
1158 );
1159 }
1160
1161 #[test]
1162 fn vector_get_with_literal_default_lowers_to_direct_unwrap_or_code() {
1163 let mut ctx = ctx_from_source(
1164 r#"
1165module Demo
1166
1167fn cellAt(grid: Vector<Int>, idx: Int) -> Int
1168 Option.withDefault(Vector.get(grid, idx), 0)
1169"#,
1170 "demo",
1171 );
1172
1173 let out = transpile(&mut ctx);
1174 let entry = generated_rust_entry_file(&out);
1175
1176 assert!(entry.contains("grid.get(idx as usize).cloned().unwrap_or(0i64)"));
1177 }
1178
1179 #[test]
1180 fn vector_set_default_stays_structured_in_semantic_mode() {
1181 let mut ctx = ctx_from_source(
1182 r#"
1183module Demo
1184
1185fn updateOrKeep(vec: Vector<Int>, idx: Int, value: Int) -> Vector<Int>
1186 Option.withDefault(Vector.set(vec, idx, value), vec)
1187"#,
1188 "demo",
1189 );
1190
1191 let out = transpile(&mut ctx);
1192 let entry = generated_rust_entry_file(&out);
1193
1194 assert!(entry.contains("set_unchecked"));
1196 assert!(!entry.contains(".unwrap_or("));
1197 }
1198
1199 #[test]
1200 fn vector_set_default_uses_ir_leaf_fast_path_when_optimized() {
1201 let mut ctx = ctx_from_source(
1202 r#"
1203module Demo
1204
1205fn updateOrKeep(vec: Vector<Int>, idx: Int, value: Int) -> Vector<Int>
1206 Option.withDefault(Vector.set(vec, idx, value), vec)
1207"#,
1208 "demo",
1209 );
1210
1211 let out = transpile(&mut ctx);
1212 let entry = generated_rust_entry_file(&out);
1213
1214 assert!(entry.contains("set_unchecked"));
1215 assert!(!entry.contains(".unwrap_or("));
1216 }
1217
1218 #[test]
1219 fn vector_set_uses_owned_update_lowering() {
1220 let mut ctx = ctx_from_source(
1221 r#"
1222module Demo
1223
1224fn update(vec: Vector<Int>, idx: Int, value: Int) -> Option<Vector<Int>>
1225 Vector.set(vec, idx, value)
1226"#,
1227 "demo",
1228 );
1229
1230 let out = transpile(&mut ctx);
1231 let entry = generated_rust_entry_file(&out);
1232
1233 assert!(entry.contains(".set_owned("));
1234 assert!(!entry.contains(".set(idx as usize,"));
1235 }
1236
1237 #[test]
1238 fn map_remove_uses_owned_update_lowering() {
1239 let mut ctx = ctx_from_source(
1240 r#"
1241module Demo
1242
1243fn dropKey(m: Map<String, Int>, key: String) -> Map<String, Int>
1244 Map.remove(m, key)
1245"#,
1246 "demo",
1247 );
1248
1249 let out = transpile(&mut ctx);
1250 let entry = generated_rust_entry_file(&out);
1251
1252 assert!(entry.contains(".remove_owned(&"));
1253 }
1254
1255 #[test]
1256 fn semantic_keeps_known_leaf_wrapper_call_structured() {
1257 let mut ctx = ctx_from_source(
1258 r#"
1259module Demo
1260
1261fn cellAt(grid: Vector<Int>, idx: Int) -> Int
1262 Option.withDefault(Vector.get(grid, idx), 0)
1263
1264fn read(grid: Vector<Int>, idx: Int) -> Int
1265 cellAt(grid, idx)
1266"#,
1267 "demo",
1268 );
1269
1270 let out = transpile(&mut ctx);
1271 let entry = generated_rust_entry_file(&out);
1272
1273 assert!(entry.contains("cellAt(grid, idx)"));
1274 assert!(!entry.contains("__aver_thin_arg0"));
1275 }
1276
1277 #[test]
1278 fn optimized_keeps_known_leaf_wrapper_callsite_and_leaves_absorption_to_rust() {
1279 let mut ctx = ctx_from_source(
1280 r#"
1281module Demo
1282
1283fn cellAt(grid: Vector<Int>, idx: Int) -> Int
1284 Option.withDefault(Vector.get(grid, idx), 0)
1285
1286fn read(grid: Vector<Int>, idx: Int) -> Int
1287 cellAt(grid, idx)
1288"#,
1289 "demo",
1290 );
1291
1292 let out = transpile(&mut ctx);
1293 let entry = generated_rust_entry_file(&out);
1294
1295 assert!(entry.contains("cellAt(grid, idx)"));
1296 assert!(!entry.contains("__aver_thin_arg0"));
1297 }
1298
1299 #[test]
1300 fn optimized_keeps_known_dispatch_wrapper_callsite_and_leaves_absorption_to_rust() {
1301 let mut ctx = ctx_from_source(
1302 r#"
1303module Demo
1304
1305fn bucket(n: Int) -> Int
1306 match n == 0
1307 true -> 0
1308 false -> 1
1309
1310fn readBucket(n: Int) -> Int
1311 bucket(n)
1312"#,
1313 "demo",
1314 );
1315
1316 let out = transpile(&mut ctx);
1317 let entry = generated_rust_entry_file(&out);
1318
1319 assert!(entry.contains("bucket(n)"));
1320 assert!(!entry.contains("__aver_thin_arg0"));
1321 }
1322
1323 #[test]
1324 fn bool_match_on_gte_normalizes_to_base_comparison_when_optimized() {
1325 let mut ctx = ctx_from_source(
1326 r#"
1327module Demo
1328
1329fn bucket(n: Int) -> Int
1330 match n >= 10
1331 true -> 7
1332 false -> 3
1333"#,
1334 "demo",
1335 );
1336
1337 let out = transpile(&mut ctx);
1338 let entry = generated_rust_entry_file(&out);
1339
1340 assert!(entry.contains("if (n < 10i64) { 3i64 } else { 7i64 }"));
1341 }
1342
1343 #[test]
1344 fn bool_match_stays_as_match_in_semantic_mode() {
1345 let mut ctx = ctx_from_source(
1346 r#"
1347module Demo
1348
1349fn bucket(n: Int) -> Int
1350 match n >= 10
1351 true -> 7
1352 false -> 3
1353"#,
1354 "demo",
1355 );
1356
1357 let out = transpile(&mut ctx);
1358 let entry = generated_rust_entry_file(&out);
1359
1360 assert!(entry.contains("if (n < 10i64) { 3i64 } else { 7i64 }"));
1362 }
1363
1364 #[test]
1365 fn optimized_self_tco_uses_dispatch_table_for_wrapper_match() {
1366 let mut ctx = ctx_from_source(
1367 r#"
1368module Demo
1369
1370fn loop(r: Result<Int, String>) -> Int
1371 match r
1372 Result.Ok(n) -> n
1373 Result.Err(_) -> loop(Result.Ok(1))
1374"#,
1375 "demo",
1376 );
1377
1378 let out = transpile(&mut ctx);
1379 let entry = generated_rust_entry_file(&out);
1380
1381 assert!(entry.contains("match r.clone() {"));
1383 assert!(entry.contains("Ok(n)"));
1384 assert!(!entry.contains("__dispatch_subject"));
1385 }
1386
1387 #[test]
1388 fn optimized_mutual_tco_uses_dispatch_table_for_wrapper_match() {
1389 let mut ctx = ctx_from_source(
1390 r#"
1391module Demo
1392
1393fn left(r: Result<Int, String>) -> Int
1394 match r
1395 Result.Ok(n) -> n
1396 Result.Err(_) -> right(Result.Ok(1))
1397
1398fn right(r: Result<Int, String>) -> Int
1399 match r
1400 Result.Ok(n) -> n
1401 Result.Err(_) -> left(Result.Ok(1))
1402"#,
1403 "demo",
1404 );
1405
1406 let out = transpile(&mut ctx);
1407 let entry = generated_rust_entry_file(&out);
1408
1409 assert!(entry.contains("match r.clone() {"));
1411 assert!(entry.contains("Ok(n)"));
1412 assert!(!entry.contains("__dispatch_subject"));
1413 }
1414
1415 #[test]
1416 fn single_field_variant_display_avoids_vec_join() {
1417 let mut ctx = ctx_from_source(
1418 r#"
1419module Demo
1420
1421type Wrapper
1422 Wrap(Int)
1423 Pair(Int, Int)
1424 Empty
1425"#,
1426 "demo",
1427 );
1428
1429 let out = transpile(&mut ctx);
1430 let entry = generated_rust_entry_file(&out);
1431
1432 assert!(
1434 !entry.contains("vec![f0.aver_display_inner()].join"),
1435 "single-field variant should use direct format, not vec join:\n{}",
1436 entry
1437 );
1438 assert!(
1440 entry.contains("vec![f0.aver_display_inner(), f1.aver_display_inner()].join(\", \")"),
1441 "multi-field variant should use vec join:\n{}",
1442 entry
1443 );
1444 }
1445
1446 #[test]
1447 fn replay_codegen_wraps_guest_entry_in_scoped_runtime() {
1448 let mut ctx = ctx_from_source(
1449 r#"
1450module Demo
1451
1452fn runGuestProgram(path: String) -> Result<String, String>
1453 ! [Disk.readText]
1454 Disk.readText(path)
1455"#,
1456 "demo",
1457 );
1458 ctx.emit_replay_runtime = true;
1459 ctx.guest_entry = Some("runGuestProgram".to_string());
1460
1461 let out = transpile(&mut ctx);
1462 let entry = generated_rust_entry_file(&out);
1463 let replay_support = generated_file(&out, "src/replay_support.rs");
1464 let cargo_toml = generated_file(&out, "Cargo.toml");
1465
1466 assert!(entry.contains("aver_replay::with_guest_scope_result(\"runGuestProgram\""));
1467 assert!(replay_support.contains("pub mod aver_replay"));
1468 assert!(cargo_toml.contains("serde_json = \"1\""));
1469 }
1470
1471 #[test]
1472 fn replay_codegen_uses_guest_args_param_override_when_present() {
1473 let mut ctx = ctx_from_source(
1474 r#"
1475module Demo
1476
1477fn runGuestProgram(path: String, guestArgs: List<String>) -> Result<String, String>
1478 ! [Args.get]
1479 Result.Ok(String.join(Args.get(), ","))
1480"#,
1481 "demo",
1482 );
1483 ctx.emit_replay_runtime = true;
1484 ctx.guest_entry = Some("runGuestProgram".to_string());
1485
1486 let out = transpile(&mut ctx);
1487 let entry = generated_rust_entry_file(&out);
1488 let cargo_toml = generated_file(&out, "Cargo.toml");
1489
1490 assert!(entry.contains("aver_replay::with_guest_scope_args_result(\"runGuestProgram\""));
1491 assert!(entry.contains("guestArgs.clone()"));
1492 assert!(cargo_toml.contains("edition = \"2024\""));
1493 }
1494
1495 #[test]
1496 fn replay_codegen_wraps_root_main_when_no_guest_entry_is_set() {
1497 let mut ctx = ctx_from_source(
1498 r#"
1499module Demo
1500
1501fn main() -> Result<String, String>
1502 ! [Disk.readText]
1503 Disk.readText("demo.av")
1504"#,
1505 "demo",
1506 );
1507 ctx.emit_replay_runtime = true;
1508
1509 let out = transpile(&mut ctx);
1510 let root_main = generated_file(&out, "src/main.rs");
1511
1512 assert!(
1513 root_main.contains("aver_replay::with_guest_scope(\"main\", serde_json::Value::Null")
1514 );
1515 }
1516
1517 #[test]
1518 fn runtime_policy_codegen_uses_runtime_loader() {
1519 let mut ctx = ctx_from_source(
1520 r#"
1521module Demo
1522
1523fn main() -> Result<String, String>
1524 ! [Disk.readText]
1525 Disk.readText("demo.av")
1526"#,
1527 "demo",
1528 );
1529 ctx.emit_replay_runtime = true;
1530 ctx.runtime_policy_from_env = true;
1531
1532 let out = transpile(&mut ctx);
1533 let root_main = generated_file(&out, "src/main.rs");
1534 let replay_support = generated_file(&out, "src/replay_support.rs");
1535 let cargo_toml = generated_file(&out, "Cargo.toml");
1536
1537 assert!(!root_main.contains("mod policy_support;"));
1538 assert!(replay_support.contains("load_runtime_policy_from_env"));
1539 assert!(cargo_toml.contains("url = \"2\""));
1540 assert!(cargo_toml.contains("toml = \"0.8\""));
1541 }
1542
1543 #[test]
1544 fn replay_codegen_can_keep_embedded_policy_when_requested() {
1545 let mut ctx = ctx_from_source(
1546 r#"
1547module Demo
1548
1549fn main() -> Result<String, String>
1550 ! [Disk.readText]
1551 Disk.readText("demo.av")
1552"#,
1553 "demo",
1554 );
1555 ctx.emit_replay_runtime = true;
1556 ctx.policy = Some(crate::config::ProjectConfig {
1557 effect_policies: std::collections::HashMap::new(),
1558 check_suppressions: Vec::new(),
1559 independence_mode: crate::config::IndependenceMode::default(),
1560 });
1561
1562 let out = transpile(&mut ctx);
1563 let root_main = generated_file(&out, "src/main.rs");
1564 let replay_support = generated_file(&out, "src/replay_support.rs");
1565
1566 assert!(root_main.contains("mod policy_support;"));
1567 assert!(replay_support.contains("aver_policy::check_disk"));
1568 assert!(!replay_support.contains("RuntimeEffectPolicy"));
1569 }
1570
1571 #[test]
1572 fn self_host_support_is_emitted_as_separate_module() {
1573 let mut ctx = ctx_from_source(
1574 r#"
1575module Demo
1576
1577fn runGuestProgram(prog: Int, moduleFns: Int) -> Result<String, String>
1578 Result.Ok("ok")
1579"#,
1580 "demo",
1581 );
1582 ctx.emit_self_host_support = true;
1583 ctx.guest_entry = Some("runGuestProgram".to_string());
1584
1585 let out = transpile(&mut ctx);
1586 let root_main = generated_file(&out, "src/main.rs");
1587 let runtime_support = generated_file(&out, "src/runtime_support.rs");
1588 let self_host_support = generated_file(&out, "src/self_host_support.rs");
1589 let entry = generated_rust_entry_file(&out);
1590
1591 assert!(root_main.contains("mod self_host_support;"));
1592 assert!(!runtime_support.contains("with_fn_store"));
1593 assert!(self_host_support.contains("pub fn with_program_fn_store"));
1594 assert!(entry.contains("crate::self_host_support::with_program_fn_store("));
1595 }
1596
1597 #[test]
1598 fn independent_product_codegen_avoids_string_specific_error_type() {
1599 let mut ctx = ctx_from_source(
1600 r#"
1601module Demo
1602
1603fn left() -> Result<Int, Int>
1604 Result.Ok(1)
1605
1606fn right() -> Result<Int, Int>
1607 Result.Ok(2)
1608
1609fn main() -> Result<Tuple<Int, Int>, Int>
1610 data = (left(), right())?!
1611 Result.Ok(data)
1612"#,
1613 "demo",
1614 );
1615 ctx.emit_replay_runtime = true;
1616
1617 let out = transpile(&mut ctx);
1618 let entry = generated_rust_entry_file(&out);
1619
1620 assert!(!entry.contains("Ok::<_, aver_rt::AverStr>"));
1621 assert!(entry.contains("crate::aver_replay::exit_effect_group();"));
1622 assert!(entry.contains("match (_r0, _r1)"));
1623 assert!(!entry.contains("let _r0 = left()?;"));
1624 }
1625
1626 #[test]
1627 fn independent_product_codegen_emits_cancel_runtime_and_scope_propagation() {
1628 let mut ctx = ctx_from_source(
1629 r#"
1630module Demo
1631
1632fn left() -> Result<Int, String>
1633 Result.Ok(1)
1634
1635fn right() -> Result<Int, String>
1636 Result.Ok(2)
1637
1638fn main() -> Result<Tuple<Int, Int>, String>
1639 data = (left(), right())?!
1640 Result.Ok(data)
1641"#,
1642 "demo",
1643 );
1644 ctx.emit_replay_runtime = true;
1645 ctx.policy = Some(crate::config::ProjectConfig {
1646 effect_policies: std::collections::HashMap::new(),
1647 check_suppressions: Vec::new(),
1648 independence_mode: crate::config::IndependenceMode::Cancel,
1649 });
1650
1651 let out = transpile(&mut ctx);
1652 let entry = generated_rust_entry_file(&out);
1653 let runtime_support = generated_file(&out, "src/runtime_support.rs");
1654 let replay_support = generated_file(&out, "src/replay_support.rs");
1655
1656 assert!(entry.contains("crate::run_cancelable_branch"));
1657 assert!(entry.contains("capture_parallel_scope_context"));
1658 assert!(entry.contains("_s.spawn(move ||"));
1659 assert!(runtime_support.contains("pub fn run_cancelable_branch"));
1660 assert!(runtime_support.contains("panic_any(AverCancelled)"));
1661 assert!(replay_support.contains("pub fn capture_parallel_scope_context"));
1662 assert!(replay_support.contains("pub fn independence_mode_is_cancel()"));
1663 }
1664
1665 #[test]
1666 fn runtime_policy_codegen_parses_independence_mode() {
1667 let mut ctx = ctx_from_source(
1668 r#"
1669module Demo
1670
1671fn main() -> Result<String, String>
1672 ! [Disk.readText]
1673 Disk.readText("demo.av")
1674"#,
1675 "demo",
1676 );
1677 ctx.emit_replay_runtime = true;
1678 ctx.runtime_policy_from_env = true;
1679
1680 let out = transpile(&mut ctx);
1681 let replay_support = generated_file(&out, "src/replay_support.rs");
1682
1683 assert!(replay_support.contains("independence_mode_cancel"));
1684 assert!(replay_support.contains("[independence].mode must be 'complete' or 'cancel'"));
1685 }
1686
1687 #[test]
1688 fn effectful_codegen_inserts_cancel_checkpoint_before_builtin_calls() {
1689 let mut ctx = ctx_from_source(
1690 r#"
1691module Demo
1692
1693fn main() -> Result<String, String>
1694 ! [Disk.readText]
1695 Disk.readText("demo.av")
1696"#,
1697 "demo",
1698 );
1699
1700 let out = transpile(&mut ctx);
1701 let entry = generated_rust_entry_file(&out);
1702
1703 assert!(entry.contains("crate::cancel_checkpoint(); (aver_rt::read_text"));
1704 }
1705
1706 #[test]
1707 fn replay_support_matches_group_effects_by_occurrence_and_args() {
1708 let mut ctx = ctx_from_source(
1709 r#"
1710module Demo
1711
1712fn left() -> Result<Int, String>
1713 Result.Ok(1)
1714
1715fn right() -> Result<Int, String>
1716 Result.Ok(2)
1717
1718fn main() -> Result<Tuple<Int, Int>, String>
1719 data = (left(), right())?!
1720 Result.Ok(data)
1721"#,
1722 "demo",
1723 );
1724 ctx.emit_replay_runtime = true;
1725
1726 let out = transpile(&mut ctx);
1727 let replay_support = generated_file(&out, "src/replay_support.rs");
1728
1729 assert!(replay_support.contains("candidate.effect_occurrence"));
1730 assert!(replay_support.contains("candidate.args != args"));
1731 }
1732}