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 String::new(),
235 "mod runtime_support;".to_string(),
236 "pub use runtime_support::*;".to_string(),
237 ];
238
239 if has_policy {
240 sections.push(String::new());
241 sections.push("mod policy_support;".to_string());
242 sections.push("pub use policy_support::*;".to_string());
243 }
244
245 if has_replay {
246 sections.push(String::new());
247 sections.push("mod replay_support;".to_string());
248 sections.push("pub use replay_support::*;".to_string());
249 }
250
251 if has_self_host_support {
252 sections.push(String::new());
253 sections.push("mod self_host_support;".to_string());
254 }
255
256 sections.push(String::new());
257 sections.push("pub mod aver_generated;".to_string());
258
259 if has_verify {
260 sections.push(String::new());
261 sections.push("#[cfg(test)]".to_string());
262 sections.push("mod verify;".to_string());
263 }
264
265 sections.push(String::new());
267 let returns_result = main_fn.is_some_and(|fd| fd.return_type.starts_with("Result<"));
268 let result_unit_string =
269 main_fn.is_some_and(|fd| fd.return_type.replace(' ', "") == "Result<Unit,String>");
270 if returns_result {
271 if result_unit_string {
272 sections.push("fn main() {".to_string());
273 sections.push(" let child = std::thread::Builder::new()".to_string());
274 sections.push(" .stack_size(256 * 1024 * 1024)".to_string());
275 if has_replay && guest_entry.is_none() {
276 sections.push(" .spawn(|| {".to_string());
277 sections.push(" let __result = aver_replay::with_guest_scope(\"main\", serde_json::Value::Null, aver_generated::entry::main);".to_string());
278 sections.push(" __result.map_err(|e| e.to_string())".to_string());
279 sections.push(" })".to_string());
280 } else {
281 sections.push(" .spawn(|| {".to_string());
282 sections
283 .push(" let __result = aver_generated::entry::main();".to_string());
284 sections.push(" __result.map_err(|e| e.to_string())".to_string());
285 sections.push(" })".to_string());
286 }
287 sections.push(" .expect(\"thread spawn\");".to_string());
288 sections.push(" match child.join().expect(\"thread join\") {".to_string());
289 sections.push(" Ok(()) => {}".to_string());
290 sections.push(" Err(e) => {".to_string());
291 sections.push(" eprintln!(\"{}\", e);".to_string());
292 sections.push(" std::process::exit(1);".to_string());
293 sections.push(" }".to_string());
294 sections.push(" }".to_string());
295 } else {
296 let ret_type = types::type_annotation_to_rust(&main_fn.unwrap().return_type);
297 sections.push(format!("fn main() -> {} {{", ret_type));
298 if has_replay && guest_entry.is_none() {
299 sections.push(
300 " aver_replay::with_guest_scope(\"main\", serde_json::Value::Null, aver_generated::entry::main)"
301 .to_string(),
302 );
303 } else {
304 sections.push(" aver_generated::entry::main()".to_string());
305 }
306 }
307 } else {
308 sections.push("fn main() {".to_string());
309 if main_fn.is_some() {
310 sections.push(" let child = std::thread::Builder::new()".to_string());
311 sections.push(" .stack_size(256 * 1024 * 1024)".to_string());
312 if has_replay && guest_entry.is_none() {
313 sections.push(" .spawn(|| aver_replay::with_guest_scope(\"main\", serde_json::Value::Null, || aver_generated::entry::main()))".to_string());
314 } else {
315 sections.push(" .spawn(|| aver_generated::entry::main())".to_string());
316 }
317 sections.push(" .expect(\"thread spawn\");".to_string());
318 sections.push(" child.join().expect(\"thread join\");".to_string());
319 }
320 }
321 sections.push("}".to_string());
322 sections.push(String::new());
323
324 sections.join("\n")
325}
326
327fn render_runtime_support(
328 has_tcp_types: bool,
329 has_http_types: bool,
330 has_http_server_types: bool,
331 has_replay: bool,
332 embedded_independence_cancel: bool,
333) -> String {
334 let mut sections = vec![runtime::generate_runtime(
335 has_replay,
336 has_http_server_types,
337 embedded_independence_cancel,
338 )];
339 if has_tcp_types {
340 sections.push(runtime::generate_tcp_types());
341 }
342 if has_http_types {
343 sections.push(runtime::generate_http_types());
344 }
345 if has_http_server_types {
346 sections.push(runtime::generate_http_server_types());
347 }
348 format!("{}\n", sections.join("\n\n"))
349}
350
351fn render_verify_module(
352 verify_blocks: &[&crate::ast::VerifyBlock],
353 ctx: &CodegenContext,
354) -> String {
355 [
356 "#[allow(unused_imports)]".to_string(),
357 "use crate::*;".to_string(),
358 "#[allow(unused_imports)]".to_string(),
359 "use crate::aver_generated::entry::*;".to_string(),
360 String::new(),
361 toplevel::emit_verify_blocks(verify_blocks, ctx),
362 String::new(),
363 ]
364 .join("\n")
365}
366
367fn render_generated_module(depends: Vec<String>, sections: Vec<String>) -> String {
368 if sections.is_empty() {
369 String::new()
370 } else {
371 let mut lines = vec![
372 "#[allow(unused_imports)]".to_string(),
373 "use crate::*;".to_string(),
374 ];
375 for dep in depends {
376 let path = module_prefix_to_rust_segments(&dep).join("::");
377 lines.push("#[allow(unused_imports)]".to_string());
378 lines.push(format!("use crate::aver_generated::{}::*;", path));
379 }
380 lines.push(String::new());
381 lines.push(sections.join("\n\n"));
382 lines.push(String::new());
383 lines.join("\n")
384 }
385}
386
387fn entry_module_sections(
388 ctx: &CodegenContext,
389 main_fn: Option<&FnDef>,
390 top_level_stmts: &[&crate::ast::Stmt],
391) -> Vec<String> {
392 let mut sections = Vec::new();
393
394 for td in &ctx.type_defs {
395 if is_shared_runtime_type(td) {
396 continue;
397 }
398 sections.push(toplevel::emit_public_type_def(td, ctx));
399 if ctx.emit_replay_runtime {
400 sections.push(replay::emit_replay_value_impl(td));
401 }
402 }
403
404 let non_main_fns: Vec<&FnDef> = ctx.fn_defs.iter().filter(|fd| fd.name != "main").collect();
406 let mutual_groups = toplevel::find_mutual_tco_groups(&non_main_fns);
407 let mut mutual_tco_members: HashSet<String> = HashSet::new();
408
409 for (group_id, group_indices) in mutual_groups.iter().enumerate() {
410 let group_fns: Vec<&FnDef> = group_indices.iter().map(|&idx| non_main_fns[idx]).collect();
411 for fd in &group_fns {
412 mutual_tco_members.insert(fd.name.clone());
413 }
414 sections.push(toplevel::emit_mutual_tco_block(
415 group_id + 1,
416 &group_fns,
417 ctx,
418 "pub ",
419 ));
420 }
421
422 for fd in &ctx.fn_defs {
423 if fd.name == "main" || mutual_tco_members.contains(&fd.name) {
424 continue;
425 }
426 let is_memo = ctx.memo_fns.contains(&fd.name);
427 sections.push(toplevel::emit_public_fn_def(fd, is_memo, ctx));
428 }
429
430 if main_fn.is_some() || !top_level_stmts.is_empty() {
431 sections.push(toplevel::emit_public_main(main_fn, top_level_stmts, ctx));
432 }
433
434 sections
435}
436
437fn module_sections(module: &crate::codegen::ModuleInfo, ctx: &CodegenContext) -> Vec<String> {
438 let mut sections = Vec::new();
439
440 for td in &module.type_defs {
441 if is_shared_runtime_type(td) {
442 continue;
443 }
444 sections.push(toplevel::emit_public_type_def(td, ctx));
445 if ctx.emit_replay_runtime {
446 sections.push(replay::emit_replay_value_impl(td));
447 }
448 }
449
450 let fn_refs: Vec<&FnDef> = module.fn_defs.iter().collect();
452 let mutual_groups = toplevel::find_mutual_tco_groups(&fn_refs);
453 let mut mutual_tco_members: HashSet<String> = HashSet::new();
454
455 for (group_id, group_indices) in mutual_groups.iter().enumerate() {
456 let group_fns: Vec<&FnDef> = group_indices.iter().map(|&idx| fn_refs[idx]).collect();
457 for fd in &group_fns {
458 mutual_tco_members.insert(fd.name.clone());
459 }
460 sections.push(toplevel::emit_mutual_tco_block(
461 group_id + 1,
462 &group_fns,
463 ctx,
464 "pub ",
465 ));
466 }
467
468 for fd in &module.fn_defs {
469 if mutual_tco_members.contains(&fd.name) {
470 continue;
471 }
472 let is_memo = ctx.memo_fns.contains(&fd.name);
473 sections.push(toplevel::emit_public_fn_def(fd, is_memo, ctx));
474 }
475
476 sections
477}
478
479fn root_module_depends(items: &[TopLevel]) -> Vec<String> {
480 items
481 .iter()
482 .find_map(|item| {
483 if let TopLevel::Module(module) = item {
484 Some(module.depends.clone())
485 } else {
486 None
487 }
488 })
489 .unwrap_or_default()
490}
491
492fn detect_used_services(ctx: &CodegenContext) -> HashSet<String> {
494 let mut services = HashSet::new();
495 for item in &ctx.items {
496 if let TopLevel::FnDef(fd) = item {
497 for eff in &fd.effects {
498 services.insert(eff.node.clone());
499 if let Some((service, _)) = eff.node.split_once('.') {
500 services.insert(service.to_string());
501 }
502 }
503 }
504 }
505 for module in &ctx.modules {
506 for fd in &module.fn_defs {
507 for eff in &fd.effects {
508 services.insert(eff.node.clone());
509 if let Some((service, _)) = eff.node.split_once('.') {
510 services.insert(service.to_string());
511 }
512 }
513 }
514 }
515 services
516}
517
518fn is_shared_runtime_type(td: &TypeDef) -> bool {
519 matches!(
520 td,
521 TypeDef::Product { name, .. }
522 if matches!(name.as_str(), "HttpResponse" | "HttpRequest")
523 )
524}
525
526fn needs_named_type(ctx: &CodegenContext, wanted: &str) -> bool {
527 ctx.fn_sigs.values().any(|(params, ret, _effects)| {
528 params.iter().any(|p| type_contains_named(p, wanted)) || type_contains_named(ret, wanted)
529 })
530}
531
532fn type_contains_named(ty: &Type, wanted: &str) -> bool {
533 match ty {
534 Type::Named(name) => name == wanted,
535 Type::Result(ok, err) => {
536 type_contains_named(ok, wanted) || type_contains_named(err, wanted)
537 }
538 Type::Option(inner) | Type::List(inner) | Type::Vector(inner) => {
539 type_contains_named(inner, wanted)
540 }
541 Type::Tuple(items) => items.iter().any(|t| type_contains_named(t, wanted)),
542 Type::Map(k, v) => type_contains_named(k, wanted) || type_contains_named(v, wanted),
543 Type::Fn(params, ret, _effects) => {
544 params.iter().any(|t| type_contains_named(t, wanted))
545 || type_contains_named(ret, wanted)
546 }
547 Type::Int | Type::Float | Type::Str | Type::Bool | Type::Unit | Type::Unknown => false,
548 }
549}
550
551#[cfg(test)]
552mod tests {
553 use super::{render_generated_module, synthesize_rust_module_cascade, transpile};
554 use crate::codegen::build_context;
555 use crate::source::parse_source;
556 use crate::tco;
557 use crate::types::checker::run_type_check_full;
558 use std::collections::HashSet;
559
560 fn ctx_from_source(source: &str, project_name: &str) -> crate::codegen::CodegenContext {
561 let mut items = parse_source(source).expect("source should parse");
562 tco::transform_program(&mut items);
563 let tc = run_type_check_full(&items, None);
564 assert!(
565 tc.errors.is_empty(),
566 "source should typecheck without errors: {:?}",
567 tc.errors
568 );
569 build_context(items, &tc, HashSet::new(), project_name.to_string(), vec![])
570 }
571
572 fn generated_rust_entry_file(out: &crate::codegen::ProjectOutput) -> &str {
573 out.files
574 .iter()
575 .find_map(|(name, content)| {
576 (name == "src/aver_generated/entry/mod.rs").then_some(content.as_str())
577 })
578 .expect("expected generated Rust entry module")
579 }
580
581 fn generated_file<'a>(out: &'a crate::codegen::ProjectOutput, path: &str) -> &'a str {
582 out.files
583 .iter()
584 .find_map(|(name, content)| (name == path).then_some(content.as_str()))
585 .unwrap_or_else(|| panic!("expected generated file '{}'", path))
586 }
587
588 #[test]
589 fn emission_banner_appears_in_root_main() {
590 let mut ctx = ctx_from_source(
591 r#"
592module Demo
593
594fn main() -> Int
595 1
596"#,
597 "demo",
598 );
599
600 let out = transpile(&mut ctx);
601 let root_main = generated_file(&out, "src/main.rs");
602
603 assert!(root_main.contains("// Aver Rust emission"));
604 }
605
606 #[test]
607 fn generated_module_imports_direct_depends() {
608 let rendered = render_generated_module(
609 vec!["Domain.Types".to_string(), "App.Commands".to_string()],
610 vec!["pub fn demo() {}".to_string()],
611 );
612
613 assert!(rendered.contains("use crate::aver_generated::domain::types::*;"));
614 assert!(rendered.contains("use crate::aver_generated::app::commands::*;"));
615 assert!(rendered.contains("pub fn demo() {}"));
616 }
617
618 #[test]
619 fn module_tree_files_do_not_reexport_children() {
620 let modules = vec![(
621 vec!["app".to_string(), "cli".to_string()],
622 "pub fn run() {}".to_string(),
623 )];
624 let files = synthesize_rust_module_cascade("src/aver_generated", &modules);
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}