Nimony

The road to Nim 3

rex

nimony/lib/std/rex.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/rex

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.

Patterns and input may contain UTF-8 and arbitrary bytes. Literals, ., and character classes support Unicode code points; byte values 128..255 also match the corresponding individual byte. The longest match wins. Match lengths, positions, and capture bounds are byte offsets into the original string.

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 " ".

Differences from Nim's std/re ===============================

The name is different on purpose: this is not PCRE, and code ported from Nim's std/re should be checked rather than assumed to work. What stays the same is the basic shape — matchLen, match, find, findBounds, contains and startsWith take a start = 0, and "no match" is -1, or (-1, 0) for findBounds. replace inserts by literally, like Nim's replace. The differences:

  • Whitespace is ignored by default. Patterns are parsed with

reExtended, so re"a b" matches "ab". In Nim only rex does that.

  • The longest match wins, not the first alternative that works:

a|ab matches all of "ab"; PCRE stops after "a".

  • "..." is a literal run, as in std/pegs. In PCRE " is an ordinary

character.

  • Outside a character class \n matches any of \r\n, \n and \r.
  • \123 is decimal: a back reference, or, with a leading zero (\065), a

character code. It is not octal.

  • . matches every Unicode code point and nonzero byte, newlines included.

\s is \1..\32 and \w includes non-ASCII code points and bytes.

  • re takes only a string literal and compiles it with the program;

re(someVar) is tryRe here, which reports an error instead of raising.

  • Captures are positions (seq[Capture], read with capture), not a

var openArray[string], and are subject to the limits described under Captures.

  • findAll yields (first, last) bounds, not strings.
  • Not provided: =~/matches, replacef with $1, multiReplace,

transformFile, endsWith, escapeRe, rex, the split iterator and its maxsplit, and RegexError.

  • Not supported in patterns: the flags reIgnoreCase, reMultiLine and

reDotAll, inline options such as (?i), lazy quantifiers (x*? is accepted but means x*), lookaround, named groups, \x escapes and Unicode property classes such as \p{L}. Some of these features need backtracking, which this engine rules out.