Module regex/parser
Regex pattern parser — parses a regex pattern string into an AST
of RegexNode objects. Uses an iterative stack-based approach.
Internal to std/regex. Nothing here is public API: import
std/regex and use Regex. This module's shape changes freely — the one
piece of it users see is RegexError, which std/regex re-exports.
Why iterative
Nesting in a pattern is unbounded (((((a))))), and a recursive-descent
parser turns that into unbounded native stack. parse keeps an explicit
ArrayList(ParseFrame) instead: ( pushes the enclosing frame and starts a
fresh one, ) pops and folds the finished group into its parent's current
sequence. So pattern depth costs heap, not stack, and a deeply nested
pattern from untrusted input cannot overflow.
Alternation is handled by the same frames rather than by precedence: |
closes the current sequence into the frame's alternatives list and starts
a new one, and the frame is folded into right-nested binary
RegexNode.alternation nodes when it closes. That is why a|b|c becomes
alternation(a, alternation(b, c)) and not a three-child node.
Positions are BYTE offsets into the pattern throughout — _pos indexes
_bytes — and every RegexError reports one. Bytes outside ASCII are
decoded as runes only where the grammar cares (literals, class members);
the structural scan is byte-wise, which is safe because every metacharacter
is ASCII and UTF-8 is self-synchronising.
Stability
unstable, and deliberately not public. Its output type (RegexNode) is
internal and its input grammar is still growing — every construct added
(inline (?i) flag groups, \Q...\E, atomic groups, named-class
shorthands) changes both NodeKind and RegexError. RegexError is the
one piece that escapes, and its own ## Stability section explains why an
exhaustively-matched error enum cannot be frozen while the grammar moves.
Types
A parser over one pattern: the byte cursor plus the capture-numbering state that outlives the parse.
SINGLE USE — parse consumes the cursor to the end and the group counters
are cumulative, so a second parse on the same instance would return an
empty pattern with the previous run's group table. Build one per pattern,
call parse, then read group_count() / group_names() off it.
Fields
| Name | Type | Description |
|---|---|---|
_source | String | |
_bytes | ArrayList(u8) | |
_pos | usize | |
_group_count | usize | |
_group_names | ArrayList(GroupNameEntry) |
impl(RegexParser, ...)
new : (RegexParser) fn(pattern : String) -> RegexParserA parser positioned at the start of pattern, with no groups seen yet.
Parameters
| Name | Type | Notes |
|---|---|---|
pattern | String |
Returns: RegexParser
_peek : (RegexParser) fn(self : RegexParser) -> Option(u8)_advance : (RegexParser) fn(self : RegexParser) -> Option(u8)_read_codepoint : (RegexParser) fn(self : RegexParser, first : u8) -> u32_at_end : (RegexParser) fn(self : RegexParser) -> boolgroup_count : (RegexParser) fn(self : RegexParser) -> usizeHow many capturing groups the pattern declared, not counting group 0.
Only meaningful AFTER parse — it counts groups as they are opened, so
reading it early reports a partial count. compile uses it to size the
VM's capture array.
Parameters
| Name | Type | Notes |
|---|---|---|
self | RegexParser |
Returns: usize
group_names : (RegexParser) fn(self : RegexParser) -> ArrayList(GroupNameEntry)The (?<name>...) declarations, in the order they were opened. Valid
after parse, for the same reason as group_count.
Returns the parser's own list, not a copy — it travels into the compiled
program and out to RegexMatch.named_group, and nobody mutates it after
the parse.
Parameters
| Name | Type | Notes |
|---|---|---|
self | RegexParser |
Returns: ArrayList(GroupNameEntry)
_lookup_group_name : (RegexParser) fn(self : RegexParser, name : String) -> Option(usize)_escape_char_codepoint : (RegexParser) fn(self : RegexParser, ch : u8) -> u32_parse_number : (RegexParser) fn(self : RegexParser) -> Option(usize)_parse_greedy_modifier : (RegexParser) fn(self : RegexParser) -> bool_make_digit_ranges : (RegexParser) fn(self : RegexParser) -> ArrayList(CharRange)_make_word_ranges : (RegexParser) fn(self : RegexParser) -> ArrayList(CharRange)_make_space_ranges : (RegexParser) fn(self : RegexParser) -> ArrayList(CharRange)_parse_hex_byte : (RegexParser) fn(self : RegexParser) -> Option(u32)_parse_class_unwind : (RegexParser) fn(self : RegexParser) -> Result(ArrayList(CharRange), RegexError)_try_parse_char_range : (RegexParser) fn(self : RegexParser, ranges : ArrayList(CharRange), low : u32) -> unit_parse_char_class_content : (RegexParser) fn(self : RegexParser, ranges : ArrayList(CharRange)) -> Result(unit, RegexError)Parameters
| Name | Type | Notes |
|---|---|---|
self | RegexParser | |
ranges | ArrayList(CharRange) |
Returns: Result(unit, RegexError)
_parse_char_class : (RegexParser) fn(self : RegexParser) -> Result(RegexNode, RegexError)_parse_unicode_property : (RegexParser) fn(self : RegexParser, negated : bool) -> Result(RegexNode, RegexError)_parse_unwind : (RegexParser) fn(self : RegexParser) -> Result(RegexNode, RegexError)_parse_counted_quantifier : (RegexParser) fn(self : RegexParser, atom : RegexNode) -> Result(RegexNode, RegexError)_parse_atom : (RegexParser) fn(self : RegexParser) -> Result(RegexNode, RegexError)_maybe_quantify : (RegexParser) fn(self : RegexParser, a : RegexNode) -> Result(RegexNode, RegexError)_parse_quantified : (RegexParser) fn(self : RegexParser) -> Result(RegexNode, RegexError)impl(RegexParser, ...)
_make_sequence : (RegexParser) fn(self : RegexParser, nodes : ArrayList(<struct:struct_decl_1053484_file____home_runner_work_Yo_Yo_std_regex_node_yo>)) -> RegexNodeParameters
| Name | Type | Notes |
|---|---|---|
self | RegexParser | |
nodes | ArrayList(<struct:struct_decl_1053484_file____home_runner_work_Yo_Yo_std_regex_node_yo>) |
Returns: RegexNode
_finalize_frame : (RegexParser) fn(self : RegexParser, alts : ArrayList(ArrayList(<struct:struct_decl_1053484_file____home_runner_work_Yo_Yo_std_regex_node_yo>)), seq : ArrayList(<struct:struct_decl_1053484_file____home_runner_work_Yo_Yo_std_regex_node_yo>)) -> RegexNodeParameters
| Name | Type | Notes |
|---|---|---|
self | RegexParser | |
alts | ArrayList(ArrayList(<struct:struct_decl_1053484_file____home_runner_work_Yo_Yo_std_regex_node_yo>)) | |
seq | ArrayList(<struct:struct_decl_1053484_file____home_runner_work_Yo_Yo_std_regex_node_yo>) |
Returns: RegexNode
parse : (RegexParser) fn(self : RegexParser) -> Result(RegexNode, RegexError)Parse the whole pattern into an AST, or report the first fault.
One left-to-right pass with the explicit frame stack described in the
module header — no backtracking and no second pass, so the FIRST error
wins and later faults in the same pattern are never seen. Errors carry
the byte offset the parser had reached, which is not always where the
construct began (a ( opened at 0 and never closed reports the end of
the pattern).
Backreferences are validated here rather than at compile time: \1 past
group_count() is InvalidBackreference and \k<name> with no such
name is UnknownGroupName, so the compiler and VM can assume every
Backreference node names a real group.
An empty pattern is legal and parses to an empty Sequence, which
matches the empty string at every position.
Parameters
| Name | Type | Notes |
|---|---|---|
self | RegexParser |
Returns: Result(RegexNode, RegexError)