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