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