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::{CfsOptions, 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 validation options.
32pub fn try_generate_rust_with_options(
33    file: &SynFile,
34    opts: &RustOptions,
35    options: &CfsOptions,
36) -> Result<String, CodegenError> {
37    try_generate_rust_with_constants_and_options(file, opts, &ResolvedConstants::new(), options)
38}
39
40/// Try to generate Rust bindings with additional imported constants available for attributes.
41pub fn try_generate_rust_with_constants(
42    file: &SynFile,
43    opts: &RustOptions,
44    imported_constants: &ResolvedConstants,
45) -> Result<String, CodegenError> {
46    try_generate_rust_with_constants_and_options(
47        file,
48        opts,
49        imported_constants,
50        &CfsOptions::default(),
51    )
52}
53
54/// Try to generate Rust bindings with imported constants and validation options.
55pub fn try_generate_rust_with_constants_and_options(
56    file: &SynFile,
57    opts: &RustOptions,
58    imported_constants: &ResolvedConstants,
59    options: &CfsOptions,
60) -> Result<String, CodegenError> {
61    let constants = const_context(file, imported_constants);
62    validate_supported(file, &constants, options)?;
63    let mut out = format!("// {GENERATED_BANNER}\n\n");
64    emit_rust_imports(file, &mut out);
65    emit_rust_items(file, opts, &mut out, &constants);
66    Ok(out)
67}
68
69fn emit_rust_imports(file: &SynFile, out: &mut String) {
70    let mut emitted = false;
71    for item in &file.items {
72        if let Item::Import(import) = item {
73            out.push_str(&format!(
74                "use crate::{};\n",
75                import_rust_module(&import.path)
76            ));
77            emitted = true;
78        }
79    }
80    if emitted {
81        out.push('\n');
82    }
83}
84
85fn emit_rust_items(
86    file: &SynFile,
87    opts: &RustOptions,
88    out: &mut String,
89    constants: &ConstContext<'_>,
90) {
91    emit_rust_mid_consts(file, out, constants);
92    emit_rust_command_code_consts(file, out, constants);
93    emit_rust_types(file, opts, out);
94}
95
96fn emit_rust_mid_consts(file: &SynFile, out: &mut String, constants: &ConstContext<'_>) {
97    let mut has_mids = false;
98    for item in &file.items {
99        if let Some(m) = packet_item(item) {
100            if let Some(mid) = find_mid_attr(&m.attrs) {
101                if !has_mids {
102                    out.push_str("// Message IDs\n");
103                    has_mids = true;
104                }
105                let const_name = format!("{}_MID", to_screaming_snake(&m.name));
106                let val = rust_mid_str(mid, constants);
107                out.push_str(&format!("pub const {}: u16 = {};\n", const_name, val));
108            }
109        }
110    }
111    if has_mids {
112        out.push('\n');
113    }
114}
115
116fn emit_rust_command_code_consts(file: &SynFile, out: &mut String, constants: &ConstContext<'_>) {
117    let mut has_ccs = false;
118    for item in &file.items {
119        if let Item::Command(m) = item {
120            if let Some(cc) = find_cc_attr(&m.attrs) {
121                if !has_ccs {
122                    out.push_str("// Command Codes\n");
123                    has_ccs = true;
124                }
125                let const_name = format!("{}_CC", to_screaming_snake(&m.name));
126                let val = rust_cc_str(cc, constants);
127                out.push_str(&format!("pub const {}: u16 = {};\n", const_name, val));
128            }
129        }
130    }
131    if has_ccs {
132        out.push('\n');
133    }
134}
135
136fn emit_rust_types(file: &SynFile, opts: &RustOptions, out: &mut String) {
137    for item in &file.items {
138        if emit_rust_named_item(out, item) {
139            continue;
140        }
141        if let Item::Command(m) | Item::Telemetry(m) | Item::Message(m) = item {
142            emit_rust_message(out, m, opts);
143        }
144    }
145}
146
147fn emit_rust_named_item(out: &mut String, item: &Item) -> bool {
148    match item {
149        Item::Const(c) => emit_rust_const(out, c),
150        Item::Enum(e) => emit_rust_enum(out, e),
151        Item::Struct(s) | Item::Table(s) => emit_rust_struct(out, s),
152        _ => return false,
153    }
154    true
155}
156
157fn emit_rust_const(out: &mut String, c: &ConstDecl) {
158    emit_doc_lines(out, &c.doc);
159    let val = rust_typed_literal_str(&c.value, &c.ty);
160    let ty = rust_field_type_str(&c.ty);
161    out.push_str(&format!("pub const {}: {} = {};\n\n", c.name, ty, val));
162}
163
164fn emit_rust_enum(out: &mut String, e: &EnumDef) {
165    let Some(repr) = e.repr else {
166        return;
167    };
168
169    emit_doc_lines(out, &e.doc);
170    out.push_str(&format!(
171        "pub type {} = {};\n",
172        e.name,
173        rust_primitive_str(repr)
174    ));
175
176    let enum_prefix = to_screaming_snake(&e.name);
177    for variant in &e.variants {
178        emit_doc_lines(out, &variant.doc);
179        let value = variant
180            .value
181            .expect("represented enum variants validated before emission");
182        out.push_str(&format!(
183            "pub const {}_{}: {} = {};\n",
184            enum_prefix,
185            to_screaming_snake(&variant.name),
186            e.name,
187            value
188        ));
189    }
190    out.push('\n');
191}
192
193fn emit_rust_struct(out: &mut String, s: &StructDef) {
194    emit_doc_lines(out, &s.doc);
195    out.push_str("#[repr(C)]\n");
196    out.push_str(&format!("pub struct {} {{\n", s.name));
197    for f in &s.fields {
198        emit_indented_doc_lines(out, &f.doc);
199        out.push_str(&format!(
200            "    pub {}: {},\n",
201            f.name,
202            rust_field_type_str(&f.ty)
203        ));
204    }
205    out.push_str("}\n\n");
206}
207
208fn emit_rust_message(out: &mut String, m: &MessageDef, opts: &RustOptions) {
209    let header_type = if packet_is_command(m) {
210        opts.cmd_header
211    } else {
212        opts.tlm_header
213    };
214    let qualified = if opts.cfs_module.is_empty() {
215        header_type.to_string()
216    } else {
217        format!("{}::{}", opts.cfs_module, header_type)
218    };
219
220    emit_doc_lines(out, &m.doc);
221
222    out.push_str("#[repr(C)]\n");
223    out.push_str(&format!("pub struct {} {{\n", m.name));
224    out.push_str(&format!("    pub cfs_header: {},\n", qualified));
225    for f in &m.fields {
226        emit_indented_doc_lines(out, &f.doc);
227        let ty = rust_field_type_str(&f.ty);
228        out.push_str(&format!("    pub {}: {},\n", f.name, ty));
229    }
230    out.push_str("}\n\n");
231}
232
233fn rust_field_type_str(ty: &TypeExpr) -> String {
234    if ty.base == BaseType::String {
235        return rust_string_type_str(&ty.array);
236    }
237
238    let base = rust_base_type_str(&ty.base);
239    rust_array_type_str(base, &ty.array)
240}
241
242fn rust_string_type_str(array: &Option<ArraySuffix>) -> String {
243    match array {
244        None | Some(ArraySuffix::Dynamic) => "*const u8".to_string(),
245        Some(ArraySuffix::Fixed(n)) | Some(ArraySuffix::Bounded(n)) => {
246            format!("[u8; {}]", n)
247        }
248    }
249}
250
251fn rust_array_type_str(base: String, array: &Option<ArraySuffix>) -> String {
252    match array {
253        None => base,
254        Some(ArraySuffix::Fixed(n)) => format!("[{}; {}]", base, n),
255        // Dynamic/bounded: use a raw slice pointer; no alloc in cFS context.
256        Some(ArraySuffix::Dynamic) => format!("*const {}", base),
257        Some(ArraySuffix::Bounded(n)) => format!("*const {}  /* max {} */", base, n),
258    }
259}
260
261fn rust_base_type_str(base: &BaseType) -> String {
262    match base {
263        BaseType::String => "*const u8".to_string(),
264        BaseType::Primitive(p) => rust_primitive_str(*p).to_string(),
265        BaseType::Ref(segments) => segments.join("::"),
266    }
267}
268
269fn rust_primitive_str(p: PrimitiveType) -> &'static str {
270    const RUST_TYPES: &[(PrimitiveType, &str)] = &[
271        (PrimitiveType::F32, "f32"),
272        (PrimitiveType::F64, "f64"),
273        (PrimitiveType::I8, "i8"),
274        (PrimitiveType::I16, "i16"),
275        (PrimitiveType::I32, "i32"),
276        (PrimitiveType::I64, "i64"),
277        (PrimitiveType::U8, "u8"),
278        (PrimitiveType::U16, "u16"),
279        (PrimitiveType::U32, "u32"),
280        (PrimitiveType::U64, "u64"),
281        (PrimitiveType::Bool, "bool"),
282        (PrimitiveType::Bytes, "*const u8"),
283    ];
284
285    RUST_TYPES
286        .iter()
287        .find_map(|(ty, name)| (*ty == p).then_some(*name))
288        .expect("all primitive types have Rust names")
289}
290
291fn rust_mid_str(lit: &Literal, constants: &ConstContext<'_>) -> String {
292    match lit {
293        Literal::Hex(n) => format!("0x{:04X}", n),
294        Literal::Int(n) => n.to_string(),
295        Literal::Ident(segs) if constants.is_local_bare_ident(segs) => segs.join("::"),
296        Literal::Ident(segs) => resolve_ident_to_u64(segs, constants)
297            .map(|value| format!("0x{:04X}", value))
298            .unwrap_or_else(|| segs.join("::")),
299        other => rust_literal_str(other),
300    }
301}
302
303fn rust_cc_str(lit: &Literal, constants: &ConstContext<'_>) -> String {
304    match lit {
305        Literal::Hex(n) => format!("0x{:X}", n),
306        Literal::Int(n) => n.to_string(),
307        Literal::Ident(segs) if constants.is_local_bare_ident(segs) => segs.join("::"),
308        Literal::Ident(segs) => resolve_ident_to_u64(segs, constants)
309            .map(|value| value.to_string())
310            .unwrap_or_else(|| segs.join("::")),
311        other => rust_literal_str(other),
312    }
313}
314
315fn rust_literal_str(lit: &Literal) -> String {
316    match lit {
317        Literal::Hex(_) | Literal::Int(_) | Literal::Bool(_) | Literal::Float(_) => {
318            rust_scalar_literal_str(lit)
319        }
320        Literal::Str(s) => format!("{:?}", s),
321        Literal::Ident(segments) => segments.join("::"),
322    }
323}
324
325fn rust_scalar_literal_str(lit: &Literal) -> String {
326    if let Literal::Hex(n) = lit {
327        return format!("0x{:X}", n);
328    }
329    if let Literal::Int(n) = lit {
330        return n.to_string();
331    }
332    if let Literal::Bool(b) = lit {
333        return b.to_string();
334    }
335    if let Literal::Float(f) = lit {
336        return rust_float_literal_str(*f);
337    }
338    unreachable!("non-scalar literal passed to rust_scalar_literal_str")
339}
340
341fn rust_float_literal_str(value: f64) -> String {
342    let s = format!("{}", value);
343    if s.contains('.') || s.contains('e') {
344        s
345    } else {
346        format!("{}.0", s)
347    }
348}
349
350fn rust_typed_literal_str(lit: &Literal, ty: &TypeExpr) -> String {
351    match (lit, &ty.base) {
352        (Literal::Hex(n), BaseType::Primitive(p)) => rust_hex_str(*n, *p),
353        _ => rust_literal_str(lit),
354    }
355}
356
357fn rust_hex_str(value: u64, ty: PrimitiveType) -> String {
358    match ty {
359        PrimitiveType::U8 | PrimitiveType::I8 => format!("0x{:02X}", value),
360        PrimitiveType::U16 | PrimitiveType::I16 => format!("0x{:04X}", value),
361        PrimitiveType::U32 | PrimitiveType::I32 => format!("0x{:08X}", value),
362        PrimitiveType::U64 | PrimitiveType::I64 => format!("0x{:016X}", value),
363        PrimitiveType::F32 | PrimitiveType::F64 | PrimitiveType::Bool | PrimitiveType::Bytes => {
364            format!("0x{:X}", value)
365        }
366    }
367}