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