1use pest::{error::Error, iterators::Pair, Parser};
2
3use crate::synapse::{Rule, SynapseParser};
4
5#[derive(Debug, Clone, PartialEq)]
8pub struct SynFile {
9 pub items: Vec<Item>,
10}
11
12#[derive(Debug, Clone, PartialEq)]
13pub enum Item {
14 Namespace(NamespaceDecl),
15 Import(ImportDecl),
16 Const(ConstDecl),
17 Enum(EnumDef),
18 Struct(StructDef),
19 Table(StructDef),
20 Command(MessageDef),
21 Telemetry(MessageDef),
22 Message(MessageDef),
23}
24
25#[derive(Debug, Clone, PartialEq)]
26pub struct NamespaceDecl {
27 pub name: ScopedIdent,
28}
29
30#[derive(Debug, Clone, PartialEq)]
31pub struct ImportDecl {
32 pub path: String,
33}
34
35#[derive(Debug, Clone, PartialEq)]
36pub struct ConstDecl {
37 pub name: String,
38 pub ty: TypeExpr,
39 pub value: Literal,
40 pub doc: Vec<String>,
41 pub attrs: Vec<Attribute>,
42}
43
44#[derive(Debug, Clone, PartialEq)]
45pub struct EnumDef {
46 pub name: String,
47 pub variants: Vec<EnumVariant>,
48 pub doc: Vec<String>,
49 pub attrs: Vec<Attribute>,
50}
51
52#[derive(Debug, Clone, PartialEq)]
53pub struct EnumVariant {
54 pub name: String,
55 pub value: Option<i64>,
56 pub doc: Vec<String>,
57}
58
59#[derive(Debug, Clone, PartialEq)]
60pub struct StructDef {
61 pub name: String,
62 pub fields: Vec<FieldDef>,
63 pub doc: Vec<String>,
64 pub attrs: Vec<Attribute>,
65}
66
67#[derive(Debug, Clone, PartialEq)]
68pub struct MessageDef {
69 pub kind: PacketKind,
70 pub name: String,
71 pub fields: Vec<FieldDef>,
72 pub doc: Vec<String>,
73 pub attrs: Vec<Attribute>,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum PacketKind {
78 Message,
80 Command,
82 Telemetry,
84}
85
86#[derive(Debug, Clone, PartialEq)]
87pub struct FieldDef {
88 pub name: String,
89 pub optional: bool,
90 pub ty: TypeExpr,
91 pub default: Option<Literal>,
92 pub doc: Vec<String>,
93}
94
95#[derive(Debug, Clone, PartialEq)]
96pub struct TypeExpr {
97 pub base: BaseType,
98 pub array: Option<ArraySuffix>,
99}
100
101#[derive(Debug, Clone, PartialEq)]
102pub enum BaseType {
103 Primitive(PrimitiveType),
104 String,
105 Ref(ScopedIdent),
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum PrimitiveType {
110 F32,
111 F64,
112 I8,
113 I16,
114 I32,
115 I64,
116 U8,
117 U16,
118 U32,
119 U64,
120 Bool,
121 Bytes,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq)]
125pub enum ArraySuffix {
126 Dynamic,
128 Fixed(u64),
130 Bounded(u64),
132}
133
134pub type ScopedIdent = Vec<String>;
137
138#[derive(Debug, Clone, PartialEq)]
139pub enum Literal {
140 Float(f64),
141 Int(i64),
142 Hex(u64),
143 Bool(bool),
144 Str(String),
145 Ident(ScopedIdent),
147}
148
149#[derive(Debug, Clone, PartialEq)]
151pub struct Attribute {
152 pub name: String,
153 pub value: Literal,
154}
155
156pub fn parse(input: &str) -> Result<SynFile, Error<Rule>> {
159 let file_pair = SynapseParser::parse(Rule::file, input)?.next().unwrap();
160 Ok(build_file(file_pair))
161}
162
163fn build_file(pair: Pair<Rule>) -> SynFile {
166 let items = pair
167 .into_inner()
168 .filter_map(|p| match p.as_rule() {
169 Rule::namespace_decl => Some(Item::Namespace(build_namespace(p))),
170 Rule::import_decl => Some(Item::Import(build_import(p))),
171 Rule::const_decl => Some(Item::Const(build_const(p))),
172 Rule::enum_def => Some(Item::Enum(build_enum(p))),
173 Rule::struct_def => Some(Item::Struct(build_struct(p))),
174 Rule::table_def => Some(Item::Table(build_struct(p))),
175 Rule::command_def => Some(Item::Command(build_packet(p, PacketKind::Command))),
176 Rule::telemetry_def => Some(Item::Telemetry(build_packet(p, PacketKind::Telemetry))),
177 Rule::message_def => Some(Item::Message(build_packet(p, PacketKind::Message))),
178 Rule::EOI => None,
179 r => unreachable!("unexpected rule: {:?}", r),
180 })
181 .collect();
182 SynFile { items }
183}
184
185fn build_namespace(pair: Pair<Rule>) -> NamespaceDecl {
186 let scoped = pair.into_inner().next().unwrap();
187 NamespaceDecl { name: build_scoped_ident(scoped) }
188}
189
190fn build_import(pair: Pair<Rule>) -> ImportDecl {
191 let s = pair.into_inner().next().unwrap().as_str();
192 ImportDecl { path: s[1..s.len() - 1].to_string() }
193}
194
195fn build_const(pair: Pair<Rule>) -> ConstDecl {
196 let mut inner = pair.into_inner().peekable();
197 let doc = extract_doc(&mut inner);
198 let attrs = extract_attrs(&mut inner);
199 let name = inner.next().unwrap().as_str().to_string();
200 let ty = build_type_expr(inner.next().unwrap());
201 let value = build_literal(inner.next().unwrap());
202 ConstDecl { name, ty, value, doc, attrs }
203}
204
205fn build_enum(pair: Pair<Rule>) -> EnumDef {
206 let mut inner = pair.into_inner().peekable();
207 let doc = extract_doc(&mut inner);
208 let attrs = extract_attrs(&mut inner);
209 let name = inner.next().unwrap().as_str().to_string();
210 let variants = inner.map(build_enum_variant).collect();
211 EnumDef { name, variants, doc, attrs }
212}
213
214fn build_enum_variant(pair: Pair<Rule>) -> EnumVariant {
215 let mut inner = pair.into_inner().peekable();
216 let doc = extract_doc(&mut inner);
217 let name = inner.next().unwrap().as_str().to_string();
218 let value = inner.next().map(|p| p.as_str().parse::<i64>().unwrap());
219 EnumVariant { name, value, doc }
220}
221
222fn build_struct(pair: Pair<Rule>) -> StructDef {
223 let mut inner = pair.into_inner().peekable();
224 let doc = extract_doc(&mut inner);
225 let attrs = extract_attrs(&mut inner);
226 let name = inner.next().unwrap().as_str().to_string();
227 let fields = inner.map(build_field).collect();
228 StructDef { name, fields, doc, attrs }
229}
230
231fn build_packet(pair: Pair<Rule>, kind: PacketKind) -> MessageDef {
232 let mut inner = pair.into_inner().peekable();
233 let doc = extract_doc(&mut inner);
234 let attrs = extract_attrs(&mut inner);
235 let name = inner.next().unwrap().as_str().to_string();
236 let fields = inner.map(build_field).collect();
237 MessageDef { kind, name, fields, doc, attrs }
238}
239
240fn build_field(pair: Pair<Rule>) -> FieldDef {
241 let mut inner = pair.into_inner().peekable();
242 let doc = extract_doc(&mut inner);
243 let name = inner.next().unwrap().as_str().to_string();
244
245 let next = inner.next().unwrap();
246 let (optional, type_pair) = if next.as_rule() == Rule::optional_marker {
247 (true, inner.next().unwrap())
248 } else {
249 (false, next)
250 };
251
252 let ty = build_type_expr(type_pair);
253 let default = inner.next().map(build_literal);
254
255 FieldDef { name, optional, ty, default, doc }
256}
257
258fn extract_doc<'i>(
260 inner: &mut std::iter::Peekable<impl Iterator<Item = Pair<'i, Rule>>>,
261) -> Vec<String> {
262 if inner.peek().map(|p| p.as_rule()) == Some(Rule::doc_block) {
263 inner
264 .next()
265 .unwrap()
266 .into_inner()
267 .map(|p| p.as_str().strip_prefix("##").unwrap_or("").trim().to_string())
268 .collect()
269 } else {
270 vec![]
271 }
272}
273
274fn extract_attrs<'i>(
276 inner: &mut std::iter::Peekable<impl Iterator<Item = Pair<'i, Rule>>>,
277) -> Vec<Attribute> {
278 let mut attrs = vec![];
279 while inner.peek().map(|p| p.as_rule()) == Some(Rule::attribute) {
280 let attr = inner.next().unwrap();
281 let mut ai = attr.into_inner();
282 let name = ai.next().unwrap().as_str().to_string();
283 let value = build_literal(ai.next().unwrap());
284 attrs.push(Attribute { name, value });
285 }
286 attrs
287}
288
289fn build_type_expr(pair: Pair<Rule>) -> TypeExpr {
290 let mut inner = pair.into_inner();
291 let base = build_base_type(inner.next().unwrap());
292 let array = inner.next().map(build_array_suffix);
293 TypeExpr { base, array }
294}
295
296fn build_base_type(pair: Pair<Rule>) -> BaseType {
297 let inner = pair.into_inner().next().unwrap();
298 match inner.as_rule() {
299 Rule::string_type => BaseType::String,
300 Rule::primitive_type => BaseType::Primitive(build_primitive_type(inner)),
301 Rule::type_ref => BaseType::Ref(build_scoped_ident(
302 inner.into_inner().next().unwrap(),
303 )),
304 r => unreachable!("unexpected base_type rule: {:?}", r),
305 }
306}
307
308fn build_primitive_type(pair: Pair<Rule>) -> PrimitiveType {
309 match pair.as_str() {
310 "f32" => PrimitiveType::F32,
311 "f64" => PrimitiveType::F64,
312 "i8" => PrimitiveType::I8,
313 "i16" => PrimitiveType::I16,
314 "i32" => PrimitiveType::I32,
315 "i64" => PrimitiveType::I64,
316 "u8" => PrimitiveType::U8,
317 "u16" => PrimitiveType::U16,
318 "u32" => PrimitiveType::U32,
319 "u64" => PrimitiveType::U64,
320 "bool" => PrimitiveType::Bool,
321 "bytes" => PrimitiveType::Bytes,
322 s => unreachable!("unknown primitive: {}", s),
323 }
324}
325
326fn build_array_suffix(pair: Pair<Rule>) -> ArraySuffix {
327 match pair.into_inner().next() {
328 None => ArraySuffix::Dynamic,
329 Some(p) => {
330 let inner = p.into_inner().next().unwrap();
331 match inner.as_rule() {
332 Rule::bounded_size => {
333 let n = inner
334 .into_inner()
335 .next()
336 .unwrap()
337 .as_str()
338 .parse::<u64>()
339 .unwrap();
340 ArraySuffix::Bounded(n)
341 }
342 Rule::pos_int => ArraySuffix::Fixed(inner.as_str().parse::<u64>().unwrap()),
343 r => unreachable!("unexpected array_size rule: {:?}", r),
344 }
345 }
346 }
347}
348
349fn build_literal(pair: Pair<Rule>) -> Literal {
350 let inner = pair.into_inner().next().unwrap();
351 match inner.as_rule() {
352 Rule::float_lit => Literal::Float(inner.as_str().parse::<f64>().unwrap()),
353 Rule::hex_lit => {
354 let s = inner.as_str();
355 let digits = &s[2..]; Literal::Hex(u64::from_str_radix(digits, 16).unwrap())
357 }
358 Rule::int_lit => Literal::Int(inner.as_str().parse::<i64>().unwrap()),
359 Rule::bool_lit => Literal::Bool(inner.as_str() == "true"),
360 Rule::string_lit => {
361 let s = inner.as_str();
362 Literal::Str(unescape(&s[1..s.len() - 1]))
363 }
364 Rule::ident_lit => Literal::Ident(build_scoped_ident(
365 inner.into_inner().next().unwrap(),
366 )),
367 r => unreachable!("unexpected literal rule: {:?}", r),
368 }
369}
370
371fn build_scoped_ident(pair: Pair<Rule>) -> ScopedIdent {
372 pair.into_inner().map(|p| p.as_str().to_string()).collect()
373}
374
375fn unescape(s: &str) -> String {
376 let mut out = String::with_capacity(s.len());
377 let mut chars = s.chars();
378 while let Some(c) = chars.next() {
379 if c == '\\' {
380 match chars.next() {
381 Some('n') => out.push('\n'),
382 Some('t') => out.push('\t'),
383 Some('r') => out.push('\r'),
384 Some('\\') => out.push('\\'),
385 Some('"') => out.push('"'),
386 Some(c) => { out.push('\\'); out.push(c); }
387 None => out.push('\\'),
388 }
389 } else {
390 out.push(c);
391 }
392 }
393 out
394}
395
396#[cfg(test)]
399mod tests {
400 use super::*;
401
402 fn p(input: &str) -> SynFile {
403 parse(input).expect("parse failed")
404 }
405
406 #[test]
409 fn namespace_simple() {
410 let f = p("namespace geometry");
411 assert_eq!(
412 f.items[0],
413 Item::Namespace(NamespaceDecl { name: vec!["geometry".into()] })
414 );
415 }
416
417 #[test]
418 fn namespace_qualified() {
419 let f = p("namespace nav::msgs");
420 assert_eq!(
421 f.items[0],
422 Item::Namespace(NamespaceDecl {
423 name: vec!["nav".into(), "msgs".into()]
424 })
425 );
426 }
427
428 #[test]
431 fn import_path() {
432 let f = p(r#"import "geometry.syn""#);
433 assert_eq!(
434 f.items[0],
435 Item::Import(ImportDecl { path: "geometry.syn".into() })
436 );
437 }
438
439 #[test]
442 fn const_float() {
443 let f = p("const PI: f64 = 3.14");
444 assert_eq!(
445 f.items[0],
446 Item::Const(ConstDecl {
447 name: "PI".into(),
448 ty: TypeExpr { base: BaseType::Primitive(PrimitiveType::F64), array: None },
449 value: Literal::Float(3.14),
450 doc: vec![], attrs: vec![],
451 })
452 );
453 }
454
455 #[test]
456 fn const_int() {
457 let f = p("const MAX: u32 = 256");
458 assert_eq!(
459 f.items[0],
460 Item::Const(ConstDecl {
461 name: "MAX".into(),
462 ty: TypeExpr { base: BaseType::Primitive(PrimitiveType::U32), array: None },
463 value: Literal::Int(256),
464 doc: vec![], attrs: vec![],
465 })
466 );
467 }
468
469 #[test]
470 fn const_string() {
471 let f = p(r#"const FRAME: string = "world""#);
472 assert_eq!(
473 f.items[0],
474 Item::Const(ConstDecl {
475 name: "FRAME".into(),
476 ty: TypeExpr { base: BaseType::String, array: None },
477 value: Literal::Str("world".into()),
478 doc: vec![], attrs: vec![],
479 })
480 );
481 }
482
483 #[test]
486 fn enum_with_values() {
487 let f = p("enum DriveMode { Idle = 0 Forward = 1 Error = 2 }");
488 let Item::Enum(e) = &f.items[0] else { panic!() };
489 assert_eq!(e.name, "DriveMode");
490 assert_eq!(e.variants[0], EnumVariant { name: "Idle".into(), value: Some(0), doc: vec![] });
491 assert_eq!(e.variants[1], EnumVariant { name: "Forward".into(), value: Some(1), doc: vec![] });
492 assert_eq!(e.variants[2], EnumVariant { name: "Error".into(), value: Some(2), doc: vec![] });
493 assert!(e.attrs.is_empty());
494 }
495
496 #[test]
497 fn enum_without_values() {
498 let f = p("enum Dir { North South East West }");
499 let Item::Enum(e) = &f.items[0] else { panic!() };
500 assert!(e.variants.iter().all(|v| v.value.is_none()));
501 assert_eq!(e.variants.len(), 4);
502 }
503
504 #[test]
507 fn struct_basic() {
508 let f = p("struct Point { x: f64 = 0.0 y: f64 = 0.0 z: f64 = 0.0 }");
509 let Item::Struct(s) = &f.items[0] else { panic!() };
510 assert_eq!(s.name, "Point");
511 assert_eq!(s.fields.len(), 3);
512 assert_eq!(s.fields[0].name, "x");
513 assert_eq!(s.fields[0].ty.base, BaseType::Primitive(PrimitiveType::F64));
514 assert_eq!(s.fields[0].default, Some(Literal::Float(0.0)));
515 assert!(!s.fields[0].optional);
516 }
517
518 #[test]
519 fn struct_qualified_type() {
520 let f = p("struct Pose { position: geometry::Point orientation: geometry::Quaternion }");
521 let Item::Struct(s) = &f.items[0] else { panic!() };
522 assert_eq!(
523 s.fields[0].ty.base,
524 BaseType::Ref(vec!["geometry".into(), "Point".into()])
525 );
526 }
527
528 #[test]
531 fn message_optional_field() {
532 let f = p("message Foo { required: i32 optional?: string }");
533 let Item::Message(m) = &f.items[0] else { panic!() };
534 assert_eq!(m.kind, PacketKind::Message);
535 assert!(!m.fields[0].optional);
536 assert!(m.fields[1].optional);
537 assert_eq!(m.fields[1].ty.base, BaseType::String);
538 }
539
540 #[test]
541 fn command_packet_kind() {
542 let f = p("@mid(0x1880)\ncommand SetMode { mode: u8 }");
543 let Item::Command(m) = &f.items[0] else { panic!() };
544 assert_eq!(m.kind, PacketKind::Command);
545 assert_eq!(m.name, "SetMode");
546 assert_eq!(m.fields[0].name, "mode");
547 }
548
549 #[test]
550 fn telemetry_packet_kind() {
551 let f = p("@mid(0x0801)\ntelemetry NavState { x: f64 }");
552 let Item::Telemetry(m) = &f.items[0] else { panic!() };
553 assert_eq!(m.kind, PacketKind::Telemetry);
554 assert_eq!(m.name, "NavState");
555 assert_eq!(m.fields[0].name, "x");
556 }
557
558 #[test]
559 fn table_is_plain_data_item() {
560 let f = p("table NavConfig { max_speed: f64 enabled: bool }");
561 let Item::Table(t) = &f.items[0] else { panic!() };
562 assert_eq!(t.name, "NavConfig");
563 assert_eq!(t.fields.len(), 2);
564 }
565
566 #[test]
567 fn message_array_fields() {
568 let f = p("message D { dynamic: u8[] fixed: f64[3] bounded: u8[<=256] }");
569 let Item::Message(m) = &f.items[0] else { panic!() };
570 assert_eq!(m.fields[0].ty.array, Some(ArraySuffix::Dynamic));
571 assert_eq!(m.fields[1].ty.array, Some(ArraySuffix::Fixed(3)));
572 assert_eq!(m.fields[2].ty.array, Some(ArraySuffix::Bounded(256)));
573 }
574
575 #[test]
576 fn message_enum_default() {
577 let f = p("message S { mode: DriveMode = DriveMode::Idle }");
578 let Item::Message(m) = &f.items[0] else { panic!() };
579 assert_eq!(
580 m.fields[0].default,
581 Some(Literal::Ident(vec!["DriveMode".into(), "Idle".into()]))
582 );
583 }
584
585 #[test]
586 fn message_string_bounded() {
587 let f = p(r#"message S { label: string[<=64] = "robot" }"#);
588 let Item::Message(m) = &f.items[0] else { panic!() };
589 assert_eq!(m.fields[0].ty.base, BaseType::String);
590 assert_eq!(m.fields[0].ty.array, Some(ArraySuffix::Bounded(64)));
591 assert_eq!(m.fields[0].default, Some(Literal::Str("robot".into())));
592 }
593
594 #[test]
597 fn hex_literal_const() {
598 let f = p("const MID: u16 = 0x0801");
599 let Item::Const(c) = &f.items[0] else { panic!() };
600 assert_eq!(c.value, Literal::Hex(0x0801));
601 }
602
603 #[test]
604 fn hex_literal_uppercase() {
605 let f = p("const MID: u16 = 0X1F80");
606 let Item::Const(c) = &f.items[0] else { panic!() };
607 assert_eq!(c.value, Literal::Hex(0x1F80));
608 }
609
610 #[test]
613 fn attribute_hex_on_message() {
614 let f = p("@mid(0x0801)\nmessage NavTlm { x: f64 }");
615 let Item::Message(m) = &f.items[0] else { panic!() };
616 assert_eq!(m.attrs.len(), 1);
617 assert_eq!(m.attrs[0].name, "mid");
618 assert_eq!(m.attrs[0].value, Literal::Hex(0x0801));
619 }
620
621 #[test]
622 fn attribute_ident_ref() {
623 let f = p("@mid(nav_app::NAV_TLM_MID)\nmessage NavTlm { x: f64 }");
624 let Item::Message(m) = &f.items[0] else { panic!() };
625 assert_eq!(m.attrs[0].value, Literal::Ident(vec!["nav_app".into(), "NAV_TLM_MID".into()]));
626 }
627
628 #[test]
629 fn no_attrs_is_empty() {
630 let f = p("message Foo { x: i32 }");
631 let Item::Message(m) = &f.items[0] else { panic!() };
632 assert!(m.attrs.is_empty());
633 }
634
635 #[test]
638 fn string_escape_sequences() {
639 let f = p(r#"const S: string = "hello\nworld""#);
640 let Item::Const(c) = &f.items[0] else { panic!() };
641 assert_eq!(c.value, Literal::Str("hello\nworld".into()));
642 }
643
644 #[test]
645 fn string_escape_quote() {
646 let f = p(r#"const S: string = "say \"hi\"""#);
647 let Item::Const(c) = &f.items[0] else { panic!() };
648 assert_eq!(c.value, Literal::Str("say \"hi\"".into()));
649 }
650
651 #[test]
654 fn full_robot_state() {
655 let src = r#"
656 namespace robot
657 import "geometry.syn"
658
659 enum DriveMode {
660 Idle = 0
661 Forward = 1
662 Error = 2
663 }
664
665 const MAX_SPEED: f64 = 2.5
666
667 message RobotState {
668 mode: DriveMode = DriveMode::Idle
669 position: geometry::Point
670 battery: f32 = 100.0
671 label: string[<=64] = "robot"
672 sensor_data: u8[]
673 error_code?: i32
674 }
675 "#;
676
677 let f = parse(src).unwrap();
678 assert_eq!(f.items.len(), 5);
679
680 let Item::Namespace(ns) = &f.items[0] else { panic!() };
681 assert_eq!(ns.name, vec!["robot"]);
682
683 let Item::Enum(e) = &f.items[2] else { panic!() };
684 assert_eq!(e.variants.len(), 3);
685
686 let Item::Message(m) = &f.items[4] else { panic!() };
687 assert_eq!(m.name, "RobotState");
688 assert_eq!(m.fields.len(), 6);
689
690 assert!(m.fields[5].optional);
692 assert_eq!(m.fields[5].name, "error_code");
693 }
694}