// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// Which group a value names: `Whole` for the whole match, `Num` for a
/// numbered parenthesis, `Name` for a named one.
///
/// This is the shape the methods work in. Callers do not have to build it —
/// `GroupKey` is implemented by `Int`, by `String` and by this type, so
/// `m.group(1)`, `m.group("year")` and `m.group(Whole)` are one call.
pub(all) enum Group {
/// Group 0, the whole match.
Whole
/// A group by number; 1 is the first parenthesis, 0 the whole match.
Num(n~ : Int32)
/// A group by name.
Name(n~ : String)
}
///|
/// What can name a group. Two implementations do the work: a number and a
/// name, so `m.group(1)` and `m.group("year")` are the same method.
///
/// The trait is open: a caller with its own key — a slot number in a table of
/// its own, say — implements it and the methods take it without a change here.
pub(open) trait GroupKey {
fn to_group(Self) -> Group
}
///|
pub impl GroupKey for Int32 with fn to_group(self) -> Group {
if self == 0 {
Whole
} else {
Num(n=self)
}
}
///|
pub impl GroupKey for String with fn to_group(self) -> Group {
Name(n=self)
}
///|
pub impl GroupKey for Group with fn to_group(self) -> Group {
self
}
///|
#deprecated
pub extend Group with GroupKey::{to_group}
///|
/// The preset flags, for callers who want to tweak one and keep the rest.
pub let flags : @parse.Flags = @parse.flags
///|
/// The preset dialect: Python's `re`.
pub let dialect : @parse.Dialect = @parse.dialect
///|
/// The flags type, re-exported so a caller needs only this package.
pub type Flags = @parse.Flags
///|
pub type Dialect = @parse.Dialect
///|
pub type Version = @parse.Version
///|
/// The Python `re` dialect — the default, and the one most callers want.
pub let python : Dialect = @parse.Python
///|
/// The ECMA-262 `RegExp` dialect — JSON Schema and OpenAPI "string" patterns.
pub let ecma262 : Dialect = @parse.Ecma262
///|
/// The MoonBit `lexmatch` dialect — `@string.Regex` and the lexer.
pub let lexmatch_dialect : Dialect = @parse.Lexmatch
///|
/// The error the grammar and engine raise when a pattern cannot be read or a
/// group the pattern does not have is asked for.
pub type Refused = @parse.Refused
///|
/// Compile `pattern` into a `Pattern`.
///
/// `flags` and `dialect` set the grammar; `lists` feeds `\L` named lists;
/// `partial` turns on partial matching. A pattern the grammar refuses is refused
/// here, with where reading stopped.
pub fn compile(
pattern : StringView,
flags~ : @parse.Flags = @parse.flags,
dialect~ : @parse.Dialect = @parse.Python,
lists? : Map[String, Array[String]],
partial~ : Bool = false,
) -> Pattern raise @parse.Refused {
let ast = @parse.parse(pattern, flags~, dialect~, lists=lists.unwrap_or({}))
let program = @exec.compile(ast)
Pattern::{ pat: pattern.to_owned(), flags, dialect, ast, program, partial, }
}
///|
/// Scan `text` for the leftmost match, as Python's `re.search`.
pub fn search(
pattern : StringView,
text : String,
flags~ : @parse.Flags = @parse.flags,
dialect~ : @parse.Dialect = @parse.Python,
pos~ : Int32 = 0,
endpos~ : Int32 = -1,
limit~ : Int32 = 10000000,
lists? : Map[String, Array[String]],
partial~ : Bool = false,
) -> Match? raise @parse.Refused {
compile(pattern, flags~, dialect~, lists=lists.unwrap_or({}), partial~).search(
text,
pos~,
endpos~,
limit~,
)
}
///|
/// Match only at `pos` (Python's `re.match`): fail unless the pattern matches
/// where the scan begins, not somewhere later in the text.
pub fn prefix(
pattern : StringView,
text : String,
flags~ : @parse.Flags = @parse.flags,
dialect~ : @parse.Dialect = @parse.Python,
pos~ : Int32 = 0,
limit~ : Int32 = 10000000,
lists? : Map[String, Array[String]],
partial~ : Bool = false,
) -> Match? raise @parse.Refused {
compile(pattern, flags~, dialect~, lists=lists.unwrap_or({}), partial~).prefix(
text,
pos~,
limit~,
)
}
///|
/// Match the whole region `pos..endpos` (Python's `re.fullmatch`).
pub fn full(
pattern : StringView,
text : String,
flags~ : @parse.Flags = @parse.flags,
dialect~ : @parse.Dialect = @parse.Python,
pos~ : Int32 = 0,
endpos~ : Int32 = -1,
limit~ : Int32 = 10000000,
lists? : Map[String, Array[String]],
partial~ : Bool = false,
) -> Match? raise @parse.Refused {
compile(pattern, flags~, dialect~, lists=lists.unwrap_or({}), partial~).full(
text,
pos~,
endpos~,
limit~,
)
}
///|
/// All non-overlapping matches, as Python's `re.findall`.
///
/// With no group named and the pattern holding one or none, the matches (or the
/// single group) are returned. With two or more groups and no `group` given, the
/// call is ambiguous and refused — pass `group` to say which to collect, or use
/// `finditer` to walk `Match` objects that carry them all.
pub fn findall(
pattern : StringView,
text : String,
flags~ : @parse.Flags = @parse.flags,
dialect~ : @parse.Dialect = @parse.Python,
group~ : Group = Whole,
overlapped~ : Bool = false,
pos~ : Int32 = 0,
endpos~ : Int32 = -1,
limit~ : Int32 = 10000000,
lists? : Map[String, Array[String]],
partial~ : Bool = false,
) -> Array[String?] raise @parse.Refused {
compile(pattern, flags~, dialect~, lists=lists.unwrap_or({}), partial~).findall(
text,
group~,
overlapped~,
pos~,
endpos~,
limit~,
)
}
///|
/// All matches as `Match` objects, as Python's `re.finditer`.
pub fn finditer(
pattern : StringView,
text : String,
flags~ : @parse.Flags = @parse.flags,
dialect~ : @parse.Dialect = @parse.Python,
overlapped~ : Bool = false,
pos~ : Int32 = 0,
endpos~ : Int32 = -1,
limit~ : Int32 = 10000000,
lists? : Map[String, Array[String]],
partial~ : Bool = false,
) -> Array[Match] raise @parse.Refused {
compile(pattern, flags~, dialect~, lists=lists.unwrap_or({}), partial~).finditer(
text,
pos~,
endpos~,
overlapped~,
limit~,
)
}
///|
/// Split `text` on the pattern, as Python's `re.split`.
pub fn split(
pattern : StringView,
text : String,
flags~ : @parse.Flags = @parse.flags,
dialect~ : @parse.Dialect = @parse.Python,
maxsplit~ : Int32 = 0,
pos~ : Int32 = 0,
endpos~ : Int32 = -1,
limit~ : Int32 = 10000000,
lists? : Map[String, Array[String]],
partial~ : Bool = false,
) -> Array[String?] raise @parse.Refused {
compile(pattern, flags~, dialect~, lists=lists.unwrap_or({}), partial~).split(
text,
maxsplit~,
pos~,
endpos~,
limit~,
)
}
///|
/// Replace matches with `repl`, as Python's `re.sub`.
pub fn sub(
pattern : StringView,
repl : String,
text : String,
flags~ : @parse.Flags = @parse.flags,
dialect~ : @parse.Dialect = @parse.Python,
count~ : Int32 = 0,
pos~ : Int32 = 0,
endpos~ : Int32 = -1,
limit~ : Int32 = 10000000,
lists? : Map[String, Array[String]],
partial~ : Bool = false,
) -> String raise @parse.Refused {
compile(pattern, flags~, dialect~, lists=lists.unwrap_or({}), partial~).sub(
text,
repl,
count~,
pos~,
endpos~,
limit~,
)
}
///|
/// Replace matches with `repl`, returning the new text and how many matches
/// were replaced, as Python's `re.subn`.
pub fn subn(
pattern : StringView,
repl : String,
text : String,
flags~ : @parse.Flags = @parse.flags,
dialect~ : @parse.Dialect = @parse.Python,
count~ : Int32 = 0,
pos~ : Int32 = 0,
endpos~ : Int32 = -1,
limit~ : Int32 = 10000000,
lists? : Map[String, Array[String]],
partial~ : Bool = false,
) -> (String, Int32) raise @parse.Refused {
compile(pattern, flags~, dialect~, lists=lists.unwrap_or({}), partial~).subn(
text,
repl,
count~,
pos~,
endpos~,
limit~,
)
}
///|
/// Replace matches with `repl`, as `regex.subf` — `repl` is a format template
/// whose fields name groups (`{}`, `{0}`, `{name}`, `{0[0]}`).
pub fn subf(
pattern : StringView,
repl : String,
text : String,
flags~ : @parse.Flags = @parse.flags,
dialect~ : @parse.Dialect = @parse.Python,
count~ : Int32 = 0,
pos~ : Int32 = 0,
endpos~ : Int32 = -1,
limit~ : Int32 = 10000000,
lists? : Map[String, Array[String]],
partial~ : Bool = false,
) -> String raise @parse.Refused {
compile(pattern, flags~, dialect~, lists=lists.unwrap_or({}), partial~).subf(
text,
repl,
count~,
pos~,
endpos~,
limit~,
)
}
///|
/// Replace matches with `repl`, returning the new text and the count, as
/// `regex.subfn`.
pub fn subfn(
pattern : StringView,
repl : String,
text : String,
flags~ : @parse.Flags = @parse.flags,
dialect~ : @parse.Dialect = @parse.Python,
count~ : Int32 = 0,
pos~ : Int32 = 0,
endpos~ : Int32 = -1,
limit~ : Int32 = 10000000,
lists? : Map[String, Array[String]],
partial~ : Bool = false,
) -> (String, Int32) raise @parse.Refused {
compile(pattern, flags~, dialect~, lists=lists.unwrap_or({}), partial~).subfn(
text,
repl,
count~,
pos~,
endpos~,
limit~,
)
}
///|
/// Replace matches with the result of `f(match)`, as Python's `re.sub` with a
/// callable replacement.
pub fn sub_with(
pattern : StringView,
f : (Match) -> String raise @parse.Refused,
text : String,
flags~ : @parse.Flags = @parse.flags,
dialect~ : @parse.Dialect = @parse.Python,
count~ : Int32 = 0,
pos~ : Int32 = 0,
endpos~ : Int32 = -1,
limit~ : Int32 = 10000000,
lists? : Map[String, Array[String]],
partial~ : Bool = false,
) -> String raise @parse.Refused {
compile(pattern, flags~, dialect~, lists=lists.unwrap_or({}), partial~).subn_with(
text,
f,
count~,
pos~,
endpos~,
limit~,
).0
}
///|
/// Replace matches with `f(match)`, returning the new text and the count, as
/// Python's `re.subn` with a callable replacement.
pub fn subn_with(
pattern : StringView,
f : (Match) -> String raise @parse.Refused,
text : String,
flags~ : @parse.Flags = @parse.flags,
dialect~ : @parse.Dialect = @parse.Python,
count~ : Int32 = 0,
pos~ : Int32 = 0,
endpos~ : Int32 = -1,
limit~ : Int32 = 10000000,
lists? : Map[String, Array[String]],
partial~ : Bool = false,
) -> (String, Int32) raise @parse.Refused {
compile(pattern, flags~, dialect~, lists=lists.unwrap_or({}), partial~).subn_with(
text,
f,
count~,
pos~,
endpos~,
limit~,
)
}
///|
/// Escape `pattern` so the result matches it literally, under `dialect`.
///
/// The three grammars do not agree on what needs escaping, so the same string
/// comes back spelled three ways:
///
/// - `Python` — Python's `re.escape`: the characters that can mean something to
/// the grammar are backslashed, everything else is left alone. With
/// `special_only` off every non-alphanumeric is escaped; `literal_spaces`
/// stops spaces from being escaped.
/// - `Ecma262` — `RegExp.escape` of ECMA-262: punctuation that is not a syntax
/// character becomes `\x..`, syntax characters are backslashed, and a
/// leading alphanumeric is spelled `\x..` too so the result can never read
/// as a quantifier's neighbour.
/// - `Lexmatch` — the `@string.Regex` grammar: its metacharacters are
/// backslashed and control characters are spelled in the escapes that
/// grammar knows (`\n`, `\t`, `\u{..}`), never `\x..`.
pub fn escape(
pattern : StringView,
dialect~ : @parse.Dialect = @parse.Python,
special_only~ : Bool = true,
literal_spaces~ : Bool = false,
) -> String {
match dialect {
@parse.Python => escape_python(pattern, special_only, literal_spaces)
@parse.Ecma262 => escape_ecma(pattern)
@parse.Lexmatch => escape_lexmatch(pattern, special_only)
}
}
///|
/// Python's `re.escape`: a fixed set of characters carries a backslash.
fn escape_python(
pattern : StringView,
special_only : Bool,
literal_spaces : Bool,
) -> String {
let buf = StringBuilder::StringBuilder()
for c in pattern {
let code = c.to_int()
let is_alnum = is_ascii_alnum(code)
let is_space = code == 0x20 ||
code == 0x09 ||
code == 0x0A ||
code == 0x0D ||
code == 0x0B ||
code == 0x0C
if is_alnum {
buf.write_char(c)
} else if literal_spaces && is_space {
buf.write_char(c)
} else if special_only && !is_python_special(c) {
buf.write_char(c)
} else {
buf.write_char('\\')
buf.write_char(c)
}
}
buf.to_string()
}
///|
/// `RegExp.escape`: the spelling ECMA-262 settled on, whose point is that the
/// result is safe both as a pattern and as the text of one — no ambiguity
/// between a literal and a quantifier.
fn escape_ecma(pattern : StringView) -> String {
let buf = StringBuilder::StringBuilder()
let chars : Array[Char] = []
for c in pattern {
chars.push(c)
}
let mut i = 0
while i < chars.length() {
let c = chars[i]
let code = c.to_int()
// A leading alphanumeric could fuse with a quantifier that follows the
// escaped text, so the standard spells it out.
if i == 0 && is_ascii_alnum(code) {
buf.write_string(hex_escape(code, 2))
} else {
buf.write_string(encode_for_regexp_escape(code))
}
i += 1
}
buf.to_string()
}
///|
/// The `EncodeForRegExpEscape` table of ECMA-262.
fn encode_for_regexp_escape(code : Int32) -> String {
match code {
0x09 => "\\t"
0x0A => "\\n"
0x0B => "\\v"
0x0C => "\\f"
0x0D => "\\r"
_ =>
if is_ecma_syntax_char(code) {
"\\" + code.unsafe_to_char().to_string()
} else if is_ecma_other_punctuator(code) || code == 0x20 {
hex_escape(code, 2)
} else if code >= 0xD800 && code <= 0xDFFF {
"\\u" + hex_digits(code, 4)
} else {
// Everything else stands for itself: the standard leaves letters,
// digits and non-ASCII text alone.
code.unsafe_to_char().to_string()
}
}
}
///|
/// `^ $ \ . * + ? ( ) [ ] { } |` and `/` — the characters a backslash alone
/// makes literal.
fn is_ecma_syntax_char(code : Int32) -> Bool {
match code {
0x5E
| 0x24
| 0x5C
| 0x2E
| 0x2A
| 0x2B
| 0x3F
| 0x28
| 0x29
| 0x5B
| 0x5D
| 0x7B
| 0x7D
| 0x7C
| 0x2F => true
_ => false
}
}
///|
/// `, - = < > # & ! % : ; @ ~ ' ` "` — punctuation the grammar does not read,
/// which the standard still spells out so the output is stable.
fn is_ecma_other_punctuator(code : Int32) -> Bool {
match code {
0x2C
| 0x2D
| 0x3D
| 0x3C
| 0x3E
| 0x23
| 0x26
| 0x21
| 0x25
| 0x3A
| 0x3B
| 0x40
| 0x7E
| 0x27
| 0x60
| 0x22 => true
_ => false
}
}
///|
/// The `@string.Regex` grammar's escape: its metacharacters take a backslash,
/// and anything invisible is spelled in an escape that grammar knows.
fn escape_lexmatch(pattern : StringView, special_only : Bool) -> String {
let buf = StringBuilder::StringBuilder()
for c in pattern {
let code = c.to_int()
// Invisible characters first: the grammar spells them, it never carries
// them raw.
if code == 0x09 {
buf.write_string("\\t")
} else if code == 0x0A {
buf.write_string("\\n")
} else if code == 0x0B {
buf.write_string("\\v")
} else if code == 0x0C {
buf.write_string("\\f")
} else if code == 0x0D {
buf.write_string("\\r")
} else if is_lexmatch_special(code) {
buf.write_char('\\')
buf.write_char(c)
} else if special_only || is_ascii_alnum(code) {
buf.write_char(c)
} else if code < 0x20 || code == 0x7F {
buf.write_string("\\u{" + hex_digits(code, 0) + "}")
} else {
buf.write_char(c)
}
}
buf.to_string()
}
///|
fn is_lexmatch_special(code : Int32) -> Bool {
match code {
0x5C
| 0x2E
| 0x2A
| 0x2B
| 0x3F
| 0x28
| 0x29
| 0x5B
| 0x5D
| 0x7B
| 0x7D
| 0x7C
| 0x5E
| 0x24
| 0x2D => true
_ => false
}
}
///|
fn is_ascii_alnum(code : Int32) -> Bool {
(code >= 0x61 && code <= 0x7A) ||
(code >= 0x41 && code <= 0x5A) ||
(code >= 0x30 && code <= 0x39)
}
///|
/// `\x..`, with exactly `width` digits when `width` is non-zero.
fn hex_escape(code : Int32, width : Int32) -> String {
"\\x" + hex_digits(code, width)
}
///|
/// Upper-case hex, zero-padded to `width` digits; `width` 0 means "as many as
/// the value needs".
fn hex_digits(value : Int32, width : Int32) -> String {
let digits = "0123456789ABCDEF"
let out : Array[Char] = []
let mut n = value
if n == 0 {
out.push('0')
}
while n > 0 {
out.push(digits[n % 16].to_int().unsafe_to_char())
n = n / 16
}
while out.length() < width {
out.push('0')
}
let buf = StringBuilder::StringBuilder()
let mut i = out.length() - 1
while i >= 0 {
buf.write_char(out[i])
i -= 1
}
buf.to_string()
}
///|
fn is_python_special(c : Char) -> Bool {
c == '(' ||
c == ')' ||
c == '[' ||
c == ']' ||
c == '{' ||
c == '}' ||
c == '?' ||
c == '*' ||
c == '+' ||
c == '-' ||
c == '|' ||
c == '^' ||
c == '$' ||
c == '\\' ||
c == '.' ||
c == '&' ||
c == '~' ||
c == '#' ||
c == ' ' ||
c == '\t' ||
c == '\n' ||
c == '\r' ||
c == '\u{0B}' ||
c == '\u{0C}'
}
// ---------------------------------------------------------------------------
// Flag constants & module aliases (Python `re` / `regex` surface)
// ---------------------------------------------------------------------------
// MoonBit constrains `let` bindings to lower-case names and `const` to primitive
// types, so the Python `re` flag constants (which are upper-case) are exposed
// here under their lower-case spelling — `i`/`ignorecase`, `m`/`multiline`, and
// so on — each a `Flags` value, so a caller ports a pattern flag-for-flag.
///|
pub let i : Flags = @parse.Flags::new(ignorecase=true)
///|
/// `ignorecase` — Python's `re.IGNORECASE`.
pub let ignorecase : Flags = i
///|
pub let m : Flags = @parse.Flags::new(multiline=true)
///|
/// `multiline` — Python's `re.MULTILINE`.
pub let multiline : Flags = m
///|
pub let s : Flags = @parse.Flags::new(dotall=true)
///|
/// `dotall` — Python's `re.DOTALL`.
pub let dotall : Flags = s
///|
pub let x : Flags = @parse.Flags::new(verbose=true)
///|
/// `verbose` — Python's `re.VERBOSE`.
pub let verbose : Flags = x
///|
pub let a : Flags = @parse.Flags::new(ascii=true)
///|
/// `ascii` — Python's `re.ASCII`.
pub let ascii : Flags = a
///|
pub let u : Flags = @parse.Flags::new(unicode=true)
///|
/// `unicode` — Python's `re.UNICODE`.
pub let unicode : Flags = u
///|
pub let f : Flags = @parse.Flags::new(fullcase=true)
///|
/// `fullcase` — Python's `re.FULLCASE`.
pub let fullcase : Flags = f
///|
pub let w : Flags = @parse.Flags::new(word=true)
///|
/// `word` — `regex.WORD`.
pub let word : Flags = w
///|
pub let b : Flags = @parse.Flags::new(bestmatch=true)
///|
/// `bestmatch` — `regex.BESTMATCH`.
pub let bestmatch : Flags = b
///|
pub let e : Flags = @parse.Flags::new(enhancematch=true)
///|
/// `enhancematch` — `regex.ENHANCEMATCH`.
pub let enhancematch : Flags = e
///|
pub let r : Flags = @parse.Flags::new(reverse=true)
///|
/// `reverse` — `regex.REVERSE`.
pub let reverse : Flags = r
///|
pub let p : Flags = @parse.Flags::new(posix=true)
///|
/// `posix` — `regex.POSIX`.
pub let posix : Flags = p
///|
pub let v0 : Flags = @parse.Flags::new(version=@parse.V0)
///|
/// `version0` — `regex.VERSION0`.
pub let version0 : Flags = v0
///|
pub let v1 : Flags = @parse.Flags::new(version=@parse.V1)
///|
/// `version1` — `regex.VERSION1`.
pub let version1 : Flags = v1
///|
/// The `V0` behaviour set, as a `Version` value — `regex.VERSION0`'s target.
pub let version0_value : @parse.Version = @parse.V0
///|
/// The `V1` behaviour set, as a `Version` value — `regex.VERSION1`'s target.
pub let version1_value : @parse.Version = @parse.V1
///|
/// The version the engine reads a Python pattern under when none is asked for.
pub let default_version : @parse.Version = @parse.V0
///|
/// The library version, as `regex.__version__` reports it.
pub let version : String = "0.1.0"
///|
/// The library version split into `(major, minor, patch)`, as
/// `regex.__version_info__` reports it.
pub let version_info : (Int32, Int32, Int32) = (0, 1, 0)