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