LEVIATHAN v962456e · 962456eee1

Standard Library

class Regex

A compiled regular expression.

since 0.1.0-alpha.1linuxwindowswasm

Overview

Build it once from a pattern, then match it against any number of strings. Matching time is linear in the length of the input: the engine never backtracks, so no pattern can make a search run away. The syntax covers literals and escapes, ., character classes, \d \w \s, alternation, capturing, non-capturing and named groups, greedy and lazy (*?) quantifiers, anchors and word boundaries; backreferences and lookaround are not supported. Matching is leftmost-first (the first alternative that matches wins, as in Perl and C#). Offsets and lengths count bytes, and . matches one byte.

A Regex is a reference object that is safe to share: it holds only its compiled program. The constructors treat the pattern as code and throw RegexException when it is malformed. When the pattern comes from outside the program, use regex::compile, which returns None instead of throwing.

Description

The regular-expression API has five public types: Regex, Match, Group, RegexOptions and RegexException. A Regex is a compiled pattern. It is an object that you create once and share freely, and it never changes. Its methods work on a string and report what they found as plain values.

Test, find, and read named groups

Regex userRe = Regex("^[a-z0-9_]{3,16}$");
console.writeln(userRe.isMatch("ann_01"));
console.writeln(userRe.isMatch("A"));
Regex kv = Regex("(?<key>\\w+)=(?<val>[^;]*)");
Match? m = kv.find("x=1;y=22");
if (m != None) {
    console.writeln(m.value);
    console.writeln(m.index);
    console.writeln(m.length);
    Group? key = m.group("key");
    if (key != None) { console.writeln(key.value); }
    console.writeln(m.group(2).value);
    console.writeln(m.groups.length());
}
Array<Match> all = kv.matches("x=1;y=22;z=");
console.writeln(all.length());
for (Match one in all) { console.writeln(one.group(2).value); }
console.writeln(kv.groupCount());
console.writeln(kv.groupNames());
true
false
x=1
0
3
x
1
3
3
1
22

2
[key, val]

Construction

Regex(pattern) compiles a pattern string. Regex(pattern, options) also takes a RegexOptions value, and Regex(pattern, flags) takes a string of option letters (i, m and s, in any order). Regex::FromProgram(program) wraps an already compiled program in constant time; see the std.regex entry for the compile-time recipe.

A pattern you write in source code is code: a malformed one throws RegexException. A pattern that arrives as data, from a configuration file or from user input, should go through regex::compile(pattern), which returns None for a malformed pattern and never throws.

Matches

find(s) returns the first match as a Match?, None when there is none, and matches(s) returns every non-overlapping match in order. isMatch(s) only answers whether a match exists, and count(s) counts matches without building any Match values. find and isMatch also take a start offset, find(s, from), to begin searching partway into the string. Offsets and lengths are counted in bytes.

A Match is a value, and it exists only when the search succeeded. index, length and value describe the whole match. groups is an array whose element 0 is the whole match, followed by each capture group in order of its opening parenthesis; group(i) reads one by number (an out-of-range number throws RuntimeException) and group(name) reads one by its (?<name>...) name, returning None for a name the pattern does not declare.

A Group has matched, index, length and value. A group that did not take part in a successful match, such as the untaken side of an alternation, is a real and reachable state: its matched is false, its index is -1, its length is 0 and its value is "".

A group that did not take part

Regex r = Regex("(\\d+)|([a-z]+)");
Match? found = r.find("abc");
if (found != None) {
    Group digits = found.group(1);
    Group letters = found.group(2);
    console.writeln(digits.matched);
    console.writeln(digits.index);
    console.writeln("[${digits.value}]");
    console.writeln(letters.matched);
    console.writeln(letters.value);
    try {
        found.group(5);
    } catch (RuntimeException e) {
        console.writeln("no such group");
    }
}
false
-1
[]
true
abc
no such group

Replace and split

replace(s, replacement) replaces every match, and replace(s, replacement, count) replaces at most count of them. Instead of a string, the replacement can be a function (Match) => string, called once for each match. split(s) cuts the string at each match, and split(s, limit) returns at most limit pieces, with the rest of the string left unsplit in the last one. When the separator pattern contains a capture group, the captured text is included among the pieces.

A replacement string is read once per call with this grammar.

  • $0 to $99 insert a group by number; the longest valid group number is taken, so $10 is group ten when the pattern has ten groups and group one followed by 0 otherwise. $0 is the whole match.
  • ${name} inserts a named group.
  • $$ inserts a literal $. A $ followed by anything else, or at the end, is a literal $.
  • A group that did not take part inserts nothing.
  • A reference to a group that does not exist throws RegexException. A replacement is code, and a silent pass-through would hide a typo.

Leviathan's own string literals interpolate ${...}, so write a named reference in source as "\${name}" to deliver the three characters ${name} to the regex engine.

Replacing with groups, a count, and a function

Regex date = Regex("(\\d+)-(\\d+)-(\\d+)");
console.writeln(date.replace("on 2026-10-02 ok", "$3/$2/$1"));
Regex named = Regex("(?<y>\\d+)-(?<m>\\d+)");
console.writeln(named.replace("2026-10", "\${m}.\${y}"));
console.writeln(named.replace("2026-10", "$$\${m}"));
Regex digits = Regex("\\d+");
console.writeln(digits.replace("a1b22c333", "#"));
console.writeln(digits.replace("a1b22c333", "#", 2));
console.writeln(digits.replace("a1b22c333", (Match m) => "<" + m.value + ">"));
console.writeln(digits.split("a1b22c333d"));
console.writeln(digits.split("a1b22c333d", 2));
console.writeln(Regex("([,;])").split("a,b;c"));
try {
    console.writeln(digits.replace("a1", "$7"));
} catch (RegexException e) {
    console.writeln("bad reference");
}
on 02/10/2026 ok
10.2026
$10
a#b#c#
a#b#c333
a<1>b<22>c<333>
[a, b, c, d]
[a, b22c333d]
[a, ,, b, ;, c]
bad reference

Options

RegexOptions has three switches, set by name: ignoreCase (ASCII letters only), multiline (^ and $ also match at line breaks) and dotAll (. also matches a newline). The string form "im" or "s" is shorthand for the same switches. A RegexException carries the usual message and an offset, the best-known byte position of the error in the pattern, or -1 when no position is known.

Rules

  • find returns None when nothing matches, so a match is never an empty placeholder.
  • A pattern literal that is malformed throws RegexException; regex::compile is the form that returns None instead.
  • Matching is leftmost-first with greedy quantifiers by default, as in Perl, C# and JavaScript: among the matches that start leftmost, the one the pattern lists first wins, not the longest.
  • After a zero-length match at position i, the search resumes at i + 1, so matches, replace and split always terminate.
  • replace replaces every match unless a count is given.
  • Matching works on bytes: . matches one byte, so a character that takes several bytes in UTF-8 is matched by several dots.
  • ignoreCase folds ASCII letters only.
  • A Regex is immutable and can be shared freely.

Examples

Choosing between throw and None

Regex? fromData = regex::compile("[a-");
console.writeln(fromData == None);
try {
    Regex fromCode = Regex("(abc");
    console.writeln(fromCode.isMatch("abc"));
} catch (RegexException e) {
    console.writeln("malformed: ${e.message}");
    console.writeln(e.offset);
}
Regex ci = Regex("hello", RegexOptions(ignoreCase: true));
console.writeln(ci.isMatch("HeLLo"));
Regex lines = Regex("^b$", "m");
console.writeln(lines.isMatch("a\nb\nc"));
console.writeln(Regex("a.b").isMatch("a\nb"));
console.writeln(Regex("a.b", "s").isMatch("a\nb"));
true
malformed: regex: unterminated group at offset 4
4
true
true
false
true

Notes

Not implemented: Unicode character classes (\p{L} is rejected as a malformed pattern) and Unicode case folding.

Examples

Validating and extracting

Regex userRe = Regex("^[a-z0-9_]{3,16}$");
console.writeln(userRe.isMatch("alice_01"));
console.writeln(userRe.isMatch("Al"));
Regex kv = Regex(r"(?<key>\w+)=(?<val>[^;]*)");
Match? m = kv.find("name=bob;age=7");
if (m != None) {
    console.writeln(m.group("key")?.value ?? "");
    console.writeln(m.group("val")?.value ?? "");
}
console.writeln(kv.count("a=1;b=2;c=3"));
true
false
name
bob
3

Constructors

FromProgram

Regex::FromProgram(Array<int> prog)

Wrap a program that was compiled ahead of time, without parsing anything.

Use it with regex::compileProgram and comptime, so that a constant pattern is compiled when the program is built and a mistake in it is reported at its source line. The wrapper does not know the pattern text or the options, so pattern() returns "" and options() returns the defaults.

Parameters

prog
A program produced by regex::compileProgram.

Examples

Wrapping a compiled program

Array<int> prog = regex::compileProgram(r"[a-z]+@[a-z]+", "");
Regex mail = Regex::FromProgram(prog);
console.writeln(mail.isMatch("mail bob@host now"));
console.writeln(mail.count("a@b c@d"));
console.writeln(mail.pattern() == "");
true
2
true

See also: compileProgram

new

new(string pattern)

Compile a pattern.

Parameters

pattern
The pattern text.

Throws

RegexException
when the pattern is malformed.

Examples

Compiling with options

Regex plain = Regex(r"\d+");
Regex loose = Regex("hello", RegexOptions(ignoreCase: true));
Regex flagged = Regex("hello", "i");
console.writeln(plain.isMatch("order 66"));
console.writeln(loose.isMatch("HELLO"));
console.writeln(flagged.isMatch("HELLO"));
try {
    Regex bad = Regex("a", "q");
} catch (RegexException e) {
    console.writeln(e.message);
}
true
true
true
regex: unknown flag at offset 0
new(string pattern, RegexOptions options)

Compile a pattern with options.

Parameters

pattern
The pattern text.
options
The options to apply.

Throws

RegexException
when the pattern is malformed.
new(string pattern, string flags)

Compile a pattern with options given as a flag string.

Parameters

pattern
The pattern text.
flags
Any of the letters i (ignore case), m (multiline) and s (dot matches line breaks), in any order; "" for none.

Throws

RegexException
when the pattern is malformed or flags contains another character.

Methods

count

count(string s) -> int

Count the non-overlapping matches without building Match values.

The result equals matches(s).length(), computed faster.

Parameters

s
The text to search.

Returns

The number of matches.

Examples

Counting

Regex re = Regex(r"\d+-\d+");
console.writeln(re.count("2026-10 2027-01 2028-02"));
console.writeln(re.count("none"));
3
0

countAll

countAll(Array<string> rows) -> Array<int>

Count the matches in every string of an array.

The engine is set up once and reused for the whole array.

Parameters

rows
The strings to search.

Returns

One count per row, in order.

Examples

Counting per row

console.writeln(Regex(r"\d").countAll(["a1b2", "", "123"]));
[2, 0, 3]

find

find(string s) -> Match | None

Find the first match in a string.

Parameters

s
The text to search.

Returns

The first (leftmost) match, or None when there is none.

Examples

First match, and the next one from an offset

Regex re = Regex(r"(?<year>\d+)-(?<month>\d+)");
Match? m = re.find("on 2026-10 and 2027-01");
if (m != None) console.writeln("${m.index}:${m.value}");
Match? m2 = re.find("on 2026-10 and 2027-01", 8);
if (m2 != None) console.writeln("${m2.index}:${m2.value}");
console.writeln(re.find("none") == None);
3:2026-10
15:2027-01
true
find(string s, int from) -> Match | None

Find the first match at or after a byte offset.

Parameters

s
The text to search.
from
The byte offset where the search starts; a negative value is treated as 0.

Returns

The first match starting at or after from, or None when there is none.

groupCount

groupCount() -> int

The number of capture groups in the pattern.

Non-capturing groups (?:...) are not counted, and the whole match (group 0) is not counted.

Returns

The number of capture groups.

Examples

Counting groups

console.writeln(Regex(r"(\d+)-(\d+)").groupCount());
console.writeln(Regex(r"(?:ab)+(\d)").groupCount());
console.writeln(Regex(r"\d+").groupCount());
2
1
0

groupNames

groupNames() -> Array<string>

The names of the named groups, in the order they are declared in the pattern.

Returns

The names declared with (?<name>...); empty when the pattern has none.

Examples

Listing the names

Regex re = Regex(r"(?<year>\d+)-(?<month>\d+)");
console.writeln(re.groupNames());
console.writeln(Regex(r"\d+").groupNames().length());
[year, month]
0

isMatch

isMatch(string s) -> bool

Test whether the pattern matches anywhere in a string.

Parameters

s
The text to search.

Returns

true when some part of s matches.

Examples

Testing from the start and from an offset

Regex re = Regex(r"(?<year>\d+)-(?<month>\d+)");
console.writeln(re.isMatch("2026-10"));
console.writeln(re.isMatch("2026-10", 5));
console.writeln(re.isMatch("x 2026-10 y", 2));
true
false
true
isMatch(string s, int from) -> bool

Test whether the pattern matches anywhere in a string at or after a byte offset.

Parameters

s
The text to search.
from
The byte offset where the search starts; a negative value is treated as 0.

Returns

true when some part of s from that offset on matches.

isMatchAll

isMatchAll(Array<string> rows) -> Array<bool>

Test the pattern against every string of an array.

The engine is set up once and reused for the whole array.

Parameters

rows
The strings to test.

Returns

One bool per row, in order.

Examples

Testing a column

Regex code = Regex(r"^[A-Z]{2}\d{2}$");
console.writeln(code.isMatchAll(["AB12", "ab12", "XY99", "X9"]));
[true, false, true, false]

matches

matches(string s) -> Array<Match>

Find every non-overlapping match, left to right.

An empty match is reported once at its position and the search then moves one byte forward, so the call always ends.

Parameters

s
The text to search.

Returns

The matches in order; empty when there are none.

Examples

Collecting all matches

Regex re = Regex(r"\d+-\d+");
Array<Match> all = re.matches("on 2026-10 and 2027-01");
console.writeln(all.length());
for (Match x in all) console.writeln(x.value);
console.writeln(re.matches("nothing").length());
console.writeln(Regex("x*").matches("axb").length());
2
2026-10
2027-01
0
4

options

options() -> RegexOptions

The options this Regex was constructed with.

Returns

The options; every flag is false when none were given.

Examples

Reading the options back

Regex re = Regex("a", "im");
RegexOptions o = re.options();
console.writeln("${o.ignoreCase} ${o.multiline} ${o.dotAll}");
true true false

pattern

pattern() -> string

The pattern text this Regex was constructed from.

Returns

The pattern, exactly as it was passed to the constructor.

Examples

Reading the pattern back

Regex re = Regex(r"(\d+)-(\d+)");
console.writeln(re.pattern());
(\d+)-(\d+)

replace

replace(string s, (Match) => string fn) -> string

Replace matches using a function that builds each replacement.

All matches are replaced. The function receives each Match in turn and returns the text to put in its place.

Parameters

s
The text to search.
fn
Called once per match; returns the replacement text.

Returns

The text with every match replaced.

Examples

Replacing with a function

Regex num = Regex(r"\d+");
console.writeln(num.replace("a1b22c333", (Match m) => "<${m.value}>"));
console.writeln(num.replace("a1b22c333", (Match m) => "<${m.value}>", 1));
a<1>b<22>c<333>
a<1>b22c333
replace(string s, (Match) => string fn, int count) -> string

Replace the first matches using a function that builds each replacement.

Parameters

s
The text to search.
fn
Called once per replaced match; returns the replacement text.
count
The most matches to replace; the rest are left as they are.

Returns

The text with up to count matches replaced.

replace(string s, string replacement) -> string

Replace every match with a replacement string.

The replacement string may refer to groups: $0 is the whole match, $1 to $99 are groups by number (a two-digit number is read when that group exists), ${name} is a named group, $$ is a literal $, and a $ followed by anything else, or at the end, is a literal $. A group that did not take part is replaced by "". In Leviathan source write the replacement as a raw string (r"${name}") or escape it ("\${name}"), because ${...} inside an ordinary string is interpolation.

Parameters

s
The text to search.
replacement
The replacement text, with the $ references described above.

Returns

The text with every match replaced.

Throws

RegexException
when the replacement names a group the pattern does not declare, refers to a group number the pattern does not have, or has a ${ with no closing }.

Examples

Replacement references

Regex num = Regex(r"\d+");
console.writeln(num.replace("a1b22c333", "#"));
console.writeln(num.replace("a1b22c333", "#", 2));
Regex kv = Regex(r"(\w+)=(\w+)");
console.writeln(kv.replace("a=1 b=2", r"$2=$1"));
Regex named = Regex(r"(?<k>\w+)=(?<v>\w+)");
console.writeln(named.replace("a=1 b=2", r"${v}:${k}"));
console.writeln(num.replace("cost 5", r"$$$0"));
try {
    kv.replace("a=1", r"$3");
} catch (RegexException e) {
    console.writeln(e.message);
}
a#b#c#
a#b#c333
1=a 2=b
1:a 2:b
cost $5
regex: group reference out of range in replacement: $3
replace(string s, string replacement, int count) -> string

Replace the first matches with a replacement string.

The replacement string uses the same $ references as replace(s, replacement).

Parameters

s
The text to search.
replacement
The replacement text, with $ references.
count
The most matches to replace; the rest are left as they are.

Returns

The text with up to count matches replaced.

Throws

RegexException
when the replacement names a group the pattern does not declare, refers to a group number the pattern does not have, or has a ${ with no closing }.

split

split(string s) -> Array<string>

Split a string at every match.

When the pattern has capture groups, the text of each group is put into the result after the piece that precedes the match. Pieces next to the ends or to each other can be empty.

Parameters

s
The text to split.

Returns

The pieces between the matches; a string with no match gives one piece, the whole string.

Examples

Splitting

Regex sep = Regex(r"\s*,\s*");
console.writeln(sep.split("a , b,c"));
console.writeln(sep.split("abc"));
console.writeln(Regex(",").split("a,b,"));
console.writeln(Regex("([,;])").split("a,b"));
[a, b, c]
[abc]
[a, b, ]
[a, ,, b]
split(string s, int limit) -> Array<string>

Split a string at the first matches, up to a number of pieces.

Parameters

s
The text to split.
limit
The most pieces to return; the unsplit rest of the string is the last piece. A value of zero or less means no limit.

Returns

The pieces.

See also

  • compile — Compile a pattern that comes from data, without throwing.
  • Match — A successful match: where it is, what it matched, and its capture groups.
  • RegexOptions — Options that change how a pattern is interpreted: ASCII case folding, line anchors and whether . crosses line breaks.
  • Group — One capture group of a match: whether it took part, and where and what it matched.
  • RegexException — The exception thrown for a malformed pattern, and for a malformed or out-of-range reference in a replacement string.
  • regex — Regular expressions: the Regex library and its namespace-level helpers.