Skip to main content

synapse_codegen_cfs/
rust.rs

1use synapse_parser::ast::{
2    ArraySuffix, BaseType, ConstDecl, EnumDef, Item, Literal, MessageDef, PrimitiveType, StructDef,
3    SynFile, TypeExpr,
4};
5
6use crate::{
7    constants::{ConstContext, const_context, resolve_ident_to_u64},
8    error::CodegenError,
9    types::{GENERATED_BANNER, ResolvedConstants, RustOptions},
10    util::{
11        emit_doc_lines, emit_indented_doc_lines, find_cc_attr, find_mid_attr, import_rust_module,
12        packet_is_command, packet_item, to_screaming_snake,
13    },
14    validate::validate_supported,
15};
16
17/// Generate `#[repr(C)]` Rust structs compatible with NASA cFS bindings.
18///
19/// `command` and `telemetry` packets become structs with the cFS header as the
20/// first field, matching the C ABI layout. `struct` and `table` items remain
21/// plain data structs. MID constants are emitted as `pub const`.
22pub fn generate_rust(file: &SynFile, opts: &RustOptions) -> String {
23    try_generate_rust(file, opts).expect("parsed Synapse file is not supported by cFS Rust codegen")
24}
25
26/// Try to generate `#[repr(C)]` Rust structs compatible with NASA cFS bindings.
27pub fn try_generate_rust(file: &SynFile, opts: &RustOptions) -> Result<String, CodegenError> {
28    try_generate_rust_with_constants(file, opts, &ResolvedConstants::new())
29}
30
31/// Try to generate Rust bindings with additional imported constants available for attributes.
32pub fn try_generate_rust_with_constants(
33    file: &SynFile,
34    opts: &RustOptions,
35    imported_constants: &ResolvedConstants,
36) -> Result<String, CodegenError> {
37    let constants = const_context(file, imported_constants);
38    validate_supported(file, &constants)?;
39    let mut out = format!("// {GENERATED_BANNER}\n\n");
40    emit_rust_imports(file, &mut out);
41    emit_rust_items(file, opts, &mut out, &constants);
42    Ok(out)
43}
44
45fn emit_rust_imports(file: &SynFile, out: &mut String) {
46    let mut emitted = false;
47    for item in &file.items {
48        if let Item::Import(import) = item {
49            out.push_str(&format!(
50                "use crate::{};\n",
51                import_rust_module(&import.path)
52            ));
53            emitted = true;
54        }
55    }
56    if emitted {
57        out.push('\n');
58    }
59}
60
61fn emit_rust_items(
62    file: &SynFile,
63    opts: &RustOptions,
64    out: &mut String,
65    constants: &ConstContext<'_>,
66) {
67    // First pass: MID consts for Software Bus packets with @mid
68    let mut has_mids = false;
69    for item in &file.items {
70        if let Some(m) = packet_item(item) {
71            if let Some(mid) = find_mid_attr(&m.attrs) {
72                if !has_mids {
73                    out.push_str("// Message IDs\n");
74                    has_mids = true;
75                }
76                let const_name = format!("{}_MID", to_screaming_snake(&m.name));
77                let val = rust_mid_str(mid, constants);
78                out.push_str(&format!("pub const {}: u16 = {};\n", const_name, val));
79            }
80        }
81    }
82    if has_mids {
83        out.push('\n');
84    }
85
86    let mut has_ccs = false;
87    for item in &file.items {
88        if let Item::Command(m) = item {
89            if let Some(cc) = find_cc_attr(&m.attrs) {
90                if !has_ccs {
91                    out.push_str("// Command Codes\n");
92                    has_ccs = true;
93                }
94                let const_name = format!("{}_CC", to_screaming_snake(&m.name));
95                let val = rust_cc_str(cc, constants);
96                out.push_str(&format!("pub const {}: u16 = {};\n", const_name, val));
97            }
98        }
99    }
100    if has_ccs {
101        out.push('\n');
102    }
103
104    // Second pass: types
105    for item in &file.items {
106        match item {
107            Item::Namespace(_) | Item::Import(_) => {}
108            Item::Const(c) => emit_rust_const(out, c),
109            Item::Enum(e) => emit_rust_enum(out, e),
110            Item::Struct(s) | Item::Table(s) => emit_rust_struct(out, s),
111            Item::Command(m) | Item::Telemetry(m) | Item::Message(m) => {
112                emit_rust_message(out, m, opts)
113            }
114        }
115    }
116}
117
118fn emit_rust_const(out: &mut String, c: &ConstDecl) {
119    emit_doc_lines(out, &c.doc);
120    let val = rust_typed_literal_str(&c.value, &c.ty);
121    let ty = rust_field_type_str(&c.ty);
122    out.push_str(&format!("pub const {}: {} = {};\n\n", c.name, ty, val));
123}
124
125fn emit_rust_enum(out: &mut String, e: &EnumDef) {
126    let Some(repr) = e.repr else {
127        return;
128    };
129
130    emit_doc_lines(out, &e.doc);
131    out.push_str(&format!(
132        "pub type {} = {};\n",
133        e.name,
134        rust_primitive_str(repr)
135    ));
136
137    let enum_prefix = to_screaming_snake(&e.name);
138    for variant in &e.variants {
139        emit_doc_lines(out, &variant.doc);
140        let value = variant
141            .value
142            .expect("represented enum variants validated before emission");
143        out.push_str(&format!(
144            "pub const {}_{}: {} = {};\n",
145            enum_prefix,
146            to_screaming_snake(&variant.name),
147            e.name,
148            value
149        ));
150    }
151    out.push('\n');
152}
153
154fn emit_rust_struct(out: &mut String, s: &StructDef) {
155    emit_doc_lines(out, &s.doc);
156    out.push_str("#[repr(C)]\n");
157    out.push_str(&format!("pub struct {} {{\n", s.name));
158    for f in &s.fields {
159        emit_indented_doc_lines(out, &f.doc);
160        out.push_str(&format!(
161            "    pub {}: {},\n",
162            f.name,
163            rust_field_type_str(&f.ty)
164        ));
165    }
166    out.push_str("}\n\n");
167}
168
169fn emit_rust_message(out: &mut String, m: &MessageDef, opts: &RustOptions) {
170    let header_type = if packet_is_command(m) {
171        opts.cmd_header
172    } else {
173        opts.tlm_header
174    };
175    let qualified = if opts.cfs_module.is_empty() {
176        header_type.to_string()
177    } else {
178        format!("{}::{}", opts.cfs_module, header_type)
179    };
180
181    emit_doc_lines(out, &m.doc);
182
183    out.push_str("#[repr(C)]\n");
184    out.push_str(&format!("pub struct {} {{\n", m.name));
185    out.push_str(&format!("    pub cfs_header: {},\n", qualified));
186    for f in &m.fields {
187        emit_indented_doc_lines(out, &f.doc);
188        let ty = rust_field_type_str(&f.ty);
189        out.push_str(&format!("    pub {}: {},\n", f.name, ty));
190    }
191    out.push_str("}\n\n");
192}
193
194fn rust_field_type_str(ty: &TypeExpr) -> String {
195    if ty.base == BaseType::String {
196        return match &ty.array {
197            None | Some(ArraySuffix::Dynamic) => "*const u8".to_string(),
198            Some(ArraySuffix::Fixed(n)) | Some(ArraySuffix::Bounded(n)) => {
199                format!("[u8; {}]", n)
200            }
201        };
202    }
203
204    let base = rust_base_type_str(&ty.base);
205    match &ty.array {
206        None => base,
207        Some(ArraySuffix::Fixed(n)) => format!("[{}; {}]", base, n),
208        // Dynamic/bounded: use a raw slice pointer; no alloc in cFS context.
209        Some(ArraySuffix::Dynamic) => format!("*const {}", base),
210        Some(ArraySuffix::Bounded(n)) => format!("*const {}  /* max {} */", base, n),
211    }
212}
213
214fn rust_base_type_str(base: &BaseType) -> String {
215    match base {
216        BaseType::String => "*const u8".to_string(),
217        BaseType::Primitive(p) => rust_primitive_str(*p).to_string(),
218        BaseType::Ref(segments) => segments.join("::"),
219    }
220}
221
222fn rust_primitive_str(p: PrimitiveType) -> &'static str {
223    match p {
224        PrimitiveType::F32 => "f32",
225        PrimitiveType::F64 => "f64",
226        PrimitiveType::I8 => "i8",
227        PrimitiveType::I16 => "i16",
228        PrimitiveType::I32 => "i32",
229        PrimitiveType::I64 => "i64",
230        PrimitiveType::U8 => "u8",
231        PrimitiveType::U16 => "u16",
232        PrimitiveType::U32 => "u32",
233        PrimitiveType::U64 => "u64",
234        PrimitiveType::Bool => "bool",
235        PrimitiveType::Bytes => "*const u8",
236    }
237}
238
239fn rust_mid_str(lit: &Literal, constants: &ConstContext<'_>) -> String {
240    match lit {
241        Literal::Hex(n) => format!("0x{:04X}", n),
242        Literal::Int(n) => n.to_string(),
243        Literal::Ident(segs) if constants.is_local_bare_ident(segs) => segs.join("::"),
244        Literal::Ident(segs) => resolve_ident_to_u64(segs, constants)
245            .map(|value| format!("0x{:04X}", value))
246            .unwrap_or_else(|| segs.join("::")),
247        other => rust_literal_str(other),
248    }
249}
250
251fn rust_cc_str(lit: &Literal, constants: &ConstContext<'_>) -> String {
252    match lit {
253        Literal::Hex(n) => format!("0x{:X}", n),
254        Literal::Int(n) => n.to_string(),
255        Literal::Ident(segs) if constants.is_local_bare_ident(segs) => segs.join("::"),
256        Literal::Ident(segs) => resolve_ident_to_u64(segs, constants)
257            .map(|value| value.to_string())
258            .unwrap_or_else(|| segs.join("::")),
259        other => rust_literal_str(other),
260    }
261}
262
263fn rust_literal_str(lit: &Literal) -> String {
264    match lit {
265        Literal::Hex(n) => format!("0x{:X}", n),
266        Literal::Int(n) => n.to_string(),
267        Literal::Bool(b) => b.to_string(),
268        Literal::Float(f) => {
269            let s = format!("{}", f);
270            if s.contains('.') || s.contains('e') {
271                s
272            } else {
273                format!("{}.0", s)
274            }
275        }
276        Literal::Str(s) => format!("{:?}", s),
277        Literal::Ident(segments) => segments.join("::"),
278    }
279}
280
281fn rust_typed_literal_str(lit: &Literal, ty: &TypeExpr) -> String {
282    match (lit, &ty.base) {
283        (Literal::Hex(n), BaseType::Primitive(p)) => rust_hex_str(*n, *p),
284        _ => rust_literal_str(lit),
285    }
286}
287
288fn rust_hex_str(value: u64, ty: PrimitiveType) -> String {
289    match ty {
290        PrimitiveType::U8 | PrimitiveType::I8 => format!("0x{:02X}", value),
291        PrimitiveType::U16 | PrimitiveType::I16 => format!("0x{:04X}", value),
292        PrimitiveType::U32 | PrimitiveType::I32 => format!("0x{:08X}", value),
293        PrimitiveType::U64 | PrimitiveType::I64 => format!("0x{:016X}", value),
294        PrimitiveType::F32 | PrimitiveType::F64 | PrimitiveType::Bool | PrimitiveType::Bytes => {
295            format!("0x{:X}", value)
296        }
297    }
298}