regex_bnf/
helpers.rs

1#![allow(clippy::result_unit_err)]
2
3use std::{cell::RefCell, collections::HashMap};
4
5use regex::Regex;
6use thiserror::Error;
7
8#[cfg(feature = "debug-logs")]
9const PARSE_DEBUG: bool = true;
10#[cfg(not(feature = "debug-logs"))]
11const PARSE_DEBUG: bool = false;
12
13#[derive(Debug, Copy, Clone, Hash, PartialEq)]
14pub struct FileLocation {
15    pub line_number: usize,
16    pub position: usize,
17    pub index: usize,
18}
19
20impl FileLocation {
21    pub fn start() -> Self {
22        Self {
23            line_number: 1,
24            position: 0,
25            index: 0,
26        }
27    }
28
29    pub fn advanced_by(mut self, text: &str) -> Self {
30        let lines = text.chars().filter(|c| *c == '\n').count();
31        self.line_number += lines;
32        self.index += text.len();
33
34        if lines > 0 {
35            let last_newline = text.rfind('\n').unwrap();
36            self.position = text.len() - last_newline;
37        } else {
38            self.position += text.len();
39        }
40
41        self
42    }
43}
44
45impl std::fmt::Display for FileLocation {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        write!(f, "line {}:{}", self.line_number, self.position)
48    }
49}
50
51#[derive(Debug, Clone, Hash, PartialEq)]
52pub struct TextSlice<'a> {
53    pub text: &'a str,
54    pub location: FileLocation,
55}
56
57impl<'a> TextSlice<'a> {
58    pub fn new(text: &'a str, location: FileLocation) -> Self {
59        Self { text, location }
60    }
61}
62
63#[derive(Error, Debug, Clone)]
64pub struct ParseError {
65    pub message: &'static str,
66    pub at: FileLocation,
67    pub child_errors: Vec<ParseError>,
68}
69
70impl ParseError {
71    pub fn new(message: &'static str, at: FileLocation) -> Self {
72        Self {
73            message,
74            at,
75            child_errors: Vec::new(),
76        }
77    }
78
79    pub fn with_child(message: &'static str, at: FileLocation, child: ParseError) -> Self {
80        if child.child_errors.len() == 1 {
81            // If the child only has 1 error, just pass it up to avoid deep nesting
82            Self {
83                message,
84                at,
85                child_errors: child.child_errors,
86            }
87        } else {
88            Self {
89                message,
90                at,
91                child_errors: vec![child],
92            }
93        }
94    }
95
96    pub fn with_children(
97        message: &'static str,
98        at: FileLocation,
99        children: Vec<ParseError>,
100    ) -> Self {
101        Self {
102            message,
103            at,
104            child_errors: children,
105        }
106    }
107}
108
109impl std::fmt::Display for ParseError {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        writeln!(f, "{}", ParseErrorFmtDepth(self, 0))?;
112        Ok(())
113    }
114}
115
116struct ParseErrorFmtDepth<'a>(&'a ParseError, usize);
117
118impl std::fmt::Display for ParseErrorFmtDepth<'_> {
119    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120        let err = self.0;
121        let depth = self.1;
122
123        for _ in 0..depth {
124            write!(f, "  ")?;
125        }
126        writeln!(f, "{} at {}", err.message, err.at)?;
127
128        for child in &err.child_errors {
129            write!(f, "{}", ParseErrorFmtDepth(child, depth + 1))?;
130        }
131        Ok(())
132    }
133}
134
135#[derive(Debug, Clone, Copy)]
136pub struct StringParser<'a> {
137    pub text: &'a str,
138    pub pos: FileLocation,
139}
140
141impl<'a> StringParser<'a> {
142    pub fn new(input: &'a str) -> Self {
143        Self {
144            text: input,
145            pos: FileLocation::start(),
146        }
147    }
148
149    pub fn split_at(&self, pos: usize) -> (TextSlice<'a>, Self) {
150        let (left, right) = self.text.split_at(pos);
151        let forked = Self {
152            text: right,
153            pos: self.pos.advanced_by(left),
154        };
155        let left = TextSlice::new(left, self.pos);
156        (left, forked)
157    }
158
159    pub fn is_empty(&self) -> bool {
160        self.text.is_empty()
161    }
162}
163
164thread_local! {
165    pub static REGEXES: RefCell<HashMap<String,Regex>>  = RefCell::new(HashMap::new());
166}
167
168pub fn parse_string_lit<'a>(
169    input: StringParser<'a>,
170    lit: &'static str,
171) -> Result<(TextSlice<'a>, StringParser<'a>), ()> {
172    if input.text.starts_with(lit) {
173        if PARSE_DEBUG {
174            println!("Matched string literal: {:?} at {}", lit, input.pos);
175        }
176        Ok(input.split_at(lit.len()))
177    } else {
178        if PARSE_DEBUG {
179            println!("Failed to match string literal: {:?} at {}", lit, input.pos);
180        }
181        Err(())
182    }
183}
184
185fn get_regex(regex: &str) -> Regex {
186    REGEXES.with(|regexes| {
187        let mut regexes = regexes.borrow_mut();
188        if let Some(regex) = regexes.get(regex) {
189            regex.clone()
190        } else {
191            let new_regex = Regex::new(&format!("^{}", regex)).unwrap();
192            regexes.insert(regex.to_string(), new_regex.clone());
193            new_regex
194        }
195    })
196}
197
198pub fn parse_string_regex<'a>(
199    input: StringParser<'a>,
200    regex_str: &'static str,
201) -> Result<(TextSlice<'a>, StringParser<'a>), ()> {
202    let regex = get_regex(regex_str);
203
204    if let Some(captures) = regex.find_at(input.text, 0) {
205        if PARSE_DEBUG {
206            println!("Matched regex: {:?} at {}", regex_str, input.pos);
207        }
208        let capture = captures;
209        Ok(input.split_at(capture.end()))
210    } else {
211        if PARSE_DEBUG {
212            println!("Failed to match regex: {:?} at {}", regex_str, input.pos);
213        }
214        Err(())
215    }
216}