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