pub mod annotations;
pub mod backend;
pub mod boxing;
pub mod config;
pub mod delta;
pub mod emit;
pub mod enum_gen;
pub mod flags;
pub mod message;
pub mod newtype;
pub mod types;
pub mod union_gen;
pub use backend::RustBackend;
use std::collections::HashMap;
use vexil_lang::ir::{CompiledSchema, ResolvedType, TypeDef, TypeId};
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum CodegenError {
#[error("unresolved type {type_id:?} referenced by {referenced_by}")]
UnresolvedType {
type_id: TypeId,
referenced_by: String,
},
}
pub fn generate(compiled: &CompiledSchema) -> Result<String, CodegenError> {
generate_with_imports(compiled, None)
}
pub(crate) fn generate_with_imports(
compiled: &CompiledSchema,
import_paths: Option<&HashMap<TypeId, String>>,
) -> Result<String, CodegenError> {
let needs_box = boxing::detect_boxing(compiled);
let mut w = emit::CodeWriter::new();
w.line("// Code generated by vexilc. DO NOT EDIT.");
let ns = compiled.namespace.join(".");
w.line(&format!("// Source: {ns}"));
w.blank();
if schema_uses_map(compiled) {
w.line("use std::collections::BTreeMap;");
}
w.line("use vexil_runtime::*;");
if let Some(paths) = import_paths {
let mut unique_paths: Vec<&String> = paths.values().collect();
unique_paths.sort();
unique_paths.dedup();
for path in unique_paths {
w.line(&format!("use {path};"));
}
}
w.blank();
let hash = vexil_lang::canonical::schema_hash(compiled);
let hash_str = hash
.iter()
.map(|b| format!("0x{b:02x}"))
.collect::<Vec<_>>()
.join(", ");
w.line(&format!("pub const SCHEMA_HASH: [u8; 32] = [{hash_str}];"));
if let Some(ref version) = compiled.annotations.version {
w.line(&format!("pub const SCHEMA_VERSION: &str = \"{version}\";"));
}
for &type_id in &compiled.declarations {
let typedef =
compiled
.registry
.get(type_id)
.ok_or_else(|| CodegenError::UnresolvedType {
type_id,
referenced_by: "declarations".to_string(),
})?;
w.blank();
let type_name = type_name_of(typedef);
w.line(&format!("// \u{2500}\u{2500} {type_name} \u{2500}\u{2500}"));
match typedef {
TypeDef::Message(msg) => {
message::emit_message(&mut w, msg, &compiled.registry, &needs_box, type_id);
delta::emit_delta(&mut w, msg, &compiled.registry);
}
TypeDef::Enum(en) => {
enum_gen::emit_enum(&mut w, en, &compiled.registry);
}
TypeDef::Flags(fl) => {
flags::emit_flags(&mut w, fl, &compiled.registry);
}
TypeDef::Union(un) => {
union_gen::emit_union(&mut w, un, &compiled.registry, &needs_box, type_id);
}
TypeDef::Newtype(nt) => {
newtype::emit_newtype(&mut w, nt, &compiled.registry, &needs_box);
}
TypeDef::Config(cfg) => {
config::emit_config(&mut w, cfg, &compiled.registry, &needs_box);
}
_ => {} }
}
Ok(w.finish())
}
pub fn generate_mod_file(module_names: &[&str]) -> String {
let mut w = emit::CodeWriter::new();
w.line("// Code generated by vexilc. DO NOT EDIT.");
w.blank();
for name in module_names {
w.line(&format!("pub mod {name};"));
}
w.finish()
}
pub(crate) fn type_name_of(typedef: &TypeDef) -> &str {
match typedef {
TypeDef::Message(m) => m.name.as_str(),
TypeDef::Enum(e) => e.name.as_str(),
TypeDef::Flags(f) => f.name.as_str(),
TypeDef::Union(u) => u.name.as_str(),
TypeDef::Newtype(n) => n.name.as_str(),
TypeDef::Config(c) => c.name.as_str(),
_ => "Unknown", }
}
fn schema_uses_map(compiled: &CompiledSchema) -> bool {
for &type_id in &compiled.declarations {
if let Some(typedef) = compiled.registry.get(type_id) {
if typedef_uses_map(typedef) {
return true;
}
}
}
false
}
fn typedef_uses_map(typedef: &TypeDef) -> bool {
match typedef {
TypeDef::Message(msg) => msg
.fields
.iter()
.any(|f| resolved_type_uses_map(&f.resolved_type)),
TypeDef::Union(un) => un.variants.iter().any(|v| {
v.fields
.iter()
.any(|f| resolved_type_uses_map(&f.resolved_type))
}),
TypeDef::Config(cfg) => cfg
.fields
.iter()
.any(|f| resolved_type_uses_map(&f.resolved_type)),
TypeDef::Newtype(nt) => resolved_type_uses_map(&nt.inner_type),
_ => false,
}
}
fn resolved_type_uses_map(ty: &ResolvedType) -> bool {
match ty {
ResolvedType::Map(_, _) => true,
ResolvedType::Optional(inner) => resolved_type_uses_map(inner),
ResolvedType::Array(inner) => resolved_type_uses_map(inner),
ResolvedType::Result(ok, err) => resolved_type_uses_map(ok) || resolved_type_uses_map(err),
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generate_with_no_imports_matches_generate() {
let result = vexil_lang::compile("namespace test.gen\nmessage Foo { x @0 : u32 }");
let compiled = result.compiled.unwrap();
let without = generate(&compiled).unwrap();
let with = generate_with_imports(&compiled, None).unwrap();
assert_eq!(without, with);
}
#[test]
fn generate_with_imports_emits_use_statements() {
let result = vexil_lang::compile("namespace test.gen\nmessage Foo { x @0 : u32 }");
let compiled = result.compiled.unwrap();
let foo_id = compiled.declarations[0];
let mut paths = HashMap::new();
paths.insert(foo_id, "crate::other::module::Bar".to_string());
let code = generate_with_imports(&compiled, Some(&paths)).unwrap();
assert!(code.contains("use crate::other::module::Bar;"));
}
#[test]
fn generate_mod_file_produces_correct_output() {
let output = generate_mod_file(&["foo", "bar"]);
assert!(output.contains("// Code generated by vexilc. DO NOT EDIT."));
assert!(output.contains("pub mod foo;"));
assert!(output.contains("pub mod bar;"));
}
#[test]
fn rust_backend_generate_matches_free_function() {
use vexil_lang::codegen::CodegenBackend;
let result = vexil_lang::compile("namespace test.backend\nmessage Foo { x @0 : u32 }");
let compiled = result.compiled.unwrap();
let free_fn = generate(&compiled).unwrap();
let backend = crate::backend::RustBackend;
let trait_fn = backend.generate(&compiled).unwrap();
assert_eq!(free_fn, trait_fn);
}
#[test]
fn generate_project_emits_cross_file_use_statements() {
use vexil_lang::codegen::CodegenBackend;
use vexil_lang::resolve::InMemoryLoader;
let root_src = "namespace proj.root\nimport proj.dep\nmessage Foo { d @0 : Bar }";
let dep_src = "namespace proj.dep\nmessage Bar { y @0 : string }";
let mut loader = InMemoryLoader::new();
loader.schemas.insert("proj.dep".into(), dep_src.into());
let root_path = std::path::PathBuf::from("proj/root.vexil");
let result = vexil_lang::compile_project(root_src, &root_path, &loader).unwrap();
let backend = crate::backend::RustBackend;
let files = backend.generate_project(&result).unwrap();
let root_code = files
.iter()
.find(|(p, _)| p.to_string_lossy().contains("root.rs"))
.map(|(_, code)| code.as_str())
.expect("missing root.rs");
assert!(
root_code.contains("use crate::proj::dep::Bar;"),
"root.rs should contain cross-file use statement for Bar, got:\n{root_code}"
);
}
#[test]
fn rust_backend_generate_project_produces_files() {
use vexil_lang::codegen::CodegenBackend;
use vexil_lang::resolve::InMemoryLoader;
let root_src = "namespace proj.root\nimport proj.dep\nmessage Foo { x @0 : u32 }";
let dep_src = "namespace proj.dep\nmessage Bar { y @0 : string }";
let mut loader = InMemoryLoader::new();
loader.schemas.insert("proj.dep".into(), dep_src.into());
let root_path = std::path::PathBuf::from("proj/root.vexil");
let result = vexil_lang::compile_project(root_src, &root_path, &loader).unwrap();
let backend = crate::backend::RustBackend;
let files = backend.generate_project(&result).unwrap();
assert!(
files.len() >= 2,
"expected at least 2 files, got {}",
files.len()
);
let has_root = files
.keys()
.any(|p| p.to_string_lossy().contains("root.rs"));
let has_dep = files.keys().any(|p| p.to_string_lossy().contains("dep.rs"));
assert!(
has_root,
"missing root.rs in output: {:?}",
files.keys().collect::<Vec<_>>()
);
assert!(
has_dep,
"missing dep.rs in output: {:?}",
files.keys().collect::<Vec<_>>()
);
}
}