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