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