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 assert!(!ast.productions.is_empty(), "BNF file should contain productions");
128
129 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 assert!(!ast.productions.is_empty(), "BNF file should contain productions");
151
152 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 let error_msg = format!("{:?}", e);
166
167 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 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 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}