regex
regular expressions (multi-engine)
use regexThree engines with different guarantees, chosen by construction via sub-namespace: RE2 (default, linear time, no ReDoS), PCRE (backtracking, complete features, ReDoS-prone), HyperScan (multi-pattern + streaming, its own surface). A common core of operations (
is_match/find/captures/replace/split) plus the per-engine extras. A literal pattern compiles in comptime (validates + embeds; invalid = compile error); a runtime pattern compiles at runtime. The engine is heavy and volatile, hence tier 1; the RE2 algorithm is frozen (tier 0), packaged in a lib that evolves.
Construction and engines
Section titled “Construction and engines”The engine is part of the construction, not a parameter afterward. regex.compile is the shortcut for the default (RE2);
the others come from the sub-namespaces. RE2 and PCRE return the same Regex type (common core); HyperScan
works differently and has its own surface.
regex.compile(pattern) → Regex via RE2 (the default)regex.re2.compile(pattern) → Regex via RE2 (explicit)regex.pcre.compile(pattern) → Regex via PCREregex.hyperscan.compile(pats) → Scanner via HyperScan (see "HyperScan", below)Signature (identical in regex.compile/re2.compile/pcre.compile; only the engine that validates and matches changes):
fn compile(pattern: string) -> Result[Regex, RegexError] // RE2 (default)fn compile_with(pattern: string, opts: Options) -> Result[Regex, RegexError]
alias RegexError = error{ Malformed, // invalid pattern syntax Unsupported, // feature absent in the chosen engine (backref/lookaround in RE2) TooLarge // pattern exceeds the engine's limits (RE2 has a program cap)}Comptime when the pattern is known
Section titled “Comptime when the pattern is known”A literal pattern compiles in comptime: it validates (an invalid literal becomes a compile error, not a runtime
Err) and embeds the compiled program in the binary (zero compilation cost at runtime). That is why a
literal can be .or_panic() without risk: comptime already proved it matches, or the build failed. A runtime
pattern compiles at runtime and returns Result. (It mirrors the const/string story, §16: the literal is
comptime-in-source; the binding decides.)
const WORD := regex.compile("[a-z_][a-z0-9_]*").or_panic() // comptime: validates + embeds; never panicsuser_re := regex.compile(padrao_do_usuario) catch |e| { return e } // runtime: Result handled normallyA feature not supported by the engine follows the same bar: a known pattern gives a compile error; a runtime
pattern gives Err(Unsupported). (regex.compile("(a)\\1"), a backreference, fails in RE2; regex.pcre.compile
accepts it.)
Common core (Regex, RE2 and PCRE)
Section titled “Common core (Regex, RE2 and PCRE)”decl Regex { ... }fn (r: Regex) is_match(text: string) -> boolfn (r: Regex) find(text: string) -> Optional[Match] // 1st matchfn (r: Regex) find_all(text: string) -> Iterator[Match] // all (lazy)fn (r: Regex) captures(text: string) -> Optional[Captures] // 1st match with groupsfn (r: Regex) captures_all(text: string) -> Iterator[Captures]fn (r: Regex) replace(text: string, replacement: string) -> string // only the 1stfn (r: Regex) replace_all(text: string, replacement: string) -> string // allfn (r: Regex) split(text: string) -> Iterator[string]fn (r: Regex) pattern() -> string // the source patternIn replacement, groups are referenced by $1 (numbered) and ${name} (named); $$ is a literal $.
Match and Captures
Section titled “Match and Captures”Each Match carries the matched slice (view of the input) and the byte range:
decl Match { pub text: string // the matched slice (slice/view of the input) pub start: usize // starting byte offset pub end: usize // ending byte offset (exclusive)}
decl Captures { ... }fn (c: Captures) get(i: usize) -> Optional[Match] // numbered group (0 = the whole match)fn (c: Captures) get(name: string) -> Optional[Match] // named group, dispatch by type (§13)fn (c: Captures) len() -> usize // number of groupsfn (c: Captures) iter() -> Iterator[Optional[Match]] // groups in order (none = non-participating)get is one name with two argument types (§13): caps.get(1) (numbered) and caps.get("ano") (named).
An optional group that did not participate in the match is none, hence the Optional.
re := regex.compile("(?P<ano>\\d{4})-(?P<mes>\\d{2})").or_panic()match re.captures("2026-06") { // captures → Optional none => { return } caps => { ano := caps.get("ano").or_panic().text } // "2026"}Options (Unicode opt-in, ASCII-fast default)
Section titled “Options (Unicode opt-in, ASCII-fast default)”decl Options { pub unicode: bool // true: classes/`.` over codepoints + Unicode tables; false: ASCII/bytes pub case_insensitive: bool pub multi_line: bool // ^/$ match at line breaks pub dot_all: bool // `.` matches '\n' pub swap_greed: bool // inverts greedy/lazy}unicode is false by default (ASCII-fast, consistent with unicode/strings being opt-in/heavy): . and
\w/\d operate on bytes. Turning on unicode, . matches a codepoint and the classes use the Unicode
tables (which then weigh). You choose the cost at construction, instead of the language choosing for you.
HyperScan: multi-pattern and streaming (its own surface)
Section titled “HyperScan: multi-pattern and streaming (its own surface)”HyperScan has distinct semantics: matching N patterns simultaneously over a stream, with a callback
per hit (an IDS/DPI tool, not general regex). Hence Scanner, not Regex, and it is the streaming
engine (its reason to exist):
decl Scanner { ... }fn hyperscan.compile(patterns: []string) -> Result[Scanner, RegexError] // many patterns → one Scannerfn hyperscan.compile_ids(patterns: []Pattern) -> Result[Scanner, RegexError] // with an id per pattern
decl Pattern { pub expr: string; pub id: u32; pub opts: Options }
fn (s: Scanner) scan(text: string, on_match: fn(MatchEvent)) // over a stringfn (s: Scanner) scan_stream(r: Readable, on_match: fn(MatchEvent)) -> error{Io} // over a stream (streaming)
decl MatchEvent { pub id: u32 // which pattern matched (the Pattern's id; index in the list when without id) pub start: usize // offset in the stream pub end: usize}scan_stream over a Readable is the match over a stream: HyperScan matches incrementally without materializing
the input. (RE2 does partial streaming, incremental match/no-match, without full captures, noted as a
future direction in Regex; PCRE is string-only, does not stream.)
Curation
Section titled “Curation”- Multi-engine, with the engine chosen at construction (
regex.re2/pcre/hyperscan;regex.compileis RE2). The three have different guarantees, and a singleRegexpretending they are the same breaks (backref compiles in PCRE, fails in RE2). A common core plus per-engine extras, not a denominator that lies. - RE2 default, with a linear guarantee (no ReDoS): an untrusted input pattern is safe. The honest cost is
going without backreference/lookaround (
Unsupported). It is the safety differential, so it is the default. - PCRE, with complete features but ReDoS-prone: backref/lookaround/recursion, at the cost of exponential blow-up. Only with a trusted pattern. Same API; what changes is what compiles and the time guarantee.
- HyperScan, its own surface (multi-pattern + streaming,
Scanner/callback), because it has other semantics (matching N over a stream), not general regex. It is where the match overReadablelives. - Configurable Unicode at construction, ASCII-fast by default:
Options.unicodeturns on codepoints + tables; the default is ASCII/bytes. The tables only weigh when you opt in (opt-in, likeunicode). - Comptime if known: a literal compiles in comptime (validates + embeds; invalid = compile
error), so
.or_panic()on a literal never panics; runtime compiles at runtime (Result). - Captures by type dispatch:
get(i)/get(name), one name and two argument types (§13). EachMatchcarries the slice plus the byte range; a non-participating group isnone. - No allocator in the signatures (colorless);
find_all/split/captures_allare lazyIterator;replaceallocates via the context’s@mm.