Skip to main content

atoxide_parser/
lib.rs

1//! Parser for the Ato hardware description language.
2//!
3//! This crate provides a complete parser for the Ato DSL, producing a typed AST
4//! with error recovery using the chumsky parser combinator library.
5//!
6//! # Example
7//!
8//! ```
9//! use atoxide_parser::parse;
10//!
11//! let source = r#"
12//! module MyModule:
13//!     pin p1
14//!     signal sig
15//!     p1 ~ sig
16//! "#;
17//!
18//! match parse(source) {
19//!     Ok(file) => {
20//!         println!("Parsed {} statements", file.statements.len());
21//!     }
22//!     Err(errors) => {
23//!         for e in errors {
24//!             eprintln!("Parse error: {}", e);
25//!         }
26//!     }
27//! }
28//! ```
29//!
30//! # Error Recovery
31//!
32//! The parser supports error recovery, allowing it to continue parsing after
33//! encountering errors. Use `parse_with_recovery` to get both the AST and errors:
34//!
35//! ```
36//! use atoxide_parser::parse_with_recovery;
37//!
38//! let source = "module M:\n    pass\n";
39//! let (ast, errors) = parse_with_recovery(source);
40//!
41//! if let Some(file) = ast {
42//!     println!("Parsed {} statements", file.statements.len());
43//! }
44//! for error in &errors {
45//!     eprintln!("Error: {}", error);
46//! }
47//! ```
48
49pub mod ast;
50mod chumsky;
51pub mod error;
52
53pub use ast::*;
54pub use chumsky::{ParseError as ChumskyParseError, format_errors};
55pub use error::{ParseError, ParseResult};
56
57/// Parse Ato source code into an AST.
58///
59/// This is the main entry point for parsing Ato source code.
60/// Returns an error if parsing fails completely.
61///
62/// For error recovery (getting partial AST even with errors), use `parse_with_recovery`.
63pub fn parse(source: &str) -> Result<File, Vec<ChumskyParseError>> {
64    let (ast, errors) = chumsky::parse(source);
65
66    if let Some(file) = ast {
67        if errors.is_empty() {
68            Ok(file)
69        } else {
70            // We got an AST but also had errors - return the errors
71            // (the AST might be incomplete or have placeholder values)
72            Err(errors)
73        }
74    } else {
75        // No AST at all - definitely an error
76        if errors.is_empty() {
77            Err(vec![ChumskyParseError {
78                span: atoxide_lexer::Span::new(0, 0, 1, 1),
79                message: "failed to parse".to_string(),
80                expected: vec![],
81                found: None,
82                help: None,
83            }])
84        } else {
85            Err(errors)
86        }
87    }
88}
89
90/// Parse Ato source code with error recovery.
91///
92/// Returns both the AST (if any could be parsed) and all errors encountered.
93/// This is useful when you want to continue processing even with errors,
94/// or when you want to collect all errors for display.
95pub fn parse_with_recovery(source: &str) -> (Option<File>, Vec<ChumskyParseError>) {
96    chumsky::parse(source)
97}
98
99/// Parse Ato source code and format any errors using ariadne.
100///
101/// Returns the AST if parsing succeeded (possibly with recovered errors),
102/// and a formatted error string if there were any errors.
103pub fn parse_with_formatted_errors(source: &str, filename: &str) -> (Option<File>, Option<String>) {
104    chumsky::parse_with_errors(source, filename)
105}
106
107/// Parse Ato source code, returning both the AST and a source code wrapper for error display.
108///
109/// This is useful when you want to display nice error messages with miette.
110pub fn parse_with_source(
111    source: &str,
112) -> (
113    Result<File, Vec<ChumskyParseError>>,
114    miette::NamedSource<String>,
115) {
116    let named_source = miette::NamedSource::new("<input>", source.to_string());
117    let result = parse(source);
118    (result, named_source)
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn test_parse_empty() {
127        let source = "";
128        let file = parse(source).unwrap();
129        assert!(file.statements.is_empty());
130    }
131
132    #[test]
133    fn test_parse_pragma() {
134        let source = "#pragma experiment(\"FOR_LOOP\")\n";
135        let file = parse(source).unwrap();
136        assert_eq!(file.statements.len(), 1);
137        assert!(matches!(file.statements[0], Statement::Pragma(_)));
138    }
139
140    #[test]
141    fn test_parse_import() {
142        let source = "import ElectricPower\n";
143        let file = parse(source).unwrap();
144        assert_eq!(file.statements.len(), 1);
145        assert!(matches!(file.statements[0], Statement::Import(_)));
146    }
147
148    #[test]
149    fn test_parse_from_import() {
150        let source = "from \"path/to/file.ato\" import Module\n";
151        let file = parse(source).unwrap();
152        assert_eq!(file.statements.len(), 1);
153        if let Statement::Import(import) = &file.statements[0] {
154            assert!(import.from_path.is_some());
155        } else {
156            panic!("Expected import statement");
157        }
158    }
159
160    #[test]
161    fn test_parse_simple_module() {
162        let source = "module M:\n    pass\n";
163        let file = parse(source).unwrap();
164        assert_eq!(file.statements.len(), 1);
165        if let Statement::BlockDef(block) = &file.statements[0] {
166            assert_eq!(block.kind, BlockKind::Module);
167            assert_eq!(block.name.name, "M");
168            assert_eq!(block.body.len(), 1);
169        } else {
170            panic!("Expected block definition");
171        }
172    }
173
174    #[test]
175    fn test_parse_module_with_super() {
176        let source = "module Child from Parent:\n    pass\n";
177        let file = parse(source).unwrap();
178        if let Statement::BlockDef(block) = &file.statements[0] {
179            assert!(block.super_type.is_some());
180            assert_eq!(block.super_type.as_ref().unwrap().parts[0].name, "Parent");
181        } else {
182            panic!("Expected block definition");
183        }
184    }
185
186    #[test]
187    fn test_parse_component() {
188        let source = "component C:\n    pin p1\n";
189        let file = parse(source).unwrap();
190        if let Statement::BlockDef(block) = &file.statements[0] {
191            assert_eq!(block.kind, BlockKind::Component);
192        } else {
193            panic!("Expected component");
194        }
195    }
196
197    #[test]
198    fn test_parse_interface() {
199        let source = "interface I:\n    pass\n";
200        let file = parse(source).unwrap();
201        if let Statement::BlockDef(block) = &file.statements[0] {
202            assert_eq!(block.kind, BlockKind::Interface);
203        } else {
204            panic!("Expected interface");
205        }
206    }
207
208    #[test]
209    fn test_parse_pin() {
210        let source = "module M:\n    pin p1\n    pin 1\n    pin \"GND\"\n";
211        let file = parse(source).unwrap();
212        if let Statement::BlockDef(block) = &file.statements[0] {
213            assert_eq!(block.body.len(), 3);
214        } else {
215            panic!("Expected block");
216        }
217    }
218
219    #[test]
220    fn test_parse_signal() {
221        let source = "module M:\n    signal sig\n";
222        let file = parse(source).unwrap();
223        if let Statement::BlockDef(block) = &file.statements[0] {
224            assert!(matches!(block.body[0], Statement::SignalDef(_)));
225        } else {
226            panic!("Expected block");
227        }
228    }
229
230    #[test]
231    fn test_parse_assignment() {
232        let source = "module M:\n    x = 5\n";
233        let file = parse(source).unwrap();
234        if let Statement::BlockDef(block) = &file.statements[0] {
235            assert!(matches!(block.body[0], Statement::Assignment(_)));
236        } else {
237            panic!("Expected block");
238        }
239    }
240
241    #[test]
242    fn test_parse_new_expression() {
243        let source = "module M:\n    x = new SomeType\n";
244        let file = parse(source).unwrap();
245        if let Statement::BlockDef(block) = &file.statements[0] {
246            if let Statement::Assignment(assign) = &block.body[0] {
247                assert!(matches!(assign.value, Assignable::New(_)));
248            } else {
249                panic!("Expected assignment");
250            }
251        } else {
252            panic!("Expected block");
253        }
254    }
255
256    #[test]
257    fn test_parse_new_with_count() {
258        let source = "module M:\n    x = new SomeType[10]\n";
259        let file = parse(source).unwrap();
260        if let Statement::BlockDef(block) = &file.statements[0] {
261            if let Statement::Assignment(assign) = &block.body[0] {
262                if let Assignable::New(new_expr) = &assign.value {
263                    assert!(new_expr.count.is_some());
264                } else {
265                    panic!("Expected new expression");
266                }
267            } else {
268                panic!("Expected assignment");
269            }
270        } else {
271            panic!("Expected block");
272        }
273    }
274
275    #[test]
276    fn test_parse_new_with_template() {
277        let source = "module M:\n    x = new SomeType<param=1>\n";
278        let file = parse(source).unwrap();
279        if let Statement::BlockDef(block) = &file.statements[0] {
280            if let Statement::Assignment(assign) = &block.body[0] {
281                if let Assignable::New(new_expr) = &assign.value {
282                    assert!(new_expr.template.is_some());
283                } else {
284                    panic!("Expected new expression");
285                }
286            } else {
287                panic!("Expected assignment");
288            }
289        } else {
290            panic!("Expected block");
291        }
292    }
293
294    #[test]
295    fn test_parse_connection() {
296        let source = "module M:\n    a ~ b\n";
297        let file = parse(source).unwrap();
298        if let Statement::BlockDef(block) = &file.statements[0] {
299            assert!(matches!(block.body[0], Statement::Connection(_)));
300        } else {
301            panic!("Expected block");
302        }
303    }
304
305    #[test]
306    fn test_parse_directed_connection() {
307        let source = "module M:\n    a ~> b ~> c\n";
308        let file = parse(source).unwrap();
309        if let Statement::BlockDef(block) = &file.statements[0] {
310            if let Statement::DirectedConnection(conn) = &block.body[0] {
311                assert_eq!(conn.direction, ConnectionDirection::Forward);
312                assert_eq!(conn.elements.len(), 3);
313            } else {
314                panic!("Expected directed connection");
315            }
316        } else {
317            panic!("Expected block");
318        }
319    }
320
321    #[test]
322    fn test_parse_backward_connection() {
323        let source = "module M:\n    a <~ b <~ c\n";
324        let file = parse(source).unwrap();
325        if let Statement::BlockDef(block) = &file.statements[0] {
326            if let Statement::DirectedConnection(conn) = &block.body[0] {
327                assert_eq!(conn.direction, ConnectionDirection::Backward);
328            } else {
329                panic!("Expected directed connection");
330            }
331        } else {
332            panic!("Expected block");
333        }
334    }
335
336    #[test]
337    fn test_parse_retype() {
338        let source = "module M:\n    x -> NewType\n";
339        let file = parse(source).unwrap();
340        if let Statement::BlockDef(block) = &file.statements[0] {
341            assert!(matches!(block.body[0], Statement::Retype(_)));
342        } else {
343            panic!("Expected block");
344        }
345    }
346
347    #[test]
348    fn test_parse_assert() {
349        let source = "module M:\n    assert x > 5\n";
350        let file = parse(source).unwrap();
351        if let Statement::BlockDef(block) = &file.statements[0] {
352            assert!(matches!(block.body[0], Statement::Assert(_)));
353        } else {
354            panic!("Expected block");
355        }
356    }
357
358    #[test]
359    fn test_parse_assert_within() {
360        let source = "module M:\n    assert x within 1 to 10\n";
361        let file = parse(source).unwrap();
362        if let Statement::BlockDef(block) = &file.statements[0] {
363            if let Statement::Assert(assert_stmt) = &block.body[0] {
364                assert_eq!(
365                    assert_stmt.comparison.operations[0].kind,
366                    CompareOpKind::Within
367                );
368            } else {
369                panic!("Expected assert");
370            }
371        } else {
372            panic!("Expected block");
373        }
374    }
375
376    #[test]
377    fn test_parse_trait() {
378        let source = "module M:\n    trait some_trait\n";
379        let file = parse(source).unwrap();
380        if let Statement::BlockDef(block) = &file.statements[0] {
381            assert!(matches!(block.body[0], Statement::Trait(_)));
382        } else {
383            panic!("Expected block");
384        }
385    }
386
387    #[test]
388    fn test_parse_trait_with_constructor() {
389        let source = "module M:\n    trait some_trait::constructor\n";
390        let file = parse(source).unwrap();
391        if let Statement::BlockDef(block) = &file.statements[0] {
392            if let Statement::Trait(trait_stmt) = &block.body[0] {
393                assert!(trait_stmt.constructor.is_some());
394            } else {
395                panic!("Expected trait");
396            }
397        } else {
398            panic!("Expected block");
399        }
400    }
401
402    #[test]
403    fn test_parse_trait_with_template() {
404        let source = "module M:\n    trait some_trait<arg=1>\n";
405        let file = parse(source).unwrap();
406        if let Statement::BlockDef(block) = &file.statements[0] {
407            if let Statement::Trait(trait_stmt) = &block.body[0] {
408                assert!(trait_stmt.template.is_some());
409            } else {
410                panic!("Expected trait");
411            }
412        } else {
413            panic!("Expected block");
414        }
415    }
416
417    #[test]
418    fn test_parse_for_loop() {
419        let source = "module M:\n    for item in container:\n        pass\n";
420        let file = parse(source).unwrap();
421        if let Statement::BlockDef(block) = &file.statements[0] {
422            if let Statement::For(for_stmt) = &block.body[0] {
423                assert_eq!(for_stmt.variable.name, "item");
424            } else {
425                panic!("Expected for loop");
426            }
427        } else {
428            panic!("Expected block");
429        }
430    }
431
432    #[test]
433    fn test_parse_declaration() {
434        let source = "module M:\n    field: ohm\n";
435        let file = parse(source).unwrap();
436        if let Statement::BlockDef(block) = &file.statements[0] {
437            assert!(matches!(block.body[0], Statement::Declaration(_)));
438        } else {
439            panic!("Expected block");
440        }
441    }
442
443    #[test]
444    fn test_parse_declaration_with_assignment() {
445        let source = "module M:\n    field: ohm = 100\n";
446        let file = parse(source).unwrap();
447        if let Statement::BlockDef(block) = &file.statements[0] {
448            if let Statement::Assignment(assign) = &block.body[0] {
449                assert!(matches!(assign.target, AssignTarget::Declaration(_)));
450            } else {
451                panic!("Expected assignment");
452            }
453        } else {
454            panic!("Expected block");
455        }
456    }
457
458    #[test]
459    fn test_parse_physical_quantity() {
460        let source = "module M:\n    x = 10kohm\n";
461        let file = parse(source).unwrap();
462        if let Statement::BlockDef(block) = &file.statements[0] {
463            if let Statement::Assignment(assign) = &block.body[0] {
464                assert!(matches!(assign.value, Assignable::Physical(_)));
465            } else {
466                panic!("Expected assignment");
467            }
468        } else {
469            panic!("Expected block");
470        }
471    }
472
473    #[test]
474    fn test_parse_range() {
475        let source = "module M:\n    x = 1 to 10\n";
476        let file = parse(source).unwrap();
477        if let Statement::BlockDef(block) = &file.statements[0] {
478            if let Statement::Assignment(assign) = &block.body[0] {
479                if let Assignable::Physical(PhysicalLiteral::Range(_)) = &assign.value {
480                    // OK
481                } else {
482                    panic!("Expected range");
483                }
484            } else {
485                panic!("Expected assignment");
486            }
487        } else {
488            panic!("Expected block");
489        }
490    }
491
492    #[test]
493    fn test_parse_bilateral() {
494        let source = "module M:\n    x = 10 +/- 5%\n";
495        let file = parse(source).unwrap();
496        if let Statement::BlockDef(block) = &file.statements[0] {
497            if let Statement::Assignment(assign) = &block.body[0] {
498                if let Assignable::Physical(PhysicalLiteral::Bilateral(_)) = &assign.value {
499                    // OK
500                } else {
501                    panic!("Expected bilateral");
502                }
503            } else {
504                panic!("Expected assignment");
505            }
506        } else {
507            panic!("Expected block");
508        }
509    }
510
511    #[test]
512    fn test_parse_arithmetic() {
513        let source = "module M:\n    x = a + b * c\n";
514        let file = parse(source).unwrap();
515        if let Statement::BlockDef(block) = &file.statements[0] {
516            if let Statement::Assignment(assign) = &block.body[0] {
517                assert!(matches!(assign.value, Assignable::Arithmetic(_)));
518            } else {
519                panic!("Expected assignment");
520            }
521        } else {
522            panic!("Expected block");
523        }
524    }
525
526    #[test]
527    fn test_parse_string_stmt() {
528        let source = "module M:\n    \"docstring\"\n";
529        let file = parse(source).unwrap();
530        if let Statement::BlockDef(block) = &file.statements[0] {
531            assert!(matches!(block.body[0], Statement::StringStmt(_)));
532        } else {
533            panic!("Expected block");
534        }
535    }
536
537    #[test]
538    fn test_parse_semicolon_separated() {
539        let source = "module M:\n    pass; pass; pass\n";
540        let file = parse(source).unwrap();
541        if let Statement::BlockDef(block) = &file.statements[0] {
542            assert_eq!(block.body.len(), 3);
543        } else {
544            panic!("Expected block");
545        }
546    }
547
548    #[test]
549    fn test_parse_single_line_block() {
550        let source = "module M: pass\n";
551        let file = parse(source).unwrap();
552        if let Statement::BlockDef(block) = &file.statements[0] {
553            assert_eq!(block.body.len(), 1);
554        } else {
555            panic!("Expected block");
556        }
557    }
558
559    #[test]
560    fn test_parse_nested_modules() {
561        let source = "module A:\n    module B:\n        pass\n";
562        let file = parse(source).unwrap();
563        if let Statement::BlockDef(outer) = &file.statements[0] {
564            if let Statement::BlockDef(inner) = &outer.body[0] {
565                assert_eq!(inner.name.name, "B");
566            } else {
567                panic!("Expected inner block");
568            }
569        } else {
570            panic!("Expected outer block");
571        }
572    }
573
574    #[test]
575    fn test_parse_field_reference() {
576        let source = "module M:\n    a.b.c = 1\n";
577        let file = parse(source).unwrap();
578        if let Statement::BlockDef(block) = &file.statements[0] {
579            if let Statement::Assignment(assign) = &block.body[0] {
580                if let AssignTarget::FieldRef(field_ref) = &assign.target {
581                    assert_eq!(field_ref.parts.len(), 3);
582                } else {
583                    panic!("Expected field ref");
584                }
585            } else {
586                panic!("Expected assignment");
587            }
588        } else {
589            panic!("Expected block");
590        }
591    }
592
593    #[test]
594    fn test_parse_field_with_index() {
595        let source = "module M:\n    a[0].b = 1\n";
596        let file = parse(source).unwrap();
597        if let Statement::BlockDef(block) = &file.statements[0] {
598            if let Statement::Assignment(assign) = &block.body[0] {
599                if let AssignTarget::FieldRef(field_ref) = &assign.target {
600                    assert!(field_ref.parts[0].index.is_some());
601                } else {
602                    panic!("Expected field ref");
603                }
604            } else {
605                panic!("Expected assignment");
606            }
607        } else {
608            panic!("Expected block");
609        }
610    }
611
612    #[test]
613    fn test_parse_assert_multiply() {
614        let source = "module M:\n    assert x >= y * 1.5\n";
615        let file = parse(source).unwrap();
616        if let Statement::BlockDef(block) = &file.statements[0] {
617            assert!(matches!(block.body[0], Statement::Assert(_)));
618        } else {
619            panic!("Expected block");
620        }
621    }
622
623    #[test]
624    fn test_parse_assign_multiply() {
625        // Basic: x = a * b
626        let source = "module M:\n    x = 300 * y\n";
627        let file = parse(source).unwrap();
628        if let Statement::BlockDef(block) = &file.statements[0] {
629            if let Statement::Assignment(assign) = &block.body[0] {
630                assert!(matches!(assign.value, Assignable::Arithmetic(_)));
631            } else {
632                panic!("Expected assignment");
633            }
634        } else {
635            panic!("Expected block");
636        }
637    }
638
639    #[test]
640    fn test_parse_assign_chained_multiply() {
641        // k_iset = 300 * k_i * k_r (from BQ25185)
642        let source = "module M:\n    k_iset = 300 * k_i * k_r\n";
643        let file = parse(source).unwrap();
644        if let Statement::BlockDef(block) = &file.statements[0] {
645            if let Statement::Assignment(assign) = &block.body[0] {
646                assert!(matches!(assign.value, Assignable::Arithmetic(_)));
647            } else {
648                panic!("Expected assignment");
649            }
650        } else {
651            panic!("Expected block");
652        }
653    }
654
655    #[test]
656    fn test_parse_assert_multiply_rhs() {
657        // assert x >= y * 1.5 (from BQ25185)
658        let source = "module M:\n    assert x >= y * 1.5\n";
659        let file = parse(source).unwrap();
660        if let Statement::BlockDef(block) = &file.statements[0] {
661            assert!(matches!(block.body[0], Statement::Assert(_)));
662        } else {
663            panic!("Expected block");
664        }
665    }
666
667    #[test]
668    fn test_physical_literal_still_assignable() {
669        // Plain physical literals should still be Assignable::Physical
670        let source = "module M:\n    x = 10kohm +/- 10%\n";
671        let file = parse(source).unwrap();
672        if let Statement::BlockDef(block) = &file.statements[0] {
673            if let Statement::Assignment(assign) = &block.body[0] {
674                assert!(matches!(
675                    assign.value,
676                    Assignable::Physical(PhysicalLiteral::Bilateral(_))
677                ));
678            } else {
679                panic!("Expected assignment");
680            }
681        } else {
682            panic!("Expected block");
683        }
684    }
685
686    #[test]
687    fn test_parse_from_py_import() {
688        // BQ25185 has: from "ResistanceMapper.py" import ResistanceMapper
689        // This should parse (even though .py files won't be loaded)
690        let source = r#"from "ResistanceMapper.py" import ResistanceMapper
691module M:
692    pass
693"#;
694        let file = parse(source).unwrap();
695        assert!(!file.statements.is_empty());
696    }
697
698    #[test]
699    fn test_error_recovery() {
700        // This has an error (missing colon after module name)
701        let source = "module Bad\n    pass\n";
702        let (ast, errors) = parse_with_recovery(source);
703
704        // Should have errors
705        assert!(!errors.is_empty(), "Should have parse errors");
706        // With error recovery, we may or may not get an AST
707        let _ = ast;
708    }
709}