Skip to main content

sim_lib_pattern/
lua_dialect.rs

1//! Lua text-pattern compiler for the shared VM.
2
3use sim_kernel::{Error, Result};
4
5use crate::{PatternDialect, TextClass, TextOp};
6
7/// Compiler for Lua-style text patterns.
8#[derive(Clone, Copy, Debug, Default)]
9pub struct LuaPatternDialect;
10
11impl PatternDialect for LuaPatternDialect {
12    fn compile(&self, pattern: &str) -> Result<Vec<TextOp>> {
13        LuaCompiler::new(pattern).compile()
14    }
15}
16
17/// Compiles a Lua-style text pattern into shared VM operations.
18///
19/// # Errors
20///
21/// Returns an error when the pattern is malformed.
22pub fn compile_lua_pattern(pattern: &str) -> Result<Vec<TextOp>> {
23    LuaPatternDialect.compile(pattern)
24}
25
26struct LuaCompiler {
27    chars: Vec<char>,
28    index: usize,
29}
30
31impl LuaCompiler {
32    fn new(pattern: &str) -> Self {
33        Self {
34            chars: pattern.chars().collect(),
35            index: 0,
36        }
37    }
38
39    fn compile(mut self) -> Result<Vec<TextOp>> {
40        let mut ops = Vec::new();
41        while let Some(ch) = self.next() {
42            match ch {
43                '^' if ops.is_empty() => ops.push(TextOp::AnchorStart),
44                '^' => self.push_atom(&mut ops, TextOp::Literal('^'))?,
45                '$' if self.is_end() => ops.push(TextOp::AnchorEnd),
46                '$' => self.push_atom(&mut ops, TextOp::Literal('$'))?,
47                '.' => self.push_atom(&mut ops, TextOp::Any)?,
48                '(' => ops.push(TextOp::CaptureStart),
49                ')' => ops.push(TextOp::CaptureEnd),
50                '[' => {
51                    let set = self.parse_set()?;
52                    self.push_atom(&mut ops, TextOp::Class(set))?;
53                }
54                '%' => {
55                    let escaped = self.parse_percent()?;
56                    match escaped {
57                        Escaped::Atom(op) => self.push_atom(&mut ops, op)?,
58                        Escaped::ZeroWidth(op) => ops.push(op),
59                    }
60                }
61                '*' | '+' | '-' | '?' => return Err(malformed("quantifier without atom")),
62                literal => self.push_atom(&mut ops, TextOp::Literal(literal))?,
63            }
64        }
65        Ok(ops)
66    }
67
68    fn push_atom(&mut self, ops: &mut Vec<TextOp>, op: TextOp) -> Result<()> {
69        ops.push(op);
70        if let Some(quantifier) = self.peek().and_then(lua_quantifier) {
71            self.index += 1;
72            ops.push(quantifier);
73        }
74        Ok(())
75    }
76
77    fn parse_percent(&mut self) -> Result<Escaped> {
78        let Some(ch) = self.next() else {
79            return Err(malformed("dangling percent escape"));
80        };
81        Ok(match ch {
82            'a' => Escaped::Atom(TextOp::Class(TextClass::Alpha)),
83            'A' => Escaped::Atom(TextOp::Class(TextClass::Not(Box::new(TextClass::Alpha)))),
84            'd' => Escaped::Atom(TextOp::Class(TextClass::Digit)),
85            'D' => Escaped::Atom(TextOp::Class(TextClass::Not(Box::new(TextClass::Digit)))),
86            'l' => Escaped::Atom(TextOp::Class(TextClass::Lower)),
87            'L' => Escaped::Atom(TextOp::Class(TextClass::Not(Box::new(TextClass::Lower)))),
88            'u' => Escaped::Atom(TextOp::Class(TextClass::Upper)),
89            'U' => Escaped::Atom(TextOp::Class(TextClass::Not(Box::new(TextClass::Upper)))),
90            'w' => Escaped::Atom(TextOp::Class(TextClass::Alnum)),
91            'W' => Escaped::Atom(TextOp::Class(TextClass::Not(Box::new(TextClass::Alnum)))),
92            's' => Escaped::Atom(TextOp::Class(TextClass::Space)),
93            'S' => Escaped::Atom(TextOp::Class(TextClass::Not(Box::new(TextClass::Space)))),
94            'p' => Escaped::Atom(TextOp::Class(TextClass::Punct)),
95            'P' => Escaped::Atom(TextOp::Class(TextClass::Not(Box::new(TextClass::Punct)))),
96            'x' => Escaped::Atom(TextOp::Class(TextClass::Hex)),
97            'X' => Escaped::Atom(TextOp::Class(TextClass::Not(Box::new(TextClass::Hex)))),
98            'z' => Escaped::Atom(TextOp::Class(TextClass::Zero)),
99            'b' => {
100                let open = self
101                    .next()
102                    .ok_or_else(|| malformed("balanced pattern missing open delimiter"))?;
103                let close = self
104                    .next()
105                    .ok_or_else(|| malformed("balanced pattern missing close delimiter"))?;
106                Escaped::Atom(TextOp::Balanced { open, close })
107            }
108            'f' => {
109                if self.next() != Some('[') {
110                    return Err(malformed("frontier pattern requires a character set"));
111                }
112                Escaped::ZeroWidth(TextOp::Frontier(self.parse_set()?))
113            }
114            literal => Escaped::Atom(TextOp::Literal(literal)),
115        })
116    }
117
118    fn parse_set(&mut self) -> Result<TextClass> {
119        let mut negated = false;
120        if self.peek() == Some('^') {
121            self.index += 1;
122            negated = true;
123        }
124        parse_set_body(
125            &self.chars,
126            &mut self.index,
127            negated,
128            "unterminated character set",
129        )
130    }
131
132    fn next(&mut self) -> Option<char> {
133        let ch = self.chars.get(self.index).copied()?;
134        self.index += 1;
135        Some(ch)
136    }
137
138    fn peek(&self) -> Option<char> {
139        self.chars.get(self.index).copied()
140    }
141
142    fn is_end(&self) -> bool {
143        self.index >= self.chars.len()
144    }
145}
146
147enum Escaped {
148    Atom(TextOp),
149    ZeroWidth(TextOp),
150}
151
152pub(crate) fn parse_set_body(
153    chars: &[char],
154    index: &mut usize,
155    negated: bool,
156    unterminated: &str,
157) -> Result<TextClass> {
158    let mut literals = Vec::new();
159    let mut ranges = Vec::new();
160    let mut classes = Vec::new();
161    let mut first = true;
162    while let Some(ch) = chars.get(*index).copied() {
163        *index += 1;
164        if ch == ']' && !first {
165            return Ok(TextClass::Set {
166                chars: literals,
167                ranges,
168                classes,
169                negated,
170            });
171        }
172        first = false;
173        let item = if ch == '%' {
174            let escaped = chars
175                .get(*index)
176                .copied()
177                .ok_or_else(|| malformed("dangling set escape"))?;
178            *index += 1;
179            set_escape(escaped)
180        } else {
181            SetItem::Literal(ch)
182        };
183        if let SetItem::Literal(start) = item {
184            if chars.get(*index).copied() == Some('-')
185                && chars.get(*index + 1).is_some_and(|end| *end != ']')
186            {
187                *index += 1;
188                let end = chars
189                    .get(*index)
190                    .copied()
191                    .ok_or_else(|| malformed(unterminated))?;
192                *index += 1;
193                ranges.push((start, end));
194            } else {
195                literals.push(start);
196            }
197        } else if let SetItem::Class(class) = item {
198            classes.push(class);
199        }
200    }
201    Err(malformed(unterminated))
202}
203
204enum SetItem {
205    Literal(char),
206    Class(TextClass),
207}
208
209fn set_escape(ch: char) -> SetItem {
210    match ch {
211        'a' => SetItem::Class(TextClass::Alpha),
212        'd' => SetItem::Class(TextClass::Digit),
213        'l' => SetItem::Class(TextClass::Lower),
214        'u' => SetItem::Class(TextClass::Upper),
215        'w' => SetItem::Class(TextClass::Alnum),
216        's' => SetItem::Class(TextClass::Space),
217        'p' => SetItem::Class(TextClass::Punct),
218        'x' => SetItem::Class(TextClass::Hex),
219        'z' => SetItem::Class(TextClass::Zero),
220        literal => SetItem::Literal(literal),
221    }
222}
223
224fn lua_quantifier(ch: char) -> Option<TextOp> {
225    match ch {
226        '*' => Some(TextOp::Repeat {
227            min: 0,
228            max: None,
229            greedy: true,
230        }),
231        '+' => Some(TextOp::Repeat {
232            min: 1,
233            max: None,
234            greedy: true,
235        }),
236        '-' => Some(TextOp::Repeat {
237            min: 0,
238            max: None,
239            greedy: false,
240        }),
241        '?' => Some(TextOp::Repeat {
242            min: 0,
243            max: Some(1),
244            greedy: true,
245        }),
246        _ => None,
247    }
248}
249
250fn malformed(message: &str) -> Error {
251    Error::Eval(format!("malformed Lua pattern: {message}"))
252}