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