vexil_codegen_rust/
lib.rs1pub 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
29pub fn generate(compiled: &CompiledSchema) -> Result<String, CodegenError> {
31 generate_with_imports(compiled, None)
32}
33
34pub(crate) fn generate_with_imports(
39 compiled: &CompiledSchema,
40 import_paths: Option<&HashMap<TypeId, String>>,
41) -> Result<String, CodegenError> {
42 let needs_box = boxing::detect_boxing(compiled);
44
45 let mut w = emit::CodeWriter::new();
47
48 w.line("// Code generated by vexilc. DO NOT EDIT.");
50 let ns = compiled.namespace.join(".");
51 w.line(&format!("// Source: {ns}"));
52
53 w.blank();
55
56 if schema_uses_map(compiled) {
58 w.line("use std::collections::BTreeMap;");
59 }
60 w.line("use vexil_runtime::*;");
61
62 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 w.blank();
74
75 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 if let Some(ref version) = compiled.annotations.version {
86 w.line(&format!("pub const SCHEMA_VERSION: &str = \"{version}\";"));
87 }
88
89 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 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 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 _ => {} }
128 }
129
130 Ok(w.finish())
131}
132
133pub 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
144pub(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", }
155}
156
157fn 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 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 assert!(
280 files.len() >= 2,
281 "expected at least 2 files, got {}",
282 files.len()
283 );
284 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}