vexil_codegen_rust/
lib.rs1pub 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
41pub fn generate(compiled: &CompiledSchema) -> Result<String, CodegenError> {
43 generate_with_imports(compiled, None)
44}
45
46pub(crate) fn generate_with_imports(
51 compiled: &CompiledSchema,
52 import_paths: Option<&HashMap<TypeId, String>>,
53) -> Result<String, CodegenError> {
54 let needs_box = boxing::detect_boxing(compiled);
56
57 let mut w = emit::CodeWriter::new();
59
60 w.line("// Code generated by vexilc. DO NOT EDIT.");
62 let ns = compiled.namespace.join(".");
63 w.line(&format!("// Source: {ns}"));
64
65 w.blank();
67
68 if schema_uses_map(compiled) {
70 w.line("use std::collections::BTreeMap;");
71 }
72 w.line("use vexil_runtime::*;");
73
74 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 w.blank();
86
87 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 if let Some(ref version) = compiled.annotations.version {
98 w.line(&format!("pub const SCHEMA_VERSION: &str = \"{version}\";"));
99 }
100
101 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 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 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 _ => {} }
140 }
141
142 Ok(w.finish())
143}
144
145pub 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
156pub(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", }
167}
168
169fn 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 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 assert!(
292 files.len() >= 2,
293 "expected at least 2 files, got {}",
294 files.len()
295 );
296 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}