1use std::collections::HashMap;
2
3use synapse_parser::ast::{
4 ArraySuffix, Attribute, BaseType, EnumDef, FieldDef, Item, Literal, MessageDef, PacketKind,
5 PrimitiveType, StructDef, SynFile,
6};
7
8use crate::{
9 constants::{ConstContext, const_context, resolve_literal_to_u64},
10 error::CodegenError,
11 types::{CfsOptions, CfsPacket, CfsPacketKind, MsgIdLayout, ResolvedConstants},
12 util::{
13 enum_defs, file_namespace, find_cc_attr, find_mid_attr, literal_cc_str, literal_mid_str,
14 primitive_name, type_expr_display,
15 },
16};
17
18pub fn validate_cfs(file: &SynFile) -> Result<(), CodegenError> {
20 validate_cfs_with_constants(file, &ResolvedConstants::new())
21}
22
23pub fn validate_cfs_with_options(file: &SynFile, options: &CfsOptions) -> Result<(), CodegenError> {
25 validate_cfs_with_constants_and_options(file, &ResolvedConstants::new(), options)
26}
27
28pub fn validate_cfs_with_constants(
30 file: &SynFile,
31 imported_constants: &ResolvedConstants,
32) -> Result<(), CodegenError> {
33 validate_cfs_with_constants_and_options(file, imported_constants, &CfsOptions::default())
34}
35
36pub fn validate_cfs_with_constants_and_options(
38 file: &SynFile,
39 imported_constants: &ResolvedConstants,
40 options: &CfsOptions,
41) -> Result<(), CodegenError> {
42 let constants = const_context(file, imported_constants);
43 validate_supported(file, &constants, options)
44}
45
46pub fn collect_cfs_packets_with_constants(
51 file: &SynFile,
52 imported_constants: &ResolvedConstants,
53) -> Result<Vec<CfsPacket>, CodegenError> {
54 collect_cfs_packets_with_constants_and_options(file, imported_constants, &CfsOptions::default())
55}
56
57pub fn collect_cfs_packets_with_constants_and_options(
59 file: &SynFile,
60 imported_constants: &ResolvedConstants,
61 options: &CfsOptions,
62) -> Result<Vec<CfsPacket>, CodegenError> {
63 let constants = const_context(file, imported_constants);
64 collect_cfs_packets(file, &constants, options)
65}
66
67pub(crate) fn validate_supported(
68 file: &SynFile,
69 constants: &ConstContext<'_>,
70 options: &CfsOptions,
71) -> Result<(), CodegenError> {
72 let enum_defs = enum_defs(file);
73 let mut telemetry_mids = HashMap::new();
74 let mut command_codes = HashMap::new();
75 for item in &file.items {
76 validate_item(
77 item,
78 constants,
79 options,
80 &enum_defs,
81 &mut telemetry_mids,
82 &mut command_codes,
83 )?;
84 }
85 Ok(())
86}
87
88fn validate_item(
89 item: &Item,
90 constants: &ConstContext<'_>,
91 options: &CfsOptions,
92 enum_defs: &HashMap<String, &EnumDef>,
93 telemetry_mids: &mut HashMap<u64, String>,
94 command_codes: &mut HashMap<(u64, u64), String>,
95) -> Result<(), CodegenError> {
96 match item {
97 Item::Struct(s) | Item::Table(s) => validate_plain_item(s, enum_defs),
98 Item::Command(m) | Item::Telemetry(m) => validate_packet_item(
99 m,
100 constants,
101 options,
102 enum_defs,
103 telemetry_mids,
104 command_codes,
105 ),
106 _ => validate_non_packet_item(item),
107 }
108}
109
110fn validate_packet_item(
111 packet: &MessageDef,
112 constants: &ConstContext<'_>,
113 options: &CfsOptions,
114 enum_defs: &HashMap<String, &EnumDef>,
115 telemetry_mids: &mut HashMap<u64, String>,
116 command_codes: &mut HashMap<(u64, u64), String>,
117) -> Result<(), CodegenError> {
118 validate_packet(packet, constants, options, telemetry_mids, command_codes)?;
119 validate_fields(&packet.name, &packet.fields, enum_defs)
120}
121
122fn validate_non_packet_item(item: &Item) -> Result<(), CodegenError> {
123 match item {
124 Item::Message(m) => Err(CodegenError::LegacyMessageUnsupported {
125 packet: m.name.clone(),
126 }),
127 Item::Enum(e) => validate_enum(e),
128 Item::Namespace(_) | Item::Import(_) | Item::Const(_) => Ok(()),
129 Item::Struct(_) | Item::Table(_) | Item::Command(_) | Item::Telemetry(_) => {
130 unreachable!("packet and plain items handled before validate_non_packet_item")
131 }
132 }
133}
134
135fn validate_plain_item(
136 item: &StructDef,
137 enum_defs: &HashMap<String, &EnumDef>,
138) -> Result<(), CodegenError> {
139 validate_plain_item_attrs(&item.name, &item.attrs)?;
140 validate_fields(&item.name, &item.fields, enum_defs)
141}
142
143fn collect_cfs_packets(
144 file: &SynFile,
145 constants: &ConstContext<'_>,
146 options: &CfsOptions,
147) -> Result<Vec<CfsPacket>, CodegenError> {
148 let namespace = file_namespace(file);
149 let mut packets = Vec::new();
150
151 for item in &file.items {
152 if let Some(packet) = cfs_packet_from_item(item, constants, options, &namespace)? {
153 packets.push(packet);
154 }
155 }
156
157 Ok(packets)
158}
159
160fn cfs_packet_from_item(
161 item: &Item,
162 constants: &ConstContext<'_>,
163 options: &CfsOptions,
164 namespace: &[String],
165) -> Result<Option<CfsPacket>, CodegenError> {
166 let (Item::Command(packet) | Item::Telemetry(packet)) = item else {
167 return Ok(None);
168 };
169
170 let mid = required_mid(packet)?;
171 let mid_value = resolved_mid(packet, mid, constants)?;
172 validate_mid_range(packet, mid_value, mid, constants, options)?;
173 let (kind, cc_value) = collected_packet_kind(packet, constants)?;
174
175 Ok(Some(CfsPacket {
176 namespace: namespace.to_vec(),
177 name: packet.name.clone(),
178 kind,
179 mid: mid_value,
180 cc: cc_value,
181 }))
182}
183
184fn collected_packet_kind(
185 packet: &MessageDef,
186 constants: &ConstContext<'_>,
187) -> Result<(CfsPacketKind, Option<u64>), CodegenError> {
188 if packet.kind == PacketKind::Command {
189 return collected_command_packet_kind(packet, constants);
190 }
191 if packet.kind == PacketKind::Telemetry {
192 return collected_telemetry_packet_kind(packet);
193 }
194 unreachable!("legacy message items are not collected")
195}
196
197fn collected_command_packet_kind(
198 packet: &MessageDef,
199 constants: &ConstContext<'_>,
200) -> Result<(CfsPacketKind, Option<u64>), CodegenError> {
201 Ok((
202 CfsPacketKind::Command,
203 Some(required_command_code_value(packet, constants)?),
204 ))
205}
206
207fn collected_telemetry_packet_kind(
208 packet: &MessageDef,
209) -> Result<(CfsPacketKind, Option<u64>), CodegenError> {
210 reject_telemetry_command_code(packet)?;
211 Ok((CfsPacketKind::Telemetry, None))
212}
213
214fn validate_enum(e: &EnumDef) -> Result<(), CodegenError> {
215 let Some(repr) = e.repr else {
216 return Ok(());
217 };
218 let Some((min, max)) = enum_repr_range(repr) else {
219 return Err(CodegenError::EnumRepresentationUnsupported {
220 enum_name: e.name.clone(),
221 repr: primitive_name(repr).to_string(),
222 });
223 };
224
225 for variant in &e.variants {
226 let value = variant
227 .value
228 .ok_or_else(|| CodegenError::EnumVariantValueRequired {
229 enum_name: e.name.clone(),
230 variant: variant.name.clone(),
231 })?;
232 if value < min || value > max {
233 return Err(CodegenError::EnumVariantValueOutOfRange {
234 enum_name: e.name.clone(),
235 variant: variant.name.clone(),
236 value,
237 repr: primitive_name(repr).to_string(),
238 });
239 }
240 }
241 Ok(())
242}
243
244fn enum_repr_range(repr: PrimitiveType) -> Option<(i64, i64)> {
245 const RANGES: &[(PrimitiveType, (i64, i64))] = &[
246 (PrimitiveType::I8, (i8::MIN as i64, i8::MAX as i64)),
247 (PrimitiveType::I16, (i16::MIN as i64, i16::MAX as i64)),
248 (PrimitiveType::I32, (i32::MIN as i64, i32::MAX as i64)),
249 (PrimitiveType::I64, (i64::MIN, i64::MAX)),
250 (PrimitiveType::U8, (0, u8::MAX as i64)),
251 (PrimitiveType::U16, (0, u16::MAX as i64)),
252 (PrimitiveType::U32, (0, u32::MAX as i64)),
253 (PrimitiveType::U64, (0, i64::MAX)),
254 ];
255
256 RANGES
257 .iter()
258 .find_map(|(ty, range)| (*ty == repr).then_some(*range))
259}
260
261fn required_mid(packet: &MessageDef) -> Result<&Literal, CodegenError> {
262 find_mid_attr(&packet.attrs).ok_or_else(|| CodegenError::MissingMid {
263 packet: packet.name.clone(),
264 })
265}
266
267fn resolved_mid(
268 packet: &MessageDef,
269 mid: &Literal,
270 constants: &ConstContext<'_>,
271) -> Result<u64, CodegenError> {
272 resolve_literal_to_u64(mid, constants).ok_or_else(|| CodegenError::MessageIdValueUnsupported {
273 packet: packet.name.clone(),
274 })
275}
276
277fn required_command_code(packet: &MessageDef) -> Result<&Literal, CodegenError> {
278 find_cc_attr(&packet.attrs).ok_or_else(|| CodegenError::MissingCommandCode {
279 packet: packet.name.clone(),
280 })
281}
282
283fn resolved_command_code(
284 packet: &MessageDef,
285 cc: &Literal,
286 constants: &ConstContext<'_>,
287) -> Result<u64, CodegenError> {
288 resolve_literal_to_u64(cc, constants).ok_or_else(|| CodegenError::CommandCodeValueUnsupported {
289 packet: packet.name.clone(),
290 })
291}
292
293fn required_command_code_value(
294 packet: &MessageDef,
295 constants: &ConstContext<'_>,
296) -> Result<u64, CodegenError> {
297 let cc = required_command_code(packet)?;
298 resolved_command_code(packet, cc, constants)
299}
300
301fn reject_telemetry_command_code(packet: &MessageDef) -> Result<(), CodegenError> {
302 if find_cc_attr(&packet.attrs).is_some() {
303 return Err(CodegenError::CommandCodeUnsupported {
304 item: packet.name.clone(),
305 });
306 }
307 Ok(())
308}
309
310fn validate_packet(
311 packet: &MessageDef,
312 constants: &ConstContext<'_>,
313 options: &CfsOptions,
314 telemetry_mids: &mut HashMap<u64, String>,
315 command_codes: &mut HashMap<(u64, u64), String>,
316) -> Result<(), CodegenError> {
317 let (_, value) = validated_packet_mid(packet, constants, options)?;
318 validate_packet_command_code_shape(packet)?;
319 let cc_value = optional_command_code_value(packet, constants)?;
320 register_packet_mid(
321 packet,
322 constants,
323 telemetry_mids,
324 command_codes,
325 value,
326 cc_value,
327 )
328}
329
330fn validated_packet_mid<'a>(
331 packet: &'a MessageDef,
332 constants: &ConstContext<'_>,
333 options: &CfsOptions,
334) -> Result<(&'a Literal, u64), CodegenError> {
335 let mid = required_mid(packet)?;
336 let value = resolved_mid(packet, mid, constants)?;
337 validate_mid_range(packet, value, mid, constants, options)?;
338 Ok((mid, value))
339}
340
341fn validate_packet_command_code_shape(packet: &MessageDef) -> Result<(), CodegenError> {
342 match packet.kind {
343 PacketKind::Command => required_command_code(packet).map(|_| ()),
344 PacketKind::Telemetry => reject_telemetry_command_code(packet),
345 PacketKind::Message => Ok(()),
346 }
347}
348
349fn optional_command_code_value(
350 packet: &MessageDef,
351 constants: &ConstContext<'_>,
352) -> Result<Option<u64>, CodegenError> {
353 if packet.kind == PacketKind::Command {
354 return Ok(Some(required_command_code_value(packet, constants)?));
355 }
356 Ok(None)
357}
358
359fn register_packet_mid(
360 packet: &MessageDef,
361 constants: &ConstContext<'_>,
362 telemetry_mids: &mut HashMap<u64, String>,
363 command_codes: &mut HashMap<(u64, u64), String>,
364 value: u64,
365 cc_value: Option<u64>,
366) -> Result<(), CodegenError> {
367 if packet.kind == PacketKind::Command {
368 return register_command_mid(packet, constants, command_codes, value, cc_value);
369 }
370
371 if packet.kind == PacketKind::Telemetry {
372 return register_telemetry_mid(packet, constants, telemetry_mids, value);
373 }
374
375 Ok(())
376}
377
378fn register_command_mid(
379 packet: &MessageDef,
380 constants: &ConstContext<'_>,
381 command_codes: &mut HashMap<(u64, u64), String>,
382 value: u64,
383 cc_value: Option<u64>,
384) -> Result<(), CodegenError> {
385 let cc = required_command_code(packet).expect("command code was checked above");
386 let cc_value = cc_value.expect("command code value was checked above");
387 if let Some(first_packet) = command_codes.insert((value, cc_value), packet.name.clone()) {
388 return Err(CodegenError::DuplicateCommandCode {
389 mid: literal_mid_str(
390 required_mid(packet).expect("MID was checked above"),
391 constants,
392 ),
393 cc: literal_cc_str(cc, constants),
394 first_packet,
395 second_packet: packet.name.clone(),
396 });
397 }
398 Ok(())
399}
400
401fn register_telemetry_mid(
402 packet: &MessageDef,
403 constants: &ConstContext<'_>,
404 telemetry_mids: &mut HashMap<u64, String>,
405 value: u64,
406) -> Result<(), CodegenError> {
407 if let Some(first_packet) = telemetry_mids.insert(value, packet.name.clone()) {
408 return Err(CodegenError::DuplicateMid {
409 mid: literal_mid_str(
410 required_mid(packet).expect("MID was checked above"),
411 constants,
412 ),
413 first_packet,
414 second_packet: packet.name.clone(),
415 });
416 }
417 Ok(())
418}
419
420fn validate_plain_item_attrs(item_name: &str, attrs: &[Attribute]) -> Result<(), CodegenError> {
421 if find_mid_attr(attrs).is_some() {
422 return Err(CodegenError::MessageIdUnsupported {
423 item: item_name.to_string(),
424 });
425 }
426 if find_cc_attr(attrs).is_some() {
427 return Err(CodegenError::CommandCodeUnsupported {
428 item: item_name.to_string(),
429 });
430 }
431 Ok(())
432}
433
434fn validate_mid_range(
435 packet: &MessageDef,
436 value: u64,
437 mid: &Literal,
438 constants: &ConstContext<'_>,
439 options: &CfsOptions,
440) -> Result<(), CodegenError> {
441 if options.msgid_layout == MsgIdLayout::Opaque {
442 return Ok(());
443 }
444
445 let command_bit_set = (value & 0x1000) != 0;
446 let expected = match packet.kind {
447 PacketKind::Command if !command_bit_set => Some("command MID with bit 0x1000 set"),
448 PacketKind::Telemetry if command_bit_set => Some("telemetry MID with bit 0x1000 clear"),
449 PacketKind::Command | PacketKind::Telemetry | PacketKind::Message => None,
450 };
451
452 if let Some(expected) = expected {
453 return Err(CodegenError::MidRangeMismatch {
454 packet: packet.name.clone(),
455 mid: literal_mid_str(mid, constants),
456 expected,
457 });
458 }
459
460 Ok(())
461}
462
463fn validate_fields(
464 container: &str,
465 fields: &[FieldDef],
466 enum_defs: &HashMap<String, &EnumDef>,
467) -> Result<(), CodegenError> {
468 for field in fields {
469 validate_field(container, field, enum_defs)?;
470 }
471 Ok(())
472}
473
474fn validate_field(
475 container: &str,
476 field: &FieldDef,
477 enum_defs: &HashMap<String, &EnumDef>,
478) -> Result<(), CodegenError> {
479 validate_field_modifiers(container, field)?;
480 validate_field_base(container, field, enum_defs)?;
481 validate_field_array(container, field)
482}
483
484fn validate_field_modifiers(container: &str, field: &FieldDef) -> Result<(), CodegenError> {
485 if field.optional {
486 return Err(CodegenError::OptionalFieldUnsupported {
487 container: container.to_string(),
488 field: field.name.clone(),
489 });
490 }
491 if field.default.is_some() {
492 return Err(CodegenError::DefaultValueUnsupported {
493 container: container.to_string(),
494 field: field.name.clone(),
495 });
496 }
497 Ok(())
498}
499
500fn validate_field_base(
501 container: &str,
502 field: &FieldDef,
503 enum_defs: &HashMap<String, &EnumDef>,
504) -> Result<(), CodegenError> {
505 validate_string_field(container, field)?;
506 validate_enum_field(container, field, enum_defs)
507}
508
509fn validate_string_field(container: &str, field: &FieldDef) -> Result<(), CodegenError> {
510 if field.ty.base == BaseType::String && field.ty.array.is_none() {
511 return Err(CodegenError::UnboundedStringUnsupported {
512 container: container.to_string(),
513 field: field.name.clone(),
514 });
515 }
516 Ok(())
517}
518
519fn validate_enum_field(
520 container: &str,
521 field: &FieldDef,
522 enum_defs: &HashMap<String, &EnumDef>,
523) -> Result<(), CodegenError> {
524 let BaseType::Ref(segments) = &field.ty.base else {
525 return Ok(());
526 };
527 let Some(e) = segments
528 .last()
529 .and_then(|name| enum_defs.get(name.as_str()))
530 else {
531 return Ok(());
532 };
533 if e.repr.is_none() {
534 return Err(CodegenError::EnumFieldUnsupported {
535 container: container.to_string(),
536 field: field.name.clone(),
537 ty: segments.join("::"),
538 });
539 }
540 Ok(())
541}
542
543fn validate_field_array(container: &str, field: &FieldDef) -> Result<(), CodegenError> {
544 match &field.ty.array {
545 Some(ArraySuffix::Dynamic) => Err(CodegenError::DynamicArrayUnsupported {
546 container: container.to_string(),
547 field: field.name.clone(),
548 ty: type_expr_display(&field.ty),
549 }),
550 Some(ArraySuffix::Bounded(_)) if field.ty.base != BaseType::String => {
551 Err(CodegenError::BoundedArrayUnsupported {
552 container: container.to_string(),
553 field: field.name.clone(),
554 ty: type_expr_display(&field.ty),
555 })
556 }
557 Some(ArraySuffix::Bounded(_)) | Some(ArraySuffix::Fixed(_)) | None => Ok(()),
558 }
559}