Skip to content

Regular Expressions, JavaScript syntax (std/re) ​

milo
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 ​

milo
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 ​

milo
pub struct Re

A compiled pattern. Plain data: share it freely.

Fields:

  • prog: Vec<ReInst>
  • classes: Vec<ReClass>
  • groups: i64
  • names: Vec<string>
  • nameGroups: Vec<i64>
  • slotCount: i64: Loop-progress registers live after the capture slots.
  • icase: bool
  • multiline: bool
  • dotAll: bool
  • source: string
  • firsts: 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 in firsts of its preferred branch's filter, or -1 (no filter); -1 for every other instruction.

Re.canStartAt ​

milo
fn Re.canStartAt(self: &Re, s: &string, sp: i64): bool

Whether 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 ​

milo
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 ​

milo
fn Re.cpAt(self: &Re, s: &string, sp: i64): i64

The codepoint that starts at byte sp of s.

Re.expand ​

milo
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 ​

milo
fn Re.find(self: &Re, s: &string): Option<ReMatch>

The first match (JS s.match(re) without g).

Re.findAll ​

milo
fn Re.findAll(self: &Re, s: &string): Vec<ReMatch>

Every non-overlapping match, left to right (JS s.matchAll(re)).

Re.findFrom ​

milo
fn Re.findFrom(self: &Re, s: &string, from: i64): Option<ReMatch>

The first match starting at or after byte offset from.

Re.firstFrom ​

milo
fn Re.firstFrom(self: &Re, start: i64): ReFirst

Walk 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 ​

milo
fn Re.firstOk(self: &Re, f: i64, s: &string, sp: i64): bool

Whether 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 ​

milo
fn Re.isWordAt(self: &Re, s: &string, sp: i64): bool

Whether the codepoint at sp is a \w character (false at the end of input).

Re.isWordBefore ​

milo
fn Re.isWordBefore(self: &Re, s: &string, sp: i64): bool

Whether the codepoint ending at sp is a \w character (false at the start).

Re.matchFrom ​

milo
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 ​

milo
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 ​

milo
fn Re.prevCp(self: &Re, s: &string, sp: i64): i64

Start of the codepoint that ends at sp, for \b and lookbehind.

Re.replace ​

milo
fn Re.replace(self: &Re, s: &string, repl: &string): string

Replace the first match (JS s.replace(re, repl)).

Re.replaceAll ​

milo
fn Re.replaceAll(self: &Re, s: &string, repl: &string): string

Replace every match (JS s.replaceAll(re, repl) / replace with g).

Re.run ​

milo
fn Re.run(self: &Re, vm: &mut ReVm, s: &string, startPc: i64, startSp: i64, mustEnd: i64): i64

Run 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 ​

milo
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 ​

milo
fn Re.split(self: &Re, s: &string): Vec<string>

Split around every match (JS s.split(re), without captured groups spliced in).

Re.test ​

milo
fn Re.test(self: &Re, s: &string): bool

Whether the pattern matches anywhere in s (JS re.test(s)).

Re.testBounded ​

milo
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 ​

milo
pub struct ReMatch

One 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 ​

milo
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 ​

milo
fn ReMatch.named(self: &ReMatch, name: &string): Option<string>

A named group's text: (?<year>\d{4}) → m.named("year").

ReMatch.text ​

milo
fn ReMatch.text(self: &ReMatch): string

The whole matched text.