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
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 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}