///|
/// An immutable compiled Boolean search program in MoonBit core's regex dialect.
pub struct Regex {
priv source : String
priv program : @re.Program
}
///|
pub impl @debug.Debug for Regex with fn to_repr(self) {
@debug.Repr(self.source)
}
///|
/// Search with a finite allowance. Exhaustion is distinct from a completed non-match.
pub fn Regex::matches(
self : Regex,
input : StringView,
work~ : WorkLimit,
) -> MatchOutcome {
let (matched, used) = self.program.matches(input, work.units)
match matched {
Some(result) => Complete(result, used)
None => Exhausted(used)
}
}
///|
/// Compile runtime pattern data under finite structural limits. Group syntax is
/// accepted but captures are erased: this API returns only a Boolean result.
pub fn Regex::compile(
pattern : StringView,
limits~ : CompileLimits = {
source_units: 4096,
nesting: 32,
expanded_units: 1024,
},
) -> Regex raise CompileError {
guard pattern.length() <= limits.source_units else {
raise CompilationLimit(SourceLength)
}
let parsed = @regex_parser.parse(
profile=re_profile_unicode,
mode=String,
pattern,
max_depth=limits.nesting,
) catch {
error => {
let message = error.to_string()
if message.contains("Groups nested too deeply") {
raise CompilationLimit(Nesting)
}
if message.contains("Unsupported") || message.contains("not supported") {
raise UnsupportedPattern(message)
}
raise InvalidPattern(message)
}
}
// Count before UTF-16 lowering and before the NFA emitter copies repetitions.
ignore(expanded_size(parsed, limits.expanded_units))
let lowered = re_lower_to_utf16(erase_captures(parsed))
{
source: pattern.to_owned(),
program: @re.compile(profile=re_profile_utf16, lowered).program(),
}
}
///|
fn checked_size_add(
a : Int32,
b : Int32,
cap : Int32,
) -> Int32 raise CompileError {
guard b <= cap - a else { raise CompilationLimit(Expansion) }
a + b
}
///|
fn expanded_size(
pattern : @re.Pattern,
cap : Int32,
) -> Int32 raise CompileError {
match pattern.desc {
Char(set) => {
let mut size = 0
for (_, hi) in set.intervals() {
// An astral interval lowers to at most three pairs, their sequences
// an alternation and an optional BMP part. BMP intervals need one interval.
size = checked_size_add(size, if hi > 0xffff { 12 } else { 1 }, cap)
}
Int32::max(1, size)
}
Assertion(_) => 1
Capture(inner, ..) => expanded_size(inner, cap)
Sequence(items) | Alternation(items) => {
let mut size = 1
for item in items {
size = checked_size_add(size, expanded_size(item, cap), cap)
}
size
}
Quantifier(q, inner) => {
let size = expanded_size(inner, cap)
let copies = q.max.unwrap_or(Int32::max(1, q.min))
guard copies <= cap / size else { raise CompilationLimit(Expansion) }
checked_size_add(size * copies, 2, cap)
}
}
}
///|
fn erase_captures(pattern : @re.Pattern) -> @re.Pattern {
match pattern.desc {
Capture(inner, ..) => erase_captures(inner)
Char(_) | Assertion(_) => pattern
Sequence(items) => @re.seq(items.map(erase_captures))
Alternation(items) => @re.alt(items.map(erase_captures))
Quantifier(q, inner) => @re.quantifier(erase_captures(inner), q)
}
}