1use sim_kernel::{Error, Result};
4
5use crate::{PatternDialect, TextClass, TextOp};
6
7#[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
17pub 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}