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