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::{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 C header with additional imported constants available for attributes.
29pub fn try_generate_c_with_constants(
30    file: &SynFile,
31    imported_constants: &ResolvedConstants,
32) -> Result<String, CodegenError> {
33    let constants = const_context(file, imported_constants);
34    validate_supported(file, &constants)?;
35    let mut out = String::from(PREAMBLE);
36    emit_c_imports(file, &mut out);
37    emit_items(file, &mut out, &constants);
38    Ok(out)
39}
40
41fn emit_c_imports(file: &SynFile, out: &mut String) {
42    let mut emitted = false;
43    for item in &file.items {
44        if let Item::Import(import) = item {
45            out.push_str(&format!("#include \"{}\"\n", import_c_header(&import.path)));
46            emitted = true;
47        }
48    }
49    if emitted {
50        out.push('\n');
51    }
52}
53
54fn emit_items(file: &SynFile, out: &mut String, constants: &ConstContext<'_>) {
55    // First pass: emit #define MID lines for Software Bus packets with @mid
56    let mut has_mids = false;
57    for item in &file.items {
58        if let Some(m) = packet_item(item) {
59            if let Some(mid) = find_mid_attr(&m.attrs) {
60                if !has_mids {
61                    out.push_str("/* Message IDs */\n");
62                    has_mids = true;
63                }
64                let define_name = to_screaming_snake(&m.name);
65                let mid_str = literal_mid_str(mid, constants);
66                out.push_str(&format!("#define {}_MID  {}\n", define_name, mid_str));
67            }
68        }
69    }
70    if has_mids {
71        out.push('\n');
72    }
73
74    let mut has_ccs = false;
75    for item in &file.items {
76        if let Item::Command(m) = item {
77            if let Some(cc) = find_cc_attr(&m.attrs) {
78                if !has_ccs {
79                    out.push_str("/* Command Codes */\n");
80                    has_ccs = true;
81                }
82                let define_name = to_screaming_snake(&m.name);
83                let cc_str = literal_cc_str(cc, constants);
84                out.push_str(&format!("#define {}_CC   {}\n", define_name, cc_str));
85            }
86        }
87    }
88    if has_ccs {
89        out.push('\n');
90    }
91
92    // Second pass: emit enum aliases before any struct fields can reference them.
93    let mut namespace = Vec::new();
94    for item in &file.items {
95        match item {
96            Item::Namespace(ns) => namespace = ns.name.clone(),
97            Item::Enum(e) => emit_enum(out, e, &namespace),
98            Item::Import(_)
99            | Item::Const(_)
100            | Item::Struct(_)
101            | Item::Table(_)
102            | Item::Command(_)
103            | Item::Telemetry(_)
104            | Item::Message(_) => {}
105        }
106    }
107
108    // Third pass: emit const, struct, and message types.
109    let mut namespace = Vec::new();
110    for item in &file.items {
111        match item {
112            Item::Namespace(ns) => namespace = ns.name.clone(),
113            Item::Import(_) | Item::Enum(_) => {}
114            Item::Const(c) => emit_const(out, c),
115            Item::Struct(s) | Item::Table(s) => emit_struct(out, s, &namespace),
116            Item::Command(m) | Item::Telemetry(m) | Item::Message(m) => {
117                emit_message(out, m, &namespace)
118            }
119        }
120    }
121}
122
123fn emit_const(out: &mut String, c: &ConstDecl) {
124    emit_doc_lines(out, &c.doc);
125    let val = typed_literal_str(&c.value, &c.ty);
126    out.push_str(&format!("#define {}  {}\n\n", c.name, val));
127}
128
129fn emit_enum(out: &mut String, e: &EnumDef, namespace: &[String]) {
130    let Some(repr) = e.repr else {
131        return;
132    };
133
134    let type_name = c_decl_type_name(&e.name, namespace);
135    emit_doc_lines(out, &e.doc);
136    out.push_str(&format!("typedef {} {};\n", primitive_str(repr), type_name));
137
138    let enum_prefix = c_enum_variant_prefix(&e.name, namespace);
139    for variant in &e.variants {
140        emit_doc_lines(out, &variant.doc);
141        let value = variant
142            .value
143            .expect("represented enum variants validated before emission");
144        out.push_str(&format!(
145            "#define {}_{}  (({}){})\n",
146            enum_prefix,
147            to_screaming_snake(&variant.name),
148            type_name,
149            value
150        ));
151    }
152    out.push('\n');
153}
154
155fn emit_struct(out: &mut String, s: &StructDef, namespace: &[String]) {
156    emit_doc_lines(out, &s.doc);
157    out.push_str("typedef struct {\n");
158    for f in &s.fields {
159        emit_c_field(out, f, namespace);
160    }
161    out.push_str(&format!("}} {};\n\n", c_decl_type_name(&s.name, namespace)));
162}
163
164fn emit_message(out: &mut String, m: &MessageDef, namespace: &[String]) {
165    let header_type = if packet_is_command(m) {
166        "CFE_MSG_CommandHeader_t"
167    } else {
168        "CFE_MSG_TelemetryHeader_t"
169    };
170
171    emit_doc_lines(out, &m.doc);
172
173    out.push_str("typedef struct {\n");
174    out.push_str(&format!("    {} Header;\n", header_type));
175    for f in &m.fields {
176        emit_c_field(out, f, namespace);
177    }
178    out.push_str(&format!("}} {};\n\n", c_decl_type_name(&m.name, namespace)));
179}
180
181fn non_fixed_type_str(ty: &TypeExpr, namespace: &[String]) -> String {
182    if ty.base == BaseType::String {
183        return match &ty.array {
184            None | Some(ArraySuffix::Dynamic) => "const char*".to_string(),
185            Some(ArraySuffix::Fixed(_)) => unreachable!("handled by emit_c_field"),
186            Some(ArraySuffix::Bounded(n)) => format!("char[{}]", n),
187        };
188    }
189
190    let base = base_type_str(&ty.base, namespace);
191    match &ty.array {
192        None => base,
193        Some(ArraySuffix::Fixed(_)) => unreachable!("handled by caller"),
194        Some(ArraySuffix::Dynamic) => format!("CFE_Span_t /* {} */", base),
195        Some(ArraySuffix::Bounded(n)) => format!("CFE_Span_t /* {} max {} */", base, n),
196    }
197}
198
199fn base_type_str(base: &BaseType, namespace: &[String]) -> String {
200    match base {
201        BaseType::String => "const char*".to_string(),
202        BaseType::Primitive(p) => primitive_str(*p).to_string(),
203        BaseType::Ref(segments) => c_ref_type_name(segments, namespace),
204    }
205}
206
207fn emit_c_field(out: &mut String, f: &FieldDef, namespace: &[String]) {
208    emit_indented_doc_lines(out, &f.doc);
209    match (&f.ty.base, &f.ty.array) {
210        (BaseType::String, Some(ArraySuffix::Fixed(n) | ArraySuffix::Bounded(n))) => {
211            out.push_str(&format!("    char {}[{}];\n", f.name, n));
212        }
213        (_, Some(ArraySuffix::Fixed(n))) => {
214            out.push_str(&format!(
215                "    {} {}[{}];\n",
216                base_type_str(&f.ty.base, namespace),
217                f.name,
218                n
219            ));
220        }
221        _ => {
222            out.push_str(&format!(
223                "    {} {};\n",
224                non_fixed_type_str(&f.ty, namespace),
225                f.name
226            ));
227        }
228    }
229}
230
231fn c_decl_type_name(name: &str, namespace: &[String]) -> String {
232    let mut segments = namespace.to_vec();
233    segments.push(name.to_string());
234    format!("{}_t", segments.join("_"))
235}
236
237pub(crate) fn c_enum_variant_prefix(name: &str, namespace: &[String]) -> String {
238    let mut segments = namespace.to_vec();
239    segments.push(name.to_string());
240    segments
241        .iter()
242        .map(|segment| to_screaming_snake(segment))
243        .collect::<Vec<_>>()
244        .join("_")
245}
246
247fn c_ref_type_name(segments: &[String], namespace: &[String]) -> String {
248    let resolved = if segments.len() == 1 && !namespace.is_empty() {
249        let mut resolved = namespace.to_vec();
250        resolved.push(segments[0].clone());
251        resolved
252    } else {
253        segments.to_vec()
254    };
255    if resolved.is_empty() {
256        return "_t".to_string();
257    }
258    format!("{}_t", resolved.join("_"))
259}
260
261fn primitive_str(p: PrimitiveType) -> &'static str {
262    match p {
263        PrimitiveType::F32 => "float",
264        PrimitiveType::F64 => "double",
265        PrimitiveType::I8 => "int8_t",
266        PrimitiveType::I16 => "int16_t",
267        PrimitiveType::I32 => "int32_t",
268        PrimitiveType::I64 => "int64_t",
269        PrimitiveType::U8 => "uint8_t",
270        PrimitiveType::U16 => "uint16_t",
271        PrimitiveType::U32 => "uint32_t",
272        PrimitiveType::U64 => "uint64_t",
273        PrimitiveType::Bool => "bool",
274        PrimitiveType::Bytes => "uint8_t*",
275    }
276}