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