Skip to main content

vexil_codegen_rust/
lib.rs

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