1pub mod backend;
7pub mod delta;
8pub mod emit;
9pub mod enum_gen;
10pub mod flags;
11pub mod message;
12pub mod newtype;
13pub mod types;
14pub mod union_gen;
15
16pub use backend::GoBackend;
17
18use vexil_lang::ir::{CompiledSchema, TypeDef};
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: vexil_lang::ir::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(
38 compiled: &CompiledSchema,
39 import_types: Option<&std::collections::HashMap<String, String>>,
40) -> Result<String, CodegenError> {
41 let mut w = emit::CodeWriter::new();
42
43 w.line("// Code generated by vexilc. DO NOT EDIT.");
45 let ns = compiled.namespace.join(".");
46 w.line(&format!("// Source: {ns}"));
47 w.blank();
48
49 let pkg = compiled
51 .namespace
52 .last()
53 .map(|s| s.as_str())
54 .unwrap_or("main");
55 w.line(&format!("package {pkg}"));
56 w.blank();
57
58 let has_codecs = compiled.declarations.iter().any(|&id| {
60 compiled
61 .registry
62 .get(id)
63 .is_some_and(|td| !matches!(td, TypeDef::Config(_)))
64 });
65
66 let has_unions = compiled.declarations.iter().any(|&id| {
67 compiled
68 .registry
69 .get(id)
70 .is_some_and(|td| matches!(td, TypeDef::Union(_)))
71 });
72
73 let has_cross_imports = import_types.is_some_and(|m| !m.is_empty());
75
76 let needs_fmt = has_unions;
77 let needs_grouped = has_cross_imports || (has_codecs && needs_fmt);
78
79 if has_codecs || has_cross_imports {
80 if needs_grouped {
81 w.line("import (");
82 w.indent();
83 if needs_fmt {
84 w.line("\"fmt\"");
85 }
86 if has_codecs {
87 w.blank();
88 w.line("vexil \"github.com/vexil-lang/vexil/packages/runtime-go\"");
89 }
90 if let Some(imports) = import_types {
91 let mut paths: Vec<&String> = imports.values().collect();
92 paths.sort();
93 paths.dedup();
94 for path in paths {
95 w.line(&format!("\"{path}\""));
96 }
97 }
98 w.dedent();
99 w.line(")");
100 } else if has_codecs {
101 w.line("import vexil \"github.com/vexil-lang/vexil/packages/runtime-go\"");
102 }
103 w.blank();
104 }
105
106 let hash = vexil_lang::canonical::schema_hash(compiled);
108 let hash_str = hash
109 .iter()
110 .map(|b| format!("0x{b:02x}"))
111 .collect::<Vec<_>>()
112 .join(", ");
113 w.line(&format!("var SchemaHash = [32]byte{{{hash_str}}}"));
114
115 if let Some(ref version) = compiled.annotations.version {
117 w.line(&format!("const SchemaVersion = \"{version}\""));
118 }
119
120 for &type_id in &compiled.declarations {
122 let typedef =
123 compiled
124 .registry
125 .get(type_id)
126 .ok_or_else(|| CodegenError::UnresolvedType {
127 type_id,
128 referenced_by: "declarations".to_string(),
129 })?;
130
131 w.blank();
132 let type_name = type_name_of(typedef);
133 w.line(&format!("// \u{2500}\u{2500} {type_name} \u{2500}\u{2500}"));
134
135 match typedef {
136 TypeDef::Message(msg) => {
137 message::emit_message(&mut w, msg, &compiled.registry);
138 delta::emit_delta(&mut w, msg, &compiled.registry);
139 }
140 TypeDef::Enum(en) => {
141 enum_gen::emit_enum(&mut w, en, &compiled.registry);
142 }
143 TypeDef::Flags(fl) => {
144 flags::emit_flags(&mut w, fl, &compiled.registry);
145 }
146 TypeDef::Union(un) => {
147 union_gen::emit_union(&mut w, un, &compiled.registry);
148 }
149 TypeDef::Newtype(nt) => {
150 newtype::emit_newtype(&mut w, nt, &compiled.registry);
151 }
152 TypeDef::Config(cfg) => {
153 message::emit_config(&mut w, cfg, &compiled.registry);
154 }
155 _ => {} }
157 }
158
159 Ok(w.finish())
160}
161
162pub(crate) fn type_name_of(typedef: &TypeDef) -> &str {
164 match typedef {
165 TypeDef::Message(m) => m.name.as_str(),
166 TypeDef::Enum(e) => e.name.as_str(),
167 TypeDef::Flags(f) => f.name.as_str(),
168 TypeDef::Union(u) => u.name.as_str(),
169 TypeDef::Newtype(n) => n.name.as_str(),
170 TypeDef::Config(c) => c.name.as_str(),
171 _ => "Unknown",
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn generate_produces_output() {
181 let result = vexil_lang::compile("namespace test.gen\nmessage Foo { x @0 : u32 }");
182 let compiled = result.compiled.unwrap();
183 let code = generate(&compiled).unwrap();
184 assert!(code.contains("type Foo struct"));
185 assert!(code.contains("func (m *Foo) Pack"));
186 assert!(code.contains("func (m *Foo) Unpack"));
187 assert!(code.contains("package gen"));
188 }
189
190 #[test]
191 fn generate_with_no_imports_matches_generate() {
192 let result = vexil_lang::compile("namespace test.gen\nmessage Foo { x @0 : u32 }");
193 let compiled = result.compiled.unwrap();
194 let without = generate(&compiled).unwrap();
195 let with = generate_with_imports(&compiled, None).unwrap();
196 assert_eq!(without, with);
197 }
198
199 #[test]
200 fn go_backend_generate_matches_free_function() {
201 use vexil_lang::codegen::CodegenBackend;
202 let result = vexil_lang::compile("namespace test.backend\nmessage Foo { x @0 : u32 }");
203 let compiled = result.compiled.unwrap();
204 let free_fn = generate(&compiled).unwrap();
205 let backend = crate::backend::GoBackend;
206 let trait_fn = backend.generate(&compiled).unwrap();
207 assert_eq!(free_fn, trait_fn);
208 }
209}