Module regex/match

regex/match
Stability: unstable — it is one of the four public `std/regex` names and its accessors are settled, but two shapes are still open. `group(i)` and `named_group(n)` return `.None` both for "that group exists and did not participate" and for "there is no such group", so a caller cannot tell a typo'd group name from an optional group that did not match; Rust's `Captures::name` has the same conflation but its `len()` lets you rule the second out, and `group_count()` here counts only the groups the pattern declared. `named_group` is also a LINEAR scan of the name table on every call, which is fine for a handful of names and wrong for a pattern with dozens. Freezing waits on deciding whether the two failure modes separate. — stable modules only change additively; this one may still change.

Match result type — represents the result of a regex match, including the matched text, position, and captured groups.

Every offset a RegexMatch reports is a BYTE offset into the input, on a UTF-8 boundary (D4, 2026-08-26 — they used to be rune indices), so they can be fed straight into String.substring / index_of. See docs/en-US/STRINGS.md.

A match is EAGER and self-contained: it holds the matched text, the whole input, every group's text and every group's span, all captured when the VM reported success. That is why there is no separate captures method the way Rust splits Regex::find from Regex::captures — Yo's find already pays for the groups, so a second entry point would buy nothing. It also means the match keeps the input string alive for as long as it lives.

Group numbering is 1-based, with group 0 meaning the whole match, matching the $1/\1 vocabulary of the patterns themselves.

Stability

unstable — it is one of the four public std/regex names and its accessors are settled, but two shapes are still open. group(i) and named_group(n) return .None both for "that group exists and did not participate" and for "there is no such group", so a caller cannot tell a typo'd group name from an optional group that did not match; Rust's Captures::name has the same conflation but its len() lets you rule the second out, and group_count() here counts only the groups the pattern declared. named_group is also a LINEAR scan of the name table on every call, which is fine for a handful of names and wrong for a pattern with dozens. Freezing waits on deciding whether the two failure modes separate.

Types

RegexMatch object
RegexMatch

A single regex match result.

Fields

NameTypeDescription
_valueString
_indexusize
_endusize
_inputString
_groupsArrayList(Option(String))
_group_spansArrayList(Option(Range(usize)))
_group_namesArrayList(GroupNameEntry)
impl(RegexMatch, ...)
new : (RegexMatch) fn(value : String, index : usize, end : usize, input : String, groups : ArrayList(Option(String)), group_spans : ArrayList(Option(Range(usize))), group_names : ArrayList(GroupNameEntry)) -> RegexMatch

Assemble a match. Called only by the engine (Regex.find and the iterators) after the VM reports success — every argument is a slot the VM already computed, so nothing here validates or re-derives anything.

Parameters

NameTypeNotesDescription
valueString

The matched text itself — the same bytes as input().substring(index(), end()), captured at match time.

indexusize

Start position of the match as a byte offset into the input.

Basis change (D4, 2026-08-26): this used to be a character (rune) index; it is now a byte index, matching String's byte-indexed API. The offset is always on a UTF-8 character boundary (a valid pattern cannot match at a continuation byte), so it can be fed directly to String.substring / index_of on the input. Use input().substring(usize(0), m.index()).chars().count() if a rune count is really needed.

endusize

End position of the match as a byte offset into the input (exclusive — the offset of the first byte AFTER the match). Always on a UTF-8 character boundary, for the same reason as index(). span() is index() .. end().

inputString

The whole string the match was found in, not just the matched part. The match holds a reference to it, so the offsets index() / end() / span() / group_span() stay meaningful for as long as the match does.

groupsArrayList(Option(String))
group_spansArrayList(Option(Range(usize)))
group_namesArrayList(GroupNameEntry)

Returns: RegexMatch

value : (RegexMatch) fn(self : RegexMatch) -> String

The matched text itself — the same bytes as input().substring(index(), end()), captured at match time.

Parameters

NameTypeNotes
selfRegexMatch

Returns: String

index : (RegexMatch) fn(self : RegexMatch) -> usize

Start position of the match as a byte offset into the input.

Basis change (D4, 2026-08-26): this used to be a character (rune) index; it is now a byte index, matching String's byte-indexed API. The offset is always on a UTF-8 character boundary (a valid pattern cannot match at a continuation byte), so it can be fed directly to String.substring / index_of on the input. Use input().substring(usize(0), m.index()).chars().count() if a rune count is really needed.

Parameters

NameTypeNotes
selfRegexMatch

Returns: usize

end : (RegexMatch) fn(self : RegexMatch) -> usize

End position of the match as a byte offset into the input (exclusive — the offset of the first byte AFTER the match). Always on a UTF-8 character boundary, for the same reason as index(). span() is index() .. end().

Parameters

NameTypeNotes
selfRegexMatch

Returns: usize

span : (RegexMatch) fn(self : RegexMatch) -> Range(usize)

The whole match as a byte range index() .. end() into the input.

Parameters

NameTypeNotes
selfRegexMatch

Returns: Range(usize)

input : (RegexMatch) fn(self : RegexMatch) -> String

The whole string the match was found in, not just the matched part. The match holds a reference to it, so the offsets index() / end() / span() / group_span() stay meaningful for as long as the match does.

Parameters

NameTypeNotes
selfRegexMatch

Returns: String

group : (RegexMatch) fn(self : RegexMatch, idx : usize) -> Option(String)

The text of capture group idx, 1-based, with 0 meaning the whole match — the same numbering $1 and \1 use in patterns.

.None means EITHER that the group did not participate in this match (an unmatched (x)?, or an alternative that was not taken) OR that idx is past group_count(). The two are not distinguishable here; compare idx against group_count() first if that matters.

Parameters

NameTypeNotes
selfRegexMatch
idxusize

Returns: Option(String)

group_span : (RegexMatch) fn(self : RegexMatch, idx : usize) -> Option(Range(usize))

Capture group's byte range into the input (1-based, mirroring group(); group 0 is the full match's span()), or .None when the group did not participate in the match or the index is out of range. A participating group's range satisfies input().substring(r.start, r.end) == group(idx).unwrap().

Parameters

NameTypeNotes
selfRegexMatch
idxusize

Returns: Option(Range(usize))

named_group : (RegexMatch) fn(self : RegexMatch, name : String) -> Option(String)

The text of the group declared as (?<name>...), or .None when no group has that name OR the named group did not participate — the same conflation as group.

O(number of named groups): the name table is scanned linearly on every call, so a loop over many names over a long input is quadratic. Names are compared for exact equality, case-sensitively.

Parameters

NameTypeNotes
selfRegexMatch
nameString

Returns: Option(String)

group_count : (RegexMatch) fn(self : RegexMatch) -> usize

How many capture groups the PATTERN declared, not counting group 0 and not counting how many actually participated. So a non-participating group is still counted, and group(i) is a legal call for every 1 <= i <= group_count().

Parameters

NameTypeNotes
selfRegexMatch

Returns: usize