Skip to main content

cadk_bnf/
lib.rs

1use lalrpop_util::lalrpop_mod;
2pub mod ast;
3pub mod lexer;
4lalrpop_mod!(grammar);
5
6#[cfg(test)]
7mod tests {
8    use super::*;
9
10    #[test]
11    fn test_parse_choice() -> anyhow::Result<()> {
12        let source = "12 a = 'b' | 'c' .";
13        let lexer = lexer::Lexer::new(&source);
14        let parser = grammar::GrammarParser::new();
15        let ast = parser.parse(lexer)?;
16        let expected = ast::Grammar {
17            productions: vec![ast::Production {
18                index: Some(12),
19                lhs: ast::Ident("a".into()),
20                rhs: ast::Expr::Choice(vec![
21                    ast::Expr::Atom(ast::Atom::Terminal("b".into())),
22                    ast::Expr::Atom(ast::Atom::Terminal("c".into())),
23                ]),
24            }],
25        };
26        assert_eq!(ast, expected);
27        Ok(())
28    }
29
30    #[test]
31    fn test_parse_sequence() -> anyhow::Result<()> {
32        let source = "25 assignment = variable '=' expression .";
33        let lexer = lexer::Lexer::new(&source);
34        let parser = grammar::GrammarParser::new();
35        let ast = parser.parse(lexer)?;
36        let expected = ast::Grammar {
37            productions: vec![ast::Production {
38                index: Some(25),
39                lhs: ast::Ident("assignment".into()),
40                rhs: ast::Expr::Sequence(vec![
41                    ast::Expr::Atom(ast::Atom::NonTerminal(ast::Ident("variable".into()))),
42                    ast::Expr::Atom(ast::Atom::Terminal("=".into())),
43                    ast::Expr::Atom(ast::Atom::NonTerminal(ast::Ident("expression".into()))),
44                ]),
45            }],
46        };
47        assert_eq!(ast, expected);
48        Ok(())
49    }
50
51    #[test]
52    fn test_parse_optional() -> anyhow::Result<()> {
53        let source = "30 function_call = function_name [ argument_list ] .";
54        let lexer = lexer::Lexer::new(&source);
55        let parser = grammar::GrammarParser::new();
56        let ast = parser.parse(lexer)?;
57        let expected = ast::Grammar {
58            productions: vec![ast::Production {
59                index: Some(30),
60                lhs: ast::Ident("function_call".into()),
61                rhs: ast::Expr::Sequence(vec![
62                    ast::Expr::Atom(ast::Atom::NonTerminal(ast::Ident("function_name".into()))),
63                    ast::Expr::Optional(Box::new(ast::Expr::Atom(ast::Atom::NonTerminal(
64                        ast::Ident("argument_list".into()),
65                    )))),
66                ]),
67            }],
68        };
69        assert_eq!(ast, expected);
70        Ok(())
71    }
72
73    #[test]
74    fn test_parse_repeat() -> anyhow::Result<()> {
75        let source = "125 digits = digit { digit } .";
76        let lexer = lexer::Lexer::new(&source);
77        let parser = grammar::GrammarParser::new();
78        let ast = parser.parse(lexer)?;
79        let expected = ast::Grammar {
80            productions: vec![ast::Production {
81                index: Some(125),
82                lhs: ast::Ident("digits".into()),
83                rhs: ast::Expr::Sequence(vec![
84                    ast::Expr::Atom(ast::Atom::NonTerminal(ast::Ident("digit".into()))),
85                    ast::Expr::Repeat(Box::new(ast::Expr::Atom(ast::Atom::NonTerminal(
86                        ast::Ident("digit".into()),
87                    )))),
88                ]),
89            }],
90        };
91        assert_eq!(ast, expected);
92        Ok(())
93    }
94
95    #[test]
96    fn test_parse_group() -> anyhow::Result<()> {
97        let source = "40 expression = term ( '+' | '-' ) term .";
98        let lexer = lexer::Lexer::new(&source);
99        let parser = grammar::GrammarParser::new();
100        let ast = parser.parse(lexer)?;
101        let expected = ast::Grammar {
102            productions: vec![ast::Production {
103                index: Some(40),
104                lhs: ast::Ident("expression".into()),
105                rhs: ast::Expr::Sequence(vec![
106                    ast::Expr::Atom(ast::Atom::NonTerminal(ast::Ident("term".into()))),
107                    ast::Expr::Group(Box::new(ast::Expr::Choice(vec![
108                        ast::Expr::Atom(ast::Atom::Terminal("+".into())),
109                        ast::Expr::Atom(ast::Atom::Terminal("-".into())),
110                    ]))),
111                    ast::Expr::Atom(ast::Atom::NonTerminal(ast::Ident("term".into()))),
112                ]),
113            }],
114        };
115        assert_eq!(ast, expected);
116        Ok(())
117    }
118
119    #[test]
120    fn test_parse_iso_10303_11_2004_bnf() -> anyhow::Result<()> {
121        let source = std::fs::read_to_string("../cadk-express/data/iso-10303-11-2004.bnf")?;
122        let lexer = lexer::Lexer::new(&source);
123        let parser = grammar::GrammarParser::new();
124        let ast = parser.parse(lexer)?;
125        
126        // Basic sanity checks
127        assert!(!ast.productions.is_empty(), "BNF file should contain productions");
128        
129        // Check that we have some expected keywords
130        let has_abs = ast.productions.iter().any(|p| p.lhs.0 == "ABS");
131        let has_entity = ast.productions.iter().any(|p| p.lhs.0 == "ENTITY");
132        let has_schema = ast.productions.iter().any(|p| p.lhs.0 == "SCHEMA");
133        
134        assert!(has_abs, "Should have ABS production");
135        assert!(has_entity, "Should have ENTITY production");
136        assert!(has_schema, "Should have SCHEMA production");
137        
138        Ok(())
139    }
140
141    #[test]
142    fn test_parse_iso_10303_21_2002_bnf() -> anyhow::Result<()> {
143        let source = std::fs::read_to_string("../cadk-step/data/iso-10303-21-2002.bnf")?;
144        let lexer = lexer::Lexer::new(&source);
145        let parser = grammar::GrammarParser::new();
146        
147        match parser.parse(lexer) {
148            Ok(ast) => {
149                // Basic sanity checks
150                assert!(!ast.productions.is_empty(), "BNF file should contain productions");
151                
152                // Check that we have some expected STEP file format productions
153                let has_exchange_file = ast.productions.iter().any(|p| p.lhs.0 == "exchange_file");
154                let has_header_section = ast.productions.iter().any(|p| p.lhs.0 == "header_section");
155                let has_data_section = ast.productions.iter().any(|p| p.lhs.0 == "data_section");
156                
157                assert!(has_exchange_file, "Should have exchange_file production");
158                assert!(has_header_section, "Should have header_section production");
159                assert!(has_data_section, "Should have data_section production");
160                
161                Ok(())
162            }
163            Err(e) => {
164                // Extract location information from the error
165                let error_msg = format!("{:?}", e);
166                
167                // Try to find the location in the source where the error occurred
168                if let Some(location) = extract_error_location(&error_msg) {
169                    let lines: Vec<&str> = source.lines().collect();
170                    let line_start = source[..location].matches('\n').count();
171                    let context_start = line_start.saturating_sub(2);
172                    let context_end = (line_start + 3).min(lines.len());
173                    
174                    eprintln!("Parse error at position {}", location);
175                    eprintln!("Context around error:");
176                    for (i, line) in lines[context_start..context_end].iter().enumerate() {
177                        let line_num = context_start + i + 1;
178                        eprintln!("{:4}: {}", line_num, line);
179                    }
180                    
181                    // Show the specific character/token that failed
182                    if location < source.len() {
183                        let error_char = &source[location..location.saturating_add(20)];
184                        eprintln!("Failed at: '{}'", error_char);
185                    }
186                }
187                
188                Err(anyhow::anyhow!("Failed to parse ISO 10303-21-2002 BNF: {}", error_msg))
189            }
190        }
191    }
192    
193    fn extract_error_location(error_msg: &str) -> Option<usize> {
194        // Try to extract location from error message
195        // This is a simple heuristic that might need adjustment based on actual error format
196        if let Some(pos) = error_msg.find("location: ") {
197            let location_str = &error_msg[pos + 10..];
198            if let Some(end) = location_str.find(|c: char| !c.is_numeric()) {
199                location_str[..end].parse().ok()
200            } else {
201                location_str.parse().ok()
202            }
203        } else if let Some(pos) = error_msg.find(" at ") {
204            let location_str = &error_msg[pos + 4..];
205            if let Some(end) = location_str.find(|c: char| !c.is_numeric()) {
206                location_str[..end].parse().ok()
207            } else {
208                location_str.parse().ok()
209            }
210        } else {
211            None
212        }
213    }
214}