The text page gave you verbs for strings you can spell out. This page is for the shapes you can only describe: a run of digits, a word, a dash, a number. The regex unit answers those — with one promise most engines cannot make and one refusal most engines will not: matching is linear in the text for every pattern you can write, and the spellings this engine class could never honor are refused at compile time instead of silently meaning something else.

The example is examples/PatternMatching in the tutorial repository.

Getting the file

make -C examples/PatternMatching run
PatternMatching

every number in: order 66 of 512 items
  66 at rune 6
  512 at rune 12
  Count says 2, Matches says 1

Replace       order # of # items
ReplaceFirst  order # of 512 items

groups of item-42: word item, number 42

letters of every script: 4 words in "Grüße Ελλάδα 東京 café"

the backreference spelling is refused: BadEscape
lookaround is refused by name: Lookaround

The example, walked

Compile once, ask many times

numbers := Compile("[0-9]+") on fail e do leave;

A pattern is a small program, and Compile builds it once — the parse, the checks, the instruction emission all happen here, so the matching verbs never pay for them again. The compile is fallible, and the clause says so where the pattern stands: a malformed pattern answers a RegexError by name — BadEscape, BadClass, UnbalancedParen, BadQuantifier, EmptyPattern, BadProperty, Lookaround — an expected, reasoned rejection. The absence of a match, by contrast, is never a failure: it is an ordinary Match whose Found is False. Two kinds of “no”, kept apart, exactly as the crossroads drew the line.

The syntax is the familiar core: literals, ., classes like [a-z] and [^a] (with \d \w \s and \p{L} usable inside), grouping, alternation |, the quantifiers * + ?, the anchors ^ $, and a closed escape list. Closed is a feature: an escape outside the list is BadEscape at compile time, never a guess at run time.

The sweep

m := Find(address numbers, text, 0);
while m.Found do
begin
    WriteLn("  %ls at rune %lld", Copy(text, m.Start, m.Length), m.Start);
    m := Find(address numbers, text, m.Start + m.Length);
end;

Find answers a MatchFound, Start, Length, positions in runes like every string measurement in Mica — and advancing from the previous hit’s end walks every occurrence. The engine’s design promise holds throughout: match time grows with the text, never with the pattern, so a pattern that matches late or not at all costs the same linear walk, and the catastrophic slowdowns other engines document as warnings cannot be expressed here. The price of that class of engine is stated as plainly: backreferences and lookaround are not in it, and the last lines of the output show the backreference spelling refused rather than misread and the lookaround refused by name — Lookaround — because a lookaround is exactly what the linear-time guarantee cannot carry.

Rewriting and groups

WriteLn("Replace       %ls", Replace(address numbers, text, "#"));

Replace rewrites every hit, ReplaceFirst only the first, Count counts without rewriting, Matches answers the one-bit question. And the parentheses you write are answerable. A walk holds the match it found: MatchStart opens one over a text, NextMatch steps it, and Group(address walk, k) returns the k-th group’s own Match, which Copy turns into text — the example splits item-42 into its word and its number. The pattern itself is never written after Compile, which is what lets one compiled pattern serve any number of walks at once.

Letters of every script

words := Compile("\\p{L}+") on fail e do leave;

A class may name a Unicode general category: \p{L} is every letter of every script, \p{N} every number, \p{Lu} the uppercase letters alone, and \P{L} the complement — the one-letter families L M N P S Z C and the two-letter categories the Unicode character database lists, inside a bracket expression too, as [^\s\p{L}\p{N}]. The example counts the words of a line written in three scripts with one pattern and gets four, because a letter is a letter whatever its alphabet. A name the database does not carry is BadProperty at compile time, like every other closed-list refusal.

Try it

1. Refuse a pattern of your own. Change a pattern to "[a-" and watch Compile answer BadClass through the same clause. Then try ""EmptyPattern. The reasons are values; a build tool can case over them.

2. Make the engine sweat — and fail to. Against a long text, try the pattern (a+)+$ — the classic detonator elsewhere. Here it is just a pattern: compile grows the instruction program a little, and matching stays one linear sweep. The absence of drama is the feature.

What the unit promises

Compile once; match in time proportional to the text, every time; refuse at compile time what the engine class cannot honor; and keep no hidden state — a compiled pattern is a value no verb writes, and a walk over a text is a value you hold, so one pattern serves any number of walks in any number of tasks with no shared cell anywhere.

Next

Text: the value, the builder, and the window for the string verbs beside this unit, and UTF sources for why those positions are runes. Every example lives in the tutorial repository.