// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// A compiled pattern: the tree it came from, the flat program it became, and
/// the source, so it can answer what Python's `re` answers of a `Pattern` —
/// its string, its flags, how many groups, and what they are named.
pub(all) struct Pattern {
priv pat : String
priv flags : @parse.Flags
priv dialect : @parse.Dialect
priv ast : @parse.Ast
priv program : @exec.Program
priv partial : Bool
}
///|
/// The pattern's source text.
pub fn Pattern::pattern(self : Pattern) -> String {
self.pat
}
///|
/// The effective flags.
pub fn Pattern::flags(self : Pattern) -> @parse.Flags {
self.flags
}
///|
/// The dialect the pattern was read under.
pub fn Pattern::dialect(self : Pattern) -> @parse.Dialect {
self.dialect
}
///|
/// How many capturing groups the pattern has, group 0 not counted — Python's
/// `Pattern.groups`.
pub fn Pattern::groups(self : Pattern) -> Int32 {
self.ast.groups()
}
///|
/// Group name to slot, as Python's `Pattern.groupindex`.
pub fn Pattern::groupindex(self : Pattern) -> Map[String, Array[Int32]] {
self.ast.groupindex()
}
///|
/// The syntax tree re-printed, as Python's `Pattern.dump` would.
pub fn Pattern::dump(self : Pattern) -> String {
self.ast.dump()
}
///|
/// Whether the pattern was compiled for partial matching.
pub fn Pattern::partial(self : Pattern) -> Bool {
self.partial
}
///|
/// Scan `text` for the leftmost match.
pub fn Pattern::search(
self : Pattern,
text : String,
pos~ : Int32 = 0,
endpos~ : Int32 = -1,
limit~ : Int32 = 10000000,
) -> Match? raise @parse.Refused {
self.search_from(text, pos, endpos, false, limit)
}
///|
/// A search that may be told it has to consume something, which is how the
/// scan steps past an empty match without losing a non-empty one at the same
/// place.
fn Pattern::search_from(
self : Pattern,
text : String,
pos : Int32,
endpos : Int32,
must_advance : Bool,
limit : Int32,
) -> Match? raise @parse.Refused {
match self.program.search(text, pos~, endpos~, limit~, must_advance~) {
None => None
Some(r) => {
let e = if endpos < 0 { cp_length(text) } else { endpos }
Some(Match::{ pattern: self, text, result: r, pos, endpos: e, })
}
}
}
///|
/// Match only at `pos`.
pub fn Pattern::prefix(
self : Pattern,
text : String,
pos~ : Int32 = 0,
limit~ : Int32 = 10000000,
) -> Match? raise @parse.Refused {
match self.program.prefix(text, pos~, limit~) {
None => None
Some(r) =>
Some(Match::{
pattern: self,
text,
result: r,
pos,
endpos: cp_length(text),
})
}
}
///|
/// Match the whole region `pos..endpos`.
pub fn Pattern::full(
self : Pattern,
text : String,
pos~ : Int32 = 0,
endpos~ : Int32 = -1,
limit~ : Int32 = 10000000,
) -> Match? raise @parse.Refused {
match self.program.full(text, pos~, endpos~, limit~) {
None => None
Some(r) => {
let e = if endpos < 0 { cp_length(text) } else { endpos }
Some(Match::{ pattern: self, text, result: r, pos, endpos: e, })
}
}
}
///|
/// All non-overlapping matches, as `findall` at the module level.
pub fn Pattern::findall(
self : Pattern,
text : String,
group~ : Group = Whole,
overlapped~ : Bool = false,
pos~ : Int32 = 0,
endpos~ : Int32 = -1,
limit~ : Int32 = 10000000,
) -> Array[String?] raise @parse.Refused {
let ng = self.groups()
// Python answers a tuple once there are two or more groups, which a
// statically typed list cannot say, so the caller has to pick one.
if group is Whole && ng >= 2 {
raise @parse.Ambiguous(groups=ng)
}
let use_group = if group is Whole {
if ng == 0 {
Whole
} else {
Num(n=1)
}
} else {
group
}
let out : Array[String?] = []
for m in self.walk(text, pos~, endpos~, overlapped~, limit~) {
// A group that took no part answers the empty string, which is what
// Python's `findall` collects — `split` and `group` keep the difference.
out.push(
match m.group(use_group) {
None => Some("")
Some(g) => Some(g)
},
)
}
out
}
///|
/// All matches as `Match` objects, as `finditer` at the module level.
pub fn Pattern::finditer(
self : Pattern,
text : String,
pos~ : Int32 = 0,
endpos~ : Int32 = -1,
overlapped~ : Bool = false,
limit~ : Int32 = 10000000,
) -> Array[Match] raise @parse.Refused {
self.walk(text, pos~, endpos~, overlapped~, limit~)
}
///|
/// Every match in the region, the way the two Python engines walk it.
///
/// After an empty match the next attempt at the same position has to consume
/// something — that is what makes `|a` on `a` answer an empty match at 0, then
/// `a`, then an empty match at 1, and what keeps `x*` from looping.
fn Pattern::walk(
self : Pattern,
text : String,
pos~ : Int32 = 0,
endpos~ : Int32 = -1,
overlapped~ : Bool = false,
limit~ : Int32 = 10000000,
) -> Array[Match] raise @parse.Refused {
let out : Array[Match] = []
let end = if endpos < 0 { cp_length(text) } else { endpos }
let mut p = pos
let mut must_advance = false
while p <= end {
match self.search_from(text, p, end, must_advance, limit) {
None =>
if must_advance {
must_advance = false
p = p + 1
} else {
break
}
Some(m) => {
out.push(m)
if overlapped {
p = m.start(0) + 1
must_advance = false
} else if m.end(0) == m.start(0) {
// After an empty match Python retries the same position for a
// non-empty one; ECMA-262's `lastIndex` just moves on by one.
if self.dialect is @parse.Ecma262 {
p = m.end(0) + 1
must_advance = false
} else {
p = m.end(0)
must_advance = true
}
} else {
p = m.end(0)
must_advance = false
}
}
}
}
out
}
///|
/// Split `text` on the pattern, as `split` at the module level.
pub fn Pattern::split(
self : Pattern,
text : String,
maxsplit~ : Int32 = 0,
pos~ : Int32 = 0,
endpos~ : Int32 = -1,
limit~ : Int32 = 10000000,
) -> Array[String?] raise @parse.Refused {
let ng = self.groups()
let end = if endpos < 0 { cp_length(text) } else { endpos }
let out : Array[String?] = []
let mut last = pos
let mut count = 0
for m in self.walk(text, pos~, endpos~, overlapped=false, limit~) {
if maxsplit != 0 && count >= maxsplit {
break
}
out.push(Some(slice_cp(text, last, m.start(0))))
let mut g = 1
while g <= ng {
out.push(m.group(Num(n=g)))
g += 1
}
last = m.end(0)
count += 1
}
out.push(Some(slice_cp(text, last, end)))
out
}
///|
/// Replace matches with `repl`, as `sub` at the module level.
pub fn Pattern::sub(
self : Pattern,
text : String,
repl : String,
count~ : Int32 = 0,
pos~ : Int32 = 0,
endpos~ : Int32 = -1,
limit~ : Int32 = 10000000,
) -> String raise @parse.Refused {
self.subn(text, repl, count~, pos~, endpos~, limit~).0
}
///|
/// Replace matches with `repl`, returning the new text and the count, as `subn`
/// at the module level.
pub fn Pattern::subn(
self : Pattern,
text : String,
repl : String,
count~ : Int32 = 0,
pos~ : Int32 = 0,
endpos~ : Int32 = -1,
limit~ : Int32 = 10000000,
) -> (String, Int32) raise @parse.Refused {
self.replace(text, m => expand(repl, m), count~, pos~, endpos~, limit~)
}
///|
/// Replace matches with `f(match)`, as `subn_with` at the module level.
pub fn Pattern::subn_with(
self : Pattern,
text : String,
f : (Match) -> String raise @parse.Refused,
count~ : Int32 = 0,
pos~ : Int32 = 0,
endpos~ : Int32 = -1,
limit~ : Int32 = 10000000,
) -> (String, Int32) raise @parse.Refused {
self.replace(text, f, count~, pos~, endpos~, limit~)
}
///|
/// Replace matches with `f(match)`, as `sub_with` at the module level.
pub fn Pattern::sub_with(
self : Pattern,
text : String,
f : (Match) -> String raise @parse.Refused,
count~ : Int32 = 0,
pos~ : Int32 = 0,
endpos~ : Int32 = -1,
limit~ : Int32 = 10000000,
) -> String raise @parse.Refused {
self.replace(text, f, count~, pos~, endpos~, limit~).0
}
///|
/// Replace matches with `repl`, as `regex.subf` — `repl` is a format template
/// whose fields name groups (`{}`, `{0}`, `{name}`, `{0[0]}`), with an optional
/// `:spec`.
pub fn Pattern::subf(
self : Pattern,
text : String,
repl : String,
count~ : Int32 = 0,
pos~ : Int32 = 0,
endpos~ : Int32 = -1,
limit~ : Int32 = 10000000,
) -> String raise @parse.Refused {
self.replace(text, m => expandf(repl, m), count~, pos~, endpos~, limit~).0
}
///|
/// Replace matches with `repl`, returning the new text and the count, as
/// `regex.subfn`.
pub fn Pattern::subfn(
self : Pattern,
text : String,
repl : String,
count~ : Int32 = 0,
pos~ : Int32 = 0,
endpos~ : Int32 = -1,
limit~ : Int32 = 10000000,
) -> (String, Int32) raise @parse.Refused {
self.replace(text, m => expandf(repl, m), count~, pos~, endpos~, limit~)
}
///|
/// The one substitution loop: walk the matches, keep the text between them,
/// and put whatever the replacement answers in their place.
fn Pattern::replace(
self : Pattern,
text : String,
f : (Match) -> String raise @parse.Refused,
count~ : Int32 = 0,
pos~ : Int32 = 0,
endpos~ : Int32 = -1,
limit~ : Int32 = 10000000,
) -> (String, Int32) raise @parse.Refused {
let end = if endpos < 0 { cp_length(text) } else { endpos }
let buf = StringBuilder::StringBuilder()
let mut last = pos
let mut n = 0
for m in self.walk(text, pos~, endpos~, overlapped=false, limit~) {
if count != 0 && n >= count {
break
}
buf.write_string(slice_cp(text, last, m.start(0)))
buf.write_string(f(m))
last = m.end(0)
n += 1
}
buf.write_string(slice_cp(text, last, end))
(buf.to_string(), n)
}