Skip to main content

synapse_codegen_cfs/
validate.rs

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
18/// Validate that a parsed Synapse file is supported by cFS code generation.
19pub fn validate_cfs(file: &SynFile) -> Result<(), CodegenError> {
20    validate_cfs_with_constants(file, &ResolvedConstants::new())
21}
22
23/// Validate that a parsed Synapse file is supported by cFS code generation.
24pub fn validate_cfs_with_options(file: &SynFile, options: &CfsOptions) -> Result<(), CodegenError> {
25    validate_cfs_with_constants_and_options(file, &ResolvedConstants::new(), options)
26}
27
28/// Validate cFS code generation support with additional imported constants available.
29pub 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
36/// Validate cFS code generation support with imported constants and options.
37pub 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
46/// Collect resolved cFS packet facts with additional imported constants available.
47///
48/// This validates packet-level attributes needed to resolve MIDs and command
49/// codes, but it does not validate fields or other cFS ABI constraints.
50pub 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
57/// Collect resolved cFS packet facts with imported constants and options.
58pub 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}