Regular Expressions, JavaScript syntax (std/re)
from "std/re" import { Re, ReMatch }Re compiles a pattern written the way JavaScript writes it (the text between the slashes of /.../flags) and matches it in pure Milo: no libc, the same behavior on every platform, and a compiled Re is plain data any task or thread can share. It supports classes and \d \w \s \b, groups, named groups, lookahead and lookbehind, backreferences, lazy quantifiers, and the i, m and s flags. Matching is by codepoint, so . and [é-ü] see whole characters.
Use std/regex only when you need POSIX semantics; for patterns ported from JavaScript, or anything with lookaround, use this module.
Example
from "std/re" import { Re }
fn main() {
let bot = Re.new("(?<! cu)bots?(?:\\b|_)|crawl|spider", "i")!
print(bot.test("Mozilla/5.0 (compatible; Googlebot/2.1)")) // true
let date = Re.new("(?<y>\\d{4})-(?<m>\\d{2})", "")!
let m = date.find("released 2026-10")!
print(m.named("y") ?? "") // 2026
print(date.replaceAll("2026-10 and 2025-01", "$<m>/$<y>")) // 10/2026 and 01/2025
}The matcher backtracks, as JavaScript's does, but over an explicit stack (input length never deepens the call stack) and under a step budget proportional to the input. A pathological pattern on hostile input, such as (a+)+$ against "aaaa…b", reports no match in bounded time instead of hanging. testBounded tells "no match" apart from "gave up".
API reference
Re
pub struct ReA compiled pattern. Plain data: share it freely.
Fields:
prog: Vec<ReInst>classes: Vec<ReClass>groups: i64names: Vec<string>nameGroups: Vec<i64>slotCount: i64: Loop-progress registers live after the capture slots.icase: boolmultiline: booldotAll: boolsource: stringfirsts: Vec<ReFirst>: Which bytes can begin a match (firsts[0]) or a branch, so the scan skips start positions and alternatives that cannot.splitFirst: Vec<i64>: Per instruction: for a SPLIT, the index infirstsof its preferred branch's filter, or -1 (no filter); -1 for every other instruction.
Re.canStartAt
fn Re.canStartAt(self: &Re, s: &string, sp: i64): boolWhether any match can begin at byte sp; the scan skips start positions where this is false. The end of input always qualifies (an empty match may sit there).
Re.computeFirst
fn Re.computeFirst(self: &mut Re)A filter for the whole pattern and one per SPLIT's preferred branch: a 200-way alternation (a bot list) then tries only the branches whose first character is the one under the cursor.
Re.cpAt
fn Re.cpAt(self: &Re, s: &string, sp: i64): i64The codepoint that starts at byte sp of s.
Re.expand
fn Re.expand(self: &Re, s: &string, m: &ReMatch, repl: &string, out: &mut string)Expand $1, $<name>, $&, $$, and $\`` / $'(the subject before and after the match) in a replacement for one match ofs`.
Re.find
fn Re.find(self: &Re, s: &string): Option<ReMatch>The first match (JS s.match(re) without g).
Re.findAll
fn Re.findAll(self: &Re, s: &string): Vec<ReMatch>Every non-overlapping match, left to right (JS s.matchAll(re)).
Re.findFrom
fn Re.findFrom(self: &Re, s: &string, from: i64): Option<ReMatch>The first match starting at or after byte offset from.
Re.firstFrom
fn Re.firstFrom(self: &Re, start: i64): ReFirstWalk the zero-width edges from start and collect what the first consumed codepoint can be. Assertions (^ $ \b lookaround) do not consume, so they are stepped over: they only narrow where a match starts, never widen it, which keeps the set a superset of the truth (a filter, never a verdict).
Re.firstOk
fn Re.firstOk(self: &Re, f: i64, s: &string, sp: i64): boolWhether a match path whose first-codepoint filter is firsts[f] can begin at byte sp of s. A filter, never a verdict: true only means the matcher must try.
Re.isWordAt
fn Re.isWordAt(self: &Re, s: &string, sp: i64): boolWhether the codepoint at sp is a \w character (false at the end of input).
Re.isWordBefore
fn Re.isWordBefore(self: &Re, s: &string, sp: i64): boolWhether the codepoint ending at sp is a \w character (false at the start).
Re.matchFrom
fn Re.matchFrom(self: &Re, s: &string, from: i64, budgetHit: &mut bool): Option<ReMatch>The leftmost match starting at byte from or later, or None. Sets budgetHit when the step budget ran out first, so a caller can tell "gave up" from "no match".
Re.new
fn Re.new(pattern: &string, flags: &string): Result<Re, string>Compile pattern (the text between the slashes of a JS literal) with flags ("i", "m", "s"; "g", "u", "y" are accepted and ignored). The error names the byte offset of the problem.
Re.prevCp
fn Re.prevCp(self: &Re, s: &string, sp: i64): i64Start of the codepoint that ends at sp, for \b and lookbehind.
Re.replace
fn Re.replace(self: &Re, s: &string, repl: &string): stringReplace the first match (JS s.replace(re, repl)).
Re.replaceAll
fn Re.replaceAll(self: &Re, s: &string, repl: &string): stringReplace every match (JS s.replaceAll(re, repl) / replace with g).
Re.run
fn Re.run(self: &Re, vm: &mut ReVm, s: &string, startPc: i64, startSp: i64, mustEnd: i64): i64Run from pc at sp until MATCH (or, for a lookaround body, LOOKEND). Returns the end position, or -1. mustEnd >= 0 requires the body to end exactly there (a lookbehind tried from an earlier start). Slot writes made on the way to a success stay; on failure every write is undone back to the barrier this call pushed.
Re.setSlot
fn Re.setSlot(self: &Re, vm: &mut ReVm, slot: i64, v: i64)Write capture/loop register slot, recording the old value on the backtrack stack so a failed path restores it.
Re.split
fn Re.split(self: &Re, s: &string): Vec<string>Split around every match (JS s.split(re), without captured groups spliced in).
Re.test
fn Re.test(self: &Re, s: &string): boolWhether the pattern matches anywhere in s (JS re.test(s)).
Re.testBounded
fn Re.testBounded(self: &Re, s: &string): Option<bool>Whether the last step budget was exceeded is not tracked per Re (a Re is shared read-only); testBounded reports it for a caller that must tell "no" from "gave up".
ReMatch
pub struct ReMatchOne match: byte offsets into the searched text, and the text of each group (groups[0] is the whole match; an unmatched group is None).
Fields: start: i64, end: i64, groups: Vec<Option<string>>, names: Vec<string>, nameGroups: Vec<i64>.
ReMatch.group
fn ReMatch.group(self: &ReMatch, i: i64): Option<string>Group i's text; None when the group did not take part or does not exist.
ReMatch.named
fn ReMatch.named(self: &ReMatch, name: &string): Option<string>A named group's text: (?<year>\d{4}) → m.named("year").
ReMatch.text
fn ReMatch.text(self: &ReMatch): stringThe whole matched text.