1use sim_kernel::{Error, Result};
4
5use crate::{
6 Anchor, CaptureId, EnginePolicy, IrNode, PatternDialect, PatternIr, RepeatBounds, ScalarDomain,
7 TextClass, TextOp,
8};
9use std::collections::BTreeMap;
10
11#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
13pub enum LuaExtension {
14 Class(TextClass),
16 Balanced {
18 open: char,
20 close: char,
22 },
23 Frontier(TextClass),
25}
26
27#[derive(Clone, Copy, Debug, Default)]
29pub struct LuaPatternDialect;
30
31impl PatternDialect for LuaPatternDialect {
32 fn compile(&self, pattern: &str) -> Result<Vec<TextOp>> {
33 let ir = self.compile_ir(pattern)?;
34 Ok(project_compatibility_program(ir.root()))
35 }
36}
37
38impl LuaPatternDialect {
39 pub fn compile_ir(self, pattern: &str) -> Result<PatternIr<ScalarDomain, LuaExtension>> {
41 LuaCompiler::new(pattern).compile()
42 }
43}
44
45pub fn compile_lua_pattern(pattern: &str) -> Result<Vec<TextOp>> {
51 LuaPatternDialect.compile(pattern)
52}
53
54struct LuaCompiler {
55 chars: Vec<char>,
56 index: usize,
57}
58
59impl LuaCompiler {
60 fn new(pattern: &str) -> Self {
61 Self {
62 chars: pattern.chars().collect(),
63 index: 0,
64 }
65 }
66
67 fn compile(mut self) -> Result<PatternIr<ScalarDomain, LuaExtension>> {
68 let mut frames = vec![Vec::new()];
69 let mut next_capture = 0u32;
70 while let Some(ch) = self.next() {
71 match ch {
72 '^' if frames.len() == 1 && frames[0].is_empty() => {
73 frames[0].push(IrNode::Anchor(Anchor::SubjectStart));
74 }
75 '^' => self.push_atom(&mut frames, IrNode::Symbol('^'))?,
76 '$' if self.is_end() => frames
77 .last_mut()
78 .expect("root frame exists")
79 .push(IrNode::Anchor(Anchor::SubjectEnd)),
80 '$' => self.push_atom(&mut frames, IrNode::Symbol('$'))?,
81 '.' => self.push_atom(&mut frames, IrNode::Any)?,
82 '(' => frames.push(Vec::new()),
83 ')' => {
84 if frames.len() == 1 {
85 return Err(malformed("capture close without open"));
86 }
87 let body = IrNode::Concat(frames.pop().expect("capture frame exists"));
88 frames
89 .last_mut()
90 .expect("parent frame exists")
91 .push(IrNode::Capture {
92 id: CaptureId(next_capture),
93 node: Box::new(body),
94 });
95 next_capture += 1;
96 }
97 '[' => {
98 let set = self.parse_set()?;
99 self.push_atom(&mut frames, IrNode::Extension(LuaExtension::Class(set)))?;
100 }
101 '%' => {
102 let escaped = self.parse_percent()?;
103 match escaped {
104 Escaped::Atom(node) => self.push_atom(&mut frames, node)?,
105 Escaped::ZeroWidth(node) => {
106 frames.last_mut().expect("root frame exists").push(node)
107 }
108 }
109 }
110 '*' | '+' | '-' | '?' => return Err(malformed("quantifier without atom")),
111 literal => self.push_atom(&mut frames, IrNode::Symbol(literal))?,
112 }
113 }
114 if frames.len() != 1 {
115 return Err(malformed("unterminated capture"));
116 }
117 let root = IrNode::Concat(frames.pop().expect("root frame exists"));
118 let extensions = collect_extensions(&root);
119 PatternIr::new(root, BTreeMap::new(), &EnginePolicy::new(extensions))
120 .map_err(|error| malformed(&error.to_string()))
121 }
122
123 fn push_atom(
124 &mut self,
125 frames: &mut [Vec<IrNode<char, LuaExtension>>],
126 mut node: IrNode<char, LuaExtension>,
127 ) -> Result<()> {
128 if let Some((min, max, greedy)) = self.peek().and_then(lua_quantifier) {
129 self.index += 1;
130 node = IrNode::Repeat {
131 node: Box::new(node),
132 bounds: RepeatBounds::new(min, max)
133 .expect("Lua quantifiers have valid static bounds"),
134 greedy,
135 };
136 }
137 frames.last_mut().expect("root frame exists").push(node);
138 Ok(())
139 }
140
141 fn parse_percent(&mut self) -> Result<Escaped> {
142 let Some(ch) = self.next() else {
143 return Err(malformed("dangling percent escape"));
144 };
145 Ok(match ch {
146 'a' => class_atom(TextClass::Alpha),
147 'A' => class_atom(TextClass::Not(Box::new(TextClass::Alpha))),
148 'd' => class_atom(TextClass::Digit),
149 'D' => class_atom(TextClass::Not(Box::new(TextClass::Digit))),
150 'l' => class_atom(TextClass::Lower),
151 'L' => class_atom(TextClass::Not(Box::new(TextClass::Lower))),
152 'u' => class_atom(TextClass::Upper),
153 'U' => class_atom(TextClass::Not(Box::new(TextClass::Upper))),
154 'w' => class_atom(TextClass::Alnum),
155 'W' => class_atom(TextClass::Not(Box::new(TextClass::Alnum))),
156 's' => class_atom(TextClass::Space),
157 'S' => class_atom(TextClass::Not(Box::new(TextClass::Space))),
158 'p' => class_atom(TextClass::Punct),
159 'P' => class_atom(TextClass::Not(Box::new(TextClass::Punct))),
160 'x' => class_atom(TextClass::Hex),
161 'X' => class_atom(TextClass::Not(Box::new(TextClass::Hex))),
162 'z' => class_atom(TextClass::Zero),
163 'b' => {
164 let open = self
165 .next()
166 .ok_or_else(|| malformed("balanced pattern missing open delimiter"))?;
167 let close = self
168 .next()
169 .ok_or_else(|| malformed("balanced pattern missing close delimiter"))?;
170 Escaped::Atom(IrNode::Extension(LuaExtension::Balanced { open, close }))
171 }
172 'f' => {
173 if self.next() != Some('[') {
174 return Err(malformed("frontier pattern requires a character set"));
175 }
176 Escaped::ZeroWidth(IrNode::Extension(LuaExtension::Frontier(self.parse_set()?)))
177 }
178 literal => Escaped::Atom(IrNode::Symbol(literal)),
179 })
180 }
181
182 fn parse_set(&mut self) -> Result<TextClass> {
183 let mut negated = false;
184 if self.peek() == Some('^') {
185 self.index += 1;
186 negated = true;
187 }
188 parse_set_body(
189 &self.chars,
190 &mut self.index,
191 negated,
192 "unterminated character set",
193 )
194 }
195
196 fn next(&mut self) -> Option<char> {
197 let ch = self.chars.get(self.index).copied()?;
198 self.index += 1;
199 Some(ch)
200 }
201
202 fn peek(&self) -> Option<char> {
203 self.chars.get(self.index).copied()
204 }
205
206 fn is_end(&self) -> bool {
207 self.index >= self.chars.len()
208 }
209}
210
211enum Escaped {
212 Atom(IrNode<char, LuaExtension>),
213 ZeroWidth(IrNode<char, LuaExtension>),
214}
215
216fn class_atom(class: TextClass) -> Escaped {
217 Escaped::Atom(IrNode::Extension(LuaExtension::Class(class)))
218}
219
220pub(crate) fn parse_set_body(
221 chars: &[char],
222 index: &mut usize,
223 negated: bool,
224 unterminated: &str,
225) -> Result<TextClass> {
226 let mut literals = Vec::new();
227 let mut ranges = Vec::new();
228 let mut classes = Vec::new();
229 let mut first = true;
230 while let Some(ch) = chars.get(*index).copied() {
231 *index += 1;
232 if ch == ']' && !first {
233 return Ok(TextClass::Set {
234 chars: literals,
235 ranges,
236 classes,
237 negated,
238 });
239 }
240 first = false;
241 let item = if ch == '%' {
242 let escaped = chars
243 .get(*index)
244 .copied()
245 .ok_or_else(|| malformed("dangling set escape"))?;
246 *index += 1;
247 set_escape(escaped)
248 } else {
249 SetItem::Literal(ch)
250 };
251 if let SetItem::Literal(start) = item {
252 if chars.get(*index).copied() == Some('-')
253 && chars.get(*index + 1).is_some_and(|end| *end != ']')
254 {
255 *index += 1;
256 let end = chars
257 .get(*index)
258 .copied()
259 .ok_or_else(|| malformed(unterminated))?;
260 *index += 1;
261 ranges.push((start, end));
262 } else {
263 literals.push(start);
264 }
265 } else if let SetItem::Class(class) = item {
266 classes.push(class);
267 }
268 }
269 Err(malformed(unterminated))
270}
271
272enum SetItem {
273 Literal(char),
274 Class(TextClass),
275}
276
277fn set_escape(ch: char) -> SetItem {
278 match ch {
279 'a' => SetItem::Class(TextClass::Alpha),
280 'd' => SetItem::Class(TextClass::Digit),
281 'l' => SetItem::Class(TextClass::Lower),
282 'u' => SetItem::Class(TextClass::Upper),
283 'w' => SetItem::Class(TextClass::Alnum),
284 's' => SetItem::Class(TextClass::Space),
285 'p' => SetItem::Class(TextClass::Punct),
286 'x' => SetItem::Class(TextClass::Hex),
287 'z' => SetItem::Class(TextClass::Zero),
288 literal => SetItem::Literal(literal),
289 }
290}
291
292fn lua_quantifier(ch: char) -> Option<(usize, Option<usize>, bool)> {
293 match ch {
294 '*' => Some((0, None, true)),
295 '+' => Some((1, None, true)),
296 '-' => Some((0, None, false)),
297 '?' => Some((0, Some(1), true)),
298 _ => None,
299 }
300}
301
302fn collect_extensions(node: &IrNode<char, LuaExtension>) -> Vec<LuaExtension> {
303 let mut extensions = Vec::new();
304 visit(node, &mut |extension| extensions.push(extension.clone()));
305 extensions
306}
307
308fn project_compatibility_program(node: &IrNode<char, LuaExtension>) -> Vec<TextOp> {
309 let mut ops = Vec::new();
310 project(node, &mut ops);
311 ops
312}
313
314fn project(node: &IrNode<char, LuaExtension>, ops: &mut Vec<TextOp>) {
315 match node {
316 IrNode::Symbol(ch) => ops.push(TextOp::Literal(*ch)),
317 IrNode::Any => ops.push(TextOp::Any),
318 IrNode::Concat(nodes) | IrNode::Alternation(nodes) => {
319 for node in nodes {
320 project(node, ops);
321 }
322 }
323 IrNode::Repeat {
324 node,
325 bounds,
326 greedy,
327 } => {
328 project(node, ops);
329 ops.push(TextOp::Repeat {
330 min: bounds.min(),
331 max: bounds.max(),
332 greedy: *greedy,
333 });
334 }
335 IrNode::Group(node) => project(node, ops),
336 IrNode::Capture { node, .. } => {
337 ops.push(TextOp::CaptureStart);
338 project(node, ops);
339 ops.push(TextOp::CaptureEnd);
340 }
341 IrNode::Anchor(Anchor::SubjectStart) => ops.push(TextOp::AnchorStart),
342 IrNode::Anchor(Anchor::SubjectEnd) => ops.push(TextOp::AnchorEnd),
343 IrNode::Extension(LuaExtension::Class(class)) => ops.push(TextOp::Class(class.clone())),
344 IrNode::Extension(LuaExtension::Balanced { open, close }) => {
345 ops.push(TextOp::Balanced {
346 open: *open,
347 close: *close,
348 });
349 }
350 IrNode::Extension(LuaExtension::Frontier(class)) => {
351 ops.push(TextOp::Frontier(class.clone()));
352 }
353 IrNode::Assertion(_) => unreachable!("Lua lowering does not create assertions"),
354 }
355}
356
357fn visit(node: &IrNode<char, LuaExtension>, f: &mut impl FnMut(&LuaExtension)) {
358 match node {
359 IrNode::Concat(nodes) | IrNode::Alternation(nodes) => {
360 for node in nodes {
361 visit(node, f);
362 }
363 }
364 IrNode::Repeat { node, .. } | IrNode::Group(node) | IrNode::Capture { node, .. } => {
365 visit(node, f);
366 }
367 IrNode::Extension(extension) => f(extension),
368 IrNode::Symbol(_) | IrNode::Any | IrNode::Anchor(_) | IrNode::Assertion(_) => {}
369 }
370}
371
372fn malformed(message: &str) -> Error {
373 Error::Eval(format!("malformed Lua pattern: {message}"))
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379
380 #[test]
381 fn balanced_match_is_one_named_adapter_node() {
382 let ir = LuaPatternDialect.compile_ir("%b()").unwrap();
383 let mut extensions = Vec::new();
384 visit(ir.root(), &mut |extension| {
385 extensions.push(extension.clone())
386 });
387 assert_eq!(
388 extensions,
389 vec![LuaExtension::Balanced {
390 open: '(',
391 close: ')'
392 }]
393 );
394 }
395
396 #[test]
397 fn capture_ids_and_compatibility_boundaries_are_frozen() {
398 let ir = LuaPatternDialect.compile_ir("(%a+)%s+(%d+)").unwrap();
399 let mut ids = Vec::new();
400 fn collect(node: &IrNode<char, LuaExtension>, ids: &mut Vec<CaptureId>) {
401 match node {
402 IrNode::Concat(nodes) | IrNode::Alternation(nodes) => {
403 for node in nodes {
404 collect(node, ids);
405 }
406 }
407 IrNode::Repeat { node, .. } | IrNode::Group(node) => collect(node, ids),
408 IrNode::Capture { id, node } => {
409 ids.push(*id);
410 collect(node, ids);
411 }
412 IrNode::Symbol(_)
413 | IrNode::Any
414 | IrNode::Anchor(_)
415 | IrNode::Assertion(_)
416 | IrNode::Extension(_) => {}
417 }
418 }
419 collect(ir.root(), &mut ids);
420 assert_eq!(ids, vec![CaptureId(0), CaptureId(1)]);
421 assert_eq!(
422 project_compatibility_program(ir.root())
423 .iter()
424 .filter(|op| matches!(op, TextOp::CaptureStart | TextOp::CaptureEnd))
425 .count(),
426 4
427 );
428 }
429}