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, 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        match item {
53            Item::Struct(s) | Item::Table(s) => {
54                validate_plain_item_attrs(&s.name, &s.attrs)?;
55                validate_fields(&s.name, &s.fields, &enum_defs)?
56            }
57            Item::Command(m) | Item::Telemetry(m) => {
58                validate_packet(m, constants, &mut telemetry_mids, &mut command_codes)?;
59                validate_fields(&m.name, &m.fields, &enum_defs)?
60            }
61            Item::Message(m) => {
62                return Err(CodegenError::LegacyMessageUnsupported {
63                    packet: m.name.clone(),
64                });
65            }
66            Item::Enum(e) => validate_enum(e)?,
67            Item::Namespace(_) | Item::Import(_) | Item::Const(_) => {}
68        }
69    }
70    Ok(())
71}
72
73fn collect_cfs_packets(
74    file: &SynFile,
75    constants: &ConstContext<'_>,
76) -> Result<Vec<CfsPacket>, CodegenError> {
77    let namespace = file_namespace(file);
78    let mut packets = Vec::new();
79
80    for item in &file.items {
81        let packet = match item {
82            Item::Command(m) | Item::Telemetry(m) => m,
83            Item::Namespace(_)
84            | Item::Import(_)
85            | Item::Const(_)
86            | Item::Enum(_)
87            | Item::Struct(_)
88            | Item::Table(_)
89            | Item::Message(_) => continue,
90        };
91
92        let Some(mid) = find_mid_attr(&packet.attrs) else {
93            return Err(CodegenError::MissingMid {
94                packet: packet.name.clone(),
95            });
96        };
97        let mid_value = resolve_literal_to_u64(mid, constants).ok_or_else(|| {
98            CodegenError::MessageIdValueUnsupported {
99                packet: packet.name.clone(),
100            }
101        })?;
102        validate_mid_range(packet, mid_value, mid, constants)?;
103
104        let cc = find_cc_attr(&packet.attrs);
105        let (kind, cc_value) = match packet.kind {
106            PacketKind::Command => {
107                let cc = cc.ok_or_else(|| CodegenError::MissingCommandCode {
108                    packet: packet.name.clone(),
109                })?;
110                let cc_value = resolve_literal_to_u64(cc, constants).ok_or_else(|| {
111                    CodegenError::CommandCodeValueUnsupported {
112                        packet: packet.name.clone(),
113                    }
114                })?;
115                (CfsPacketKind::Command, Some(cc_value))
116            }
117            PacketKind::Telemetry => {
118                if cc.is_some() {
119                    return Err(CodegenError::CommandCodeUnsupported {
120                        item: packet.name.clone(),
121                    });
122                }
123                (CfsPacketKind::Telemetry, None)
124            }
125            PacketKind::Message => continue,
126        };
127
128        packets.push(CfsPacket {
129            namespace: namespace.clone(),
130            name: packet.name.clone(),
131            kind,
132            mid: mid_value,
133            cc: cc_value,
134        });
135    }
136
137    Ok(packets)
138}
139
140fn validate_enum(e: &EnumDef) -> Result<(), CodegenError> {
141    let Some(repr) = e.repr else {
142        return Ok(());
143    };
144    let Some((min, max)) = enum_repr_range(repr) else {
145        return Err(CodegenError::EnumRepresentationUnsupported {
146            enum_name: e.name.clone(),
147            repr: primitive_name(repr).to_string(),
148        });
149    };
150
151    for variant in &e.variants {
152        let value = variant
153            .value
154            .ok_or_else(|| CodegenError::EnumVariantValueRequired {
155                enum_name: e.name.clone(),
156                variant: variant.name.clone(),
157            })?;
158        if value < min || value > max {
159            return Err(CodegenError::EnumVariantValueOutOfRange {
160                enum_name: e.name.clone(),
161                variant: variant.name.clone(),
162                value,
163                repr: primitive_name(repr).to_string(),
164            });
165        }
166    }
167    Ok(())
168}
169
170fn enum_repr_range(repr: PrimitiveType) -> Option<(i64, i64)> {
171    match repr {
172        PrimitiveType::I8 => Some((i8::MIN as i64, i8::MAX as i64)),
173        PrimitiveType::I16 => Some((i16::MIN as i64, i16::MAX as i64)),
174        PrimitiveType::I32 => Some((i32::MIN as i64, i32::MAX as i64)),
175        PrimitiveType::I64 => Some((i64::MIN, i64::MAX)),
176        PrimitiveType::U8 => Some((0, u8::MAX as i64)),
177        PrimitiveType::U16 => Some((0, u16::MAX as i64)),
178        PrimitiveType::U32 => Some((0, u32::MAX as i64)),
179        PrimitiveType::U64 => Some((0, i64::MAX)),
180        PrimitiveType::F32 | PrimitiveType::F64 | PrimitiveType::Bool | PrimitiveType::Bytes => {
181            None
182        }
183    }
184}
185
186fn validate_packet(
187    packet: &MessageDef,
188    constants: &ConstContext<'_>,
189    telemetry_mids: &mut HashMap<u64, String>,
190    command_codes: &mut HashMap<(u64, u64), String>,
191) -> Result<(), CodegenError> {
192    let Some(mid) = find_mid_attr(&packet.attrs) else {
193        return Err(CodegenError::MissingMid {
194            packet: packet.name.clone(),
195        });
196    };
197
198    let cc = find_cc_attr(&packet.attrs);
199    match packet.kind {
200        PacketKind::Command if cc.is_none() => {
201            return Err(CodegenError::MissingCommandCode {
202                packet: packet.name.clone(),
203            });
204        }
205        PacketKind::Telemetry if cc.is_some() => {
206            return Err(CodegenError::CommandCodeUnsupported {
207                item: packet.name.clone(),
208            });
209        }
210        PacketKind::Command | PacketKind::Telemetry | PacketKind::Message => {}
211    }
212    let cc_value = if packet.kind == PacketKind::Command {
213        let cc = cc.expect("command code was checked above");
214        Some(resolve_literal_to_u64(cc, constants).ok_or_else(|| {
215            CodegenError::CommandCodeValueUnsupported {
216                packet: packet.name.clone(),
217            }
218        })?)
219    } else {
220        None
221    };
222
223    let value = resolve_literal_to_u64(mid, constants).ok_or_else(|| {
224        CodegenError::MessageIdValueUnsupported {
225            packet: packet.name.clone(),
226        }
227    })?;
228
229    validate_mid_range(packet, value, mid, constants)?;
230    match packet.kind {
231        PacketKind::Command => {
232            let cc = cc.expect("command code was checked above");
233            let cc_value = cc_value.expect("command code value was checked above");
234            if let Some(first_packet) = command_codes.insert((value, cc_value), packet.name.clone())
235            {
236                return Err(CodegenError::DuplicateCommandCode {
237                    mid: literal_mid_str(mid, constants),
238                    cc: literal_cc_str(cc, constants),
239                    first_packet,
240                    second_packet: packet.name.clone(),
241                });
242            }
243        }
244        PacketKind::Telemetry => {
245            if let Some(first_packet) = telemetry_mids.insert(value, packet.name.clone()) {
246                return Err(CodegenError::DuplicateMid {
247                    mid: literal_mid_str(mid, constants),
248                    first_packet,
249                    second_packet: packet.name.clone(),
250                });
251            }
252        }
253        PacketKind::Message => {}
254    }
255
256    Ok(())
257}
258
259fn validate_plain_item_attrs(item_name: &str, attrs: &[Attribute]) -> Result<(), CodegenError> {
260    if find_mid_attr(attrs).is_some() {
261        return Err(CodegenError::MessageIdUnsupported {
262            item: item_name.to_string(),
263        });
264    }
265    if find_cc_attr(attrs).is_some() {
266        return Err(CodegenError::CommandCodeUnsupported {
267            item: item_name.to_string(),
268        });
269    }
270    Ok(())
271}
272
273fn validate_mid_range(
274    packet: &MessageDef,
275    value: u64,
276    mid: &Literal,
277    constants: &ConstContext<'_>,
278) -> Result<(), CodegenError> {
279    let command_bit_set = (value & 0x1000) != 0;
280    let expected = match packet.kind {
281        PacketKind::Command if !command_bit_set => Some("command MID with bit 0x1000 set"),
282        PacketKind::Telemetry if command_bit_set => Some("telemetry MID with bit 0x1000 clear"),
283        PacketKind::Command | PacketKind::Telemetry | PacketKind::Message => None,
284    };
285
286    if let Some(expected) = expected {
287        return Err(CodegenError::MidRangeMismatch {
288            packet: packet.name.clone(),
289            mid: literal_mid_str(mid, constants),
290            expected,
291        });
292    }
293
294    Ok(())
295}
296
297fn validate_fields(
298    container: &str,
299    fields: &[FieldDef],
300    enum_defs: &HashMap<String, &EnumDef>,
301) -> Result<(), CodegenError> {
302    for field in fields {
303        if field.optional {
304            return Err(CodegenError::OptionalFieldUnsupported {
305                container: container.to_string(),
306                field: field.name.clone(),
307            });
308        }
309        if field.default.is_some() {
310            return Err(CodegenError::DefaultValueUnsupported {
311                container: container.to_string(),
312                field: field.name.clone(),
313            });
314        }
315        if field.ty.base == BaseType::String && field.ty.array.is_none() {
316            return Err(CodegenError::UnboundedStringUnsupported {
317                container: container.to_string(),
318                field: field.name.clone(),
319            });
320        }
321        if let BaseType::Ref(segments) = &field.ty.base {
322            if let Some(e) = segments
323                .last()
324                .and_then(|name| enum_defs.get(name.as_str()))
325            {
326                if e.repr.is_none() {
327                    return Err(CodegenError::EnumFieldUnsupported {
328                        container: container.to_string(),
329                        field: field.name.clone(),
330                        ty: segments.join("::"),
331                    });
332                }
333            }
334        }
335        match &field.ty.array {
336            Some(ArraySuffix::Dynamic) => {
337                return Err(CodegenError::DynamicArrayUnsupported {
338                    container: container.to_string(),
339                    field: field.name.clone(),
340                    ty: type_expr_display(&field.ty),
341                });
342            }
343            Some(ArraySuffix::Bounded(_)) if field.ty.base != BaseType::String => {
344                return Err(CodegenError::BoundedArrayUnsupported {
345                    container: container.to_string(),
346                    field: field.name.clone(),
347                    ty: type_expr_display(&field.ty),
348                });
349            }
350            Some(ArraySuffix::Bounded(_)) | Some(ArraySuffix::Fixed(_)) | None => {}
351        }
352    }
353    Ok(())
354}