Skip to main content

aver/codegen/rust/
mod.rs

1/// Rust backend for the Aver transpiler.
2///
3/// Transforms Aver AST -> valid Rust source code.
4mod 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
28/// Transpile an Aver program to a Rust project.
29pub 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
316/// Detect which effectful services are used in the program (including modules).
317fn 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}