Skip to main content

vexil_codegen_rust/
lib.rs

1//! Rust code generation backend for the Vexil schema compiler.
2//!
3//! Implements the [`CodegenBackend`](vexil_lang::codegen::CodegenBackend) trait,
4//! generating Rust structs with `Pack`/`Unpack` implementations for wire encoding.
5//!
6//! # Usage
7//!
8//! ```ignore
9//! let result = vexil_lang::compile(source);
10//! let code = vexil_codegen_rust::generate(&result.compiled.unwrap());
11//! ```
12
13pub mod annotations;
14pub mod backend;
15pub mod boxing;
16pub mod config;
17pub mod delta;
18pub mod emit;
19pub mod enum_gen;
20pub mod flags;
21pub mod message;
22pub mod newtype;
23pub mod types;
24pub mod union_gen;
25
26pub use backend::RustBackend;
27
28use std::collections::HashMap;
29
30use vexil_lang::ir::{CompiledSchema, ResolvedType, TypeDef, TypeId};
31
32#[derive(Debug, Clone, PartialEq, thiserror::Error)]
33pub enum CodegenError {
34    #[error("unresolved type {type_id:?} referenced by {referenced_by}")]
35    UnresolvedType {
36        type_id: TypeId,
37        referenced_by: String,
38    },
39}
40
41/// Generate Rust code for a compiled schema (no cross-file imports).
42pub fn generate(compiled: &CompiledSchema) -> Result<String, CodegenError> {
43    generate_with_imports(compiled, None)
44}
45
46/// Generate Rust code for a compiled schema, with optional cross-file import `use` statements.
47///
48/// `import_paths` maps TypeIds of imported types to their Rust module paths
49/// (e.g., `"crate::foo::bar::types::Baz"` for a type `Baz` from namespace `foo.bar.types`).
50pub(crate) fn generate_with_imports(
51    compiled: &CompiledSchema,
52    import_paths: Option<&HashMap<TypeId, String>>,
53) -> Result<String, CodegenError> {
54    // Step 1: Detect recursive types that need Box<T> wrapping.
55    let needs_box = boxing::detect_boxing(compiled);
56
57    // Step 2: Create the code writer.
58    let mut w = emit::CodeWriter::new();
59
60    // Step 3: Emit header.
61    w.line("// Code generated by vexilc. DO NOT EDIT.");
62    let ns = compiled.namespace.join(".");
63    w.line(&format!("// Source: {ns}"));
64
65    // Step 4: Emit blank line.
66    w.blank();
67
68    // Step 5: Check if any type uses Map -> emit BTreeMap import.
69    if schema_uses_map(compiled) {
70        w.line("use std::collections::BTreeMap;");
71    }
72    w.line("use vexil_runtime::*;");
73
74    // Cross-file use statements.
75    if let Some(paths) = import_paths {
76        let mut unique_paths: Vec<&String> = paths.values().collect();
77        unique_paths.sort();
78        unique_paths.dedup();
79        for path in unique_paths {
80            w.line(&format!("use {path};"));
81        }
82    }
83
84    // Step 7: Emit blank line.
85    w.blank();
86
87    // Emit SCHEMA_HASH (always present — every schema has a hash).
88    let hash = vexil_lang::canonical::schema_hash(compiled);
89    let hash_str = hash
90        .iter()
91        .map(|b| format!("0x{b:02x}"))
92        .collect::<Vec<_>>()
93        .join(", ");
94    w.line(&format!("pub const SCHEMA_HASH: [u8; 32] = [{hash_str}];"));
95
96    // Emit SCHEMA_VERSION if present (after SCHEMA_HASH per spec §8.2).
97    if let Some(ref version) = compiled.annotations.version {
98        w.line(&format!("pub const SCHEMA_VERSION: &str = \"{version}\";"));
99    }
100
101    // Step 9: For each declared type, emit the appropriate code.
102    for &type_id in &compiled.declarations {
103        let typedef =
104            compiled
105                .registry
106                .get(type_id)
107                .ok_or_else(|| CodegenError::UnresolvedType {
108                    type_id,
109                    referenced_by: "declarations".to_string(),
110                })?;
111
112        // Emit blank line + section separator comment.
113        w.blank();
114        let type_name = type_name_of(typedef);
115        w.line(&format!("// \u{2500}\u{2500} {type_name} \u{2500}\u{2500}"));
116
117        // Dispatch to the appropriate emitter.
118        match typedef {
119            TypeDef::Message(msg) => {
120                message::emit_message(&mut w, msg, &compiled.registry, &needs_box, type_id);
121                delta::emit_delta(&mut w, msg, &compiled.registry);
122            }
123            TypeDef::Enum(en) => {
124                enum_gen::emit_enum(&mut w, en, &compiled.registry);
125            }
126            TypeDef::Flags(fl) => {
127                flags::emit_flags(&mut w, fl, &compiled.registry);
128            }
129            TypeDef::Union(un) => {
130                union_gen::emit_union(&mut w, un, &compiled.registry, &needs_box, type_id);
131            }
132            TypeDef::Newtype(nt) => {
133                newtype::emit_newtype(&mut w, nt, &compiled.registry, &needs_box);
134            }
135            TypeDef::Config(cfg) => {
136                config::emit_config(&mut w, cfg, &compiled.registry, &needs_box);
137            }
138            _ => {} // non_exhaustive guard
139        }
140    }
141
142    Ok(w.finish())
143}
144
145/// Generate a `mod.rs` file that re-exports child modules.
146pub fn generate_mod_file(module_names: &[&str]) -> String {
147    let mut w = emit::CodeWriter::new();
148    w.line("// Code generated by vexilc. DO NOT EDIT.");
149    w.blank();
150    for name in module_names {
151        w.line(&format!("pub mod {name};"));
152    }
153    w.finish()
154}
155
156/// Returns the name of a TypeDef for use in section separator comments.
157pub(crate) fn type_name_of(typedef: &TypeDef) -> &str {
158    match typedef {
159        TypeDef::Message(m) => m.name.as_str(),
160        TypeDef::Enum(e) => e.name.as_str(),
161        TypeDef::Flags(f) => f.name.as_str(),
162        TypeDef::Union(u) => u.name.as_str(),
163        TypeDef::Newtype(n) => n.name.as_str(),
164        TypeDef::Config(c) => c.name.as_str(),
165        _ => "Unknown", // non_exhaustive guard
166    }
167}
168
169/// Returns true if any declaration in the schema uses a Map type (recursively).
170fn schema_uses_map(compiled: &CompiledSchema) -> bool {
171    for &type_id in &compiled.declarations {
172        if let Some(typedef) = compiled.registry.get(type_id) {
173            if typedef_uses_map(typedef) {
174                return true;
175            }
176        }
177    }
178    false
179}
180
181fn typedef_uses_map(typedef: &TypeDef) -> bool {
182    match typedef {
183        TypeDef::Message(msg) => msg
184            .fields
185            .iter()
186            .any(|f| resolved_type_uses_map(&f.resolved_type)),
187        TypeDef::Union(un) => un.variants.iter().any(|v| {
188            v.fields
189                .iter()
190                .any(|f| resolved_type_uses_map(&f.resolved_type))
191        }),
192        TypeDef::Config(cfg) => cfg
193            .fields
194            .iter()
195            .any(|f| resolved_type_uses_map(&f.resolved_type)),
196        TypeDef::Newtype(nt) => resolved_type_uses_map(&nt.inner_type),
197        _ => false,
198    }
199}
200
201fn resolved_type_uses_map(ty: &ResolvedType) -> bool {
202    match ty {
203        ResolvedType::Map(_, _) => true,
204        ResolvedType::Optional(inner) => resolved_type_uses_map(inner),
205        ResolvedType::Array(inner) => resolved_type_uses_map(inner),
206        ResolvedType::Result(ok, err) => resolved_type_uses_map(ok) || resolved_type_uses_map(err),
207        _ => false,
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn generate_with_no_imports_matches_generate() {
217        let result = vexil_lang::compile("namespace test.gen\nmessage Foo { x @0 : u32 }");
218        let compiled = result.compiled.unwrap();
219        let without = generate(&compiled).unwrap();
220        let with = generate_with_imports(&compiled, None).unwrap();
221        assert_eq!(without, with);
222    }
223
224    #[test]
225    fn generate_with_imports_emits_use_statements() {
226        let result = vexil_lang::compile("namespace test.gen\nmessage Foo { x @0 : u32 }");
227        let compiled = result.compiled.unwrap();
228        let foo_id = compiled.declarations[0];
229        let mut paths = HashMap::new();
230        paths.insert(foo_id, "crate::other::module::Bar".to_string());
231        let code = generate_with_imports(&compiled, Some(&paths)).unwrap();
232        assert!(code.contains("use crate::other::module::Bar;"));
233    }
234
235    #[test]
236    fn generate_mod_file_produces_correct_output() {
237        let output = generate_mod_file(&["foo", "bar"]);
238        assert!(output.contains("// Code generated by vexilc. DO NOT EDIT."));
239        assert!(output.contains("pub mod foo;"));
240        assert!(output.contains("pub mod bar;"));
241    }
242
243    #[test]
244    fn rust_backend_generate_matches_free_function() {
245        use vexil_lang::codegen::CodegenBackend;
246        let result = vexil_lang::compile("namespace test.backend\nmessage Foo { x @0 : u32 }");
247        let compiled = result.compiled.unwrap();
248        let free_fn = generate(&compiled).unwrap();
249        let backend = crate::backend::RustBackend;
250        let trait_fn = backend.generate(&compiled).unwrap();
251        assert_eq!(free_fn, trait_fn);
252    }
253
254    #[test]
255    fn generate_project_emits_cross_file_use_statements() {
256        use vexil_lang::codegen::CodegenBackend;
257        use vexil_lang::resolve::InMemoryLoader;
258        let root_src = "namespace proj.root\nimport proj.dep\nmessage Foo { d @0 : Bar }";
259        let dep_src = "namespace proj.dep\nmessage Bar { y @0 : string }";
260        let mut loader = InMemoryLoader::new();
261        loader.schemas.insert("proj.dep".into(), dep_src.into());
262        let root_path = std::path::PathBuf::from("proj/root.vexil");
263        let result = vexil_lang::compile_project(root_src, &root_path, &loader).unwrap();
264        let backend = crate::backend::RustBackend;
265        let files = backend.generate_project(&result).unwrap();
266        // Find root.rs — it should contain a `use crate::proj::dep::Bar;` statement
267        let root_code = files
268            .iter()
269            .find(|(p, _)| p.to_string_lossy().contains("root.rs"))
270            .map(|(_, code)| code.as_str())
271            .expect("missing root.rs");
272        assert!(
273            root_code.contains("use crate::proj::dep::Bar;"),
274            "root.rs should contain cross-file use statement for Bar, got:\n{root_code}"
275        );
276    }
277
278    #[test]
279    fn rust_backend_generate_project_produces_files() {
280        use vexil_lang::codegen::CodegenBackend;
281        use vexil_lang::resolve::InMemoryLoader;
282        let root_src = "namespace proj.root\nimport proj.dep\nmessage Foo { x @0 : u32 }";
283        let dep_src = "namespace proj.dep\nmessage Bar { y @0 : string }";
284        let mut loader = InMemoryLoader::new();
285        loader.schemas.insert("proj.dep".into(), dep_src.into());
286        let root_path = std::path::PathBuf::from("proj/root.vexil");
287        let result = vexil_lang::compile_project(root_src, &root_path, &loader).unwrap();
288        let backend = crate::backend::RustBackend;
289        let files = backend.generate_project(&result).unwrap();
290        // Should have schema files + mod.rs files
291        assert!(
292            files.len() >= 2,
293            "expected at least 2 files, got {}",
294            files.len()
295        );
296        // Should contain a .rs file for each namespace
297        let has_root = files
298            .keys()
299            .any(|p| p.to_string_lossy().contains("root.rs"));
300        let has_dep = files.keys().any(|p| p.to_string_lossy().contains("dep.rs"));
301        assert!(
302            has_root,
303            "missing root.rs in output: {:?}",
304            files.keys().collect::<Vec<_>>()
305        );
306        assert!(
307            has_dep,
308            "missing dep.rs in output: {:?}",
309            files.keys().collect::<Vec<_>>()
310        );
311    }
312}