Modules

Regex

moduleextensionSource

Available on macOS, Linux, Windows, Wasm

#import "Extensions/Regex";

Regular expressions with full Unicode support, whose patterns can be compiled while your program compiles. compile builds one at run time; Static builds one at compile time and types its named groups. find, captures, replace and split work on either.

Example↗
Regex :: #import "Extensions/Regex";

// Compiled while the program compiles: a bad pattern is a compile error, and named
// groups become typed fields (an integer for a run of digits, an enum for a few words).
Date :: Regex.Static("(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})");
date, ok := Regex.parse(Date, "2026-10-11");  // date.year is a u16, date.month a u8

// Compiled at run time, for a pattern only known then.
user_pattern := "[a-z]+";
text := "one two";
re, compiled, error := Regex.compile(user_pattern);
for Regex.find_all(*re, text) print("% ", it.text);

Matching takes time linear in the text for every pattern: there are no backreferences and no look-around, and the engines are automata, never an exponential backtracker.

Procedures#

General#

(pattern: string, options := Options.{}) -> (Regex, ok: bool, Error)

Compiles pattern. The result says whether it was valid and, if not, the error says where it went wrong. Free the regex with free_regex.

Regex :: #import "Extensions/Regex";

re, ok, error := Regex.compile("[a-z]+[0-9]");
assert(ok);
Regex.free_regex(*re);

_, ok, error = Regex.compile("(unclosed");
assert(!ok && error.message.count > 0);
↗
(re: *Regex) -> Regex

A regex for another thread: it shares the compiled pattern (parse, programs and one-pass table) with re and has a search cache of its own, so it costs far less than a second compile. Free it with free_regex, like any other regex.

Regex :: #import "Extensions/Regex";

re, _ := Regex.compile("[0-9]+");
copy := Regex.clone(*re);
Regex.free_regex(*re);   // the clone keeps the pattern alive
assert(Regex.is_match(*copy, "room 101"));
Regex.free_regex(*copy);
↗
(re: *Regex)

Releases everything compile allocated for the regex: the parsed pattern, both programs and the search tables. For a regex made by clone, that is its own search cache, and the pattern goes when the last regex sharing it is freed.

Regex :: #import "Extensions/Regex";

re, ok := Regex.compile("a+b");
assert(ok && Regex.is_match(*re, "caab"));
Regex.free_regex(*re);
assert(re.engine == null);
↗
(re: *Regex) -> bool
($R: Type) -> bool

Whether captures for this pattern are found in a single scan, which makes captures faster.

Regex :: #import "Extensions/Regex";

re, _ := Regex.compile("(a)(b)");
assert(Regex.is_one_pass(*re));
Regex.free_regex(*re);
↗
is_match2 overloadsSource
(re: *Regex, text: string, start := 0) -> bool
($R: Type, text: string, start := 0) -> bool

Whether the pattern matches anywhere in text, starting at or after byte start.

Regex :: #import "Extensions/Regex";

re, _ := Regex.compile("[0-9]+");
assert(Regex.is_match(*re, "room 101"));
assert(!Regex.is_match(*re, "no digits"));
Regex.free_regex(*re);
↗
find2 overloadsSource
(re: *Regex, text: string, start := 0) -> (Match, bool)
($R: Type, text: string, start := 0) -> (Match, bool)

The leftmost match at or after byte start.

Regex :: #import "Extensions/Regex";

re, _ := Regex.compile("[0-9]+");
match, found := Regex.find(*re, "room 101, floor 3");
assert(found && match.text == "101" && match.start == 5);
Regex.free_regex(*re);
↗
find_all2 overloadsSource
(re: *Regex, text: string) -> [..] Match
($R: Type, text: string) -> [..] Match

Every match, left to right, not overlapping. An empty match right where the previous match ended is skipped, and in Unicode mode empty matches never split a UTF-8 sequence.

Regex :: #import "Extensions/Regex";

re, _ := Regex.compile("[0-9]+");
matches := Regex.find_all(*re, "room 101, floor 3");
assert(matches.count == 2 && matches[1].text == "3");
array_free(matches);
Regex.free_regex(*re);
↗
captures2 overloadsSource
(re: *Regex, text: string, start := 0) -> (Captures, bool)
($R: Type, text: string, start := 0) -> (R.Groups, bool)

The leftmost match at or after start, with where each group matched. Read the groups with group and span.

Regex :: #import "Extensions/Regex";

re, _ := Regex.compile("(?<key>[a-z]+)=(?<value>[0-9]+)");
caps, found := Regex.captures(*re, "retries=3");
assert(found);
key, _ := Regex.group(caps, "key");
assert(key == "retries");
Regex.free_regex(*re);
↗
group2 overloadsSource
(caps: Captures, index: int) -> (string, bool)
(caps: Captures, name: string) -> (string, bool)

Group index's text, and whether it took part in the match. Group 0 is the whole match.

Regex :: #import "Extensions/Regex";

re, _ := Regex.compile("(a)(x)?");
caps, _ := Regex.captures(*re, "ab");
first, took_part := Regex.group(caps, 1);
assert(took_part && first == "a");
_, took_part = Regex.group(caps, 2);
assert(!took_part);
Regex.free_regex(*re);
↗
(caps: Captures, index: int) -> (start: int, end: int)

Group index's byte range, or -1, -1.

Regex :: #import "Extensions/Regex";

re, _ := Regex.compile("b+");
caps, _ := Regex.captures(*re, "abbc");
start, end := Regex.span(caps, 0);
assert(start == 1 && end == 3);
Regex.free_regex(*re);
↗
replace2 overloadsSource
(re: *Regex, text: string, replacement: string) -> string
($R: Type, text: string, replacement: string) -> string

Replaces the first match. In replacement, $1 / ${1} is a group's text, $name / ${name} a named group's, and $$ a dollar sign; a group that did not match (or does not exist) gives "". $name takes the longest run of letters, digits and _, so write ${1}st rather than $1st.

Regex :: #import "Extensions/Regex";

re, _ := Regex.compile("(\\w+)@(\\w+)");
assert(Regex.replace(*re, "ada@home, bob@work", "$2:$1") == "home:ada, bob@work");
Regex.free_regex(*re);
↗
(re: *Regex, text: string, replacement: string) -> string
($R: Type, text: string, replacement: string) -> string

Replaces every match; replacement works as in replace.

Regex :: #import "Extensions/Regex";

re, _ := Regex.compile("[0-9]+");
assert(Regex.replace_all(*re, "a1 b22", "#") == "a# b#");
Regex.free_regex(*re);
↗
replace_n2 overloadsSource
(re: *Regex, text: string, replacement: string, limit: int) -> string
($R: Type, text: string, replacement: string, limit: int) -> string

Replaces the first limit matches (all of them when limit < 0).

Regex :: #import "Extensions/Regex";

re, _ := Regex.compile("[0-9]+");
assert(Regex.replace_n(*re, "1 2 3", "#", 2) == "# # 3");
Regex.free_regex(*re);
↗
split2 overloadsSource
(re: *Regex, text: string) -> [..] string
($R: Type, text: string) -> [..] string

The pieces of text between matches.

Regex :: #import "Extensions/Regex";

re, _ := Regex.compile("\\s*,\\s*");
parts := Regex.split(*re, "a , b,c");
assert(parts.count == 3 && parts[1] == "b");
array_free(parts);
Regex.free_regex(*re);
↗
(text: string) -> string

text with every regex metacharacter escaped, so it matches literally.

Regex :: #import "Extensions/Regex";

assert(Regex.escape("1+1=2?") == "1\\+1=2\\?");
↗
($R: Type, text: string) -> (R.Groups, bool)

The whole of text against the pattern, with its named groups parsed.

Regex :: #import "Extensions/Regex";
Time :: Regex.Static("(?<hour>[0-9]{2}):(?<minute>[0-9]{2})");

time, ok := Regex.parse(Time, "09:30");
assert(ok && time.hour == 9 && time.minute == 30);
_, ok = Regex.parse(Time, "at 09:30");
assert(!ok);
↗
($R: Type, text: string, out: *$T) -> bool

Like parse, into your own struct: each field takes the named group of the same name, and is a string, integer, float, bool (true / false) or enum (a member's name, any case). A field with no group or a group with no field is a compile error. False if the text does not match or a group's text does not fit its field.

Regex :: #import "Extensions/Regex";
Time :: Regex.Static("(?<hour>[0-9]{2}):(?<minute>[0-9]{2})");

Clock :: struct { hour: int; minute: int; }

clock: Clock;
assert(Regex.parse_into(Time, "09:30", *clock));
assert(clock.hour == 9 && clock.minute == 30);
↗
($R: Type, text: string, out: *$T, start := 0) -> bool

Like captures, into your own struct (see parse_into).

Regex :: #import "Extensions/Regex";
Time :: Regex.Static("(?<hour>[0-9]{2}):(?<minute>[0-9]{2})");

Clock :: struct { hour: int; minute: int; }

clock: Clock;
assert(Regex.captures_into(Time, "meet at 09:30", *clock));
assert(clock.minute == 30);
↗

Syntax#

Program#

(parsed: *Parsed, reverse: bool) -> (Program, ok: bool, error: string)

Compiles root. reverse builds the program that reads the text backwards (no captures and no unanchored start: it is only run back from the end of a match).

(node: *Node) -> bool

Can only ever match the empty string at a position (a look or nothing), so it can be skipped over when looking for what a concatenation starts with.

NFA#

(look: Look, text: string, pos: int) -> bool

Whether look holds at pos. The whole text is context, so a search that starts mid-text still sees the byte before it.

Onepass#

(program: *Program, state_limit: int) -> OnePass

Builds the table for program (the forward one). ok is false in the result if the pattern is not one-pass or needs more than state_limit states.

DFA#

(d: *Dfa) -> bool

Builds every state, so that no search ever needs the program. False if there are more than the limit.

(d: *Dfa, text: string, start: int, anchored: bool, earliest: bool) -> (end: int, gave_up: bool)

The end of the leftmost-first match that starts at or after start (and exactly at start if anchored), or -1. With earliest, stops at the first position where any match ends, which answers "is there a match" sooner. gave_up means the DFA grew past its limit and the caller has to use another matcher.

(d: *Dfa, text: string, bound: int, end: int) -> (start: int, gave_up: bool)

Run back from end (a reverse DFA): the leftmost position >= bound where a match ending at end can start, or -1.

(d: *Dfa, s: u32, text: string, at: int) -> u32

The step over non-ASCII byte text[at] in a DFA with a Unicode \b, through the column that knows whether a word character starts at at and whether one ends with it.

(d: *Dfa, index: int) -> *Skip

The skip for an unanchored start (computing it the first time, in a lazy DFA).

(skip: *Skip, text: string, i: int, fence: *int) -> int

The next position at or after i where a match could start, or -1.

(skip: *Skip, text: string, at: int) -> bool

Whether the bytes from at can begin a match, as far as the skip's checks tell.

(program: *Program) -> (sets: [..][256] bool, exact: bool)

For each of the first few positions of a match, the bytes it can have there, up to the shortest match's length (looks are taken to hold, which only adds bytes).

(program: *Program) -> (required: [256] bool, before: [256] bool, ok: bool)

A rare set of bytes that every match contains, and the bytes a match can have before the first of them. Tries each ASCII byte together with its other case.

(program: *Program, required: [256] bool) -> ([256] bool, ok: bool)

The bytes a match can consume before its first byte from required; not ok if a match can do without one.

(members: [256] bool) -> float64

A rough share of typical text (mostly English, some UTF-8) that falls in the set.

(pattern: string, options: Options) -> (*Engine, bool, Error)

Builds the engine for pattern. The parse tree, the programs and the engine itself are allocated from a pool of their own (Engine.pool), so free_regex can give them all back at once.

(engine: *Engine, text: string, start: int, earliest := false) -> (start: int, end: int)

The leftmost-first match at or after start: its start and end, or end -1. earliest only promises to say whether there is one (the end may be early and the start is not computed).

(program: *Program) -> int

The length every match has, or -1 if they differ (or the program is too big to tell).

(engine: *Engine, text: string, s: int, e: int, slots: [] int)

Where each group of the match text[s .. e] is.

(the_engine: *Engine, text: string, body: Code)

Runs body for each match with s and e set, following find_all's rules for empty matches.

Generate#

(pattern: string, full: bool) -> Engine

The engine for a constant pattern, built at compile time. A bad pattern is a compile error.

(pattern: string) -> string

Static's check that its pattern parses; returns no code.

(R: Type) -> (bool, string)

The #modify of every procedure that takes a Static type.

(pattern: string) -> ([..] Field_Plan, ok: bool)

One plan per named group, in group order. ok is false if the pattern does not parse (the error is reported where the engine is built).

(node: *Node) -> (string, bool)

The letter a case-insensitive class stands for, written in lower case: (?i)y matches Y and y. Other characters in the class must be outside ASCII (like the Kelvin sign for k).

(word: string) -> string

An identifier in upper snake case, the way enum members are written.

(node: *Node) -> (string, bool)

The text a node always matches exactly, if it is a plain string.

(node: *Node) -> (signed: bool, digits: int, is_float: bool, ok: bool)

Whether a node only matches numbers: an optional sign, digits, and for a float a . and more digits. digits is the most integer digits it can match, -1 for no limit.

(node: *Node) -> (ok: bool, lo: int, hi: int)

Matches only decimal digits; how many (hi -1: no limit).

(pattern: string) -> string

Statements that fill out (a *Groups(pattern)) from text and slots; return false when a number does not fit its field.

(pattern: string, T: Type) -> string

Statements that fill out (a *T the caller declared) from text and slots. Every field must have a named group and every named group a field; anything else is a compile error.

(piece: string, $T: Type) -> (T, bool)

Decimal digits (any script's) with an optional sign, into an integer of type T. False when it does not fit.

(piece: string) -> (float64, bool)

A number like 12.5 or -3 (digits from any script) as a float64.

(text: string, index: int) -> (int, int)

The value of the decimal digit at index and its size in bytes; -1 if it is not one.

Types#

:: struct

Pattern flags, also settable inside the pattern: (?i) (?m) (?s) (?U) (?x) (?R) (?-u).

case_insensitive: bool
i: letters match either case (Unicode simple case folding)
multi_line: bool
m: ^ and $ match at line breaks, not just the ends of the text
dot_all: bool
s: . also matches \n
swap_greed: bool
U: x* is lazy and x*? greedy
ignore_space: bool
x: whitespace is ignored and # starts a comment
crlf: bool
R: with m, \r\n also ends a line, and . does not match \r
unicode: := true
u: classes and \b are Unicode-aware; off, they are ASCII and match bytes
Regex :: #import "Extensions/Regex";

options: Regex.Options;
options.case_insensitive = true;
re, ok := Regex.compile("hello", options);
assert(ok && Regex.is_match(*re, "Say HELLO"));
Regex.free_regex(*re);
↗
:: struct

A compiled pattern from compile. Plain copies of the struct share the compiled program and the search cache, which is not safe to use from two threads at once: give each thread its own clone.

pattern: string
group_count: int
capture groups, not counting group 0 (the whole match)
names: [] string
names[i] is group i's name, or ""
engine: *Engine
:: struct

Why a pattern did not compile.

message: string
unclosed group: expected )``, in temporary storage
offset: int
byte offset in the pattern where the problem is
:: struct

One match: its byte range and a view of the matched text.

start: int
end: int
text: string
the matched text, a view into the searched string
:: struct

Where each group matched. Group 0 is the whole match; a group that did not take part has start -1. Read it with group and span.

text: string
the whole searched string
slots: [] int
start and end of each group, in pairs
names: [] string
:: struct

A regex compiled while your program compiles: a type, made from a constant pattern.

Regex :: #import "Extensions/Regex";
Date :: Regex.Static("(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})");

date, ok := Regex.parse(Date, "2026-10-11");  // a Date.Groups: date.year is a u16
assert(ok && date.year == 2026 && date.month == 10 && date.day == 11);
↗
:: enum
START_TEXT
\A, or ^ without (?m)
END_TEXT
\z, or $ without (?m)
START_LINE
^ with (?m)
END_LINE
$ with (?m)
START_LINE_CRLF
^ with (?mR)
END_LINE_CRLF
$ with (?mR)
WORD
\b
NOT_WORD
\B
WORD_START
\b{start}, <
WORD_END
\b{end}, >
WORD_START_HALF
\b{start-half}
WORD_END_HALF
\b{end-half}
WORD_ASCII
the same six with (?-u): word characters are [0-9A-Za-z_]
NOT_WORD_ASCII
WORD_START_ASCII
WORD_END_ASCII
WORD_START_HALF_ASCII
WORD_END_HALF_ASCII
:: enum
EMPTY
LITERAL
CLASS
LOOK
REPEAT
CAPTURE
CONCAT
ALTERNATE
:: struct
kind: Node_Kind
literal: string
LITERAL: the bytes to match
ranges: [..] Char_Range
CLASS: sorted and merged; code points, or bytes when bytes
bytes: bool
look: Look
min: int
REPEAT: max < 0 means no upper bound
max: int
greedy: bool
index: int
CAPTURE: the group number, from 1
name: string
CAPTURE: "" for an unnamed group
children: [..] *Node
REPEAT and CAPTURE have exactly one
:: struct
root: *Node
groups: int
capture groups, not counting the whole match
names: [..] string
names[i] is group i's name; names[0] is ""
ok: bool
error: string
offset: int
where in the pattern the error is
:: enum
MATCH
RANGES
consume one byte that falls in one of transitions[next .. next + alt]
SPLIT
try next, then alt
SAVE
record the position in slot alt, go on to next
LOOK
go on to next if look holds here
FAIL
:: struct
insts: [] Inst
transitions: [] Transition
start: u32
anchored: tries to match right here
start_unanchored: u32
lets the match begin at any later byte, at lower priority
slot_count: int
2 per group, counting the whole match as group 0
names: [] string
names[i] is group i's name, "" if unnamed
anchored: bool
every match starts at the start of the text (\A)
has_looks: bool
has_unicode_word: bool
\b and friends in Unicode mode, which byte automata can't see
byte_class: [256] u8
bytes that the program never tells apart share a class
class_count: int
class_byte: [256] u8
one byte of each class
:: struct
ok: bool
false: not one-pass, or too big; nothing below is set
stride: int
byte classes
slot_count: int
start: int
premultiplied by stride
byte_class: [256] u8
trans: [] Edge
[state * stride + class]
finals: [] Edge
[state]: next is 1 if the match can end in this state
updates: [] u32
lists of slots to set: a count, then the slots. Entry 0 is the empty list.
:: struct
next: u32
premultiplied by stride; NO_EDGE if the byte is not allowed
updates: u32
index into OnePass.updates
need: u32
bit set of the Looks that must hold before this edge is taken
DfastructSource
:: struct
program: *Program
null for a DFA that was built completely at compile time
reverse: bool
match_all: bool
keep going after a match (the reverse DFA wants the leftmost start)
stride: int
byte classes + 1; the last column is the end of the text
byte_class: [256] u8
trans: [] u32
[state * stride + class]; states are stored premultiplied by stride
starts: [START_COUNT] u32
by START_INDEX(side, anchored)
skips: [START_COUNT] Skip
per unanchored start: how to jump over bytes that stay in it
rows: [..] u32
Only while building:
lists: [..] u32
every state's instructions, back to back
list_start: [..] u32
per state
list_count: [..] u32
left: [..] u8
per state: the kind of byte before it (for unresolved looks)
known: Table(string, u32)
resolved: [..] u32
next: [..] u32
stack: [..] u32
mark_a: [] u32
mark_b: [] u32
generation: u32
state_limit: int
gave_up: bool
unicode_word: bool
The program has a Unicode \b, which needs to see whole characters: a non-ASCII byte's column holds QUIT, and the search decodes the text there and steps through one of the CHARACTER_COLUMNS copies of the byte's column instead, which says whether a word character starts at the byte and whether one ends with it.
classes: int
:: struct

An unanchored start state usually goes back to itself on most bytes: the search is waiting for the first byte of a match. A vector search jumps to the next place a match could start. Without looks, any position will do as a restart (the start state does not depend on the byte before), so the search can use bytes from further into the match, at a fixed distance from its start: two of them, if no single one is rare. When every byte near the start is common, a rare byte that every match must contain will do: find it, then walk back over the bytes a match can have before it to where the match could start. With looks, it can only jump over bytes that keep the DFA in its start state.

computed: bool
state: u32
the start state, or UNKNOWN when nothing is skipped
kind: Skip_Kind
offset: int
from a match's start to the byte searched for (the first of a pair)
gap: int
PAIR and TRIPLE: from the first byte to the second
gap_3: int
TRIPLE: from the first byte to the third
count: int
BYTES: how many of bytes
bytes: [3] u8
second: Byte_Search.Byte_Set
PAIR, TRIPLE: the second byte; REQUIRED: the bytes a match can have before set
third: Byte_Search.Byte_Set
TRIPLE: the third byte
checks: int
BYTES, SET and PAIR without looks: the bytes a match can have at each of its first checks positions, as bit sets, checked before the DFA is given a candidate.
check: [FINGERPRINT_DEPTH][32] u8
exact: bool
Every match is exactly checks bytes, any of the checked bytes at each position: a candidate that fits the checks is the match, and the DFA need not run.
:: struct
pattern: string
program: Program
reverse: Program
forward: Dfa
backward: Dfa
onepass: OnePass
the one-pass capture table, if the pattern has one (onepass.ok)
fixed_length: := -1
every match is this many bytes long: no need to look for the start
pool: *Memory_Pool.Pool
holds the parse tree, the programs and this struct; null for an engine built at compile time
origin: *Engine
Clones (clone) copy the programs and the one-pass table by value, which shares what they point to, and have DFAs of their own.
refs: s64
on the origin: the origin itself plus its live clones; the last one to be freed releases the pool
frozen: bool
built at compile time: both DFAs are complete and the programs are constants
:: enum

What a named group's field holds.

STRING
INTEGER
FLOAT
ENUM
:: struct
name: string
group: int
type_name: string
u16, s64, float64, or the enum's name
members: [..] string
ENUM: the member names, in pattern order
words: [..] string
ENUM: each word of the alternation...
word_members: [..] int
...and the member it becomes
optional: bool
may not take part in a match

Constants#

:: 0xFFFF_FFFF

as in Rust; big counts still fail as too large a program

:: 0xFFFF_FFFE

A non-ASCII byte in a DFA with a Unicode \b: look at the characters around it (see unicode_word).

:: 0

What a byte looks like to a look: the edge of the text, \n, \r, a word byte, anything else.

:: 0.25

A search whose bytes make up more than this share of typical text stops too often to pay.

:: #run -> [256] float64 { share: [256] float64; for b: 0..255 { NON_ASCII_SHARE :: 0.004; UPPER_AND_DIGIT_SHARE :: 0.003; PUNCTUATION_SHARE :: 0.01; SPACE_SHARE : ...
:: 256

Bigger than this and the DFA is built lazily at run time instead of stored in the program.

:: string.["if", "else", "then", "case", "for", "while", "break", "continue", "return", "defer", "using", "cast", "xx", "ifx", "struct", "union", "enum", "enum_fla ...