// Copyright (c) 2026 colmugx
// SPDX-License-Identifier: Apache-2.0
///|
/// Opaque compiled pattern. No external regexp type leaks through this API.
pub struct CompiledPattern {
priv expression : @regexp.Regexp
priv assertions : Array[(Bool, @regexp.Regexp)]
priv anchored : Bool
}
///|
pub(all) enum PatternSupport {
Supported(CompiledPattern)
Unsupported(reason~ : String)
}
///|
pub(all) suberror PatternError {
NotSupported(String)
} derive(Debug)
///|
pub extend PatternError with @debug.Debug::{to_repr}
///|
const ECMA_WS : String = " \\t\\n\\v\\f\\r\\u{00A0}\\u{1680}\\u{2000}-\\u{200A}\\u{2028}\\u{2029}\\u{202F}\\u{205F}\\u{3000}\\u{FEFF}"
///|
fn char_at(source : String, index : Int) -> Char {
source.get_char(index).unwrap_or_else(() => source[index].unsafe_to_char())
}
///|
fn char_width(c : Char) -> Int {
if c.to_int() > 65535 {
2
} else {
1
}
}
///|
fn shorthand(c : Char, in_class : Bool) -> String? raise PatternError {
let content = match c {
'd' => "0-9"
'w' => "A-Za-z0-9_"
's' => ECMA_WS
'D' | 'W' | 'S' => {
if in_class {
raise NotSupported("negated shorthand inside a class is not supported")
}
match c {
'D' => return Some("[^0-9]")
'W' => return Some("[^A-Za-z0-9_]")
_ => return Some("[^\{ECMA_WS}]")
}
}
_ => return None
}
Some(if in_class { content } else { "[" + content + "]" })
}
///|
fn translate(pattern : String) -> String raise PatternError {
let output = StringBuilder()
let mut index = 0
let mut in_class = false
while index < pattern.length() {
let c = char_at(pattern, index)
if c == '\\' && index + 1 < pattern.length() {
let next = char_at(pattern, index + 1)
if in_class &&
(next == 'd' || next == 'w' || next == 's') &&
(
(index > 0 && char_at(pattern, index - 1) == '-') ||
(index + 2 < pattern.length() && char_at(pattern, index + 2) == '-')
) {
raise NotSupported(
"shorthand classes used as range endpoints are not supported",
)
}
match shorthand(next, in_class) {
Some(replacement) => output.write_string(replacement)
None => {
output.write_char('\\')
output.write_char(next)
}
}
index += 1 + char_width(next)
continue
}
if c == '[' && !in_class {
in_class = true
} else if c == ']' && in_class {
in_class = false
}
if !in_class && c == '(' && pattern[index:].has_prefix("(?") {
if pattern[index:].has_prefix("(?=") || pattern[index:].has_prefix("(?!") {
raise NotSupported("only a leading lookahead chain is supported")
}
if pattern[index:].has_prefix("(?<=") ||
pattern[index:].has_prefix("(? Int raise PatternError {
let mut index = start + 3
let mut nesting = 1
let mut in_class = false
while index < pattern.length() {
let c = char_at(pattern, index)
if c == '\\' && index + 1 < pattern.length() {
index += 1 + char_width(char_at(pattern, index + 1))
continue
}
if c == '[' && !in_class {
in_class = true
} else if c == ']' && in_class {
in_class = false
}
if !in_class {
if c == '(' {
if !pattern[index:].has_prefix("(?:") {
raise NotSupported(
"captures and nested lookarounds inside assertions are not supported",
)
}
nesting += 1
} else if c == ')' {
nesting -= 1
if nesting == 0 {
return index
}
}
}
index += char_width(c)
}
raise NotSupported("unterminated lookahead assertion")
}
///|
fn has_top_alternative(pattern : String) -> Bool {
let mut nesting = 0
let mut in_class = false
let mut index = 0
while index < pattern.length() {
let c = char_at(pattern, index)
if c == '\\' && index + 1 < pattern.length() {
index += 1 + char_width(char_at(pattern, index + 1))
continue
}
if c == '[' && !in_class {
in_class = true
} else if c == ']' && in_class {
in_class = false
}
if !in_class {
if c == '(' {
nesting += 1
} else if c == ')' {
nesting -= 1
} else if c == '|' && nesting == 0 {
return true
}
}
index += char_width(c)
}
false
}
///|
/// Unanchored search semantics. Supports ordinary regexp.mbt patterns with
/// ECMA digit/word/whitespace rewrites and a precise leading lookahead chain.
/// Other lookarounds and ambiguous rewrites are explicit unsupported errors.
pub fn compile_pattern(pattern : String) -> PatternSupport {
try {
let anchored = pattern.has_prefix("^")
let mut offset = if anchored { 1 } else { 0 }
let assertions : Array[(Bool, @regexp.Regexp)] = []
while pattern[offset:].has_prefix("(?=") ||
pattern[offset:].has_prefix("(?!") {
if !anchored {
raise NotSupported(
"leading lookahead chains must be anchored with ^ in this profile",
)
}
let positive = pattern[offset:].has_prefix("(?=")
let end = assertion_end(pattern, offset)
let body = pattern[offset + 3:end].to_owned()
assertions.push(
(positive, @regexp.compile("^(?:" + translate(body) + ")")),
)
offset = end + 1
}
let expression = if assertions.is_empty() {
@regexp.compile(translate(pattern))
} else {
let tail = pattern[offset:].to_owned()
if has_top_alternative(tail) {
raise NotSupported(
"top-level alternatives after a lookahead chain are not supported",
)
}
@regexp.compile("^(?:" + translate(tail) + ")")
}
Supported({ expression, assertions, anchored, })
} catch {
NotSupported(reason) => Unsupported(reason~)
_ => Unsupported(reason="pattern rejected by the regexp engine")
}
}
///|
/// Unsupported patterns cannot silently turn into a false validation result.
pub fn pattern_matches(
support : PatternSupport,
text : String,
) -> Bool raise PatternError {
match support {
Unsupported(reason~) => raise NotSupported(reason)
Supported(compiled) => {
if !compiled.assertions.is_empty() && !compiled.anchored {
raise NotSupported("invalid compiled assertion origin")
}
for entry in compiled.assertions {
let (positive, assertion) = entry
if assertion.execute(text).matched() != positive {
return false
}
}
compiled.expression.execute(text).matched()
}
}
}