1use pest::{Parser, error::Error, iterators::Pair};
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 repr: Option<PrimitiveType>,
48 pub variants: Vec<EnumVariant>,
49 pub doc: Vec<String>,
50 pub attrs: Vec<Attribute>,
51}
52
53#[derive(Debug, Clone, PartialEq)]
54pub struct EnumVariant {
55 pub name: String,
56 pub value: Option<i64>,
57 pub doc: Vec<String>,
58}
59
60#[derive(Debug, Clone, PartialEq)]
61pub struct StructDef {
62 pub name: String,
63 pub fields: Vec<FieldDef>,
64 pub doc: Vec<String>,
65 pub attrs: Vec<Attribute>,
66}
67
68#[derive(Debug, Clone, PartialEq)]
69pub struct MessageDef {
70 pub kind: PacketKind,
71 pub name: String,
72 pub fields: Vec<FieldDef>,
73 pub doc: Vec<String>,
74 pub attrs: Vec<Attribute>,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum PacketKind {
79 Message,
81 Command,
83 Telemetry,
85}
86
87#[derive(Debug, Clone, PartialEq)]
88pub struct FieldDef {
89 pub name: String,
90 pub optional: bool,
91 pub ty: TypeExpr,
92 pub default: Option<Literal>,
93 pub doc: Vec<String>,
94}
95
96#[derive(Debug, Clone, PartialEq)]
97pub struct TypeExpr {
98 pub base: BaseType,
99 pub array: Option<ArraySuffix>,
100}
101
102#[derive(Debug, Clone, PartialEq)]
103pub enum BaseType {
104 Primitive(PrimitiveType),
105 String,
106 Ref(ScopedIdent),
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum PrimitiveType {
111 F32,
112 F64,
113 I8,
114 I16,
115 I32,
116 I64,
117 U8,
118 U16,
119 U32,
120 U64,
121 Bool,
122 Bytes,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub enum ArraySuffix {
127 Dynamic,
129 Fixed(u64),
131 Bounded(u64),
133}
134
135pub type ScopedIdent = Vec<String>;
138
139#[derive(Debug, Clone, PartialEq)]
140pub enum Literal {
141 Float(f64),
142 Int(i64),
143 Hex(u64),
144 Bool(bool),
145 Str(String),
146 Ident(ScopedIdent),
148}
149
150#[derive(Debug, Clone, PartialEq)]
152pub struct Attribute {
153 pub name: String,
154 pub value: Literal,
155}
156
157pub fn parse(input: &str) -> Result<SynFile, Error<Rule>> {
160 let file_pair = SynapseParser::parse(Rule::file, input)?.next().unwrap();
161 Ok(build_file(file_pair))
162}
163
164fn build_file(pair: Pair<Rule>) -> SynFile {
167 let items = pair
168 .into_inner()
169 .filter_map(|p| match p.as_rule() {
170 Rule::namespace_decl => Some(Item::Namespace(build_namespace(p))),
171 Rule::import_decl => Some(Item::Import(build_import(p))),
172 Rule::const_decl => Some(Item::Const(build_const(p))),
173 Rule::enum_def => Some(Item::Enum(build_enum(p))),
174 Rule::struct_def => Some(Item::Struct(build_struct(p))),
175 Rule::table_def => Some(Item::Table(build_struct(p))),
176 Rule::command_def => Some(Item::Command(build_packet(p, PacketKind::Command))),
177 Rule::telemetry_def => Some(Item::Telemetry(build_packet(p, PacketKind::Telemetry))),
178 Rule::message_def => Some(Item::Message(build_packet(p, PacketKind::Message))),
179 Rule::EOI => None,
180 r => unreachable!("unexpected rule: {:?}", r),
181 })
182 .collect();
183 SynFile { items }
184}
185
186fn build_namespace(pair: Pair<Rule>) -> NamespaceDecl {
187 let scoped = pair.into_inner().next().unwrap();
188 NamespaceDecl {
189 name: build_scoped_ident(scoped),
190 }
191}
192
193fn build_import(pair: Pair<Rule>) -> ImportDecl {
194 let s = pair.into_inner().next().unwrap().as_str();
195 ImportDecl {
196 path: s[1..s.len() - 1].to_string(),
197 }
198}
199
200fn build_const(pair: Pair<Rule>) -> ConstDecl {
201 let mut inner = pair.into_inner().peekable();
202 let doc = extract_doc(&mut inner);
203 let attrs = extract_attrs(&mut inner);
204 let name = inner.next().unwrap().as_str().to_string();
205 let ty = build_type_expr(inner.next().unwrap());
206 let value = build_literal(inner.next().unwrap());
207 ConstDecl {
208 name,
209 ty,
210 value,
211 doc,
212 attrs,
213 }
214}
215
216fn build_enum(pair: Pair<Rule>) -> EnumDef {
217 let mut inner = pair.into_inner().peekable();
218 let doc = extract_doc(&mut inner);
219 let attrs = extract_attrs(&mut inner);
220 let first = inner.next().unwrap();
221 let (repr, name) = if first.as_rule() == Rule::primitive_type {
222 let repr = build_primitive_type(first);
223 (Some(repr), inner.next().unwrap().as_str().to_string())
224 } else {
225 (None, first.as_str().to_string())
226 };
227 let variants = inner.map(build_enum_variant).collect();
228 EnumDef {
229 name,
230 repr,
231 variants,
232 doc,
233 attrs,
234 }
235}
236
237fn build_enum_variant(pair: Pair<Rule>) -> EnumVariant {
238 let mut inner = pair.into_inner().peekable();
239 let doc = extract_doc(&mut inner);
240 let name = inner.next().unwrap().as_str().to_string();
241 let value = inner.next().map(|p| p.as_str().parse::<i64>().unwrap());
242 EnumVariant { name, value, doc }
243}
244
245fn build_struct(pair: Pair<Rule>) -> StructDef {
246 let mut inner = pair.into_inner().peekable();
247 let doc = extract_doc(&mut inner);
248 let attrs = extract_attrs(&mut inner);
249 let name = inner.next().unwrap().as_str().to_string();
250 let fields = inner.map(build_field).collect();
251 StructDef {
252 name,
253 fields,
254 doc,
255 attrs,
256 }
257}
258
259fn build_packet(pair: Pair<Rule>, kind: PacketKind) -> MessageDef {
260 let mut inner = pair.into_inner().peekable();
261 let doc = extract_doc(&mut inner);
262 let attrs = extract_attrs(&mut inner);
263 let name = inner.next().unwrap().as_str().to_string();
264 let fields = inner.map(build_field).collect();
265 MessageDef {
266 kind,
267 name,
268 fields,
269 doc,
270 attrs,
271 }
272}
273
274fn build_field(pair: Pair<Rule>) -> FieldDef {
275 let mut inner = pair.into_inner().peekable();
276 let doc = extract_doc(&mut inner);
277 let name = inner.next().unwrap().as_str().to_string();
278
279 let next = inner.next().unwrap();
280 let (optional, type_pair) = if next.as_rule() == Rule::optional_marker {
281 (true, inner.next().unwrap())
282 } else {
283 (false, next)
284 };
285
286 let ty = build_type_expr(type_pair);
287 let default = inner.next().map(build_literal);
288
289 FieldDef {
290 name,
291 optional,
292 ty,
293 default,
294 doc,
295 }
296}
297
298fn extract_doc<'i>(
300 inner: &mut std::iter::Peekable<impl Iterator<Item = Pair<'i, Rule>>>,
301) -> Vec<String> {
302 if inner.peek().map(|p| p.as_rule()) == Some(Rule::doc_block) {
303 inner
304 .next()
305 .unwrap()
306 .into_inner()
307 .map(|p| {
308 p.as_str()
309 .strip_prefix("///")
310 .unwrap_or("")
311 .trim()
312 .to_string()
313 })
314 .collect()
315 } else {
316 vec![]
317 }
318}
319
320fn extract_attrs<'i>(
322 inner: &mut std::iter::Peekable<impl Iterator<Item = Pair<'i, Rule>>>,
323) -> Vec<Attribute> {
324 let mut attrs = vec![];
325 while inner.peek().map(|p| p.as_rule()) == Some(Rule::attribute) {
326 let attr = inner.next().unwrap();
327 let mut ai = attr.into_inner();
328 let name = ai.next().unwrap().as_str().to_string();
329 let value = build_literal(ai.next().unwrap());
330 attrs.push(Attribute { name, value });
331 }
332 attrs
333}
334
335fn build_type_expr(pair: Pair<Rule>) -> TypeExpr {
336 let mut inner = pair.into_inner();
337 let base = build_base_type(inner.next().unwrap());
338 let array = inner.next().map(build_array_suffix);
339 TypeExpr { base, array }
340}
341
342fn build_base_type(pair: Pair<Rule>) -> BaseType {
343 let inner = pair.into_inner().next().unwrap();
344 match inner.as_rule() {
345 Rule::string_type => BaseType::String,
346 Rule::primitive_type => BaseType::Primitive(build_primitive_type(inner)),
347 Rule::type_ref => BaseType::Ref(build_scoped_ident(inner.into_inner().next().unwrap())),
348 r => unreachable!("unexpected base_type rule: {:?}", r),
349 }
350}
351
352fn build_primitive_type(pair: Pair<Rule>) -> PrimitiveType {
353 match pair.as_str() {
354 "f32" => PrimitiveType::F32,
355 "f64" => PrimitiveType::F64,
356 "i8" => PrimitiveType::I8,
357 "i16" => PrimitiveType::I16,
358 "i32" => PrimitiveType::I32,
359 "i64" => PrimitiveType::I64,
360 "u8" => PrimitiveType::U8,
361 "u16" => PrimitiveType::U16,
362 "u32" => PrimitiveType::U32,
363 "u64" => PrimitiveType::U64,
364 "bool" => PrimitiveType::Bool,
365 "bytes" => PrimitiveType::Bytes,
366 s => unreachable!("unknown primitive: {}", s),
367 }
368}
369
370fn build_array_suffix(pair: Pair<Rule>) -> ArraySuffix {
371 match pair.into_inner().next() {
372 None => ArraySuffix::Dynamic,
373 Some(p) => {
374 let inner = p.into_inner().next().unwrap();
375 match inner.as_rule() {
376 Rule::bounded_size => {
377 let n = inner
378 .into_inner()
379 .next()
380 .unwrap()
381 .as_str()
382 .parse::<u64>()
383 .unwrap();
384 ArraySuffix::Bounded(n)
385 }
386 Rule::pos_int => ArraySuffix::Fixed(inner.as_str().parse::<u64>().unwrap()),
387 r => unreachable!("unexpected array_size rule: {:?}", r),
388 }
389 }
390 }
391}
392
393fn build_literal(pair: Pair<Rule>) -> Literal {
394 let inner = pair.into_inner().next().unwrap();
395 match inner.as_rule() {
396 Rule::float_lit => Literal::Float(inner.as_str().parse::<f64>().unwrap()),
397 Rule::hex_lit => {
398 let s = inner.as_str();
399 let digits = &s[2..]; Literal::Hex(u64::from_str_radix(digits, 16).unwrap())
401 }
402 Rule::int_lit => Literal::Int(inner.as_str().parse::<i64>().unwrap()),
403 Rule::bool_lit => Literal::Bool(inner.as_str() == "true"),
404 Rule::string_lit => {
405 let s = inner.as_str();
406 Literal::Str(unescape(&s[1..s.len() - 1]))
407 }
408 Rule::ident_lit => Literal::Ident(build_scoped_ident(inner.into_inner().next().unwrap())),
409 r => unreachable!("unexpected literal rule: {:?}", r),
410 }
411}
412
413fn build_scoped_ident(pair: Pair<Rule>) -> ScopedIdent {
414 pair.into_inner().map(|p| p.as_str().to_string()).collect()
415}
416
417fn unescape(s: &str) -> String {
418 let mut out = String::with_capacity(s.len());
419 let mut chars = s.chars();
420 while let Some(c) = chars.next() {
421 if c == '\\' {
422 match chars.next() {
423 Some('n') => out.push('\n'),
424 Some('t') => out.push('\t'),
425 Some('r') => out.push('\r'),
426 Some('\\') => out.push('\\'),
427 Some('"') => out.push('"'),
428 Some(c) => {
429 out.push('\\');
430 out.push(c);
431 }
432 None => out.push('\\'),
433 }
434 } else {
435 out.push(c);
436 }
437 }
438 out
439}
440
441#[cfg(test)]
444mod tests {
445 use super::*;
446
447 fn p(input: &str) -> SynFile {
448 parse(input).expect("parse failed")
449 }
450
451 #[test]
454 fn namespace_simple() {
455 let f = p("namespace geometry");
456 assert_eq!(
457 f.items[0],
458 Item::Namespace(NamespaceDecl {
459 name: vec!["geometry".into()]
460 })
461 );
462 }
463
464 #[test]
465 fn namespace_qualified() {
466 let f = p("namespace nav::msgs");
467 assert_eq!(
468 f.items[0],
469 Item::Namespace(NamespaceDecl {
470 name: vec!["nav".into(), "msgs".into()]
471 })
472 );
473 }
474
475 #[test]
478 fn import_path() {
479 let f = p(r#"import "geometry.syn""#);
480 assert_eq!(
481 f.items[0],
482 Item::Import(ImportDecl {
483 path: "geometry.syn".into()
484 })
485 );
486 }
487
488 #[test]
491 fn const_float() {
492 let f = p("const PI: f64 = 3.14");
493 assert_eq!(
494 f.items[0],
495 Item::Const(ConstDecl {
496 name: "PI".into(),
497 ty: TypeExpr {
498 base: BaseType::Primitive(PrimitiveType::F64),
499 array: None
500 },
501 value: Literal::Float(3.14),
502 doc: vec![],
503 attrs: vec![],
504 })
505 );
506 }
507
508 #[test]
509 fn const_int() {
510 let f = p("const MAX: u32 = 256");
511 assert_eq!(
512 f.items[0],
513 Item::Const(ConstDecl {
514 name: "MAX".into(),
515 ty: TypeExpr {
516 base: BaseType::Primitive(PrimitiveType::U32),
517 array: None
518 },
519 value: Literal::Int(256),
520 doc: vec![],
521 attrs: vec![],
522 })
523 );
524 }
525
526 #[test]
527 fn const_string() {
528 let f = p(r#"const FRAME: string = "world""#);
529 assert_eq!(
530 f.items[0],
531 Item::Const(ConstDecl {
532 name: "FRAME".into(),
533 ty: TypeExpr {
534 base: BaseType::String,
535 array: None
536 },
537 value: Literal::Str("world".into()),
538 doc: vec![],
539 attrs: vec![],
540 })
541 );
542 }
543
544 #[test]
547 fn enum_with_values() {
548 let f = p("enum DriveMode { Idle = 0 Forward = 1 Error = 2 }");
549 let Item::Enum(e) = &f.items[0] else { panic!() };
550 assert_eq!(e.name, "DriveMode");
551 assert_eq!(e.repr, None);
552 assert_eq!(
553 e.variants[0],
554 EnumVariant {
555 name: "Idle".into(),
556 value: Some(0),
557 doc: vec![]
558 }
559 );
560 assert_eq!(
561 e.variants[1],
562 EnumVariant {
563 name: "Forward".into(),
564 value: Some(1),
565 doc: vec![]
566 }
567 );
568 assert_eq!(
569 e.variants[2],
570 EnumVariant {
571 name: "Error".into(),
572 value: Some(2),
573 doc: vec![]
574 }
575 );
576 assert!(e.attrs.is_empty());
577 }
578
579 #[test]
580 fn enum_without_values() {
581 let f = p("enum Dir { North South East West }");
582 let Item::Enum(e) = &f.items[0] else { panic!() };
583 assert_eq!(e.repr, None);
584 assert!(e.variants.iter().all(|v| v.value.is_none()));
585 assert_eq!(e.variants.len(), 4);
586 }
587
588 #[test]
589 fn enum_with_repr() {
590 let f = p("enum u8 CameraMode { Idle = 0 Streaming = 1 }");
591 let Item::Enum(e) = &f.items[0] else { panic!() };
592 assert_eq!(e.name, "CameraMode");
593 assert_eq!(e.repr, Some(PrimitiveType::U8));
594 assert_eq!(e.variants.len(), 2);
595 }
596
597 #[test]
600 fn struct_basic() {
601 let f = p("struct Point { x: f64 = 0.0 y: f64 = 0.0 z: f64 = 0.0 }");
602 let Item::Struct(s) = &f.items[0] else {
603 panic!()
604 };
605 assert_eq!(s.name, "Point");
606 assert_eq!(s.fields.len(), 3);
607 assert_eq!(s.fields[0].name, "x");
608 assert_eq!(s.fields[0].ty.base, BaseType::Primitive(PrimitiveType::F64));
609 assert_eq!(s.fields[0].default, Some(Literal::Float(0.0)));
610 assert!(!s.fields[0].optional);
611 }
612
613 #[test]
614 fn struct_qualified_type() {
615 let f = p("struct Pose { position: geometry::Point orientation: geometry::Quaternion }");
616 let Item::Struct(s) = &f.items[0] else {
617 panic!()
618 };
619 assert_eq!(
620 s.fields[0].ty.base,
621 BaseType::Ref(vec!["geometry".into(), "Point".into()])
622 );
623 }
624
625 #[test]
628 fn message_optional_field() {
629 let f = p("message Foo { required: i32 optional?: string }");
630 let Item::Message(m) = &f.items[0] else {
631 panic!()
632 };
633 assert_eq!(m.kind, PacketKind::Message);
634 assert!(!m.fields[0].optional);
635 assert!(m.fields[1].optional);
636 assert_eq!(m.fields[1].ty.base, BaseType::String);
637 }
638
639 #[test]
640 fn command_packet_kind() {
641 let f = p("@mid(0x1880)\ncommand SetMode { mode: u8 }");
642 let Item::Command(m) = &f.items[0] else {
643 panic!()
644 };
645 assert_eq!(m.kind, PacketKind::Command);
646 assert_eq!(m.name, "SetMode");
647 assert_eq!(m.fields[0].name, "mode");
648 }
649
650 #[test]
651 fn telemetry_packet_kind() {
652 let f = p("@mid(0x0801)\ntelemetry NavState { x: f64 }");
653 let Item::Telemetry(m) = &f.items[0] else {
654 panic!()
655 };
656 assert_eq!(m.kind, PacketKind::Telemetry);
657 assert_eq!(m.name, "NavState");
658 assert_eq!(m.fields[0].name, "x");
659 }
660
661 #[test]
662 fn table_is_plain_data_item() {
663 let f = p("table NavConfig { max_speed: f64 enabled: bool }");
664 let Item::Table(t) = &f.items[0] else {
665 panic!()
666 };
667 assert_eq!(t.name, "NavConfig");
668 assert_eq!(t.fields.len(), 2);
669 }
670
671 #[test]
672 fn message_array_fields() {
673 let f = p("message D { dynamic: u8[] fixed: f64[3] bounded: u8[<=256] }");
674 let Item::Message(m) = &f.items[0] else {
675 panic!()
676 };
677 assert_eq!(m.fields[0].ty.array, Some(ArraySuffix::Dynamic));
678 assert_eq!(m.fields[1].ty.array, Some(ArraySuffix::Fixed(3)));
679 assert_eq!(m.fields[2].ty.array, Some(ArraySuffix::Bounded(256)));
680 }
681
682 #[test]
683 fn message_enum_default() {
684 let f = p("message S { mode: DriveMode = DriveMode::Idle }");
685 let Item::Message(m) = &f.items[0] else {
686 panic!()
687 };
688 assert_eq!(
689 m.fields[0].default,
690 Some(Literal::Ident(vec!["DriveMode".into(), "Idle".into()]))
691 );
692 }
693
694 #[test]
695 fn message_string_bounded() {
696 let f = p(r#"message S { label: string[<=64] = "robot" }"#);
697 let Item::Message(m) = &f.items[0] else {
698 panic!()
699 };
700 assert_eq!(m.fields[0].ty.base, BaseType::String);
701 assert_eq!(m.fields[0].ty.array, Some(ArraySuffix::Bounded(64)));
702 assert_eq!(m.fields[0].default, Some(Literal::Str("robot".into())));
703 }
704
705 #[test]
708 fn hex_literal_const() {
709 let f = p("const MID: u16 = 0x0801");
710 let Item::Const(c) = &f.items[0] else {
711 panic!()
712 };
713 assert_eq!(c.value, Literal::Hex(0x0801));
714 }
715
716 #[test]
717 fn hex_literal_uppercase() {
718 let f = p("const MID: u16 = 0X1F80");
719 let Item::Const(c) = &f.items[0] else {
720 panic!()
721 };
722 assert_eq!(c.value, Literal::Hex(0x1F80));
723 }
724
725 #[test]
728 fn attribute_hex_on_message() {
729 let f = p("@mid(0x0801)\nmessage NavTlm { x: f64 }");
730 let Item::Message(m) = &f.items[0] else {
731 panic!()
732 };
733 assert_eq!(m.attrs.len(), 1);
734 assert_eq!(m.attrs[0].name, "mid");
735 assert_eq!(m.attrs[0].value, Literal::Hex(0x0801));
736 }
737
738 #[test]
739 fn attribute_ident_ref() {
740 let f = p("@mid(nav_app::NAV_TLM_MID)\nmessage NavTlm { x: f64 }");
741 let Item::Message(m) = &f.items[0] else {
742 panic!()
743 };
744 assert_eq!(
745 m.attrs[0].value,
746 Literal::Ident(vec!["nav_app".into(), "NAV_TLM_MID".into()])
747 );
748 }
749
750 #[test]
751 fn no_attrs_is_empty() {
752 let f = p("message Foo { x: i32 }");
753 let Item::Message(m) = &f.items[0] else {
754 panic!()
755 };
756 assert!(m.attrs.is_empty());
757 }
758
759 #[test]
762 fn string_escape_sequences() {
763 let f = p(r#"const S: string = "hello\nworld""#);
764 let Item::Const(c) = &f.items[0] else {
765 panic!()
766 };
767 assert_eq!(c.value, Literal::Str("hello\nworld".into()));
768 }
769
770 #[test]
771 fn string_escape_quote() {
772 let f = p(r#"const S: string = "say \"hi\"""#);
773 let Item::Const(c) = &f.items[0] else {
774 panic!()
775 };
776 assert_eq!(c.value, Literal::Str("say \"hi\"".into()));
777 }
778
779 #[test]
782 fn full_robot_state() {
783 let src = r#"
784 namespace robot
785 import "geometry.syn"
786
787 enum DriveMode {
788 Idle = 0
789 Forward = 1
790 Error = 2
791 }
792
793 const MAX_SPEED: f64 = 2.5
794
795 message RobotState {
796 mode: DriveMode = DriveMode::Idle
797 position: geometry::Point
798 battery: f32 = 100.0
799 label: string[<=64] = "robot"
800 sensor_data: u8[]
801 error_code?: i32
802 }
803 "#;
804
805 let f = parse(src).unwrap();
806 assert_eq!(f.items.len(), 5);
807
808 let Item::Namespace(ns) = &f.items[0] else {
809 panic!()
810 };
811 assert_eq!(ns.name, vec!["robot"]);
812
813 let Item::Enum(e) = &f.items[2] else { panic!() };
814 assert_eq!(e.variants.len(), 3);
815
816 let Item::Message(m) = &f.items[4] else {
817 panic!()
818 };
819 assert_eq!(m.name, "RobotState");
820 assert_eq!(m.fields.len(), 6);
821
822 assert!(m.fields[5].optional);
824 assert_eq!(m.fields[5].name, "error_code");
825 }
826}