Skip to main content

synapse_parser/
lib.rs

1pub mod ast;
2
3pub mod synapse {
4    use pest_derive::Parser;
5
6    #[derive(Parser)]
7    #[grammar = "synapse.pest"]
8    pub struct SynapseParser;
9}
10
11pub use synapse::SynapseParser;
12
13#[cfg(test)]
14mod synapse_tests {
15    use super::synapse::{Rule, SynapseParser};
16    use pest::Parser;
17
18    fn parses(rule: Rule, input: &str) -> bool {
19        SynapseParser::parse(rule, input)
20            .map(|mut p| p.next().map_or(false, |pair| pair.as_span().end() == input.len()))
21            .unwrap_or(false)
22    }
23
24    fn parses_file(input: &str) -> bool {
25        parses(Rule::file, input)
26    }
27
28    // =========================================================
29    // Identifiers and scoped names
30    // =========================================================
31
32    #[test]
33    fn ident_basic() {
34        assert!(parses(Rule::ident, "foo"));
35        assert!(parses(Rule::ident, "MyType"));
36        assert!(parses(Rule::ident, "snake_case"));
37        assert!(parses(Rule::ident, "CamelCase123"));
38    }
39
40    #[test]
41    fn ident_rejects_leading_digit() {
42        assert!(!parses(Rule::ident, "1bad"));
43    }
44
45    #[test]
46    fn scoped_ident_bare() {
47        assert!(parses(Rule::scoped_ident, "Point"));
48    }
49
50    #[test]
51    fn scoped_ident_qualified() {
52        assert!(parses(Rule::scoped_ident, "geometry::Point"));
53        assert!(parses(Rule::scoped_ident, "nav::msgs::Odometry"));
54    }
55
56    // =========================================================
57    // Literals
58    // =========================================================
59
60    #[test]
61    fn int_literals() {
62        assert!(parses(Rule::int_lit, "0"));
63        assert!(parses(Rule::int_lit, "42"));
64        assert!(parses(Rule::int_lit, "-7"));
65    }
66
67    #[test]
68    fn float_literals() {
69        assert!(parses(Rule::float_lit, "3.14"));
70        assert!(parses(Rule::float_lit, "-2.5"));
71        assert!(parses(Rule::float_lit, "1.5e-3"));
72        assert!(parses(Rule::float_lit, "1e10"));
73        assert!(!parses(Rule::float_lit, "42"));   // bare int is not a float
74    }
75
76    #[test]
77    fn bool_literals() {
78        assert!(parses(Rule::bool_lit, "true"));
79        assert!(parses(Rule::bool_lit, "false"));
80        assert!(!parses(Rule::bool_lit, "True"));
81        assert!(!parses(Rule::bool_lit, "TRUE"));
82    }
83
84    #[test]
85    fn string_literals() {
86        assert!(parses(Rule::string_lit, r#""hello""#));
87        assert!(parses(Rule::string_lit, r#""""#));
88        assert!(parses(Rule::string_lit, r#""escaped\"quote""#));
89    }
90
91    #[test]
92    fn ident_literal_for_enum_refs() {
93        assert!(parses(Rule::ident_lit, "Idle"));
94        assert!(parses(Rule::ident_lit, "DriveMode::Idle"));
95        assert!(parses(Rule::ident_lit, "pkg::Status::Active"));
96    }
97
98    // =========================================================
99    // Types
100    // =========================================================
101
102    #[test]
103    fn primitive_types() {
104        for t in &["f32", "f64", "i8", "i16", "i32", "i64",
105                   "u8", "u16", "u32", "u64", "bool", "bytes"] {
106            assert!(parses(Rule::primitive_type, t), "failed: {t}");
107        }
108    }
109
110    #[test]
111    fn string_type() {
112        assert!(parses(Rule::string_type, "string"));
113    }
114
115    #[test]
116    fn type_ref_bare_and_qualified() {
117        assert!(parses(Rule::type_ref, "Point"));
118        assert!(parses(Rule::type_ref, "geometry::Point"));
119    }
120
121    #[test]
122    fn type_expr_scalar() {
123        assert!(parses(Rule::type_expr, "f64"));
124        assert!(parses(Rule::type_expr, "bool"));
125        assert!(parses(Rule::type_expr, "string"));
126        assert!(parses(Rule::type_expr, "Point"));
127        assert!(parses(Rule::type_expr, "geometry::Point"));
128    }
129
130    #[test]
131    fn type_expr_dynamic_array() {
132        assert!(parses(Rule::type_expr, "f64[]"));
133        assert!(parses(Rule::type_expr, "u8[]"));
134        assert!(parses(Rule::type_expr, "string[]"));
135        assert!(parses(Rule::type_expr, "geometry::Point[]"));
136    }
137
138    #[test]
139    fn type_expr_fixed_array() {
140        assert!(parses(Rule::type_expr, "f64[3]"));
141        assert!(parses(Rule::type_expr, "u8[256]"));
142        assert!(parses(Rule::type_expr, "f64[36]"));
143    }
144
145    #[test]
146    fn type_expr_bounded_array() {
147        assert!(parses(Rule::type_expr, "u8[<=256]"));
148        assert!(parses(Rule::type_expr, "string[<=64]"));    // bounded string
149        assert!(parses(Rule::type_expr, "geometry::Point[<=100]"));
150    }
151
152    // =========================================================
153    // Enum
154    // =========================================================
155
156    #[test]
157    fn enum_with_explicit_values() {
158        assert!(parses_file(
159            "enum Status { Idle = 0  Moving = 1  Error = 2 }"
160        ));
161    }
162
163    #[test]
164    fn enum_without_values() {
165        assert!(parses_file(
166            "enum Direction { North South East West }"
167        ));
168    }
169
170    #[test]
171    fn enum_multiline() {
172        assert!(parses_file(
173            "enum DriveMode {\n    Idle    = 0\n    Forward = 1\n    Error   = 2\n}"
174        ));
175    }
176
177    #[test]
178    fn enum_mixed_values() {
179        assert!(parses_file(
180            "enum Mixed { A  B = 5  C  D = 10 }"
181        ));
182    }
183
184    // =========================================================
185    // Struct
186    // =========================================================
187
188    #[test]
189    fn struct_basic() {
190        assert!(parses_file(
191            "struct Point { x: f64  y: f64  z: f64 }"
192        ));
193    }
194
195    #[test]
196    fn struct_with_defaults() {
197        assert!(parses_file(
198            "struct Point { x: f64 = 0.0  y: f64 = 0.0  z: f64 = 0.0 }"
199        ));
200    }
201
202    #[test]
203    fn struct_empty() {
204        assert!(parses_file("struct Empty {}"));
205    }
206
207    #[test]
208    fn struct_multiline() {
209        assert!(parses_file(
210            "struct Pose {\n    position: Point\n    orientation: Quaternion\n}"
211        ));
212    }
213
214    // =========================================================
215    // Message
216    // =========================================================
217
218    #[test]
219    fn message_basic() {
220        assert!(parses_file(
221            "message Ping { seq: u32  stamp: u64 }"
222        ));
223    }
224
225    #[test]
226    fn command_basic() {
227        assert!(parses_file(
228            "@mid(0x1880)\ncommand SetMode { mode: u8 }"
229        ));
230    }
231
232    #[test]
233    fn telemetry_basic() {
234        assert!(parses_file(
235            "@mid(0x0801)\ntelemetry NavState { x: f64  y: f64 }"
236        ));
237    }
238
239    #[test]
240    fn table_basic() {
241        assert!(parses_file(
242            "table NavConfig { max_speed: f64  enabled: bool }"
243        ));
244    }
245
246    #[test]
247    fn message_optional_field() {
248        assert!(parses_file(
249            "message Foo { required: i32  optional?: string }"
250        ));
251    }
252
253    #[test]
254    fn message_with_defaults() {
255        assert!(parses_file(
256            r#"message Config { label: string[<=64] = "default"  retries: u8 = 3  verbose: bool = false }"#
257        ));
258    }
259
260    #[test]
261    fn message_array_fields() {
262        assert!(parses_file(
263            "message Data { raw: u8[]  fixed: f64[3]  bounded: u8[<=256] }"
264        ));
265    }
266
267    #[test]
268    fn message_nested_types() {
269        assert!(parses_file(
270            "message Odom { position: Point  velocity: Twist  path: Point[] }"
271        ));
272    }
273
274    #[test]
275    fn message_qualified_types() {
276        assert!(parses_file(
277            "message Pose { position: geometry::Point  orientation: geometry::Quaternion }"
278        ));
279    }
280
281    #[test]
282    fn message_enum_default() {
283        assert!(parses_file(
284            "message State { mode: DriveMode = DriveMode::Idle  speed: f64 = 0.0 }"
285        ));
286    }
287
288    // =========================================================
289    // Const
290    // =========================================================
291
292    #[test]
293    fn const_float() {
294        assert!(parses_file("const PI: f64 = 3.14159265358979"));
295        assert!(parses_file("const G: f32 = 9.81"));
296    }
297
298    #[test]
299    fn const_int() {
300        assert!(parses_file("const MAX_SIZE: u32 = 100"));
301        assert!(parses_file("const MIN: i32 = -32768"));
302    }
303
304    #[test]
305    fn const_bool() {
306        assert!(parses_file("const DEBUG: bool = true"));
307    }
308
309    #[test]
310    fn const_string() {
311        assert!(parses_file(r#"const FRAME: string = "world""#));
312    }
313
314    // =========================================================
315    // Namespace and import
316    // =========================================================
317
318    #[test]
319    fn namespace_bare() {
320        assert!(parses_file("namespace geometry\nstruct Point { x: f64  y: f64 }"));
321    }
322
323    #[test]
324    fn namespace_qualified() {
325        assert!(parses_file("namespace nav::msgs\nmessage Odom { x: f64 }"));
326    }
327
328    #[test]
329    fn import_decl() {
330        assert!(parses_file(r#"import "geometry.syn""#));
331        assert!(parses_file(r#"import "common/types.syn""#));
332    }
333
334    // =========================================================
335    // Comments
336    // =========================================================
337
338    #[test]
339    fn line_comments() {
340        assert!(parses_file(
341            "# top comment\nstruct S { x: f64 # x coord\n}"
342        ));
343    }
344
345    #[test]
346    fn doc_comments() {
347        assert!(parses_file(
348            "## A struct\nstruct S {\n## x field\nx: f64\n}"
349        ));
350    }
351
352    // =========================================================
353    // Full realistic files
354    // =========================================================
355
356    #[test]
357    fn geometry_file() {
358        assert!(parses_file(
359            "namespace geometry
360
361            struct Point {
362                x: f64 = 0.0
363                y: f64 = 0.0
364                z: f64 = 0.0
365            }
366
367            struct Quaternion {
368                x: f64 = 0.0
369                y: f64 = 0.0
370                z: f64 = 0.0
371                w: f64 = 1.0
372            }
373
374            struct Pose {
375                position:    Point
376                orientation: Quaternion
377            }"
378        ));
379    }
380
381    #[test]
382    fn robot_state_file() {
383        assert!(parses_file(
384            r#"import "geometry.syn"
385
386            enum DriveMode {
387                Idle    = 0
388                Forward = 1
389                Turning = 2
390                Error   = 3
391            }
392
393            const MAX_SPEED: f64 = 2.5
394
395            message RobotState {
396                mode:         DriveMode         = DriveMode::Idle
397                position:     geometry::Point
398                velocity:     geometry::Point
399                battery:      f32               = 100.0
400                label:        string[<=64]       = "robot"
401                sensor_data:  u8[]
402                waypoints:    geometry::Point[]
403                error_code?:  i32
404            }"#
405        ));
406    }
407
408    // =========================================================
409    // Rejection tests
410    // =========================================================
411
412    #[test]
413    fn rejects_field_without_type() {
414        assert!(!parses_file("struct S { x }"));
415    }
416
417    #[test]
418    fn rejects_missing_closing_brace() {
419        assert!(!parses_file("struct S { x: f64"));
420    }
421
422    #[test]
423    fn rejects_unknown_top_level() {
424        assert!(!parses_file("typedef i32 MyInt"));
425    }
426}