sim_lib_pattern/
glob_dialect.rs1use sim_kernel::{Error, Result};
4
5use crate::lua_dialect::parse_set_body;
6use crate::{
7 Anchor, EnginePolicy, IrNode, PatternDialect, PatternIr, RepeatBounds, ScalarDomain, TextClass,
8 TextOp,
9};
10use std::collections::BTreeMap;
11
12#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
14pub enum GlobExtension {
15 Class(TextClass),
17}
18
19#[derive(Clone, Copy, Debug, Default)]
21pub struct GlobPatternDialect;
22
23impl PatternDialect for GlobPatternDialect {
24 fn compile(&self, pattern: &str) -> Result<Vec<TextOp>> {
25 let ir = self.compile_ir(pattern)?;
26 Ok(project_compatibility_program(ir.root()))
27 }
28}
29
30impl GlobPatternDialect {
31 pub fn compile_ir(self, pattern: &str) -> Result<PatternIr<ScalarDomain, GlobExtension>> {
33 let chars = pattern.chars().collect::<Vec<_>>();
34 let mut index = 0;
35 let mut nodes = vec![IrNode::Anchor(Anchor::SubjectStart)];
36 while let Some(ch) = chars.get(index).copied() {
37 index += 1;
38 match ch {
39 '*' => {
40 nodes.push(IrNode::Repeat {
41 node: Box::new(IrNode::Any),
42 bounds: RepeatBounds::new(0, None)
43 .expect("glob star has valid static bounds"),
44 greedy: true,
45 });
46 }
47 '?' => nodes.push(IrNode::Any),
48 '[' => {
49 let negated = matches!(chars.get(index), Some('!') | Some('^'));
50 if negated {
51 index += 1;
52 }
53 nodes.push(IrNode::Extension(GlobExtension::Class(parse_set_body(
54 &chars,
55 &mut index,
56 negated,
57 "unterminated glob character set",
58 )?)));
59 }
60 '\\' => {
61 let literal = chars
62 .get(index)
63 .copied()
64 .ok_or_else(|| malformed("dangling escape"))?;
65 index += 1;
66 nodes.push(IrNode::Symbol(literal));
67 }
68 literal => nodes.push(IrNode::Symbol(literal)),
69 }
70 }
71 nodes.push(IrNode::Anchor(Anchor::SubjectEnd));
72 let root = IrNode::Concat(nodes);
73 let extensions = collect_extensions(&root);
74 PatternIr::new(root, BTreeMap::new(), &EnginePolicy::new(extensions))
75 .map_err(|error| malformed(&error.to_string()))
76 }
77}
78
79pub fn compile_glob_pattern(pattern: &str) -> Result<Vec<TextOp>> {
85 GlobPatternDialect.compile(pattern)
86}
87
88fn malformed(message: &str) -> Error {
89 Error::Eval(format!("malformed glob pattern: {message}"))
90}
91
92fn collect_extensions(node: &IrNode<char, GlobExtension>) -> Vec<GlobExtension> {
93 let mut extensions = Vec::new();
94 visit(node, &mut |extension| extensions.push(extension.clone()));
95 extensions
96}
97
98fn project_compatibility_program(node: &IrNode<char, GlobExtension>) -> Vec<TextOp> {
99 let mut ops = Vec::new();
100 project(node, &mut ops);
101 ops
102}
103
104fn project(node: &IrNode<char, GlobExtension>, ops: &mut Vec<TextOp>) {
105 match node {
106 IrNode::Symbol(ch) => ops.push(TextOp::Literal(*ch)),
107 IrNode::Any => ops.push(TextOp::Any),
108 IrNode::Concat(nodes) | IrNode::Alternation(nodes) => {
109 for node in nodes {
110 project(node, ops);
111 }
112 }
113 IrNode::Repeat {
114 node,
115 bounds,
116 greedy,
117 } => {
118 project(node, ops);
119 ops.push(TextOp::Repeat {
120 min: bounds.min(),
121 max: bounds.max(),
122 greedy: *greedy,
123 });
124 }
125 IrNode::Group(node) | IrNode::Capture { node, .. } => project(node, ops),
126 IrNode::Anchor(Anchor::SubjectStart) => ops.push(TextOp::AnchorStart),
127 IrNode::Anchor(Anchor::SubjectEnd) => ops.push(TextOp::AnchorEnd),
128 IrNode::Extension(GlobExtension::Class(class)) => ops.push(TextOp::Class(class.clone())),
129 IrNode::Assertion(_) => unreachable!("glob lowering does not create assertions"),
130 }
131}
132
133fn visit(node: &IrNode<char, GlobExtension>, f: &mut impl FnMut(&GlobExtension)) {
134 match node {
135 IrNode::Concat(nodes) | IrNode::Alternation(nodes) => {
136 for node in nodes {
137 visit(node, f);
138 }
139 }
140 IrNode::Repeat { node, .. } | IrNode::Group(node) | IrNode::Capture { node, .. } => {
141 visit(node, f);
142 }
143 IrNode::Extension(extension) => f(extension),
144 IrNode::Symbol(_) | IrNode::Any | IrNode::Anchor(_) | IrNode::Assertion(_) => {}
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn frozen_globs_project_from_validated_ir() {
154 for pattern in ["*.rs", "src/?ain.rs", "file[!0-9].txt", r"literal\*"] {
155 let ir = GlobPatternDialect.compile_ir(pattern).unwrap();
156 assert_eq!(
157 project_compatibility_program(ir.root()),
158 compile_glob_pattern(pattern).unwrap(),
159 "{pattern:?}"
160 );
161 }
162 }
163}