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