Skip to main content

synapse_codegen_cfs/
c.rs

1use synapse_parser::ast::{
2    ArraySuffix, BaseType, ConstDecl, EnumDef, FieldDef, Item, MessageDef, PrimitiveType,
3    StructDef, SynFile, TypeExpr,
4};
5
6use crate::{
7    constants::{ConstContext, const_context},
8    error::CodegenError,
9    types::{CfsOptions, PREAMBLE, ResolvedConstants},
10    util::{
11        emit_doc_lines, emit_indented_doc_lines, find_cc_attr, find_mid_attr, import_c_header,
12        literal_cc_str, literal_mid_str, packet_is_command, packet_item, to_screaming_snake,
13        typed_literal_str,
14    },
15    validate::validate_supported,
16};
17
18/// Generate a NASA cFS C header (`*_msg.h` + MID `#define`s) from a parsed Synapse file.
19pub fn generate_c(file: &SynFile) -> String {
20    try_generate_c(file).expect("parsed Synapse file is not supported by cFS C codegen")
21}
22
23/// Try to generate a NASA cFS C header (`*_msg.h` + MID `#define`s`) from a parsed Synapse file.
24pub fn try_generate_c(file: &SynFile) -> Result<String, CodegenError> {
25    try_generate_c_with_constants(file, &ResolvedConstants::new())
26}
27
28/// Try to generate a NASA cFS C header with validation options.
29pub fn try_generate_c_with_options(
30    file: &SynFile,
31    options: &CfsOptions,
32) -> Result<String, CodegenError> {
33    try_generate_c_with_constants_and_options(file, &ResolvedConstants::new(), options)
34}
35
36/// Try to generate a C header with additional imported constants available for attributes.
37pub fn try_generate_c_with_constants(
38    file: &SynFile,
39    imported_constants: &ResolvedConstants,
40) -> Result<String, CodegenError> {
41    try_generate_c_with_constants_and_options(file, imported_constants, &CfsOptions::default())
42}
43
44/// Try to generate a C header with imported constants and validation options.
45pub fn try_generate_c_with_constants_and_options(
46    file: &SynFile,
47    imported_constants: &ResolvedConstants,
48    options: &CfsOptions,
49) -> Result<String, CodegenError> {
50    let constants = const_context(file, imported_constants);
51    validate_supported(file, &constants, options)?;
52    let mut out = String::from(PREAMBLE);
53    emit_c_imports(file, &mut out);
54    emit_items(file, &mut out, &constants);
55    Ok(out)
56}
57
58fn emit_c_imports(file: &SynFile, out: &mut String) {
59    let mut emitted = false;
60    for item in &file.items {
61        if let Item::Import(import) = item {
62            out.push_str(&format!("#include \"{}\"\n", import_c_header(&import.path)));
63            emitted = true;
64        }
65    }
66    if emitted {
67        out.push('\n');
68    }
69}
70
71fn emit_items(file: &SynFile, out: &mut String, constants: &ConstContext<'_>) {
72    emit_mid_defines(file, out, constants);
73    emit_command_code_defines(file, out, constants);
74    emit_enum_aliases(file, out);
75    emit_c_types(file, out);
76}
77
78fn emit_mid_defines(file: &SynFile, out: &mut String, constants: &ConstContext<'_>) {
79    let defines: Vec<_> = file
80        .items
81        .iter()
82        .filter_map(|item| mid_define(item, constants))
83        .collect();
84
85    if defines.is_empty() {
86        return;
87    }
88
89    out.push_str("/* Message IDs */\n");
90    for define in defines {
91        out.push_str(&define);
92    }
93    out.push('\n');
94}
95
96fn mid_define(item: &Item, constants: &ConstContext<'_>) -> Option<String> {
97    let packet = packet_item(item)?;
98    let mid = find_mid_attr(&packet.attrs)?;
99    let define_name = to_screaming_snake(&packet.name);
100    let mid_str = literal_mid_str(mid, constants);
101    Some(format!("#define {}_MID  {}\n", define_name, mid_str))
102}
103
104fn emit_command_code_defines(file: &SynFile, out: &mut String, constants: &ConstContext<'_>) {
105    let defines: Vec<_> = file
106        .items
107        .iter()
108        .filter_map(|item| command_code_define(item, constants))
109        .collect();
110
111    if defines.is_empty() {
112        return;
113    }
114
115    out.push_str("/* Command Codes */\n");
116    for define in defines {
117        out.push_str(&define);
118    }
119    out.push('\n');
120}
121
122fn command_code_define(item: &Item, constants: &ConstContext<'_>) -> Option<String> {
123    let Item::Command(packet) = item else {
124        return None;
125    };
126    let cc = find_cc_attr(&packet.attrs)?;
127    let define_name = to_screaming_snake(&packet.name);
128    let cc_str = literal_cc_str(cc, constants);
129    Some(format!("#define {}_CC   {}\n", define_name, cc_str))
130}
131
132fn emit_enum_aliases(file: &SynFile, out: &mut String) {
133    let mut namespace = Vec::new();
134    for item in &file.items {
135        match item {
136            Item::Namespace(ns) => namespace = ns.name.clone(),
137            Item::Enum(e) => emit_enum(out, e, &namespace),
138            Item::Import(_)
139            | Item::Const(_)
140            | Item::Struct(_)
141            | Item::Table(_)
142            | Item::Command(_)
143            | Item::Telemetry(_)
144            | Item::Message(_) => {}
145        }
146    }
147}
148
149fn emit_c_types(file: &SynFile, out: &mut String) {
150    let mut namespace = Vec::new();
151    for item in &file.items {
152        if update_namespace(&mut namespace, item) {
153            continue;
154        }
155        emit_c_type(out, item, &namespace);
156    }
157}
158
159fn update_namespace(namespace: &mut Vec<String>, item: &Item) -> bool {
160    if let Item::Namespace(ns) = item {
161        *namespace = ns.name.clone();
162        true
163    } else {
164        false
165    }
166}
167
168fn emit_c_type(out: &mut String, item: &Item, namespace: &[String]) {
169    match item {
170        Item::Const(c) => emit_const(out, c),
171        Item::Struct(s) | Item::Table(s) => emit_struct(out, s, namespace),
172        Item::Command(m) | Item::Telemetry(m) | Item::Message(m) => emit_message(out, m, namespace),
173        Item::Namespace(_) | Item::Import(_) | Item::Enum(_) => {}
174    }
175}
176
177fn emit_const(out: &mut String, c: &ConstDecl) {
178    emit_doc_lines(out, &c.doc);
179    let val = typed_literal_str(&c.value, &c.ty);
180    out.push_str(&format!("#define {}  {}\n\n", c.name, val));
181}
182
183fn emit_enum(out: &mut String, e: &EnumDef, namespace: &[String]) {
184    let Some(repr) = e.repr else {
185        return;
186    };
187
188    let type_name = c_decl_type_name(&e.name, namespace);
189    emit_doc_lines(out, &e.doc);
190    out.push_str(&format!("typedef {} {};\n", primitive_str(repr), type_name));
191
192    let enum_prefix = c_enum_variant_prefix(&e.name, namespace);
193    for variant in &e.variants {
194        emit_doc_lines(out, &variant.doc);
195        let value = variant
196            .value
197            .expect("represented enum variants validated before emission");
198        out.push_str(&format!(
199            "#define {}_{}  (({}){})\n",
200            enum_prefix,
201            to_screaming_snake(&variant.name),
202            type_name,
203            value
204        ));
205    }
206    out.push('\n');
207}
208
209fn emit_struct(out: &mut String, s: &StructDef, namespace: &[String]) {
210    emit_doc_lines(out, &s.doc);
211    out.push_str("typedef struct {\n");
212    for f in &s.fields {
213        emit_c_field(out, f, namespace);
214    }
215    out.push_str(&format!("}} {};\n\n", c_decl_type_name(&s.name, namespace)));
216}
217
218fn emit_message(out: &mut String, m: &MessageDef, namespace: &[String]) {
219    let header_type = if packet_is_command(m) {
220        "CFE_MSG_CommandHeader_t"
221    } else {
222        "CFE_MSG_TelemetryHeader_t"
223    };
224
225    emit_doc_lines(out, &m.doc);
226
227    out.push_str("typedef struct {\n");
228    out.push_str(&format!("    {} Header;\n", header_type));
229    for f in &m.fields {
230        emit_c_field(out, f, namespace);
231    }
232    out.push_str(&format!("}} {};\n\n", c_decl_type_name(&m.name, namespace)));
233}
234
235fn non_fixed_type_str(ty: &TypeExpr, namespace: &[String]) -> String {
236    if ty.base == BaseType::String {
237        return non_fixed_string_type_str(&ty.array);
238    }
239
240    let base = base_type_str(&ty.base, namespace);
241    non_fixed_array_type_str(base, &ty.array)
242}
243
244fn non_fixed_string_type_str(array: &Option<ArraySuffix>) -> String {
245    match array {
246        None | Some(ArraySuffix::Dynamic) => "const char*".to_string(),
247        Some(ArraySuffix::Fixed(_)) => unreachable!("handled by emit_c_field"),
248        Some(ArraySuffix::Bounded(n)) => format!("char[{}]", n),
249    }
250}
251
252fn non_fixed_array_type_str(base: String, array: &Option<ArraySuffix>) -> String {
253    match array {
254        None => base,
255        Some(ArraySuffix::Fixed(_)) => unreachable!("handled by caller"),
256        Some(ArraySuffix::Dynamic) => format!("CFE_Span_t /* {} */", base),
257        Some(ArraySuffix::Bounded(n)) => format!("CFE_Span_t /* {} max {} */", base, n),
258    }
259}
260
261fn base_type_str(base: &BaseType, namespace: &[String]) -> String {
262    match base {
263        BaseType::String => "const char*".to_string(),
264        BaseType::Primitive(p) => primitive_str(*p).to_string(),
265        BaseType::Ref(segments) => c_ref_type_name(segments, namespace),
266    }
267}
268
269fn emit_c_field(out: &mut String, f: &FieldDef, namespace: &[String]) {
270    emit_indented_doc_lines(out, &f.doc);
271    match (&f.ty.base, &f.ty.array) {
272        (BaseType::String, Some(ArraySuffix::Fixed(n) | ArraySuffix::Bounded(n))) => {
273            out.push_str(&format!("    char {}[{}];\n", f.name, n));
274        }
275        (_, Some(ArraySuffix::Fixed(n))) => {
276            out.push_str(&format!(
277                "    {} {}[{}];\n",
278                base_type_str(&f.ty.base, namespace),
279                f.name,
280                n
281            ));
282        }
283        _ => {
284            out.push_str(&format!(
285                "    {} {};\n",
286                non_fixed_type_str(&f.ty, namespace),
287                f.name
288            ));
289        }
290    }
291}
292
293fn c_decl_type_name(name: &str, namespace: &[String]) -> String {
294    let mut segments = namespace.to_vec();
295    segments.push(name.to_string());
296    format!("{}_t", segments.join("_"))
297}
298
299pub(crate) fn c_enum_variant_prefix(name: &str, namespace: &[String]) -> String {
300    let mut segments = namespace.to_vec();
301    segments.push(name.to_string());
302    segments
303        .iter()
304        .map(|segment| to_screaming_snake(segment))
305        .collect::<Vec<_>>()
306        .join("_")
307}
308
309fn c_ref_type_name(segments: &[String], namespace: &[String]) -> String {
310    let resolved = if segments.len() == 1 && !namespace.is_empty() {
311        let mut resolved = namespace.to_vec();
312        resolved.push(segments[0].clone());
313        resolved
314    } else {
315        segments.to_vec()
316    };
317    if resolved.is_empty() {
318        return "_t".to_string();
319    }
320    format!("{}_t", resolved.join("_"))
321}
322
323fn primitive_str(p: PrimitiveType) -> &'static str {
324    const C_TYPES: &[(PrimitiveType, &str)] = &[
325        (PrimitiveType::F32, "float"),
326        (PrimitiveType::F64, "double"),
327        (PrimitiveType::I8, "int8_t"),
328        (PrimitiveType::I16, "int16_t"),
329        (PrimitiveType::I32, "int32_t"),
330        (PrimitiveType::I64, "int64_t"),
331        (PrimitiveType::U8, "uint8_t"),
332        (PrimitiveType::U16, "uint16_t"),
333        (PrimitiveType::U32, "uint32_t"),
334        (PrimitiveType::U64, "uint64_t"),
335        (PrimitiveType::Bool, "bool"),
336        (PrimitiveType::Bytes, "uint8_t*"),
337    ];
338
339    C_TYPES
340        .iter()
341        .find_map(|(ty, name)| (*ty == p).then_some(*name))
342        .expect("all primitive types have C names")
343}