Skip to main content

vexil_codegen_go/
lib.rs

1//! Go code generation backend for the Vexil schema compiler.
2//!
3//! Generates Go structs implementing the `Packer`/`Unpacker` interfaces
4//! from the `github.com/vexil-lang/vexil/packages/runtime-go` module.
5
6pub 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
29/// Generate Go 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 Go code for a compiled schema, with optional cross-file import information.
35///
36/// `import_types` maps type names that are imported from other packages.
37pub(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    // Header
44    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    // Package declaration — last segment of namespace
50    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    // Import block
59    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    // Check if we need cross-package imports
74    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    // Schema hash
107    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    // Schema version
116    if let Some(ref version) = compiled.annotations.version {
117        w.line(&format!("const SchemaVersion = \"{version}\""));
118    }
119
120    // Emit each declared type
121    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            _ => {} // non_exhaustive guard
156        }
157    }
158
159    Ok(w.finish())
160}
161
162/// Returns the name of a TypeDef for use in section separator comments.
163pub(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}