Nimony

The road to Nim 3

regex

nimony/lib/std/regex.nim

Regular expressions, and a lexer generator built on the same engine.

The engine is a DFA: a pattern is turned into a deterministic automaton once, and matching then costs one table step per input character with no backtracking. That is what makes the worst case linear — there is no input that makes (a+)+b take exponential time here — and it is also what bounds what the module can express: a capture must lie on every accepting path ((abc)|(xyz) is rejected, (abc|xyz) is fine). The automaton's size is not capped, and it can be exponential in the pattern's: (a|b)*a(a|b){20} needs two million states. Matching stays linear, but compiling a pattern from an untrusted source with tryRe can take unbounded time and memory.

Two ways to use it:

At runtime — build a Regex from a string and match against it:

`nim import std/regex

let pattern = re"[a-z0-9]+\s=\s[a-z0-9]+" echo matchLen("key1 = cal9", pattern) # 12 `

At compile time — lex turns a whole set of patterns into one automaton and emits it as a case statement, so the lexer in your program contains no regex engine at all:

nim var pos = 0 while pos < input.len: let start = pos lex input, pos: of r"\d+": echo "an integer ", substr(input, start, pos-1) of "else": echo "the ELSE keyword" of r"[a-zA-Z_]\w*": echo "an identifier" of r".": discard

The two share one implementation of what a pattern means (std/private/regexcore), so a pattern cannot mean one thing in a generated lexer and another at runtime.

Captures ========

(x) records where the group matched; matchLen/match/fullMatch take a var seq[Capture] and capture reads the text back out:

nim var caps: seq[Capture] = @[] if match("key=value", re"(\w+)=(\w+)", caps): echo capture("key=value", caps, 0) # key echo capture("key=value", caps, 1) # value

A DFA is a poor place to track captures, and this one is honest about it rather than clever. Two limits follow:

  • A capture must lie on every accepting path, so (abc)|(xyz) is a

compile-time error. Write (abc|xyz).

  • Otherwise the match is always right, but the capture bounds are

best-effort: for a group sitting next to something of variable length ((\w+)\s*=) the engine cannot say where the group ended, and caps comes back empty. capture then returns "". Check for it during development rather than assuming; a fixed-shape pattern always works.

Where captures are essential and the pattern is awkward, split it: match the overall shape with one regex and pull the pieces out with split or plain string operations.

Syntax ======

. any character except \0 · [abc] [^abc] [a-z] character classes · x* x+ x? x{m,n} repetition · x|y alternation · (x) capture · (?:x) grouping without a capture · "abc" a literal run · \d \D \s \S \w \W classes · \A (^) start, \Z ($) end, \b \B word boundary · \1 back reference · \n \r \t \e \a \v \f \b and \123 escapes.

By default patterns are parsed with reExtended, so unescaped spaces and tabs are ignored and a pattern may be laid out for reading. Match a literal space with \ , [ ] or " ".