1use std::{collections::HashMap, error::Error as StdError, fmt};
2
3use synapse_parser::ast::{
4 ArraySuffix, Attribute, BaseType, ConstDecl, EnumDef, FieldDef, Item, Literal, MessageDef,
5 PacketKind, PrimitiveType, StructDef, SynFile, TypeExpr,
6};
7
8pub const GENERATED_BANNER: &str = "Generated by Synapse. Do not edit directly.";
12
13pub const PREAMBLE: &str = "\
15/* Generated by Synapse. Do not edit directly. */
16#pragma once
17#include \"cfe.h\"
18
19";
20
21pub type ResolvedConstants = HashMap<Vec<String>, u64>;
23
24pub struct RustOptions<'a> {
26 pub cfs_module: &'a str,
30 pub tlm_header: &'a str,
32 pub cmd_header: &'a str,
34}
35
36impl Default for RustOptions<'_> {
37 fn default() -> Self {
38 RustOptions {
39 cfs_module: "cfs_sys",
40 tlm_header: "CFE_MSG_TelemetryHeader_t",
41 cmd_header: "CFE_MSG_CommandHeader_t",
42 }
43 }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
48pub enum CodegenError {
49 OptionalFieldUnsupported { container: String, field: String },
51 DefaultValueUnsupported { container: String, field: String },
53 EnumFieldUnsupported {
55 container: String,
56 field: String,
57 ty: String,
58 },
59 EnumRepresentationUnsupported { enum_name: String, repr: String },
61 EnumVariantValueRequired { enum_name: String, variant: String },
63 EnumVariantValueOutOfRange {
65 enum_name: String,
66 variant: String,
67 value: i64,
68 repr: String,
69 },
70 UnboundedStringUnsupported { container: String, field: String },
72 LegacyMessageUnsupported { packet: String },
74 MissingMid { packet: String },
76 MessageIdUnsupported { item: String },
78 MessageIdValueUnsupported { packet: String },
80 MissingCommandCode { packet: String },
82 CommandCodeUnsupported { item: String },
84 CommandCodeValueUnsupported { packet: String },
86 DuplicateMid {
88 mid: String,
89 first_packet: String,
90 second_packet: String,
91 },
92 DuplicateCommandCode {
94 mid: String,
95 cc: String,
96 first_packet: String,
97 second_packet: String,
98 },
99 MidRangeMismatch {
101 packet: String,
102 mid: String,
103 expected: &'static str,
104 },
105 DynamicArrayUnsupported {
107 container: String,
108 field: String,
109 ty: String,
110 },
111 BoundedArrayUnsupported {
113 container: String,
114 field: String,
115 ty: String,
116 },
117}
118
119impl fmt::Display for CodegenError {
120 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121 match self {
122 CodegenError::OptionalFieldUnsupported { container, field } => write!(
123 f,
124 "optional field `{container}.{field}` is not supported by cFS codegen yet"
125 ),
126 CodegenError::DefaultValueUnsupported { container, field } => write!(
127 f,
128 "default value for field `{container}.{field}` is not supported by cFS codegen yet"
129 ),
130 CodegenError::EnumFieldUnsupported {
131 container,
132 field,
133 ty,
134 } => write!(
135 f,
136 "enum field `{container}.{field}` with type `{ty}` needs an explicit integer representation for cFS codegen"
137 ),
138 CodegenError::EnumRepresentationUnsupported { enum_name, repr } => write!(
139 f,
140 "enum `{enum_name}` uses unsupported representation `{repr}`; cFS codegen supports integer enum representations"
141 ),
142 CodegenError::EnumVariantValueRequired { enum_name, variant } => write!(
143 f,
144 "enum `{enum_name}` variant `{variant}` needs an explicit value for cFS codegen"
145 ),
146 CodegenError::EnumVariantValueOutOfRange {
147 enum_name,
148 variant,
149 value,
150 repr,
151 } => write!(
152 f,
153 "enum `{enum_name}` variant `{variant}` value `{value}` does not fit `{repr}`"
154 ),
155 CodegenError::UnboundedStringUnsupported { container, field } => write!(
156 f,
157 "unbounded string field `{container}.{field}` is not supported by cFS codegen; use `string[<=N]` or `string[N]`"
158 ),
159 CodegenError::LegacyMessageUnsupported { packet } => write!(
160 f,
161 "legacy message `{packet}` is not supported by cFS codegen; use `command` or `telemetry`"
162 ),
163 CodegenError::MissingMid { packet } => {
164 write!(f, "packet `{packet}` is missing required `@mid(...)`")
165 }
166 CodegenError::MessageIdUnsupported { item } => write!(
167 f,
168 "`@mid(...)` is only supported on command and telemetry packets, found on `{item}`"
169 ),
170 CodegenError::MessageIdValueUnsupported { packet } => write!(
171 f,
172 "packet `{packet}` has unresolved or non-integer `@mid(...)`; cFS codegen requires an integer, hex, local integer constant, or imported integer constant message ID"
173 ),
174 CodegenError::MissingCommandCode { packet } => {
175 write!(f, "command `{packet}` is missing required `@cc(...)`")
176 }
177 CodegenError::CommandCodeUnsupported { item } => write!(
178 f,
179 "`@cc(...)` is only supported on command packets, found on `{item}`"
180 ),
181 CodegenError::CommandCodeValueUnsupported { packet } => write!(
182 f,
183 "command `{packet}` has unresolved or non-integer `@cc(...)`; cFS codegen requires an integer, hex, or local integer constant command code"
184 ),
185 CodegenError::DuplicateMid {
186 mid,
187 first_packet,
188 second_packet,
189 } => write!(
190 f,
191 "duplicate MID `{mid}` used by packets `{first_packet}` and `{second_packet}`"
192 ),
193 CodegenError::DuplicateCommandCode {
194 mid,
195 cc,
196 first_packet,
197 second_packet,
198 } => write!(
199 f,
200 "duplicate command MID/CC pair `{mid}`/`{cc}` used by packets `{first_packet}` and `{second_packet}`"
201 ),
202 CodegenError::MidRangeMismatch {
203 packet,
204 mid,
205 expected,
206 } => write!(
207 f,
208 "packet `{packet}` has MID `{mid}`, expected {expected}"
209 ),
210 CodegenError::DynamicArrayUnsupported {
211 container,
212 field,
213 ty,
214 } => write!(
215 f,
216 "dynamic array field `{container}.{field}` with type `{ty}` is not supported by cFS codegen yet"
217 ),
218 CodegenError::BoundedArrayUnsupported {
219 container,
220 field,
221 ty,
222 } => write!(
223 f,
224 "bounded array field `{container}.{field}` with type `{ty}` is not supported by cFS codegen yet"
225 ),
226 }
227 }
228}
229
230impl StdError for CodegenError {}
231
232pub fn generate_c(file: &SynFile) -> String {
234 try_generate_c(file).expect("parsed Synapse file is not supported by cFS C codegen")
235}
236
237pub fn try_generate_c(file: &SynFile) -> Result<String, CodegenError> {
239 try_generate_c_with_constants(file, &ResolvedConstants::new())
240}
241
242pub fn try_generate_c_with_constants(
244 file: &SynFile,
245 imported_constants: &ResolvedConstants,
246) -> Result<String, CodegenError> {
247 let constants = const_context(file, imported_constants);
248 validate_supported(file, &constants)?;
249 let mut out = String::from(PREAMBLE);
250 emit_c_imports(file, &mut out);
251 emit_items(file, &mut out, &constants);
252 Ok(out)
253}
254
255pub fn generate_rust(file: &SynFile, opts: &RustOptions) -> String {
261 try_generate_rust(file, opts).expect("parsed Synapse file is not supported by cFS Rust codegen")
262}
263
264pub fn try_generate_rust(file: &SynFile, opts: &RustOptions) -> Result<String, CodegenError> {
266 try_generate_rust_with_constants(file, opts, &ResolvedConstants::new())
267}
268
269pub fn try_generate_rust_with_constants(
271 file: &SynFile,
272 opts: &RustOptions,
273 imported_constants: &ResolvedConstants,
274) -> Result<String, CodegenError> {
275 let constants = const_context(file, imported_constants);
276 validate_supported(file, &constants)?;
277 let mut out = format!("// {GENERATED_BANNER}\n\n");
278 emit_rust_imports(file, &mut out);
279 emit_rust_items(file, opts, &mut out, &constants);
280 Ok(out)
281}
282
283pub fn resolve_integer_constants(
285 file: &SynFile,
286 imported_constants: &ResolvedConstants,
287) -> ResolvedConstants {
288 let constants = const_context(file, imported_constants);
289 constants.resolved_local_constants()
290}
291
292fn validate_supported(file: &SynFile, constants: &ConstContext<'_>) -> Result<(), CodegenError> {
295 let enum_defs = enum_defs(file);
296 let mut telemetry_mids = HashMap::new();
297 let mut command_codes = HashMap::new();
298 for item in &file.items {
299 match item {
300 Item::Struct(s) | Item::Table(s) => {
301 validate_plain_item_attrs(&s.name, &s.attrs)?;
302 validate_fields(&s.name, &s.fields, &enum_defs)?
303 }
304 Item::Command(m) | Item::Telemetry(m) => {
305 validate_packet(m, constants, &mut telemetry_mids, &mut command_codes)?;
306 validate_fields(&m.name, &m.fields, &enum_defs)?
307 }
308 Item::Message(m) => {
309 return Err(CodegenError::LegacyMessageUnsupported {
310 packet: m.name.clone(),
311 });
312 }
313 Item::Enum(e) => validate_enum(e)?,
314 Item::Namespace(_) | Item::Import(_) | Item::Const(_) => {}
315 }
316 }
317 Ok(())
318}
319
320struct ConstContext<'a> {
321 local_defs: HashMap<Vec<String>, &'a ConstDecl>,
322 imported_values: &'a ResolvedConstants,
323}
324
325fn const_context<'a>(
326 file: &'a SynFile,
327 imported_values: &'a ResolvedConstants,
328) -> ConstContext<'a> {
329 let namespace = file_namespace(file);
330 let mut local_defs = HashMap::new();
331 for item in &file.items {
332 if let Item::Const(c) = item {
333 local_defs.insert(vec![c.name.clone()], c);
334 if !namespace.is_empty() {
335 let mut qualified = namespace.clone();
336 qualified.push(c.name.clone());
337 local_defs.insert(qualified, c);
338 }
339 }
340 }
341 ConstContext {
342 local_defs,
343 imported_values,
344 }
345}
346
347impl ConstContext<'_> {
348 fn resolved_local_constants(&self) -> ResolvedConstants {
349 self.local_defs
350 .keys()
351 .filter_map(|segments| {
352 resolve_ident_to_u64(segments, self).map(|value| (segments.clone(), value))
353 })
354 .collect()
355 }
356
357 fn is_local_bare_ident(&self, segments: &[String]) -> bool {
358 segments.len() == 1 && self.local_defs.contains_key(segments)
359 }
360}
361
362fn file_namespace(file: &SynFile) -> Vec<String> {
363 file.items
364 .iter()
365 .find_map(|item| match item {
366 Item::Namespace(ns) => Some(ns.name.clone()),
367 _ => None,
368 })
369 .unwrap_or_default()
370}
371
372fn enum_defs(file: &SynFile) -> HashMap<String, &EnumDef> {
373 file.items
374 .iter()
375 .filter_map(|item| match item {
376 Item::Enum(e) => Some((e.name.clone(), e)),
377 _ => None,
378 })
379 .collect()
380}
381
382fn validate_enum(e: &EnumDef) -> Result<(), CodegenError> {
383 let Some(repr) = e.repr else {
384 return Ok(());
385 };
386 let Some((min, max)) = enum_repr_range(repr) else {
387 return Err(CodegenError::EnumRepresentationUnsupported {
388 enum_name: e.name.clone(),
389 repr: primitive_name(repr).to_string(),
390 });
391 };
392
393 for variant in &e.variants {
394 let value = variant
395 .value
396 .ok_or_else(|| CodegenError::EnumVariantValueRequired {
397 enum_name: e.name.clone(),
398 variant: variant.name.clone(),
399 })?;
400 if value < min || value > max {
401 return Err(CodegenError::EnumVariantValueOutOfRange {
402 enum_name: e.name.clone(),
403 variant: variant.name.clone(),
404 value,
405 repr: primitive_name(repr).to_string(),
406 });
407 }
408 }
409 Ok(())
410}
411
412fn enum_repr_range(repr: PrimitiveType) -> Option<(i64, i64)> {
413 match repr {
414 PrimitiveType::I8 => Some((i8::MIN as i64, i8::MAX as i64)),
415 PrimitiveType::I16 => Some((i16::MIN as i64, i16::MAX as i64)),
416 PrimitiveType::I32 => Some((i32::MIN as i64, i32::MAX as i64)),
417 PrimitiveType::I64 => Some((i64::MIN, i64::MAX)),
418 PrimitiveType::U8 => Some((0, u8::MAX as i64)),
419 PrimitiveType::U16 => Some((0, u16::MAX as i64)),
420 PrimitiveType::U32 => Some((0, u32::MAX as i64)),
421 PrimitiveType::U64 => Some((0, i64::MAX)),
422 PrimitiveType::F32 | PrimitiveType::F64 | PrimitiveType::Bool | PrimitiveType::Bytes => {
423 None
424 }
425 }
426}
427
428fn validate_packet(
429 packet: &MessageDef,
430 constants: &ConstContext<'_>,
431 telemetry_mids: &mut HashMap<u64, String>,
432 command_codes: &mut HashMap<(u64, u64), String>,
433) -> Result<(), CodegenError> {
434 let Some(mid) = find_mid_attr(&packet.attrs) else {
435 return Err(CodegenError::MissingMid {
436 packet: packet.name.clone(),
437 });
438 };
439
440 let cc = find_cc_attr(&packet.attrs);
441 match packet.kind {
442 PacketKind::Command if cc.is_none() => {
443 return Err(CodegenError::MissingCommandCode {
444 packet: packet.name.clone(),
445 });
446 }
447 PacketKind::Telemetry if cc.is_some() => {
448 return Err(CodegenError::CommandCodeUnsupported {
449 item: packet.name.clone(),
450 });
451 }
452 PacketKind::Command | PacketKind::Telemetry | PacketKind::Message => {}
453 }
454 let cc_value = if packet.kind == PacketKind::Command {
455 let cc = cc.expect("command code was checked above");
456 Some(resolve_literal_to_u64(cc, constants).ok_or_else(|| {
457 CodegenError::CommandCodeValueUnsupported {
458 packet: packet.name.clone(),
459 }
460 })?)
461 } else {
462 None
463 };
464
465 let value = resolve_literal_to_u64(mid, constants).ok_or_else(|| {
466 CodegenError::MessageIdValueUnsupported {
467 packet: packet.name.clone(),
468 }
469 })?;
470
471 validate_mid_range(packet, value, mid, constants)?;
472 match packet.kind {
473 PacketKind::Command => {
474 let cc = cc.expect("command code was checked above");
475 let cc_value = cc_value.expect("command code value was checked above");
476 if let Some(first_packet) = command_codes.insert((value, cc_value), packet.name.clone())
477 {
478 return Err(CodegenError::DuplicateCommandCode {
479 mid: literal_mid_str(mid, constants),
480 cc: literal_cc_str(cc, constants),
481 first_packet,
482 second_packet: packet.name.clone(),
483 });
484 }
485 }
486 PacketKind::Telemetry => {
487 if let Some(first_packet) = telemetry_mids.insert(value, packet.name.clone()) {
488 return Err(CodegenError::DuplicateMid {
489 mid: literal_mid_str(mid, constants),
490 first_packet,
491 second_packet: packet.name.clone(),
492 });
493 }
494 }
495 PacketKind::Message => {}
496 }
497
498 Ok(())
499}
500
501fn validate_plain_item_attrs(item_name: &str, attrs: &[Attribute]) -> Result<(), CodegenError> {
502 if find_mid_attr(attrs).is_some() {
503 return Err(CodegenError::MessageIdUnsupported {
504 item: item_name.to_string(),
505 });
506 }
507 if find_cc_attr(attrs).is_some() {
508 return Err(CodegenError::CommandCodeUnsupported {
509 item: item_name.to_string(),
510 });
511 }
512 Ok(())
513}
514
515fn validate_mid_range(
516 packet: &MessageDef,
517 value: u64,
518 mid: &Literal,
519 constants: &ConstContext<'_>,
520) -> Result<(), CodegenError> {
521 let command_bit_set = (value & 0x1000) != 0;
522 let expected = match packet.kind {
523 PacketKind::Command if !command_bit_set => Some("command MID with bit 0x1000 set"),
524 PacketKind::Telemetry if command_bit_set => Some("telemetry MID with bit 0x1000 clear"),
525 PacketKind::Command | PacketKind::Telemetry | PacketKind::Message => None,
526 };
527
528 if let Some(expected) = expected {
529 return Err(CodegenError::MidRangeMismatch {
530 packet: packet.name.clone(),
531 mid: literal_mid_str(mid, constants),
532 expected,
533 });
534 }
535
536 Ok(())
537}
538
539fn validate_fields(
540 container: &str,
541 fields: &[FieldDef],
542 enum_defs: &HashMap<String, &EnumDef>,
543) -> Result<(), CodegenError> {
544 for field in fields {
545 if field.optional {
546 return Err(CodegenError::OptionalFieldUnsupported {
547 container: container.to_string(),
548 field: field.name.clone(),
549 });
550 }
551 if field.default.is_some() {
552 return Err(CodegenError::DefaultValueUnsupported {
553 container: container.to_string(),
554 field: field.name.clone(),
555 });
556 }
557 if field.ty.base == BaseType::String && field.ty.array.is_none() {
558 return Err(CodegenError::UnboundedStringUnsupported {
559 container: container.to_string(),
560 field: field.name.clone(),
561 });
562 }
563 if let BaseType::Ref(segments) = &field.ty.base {
564 if let Some(e) = segments
565 .last()
566 .and_then(|name| enum_defs.get(name.as_str()))
567 {
568 if e.repr.is_none() {
569 return Err(CodegenError::EnumFieldUnsupported {
570 container: container.to_string(),
571 field: field.name.clone(),
572 ty: segments.join("::"),
573 });
574 }
575 }
576 }
577 match &field.ty.array {
578 Some(ArraySuffix::Dynamic) => {
579 return Err(CodegenError::DynamicArrayUnsupported {
580 container: container.to_string(),
581 field: field.name.clone(),
582 ty: type_expr_display(&field.ty),
583 });
584 }
585 Some(ArraySuffix::Bounded(_)) if field.ty.base != BaseType::String => {
586 return Err(CodegenError::BoundedArrayUnsupported {
587 container: container.to_string(),
588 field: field.name.clone(),
589 ty: type_expr_display(&field.ty),
590 });
591 }
592 Some(ArraySuffix::Bounded(_)) | Some(ArraySuffix::Fixed(_)) | None => {}
593 }
594 }
595 Ok(())
596}
597
598fn emit_c_imports(file: &SynFile, out: &mut String) {
599 let mut emitted = false;
600 for item in &file.items {
601 if let Item::Import(import) = item {
602 out.push_str(&format!("#include \"{}\"\n", import_c_header(&import.path)));
603 emitted = true;
604 }
605 }
606 if emitted {
607 out.push('\n');
608 }
609}
610
611fn emit_rust_imports(file: &SynFile, out: &mut String) {
612 let mut emitted = false;
613 for item in &file.items {
614 if let Item::Import(import) = item {
615 out.push_str(&format!(
616 "use crate::{};\n",
617 import_rust_module(&import.path)
618 ));
619 emitted = true;
620 }
621 }
622 if emitted {
623 out.push('\n');
624 }
625}
626
627fn emit_items(file: &SynFile, out: &mut String, constants: &ConstContext<'_>) {
628 let mut has_mids = false;
630 for item in &file.items {
631 if let Some(m) = packet_item(item) {
632 if let Some(mid) = find_mid_attr(&m.attrs) {
633 if !has_mids {
634 out.push_str("/* Message IDs */\n");
635 has_mids = true;
636 }
637 let define_name = to_screaming_snake(&m.name);
638 let mid_str = literal_mid_str(mid, constants);
639 out.push_str(&format!("#define {}_MID {}\n", define_name, mid_str));
640 }
641 }
642 }
643 if has_mids {
644 out.push('\n');
645 }
646
647 let mut has_ccs = false;
648 for item in &file.items {
649 if let Item::Command(m) = item {
650 if let Some(cc) = find_cc_attr(&m.attrs) {
651 if !has_ccs {
652 out.push_str("/* Command Codes */\n");
653 has_ccs = true;
654 }
655 let define_name = to_screaming_snake(&m.name);
656 let cc_str = literal_cc_str(cc, constants);
657 out.push_str(&format!("#define {}_CC {}\n", define_name, cc_str));
658 }
659 }
660 }
661 if has_ccs {
662 out.push('\n');
663 }
664
665 let mut namespace = Vec::new();
667 for item in &file.items {
668 match item {
669 Item::Namespace(ns) => namespace = ns.name.clone(),
670 Item::Enum(e) => emit_enum(out, e, &namespace),
671 Item::Import(_)
672 | Item::Const(_)
673 | Item::Struct(_)
674 | Item::Table(_)
675 | Item::Command(_)
676 | Item::Telemetry(_)
677 | Item::Message(_) => {}
678 }
679 }
680
681 let mut namespace = Vec::new();
683 for item in &file.items {
684 match item {
685 Item::Namespace(ns) => namespace = ns.name.clone(),
686 Item::Import(_) | Item::Enum(_) => {}
687 Item::Const(c) => emit_const(out, c),
688 Item::Struct(s) | Item::Table(s) => emit_struct(out, s, &namespace),
689 Item::Command(m) | Item::Telemetry(m) | Item::Message(m) => {
690 emit_message(out, m, &namespace)
691 }
692 }
693 }
694}
695
696fn emit_const(out: &mut String, c: &ConstDecl) {
699 emit_doc_lines(out, &c.doc);
700 let val = literal_str(&c.value);
701 out.push_str(&format!("#define {} {}\n\n", c.name, val));
702}
703
704fn emit_enum(out: &mut String, e: &EnumDef, namespace: &[String]) {
707 let Some(repr) = e.repr else {
708 return;
709 };
710
711 let type_name = c_decl_type_name(&e.name, namespace);
712 emit_doc_lines(out, &e.doc);
713 out.push_str(&format!("typedef {} {};\n", primitive_str(repr), type_name));
714
715 let enum_prefix = to_screaming_snake(&e.name);
716 for variant in &e.variants {
717 emit_doc_lines(out, &variant.doc);
718 let value = variant
719 .value
720 .expect("represented enum variants validated before emission");
721 out.push_str(&format!(
722 "#define {}_{} (({}){})\n",
723 enum_prefix,
724 to_screaming_snake(&variant.name),
725 type_name,
726 value
727 ));
728 }
729 out.push('\n');
730}
731
732fn emit_struct(out: &mut String, s: &StructDef, namespace: &[String]) {
735 emit_doc_lines(out, &s.doc);
736 out.push_str("typedef struct {\n");
737 for f in &s.fields {
738 emit_c_field(out, f, namespace);
739 }
740 out.push_str(&format!("}} {};\n\n", c_decl_type_name(&s.name, namespace)));
741}
742
743fn emit_message(out: &mut String, m: &MessageDef, namespace: &[String]) {
746 let header_type = if packet_is_command(m) {
747 "CFE_MSG_CommandHeader_t"
748 } else {
749 "CFE_MSG_TelemetryHeader_t"
750 };
751
752 emit_doc_lines(out, &m.doc);
753
754 out.push_str(&format!("typedef struct {{\n"));
755 out.push_str(&format!(" {} Header;\n", header_type));
756 for f in &m.fields {
757 emit_c_field(out, f, namespace);
758 }
759 out.push_str(&format!("}} {};\n\n", c_decl_type_name(&m.name, namespace)));
760}
761
762fn emit_rust_items(
765 file: &SynFile,
766 opts: &RustOptions,
767 out: &mut String,
768 constants: &ConstContext<'_>,
769) {
770 let mut has_mids = false;
772 for item in &file.items {
773 if let Some(m) = packet_item(item) {
774 if let Some(mid) = find_mid_attr(&m.attrs) {
775 if !has_mids {
776 out.push_str("// Message IDs\n");
777 has_mids = true;
778 }
779 let const_name = format!("{}_MID", to_screaming_snake(&m.name));
780 let val = rust_mid_str(mid, constants);
781 out.push_str(&format!("pub const {}: u16 = {};\n", const_name, val));
782 }
783 }
784 }
785 if has_mids {
786 out.push('\n');
787 }
788
789 let mut has_ccs = false;
790 for item in &file.items {
791 if let Item::Command(m) = item {
792 if let Some(cc) = find_cc_attr(&m.attrs) {
793 if !has_ccs {
794 out.push_str("// Command Codes\n");
795 has_ccs = true;
796 }
797 let const_name = format!("{}_CC", to_screaming_snake(&m.name));
798 let val = rust_cc_str(cc, constants);
799 out.push_str(&format!("pub const {}: u16 = {};\n", const_name, val));
800 }
801 }
802 }
803 if has_ccs {
804 out.push('\n');
805 }
806
807 for item in &file.items {
809 match item {
810 Item::Namespace(_) | Item::Import(_) => {}
811 Item::Const(c) => emit_rust_const(out, c),
812 Item::Enum(e) => emit_rust_enum(out, e),
813 Item::Struct(s) | Item::Table(s) => emit_rust_struct(out, s),
814 Item::Command(m) | Item::Telemetry(m) | Item::Message(m) => {
815 emit_rust_message(out, m, opts)
816 }
817 }
818 }
819}
820
821fn emit_rust_const(out: &mut String, c: &ConstDecl) {
822 emit_doc_lines(out, &c.doc);
823 let val = rust_literal_str(&c.value);
824 let ty = rust_field_type_str(&c.ty);
825 out.push_str(&format!("pub const {}: {} = {};\n\n", c.name, ty, val));
826}
827
828fn emit_rust_enum(out: &mut String, e: &EnumDef) {
829 let Some(repr) = e.repr else {
830 return;
831 };
832
833 emit_doc_lines(out, &e.doc);
834 out.push_str(&format!(
835 "pub type {} = {};\n",
836 e.name,
837 rust_primitive_str(repr)
838 ));
839
840 let enum_prefix = to_screaming_snake(&e.name);
841 for variant in &e.variants {
842 emit_doc_lines(out, &variant.doc);
843 let value = variant
844 .value
845 .expect("represented enum variants validated before emission");
846 out.push_str(&format!(
847 "pub const {}_{}: {} = {};\n",
848 enum_prefix,
849 to_screaming_snake(&variant.name),
850 e.name,
851 value
852 ));
853 }
854 out.push('\n');
855}
856
857fn emit_rust_struct(out: &mut String, s: &StructDef) {
858 emit_doc_lines(out, &s.doc);
859 out.push_str("#[repr(C)]\n");
860 out.push_str(&format!("pub struct {} {{\n", s.name));
861 for f in &s.fields {
862 emit_indented_doc_lines(out, &f.doc);
863 out.push_str(&format!(
864 " pub {}: {},\n",
865 f.name,
866 rust_field_type_str(&f.ty)
867 ));
868 }
869 out.push_str("}\n\n");
870}
871
872fn emit_rust_message(out: &mut String, m: &MessageDef, opts: &RustOptions) {
873 let header_type = if packet_is_command(m) {
874 opts.cmd_header
875 } else {
876 opts.tlm_header
877 };
878 let qualified = if opts.cfs_module.is_empty() {
879 header_type.to_string()
880 } else {
881 format!("{}::{}", opts.cfs_module, header_type)
882 };
883
884 emit_doc_lines(out, &m.doc);
885
886 out.push_str("#[repr(C)]\n");
887 out.push_str(&format!("pub struct {} {{\n", m.name));
888 out.push_str(&format!(" pub cfs_header: {},\n", qualified));
889 for f in &m.fields {
890 emit_indented_doc_lines(out, &f.doc);
891 let ty = rust_field_type_str(&f.ty);
892 out.push_str(&format!(" pub {}: {},\n", f.name, ty));
893 }
894 out.push_str("}\n\n");
895}
896
897fn rust_field_type_str(ty: &TypeExpr) -> String {
898 if ty.base == BaseType::String {
899 return match &ty.array {
900 None | Some(ArraySuffix::Dynamic) => "*const u8".to_string(),
901 Some(ArraySuffix::Fixed(n)) | Some(ArraySuffix::Bounded(n)) => {
902 format!("[u8; {}]", n)
903 }
904 };
905 }
906
907 let base = rust_base_type_str(&ty.base);
908 match &ty.array {
909 None => base,
910 Some(ArraySuffix::Fixed(n)) => format!("[{}; {}]", base, n),
911 Some(ArraySuffix::Dynamic) => format!("*const {}", base),
913 Some(ArraySuffix::Bounded(n)) => format!("*const {} /* max {} */", base, n),
914 }
915}
916
917fn rust_base_type_str(base: &BaseType) -> String {
918 match base {
919 BaseType::String => "*const u8".to_string(),
920 BaseType::Primitive(p) => rust_primitive_str(*p).to_string(),
921 BaseType::Ref(segments) => segments.join("::"),
922 }
923}
924
925fn rust_primitive_str(p: PrimitiveType) -> &'static str {
926 match p {
927 PrimitiveType::F32 => "f32",
928 PrimitiveType::F64 => "f64",
929 PrimitiveType::I8 => "i8",
930 PrimitiveType::I16 => "i16",
931 PrimitiveType::I32 => "i32",
932 PrimitiveType::I64 => "i64",
933 PrimitiveType::U8 => "u8",
934 PrimitiveType::U16 => "u16",
935 PrimitiveType::U32 => "u32",
936 PrimitiveType::U64 => "u64",
937 PrimitiveType::Bool => "bool",
938 PrimitiveType::Bytes => "*const u8",
939 }
940}
941
942fn rust_mid_str(lit: &Literal, constants: &ConstContext<'_>) -> String {
943 match lit {
944 Literal::Hex(n) => format!("0x{:04X}", n),
945 Literal::Int(n) => n.to_string(),
946 Literal::Ident(segs) if constants.is_local_bare_ident(segs) => segs.join("::"),
947 Literal::Ident(segs) => resolve_ident_to_u64(segs, constants)
948 .map(|value| format!("0x{:04X}", value))
949 .unwrap_or_else(|| segs.join("::")),
950 other => rust_literal_str(other),
951 }
952}
953
954fn rust_cc_str(lit: &Literal, constants: &ConstContext<'_>) -> String {
955 match lit {
956 Literal::Hex(n) => format!("0x{:X}", n),
957 Literal::Int(n) => n.to_string(),
958 Literal::Ident(segs) if constants.is_local_bare_ident(segs) => segs.join("::"),
959 Literal::Ident(segs) => resolve_ident_to_u64(segs, constants)
960 .map(|value| value.to_string())
961 .unwrap_or_else(|| segs.join("::")),
962 other => rust_literal_str(other),
963 }
964}
965
966fn rust_literal_str(lit: &Literal) -> String {
967 match lit {
968 Literal::Hex(n) => format!("0x{:X}", n),
969 Literal::Int(n) => n.to_string(),
970 Literal::Bool(b) => b.to_string(),
971 Literal::Float(f) => {
972 let s = format!("{}", f);
973 if s.contains('.') || s.contains('e') {
974 s
975 } else {
976 format!("{}.0", s)
977 }
978 }
979 Literal::Str(s) => format!("{:?}", s),
980 Literal::Ident(segments) => segments.join("::"),
981 }
982}
983
984fn find_mid_attr(attrs: &[Attribute]) -> Option<&Literal> {
988 attrs.iter().find(|a| a.name == "mid").map(|a| &a.value)
989}
990
991fn find_cc_attr(attrs: &[Attribute]) -> Option<&Literal> {
993 attrs.iter().find(|a| a.name == "cc").map(|a| &a.value)
994}
995
996fn packet_item(item: &Item) -> Option<&MessageDef> {
997 match item {
998 Item::Command(m) | Item::Telemetry(m) => Some(m),
999 _ => None,
1000 }
1001}
1002
1003fn packet_is_command(m: &MessageDef) -> bool {
1004 match m.kind {
1005 PacketKind::Command => true,
1006 PacketKind::Telemetry | PacketKind::Message => false,
1007 }
1008}
1009
1010fn literal_to_u64(lit: &Literal) -> Option<u64> {
1011 match lit {
1012 Literal::Hex(n) => Some(*n),
1013 Literal::Int(n) if *n >= 0 => Some(*n as u64),
1014 _ => None,
1015 }
1016}
1017
1018fn resolve_literal_to_u64(lit: &Literal, constants: &ConstContext<'_>) -> Option<u64> {
1019 resolve_literal_to_u64_inner(lit, constants, &mut Vec::new())
1020}
1021
1022fn resolve_literal_to_u64_inner(
1023 lit: &Literal,
1024 constants: &ConstContext<'_>,
1025 seen: &mut Vec<Vec<String>>,
1026) -> Option<u64> {
1027 match lit {
1028 Literal::Ident(segments) => resolve_ident_to_u64_inner(segments, constants, seen),
1029 other => literal_to_u64(other),
1030 }
1031}
1032
1033fn resolve_ident_to_u64(segments: &[String], constants: &ConstContext<'_>) -> Option<u64> {
1034 resolve_ident_to_u64_inner(segments, constants, &mut Vec::new())
1035}
1036
1037fn resolve_ident_to_u64_inner(
1038 segments: &[String],
1039 constants: &ConstContext<'_>,
1040 seen: &mut Vec<Vec<String>>,
1041) -> Option<u64> {
1042 if seen.iter().any(|s| s == segments) {
1043 return None;
1044 }
1045 if let Some(c) = constants.local_defs.get(segments) {
1046 seen.push(segments.to_vec());
1047 let resolved = resolve_literal_to_u64_inner(&c.value, constants, seen);
1048 seen.pop();
1049 return resolved;
1050 }
1051 constants.imported_values.get(segments).copied()
1052}
1053
1054fn type_expr_display(ty: &TypeExpr) -> String {
1055 let mut out = base_type_display(&ty.base);
1056 match &ty.array {
1057 None => {}
1058 Some(ArraySuffix::Dynamic) => out.push_str("[]"),
1059 Some(ArraySuffix::Fixed(n)) => out.push_str(&format!("[{n}]")),
1060 Some(ArraySuffix::Bounded(n)) => out.push_str(&format!("[<={n}]")),
1061 }
1062 out
1063}
1064
1065fn base_type_display(base: &BaseType) -> String {
1066 match base {
1067 BaseType::String => "string".to_string(),
1068 BaseType::Primitive(p) => primitive_name(*p).to_string(),
1069 BaseType::Ref(segments) => segments.join("::"),
1070 }
1071}
1072
1073fn primitive_name(p: PrimitiveType) -> &'static str {
1074 match p {
1075 PrimitiveType::F32 => "f32",
1076 PrimitiveType::F64 => "f64",
1077 PrimitiveType::I8 => "i8",
1078 PrimitiveType::I16 => "i16",
1079 PrimitiveType::I32 => "i32",
1080 PrimitiveType::I64 => "i64",
1081 PrimitiveType::U8 => "u8",
1082 PrimitiveType::U16 => "u16",
1083 PrimitiveType::U32 => "u32",
1084 PrimitiveType::U64 => "u64",
1085 PrimitiveType::Bool => "bool",
1086 PrimitiveType::Bytes => "bytes",
1087 }
1088}
1089
1090fn emit_doc_lines(out: &mut String, doc: &[String]) {
1091 for line in doc {
1092 if line.is_empty() {
1093 out.push_str("///\n");
1094 } else {
1095 out.push_str(&format!("/// {line}\n"));
1096 }
1097 }
1098}
1099
1100fn emit_indented_doc_lines(out: &mut String, doc: &[String]) {
1101 for line in doc {
1102 if line.is_empty() {
1103 out.push_str(" ///\n");
1104 } else {
1105 out.push_str(&format!(" /// {line}\n"));
1106 }
1107 }
1108}
1109
1110fn literal_mid_str(lit: &Literal, constants: &ConstContext<'_>) -> String {
1112 match lit {
1113 Literal::Hex(n) => format!("0x{:04X}U", n),
1114 Literal::Int(n) => format!("{}U", n),
1115 Literal::Ident(segs) if constants.is_local_bare_ident(segs) => segs.join("::"),
1116 Literal::Ident(segs) => resolve_ident_to_u64(segs, constants)
1117 .map(|value| format!("0x{:04X}U", value))
1118 .unwrap_or_else(|| segs.join("::")),
1119 other => literal_str(other),
1120 }
1121}
1122
1123fn literal_cc_str(lit: &Literal, constants: &ConstContext<'_>) -> String {
1125 match lit {
1126 Literal::Hex(n) => format!("0x{:X}U", n),
1127 Literal::Int(n) => format!("{}U", n),
1128 Literal::Ident(segs) if constants.is_local_bare_ident(segs) => segs.join("::"),
1129 Literal::Ident(segs) => resolve_ident_to_u64(segs, constants)
1130 .map(|value| format!("{}U", value))
1131 .unwrap_or_else(|| segs.join("::")),
1132 other => literal_str(other),
1133 }
1134}
1135
1136fn literal_str(lit: &Literal) -> String {
1137 match lit {
1138 Literal::Float(f) => {
1139 let s = format!("{}", f);
1140 if s.contains('.') || s.contains('e') {
1141 s
1142 } else {
1143 format!("{}.0", s)
1144 }
1145 }
1146 Literal::Int(n) => n.to_string(),
1147 Literal::Hex(n) => format!("0x{:X}U", n),
1148 Literal::Bool(b) => {
1149 if *b {
1150 "1".to_string()
1151 } else {
1152 "0".to_string()
1153 }
1154 }
1155 Literal::Str(s) => format!("{:?}", s),
1156 Literal::Ident(segments) => segments.join("::"),
1157 }
1158}
1159
1160fn non_fixed_type_str(ty: &TypeExpr, namespace: &[String]) -> String {
1161 if ty.base == BaseType::String {
1162 return match &ty.array {
1163 None | Some(ArraySuffix::Dynamic) => "const char*".to_string(),
1164 Some(ArraySuffix::Fixed(_)) => unreachable!("handled by emit_c_field"),
1165 Some(ArraySuffix::Bounded(n)) => format!("char[{}]", n),
1166 };
1167 }
1168
1169 let base = base_type_str(&ty.base, namespace);
1170 match &ty.array {
1171 None => base,
1172 Some(ArraySuffix::Fixed(_)) => unreachable!("handled by caller"),
1173 Some(ArraySuffix::Dynamic) => format!("CFE_Span_t /* {} */", base),
1174 Some(ArraySuffix::Bounded(n)) => format!("CFE_Span_t /* {} max {} */", base, n),
1175 }
1176}
1177
1178fn base_type_str(base: &BaseType, namespace: &[String]) -> String {
1179 match base {
1180 BaseType::String => "const char*".to_string(),
1181 BaseType::Primitive(p) => primitive_str(*p).to_string(),
1182 BaseType::Ref(segments) => c_ref_type_name(segments, namespace),
1183 }
1184}
1185
1186fn emit_c_field(out: &mut String, f: &synapse_parser::ast::FieldDef, namespace: &[String]) {
1187 emit_indented_doc_lines(out, &f.doc);
1188 match (&f.ty.base, &f.ty.array) {
1189 (BaseType::String, Some(ArraySuffix::Fixed(n) | ArraySuffix::Bounded(n))) => {
1190 out.push_str(&format!(" char {}[{}];\n", f.name, n));
1191 }
1192 (_, Some(ArraySuffix::Fixed(n))) => {
1193 out.push_str(&format!(
1194 " {} {}[{}];\n",
1195 base_type_str(&f.ty.base, namespace),
1196 f.name,
1197 n
1198 ));
1199 }
1200 _ => {
1201 out.push_str(&format!(
1202 " {} {};\n",
1203 non_fixed_type_str(&f.ty, namespace),
1204 f.name
1205 ));
1206 }
1207 }
1208}
1209
1210fn c_decl_type_name(name: &str, namespace: &[String]) -> String {
1211 let mut segments = namespace.to_vec();
1212 segments.push(name.to_string());
1213 format!("{}_t", segments.join("_"))
1214}
1215
1216fn c_ref_type_name(segments: &[String], namespace: &[String]) -> String {
1217 let resolved = if segments.len() == 1 && !namespace.is_empty() {
1218 let mut resolved = namespace.to_vec();
1219 resolved.push(segments[0].clone());
1220 resolved
1221 } else {
1222 segments.to_vec()
1223 };
1224 if resolved.is_empty() {
1225 return "_t".to_string();
1226 }
1227 format!("{}_t", resolved.join("_"))
1228}
1229
1230fn import_c_header(path: &str) -> String {
1231 replace_extension(path, "h")
1232}
1233
1234fn import_rust_module(path: &str) -> String {
1235 let header = path.rsplit('/').next().unwrap_or(path);
1236 replace_extension(header, "")
1237}
1238
1239fn replace_extension(path: &str, ext: &str) -> String {
1240 match path.rsplit_once('.') {
1241 Some((stem, _)) if ext.is_empty() => stem.to_string(),
1242 Some((stem, _)) => format!("{stem}.{ext}"),
1243 None if ext.is_empty() => path.to_string(),
1244 None => format!("{path}.{ext}"),
1245 }
1246}
1247
1248fn primitive_str(p: PrimitiveType) -> &'static str {
1249 match p {
1250 PrimitiveType::F32 => "float",
1251 PrimitiveType::F64 => "double",
1252 PrimitiveType::I8 => "int8_t",
1253 PrimitiveType::I16 => "int16_t",
1254 PrimitiveType::I32 => "int32_t",
1255 PrimitiveType::I64 => "int64_t",
1256 PrimitiveType::U8 => "uint8_t",
1257 PrimitiveType::U16 => "uint16_t",
1258 PrimitiveType::U32 => "uint32_t",
1259 PrimitiveType::U64 => "uint64_t",
1260 PrimitiveType::Bool => "bool",
1261 PrimitiveType::Bytes => "uint8_t*",
1262 }
1263}
1264
1265fn to_screaming_snake(name: &str) -> String {
1267 let mut out = String::new();
1268 for (i, ch) in name.chars().enumerate() {
1269 if ch.is_uppercase() && i > 0 {
1270 out.push('_');
1271 }
1272 out.push(ch.to_ascii_uppercase());
1273 }
1274 out
1275}
1276
1277#[cfg(test)]
1280mod tests {
1281 use super::*;
1282 use synapse_parser::ast::parse;
1283
1284 fn codegen(src: &str) -> String {
1285 generate_c(&parse(src).unwrap())
1286 }
1287
1288 #[test]
1289 fn telemetry_with_hex_mid() {
1290 let out = codegen("@mid(0x0801)\ntelemetry NavTlm { x: f64 y: f64 }");
1291 assert!(out.starts_with("/* Generated by Synapse. Do not edit directly. */\n"));
1292 assert!(out.contains("#define NAV_TLM_MID 0x0801U"));
1293 assert!(out.contains("CFE_MSG_TelemetryHeader_t Header;"));
1294 assert!(out.contains("typedef struct {"));
1295 assert!(out.contains("} NavTlm_t;"));
1296 assert!(out.contains(" double x;"));
1297 assert!(out.contains(" double y;"));
1298 }
1299
1300 #[test]
1301 fn command_uses_declared_packet_kind() {
1302 let out = codegen("@mid(0x1881)\n@cc(1)\ncommand NavCmd { seq: u16 }");
1303 assert!(out.contains("#define NAV_CMD_MID 0x1881U"));
1304 assert!(out.contains("#define NAV_CMD_CC 1U"));
1305 assert!(out.contains("CFE_MSG_CommandHeader_t Header;"));
1306 assert!(out.contains("} NavCmd_t;"));
1307 }
1308
1309 #[test]
1310 fn command_uses_command_header() {
1311 let out = codegen("@mid(0x1880)\n@cc(2)\ncommand SetMode { mode: u8 }");
1312 assert!(out.contains("#define SET_MODE_MID 0x1880U"));
1313 assert!(out.contains("#define SET_MODE_CC 2U"));
1314 assert!(out.contains("CFE_MSG_CommandHeader_t Header;"));
1315 assert!(!out.contains("CFE_MSG_TelemetryHeader_t Header;"));
1316 }
1317
1318 #[test]
1319 fn telemetry_uses_telemetry_header() {
1320 let out = codegen("@mid(0x0801)\ntelemetry NavState { x: f64 }");
1321 assert!(out.contains("#define NAV_STATE_MID 0x0801U"));
1322 assert!(out.contains("CFE_MSG_TelemetryHeader_t Header;"));
1323 assert!(!out.contains("CFE_MSG_CommandHeader_t Header;"));
1324 }
1325
1326 #[test]
1327 fn table_is_plain_data_without_bus_header() {
1328 let out = codegen("table NavConfig { max_speed: f64 enabled: bool }");
1329 assert!(out.contains("} NavConfig_t;"));
1330 assert!(out.contains(" double max_speed;"));
1331 assert!(!out.contains("CFE_MSG_CommandHeader_t Header;"));
1332 assert!(!out.contains("CFE_MSG_TelemetryHeader_t Header;"));
1333 }
1334
1335 #[test]
1336 fn c_rejects_legacy_message() {
1337 let file = parse("message Bare { x: f32 }").unwrap();
1338 let err = try_generate_c(&file).unwrap_err();
1339 assert_eq!(
1340 err,
1341 CodegenError::LegacyMessageUnsupported {
1342 packet: "Bare".to_string(),
1343 }
1344 );
1345 assert_eq!(
1346 err.to_string(),
1347 "legacy message `Bare` is not supported by cFS codegen; use `command` or `telemetry`"
1348 );
1349 }
1350
1351 #[test]
1352 fn c_rejects_command_without_mid() {
1353 let file = parse("command SetMode { mode: u8 }").unwrap();
1354 let err = try_generate_c(&file).unwrap_err();
1355 assert_eq!(
1356 err,
1357 CodegenError::MissingMid {
1358 packet: "SetMode".to_string(),
1359 }
1360 );
1361 assert_eq!(
1362 err.to_string(),
1363 "packet `SetMode` is missing required `@mid(...)`"
1364 );
1365 }
1366
1367 #[test]
1368 fn c_rejects_command_without_cc() {
1369 let file = parse("@mid(0x1880)\ncommand SetMode { mode: u8 }").unwrap();
1370 let err = try_generate_c(&file).unwrap_err();
1371 assert_eq!(
1372 err,
1373 CodegenError::MissingCommandCode {
1374 packet: "SetMode".to_string(),
1375 }
1376 );
1377 assert_eq!(
1378 err.to_string(),
1379 "command `SetMode` is missing required `@cc(...)`"
1380 );
1381 }
1382
1383 #[test]
1384 fn c_rejects_cc_on_telemetry() {
1385 let file = parse("@mid(0x0801)\n@cc(1)\ntelemetry Status { x: f32 }").unwrap();
1386 let err = try_generate_c(&file).unwrap_err();
1387 assert_eq!(
1388 err,
1389 CodegenError::CommandCodeUnsupported {
1390 item: "Status".to_string(),
1391 }
1392 );
1393 }
1394
1395 #[test]
1396 fn c_rejects_cc_on_table() {
1397 let file = parse("@cc(1)\ntable Config { enabled: bool }").unwrap();
1398 let err = try_generate_c(&file).unwrap_err();
1399 assert_eq!(
1400 err,
1401 CodegenError::CommandCodeUnsupported {
1402 item: "Config".to_string(),
1403 }
1404 );
1405 }
1406
1407 #[test]
1408 fn c_rejects_mid_on_table() {
1409 let file = parse("@mid(0x0801)\ntable Config { enabled: bool }").unwrap();
1410 let err = try_generate_c(&file).unwrap_err();
1411 assert_eq!(
1412 err,
1413 CodegenError::MessageIdUnsupported {
1414 item: "Config".to_string(),
1415 }
1416 );
1417 assert_eq!(
1418 err.to_string(),
1419 "`@mid(...)` is only supported on command and telemetry packets, found on `Config`"
1420 );
1421 }
1422
1423 #[test]
1424 fn c_rejects_unresolved_symbolic_command_code() {
1425 let file = parse("@mid(0x1880)\n@cc(SET_MODE_CC)\ncommand SetMode { mode: u8 }").unwrap();
1426 let err = try_generate_c(&file).unwrap_err();
1427 assert_eq!(
1428 err,
1429 CodegenError::CommandCodeValueUnsupported {
1430 packet: "SetMode".to_string(),
1431 }
1432 );
1433 }
1434
1435 #[test]
1436 fn c_rejects_unresolved_symbolic_mid() {
1437 let file = parse("@mid(NAV_TLM_MID)\ntelemetry Status { x: f32 }").unwrap();
1438 let err = try_generate_c(&file).unwrap_err();
1439 assert_eq!(
1440 err,
1441 CodegenError::MessageIdValueUnsupported {
1442 packet: "Status".to_string(),
1443 }
1444 );
1445 }
1446
1447 #[test]
1448 fn c_resolves_local_symbolic_mid_and_command_code() {
1449 let out = codegen(
1450 "const SET_MODE_MID_VALUE: u16 = 0x1880\nconst SET_MODE_CODE: u16 = 1\n@mid(SET_MODE_MID_VALUE)\n@cc(SET_MODE_CODE)\ncommand SetMode { mode: u8 }",
1451 );
1452 assert!(out.contains("#define SET_MODE_MID SET_MODE_MID_VALUE"));
1453 assert!(out.contains("#define SET_MODE_CC SET_MODE_CODE"));
1454 }
1455
1456 #[test]
1457 fn c_resolves_imported_symbolic_mid_and_command_code() {
1458 let file = parse(
1459 "@mid(nav_app::SET_MODE_MID_VALUE)\n@cc(nav_app::SET_MODE_CODE)\ncommand SetMode { mode: u8 }",
1460 )
1461 .unwrap();
1462 let mut constants = ResolvedConstants::new();
1463 constants.insert(
1464 vec!["nav_app".to_string(), "SET_MODE_MID_VALUE".to_string()],
1465 0x1880,
1466 );
1467 constants.insert(vec!["nav_app".to_string(), "SET_MODE_CODE".to_string()], 2);
1468
1469 let out = try_generate_c_with_constants(&file, &constants).unwrap();
1470 assert!(out.contains("#define SET_MODE_MID 0x1880U"));
1471 assert!(out.contains("#define SET_MODE_CC 2U"));
1472 }
1473
1474 #[test]
1475 fn c_validates_local_symbolic_mid_range() {
1476 let file = parse(
1477 "const SET_MODE_MID_VALUE: u16 = 0x0801\n@mid(SET_MODE_MID_VALUE)\n@cc(1)\ncommand SetMode { mode: u8 }",
1478 )
1479 .unwrap();
1480 let err = try_generate_c(&file).unwrap_err();
1481 assert_eq!(
1482 err,
1483 CodegenError::MidRangeMismatch {
1484 packet: "SetMode".to_string(),
1485 mid: "SET_MODE_MID_VALUE".to_string(),
1486 expected: "command MID with bit 0x1000 set",
1487 }
1488 );
1489 }
1490
1491 #[test]
1492 fn c_detects_duplicate_local_symbolic_command_codes() {
1493 let file = parse(
1494 "const CMD_MID: u16 = 0x1880\nconst SET_CC: u16 = 1\n@mid(CMD_MID)\n@cc(SET_CC)\ncommand A { x: u8 }\n@mid(CMD_MID)\n@cc(SET_CC)\ncommand B { x: u8 }",
1495 )
1496 .unwrap();
1497 let err = try_generate_c(&file).unwrap_err();
1498 assert_eq!(
1499 err,
1500 CodegenError::DuplicateCommandCode {
1501 mid: "CMD_MID".to_string(),
1502 cc: "SET_CC".to_string(),
1503 first_packet: "A".to_string(),
1504 second_packet: "B".to_string(),
1505 }
1506 );
1507 }
1508
1509 #[test]
1510 fn c_rejects_duplicate_telemetry_mids() {
1511 let file =
1512 parse("@mid(0x0801)\ntelemetry A { x: u8 }\n@mid(0x0801)\ntelemetry B { x: u8 }")
1513 .unwrap();
1514 let err = try_generate_c(&file).unwrap_err();
1515 assert_eq!(
1516 err,
1517 CodegenError::DuplicateMid {
1518 mid: "0x0801U".to_string(),
1519 first_packet: "A".to_string(),
1520 second_packet: "B".to_string(),
1521 }
1522 );
1523 assert_eq!(
1524 err.to_string(),
1525 "duplicate MID `0x0801U` used by packets `A` and `B`"
1526 );
1527 }
1528
1529 #[test]
1530 fn c_allows_shared_command_mid_with_distinct_ccs() {
1531 let out = codegen(
1532 "@mid(0x1880)\n@cc(1)\ncommand A { x: u8 }\n@mid(0x1880)\n@cc(2)\ncommand B { x: u8 }",
1533 );
1534 assert!(out.contains("#define A_MID 0x1880U"));
1535 assert!(out.contains("#define B_MID 0x1880U"));
1536 assert!(out.contains("#define A_CC 1U"));
1537 assert!(out.contains("#define B_CC 2U"));
1538 }
1539
1540 #[test]
1541 fn c_rejects_duplicate_command_mid_cc_pairs() {
1542 let file = parse(
1543 "@mid(0x1880)\n@cc(1)\ncommand A { x: u8 }\n@mid(0x1880)\n@cc(1)\ncommand B { x: u8 }",
1544 )
1545 .unwrap();
1546 let err = try_generate_c(&file).unwrap_err();
1547 assert_eq!(
1548 err,
1549 CodegenError::DuplicateCommandCode {
1550 mid: "0x1880U".to_string(),
1551 cc: "1U".to_string(),
1552 first_packet: "A".to_string(),
1553 second_packet: "B".to_string(),
1554 }
1555 );
1556 assert_eq!(
1557 err.to_string(),
1558 "duplicate command MID/CC pair `0x1880U`/`1U` used by packets `A` and `B`"
1559 );
1560 }
1561
1562 #[test]
1563 fn c_rejects_command_mid_without_command_bit() {
1564 let file = parse("@mid(0x0801)\n@cc(1)\ncommand SetMode { mode: u8 }").unwrap();
1565 let err = try_generate_c(&file).unwrap_err();
1566 assert_eq!(
1567 err,
1568 CodegenError::MidRangeMismatch {
1569 packet: "SetMode".to_string(),
1570 mid: "0x0801U".to_string(),
1571 expected: "command MID with bit 0x1000 set",
1572 }
1573 );
1574 assert_eq!(
1575 err.to_string(),
1576 "packet `SetMode` has MID `0x0801U`, expected command MID with bit 0x1000 set"
1577 );
1578 }
1579
1580 #[test]
1581 fn c_rejects_telemetry_mid_with_command_bit() {
1582 let file = parse("@mid(0x1880)\ntelemetry Status { x: f32 }").unwrap();
1583 let err = try_generate_c(&file).unwrap_err();
1584 assert_eq!(
1585 err,
1586 CodegenError::MidRangeMismatch {
1587 packet: "Status".to_string(),
1588 mid: "0x1880U".to_string(),
1589 expected: "telemetry MID with bit 0x1000 clear",
1590 }
1591 );
1592 }
1593
1594 #[test]
1595 fn c_rejects_optional_fields() {
1596 let file = parse("@mid(0x0801)\ntelemetry Status { error_code?: u32 }").unwrap();
1597 let err = try_generate_c(&file).unwrap_err();
1598 assert_eq!(
1599 err,
1600 CodegenError::OptionalFieldUnsupported {
1601 container: "Status".to_string(),
1602 field: "error_code".to_string(),
1603 }
1604 );
1605 assert_eq!(
1606 err.to_string(),
1607 "optional field `Status.error_code` is not supported by cFS codegen yet"
1608 );
1609 }
1610
1611 #[test]
1612 fn c_rejects_default_values() {
1613 let file = parse("table Config { exposure_us: u32 = 10000 }").unwrap();
1614 let err = try_generate_c(&file).unwrap_err();
1615 assert_eq!(
1616 err,
1617 CodegenError::DefaultValueUnsupported {
1618 container: "Config".to_string(),
1619 field: "exposure_us".to_string(),
1620 }
1621 );
1622 assert_eq!(
1623 err.to_string(),
1624 "default value for field `Config.exposure_us` is not supported by cFS codegen yet"
1625 );
1626 }
1627
1628 #[test]
1629 fn c_rejects_enum_fields() {
1630 let file = parse(
1631 "enum CameraMode { Idle = 0 Streaming = 1 }\n@mid(0x0801)\ntelemetry Status { mode: CameraMode }",
1632 )
1633 .unwrap();
1634 let err = try_generate_c(&file).unwrap_err();
1635 assert_eq!(
1636 err,
1637 CodegenError::EnumFieldUnsupported {
1638 container: "Status".to_string(),
1639 field: "mode".to_string(),
1640 ty: "CameraMode".to_string(),
1641 }
1642 );
1643 assert_eq!(
1644 err.to_string(),
1645 "enum field `Status.mode` with type `CameraMode` needs an explicit integer representation for cFS codegen"
1646 );
1647 }
1648
1649 #[test]
1650 fn c_emits_represented_enum_fields() {
1651 let file = parse(
1652 "enum u8 CameraMode { Idle = 0 Streaming = 1 }\n@mid(0x0801)\ntelemetry Status { mode: CameraMode }",
1653 )
1654 .unwrap();
1655 let out = try_generate_c(&file).unwrap();
1656 assert!(out.contains("typedef uint8_t CameraMode_t;"));
1657 assert!(out.contains("#define CAMERA_MODE_IDLE ((CameraMode_t)0)"));
1658 assert!(out.contains("#define CAMERA_MODE_STREAMING ((CameraMode_t)1)"));
1659 assert!(out.contains(" CameraMode_t mode;"));
1660 }
1661
1662 #[test]
1663 fn c_rejects_represented_enum_missing_value() {
1664 let file = parse("enum u8 CameraMode { Idle Streaming = 1 }").unwrap();
1665 let err = try_generate_c(&file).unwrap_err();
1666 assert_eq!(
1667 err,
1668 CodegenError::EnumVariantValueRequired {
1669 enum_name: "CameraMode".to_string(),
1670 variant: "Idle".to_string(),
1671 }
1672 );
1673 }
1674
1675 #[test]
1676 fn c_rejects_non_integer_enum_repr() {
1677 let file = parse("enum bool CameraMode { Idle = 0 Streaming = 1 }").unwrap();
1678 let err = try_generate_c(&file).unwrap_err();
1679 assert_eq!(
1680 err,
1681 CodegenError::EnumRepresentationUnsupported {
1682 enum_name: "CameraMode".to_string(),
1683 repr: "bool".to_string(),
1684 }
1685 );
1686 }
1687
1688 #[test]
1689 fn c_rejects_represented_enum_out_of_range() {
1690 let file = parse("enum u8 CameraMode { TooLarge = 256 }").unwrap();
1691 let err = try_generate_c(&file).unwrap_err();
1692 assert_eq!(
1693 err,
1694 CodegenError::EnumVariantValueOutOfRange {
1695 enum_name: "CameraMode".to_string(),
1696 variant: "TooLarge".to_string(),
1697 value: 256,
1698 repr: "u8".to_string(),
1699 }
1700 );
1701 }
1702
1703 #[test]
1704 fn c_rejects_dynamic_arrays() {
1705 let file = parse("@mid(0x0801)\ntelemetry Samples { values: f32[] }").unwrap();
1706 let err = try_generate_c(&file).unwrap_err();
1707 assert_eq!(
1708 err,
1709 CodegenError::DynamicArrayUnsupported {
1710 container: "Samples".to_string(),
1711 field: "values".to_string(),
1712 ty: "f32[]".to_string(),
1713 }
1714 );
1715 assert_eq!(
1716 err.to_string(),
1717 "dynamic array field `Samples.values` with type `f32[]` is not supported by cFS codegen yet"
1718 );
1719 }
1720
1721 #[test]
1722 fn c_rejects_non_string_bounded_arrays() {
1723 let file = parse("table Buffer { bytes: u8[<=256] }").unwrap();
1724 let err = try_generate_c(&file).unwrap_err();
1725 assert_eq!(
1726 err,
1727 CodegenError::BoundedArrayUnsupported {
1728 container: "Buffer".to_string(),
1729 field: "bytes".to_string(),
1730 ty: "u8[<=256]".to_string(),
1731 }
1732 );
1733 assert_eq!(
1734 err.to_string(),
1735 "bounded array field `Buffer.bytes` with type `u8[<=256]` is not supported by cFS codegen yet"
1736 );
1737 }
1738
1739 #[test]
1740 fn const_emits_define() {
1741 let out = codegen("const NAV_TLM_MID: u16 = 0x0801");
1742 assert!(out.contains("#define NAV_TLM_MID 0x801U"));
1743 }
1744
1745 #[test]
1746 fn fixed_array_field() {
1747 let out = codegen("@mid(0x0802)\ntelemetry Imu { covariance: f64[9] }");
1748 assert!(out.contains(" double covariance[9];"));
1749 }
1750
1751 #[test]
1752 fn c_refs_use_declared_typedef_names() {
1753 let out = codegen("struct Point { x: f64 }\n@mid(0x0801)\ntelemetry Pose { point: Point }");
1754 assert!(out.contains("} Point_t;"));
1755 assert!(out.contains(" Point_t point;"));
1756 }
1757
1758 #[test]
1759 fn c_qualified_refs_use_declared_typedef_names() {
1760 let out = codegen("@mid(0x0801)\ntelemetry Stamped { header: std_msgs::Header }");
1761 assert!(out.contains(" std_msgs_Header_t header;"));
1762 }
1763
1764 #[test]
1765 fn c_bounded_string_uses_inline_storage() {
1766 let out = codegen("struct Label { name: string[<=64] }");
1767 assert!(out.contains(" char name[64];"));
1768 }
1769
1770 #[test]
1771 fn c_rejects_unbounded_strings() {
1772 let file = parse("struct Label { name: string }").unwrap();
1773 let err = try_generate_c(&file).unwrap_err();
1774 assert_eq!(
1775 err,
1776 CodegenError::UnboundedStringUnsupported {
1777 container: "Label".to_string(),
1778 field: "name".to_string(),
1779 }
1780 );
1781 assert_eq!(
1782 err.to_string(),
1783 "unbounded string field `Label.name` is not supported by cFS codegen; use `string[<=N]` or `string[N]`"
1784 );
1785 }
1786
1787 #[test]
1788 fn c_imports_emit_header_includes() {
1789 let out = codegen(r#"import "std_msgs.syn""#);
1790 assert!(out.contains("#include \"std_msgs.h\""));
1791 }
1792
1793 #[test]
1794 fn c_doc_comments_emit_for_declarations_and_fields() {
1795 let out = codegen("/// A point\nstruct Point {\n/// X axis\nx: f64\n}");
1796 assert!(out.contains("/// A point\ntypedef struct {"));
1797 assert!(out.contains(" /// X axis\n double x;"));
1798 }
1799
1800 fn rust_codegen(src: &str) -> String {
1803 generate_rust(&parse(src).unwrap(), &RustOptions::default())
1804 }
1805
1806 #[test]
1807 fn rust_tlm_struct() {
1808 let out = rust_codegen("@mid(0x0801)\ntelemetry NavTlm { x: f64 y: f64 }");
1809 assert!(out.starts_with("// Generated by Synapse. Do not edit directly.\n"));
1810 assert!(out.contains("pub const NAV_TLM_MID: u16 = 0x0801;"));
1811 assert!(out.contains("#[repr(C)]"));
1812 assert!(out.contains("pub struct NavTlm {"));
1813 assert!(out.contains(" pub cfs_header: cfs_sys::CFE_MSG_TelemetryHeader_t,"));
1814 assert!(out.contains(" pub x: f64,"));
1815 assert!(out.contains(" pub y: f64,"));
1816 }
1817
1818 #[test]
1819 fn rust_cmd_struct() {
1820 let out = rust_codegen("@mid(0x1880)\n@cc(1)\ncommand NavCmd { seq: u16 }");
1821 assert!(out.contains("pub const NAV_CMD_MID: u16 = 0x1880;"));
1822 assert!(out.contains("pub const NAV_CMD_CC: u16 = 1;"));
1823 assert!(out.contains(" pub cfs_header: cfs_sys::CFE_MSG_CommandHeader_t,"));
1824 }
1825
1826 #[test]
1827 fn rust_command_uses_command_header() {
1828 let out = rust_codegen("@mid(0x1881)\n@cc(2)\ncommand SetMode { mode: u8 }");
1829 assert!(out.contains("pub const SET_MODE_MID: u16 = 0x1881;"));
1830 assert!(out.contains("pub const SET_MODE_CC: u16 = 2;"));
1831 assert!(out.contains(" pub cfs_header: cfs_sys::CFE_MSG_CommandHeader_t,"));
1832 assert!(!out.contains("CFE_MSG_TelemetryHeader_t"));
1833 }
1834
1835 #[test]
1836 fn rust_resolves_imported_symbolic_mid_and_command_code() {
1837 let file = parse(
1838 "@mid(nav_app::SET_MODE_MID_VALUE)\n@cc(nav_app::SET_MODE_CODE)\ncommand SetMode { mode: u8 }",
1839 )
1840 .unwrap();
1841 let mut constants = ResolvedConstants::new();
1842 constants.insert(
1843 vec!["nav_app".to_string(), "SET_MODE_MID_VALUE".to_string()],
1844 0x1880,
1845 );
1846 constants.insert(vec!["nav_app".to_string(), "SET_MODE_CODE".to_string()], 2);
1847
1848 let out =
1849 try_generate_rust_with_constants(&file, &RustOptions::default(), &constants).unwrap();
1850 assert!(out.contains("pub const SET_MODE_MID: u16 = 0x1880;"));
1851 assert!(out.contains("pub const SET_MODE_CC: u16 = 2;"));
1852 }
1853
1854 #[test]
1855 fn rust_telemetry_uses_telemetry_header() {
1856 let out = rust_codegen("@mid(0x0801)\ntelemetry NavState { x: f64 }");
1857 assert!(out.contains("pub const NAV_STATE_MID: u16 = 0x0801;"));
1858 assert!(out.contains(" pub cfs_header: cfs_sys::CFE_MSG_TelemetryHeader_t,"));
1859 assert!(!out.contains("CFE_MSG_CommandHeader_t"));
1860 }
1861
1862 #[test]
1863 fn rust_table_is_plain_data_without_bus_header() {
1864 let out = rust_codegen("table NavConfig { max_speed: f64 enabled: bool }");
1865 assert!(out.contains("pub struct NavConfig {"));
1866 assert!(out.contains(" pub max_speed: f64,"));
1867 assert!(!out.contains("cfs_header"));
1868 }
1869
1870 #[test]
1871 fn rust_fixed_array() {
1872 let out = rust_codegen("@mid(0x0802)\ntelemetry Imu { covariance: f64[9] }");
1873 assert!(out.contains(" pub covariance: [f64; 9],"));
1874 }
1875
1876 #[test]
1877 fn rust_custom_module() {
1878 let opts = RustOptions {
1879 cfs_module: "my_cfs",
1880 ..Default::default()
1881 };
1882 let out = generate_rust(
1883 &parse("@mid(0x0801)\ntelemetry T { x: f32 }").unwrap(),
1884 &opts,
1885 );
1886 assert!(out.contains("my_cfs::CFE_MSG_TelemetryHeader_t"));
1887 }
1888
1889 #[test]
1890 fn rust_bare_module() {
1891 let opts = RustOptions {
1892 cfs_module: "",
1893 ..Default::default()
1894 };
1895 let out = generate_rust(
1896 &parse("@mid(0x0801)\ntelemetry T { x: f32 }").unwrap(),
1897 &opts,
1898 );
1899 assert!(out.contains(" pub cfs_header: CFE_MSG_TelemetryHeader_t,"));
1900 assert!(!out.contains("::CFE_MSG_TelemetryHeader_t"));
1901 }
1902
1903 #[test]
1904 fn rust_message_can_have_payload_header_field() {
1905 let out = rust_codegen("@mid(0x0801)\ntelemetry Stamped { header: std_msgs::Header }");
1906 assert!(out.contains(" pub cfs_header: cfs_sys::CFE_MSG_TelemetryHeader_t,"));
1907 assert!(out.contains(" pub header: std_msgs::Header,"));
1908 }
1909
1910 #[test]
1911 fn rust_rejects_legacy_message() {
1912 let file = parse("@mid(0x0801)\nmessage Bare { x: f32 }").unwrap();
1913 let err = try_generate_rust(&file, &RustOptions::default()).unwrap_err();
1914 assert_eq!(
1915 err,
1916 CodegenError::LegacyMessageUnsupported {
1917 packet: "Bare".to_string(),
1918 }
1919 );
1920 }
1921
1922 #[test]
1923 fn rust_rejects_telemetry_without_mid() {
1924 let file = parse("telemetry Status { x: f32 }").unwrap();
1925 let err = try_generate_rust(&file, &RustOptions::default()).unwrap_err();
1926 assert_eq!(
1927 err,
1928 CodegenError::MissingMid {
1929 packet: "Status".to_string(),
1930 }
1931 );
1932 }
1933
1934 #[test]
1935 fn rust_rejects_mid_range_mismatch() {
1936 let file = parse("@mid(0x1880)\ntelemetry Status { x: f32 }").unwrap();
1937 let err = try_generate_rust(&file, &RustOptions::default()).unwrap_err();
1938 assert_eq!(
1939 err,
1940 CodegenError::MidRangeMismatch {
1941 packet: "Status".to_string(),
1942 mid: "0x1880U".to_string(),
1943 expected: "telemetry MID with bit 0x1000 clear",
1944 }
1945 );
1946 }
1947
1948 #[test]
1949 fn rust_rejects_optional_fields() {
1950 let file = parse("struct Status { error_code?: u32 }").unwrap();
1951 let err = try_generate_rust(&file, &RustOptions::default()).unwrap_err();
1952 assert_eq!(
1953 err,
1954 CodegenError::OptionalFieldUnsupported {
1955 container: "Status".to_string(),
1956 field: "error_code".to_string(),
1957 }
1958 );
1959 }
1960
1961 #[test]
1962 fn rust_rejects_default_values() {
1963 let file = parse("struct Config { gain: f32 = 1.0 }").unwrap();
1964 let err = try_generate_rust(&file, &RustOptions::default()).unwrap_err();
1965 assert_eq!(
1966 err,
1967 CodegenError::DefaultValueUnsupported {
1968 container: "Config".to_string(),
1969 field: "gain".to_string(),
1970 }
1971 );
1972 }
1973
1974 #[test]
1975 fn rust_rejects_enum_fields() {
1976 let file = parse("enum CameraMode { Idle Streaming }\nstruct Status { mode: CameraMode }")
1977 .unwrap();
1978 let err = try_generate_rust(&file, &RustOptions::default()).unwrap_err();
1979 assert_eq!(
1980 err,
1981 CodegenError::EnumFieldUnsupported {
1982 container: "Status".to_string(),
1983 field: "mode".to_string(),
1984 ty: "CameraMode".to_string(),
1985 }
1986 );
1987 }
1988
1989 #[test]
1990 fn rust_emits_represented_enum_fields() {
1991 let file = parse(
1992 "enum u8 CameraMode { Idle = 0 Streaming = 1 }\nstruct Status { mode: CameraMode }",
1993 )
1994 .unwrap();
1995 let out = try_generate_rust(&file, &RustOptions::default()).unwrap();
1996 assert!(out.contains("pub type CameraMode = u8;"));
1997 assert!(out.contains("pub const CAMERA_MODE_IDLE: CameraMode = 0;"));
1998 assert!(out.contains("pub const CAMERA_MODE_STREAMING: CameraMode = 1;"));
1999 assert!(out.contains(" pub mode: CameraMode,"));
2000 }
2001
2002 #[test]
2003 fn rust_rejects_dynamic_arrays() {
2004 let file = parse("struct Samples { values: Point[] }").unwrap();
2005 let err = try_generate_rust(&file, &RustOptions::default()).unwrap_err();
2006 assert_eq!(
2007 err,
2008 CodegenError::DynamicArrayUnsupported {
2009 container: "Samples".to_string(),
2010 field: "values".to_string(),
2011 ty: "Point[]".to_string(),
2012 }
2013 );
2014 }
2015
2016 #[test]
2017 fn rust_rejects_non_string_bounded_arrays() {
2018 let file = parse("struct Buffer { bytes: bytes[<=256] }").unwrap();
2019 let err = try_generate_rust(&file, &RustOptions::default()).unwrap_err();
2020 assert_eq!(
2021 err,
2022 CodegenError::BoundedArrayUnsupported {
2023 container: "Buffer".to_string(),
2024 field: "bytes".to_string(),
2025 ty: "bytes[<=256]".to_string(),
2026 }
2027 );
2028 }
2029
2030 #[test]
2031 fn rust_const_uses_declared_type() {
2032 let out = rust_codegen("const PI: f64 = 3.14\nconst ENABLED: bool = true");
2033 assert!(out.contains("pub const PI: f64 = 3.14;"));
2034 assert!(out.contains("pub const ENABLED: bool = true;"));
2035 }
2036
2037 #[test]
2038 fn rust_bounded_string_uses_inline_storage() {
2039 let out = rust_codegen("struct Label { name: string[<=64] }");
2040 assert!(out.contains(" pub name: [u8; 64],"));
2041 }
2042
2043 #[test]
2044 fn rust_rejects_unbounded_strings() {
2045 let file = parse("struct Label { name: string }").unwrap();
2046 let err = try_generate_rust(&file, &RustOptions::default()).unwrap_err();
2047 assert_eq!(
2048 err,
2049 CodegenError::UnboundedStringUnsupported {
2050 container: "Label".to_string(),
2051 field: "name".to_string(),
2052 }
2053 );
2054 }
2055
2056 #[test]
2057 fn rust_imports_emit_crate_uses() {
2058 let out = rust_codegen(r#"import "std_msgs.syn""#);
2059 assert!(out.contains("use crate::std_msgs;"));
2060 }
2061
2062 #[test]
2063 fn rust_doc_comments_emit_for_declarations_and_fields() {
2064 let out = rust_codegen("/// A point\nstruct Point {\n/// X axis\nx: f64\n}");
2065 assert!(out.contains("/// A point\n#[repr(C)]\npub struct Point {"));
2066 assert!(out.contains(" /// X axis\n pub x: f64,"));
2067 }
2068
2069 #[test]
2070 fn screaming_snake_conversion() {
2071 assert_eq!(to_screaming_snake("NavTelemetry"), "NAV_TELEMETRY");
2072 assert_eq!(to_screaming_snake("PoseStamped"), "POSE_STAMPED");
2073 assert_eq!(to_screaming_snake("Foo"), "FOO");
2074 }
2075}