Skip to main content

sml_pattern/
lib.rs

1//! SML 模式语言(Loom 融入 SML 的产物):L1 层的无回溯匹配引擎。
2//!
3//! # 为什么不是正则
4//!
5//! 规则就是**普通 SML 数据**(片段),不引入新 token,因此能被 SML 自身的
6//! include / 契约 / 中文关键字复用。匹配侧用 Thompson NFA 并行推进,
7//! **结构上不存在回溯**,故免疫 ReDoS(见 [`nfa`] 模块说明)。
8//!
9//! # 关键字与语言无关
10//!
11//! 书写形式(中文 / 英文 / 任意语言)由 [`KeywordTable`] trait 决定,
12//! 编译逻辑只认 [`Concept`]。默认 [`BILINGUAL`](中英等价)。
13//!
14//! # 规则写法(中英等价,可混用)
15//!
16//! ```sml
17//! @日期ISO {
18//!     序列: [
19//!         { 名: 年, 类: 数字, 次: 4 }
20//!         { 字面: "-" }
21//!         { 名: 月, 类: 数字, 次: 2 }
22//!     ]
23//! }
24//! ```
25
26mod i18n;
27mod nfa;
28pub mod regex;
29
30use std::collections::BTreeMap;
31
32pub use i18n::{BILINGUAL, CHINESE, Concept, ENGLISH, KeywordTable, StaticTable};
33use i18n::Concept::*;
34
35use sml_value::Value;
36
37/// 字符类。默认 Unicode 语义:`Alpha` 匹配汉字,`Digit` 匹配全角数字。
38/// (regex 的 `\d` 在 JS 是 ASCII、Python 是 Unicode,这类跨语言不一致是 bug 温床,
39/// 这里明确选定 Unicode 一侧。)
40#[derive(Debug, Clone, PartialEq)]
41pub enum Class {
42    Digit,
43    Alpha,
44    Space,
45    Word,
46    Any,
47    /// 单个字符(由字面量编译而来)
48    One(char),
49    /// 字符范围 `[a-z]`
50    Range(char, char),
51    /// 字符类的并集 `[abc]` / `[a-z_]`
52    Set(Vec<Class>),
53    /// 补集 `[^...]` / `\D` `\W` `\S`
54    Not(Box<Class>),
55}
56
57impl Class {
58    pub fn matches(&self, ch: char) -> bool {
59        match self {
60            Class::Digit => ch.is_numeric(),
61            Class::Alpha => ch.is_alphabetic(),
62            Class::Space => ch.is_whitespace(),
63            Class::Word => ch.is_alphanumeric() || ch == '_',
64            Class::Any => true,
65            Class::One(c) => ch == *c,
66            Class::Range(a, b) => (*a..=*b).contains(&ch),
67            Class::Set(cs) => cs.iter().any(|c| c.matches(ch)),
68            Class::Not(c) => !c.matches(ch),
69        }
70    }
71}
72
73/// 模式 IR(L1:无递归、无守卫)
74// PartialEq 供 sml-contract 的 TypeSpec 派生比较(契约类型需要可比较)
75#[derive(Debug, Clone, PartialEq)]
76pub enum Pat {
77    Class(Class),
78    Lit(String),
79    Seq(Vec<Pat>),
80    Alt(Vec<Pat>),
81    /// 量词。`max: None` 表示无上界(`+` / `*`)
82    Repeat {
83        pat: Box<Pat>,
84        min: usize,
85        max: Option<usize>,
86    },
87    /// 命名捕获:与规则定义同形,只是多一个名字
88    Named {
89        name: String,
90        pat: Box<Pat>,
91    },
92    /// 反复推进直到 `pat` 成立(等价 regex 的 `.*?pat`,但无贪婪性概念)
93    Until {
94        pat: Box<Pat>,
95    },
96}
97
98// ---------------------------------------------------------------------------
99// 从 SML 数据编译
100// ---------------------------------------------------------------------------
101
102/// 默认步数预算:无回溯已保证多项式时间,此上限仅用于拦住极端病态输入。
103const DEFAULT_BUDGET: u64 = 10_000_000;
104
105/// 把一条规则编译为模式 IR,关键字表取默认 [`BILINGUAL`]。
106pub fn compile_rule(rule: &Value, rules: &BTreeMap<String, Value>) -> Result<Pat, String> {
107    compile_rule_with(rule, rules, &BILINGUAL)
108}
109
110/// 用指定的关键字表编译(供非中英语言或项目自有方言使用)。
111pub fn compile_rule_with(
112    rule: &Value,
113    rules: &BTreeMap<String, Value>,
114    table: &dyn KeywordTable,
115) -> Result<Pat, String> {
116    let mut stack = Vec::new();
117    compile_value(rule, rules, &mut stack, table)
118}
119
120fn compile_value(
121    v: &Value,
122    rules: &BTreeMap<String, Value>,
123    stack: &mut Vec<String>,
124    table: &dyn KeywordTable,
125) -> Result<Pat, String> {
126    match v {
127        // 数组:视为序列(保序,这是规则体的常见形态)
128        Value::Array(items) => {
129            let mut seq = Vec::new();
130            for it in items {
131                seq.push(compile_value(it, rules, stack, table)?);
132            }
133            Ok(Pat::Seq(seq))
134        }
135        // 裸词 / 字符串:字面量
136        Value::Str(s) => Ok(Pat::Lit(s.clone())),
137        Value::Object(map) => compile_elem(map, rules, stack, table),
138        other => Err(format!("sml: 模式元素类型不支持: {}", type_name(other))),
139    }
140}
141
142fn compile_elem(
143    map: &BTreeMap<String, Value>,
144    rules: &BTreeMap<String, Value>,
145    stack: &mut Vec<String>,
146    table: &dyn KeywordTable,
147) -> Result<Pat, String> {
148    // 1) 按键的**语义**归类。遍历而非硬编码查某个键名,
149    //    这样换一张关键字表就能换语言,编译逻辑无需改动。
150    let mut lit: Option<&Value> = None;
151    let mut cls: Option<&Value> = None;
152    let mut alt: Option<&Value> = None;
153    let mut grp: Option<&Value> = None;
154    let mut seq: Option<&Value> = None;
155    let mut use_ref: Option<&Value> = None;
156    let mut until: Option<&Value> = None;
157    let mut times: Option<&Value> = None;
158    let mut optional: Option<&Value> = None;
159    let mut name: Option<&Value> = None;
160    let mut regex_src: Option<&Value> = None;
161    // 平铺量词(机翻等价直觉写法):最小 / 最大 直接写在元素上,
162    // 与 `次: { 最小, 最大 }` 同义。此前归入 `_ => {}` 被静默忽略,现已生效。
163    let mut flat_min_raw: Option<&Value> = None;
164    let mut flat_max_raw: Option<&Value> = None;
165
166    for (k, v) in map.iter() {
167        match table.lookup(k) {
168            Some(Concept::Lit) => lit = Some(v),
169            Some(Concept::Class) => cls = Some(v),
170            Some(Concept::Alt) => alt = Some(v),
171            Some(Concept::Group) => grp = Some(v),
172            Some(Concept::Seq) => seq = Some(v),
173            Some(Concept::Use) => use_ref = Some(v),
174            Some(Concept::Until) => until = Some(v),
175            Some(Concept::Times) => times = Some(v),
176            Some(Concept::Optional) => optional = Some(v),
177            Some(Concept::Min) => flat_min_raw = Some(v),
178            Some(Concept::Max) => flat_max_raw = Some(v),
179            Some(Concept::Name) => name = Some(v),
180            Some(Concept::Regex) => regex_src = Some(v),
181            // 未知键:容忍,便于在规则里附加说明性字段(如 说明: "手机号")
182            _ => {}
183        }
184    }
185
186    // 2) 基底
187    let base = if let Some(v) = lit {
188        Pat::Lit(as_str(v, "字面/lit")?)
189    } else if let Some(v) = cls {
190        Pat::Class(parse_class(&as_str(v, "类/class")?, table)?)
191    } else if let Some(v) = alt {
192        let items = as_array(v, "任一/alt")?;
193        let mut alts = Vec::new();
194        for it in items {
195            alts.push(compile_value(it, rules, stack, table)?);
196        }
197        Pat::Alt(alts)
198    } else if let Some(v) = grp {
199        compile_value(v, rules, stack, table)?
200    } else if let Some(v) = seq {
201        compile_value(v, rules, stack, table)?
202    } else if let Some(v) = use_ref {
203        let ref_name = as_str(v, "用/use")?;
204        if stack.contains(&ref_name) {
205            return Err(format!(
206                "sml: 规则 `{ref_name}` 存在循环引用(L1 不支持递归;如需嵌套结构请先升级到 L2)"
207            ));
208        }
209        let target = rules
210            .get(&ref_name)
211            .ok_or_else(|| format!("sml: 未定义的规则 `{ref_name}`"))?;
212        stack.push(ref_name.clone());
213        let p = compile_value(target, rules, stack, table)?;
214        stack.pop();
215        p
216    } else if let Some(v) = until {
217        let u = as_str(v, "直到/until")?;
218        match table.lookup(&u) {
219            // 「直到行尾」= 吃掉剩余全部字符
220            Some(Concept::Eol) => Pat::Repeat {
221                pat: Box::new(Pat::Class(Class::Any)),
222                min: 0,
223                max: None,
224            },
225            _ => Pat::Until {
226                pat: Box::new(Pat::Lit(u)),
227            },
228        }
229    } else if let Some(v) = regex_src {
230        // regex 逃生舱:解析后编译进同一个 IR,故仍是无回溯、仍免疫 ReDoS
231        let src = as_str(v, "正则/regex")?;
232        crate::regex::parse_regex(&src)
233            .map_err(|e| format!("sml: regex 编译失败:{e}"))?
234    } else {
235        return Err(format!(
236            "sml: 无法识别的模式元素(需 字面/类/任一/组/序列/用/直到/正则 之一),实际键: {:?}",
237            map.keys().collect::<Vec<_>>()
238        ));
239    };
240
241    // 3) 量词:优先平铺 最小/最大(机翻等价直觉写法),否则用 次 / 可选。
242    //    任一来源只产生一个 Repeat,避免重复包裹。无效输入一律报错,绝不静默忽略。
243    let flat_min = match flat_min_raw {
244        Some(v) => Some(as_int(v).ok_or_else(|| format!("sml: 量词最小必须为整数,得 {v:?}"))?),
245        None => None,
246    };
247    let flat_max = match flat_max_raw {
248        Some(v) => Some(as_int(v).ok_or_else(|| format!("sml: 量词最大必须为整数,得 {v:?}"))?),
249        None => None,
250    };
251
252    let base = if flat_min.is_some() || flat_max.is_some() {
253        let mn = flat_min.unwrap_or(0);
254        let mx = flat_max;
255        if mn < 0 {
256            return Err(format!("sml: 量词最小不能为负(得 {mn})"));
257        }
258        if let Some(mxv) = mx {
259            if mxv < mn {
260                return Err(format!("sml: 量词最大({mxv})不能小于最小({mn})"));
261            }
262        }
263        Pat::Repeat {
264            pat: Box::new(base),
265            min: mn as usize,
266            max: mx.map(|x| x as usize),
267        }
268    } else if let Some(t) = times {
269        let (min, max) = parse_times(t, table)?;
270        Pat::Repeat {
271            pat: Box::new(base),
272            min,
273            max,
274        }
275    } else if matches!(optional, Some(Value::Bool(true))) {
276        Pat::Repeat {
277            pat: Box::new(base),
278            min: 0,
279            max: Some(1),
280        }
281    } else {
282        base
283    };
284
285    // 5) 命名
286    match name {
287        None => Ok(base),
288        Some(n) => Ok(Pat::Named {
289            name: as_str(n, "名/name")?,
290            pat: Box::new(base),
291        }),
292    }
293}
294
295fn parse_class(s: &str, table: &dyn KeywordTable) -> Result<Class, String> {
296    match table.lookup(s) {
297        Some(Concept::Digit) => Ok(Class::Digit),
298        Some(Concept::Alpha) => Ok(Class::Alpha),
299        Some(Concept::Space) => Ok(Class::Space),
300        Some(Concept::Word) => Ok(Class::Word),
301        Some(Concept::Any) => Ok(Class::Any),
302        // 非内置类:单字符按字面处理(如 类: "-")
303        _ => {
304            let mut chars = s.chars();
305            match (chars.next(), chars.next()) {
306                (Some(c), None) => Ok(Class::One(c)),
307                _ => Err(format!("sml: 未知字符类 `{s}`")),
308            }
309        }
310    }
311}
312
313fn parse_times(v: &Value, table: &dyn KeywordTable) -> Result<(usize, Option<usize>), String> {
314    let (min, max): (i64, Option<i64>) = match v {
315        Value::Int(n) if *n >= 0 => (*n, Some(*n)),
316        Value::Str(s) => match s.as_str() {
317            "+" => (1, None),
318            "*" => (0, None),
319            "?" => (0, Some(1)),
320            other => {
321                if let Some((a, b)) = other.split_once('-') {
322                    let a = a
323                        .trim()
324                        .parse::<i64>()
325                        .map_err(|_| format!("sml: 量词 `{other}` 非法"))?;
326                    let b = b
327                        .trim()
328                        .parse::<i64>()
329                        .map_err(|_| format!("sml: 量词 `{other}` 非法"))?;
330                    (a, Some(b))
331                } else {
332                    return Err(format!(
333                        "sml: 未知量词 `{other}`(可用 数字 / + / * / ? / a-b)"
334                    ));
335                }
336            }
337        },
338        Value::Object(m) => {
339            // 量词对象形式:{ 最小: 2, 最大: 5 } —— 键名同样走关键字表
340            let mut min = 0i64;
341            let mut max: Option<i64> = None;
342            for (k, v) in m.iter() {
343                match table.lookup(k) {
344                    Some(Concept::Min) => {
345                        min = as_int(v).ok_or_else(|| format!("sml: 量词最小必须为整数,得 {v:?}"))?
346                    }
347                    Some(Concept::Max) => {
348                        max = Some(
349                            as_int(v)
350                                .ok_or_else(|| format!("sml: 量词最大必须为整数,得 {v:?}"))?,
351                        )
352                    }
353                    _ => {}
354                }
355            }
356            (min, max)
357        }
358        other => return Err(format!("sml: 量词类型不支持: {}", type_name(other))),
359    };
360    // 统一校验:下界非负、上界不小于下界(非法输入一律报错,绝不静默)
361    if min < 0 {
362        return Err(format!("sml: 量词最小不能为负(得 {min})"));
363    }
364    if let Some(mx) = max {
365        if mx < min {
366            return Err(format!("sml: 量词最大({mx})不能小于最小({min})"));
367        }
368    }
369    Ok((min as usize, max.map(|x| x as usize)))
370}
371
372// ---------------------------------------------------------------------------
373// 公开匹配 API
374// ---------------------------------------------------------------------------
375
376/// 用已编译的模式匹配整段文本(全文匹配,非搜索)。
377pub fn is_match(pat: &Pat, text: &str) -> Result<bool, String> {
378    let prog = nfa::compile(pat);
379    nfa::run(&prog, text, DEFAULT_BUDGET)
380}
381
382/// 便捷入口:直接用「规则表 + 规则名」匹配文本(默认双语关键字)。
383pub fn matches(rules: &BTreeMap<String, Value>, name: &str, text: &str) -> Result<bool, String> {
384    matches_with(rules, name, text, &BILINGUAL)
385}
386
387/// 指定关键字表的匹配入口。
388pub fn matches_with(
389    rules: &BTreeMap<String, Value>,
390    name: &str,
391    text: &str,
392    table: &dyn KeywordTable,
393) -> Result<bool, String> {
394    let rule = rules
395        .get(name)
396        .ok_or_else(|| format!("sml: 未定义的规则 `{name}`"))?;
397    let pat = compile_rule_with(rule, rules, table)?;
398    is_match(&pat, text)
399}
400
401// ---------------------------------------------------------------------------
402// 取值辅助(Value API 的最小适配)
403// ---------------------------------------------------------------------------
404
405fn as_str(v: &Value, field: &str) -> Result<String, String> {
406    match v {
407        Value::Str(s) => Ok(s.clone()),
408        other => Err(format!(
409            "sml: `{field}` 须为字符串,实际 {}",
410            type_name(other)
411        )),
412    }
413}
414
415fn as_array<'a>(v: &'a Value, field: &str) -> Result<&'a Vec<Value>, String> {
416    match v {
417        Value::Array(a) => Ok(a),
418        other => Err(format!("sml: `{field}` 须为数组,实际 {}", type_name(other))),
419    }
420}
421
422fn as_int(v: &Value) -> Option<i64> {
423    match v {
424        Value::Int(n) => Some(*n),
425        _ => None,
426    }
427}
428
429fn type_name(v: &Value) -> &'static str {
430    match v {
431        Value::Str(_) => "字符串",
432        Value::Int(_) => "整数",
433        Value::Float(..) => "浮点数",
434        Value::Bool(_) => "布尔",
435        Value::Null => "null",
436        Value::Array(_) => "数组",
437        Value::Object(_) => "对象",
438    }
439}