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, primitive_name,
14        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 logical-topic and command-code attributes, but it does not
35/// 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 command_group_codes = HashMap::new();
50    for item in &file.items {
51        validate_item(item, constants, &enum_defs, &mut command_group_codes)?;
52    }
53    Ok(())
54}
55
56fn validate_item(
57    item: &Item,
58    constants: &ConstContext<'_>,
59    enum_defs: &HashMap<String, &EnumDef>,
60    command_group_codes: &mut HashMap<(String, u64), String>,
61) -> Result<(), CodegenError> {
62    match item {
63        Item::Struct(s) | Item::Table(s) => validate_plain_item(s, enum_defs),
64        Item::Command(m) => validate_command_item(m, constants, enum_defs, command_group_codes),
65        Item::Telemetry(m) => validate_telemetry_item(m, enum_defs),
66        _ => validate_non_packet_item(item),
67    }
68}
69
70fn validate_command_item(
71    command: &MessageDef,
72    constants: &ConstContext<'_>,
73    enum_defs: &HashMap<String, &EnumDef>,
74    command_group_codes: &mut HashMap<(String, u64), String>,
75) -> Result<(), CodegenError> {
76    reject_message_id(&command.name, &command.attrs)?;
77    let group =
78        command
79            .command_group
80            .as_ref()
81            .ok_or_else(|| CodegenError::CommandGroupRequired {
82                packet: command.name.clone(),
83            })?;
84    let cc = required_command_code(command)?;
85    let cc_value = resolved_command_code(command, cc, constants)?;
86    let key = (group.clone(), cc_value);
87    if let Some(first_packet) = command_group_codes.insert(key, command.name.clone()) {
88        return Err(CodegenError::DuplicateCommandCodeInGroup {
89            group: group.clone(),
90            cc: literal_cc_str(cc, constants),
91            first_packet,
92            second_packet: command.name.clone(),
93        });
94    }
95
96    validate_fields(&command.name, &command.fields, enum_defs)
97}
98
99fn validate_telemetry_item(
100    telemetry: &MessageDef,
101    enum_defs: &HashMap<String, &EnumDef>,
102) -> Result<(), CodegenError> {
103    reject_message_id(&telemetry.name, &telemetry.attrs)?;
104    reject_telemetry_command_code(telemetry)?;
105    validate_fields(&telemetry.name, &telemetry.fields, enum_defs)
106}
107
108fn validate_non_packet_item(item: &Item) -> Result<(), CodegenError> {
109    match item {
110        Item::Message(m) => Err(CodegenError::LegacyMessageUnsupported {
111            packet: m.name.clone(),
112        }),
113        Item::Enum(e) => validate_enum(e),
114        Item::Namespace(_) | Item::Import(_) | Item::Const(_) => Ok(()),
115        Item::Struct(_) | Item::Table(_) | Item::Command(_) | Item::Telemetry(_) => {
116            unreachable!("packet and plain items handled before validate_non_packet_item")
117        }
118    }
119}
120
121fn validate_plain_item(
122    item: &StructDef,
123    enum_defs: &HashMap<String, &EnumDef>,
124) -> Result<(), CodegenError> {
125    validate_plain_item_attrs(&item.name, &item.attrs)?;
126    validate_fields(&item.name, &item.fields, enum_defs)
127}
128
129fn collect_cfs_packets(
130    file: &SynFile,
131    constants: &ConstContext<'_>,
132) -> Result<Vec<CfsPacket>, CodegenError> {
133    let namespace = file_namespace(file);
134    let mut packets = Vec::new();
135
136    for item in &file.items {
137        if let Some(packet) = cfs_packet_from_item(item, constants, &namespace)? {
138            packets.push(packet);
139        }
140    }
141
142    Ok(packets)
143}
144
145fn cfs_packet_from_item(
146    item: &Item,
147    constants: &ConstContext<'_>,
148    namespace: &[String],
149) -> Result<Option<CfsPacket>, CodegenError> {
150    let (Item::Command(packet) | Item::Telemetry(packet)) = item else {
151        return Ok(None);
152    };
153
154    reject_message_id(&packet.name, &packet.attrs)?;
155    let (kind, cc_value) = collected_packet_kind(packet, constants)?;
156    let topic = match packet.kind {
157        PacketKind::Command => {
158            packet
159                .command_group
160                .clone()
161                .ok_or_else(|| CodegenError::CommandGroupRequired {
162                    packet: packet.name.clone(),
163                })?
164        }
165        PacketKind::Telemetry => packet.name.clone(),
166        PacketKind::Message => unreachable!("legacy message items are not collected"),
167    };
168
169    Ok(Some(CfsPacket {
170        namespace: namespace.to_vec(),
171        name: packet.name.clone(),
172        kind,
173        topic,
174        cc: cc_value,
175    }))
176}
177
178fn collected_packet_kind(
179    packet: &MessageDef,
180    constants: &ConstContext<'_>,
181) -> Result<(CfsPacketKind, Option<u64>), CodegenError> {
182    if packet.kind == PacketKind::Command {
183        return collected_command_packet_kind(packet, constants);
184    }
185    if packet.kind == PacketKind::Telemetry {
186        return collected_telemetry_packet_kind(packet);
187    }
188    unreachable!("legacy message items are not collected")
189}
190
191fn collected_command_packet_kind(
192    packet: &MessageDef,
193    constants: &ConstContext<'_>,
194) -> Result<(CfsPacketKind, Option<u64>), CodegenError> {
195    Ok((
196        CfsPacketKind::Command,
197        Some(required_command_code_value(packet, constants)?),
198    ))
199}
200
201fn collected_telemetry_packet_kind(
202    packet: &MessageDef,
203) -> Result<(CfsPacketKind, Option<u64>), CodegenError> {
204    reject_telemetry_command_code(packet)?;
205    Ok((CfsPacketKind::Telemetry, None))
206}
207
208fn validate_enum(e: &EnumDef) -> Result<(), CodegenError> {
209    let Some(repr) = e.repr else {
210        return Ok(());
211    };
212    let Some((min, max)) = enum_repr_range(repr) else {
213        return Err(CodegenError::EnumRepresentationUnsupported {
214            enum_name: e.name.clone(),
215            repr: primitive_name(repr).to_string(),
216        });
217    };
218
219    for variant in &e.variants {
220        let value = variant
221            .value
222            .ok_or_else(|| CodegenError::EnumVariantValueRequired {
223                enum_name: e.name.clone(),
224                variant: variant.name.clone(),
225            })?;
226        if value < min || value > max {
227            return Err(CodegenError::EnumVariantValueOutOfRange {
228                enum_name: e.name.clone(),
229                variant: variant.name.clone(),
230                value,
231                repr: primitive_name(repr).to_string(),
232            });
233        }
234    }
235    Ok(())
236}
237
238fn enum_repr_range(repr: PrimitiveType) -> Option<(i64, i64)> {
239    const RANGES: &[(PrimitiveType, (i64, i64))] = &[
240        (PrimitiveType::I8, (i8::MIN as i64, i8::MAX as i64)),
241        (PrimitiveType::I16, (i16::MIN as i64, i16::MAX as i64)),
242        (PrimitiveType::I32, (i32::MIN as i64, i32::MAX as i64)),
243        (PrimitiveType::I64, (i64::MIN, i64::MAX)),
244        (PrimitiveType::U8, (0, u8::MAX as i64)),
245        (PrimitiveType::U16, (0, u16::MAX as i64)),
246        (PrimitiveType::U32, (0, u32::MAX as i64)),
247        (PrimitiveType::U64, (0, i64::MAX)),
248    ];
249
250    RANGES
251        .iter()
252        .find_map(|(ty, range)| (*ty == repr).then_some(*range))
253}
254
255fn required_command_code(packet: &MessageDef) -> Result<&Literal, CodegenError> {
256    find_cc_attr(&packet.attrs).ok_or_else(|| CodegenError::MissingCommandCode {
257        packet: packet.name.clone(),
258    })
259}
260
261fn resolved_command_code(
262    packet: &MessageDef,
263    cc: &Literal,
264    constants: &ConstContext<'_>,
265) -> Result<u64, CodegenError> {
266    let value = resolve_literal_to_u64(cc, constants).ok_or_else(|| {
267        CodegenError::CommandCodeValueUnsupported {
268            packet: packet.name.clone(),
269        }
270    })?;
271    if value > u16::MAX as u64 {
272        return Err(CodegenError::CommandCodeOutOfRange {
273            packet: packet.name.clone(),
274            value,
275        });
276    }
277    Ok(value)
278}
279
280fn required_command_code_value(
281    packet: &MessageDef,
282    constants: &ConstContext<'_>,
283) -> Result<u64, CodegenError> {
284    let cc = required_command_code(packet)?;
285    resolved_command_code(packet, cc, constants)
286}
287
288fn reject_telemetry_command_code(packet: &MessageDef) -> Result<(), CodegenError> {
289    if find_cc_attr(&packet.attrs).is_some() {
290        return Err(CodegenError::CommandCodeUnsupported {
291            item: packet.name.clone(),
292        });
293    }
294    Ok(())
295}
296
297fn validate_plain_item_attrs(item_name: &str, attrs: &[Attribute]) -> Result<(), CodegenError> {
298    reject_message_id(item_name, attrs)?;
299    if find_cc_attr(attrs).is_some() {
300        return Err(CodegenError::CommandCodeUnsupported {
301            item: item_name.to_string(),
302        });
303    }
304    Ok(())
305}
306
307fn reject_message_id(item_name: &str, attrs: &[Attribute]) -> Result<(), CodegenError> {
308    if find_mid_attr(attrs).is_some() {
309        return Err(CodegenError::MessageIdUnsupported {
310            item: item_name.to_string(),
311        });
312    }
313    Ok(())
314}
315
316fn validate_fields(
317    container: &str,
318    fields: &[FieldDef],
319    enum_defs: &HashMap<String, &EnumDef>,
320) -> Result<(), CodegenError> {
321    for field in fields {
322        validate_field(container, field, enum_defs)?;
323    }
324    Ok(())
325}
326
327fn validate_field(
328    container: &str,
329    field: &FieldDef,
330    enum_defs: &HashMap<String, &EnumDef>,
331) -> Result<(), CodegenError> {
332    validate_field_modifiers(container, field)?;
333    validate_field_base(container, field, enum_defs)?;
334    validate_field_array(container, field)
335}
336
337fn validate_field_modifiers(container: &str, field: &FieldDef) -> Result<(), CodegenError> {
338    if field.optional {
339        return Err(CodegenError::OptionalFieldUnsupported {
340            container: container.to_string(),
341            field: field.name.clone(),
342        });
343    }
344    if field.default.is_some() {
345        return Err(CodegenError::DefaultValueUnsupported {
346            container: container.to_string(),
347            field: field.name.clone(),
348        });
349    }
350    Ok(())
351}
352
353fn validate_field_base(
354    container: &str,
355    field: &FieldDef,
356    enum_defs: &HashMap<String, &EnumDef>,
357) -> Result<(), CodegenError> {
358    validate_string_field(container, field)?;
359    validate_enum_field(container, field, enum_defs)
360}
361
362fn validate_string_field(container: &str, field: &FieldDef) -> Result<(), CodegenError> {
363    if field.ty.base == BaseType::String && field.ty.array.is_none() {
364        return Err(CodegenError::UnboundedStringUnsupported {
365            container: container.to_string(),
366            field: field.name.clone(),
367        });
368    }
369    Ok(())
370}
371
372fn validate_enum_field(
373    container: &str,
374    field: &FieldDef,
375    enum_defs: &HashMap<String, &EnumDef>,
376) -> Result<(), CodegenError> {
377    let BaseType::Ref(segments) = &field.ty.base else {
378        return Ok(());
379    };
380    let Some(e) = segments
381        .last()
382        .and_then(|name| enum_defs.get(name.as_str()))
383    else {
384        return Ok(());
385    };
386    if e.repr.is_none() {
387        return Err(CodegenError::EnumFieldUnsupported {
388            container: container.to_string(),
389            field: field.name.clone(),
390            ty: segments.join("::"),
391        });
392    }
393    Ok(())
394}
395
396fn validate_field_array(container: &str, field: &FieldDef) -> Result<(), CodegenError> {
397    match &field.ty.array {
398        Some(ArraySuffix::Dynamic) => Err(CodegenError::DynamicArrayUnsupported {
399            container: container.to_string(),
400            field: field.name.clone(),
401            ty: type_expr_display(&field.ty),
402        }),
403        Some(ArraySuffix::Bounded(_)) if field.ty.base != BaseType::String => {
404            Err(CodegenError::BoundedArrayUnsupported {
405                container: container.to_string(),
406                field: field.name.clone(),
407                ty: type_expr_display(&field.ty),
408            })
409        }
410        Some(ArraySuffix::Bounded(_)) | Some(ArraySuffix::Fixed(_)) | None => Ok(()),
411    }
412}