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::{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
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 cFS code generation support with additional imported constants available.
24pub 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
32/// Collect resolved cFS packet facts with additional imported constants available.
33///
34/// This validates packet-level attributes needed to resolve MIDs and command
35/// codes, but it does not validate fields or other cFS ABI constraints.
36pub 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}