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, HashSet};
18
19use crate::ast::{FnDef, TopLevel, TypeDef};
20use crate::codegen::common::module_prefix_to_rust_segments;
21use crate::codegen::{CodegenContext, ProjectOutput};
22use crate::types::Type;
23
24#[derive(Default)]
25struct ModuleTreeNode {
26 content: Option<String>,
27 children: BTreeMap<String, ModuleTreeNode>,
28}
29
30pub fn transpile(ctx: &mut CodegenContext) -> ProjectOutput {
32 let has_embedded_policy = ctx.policy.is_some();
33 let has_runtime_policy = ctx.runtime_policy_from_env;
34 let embedded_independence_cancel = ctx
35 .policy
36 .as_ref()
37 .is_some_and(|config| config.independence_mode == crate::config::IndependenceMode::Cancel);
38 let used_services = detect_used_services(ctx);
39 let needs_http_types = needs_named_type(ctx, "Header")
40 || needs_named_type(ctx, "HttpResponse")
41 || needs_named_type(ctx, "HttpRequest");
42 let needs_tcp_types = needs_named_type(ctx, "Tcp.Connection");
43 let needs_terminal_types = needs_named_type(ctx, "Terminal.Size");
44
45 let has_tcp_runtime = used_services.contains("Tcp");
46 let has_http_runtime = used_services.contains("Http");
47 let has_http_server_runtime = used_services.contains("HttpServer");
48 let has_terminal_runtime = used_services.contains("Terminal");
49
50 let has_tcp_types = has_tcp_runtime || needs_tcp_types;
51 let has_http_types = has_http_runtime || has_http_server_runtime || needs_http_types;
52 let has_http_server_types = has_http_server_runtime || needs_named_type(ctx, "HttpRequest");
53 let has_terminal_types = has_terminal_runtime || needs_terminal_types;
54
55 let main_fn = ctx.fn_defs.iter().find(|fd| fd.name == "main");
56 let top_level_stmts: Vec<_> = ctx
57 .items
58 .iter()
59 .filter_map(|item| {
60 if let TopLevel::Stmt(stmt) = item {
61 Some(stmt)
62 } else {
63 None
64 }
65 })
66 .collect();
67 let verify_blocks: Vec<_> = ctx
68 .items
69 .iter()
70 .filter_map(|item| {
71 if let TopLevel::Verify(vb) = item {
72 Some(vb)
73 } else {
74 None
75 }
76 })
77 .collect();
78
79 let mut files = vec![
80 (
81 "Cargo.toml".to_string(),
82 project::generate_cargo_toml(
83 &ctx.project_name,
84 &used_services,
85 has_embedded_policy,
86 has_runtime_policy,
87 ctx.emit_replay_runtime,
88 ),
89 ),
90 (
91 "src/main.rs".to_string(),
92 render_root_main(
93 main_fn,
94 has_embedded_policy,
95 ctx.emit_replay_runtime,
96 ctx.guest_entry.as_deref(),
97 !verify_blocks.is_empty(),
98 ctx.emit_self_host_support,
99 ),
100 ),
101 (
102 "src/runtime_support.rs".to_string(),
103 render_runtime_support(
104 has_tcp_types,
105 has_http_types,
106 has_http_server_types,
107 ctx.emit_replay_runtime,
108 embedded_independence_cancel,
109 ),
110 ),
111 ];
112
113 if ctx.emit_self_host_support {
114 files.push((
115 "src/self_host_support.rs".to_string(),
116 self_host::generate_self_host_support(),
117 ));
118 }
119
120 if has_embedded_policy && let Some(config) = &ctx.policy {
121 files.push((
122 "src/policy_support.rs".to_string(),
123 format!("{}\n", policy::generate_policy_runtime(config)),
124 ));
125 }
126
127 if ctx.emit_replay_runtime {
128 files.push((
129 "src/replay_support.rs".to_string(),
130 replay::generate_replay_runtime(
131 has_embedded_policy,
132 has_runtime_policy,
133 has_terminal_types,
134 has_tcp_types,
135 has_http_types,
136 has_http_server_types,
137 embedded_independence_cancel,
138 ),
139 ));
140 }
141
142 if !verify_blocks.is_empty() {
143 files.push((
144 "src/verify.rs".to_string(),
145 render_verify_module(&verify_blocks, ctx),
146 ));
147 }
148
149 let mut module_tree = ModuleTreeNode::default();
150 insert_module_content(
151 &mut module_tree,
152 &[String::from("entry")],
153 render_generated_module(
154 root_module_depends(&ctx.items),
155 entry_module_sections(ctx, main_fn, &top_level_stmts),
156 ),
157 );
158
159 for i in 0..ctx.modules.len() {
160 ctx.extra_fn_defs = ctx.modules[i].fn_defs.clone();
163 let module = &ctx.modules[i];
164 let path = module_prefix_to_rust_segments(&module.prefix);
165 insert_module_content(
166 &mut module_tree,
167 &path,
168 render_generated_module(module.depends.clone(), module_sections(module, ctx)),
169 );
170 }
171 ctx.extra_fn_defs.clear();
172
173 emit_module_tree_files(&module_tree, "src/aver_generated", &mut files);
174 files.sort_by(|left, right| left.0.cmp(&right.0));
175
176 ProjectOutput { files }
177}
178
179fn render_root_main(
180 main_fn: Option<&FnDef>,
181 has_policy: bool,
182 has_replay: bool,
183 guest_entry: Option<&str>,
184 has_verify: bool,
185 has_self_host_support: bool,
186) -> String {
187 let mut sections = vec![
188 "#![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(),
189 "// Aver Rust emission".to_string(),
190 "#[macro_use] extern crate aver_rt;".to_string(),
191 "pub use ::aver_rt::AverMap as HashMap;".to_string(),
192 "pub use ::aver_rt::AverStr;".to_string(),
193 String::new(),
194 "mod runtime_support;".to_string(),
195 "pub use runtime_support::*;".to_string(),
196 ];
197
198 if has_policy {
199 sections.push(String::new());
200 sections.push("mod policy_support;".to_string());
201 sections.push("pub use policy_support::*;".to_string());
202 }
203
204 if has_replay {
205 sections.push(String::new());
206 sections.push("mod replay_support;".to_string());
207 sections.push("pub use replay_support::*;".to_string());
208 }
209
210 if has_self_host_support {
211 sections.push(String::new());
212 sections.push("mod self_host_support;".to_string());
213 }
214
215 sections.push(String::new());
216 sections.push("pub mod aver_generated;".to_string());
217
218 if has_verify {
219 sections.push(String::new());
220 sections.push("#[cfg(test)]".to_string());
221 sections.push("mod verify;".to_string());
222 }
223
224 sections.push(String::new());
226 let returns_result = main_fn.is_some_and(|fd| fd.return_type.starts_with("Result<"));
227 let result_unit_string =
228 main_fn.is_some_and(|fd| fd.return_type.replace(' ', "") == "Result<Unit,String>");
229 if returns_result {
230 if result_unit_string {
231 sections.push("fn main() {".to_string());
232 sections.push(" let child = std::thread::Builder::new()".to_string());
233 sections.push(" .stack_size(256 * 1024 * 1024)".to_string());
234 if has_replay && guest_entry.is_none() {
235 sections.push(" .spawn(|| {".to_string());
236 sections.push(" let __result = aver_replay::with_guest_scope(\"main\", serde_json::Value::Null, aver_generated::entry::main);".to_string());
237 sections.push(" __result.map_err(|e| e.to_string())".to_string());
238 sections.push(" })".to_string());
239 } else {
240 sections.push(" .spawn(|| {".to_string());
241 sections
242 .push(" let __result = aver_generated::entry::main();".to_string());
243 sections.push(" __result.map_err(|e| e.to_string())".to_string());
244 sections.push(" })".to_string());
245 }
246 sections.push(" .expect(\"thread spawn\");".to_string());
247 sections.push(" match child.join().expect(\"thread join\") {".to_string());
248 sections.push(" Ok(()) => {}".to_string());
249 sections.push(" Err(e) => {".to_string());
250 sections.push(" eprintln!(\"{}\", e);".to_string());
251 sections.push(" std::process::exit(1);".to_string());
252 sections.push(" }".to_string());
253 sections.push(" }".to_string());
254 } else {
255 let ret_type = types::type_annotation_to_rust(&main_fn.unwrap().return_type);
256 sections.push(format!("fn main() -> {} {{", ret_type));
257 if has_replay && guest_entry.is_none() {
258 sections.push(
259 " aver_replay::with_guest_scope(\"main\", serde_json::Value::Null, aver_generated::entry::main)"
260 .to_string(),
261 );
262 } else {
263 sections.push(" aver_generated::entry::main()".to_string());
264 }
265 }
266 } else {
267 sections.push("fn main() {".to_string());
268 if main_fn.is_some() {
269 sections.push(" let child = std::thread::Builder::new()".to_string());
270 sections.push(" .stack_size(256 * 1024 * 1024)".to_string());
271 if has_replay && guest_entry.is_none() {
272 sections.push(" .spawn(|| aver_replay::with_guest_scope(\"main\", serde_json::Value::Null, || aver_generated::entry::main()))".to_string());
273 } else {
274 sections.push(" .spawn(|| aver_generated::entry::main())".to_string());
275 }
276 sections.push(" .expect(\"thread spawn\");".to_string());
277 sections.push(" child.join().expect(\"thread join\");".to_string());
278 }
279 }
280 sections.push("}".to_string());
281 sections.push(String::new());
282
283 sections.join("\n")
284}
285
286fn render_runtime_support(
287 has_tcp_types: bool,
288 has_http_types: bool,
289 has_http_server_types: bool,
290 has_replay: bool,
291 embedded_independence_cancel: bool,
292) -> String {
293 let mut sections = vec![runtime::generate_runtime(
294 has_replay,
295 has_http_server_types,
296 embedded_independence_cancel,
297 )];
298 if has_tcp_types {
299 sections.push(runtime::generate_tcp_types());
300 }
301 if has_http_types {
302 sections.push(runtime::generate_http_types());
303 }
304 if has_http_server_types {
305 sections.push(runtime::generate_http_server_types());
306 }
307 format!("{}\n", sections.join("\n\n"))
308}
309
310fn render_verify_module(
311 verify_blocks: &[&crate::ast::VerifyBlock],
312 ctx: &CodegenContext,
313) -> String {
314 [
315 "#[allow(unused_imports)]".to_string(),
316 "use crate::*;".to_string(),
317 "#[allow(unused_imports)]".to_string(),
318 "use crate::aver_generated::entry::*;".to_string(),
319 String::new(),
320 toplevel::emit_verify_blocks(verify_blocks, ctx),
321 String::new(),
322 ]
323 .join("\n")
324}
325
326fn render_generated_module(depends: Vec<String>, sections: Vec<String>) -> String {
327 if sections.is_empty() {
328 String::new()
329 } else {
330 let mut lines = vec![
331 "#[allow(unused_imports)]".to_string(),
332 "use crate::*;".to_string(),
333 ];
334 for dep in depends {
335 let path = module_prefix_to_rust_segments(&dep).join("::");
336 lines.push("#[allow(unused_imports)]".to_string());
337 lines.push(format!("use crate::aver_generated::{}::*;", path));
338 }
339 lines.push(String::new());
340 lines.push(sections.join("\n\n"));
341 lines.push(String::new());
342 lines.join("\n")
343 }
344}
345
346fn entry_module_sections(
347 ctx: &CodegenContext,
348 main_fn: Option<&FnDef>,
349 top_level_stmts: &[&crate::ast::Stmt],
350) -> Vec<String> {
351 let mut sections = Vec::new();
352
353 for td in &ctx.type_defs {
354 if is_shared_runtime_type(td) {
355 continue;
356 }
357 sections.push(toplevel::emit_public_type_def(td, ctx));
358 if ctx.emit_replay_runtime {
359 sections.push(replay::emit_replay_value_impl(td));
360 }
361 }
362
363 let non_main_fns: Vec<&FnDef> = ctx.fn_defs.iter().filter(|fd| fd.name != "main").collect();
365 let mutual_groups = toplevel::find_mutual_tco_groups(&non_main_fns);
366 let mut mutual_tco_members: HashSet<String> = HashSet::new();
367
368 for (group_id, group_indices) in mutual_groups.iter().enumerate() {
369 let group_fns: Vec<&FnDef> = group_indices.iter().map(|&idx| non_main_fns[idx]).collect();
370 for fd in &group_fns {
371 mutual_tco_members.insert(fd.name.clone());
372 }
373 sections.push(toplevel::emit_mutual_tco_block(
374 group_id + 1,
375 &group_fns,
376 ctx,
377 "pub ",
378 ));
379 }
380
381 for fd in &ctx.fn_defs {
382 if fd.name == "main" || mutual_tco_members.contains(&fd.name) {
383 continue;
384 }
385 let is_memo = ctx.memo_fns.contains(&fd.name);
386 sections.push(toplevel::emit_public_fn_def(fd, is_memo, ctx));
387 }
388
389 if main_fn.is_some() || !top_level_stmts.is_empty() {
390 sections.push(toplevel::emit_public_main(main_fn, top_level_stmts, ctx));
391 }
392
393 sections
394}
395
396fn module_sections(module: &crate::codegen::ModuleInfo, ctx: &CodegenContext) -> Vec<String> {
397 let mut sections = Vec::new();
398
399 for td in &module.type_defs {
400 if is_shared_runtime_type(td) {
401 continue;
402 }
403 sections.push(toplevel::emit_public_type_def(td, ctx));
404 if ctx.emit_replay_runtime {
405 sections.push(replay::emit_replay_value_impl(td));
406 }
407 }
408
409 let fn_refs: Vec<&FnDef> = module.fn_defs.iter().collect();
411 let mutual_groups = toplevel::find_mutual_tco_groups(&fn_refs);
412 let mut mutual_tco_members: HashSet<String> = HashSet::new();
413
414 for (group_id, group_indices) in mutual_groups.iter().enumerate() {
415 let group_fns: Vec<&FnDef> = group_indices.iter().map(|&idx| fn_refs[idx]).collect();
416 for fd in &group_fns {
417 mutual_tco_members.insert(fd.name.clone());
418 }
419 sections.push(toplevel::emit_mutual_tco_block(
420 group_id + 1,
421 &group_fns,
422 ctx,
423 "pub ",
424 ));
425 }
426
427 for fd in &module.fn_defs {
428 if mutual_tco_members.contains(&fd.name) {
429 continue;
430 }
431 let is_memo = ctx.memo_fns.contains(&fd.name);
432 sections.push(toplevel::emit_public_fn_def(fd, is_memo, ctx));
433 }
434
435 sections
436}
437
438fn insert_module_content(node: &mut ModuleTreeNode, segments: &[String], content: String) {
439 let child = node.children.entry(segments[0].clone()).or_default();
440 if segments.len() == 1 {
441 child.content = Some(content);
442 } else {
443 insert_module_content(child, &segments[1..], content);
444 }
445}
446
447fn emit_module_tree_files(node: &ModuleTreeNode, rel_dir: &str, files: &mut Vec<(String, String)>) {
448 let mut parts = Vec::new();
449
450 if let Some(content) = &node.content
451 && !content.trim().is_empty()
452 {
453 parts.push(content.trim_end().to_string());
454 }
455
456 for child_name in node.children.keys() {
457 parts.push(format!("pub mod {};", child_name));
458 }
459
460 let mut mod_rs = parts.join("\n\n");
461 if !mod_rs.is_empty() {
462 mod_rs.push('\n');
463 }
464 files.push((format!("{}/mod.rs", rel_dir), mod_rs));
465
466 for (child_name, child) in &node.children {
467 emit_module_tree_files(child, &format!("{}/{}", rel_dir, child_name), files);
468 }
469}
470
471fn root_module_depends(items: &[TopLevel]) -> Vec<String> {
472 items
473 .iter()
474 .find_map(|item| {
475 if let TopLevel::Module(module) = item {
476 Some(module.depends.clone())
477 } else {
478 None
479 }
480 })
481 .unwrap_or_default()
482}
483
484fn detect_used_services(ctx: &CodegenContext) -> HashSet<String> {
486 let mut services = HashSet::new();
487 for item in &ctx.items {
488 if let TopLevel::FnDef(fd) = item {
489 for eff in &fd.effects {
490 services.insert(eff.node.clone());
491 if let Some((service, _)) = eff.node.split_once('.') {
492 services.insert(service.to_string());
493 }
494 }
495 }
496 }
497 for module in &ctx.modules {
498 for fd in &module.fn_defs {
499 for eff in &fd.effects {
500 services.insert(eff.node.clone());
501 if let Some((service, _)) = eff.node.split_once('.') {
502 services.insert(service.to_string());
503 }
504 }
505 }
506 }
507 services
508}
509
510fn is_shared_runtime_type(td: &TypeDef) -> bool {
511 matches!(
512 td,
513 TypeDef::Product { name, .. }
514 if matches!(name.as_str(), "Header" | "HttpResponse" | "HttpRequest")
515 )
516}
517
518fn needs_named_type(ctx: &CodegenContext, wanted: &str) -> bool {
519 ctx.fn_sigs.values().any(|(params, ret, _effects)| {
520 params.iter().any(|p| type_contains_named(p, wanted)) || type_contains_named(ret, wanted)
521 })
522}
523
524fn type_contains_named(ty: &Type, wanted: &str) -> bool {
525 match ty {
526 Type::Named(name) => name == wanted,
527 Type::Result(ok, err) => {
528 type_contains_named(ok, wanted) || type_contains_named(err, wanted)
529 }
530 Type::Option(inner) | Type::List(inner) | Type::Vector(inner) => {
531 type_contains_named(inner, wanted)
532 }
533 Type::Tuple(items) => items.iter().any(|t| type_contains_named(t, wanted)),
534 Type::Map(k, v) => type_contains_named(k, wanted) || type_contains_named(v, wanted),
535 Type::Fn(params, ret, _effects) => {
536 params.iter().any(|t| type_contains_named(t, wanted))
537 || type_contains_named(ret, wanted)
538 }
539 Type::Int | Type::Float | Type::Str | Type::Bool | Type::Unit | Type::Unknown => false,
540 }
541}
542
543#[cfg(test)]
544mod tests {
545 use super::{
546 ModuleTreeNode, emit_module_tree_files, insert_module_content, render_generated_module,
547 transpile,
548 };
549 use crate::codegen::build_context;
550 use crate::source::parse_source;
551 use crate::tco;
552 use crate::types::checker::run_type_check_full;
553 use std::collections::HashSet;
554
555 fn ctx_from_source(source: &str, project_name: &str) -> crate::codegen::CodegenContext {
556 let mut items = parse_source(source).expect("source should parse");
557 tco::transform_program(&mut items);
558 let tc = run_type_check_full(&items, None);
559 assert!(
560 tc.errors.is_empty(),
561 "source should typecheck without errors: {:?}",
562 tc.errors
563 );
564 build_context(items, &tc, HashSet::new(), project_name.to_string(), vec![])
565 }
566
567 fn generated_rust_entry_file(out: &crate::codegen::ProjectOutput) -> &str {
568 out.files
569 .iter()
570 .find_map(|(name, content)| {
571 (name == "src/aver_generated/entry/mod.rs").then_some(content.as_str())
572 })
573 .expect("expected generated Rust entry module")
574 }
575
576 fn generated_file<'a>(out: &'a crate::codegen::ProjectOutput, path: &str) -> &'a str {
577 out.files
578 .iter()
579 .find_map(|(name, content)| (name == path).then_some(content.as_str()))
580 .unwrap_or_else(|| panic!("expected generated file '{}'", path))
581 }
582
583 #[test]
584 fn emission_banner_appears_in_root_main() {
585 let mut ctx = ctx_from_source(
586 r#"
587module Demo
588
589fn main() -> Int
590 1
591"#,
592 "demo",
593 );
594
595 let out = transpile(&mut ctx);
596 let root_main = generated_file(&out, "src/main.rs");
597
598 assert!(root_main.contains("// Aver Rust emission"));
599 }
600
601 #[test]
602 fn generated_module_imports_direct_depends() {
603 let rendered = render_generated_module(
604 vec!["Domain.Types".to_string(), "App.Commands".to_string()],
605 vec!["pub fn demo() {}".to_string()],
606 );
607
608 assert!(rendered.contains("use crate::aver_generated::domain::types::*;"));
609 assert!(rendered.contains("use crate::aver_generated::app::commands::*;"));
610 assert!(rendered.contains("pub fn demo() {}"));
611 }
612
613 #[test]
614 fn module_tree_files_do_not_reexport_children() {
615 let mut tree = ModuleTreeNode::default();
616 insert_module_content(
617 &mut tree,
618 &["app".to_string(), "cli".to_string()],
619 "pub fn run() {}".to_string(),
620 );
621
622 let mut files = Vec::new();
623 emit_module_tree_files(&tree, "src/aver_generated", &mut files);
624
625 let root_mod = files
626 .iter()
627 .find(|(path, _)| path == "src/aver_generated/mod.rs")
628 .map(|(_, content)| content)
629 .expect("root mod.rs should exist");
630
631 assert!(root_mod.contains("pub mod app;"));
632 assert!(!root_mod.contains("pub use app::*;"));
633 }
634
635 #[test]
636 fn list_cons_match_uses_cloned_uncons_fast_path_when_optimized() {
637 let mut ctx = ctx_from_source(
638 r#"
639module Demo
640
641fn headPlusTailLen(xs: List<Int>) -> Int
642 match xs
643 [] -> 0
644 [h, ..t] -> h + List.len(t)
645"#,
646 "demo",
647 );
648
649 let out = transpile(&mut ctx);
650 let entry = generated_rust_entry_file(&out);
651
652 assert!(entry.contains("aver_list_match!"));
654 }
655
656 #[test]
657 fn list_cons_match_stays_structured_in_semantic_mode() {
658 let mut ctx = ctx_from_source(
659 r#"
660module Demo
661
662fn headPlusTailLen(xs: List<Int>) -> Int
663 match xs
664 [] -> 0
665 [h, ..t] -> h + List.len(t)
666"#,
667 "demo",
668 );
669
670 let out = transpile(&mut ctx);
671 let entry = generated_rust_entry_file(&out);
672
673 assert!(entry.contains("aver_list_match!"));
675 }
676
677 #[test]
678 fn list_literal_clones_ident_when_used_afterward() {
679 let mut ctx = ctx_from_source(
680 r#"
681module Demo
682
683record Audit
684 message: String
685
686fn useTwice(audit: Audit) -> List<Audit>
687 first = [audit]
688 [audit]
689"#,
690 "demo",
691 );
692
693 let out = transpile(&mut ctx);
694 let entry = generated_rust_entry_file(&out);
695
696 assert!(entry.contains("let first = aver_rt::AverList::from_vec(vec![audit.clone()]);"));
697 assert!(entry.contains("aver_rt::AverList::from_vec(vec![audit.clone()])"));
699 }
700
701 #[test]
702 fn record_update_clones_base_when_value_is_used_afterward() {
703 let mut ctx = ctx_from_source(
704 r#"
705module Demo
706
707record PaymentState
708 paymentId: String
709 currency: String
710
711fn touch(state: PaymentState) -> String
712 updated = PaymentState.update(state, currency = "EUR")
713 state.paymentId
714"#,
715 "demo",
716 );
717
718 let out = transpile(&mut ctx);
719 let entry = generated_rust_entry_file(&out);
720
721 assert!(entry.contains("..state.clone()"));
722 }
723
724 #[test]
725 fn mutual_tco_generates_trampoline_instead_of_regular_calls() {
726 let mut ctx = ctx_from_source(
727 r#"
728module Demo
729
730fn isEven(n: Int) -> Bool
731 match n == 0
732 true -> true
733 false -> isOdd(n - 1)
734
735fn isOdd(n: Int) -> Bool
736 match n == 0
737 true -> false
738 false -> isEven(n - 1)
739"#,
740 "demo",
741 );
742
743 let out = transpile(&mut ctx);
744 let entry = generated_rust_entry_file(&out);
745
746 assert!(entry.contains("enum __MutualTco1"));
748 assert!(entry.contains("fn __mutual_tco_trampoline_1"));
749 assert!(entry.contains("loop {"));
750
751 assert!(entry.contains("pub fn isEven"));
753 assert!(entry.contains("pub fn isOdd"));
754 assert!(entry.contains("__mutual_tco_trampoline_1("));
755
756 assert!(!entry.contains("isOdd((n - 1i64))"));
758 }
759
760 #[test]
761 fn field_access_does_not_double_clone() {
762 let mut ctx = ctx_from_source(
763 r#"
764module Demo
765
766record User
767 name: String
768 age: Int
769
770fn greet(u: User) -> String
771 u.name
772"#,
773 "demo",
774 );
775
776 let out = transpile(&mut ctx);
777 let entry = generated_rust_entry_file(&out);
778
779 assert!(
781 !entry.contains(".clone().clone()"),
782 "double clone detected in generated code:\n{}",
783 entry
784 );
785 }
786
787 #[test]
788 fn borrowed_record_field_return_clones_for_owned_result() {
789 let mut ctx = ctx_from_source(
790 r#"
791module Demo
792
793record User
794 name: String
795
796fn getName(user: User) -> String
797 user.name
798"#,
799 "demo",
800 );
801
802 let out = transpile(&mut ctx);
803 let entry = generated_rust_entry_file(&out);
804
805 assert!(entry.contains("pub fn getName(user: &User) -> AverStr"));
806 assert!(
807 entry.contains("user.name.clone()"),
808 "missing owned clone:\n{}",
809 entry
810 );
811 }
812
813 #[test]
814 fn vector_get_with_literal_default_lowers_to_direct_unwrap_or_code() {
815 let mut ctx = ctx_from_source(
816 r#"
817module Demo
818
819fn cellAt(grid: Vector<Int>, idx: Int) -> Int
820 Option.withDefault(Vector.get(grid, idx), 0)
821"#,
822 "demo",
823 );
824
825 let out = transpile(&mut ctx);
826 let entry = generated_rust_entry_file(&out);
827
828 assert!(entry.contains("grid.get(idx as usize).cloned().unwrap_or(0i64)"));
829 }
830
831 #[test]
832 fn vector_set_default_stays_structured_in_semantic_mode() {
833 let mut ctx = ctx_from_source(
834 r#"
835module Demo
836
837fn updateOrKeep(vec: Vector<Int>, idx: Int, value: Int) -> Vector<Int>
838 Option.withDefault(Vector.set(vec, idx, value), vec)
839"#,
840 "demo",
841 );
842
843 let out = transpile(&mut ctx);
844 let entry = generated_rust_entry_file(&out);
845
846 assert!(entry.contains("set_unchecked"));
848 assert!(!entry.contains(".unwrap_or("));
849 }
850
851 #[test]
852 fn vector_set_default_uses_ir_leaf_fast_path_when_optimized() {
853 let mut ctx = ctx_from_source(
854 r#"
855module Demo
856
857fn updateOrKeep(vec: Vector<Int>, idx: Int, value: Int) -> Vector<Int>
858 Option.withDefault(Vector.set(vec, idx, value), vec)
859"#,
860 "demo",
861 );
862
863 let out = transpile(&mut ctx);
864 let entry = generated_rust_entry_file(&out);
865
866 assert!(entry.contains("set_unchecked"));
867 assert!(!entry.contains(".unwrap_or("));
868 }
869
870 #[test]
871 fn vector_set_uses_owned_update_lowering() {
872 let mut ctx = ctx_from_source(
873 r#"
874module Demo
875
876fn update(vec: Vector<Int>, idx: Int, value: Int) -> Option<Vector<Int>>
877 Vector.set(vec, idx, value)
878"#,
879 "demo",
880 );
881
882 let out = transpile(&mut ctx);
883 let entry = generated_rust_entry_file(&out);
884
885 assert!(entry.contains(".set_owned("));
886 assert!(!entry.contains(".set(idx as usize,"));
887 }
888
889 #[test]
890 fn map_remove_uses_owned_update_lowering() {
891 let mut ctx = ctx_from_source(
892 r#"
893module Demo
894
895fn dropKey(m: Map<String, Int>, key: String) -> Map<String, Int>
896 Map.remove(m, key)
897"#,
898 "demo",
899 );
900
901 let out = transpile(&mut ctx);
902 let entry = generated_rust_entry_file(&out);
903
904 assert!(entry.contains(".remove_owned(&"));
905 }
906
907 #[test]
908 fn semantic_keeps_known_leaf_wrapper_call_structured() {
909 let mut ctx = ctx_from_source(
910 r#"
911module Demo
912
913fn cellAt(grid: Vector<Int>, idx: Int) -> Int
914 Option.withDefault(Vector.get(grid, idx), 0)
915
916fn read(grid: Vector<Int>, idx: Int) -> Int
917 cellAt(grid, idx)
918"#,
919 "demo",
920 );
921
922 let out = transpile(&mut ctx);
923 let entry = generated_rust_entry_file(&out);
924
925 assert!(entry.contains("cellAt(grid, idx)"));
926 assert!(!entry.contains("__aver_thin_arg0"));
927 }
928
929 #[test]
930 fn optimized_keeps_known_leaf_wrapper_callsite_and_leaves_absorption_to_rust() {
931 let mut ctx = ctx_from_source(
932 r#"
933module Demo
934
935fn cellAt(grid: Vector<Int>, idx: Int) -> Int
936 Option.withDefault(Vector.get(grid, idx), 0)
937
938fn read(grid: Vector<Int>, idx: Int) -> Int
939 cellAt(grid, idx)
940"#,
941 "demo",
942 );
943
944 let out = transpile(&mut ctx);
945 let entry = generated_rust_entry_file(&out);
946
947 assert!(entry.contains("cellAt(grid, idx)"));
948 assert!(!entry.contains("__aver_thin_arg0"));
949 }
950
951 #[test]
952 fn optimized_keeps_known_dispatch_wrapper_callsite_and_leaves_absorption_to_rust() {
953 let mut ctx = ctx_from_source(
954 r#"
955module Demo
956
957fn bucket(n: Int) -> Int
958 match n == 0
959 true -> 0
960 false -> 1
961
962fn readBucket(n: Int) -> Int
963 bucket(n)
964"#,
965 "demo",
966 );
967
968 let out = transpile(&mut ctx);
969 let entry = generated_rust_entry_file(&out);
970
971 assert!(entry.contains("bucket(n)"));
972 assert!(!entry.contains("__aver_thin_arg0"));
973 }
974
975 #[test]
976 fn bool_match_on_gte_normalizes_to_base_comparison_when_optimized() {
977 let mut ctx = ctx_from_source(
978 r#"
979module Demo
980
981fn bucket(n: Int) -> Int
982 match n >= 10
983 true -> 7
984 false -> 3
985"#,
986 "demo",
987 );
988
989 let out = transpile(&mut ctx);
990 let entry = generated_rust_entry_file(&out);
991
992 assert!(entry.contains("if (n < 10i64) { 3i64 } else { 7i64 }"));
993 }
994
995 #[test]
996 fn bool_match_stays_as_match_in_semantic_mode() {
997 let mut ctx = ctx_from_source(
998 r#"
999module Demo
1000
1001fn bucket(n: Int) -> Int
1002 match n >= 10
1003 true -> 7
1004 false -> 3
1005"#,
1006 "demo",
1007 );
1008
1009 let out = transpile(&mut ctx);
1010 let entry = generated_rust_entry_file(&out);
1011
1012 assert!(entry.contains("if (n < 10i64) { 3i64 } else { 7i64 }"));
1014 }
1015
1016 #[test]
1017 fn optimized_self_tco_uses_dispatch_table_for_wrapper_match() {
1018 let mut ctx = ctx_from_source(
1019 r#"
1020module Demo
1021
1022fn loop(r: Result<Int, String>) -> Int
1023 match r
1024 Result.Ok(n) -> n
1025 Result.Err(_) -> loop(Result.Ok(1))
1026"#,
1027 "demo",
1028 );
1029
1030 let out = transpile(&mut ctx);
1031 let entry = generated_rust_entry_file(&out);
1032
1033 assert!(entry.contains("match r.clone() {"));
1035 assert!(entry.contains("Ok(n)"));
1036 assert!(!entry.contains("__dispatch_subject"));
1037 }
1038
1039 #[test]
1040 fn optimized_mutual_tco_uses_dispatch_table_for_wrapper_match() {
1041 let mut ctx = ctx_from_source(
1042 r#"
1043module Demo
1044
1045fn left(r: Result<Int, String>) -> Int
1046 match r
1047 Result.Ok(n) -> n
1048 Result.Err(_) -> right(Result.Ok(1))
1049
1050fn right(r: Result<Int, String>) -> Int
1051 match r
1052 Result.Ok(n) -> n
1053 Result.Err(_) -> left(Result.Ok(1))
1054"#,
1055 "demo",
1056 );
1057
1058 let out = transpile(&mut ctx);
1059 let entry = generated_rust_entry_file(&out);
1060
1061 assert!(entry.contains("match r.clone() {"));
1063 assert!(entry.contains("Ok(n)"));
1064 assert!(!entry.contains("__dispatch_subject"));
1065 }
1066
1067 #[test]
1068 fn single_field_variant_display_avoids_vec_join() {
1069 let mut ctx = ctx_from_source(
1070 r#"
1071module Demo
1072
1073type Wrapper
1074 Wrap(Int)
1075 Pair(Int, Int)
1076 Empty
1077"#,
1078 "demo",
1079 );
1080
1081 let out = transpile(&mut ctx);
1082 let entry = generated_rust_entry_file(&out);
1083
1084 assert!(
1086 !entry.contains("vec![f0.aver_display_inner()].join"),
1087 "single-field variant should use direct format, not vec join:\n{}",
1088 entry
1089 );
1090 assert!(
1092 entry.contains("vec![f0.aver_display_inner(), f1.aver_display_inner()].join(\", \")"),
1093 "multi-field variant should use vec join:\n{}",
1094 entry
1095 );
1096 }
1097
1098 #[test]
1099 fn replay_codegen_wraps_guest_entry_in_scoped_runtime() {
1100 let mut ctx = ctx_from_source(
1101 r#"
1102module Demo
1103
1104fn runGuestProgram(path: String) -> Result<String, String>
1105 ! [Disk.readText]
1106 Disk.readText(path)
1107"#,
1108 "demo",
1109 );
1110 ctx.emit_replay_runtime = true;
1111 ctx.guest_entry = Some("runGuestProgram".to_string());
1112
1113 let out = transpile(&mut ctx);
1114 let entry = generated_rust_entry_file(&out);
1115 let replay_support = generated_file(&out, "src/replay_support.rs");
1116 let cargo_toml = generated_file(&out, "Cargo.toml");
1117
1118 assert!(entry.contains("aver_replay::with_guest_scope_result(\"runGuestProgram\""));
1119 assert!(replay_support.contains("pub mod aver_replay"));
1120 assert!(cargo_toml.contains("serde_json = \"1\""));
1121 }
1122
1123 #[test]
1124 fn replay_codegen_uses_guest_args_param_override_when_present() {
1125 let mut ctx = ctx_from_source(
1126 r#"
1127module Demo
1128
1129fn runGuestProgram(path: String, guestArgs: List<String>) -> Result<String, String>
1130 ! [Args.get]
1131 Result.Ok(String.join(Args.get(), ","))
1132"#,
1133 "demo",
1134 );
1135 ctx.emit_replay_runtime = true;
1136 ctx.guest_entry = Some("runGuestProgram".to_string());
1137
1138 let out = transpile(&mut ctx);
1139 let entry = generated_rust_entry_file(&out);
1140 let cargo_toml = generated_file(&out, "Cargo.toml");
1141
1142 assert!(entry.contains("aver_replay::with_guest_scope_args_result(\"runGuestProgram\""));
1143 assert!(entry.contains("guestArgs.clone()"));
1144 assert!(cargo_toml.contains("edition = \"2024\""));
1145 }
1146
1147 #[test]
1148 fn replay_codegen_wraps_root_main_when_no_guest_entry_is_set() {
1149 let mut ctx = ctx_from_source(
1150 r#"
1151module Demo
1152
1153fn main() -> Result<String, String>
1154 ! [Disk.readText]
1155 Disk.readText("demo.av")
1156"#,
1157 "demo",
1158 );
1159 ctx.emit_replay_runtime = true;
1160
1161 let out = transpile(&mut ctx);
1162 let root_main = generated_file(&out, "src/main.rs");
1163
1164 assert!(
1165 root_main.contains("aver_replay::with_guest_scope(\"main\", serde_json::Value::Null")
1166 );
1167 }
1168
1169 #[test]
1170 fn runtime_policy_codegen_uses_runtime_loader() {
1171 let mut ctx = ctx_from_source(
1172 r#"
1173module Demo
1174
1175fn main() -> Result<String, String>
1176 ! [Disk.readText]
1177 Disk.readText("demo.av")
1178"#,
1179 "demo",
1180 );
1181 ctx.emit_replay_runtime = true;
1182 ctx.runtime_policy_from_env = true;
1183
1184 let out = transpile(&mut ctx);
1185 let root_main = generated_file(&out, "src/main.rs");
1186 let replay_support = generated_file(&out, "src/replay_support.rs");
1187 let cargo_toml = generated_file(&out, "Cargo.toml");
1188
1189 assert!(!root_main.contains("mod policy_support;"));
1190 assert!(replay_support.contains("load_runtime_policy_from_env"));
1191 assert!(cargo_toml.contains("url = \"2\""));
1192 assert!(cargo_toml.contains("toml = \"0.8\""));
1193 }
1194
1195 #[test]
1196 fn replay_codegen_can_keep_embedded_policy_when_requested() {
1197 let mut ctx = ctx_from_source(
1198 r#"
1199module Demo
1200
1201fn main() -> Result<String, String>
1202 ! [Disk.readText]
1203 Disk.readText("demo.av")
1204"#,
1205 "demo",
1206 );
1207 ctx.emit_replay_runtime = true;
1208 ctx.policy = Some(crate::config::ProjectConfig {
1209 effect_policies: std::collections::HashMap::new(),
1210 check_suppressions: Vec::new(),
1211 independence_mode: crate::config::IndependenceMode::default(),
1212 });
1213
1214 let out = transpile(&mut ctx);
1215 let root_main = generated_file(&out, "src/main.rs");
1216 let replay_support = generated_file(&out, "src/replay_support.rs");
1217
1218 assert!(root_main.contains("mod policy_support;"));
1219 assert!(replay_support.contains("aver_policy::check_disk"));
1220 assert!(!replay_support.contains("RuntimeEffectPolicy"));
1221 }
1222
1223 #[test]
1224 fn self_host_support_is_emitted_as_separate_module() {
1225 let mut ctx = ctx_from_source(
1226 r#"
1227module Demo
1228
1229fn runGuestProgram(prog: Int, moduleFns: Int) -> Result<String, String>
1230 Result.Ok("ok")
1231"#,
1232 "demo",
1233 );
1234 ctx.emit_self_host_support = true;
1235 ctx.guest_entry = Some("runGuestProgram".to_string());
1236
1237 let out = transpile(&mut ctx);
1238 let root_main = generated_file(&out, "src/main.rs");
1239 let runtime_support = generated_file(&out, "src/runtime_support.rs");
1240 let self_host_support = generated_file(&out, "src/self_host_support.rs");
1241 let entry = generated_rust_entry_file(&out);
1242
1243 assert!(root_main.contains("mod self_host_support;"));
1244 assert!(!runtime_support.contains("with_fn_store"));
1245 assert!(self_host_support.contains("pub fn with_program_fn_store"));
1246 assert!(entry.contains("crate::self_host_support::with_program_fn_store("));
1247 }
1248
1249 #[test]
1250 fn independent_product_codegen_avoids_string_specific_error_type() {
1251 let mut ctx = ctx_from_source(
1252 r#"
1253module Demo
1254
1255fn left() -> Result<Int, Int>
1256 Result.Ok(1)
1257
1258fn right() -> Result<Int, Int>
1259 Result.Ok(2)
1260
1261fn main() -> Result<(Int, Int), Int>
1262 data = (left(), right())?!
1263 Result.Ok(data)
1264"#,
1265 "demo",
1266 );
1267 ctx.emit_replay_runtime = true;
1268
1269 let out = transpile(&mut ctx);
1270 let entry = generated_rust_entry_file(&out);
1271
1272 assert!(!entry.contains("Ok::<_, aver_rt::AverStr>"));
1273 assert!(entry.contains("crate::aver_replay::exit_effect_group();"));
1274 assert!(entry.contains("match (_r0, _r1)"));
1275 assert!(!entry.contains("let _r0 = left()?;"));
1276 }
1277
1278 #[test]
1279 fn independent_product_codegen_emits_cancel_runtime_and_scope_propagation() {
1280 let mut ctx = ctx_from_source(
1281 r#"
1282module Demo
1283
1284fn left() -> Result<Int, String>
1285 Result.Ok(1)
1286
1287fn right() -> Result<Int, String>
1288 Result.Ok(2)
1289
1290fn main() -> Result<(Int, Int), String>
1291 data = (left(), right())?!
1292 Result.Ok(data)
1293"#,
1294 "demo",
1295 );
1296 ctx.emit_replay_runtime = true;
1297 ctx.policy = Some(crate::config::ProjectConfig {
1298 effect_policies: std::collections::HashMap::new(),
1299 check_suppressions: Vec::new(),
1300 independence_mode: crate::config::IndependenceMode::Cancel,
1301 });
1302
1303 let out = transpile(&mut ctx);
1304 let entry = generated_rust_entry_file(&out);
1305 let runtime_support = generated_file(&out, "src/runtime_support.rs");
1306 let replay_support = generated_file(&out, "src/replay_support.rs");
1307
1308 assert!(entry.contains("crate::run_cancelable_branch"));
1309 assert!(entry.contains("capture_parallel_scope_context"));
1310 assert!(entry.contains("_s.spawn(move ||"));
1311 assert!(runtime_support.contains("pub fn run_cancelable_branch"));
1312 assert!(runtime_support.contains("panic_any(AverCancelled)"));
1313 assert!(replay_support.contains("pub fn capture_parallel_scope_context"));
1314 assert!(replay_support.contains("pub fn independence_mode_is_cancel()"));
1315 }
1316
1317 #[test]
1318 fn runtime_policy_codegen_parses_independence_mode() {
1319 let mut ctx = ctx_from_source(
1320 r#"
1321module Demo
1322
1323fn main() -> Result<String, String>
1324 ! [Disk.readText]
1325 Disk.readText("demo.av")
1326"#,
1327 "demo",
1328 );
1329 ctx.emit_replay_runtime = true;
1330 ctx.runtime_policy_from_env = true;
1331
1332 let out = transpile(&mut ctx);
1333 let replay_support = generated_file(&out, "src/replay_support.rs");
1334
1335 assert!(replay_support.contains("independence_mode_cancel"));
1336 assert!(replay_support.contains("[independence].mode must be 'complete' or 'cancel'"));
1337 }
1338
1339 #[test]
1340 fn effectful_codegen_inserts_cancel_checkpoint_before_builtin_calls() {
1341 let mut ctx = ctx_from_source(
1342 r#"
1343module Demo
1344
1345fn main() -> Result<String, String>
1346 ! [Disk.readText]
1347 Disk.readText("demo.av")
1348"#,
1349 "demo",
1350 );
1351
1352 let out = transpile(&mut ctx);
1353 let entry = generated_rust_entry_file(&out);
1354
1355 assert!(entry.contains("crate::cancel_checkpoint(); (aver_rt::read_text"));
1356 }
1357
1358 #[test]
1359 fn replay_support_matches_group_effects_by_occurrence_and_args() {
1360 let mut ctx = ctx_from_source(
1361 r#"
1362module Demo
1363
1364fn left() -> Result<Int, String>
1365 Result.Ok(1)
1366
1367fn right() -> Result<Int, String>
1368 Result.Ok(2)
1369
1370fn main() -> Result<(Int, Int), String>
1371 data = (left(), right())?!
1372 Result.Ok(data)
1373"#,
1374 "demo",
1375 );
1376 ctx.emit_replay_runtime = true;
1377
1378 let out = transpile(&mut ctx);
1379 let replay_support = generated_file(&out, "src/replay_support.rs");
1380
1381 assert!(replay_support.contains("candidate.effect_occurrence"));
1382 assert!(replay_support.contains("candidate.args != args"));
1383 }
1384}