// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// Which grammar a pattern is written in.
///
/// The interface is the same whichever is chosen; the grammar is a parameter
/// because the callers need different ones. JSON Schema's `pattern` and
/// OpenAPI's `pattern` are defined as ECMA-262 §22.2 and are unforgiving
/// about it, while everyone else writes Python's.
pub(all) enum Dialect {
/// Python's `re` and the `regex` superset behind it.
Python
/// ECMA-262's `RegExp`, as a JavaScript engine reads it.
Ecma262
/// The `lexmatch` literal MoonBit's own `@string.Regex` speaks.
Lexmatch
} derive(Eq, Debug)
///|
pub extend Dialect with Eq::{equal, not_equal}
///|
pub extend Dialect with Debug::{to_repr}
///|
pub extend Dialect with Show::{output, to_string}
///|
pub impl Show for Dialect with fn output(self, logger) {
logger.write_string(
match self {
Python => "Python"
Ecma262 => "Ecma262"
Lexmatch => "Lexmatch"
},
)
}
///|
/// Which `regex`-module behaviour set a Python pattern is read under.
///
/// `V0` is `re` as everyone writes it; `V1` opens the set algebra (`--`, `&&`,
/// `~~`, nested sets) and the rest of what `(?V1)` unlocks. Only the Python
/// dialect has versions — ECMA-262 gates the same algebra behind its `v` flag.
pub(all) enum Version {
V0
V1
} derive(Eq, Debug)
///|
pub extend Version with Eq::{equal, not_equal}
///|
pub extend Version with Debug::{to_repr}
///|
/// The switches a pattern can carry.
///
/// A record rather than a bitmask, because every one of these is a thing a
/// reader may want to look at, set, or refuse — `(?a)` inside a scoped group
/// changes what `\w` means for that group alone, and a bitmask cannot be
/// asked about politely.
///
/// Flags are resolved into the syntax tree at parse time: `(?s:.)` puts the
/// dotall behaviour inside the node, so the executor never consults them.
pub(all) struct Flags {
/// `i` — case-insensitive matching.
ignorecase : Bool
/// `m` — `^` and `$` match at line boundaries.
multiline : Bool
/// `s` — `.` matches a newline.
dotall : Bool
/// `x` — whitespace and `#` comments in the pattern are not literals.
verbose : Bool
/// `a` — `\w`, `\d`, `\s` and `\b` are ASCII-only.
ascii : Bool
/// `u` — Unicode semantics. On by default under the Python dialect, where
/// patterns are Unicode already; under ECMA-262 it switches from UTF-16
/// code units to code points.
unicode : Bool
/// `v` — ECMA-262's Unicode sets: `--`, `&&` and string properties.
unicode_sets : Bool
/// `f` — full case folding, the default under `V1`.
fullcase : Bool
/// `b` — fuzzy matching takes the best match, not the first.
bestmatch : Bool
/// `e` — fuzzy matching improves a match once found.
enhancematch : Bool
/// `p` — leftmost-longest rather than leftmost-first.
posix : Bool
/// `r` — search from the end of the string backwards.
reverse : Bool
/// `w` — Unicode word boundaries and line endings (`\m`, `\M`).
word : Bool
/// Which `regex` behaviour set the Python grammar is read under.
version : Version
} derive(Eq, Debug)
///|
pub extend Flags with Eq::{equal, not_equal}
///|
pub extend Flags with Debug::{to_repr}
///|
/// Everything off, `unicode` on, version `V0`.
///
/// This is the preset every default argument points at, so a pattern written
/// with no flags at all behaves the way Python's `re` does by default.
pub let flags : Flags = {
ignorecase: false,
multiline: false,
dotall: false,
verbose: false,
ascii: false,
unicode: true,
unicode_sets: false,
fullcase: false,
bestmatch: false,
enhancematch: false,
posix: false,
reverse: false,
word: false,
version: V0,
}
///|
/// The dialect a pattern is read under when the caller does not say.
pub let dialect : Dialect = Python
///|
/// Build flags, leaving unset switches at their preset.
///
/// A flag that is only meaningful in one dialect is accepted everywhere and
/// refused by the parser when the grammar cannot honour it — `verbose` under
/// ECMA-262, for instance — so that the mistake is named where it happens.
pub fn Flags::new(
ignorecase~ : Bool = false,
multiline~ : Bool = false,
dotall~ : Bool = false,
verbose~ : Bool = false,
ascii~ : Bool = false,
unicode~ : Bool = true,
unicode_sets~ : Bool = false,
fullcase~ : Bool = false,
bestmatch~ : Bool = false,
enhancematch~ : Bool = false,
posix~ : Bool = false,
reverse~ : Bool = false,
word~ : Bool = false,
version~ : Version = V0,
) -> Flags {
{
ignorecase,
multiline,
dotall,
verbose,
ascii,
unicode,
unicode_sets,
fullcase,
bestmatch,
enhancematch,
posix,
reverse,
word,
version,
}
}
///|
/// Why a pattern, or a run of one, was refused.
///
/// The word is deliberate: a pattern is input, and this library is fed
/// patterns it did not write. Refusals say what was wrong and where; the
/// step ceiling says how far the run got before giving up.
pub(all) suberror Refused {
/// The pattern is not the grammar it claims: `at` is the code point offset
/// reading stopped at, `line` and `column` are where that is in the text.
Pattern(msg~ : String, at~ : Int32, line~ : Int32, column~ : Int32)
/// The run passed `limit` steps and stopped; `got` is how far it got.
Exceeded(limit~ : Int32, got~ : Int32)
/// A call needed one group out of a pattern that has several, and none was
/// named — `findall` with two groups or more, for instance.
Ambiguous(groups~ : Int32)
/// `(*ACCEPT)`: the pattern asked for the match to end here, successfully.
/// Carries the match's start and end so the caller can build it.
Accept(start~ : Int32, end~ : Int32)
/// `(*COMMIT)` / `(*FENCE)`: the whole match fails, no further search.
Commit
/// `(*PRUNE)`: abort this starting position and advance the search.
Prune
/// `(*SKIP)`: fail and resume the search at `target` (the last mark, or the
/// search start when no mark was set).
Skip(target~ : Int32)
} derive(Eq, Debug)
///|
pub extend Refused with Eq::{equal, not_equal}
///|
pub extend Refused with Debug::{to_repr}
///|
pub extend Refused with Show::{output, to_string}
///|
pub impl Show for Refused with fn output(self, logger) {
logger.write_string(
match self {
Pattern(msg~, at~, line~, column~) =>
"\{msg} at position \{at} (line \{line}, column \{column})"
Exceeded(limit~, got~) => "match exceeded \{limit} steps (got \{got})"
Ambiguous(groups~) =>
"pattern has \{groups} groups; pass group= to say which one to take"
Accept(start~, end~) => "match accepted at \{start}..\{end}"
Commit => "match committed: no match is possible"
Prune => "match pruned at this position"
Skip(target~) => "match skipped to \{target}"
},
)
}