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
17pub 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
26pub fn try_generate_rust(file: &SynFile, opts: &RustOptions) -> Result<String, CodegenError> {
28 try_generate_rust_with_constants(file, opts, &ResolvedConstants::new())
29}
30
31pub 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 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}