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