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