1mod builtins;
5mod expr;
6mod liveness;
7mod pattern;
8mod policy;
9mod project;
10mod runtime;
11mod syntax;
12mod toplevel;
13mod types;
14
15use std::collections::{BTreeMap, HashSet};
16
17use crate::ast::{FnDef, TopLevel, TypeDef};
18use crate::codegen::common::module_prefix_to_rust_segments;
19use crate::codegen::{CodegenContext, ProjectOutput};
20use crate::types::Type;
21
22#[derive(Default)]
23struct ModuleTreeNode {
24 content: Option<String>,
25 children: BTreeMap<String, ModuleTreeNode>,
26}
27
28pub fn transpile(ctx: &CodegenContext) -> ProjectOutput {
30 let used_services = detect_used_services(ctx);
31 let needs_http_types = needs_named_type(ctx, "Header")
32 || needs_named_type(ctx, "HttpResponse")
33 || needs_named_type(ctx, "HttpRequest");
34 let needs_tcp_types = needs_named_type(ctx, "Tcp.Connection");
35
36 let has_tcp_runtime = used_services.contains("Tcp");
37 let has_http_runtime = used_services.contains("Http");
38 let has_http_server_runtime = used_services.contains("HttpServer");
39
40 let has_tcp_types = has_tcp_runtime || needs_tcp_types;
41 let has_http_types = has_http_runtime || has_http_server_runtime || needs_http_types;
42 let has_http_server_types = has_http_server_runtime || needs_named_type(ctx, "HttpRequest");
43
44 let main_fn = ctx.fn_defs.iter().find(|fd| fd.name == "main");
45 let top_level_stmts: Vec<_> = ctx
46 .items
47 .iter()
48 .filter_map(|item| {
49 if let TopLevel::Stmt(stmt) = item {
50 Some(stmt)
51 } else {
52 None
53 }
54 })
55 .collect();
56 let verify_blocks: Vec<_> = ctx
57 .items
58 .iter()
59 .filter_map(|item| {
60 if let TopLevel::Verify(vb) = item {
61 Some(vb)
62 } else {
63 None
64 }
65 })
66 .collect();
67
68 let mut files = vec![
69 (
70 "Cargo.toml".to_string(),
71 project::generate_cargo_toml(
72 &ctx.project_name,
73 &used_services,
74 ctx.policy.is_some(),
75 &std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("aver-rt"),
76 ),
77 ),
78 (
79 "src/main.rs".to_string(),
80 render_root_main(main_fn, ctx.policy.is_some(), !verify_blocks.is_empty()),
81 ),
82 (
83 "src/runtime_support.rs".to_string(),
84 render_runtime_support(has_tcp_types, has_http_types, has_http_server_types),
85 ),
86 ];
87
88 if let Some(config) = &ctx.policy {
89 files.push((
90 "src/policy_support.rs".to_string(),
91 format!("{}\n", policy::generate_policy_runtime(config)),
92 ));
93 }
94
95 if !verify_blocks.is_empty() {
96 files.push((
97 "src/verify.rs".to_string(),
98 render_verify_module(&verify_blocks, ctx),
99 ));
100 }
101
102 let mut module_tree = ModuleTreeNode::default();
103 insert_module_content(
104 &mut module_tree,
105 &[String::from("entry")],
106 render_generated_module(
107 root_module_depends(&ctx.items),
108 entry_module_sections(ctx, main_fn, &top_level_stmts),
109 ),
110 );
111
112 for module in &ctx.modules {
113 let path = module_prefix_to_rust_segments(&module.prefix);
114 insert_module_content(
115 &mut module_tree,
116 &path,
117 render_generated_module(module.depends.clone(), module_sections(module, ctx)),
118 );
119 }
120
121 emit_module_tree_files(&module_tree, "src/aver_generated", &mut files);
122 files.sort_by(|left, right| left.0.cmp(&right.0));
123
124 ProjectOutput { files }
125}
126
127fn render_root_main(main_fn: Option<&FnDef>, has_policy: bool, has_verify: bool) -> String {
128 let mut sections = vec![
129 "#![allow(unused_variables, unused_mut, dead_code, unused_imports, unused_parens, non_snake_case, non_camel_case_types, unreachable_patterns)]".to_string(),
130 "pub use std::collections::HashMap;".to_string(),
131 String::new(),
132 "mod runtime_support;".to_string(),
133 "pub use runtime_support::*;".to_string(),
134 ];
135
136 if has_policy {
137 sections.push(String::new());
138 sections.push("mod policy_support;".to_string());
139 sections.push("pub use policy_support::*;".to_string());
140 }
141
142 sections.push(String::new());
143 sections.push("pub mod aver_generated;".to_string());
144
145 if has_verify {
146 sections.push(String::new());
147 sections.push("#[cfg(test)]".to_string());
148 sections.push("mod verify;".to_string());
149 }
150
151 sections.push(String::new());
152 let returns_result = main_fn.is_some_and(|fd| fd.return_type.starts_with("Result<"));
153 if returns_result {
154 let ret_type = types::type_annotation_to_rust(&main_fn.unwrap().return_type);
155 sections.push(format!("fn main() -> {} {{", ret_type));
156 sections.push(" aver_generated::entry::main()".to_string());
157 } else {
158 sections.push("fn main() {".to_string());
159 if main_fn.is_some() {
160 sections.push(" aver_generated::entry::main()".to_string());
161 }
162 }
163 sections.push("}".to_string());
164 sections.push(String::new());
165
166 sections.join("\n")
167}
168
169fn render_runtime_support(
170 has_tcp_types: bool,
171 has_http_types: bool,
172 has_http_server_types: bool,
173) -> String {
174 let mut sections = vec![runtime::generate_runtime()];
175 if has_tcp_types {
176 sections.push(runtime::generate_tcp_types());
177 }
178 if has_http_types {
179 sections.push(runtime::generate_http_types());
180 }
181 if has_http_server_types {
182 sections.push(runtime::generate_http_server_types());
183 }
184 format!("{}\n", sections.join("\n\n"))
185}
186
187fn render_verify_module(
188 verify_blocks: &[&crate::ast::VerifyBlock],
189 ctx: &CodegenContext,
190) -> String {
191 [
192 "#[allow(unused_imports)]".to_string(),
193 "use crate::*;".to_string(),
194 "#[allow(unused_imports)]".to_string(),
195 "use crate::aver_generated::entry::*;".to_string(),
196 String::new(),
197 toplevel::emit_verify_blocks(verify_blocks, ctx),
198 String::new(),
199 ]
200 .join("\n")
201}
202
203fn render_generated_module(depends: Vec<String>, sections: Vec<String>) -> String {
204 if sections.is_empty() {
205 String::new()
206 } else {
207 let mut lines = vec![
208 "#[allow(unused_imports)]".to_string(),
209 "use crate::*;".to_string(),
210 ];
211 for dep in depends {
212 let path = module_prefix_to_rust_segments(&dep).join("::");
213 lines.push("#[allow(unused_imports)]".to_string());
214 lines.push(format!("use crate::aver_generated::{}::*;", path));
215 }
216 lines.push(String::new());
217 lines.push(sections.join("\n\n"));
218 lines.push(String::new());
219 lines.join("\n")
220 }
221}
222
223fn entry_module_sections(
224 ctx: &CodegenContext,
225 main_fn: Option<&FnDef>,
226 top_level_stmts: &[&crate::ast::Stmt],
227) -> Vec<String> {
228 let mut sections = Vec::new();
229
230 for td in &ctx.type_defs {
231 if is_shared_runtime_type(td) {
232 continue;
233 }
234 sections.push(toplevel::emit_public_type_def(td));
235 }
236
237 for fd in &ctx.fn_defs {
238 if fd.name == "main" {
239 continue;
240 }
241 let is_memo = ctx.memo_fns.contains(&fd.name);
242 sections.push(toplevel::emit_public_fn_def(fd, is_memo, ctx));
243 }
244
245 if main_fn.is_some() || !top_level_stmts.is_empty() {
246 sections.push(toplevel::emit_public_main(main_fn, top_level_stmts, ctx));
247 }
248
249 sections
250}
251
252fn module_sections(module: &crate::codegen::ModuleInfo, ctx: &CodegenContext) -> Vec<String> {
253 let mut sections = Vec::new();
254
255 for td in &module.type_defs {
256 if is_shared_runtime_type(td) {
257 continue;
258 }
259 sections.push(toplevel::emit_public_type_def(td));
260 }
261
262 for fd in &module.fn_defs {
263 let is_memo = ctx.memo_fns.contains(&fd.name);
264 sections.push(toplevel::emit_public_fn_def(fd, is_memo, ctx));
265 }
266
267 sections
268}
269
270fn insert_module_content(node: &mut ModuleTreeNode, segments: &[String], content: String) {
271 let child = node.children.entry(segments[0].clone()).or_default();
272 if segments.len() == 1 {
273 child.content = Some(content);
274 } else {
275 insert_module_content(child, &segments[1..], content);
276 }
277}
278
279fn emit_module_tree_files(node: &ModuleTreeNode, rel_dir: &str, files: &mut Vec<(String, String)>) {
280 let mut parts = Vec::new();
281
282 if let Some(content) = &node.content
283 && !content.trim().is_empty()
284 {
285 parts.push(content.trim_end().to_string());
286 }
287
288 for child_name in node.children.keys() {
289 parts.push(format!("pub mod {};", child_name));
290 }
291
292 let mut mod_rs = parts.join("\n\n");
293 if !mod_rs.is_empty() {
294 mod_rs.push('\n');
295 }
296 files.push((format!("{}/mod.rs", rel_dir), mod_rs));
297
298 for (child_name, child) in &node.children {
299 emit_module_tree_files(child, &format!("{}/{}", rel_dir, child_name), files);
300 }
301}
302
303fn root_module_depends(items: &[TopLevel]) -> Vec<String> {
304 items
305 .iter()
306 .find_map(|item| {
307 if let TopLevel::Module(module) = item {
308 Some(module.depends.clone())
309 } else {
310 None
311 }
312 })
313 .unwrap_or_default()
314}
315
316fn detect_used_services(ctx: &CodegenContext) -> HashSet<String> {
318 let mut services = HashSet::new();
319 for item in &ctx.items {
320 if let TopLevel::FnDef(fd) = item {
321 for eff in &fd.effects {
322 services.insert(eff.clone());
323 if let Some((service, _)) = eff.split_once('.') {
324 services.insert(service.to_string());
325 }
326 }
327 }
328 }
329 for module in &ctx.modules {
330 for fd in &module.fn_defs {
331 for eff in &fd.effects {
332 services.insert(eff.clone());
333 if let Some((service, _)) = eff.split_once('.') {
334 services.insert(service.to_string());
335 }
336 }
337 }
338 }
339 services
340}
341
342fn is_shared_runtime_type(td: &TypeDef) -> bool {
343 matches!(
344 td,
345 TypeDef::Product { name, .. }
346 if matches!(name.as_str(), "Header" | "HttpResponse" | "HttpRequest")
347 )
348}
349
350fn needs_named_type(ctx: &CodegenContext, wanted: &str) -> bool {
351 ctx.fn_sigs.values().any(|(params, ret, _effects)| {
352 params.iter().any(|p| type_contains_named(p, wanted)) || type_contains_named(ret, wanted)
353 })
354}
355
356fn type_contains_named(ty: &Type, wanted: &str) -> bool {
357 match ty {
358 Type::Named(name) => name == wanted,
359 Type::Result(ok, err) => {
360 type_contains_named(ok, wanted) || type_contains_named(err, wanted)
361 }
362 Type::Option(inner) | Type::List(inner) => type_contains_named(inner, wanted),
363 Type::Tuple(items) => items.iter().any(|t| type_contains_named(t, wanted)),
364 Type::Map(k, v) => type_contains_named(k, wanted) || type_contains_named(v, wanted),
365 Type::Fn(params, ret, _effects) => {
366 params.iter().any(|t| type_contains_named(t, wanted))
367 || type_contains_named(ret, wanted)
368 }
369 Type::Int | Type::Float | Type::Str | Type::Bool | Type::Unit | Type::Unknown => false,
370 }
371}
372
373#[cfg(test)]
374mod tests {
375 use super::{
376 ModuleTreeNode, emit_module_tree_files, insert_module_content, render_generated_module,
377 };
378
379 #[test]
380 fn generated_module_imports_direct_depends() {
381 let rendered = render_generated_module(
382 vec!["Domain.Types".to_string(), "App.Commands".to_string()],
383 vec!["pub fn demo() {}".to_string()],
384 );
385
386 assert!(rendered.contains("use crate::aver_generated::domain::types::*;"));
387 assert!(rendered.contains("use crate::aver_generated::app::commands::*;"));
388 assert!(rendered.contains("pub fn demo() {}"));
389 }
390
391 #[test]
392 fn module_tree_files_do_not_reexport_children() {
393 let mut tree = ModuleTreeNode::default();
394 insert_module_content(
395 &mut tree,
396 &["app".to_string(), "cli".to_string()],
397 "pub fn run() {}".to_string(),
398 );
399
400 let mut files = Vec::new();
401 emit_module_tree_files(&tree, "src/aver_generated", &mut files);
402
403 let root_mod = files
404 .iter()
405 .find(|(path, _)| path == "src/aver_generated/mod.rs")
406 .map(|(_, content)| content)
407 .expect("root mod.rs should exist");
408
409 assert!(root_mod.contains("pub mod app;"));
410 assert!(!root_mod.contains("pub use app::*;"));
411 }
412}