Documentation
¶
Overview ¶
Package onigmo is a pure-Go (cgo-free) reimplementation of Onigmo, the regular-expression engine used by Ruby.
Unlike Go's standard library regexp (RE2), it is a backtracking VM and so can support the Onigmo features RE2 omits — backreferences, lookahead/lookbehind, possessive quantifiers, atomic groups, named groups and subexpression calls — with Ruby's leftmost-first match semantics, hardened against catastrophic backtracking (ReDoS) by memoization and a deterministic time/step budget.
Phase 0 is implemented: a greedy backtracking VM with leftmost-first match semantics covering literals and escapes, the dot metacharacter, character classes (ranges, negation, and the \d \D \w \W \s \S escapes), the anchors \A \z \Z ^ $, the greedy quantifiers * + ? {m} {m,} {m,n}, capturing and non-capturing groups, and alternation. Phase 1 adds named groups (?<name>...), backreferences (\1..\9 and \k<name>), and the non-greedy (lazy) quantifiers *? +? ?? {m,n}? (which match the fewest repetitions first, so <.+?> matches the shortest tag) and the possessive quantifiers *+ ++ ?+ and atomic groups (?>...) (which commit a matched span — once it matches, its backtrack points are discarded, so a++a and (?>a+)a never match "aaa"; a trailing + on a {m,n} brace is instead a stacked greedy repeat, as in Onigmo). Phase 2 adds the lookaround assertions — positive and negative lookahead (?=...) (?!...) and lookbehind (?<=...) (?<!...) — and the \G anchor (which pins a match to the scan start). Lookbehind bodies must be of constant width per alternative, as in Onigmo/Ruby; variable-width lookbehind is rejected. Phase 3 begins with POSIX bracket classes [[:name:]] (and negated [[:^name:]]) inside character classes, for the 14 standard names alpha, digit, alnum, upper, lower, space, blank, cntrl, graph, print, punct, xdigit, and word; and the inline options (?flags) / (?flags:...) (with the (?-flags) turn-off form) for the letters i (case-insensitive matching), m (dot-all: the dot also matches a newline), and x (extended/free-spacing: unescaped whitespace and # comments in the pattern are ignored, except inside a character class).
Under /i, literals and character classes fold rune-aware using Go's unicode.SimpleFold (simple, 1:1 case folding): a folded character is decoded as a whole UTF-8 code point and matches any code point in its simple-case-folding orbit, so /É/i matches é, Greek and Cyrillic case pairs fold, and even ASCII /k/i reaches the Kelvin sign U+212A. A folded class is rune-aware too (so (?i)[a-z] matches A and the Kelvin sign, and (?i)[α-ω] an uppercase Greek letter), with multi-byte members and ranges and last-applied negation. A folded atom obeys the same rune/byte boundary as \p{…} (it never matches at a UTF-8 continuation byte, keeps byte offsets, and is rejected inside a fixed-width lookbehind). Only simple folding is done: full/special folding (ß→"ss", Turkish dotless-i) is out of scope, and backreference folding stays ASCII-only.
Phase 3 also adds Unicode property classes \p{name} / \P{name} (and the in-brace negation \p{^name}), the one rune-aware atom in the otherwise byte-oriented engine: it decodes a single UTF-8 code point and advances by its byte length. The supported names are the general categories L, N, P, S, Z, C and the letter/number subcategories Lu, Ll, Lt, Lm, Lo, Nd, plus the Onigmo POSIX-style aliases Alpha, Alnum, Digit, Space, Upper, Lower and Word (following Ruby's definitions). A property may also appear inside a character class ([\p{L}\d]), which makes that class rune-aware while its ASCII byte-range members keep working. A rune-aware atom never matches at a UTF-8 continuation byte, so the scan never tests a code point mid-character (as MRI, which positions by character). Note that match offsets remain byte offsets, whereas MRI reports character offsets, so the two agree on matched text but not on the numeric span on multi-byte input. (Rune-level Unicode case-folding for /i is described above.)
Multi-encoding (Phase 3) gives the Regexp a first-class encoding, matching the way Ruby's Regexp#encoding governs matching on a UTF-8 versus a binary string. In the default UTF8 mode the dot (.) and a byte-oriented character class advance by a whole UTF-8 code point, so /./ matches a complete multi-byte character — /./ on "é" consumes "é" as one unit, and [^a] consumes a whole character rather than a single byte, exactly as MRI does on a UTF-8 string (a positive ASCII range such as [a-z] still fails on a multi-byte character, whose code point exceeds the range). This resolves the engine's original "dot matches one byte" limitation. In ASCII8BIT mode (Ruby's binary /n encoding, selected by CompileEnc(pattern, ASCII8BIT)) every atom advances a single byte and Unicode case-folding (/i) and \p{…} properties operate per byte, ASCII-only. The encoding is exposed as re.Encoding(). Match offsets are byte offsets in both modes. In UTF8 mode a bare . or a byte-oriented class inside a fixed-width lookbehind has a variable byte width (1..4); the lookbehind's candidate-position scan enumerates the alignments and only a code-point-aligned one matches, so a lookbehind over a multi-byte character works ((?<=.)x matches the x after "é"). A literal multi-byte character-class member is a whole code point in UTF8 mode: [é] matches "é", [à-ï] is a code-point range, [αβγ]/[中文] work, a mixed class such as [a-zé] combines an ASCII range with a multi-byte member, and a range may span ASCII into the multi-byte space ([a-é]); in ASCII8BIT mode such a member stays byte-oriented ([é] is its two raw bytes). Per-encoding cursors beyond UTF-8 and ASCII-8BIT (UTF-16/32, EUC, Shift_JIS) are deliberately out of scope: a Go string is UTF-8 by convention, and text in a legacy or wide encoding is transcoded to UTF-8 at the boundary before matching.
Phase 3 also adds the hex-digit class \h (Onigmo's [0-9A-Fa-f]) and its byte-complement \H — usable standalone or inside a character class, and byte-oriented like \d/\w — and the linebreak escape \R, lowered to (?>\r\n|[\n\v\f\r\x{85}\x{2028}\x{2029}]): a CR-LF pair matches atomically as one unit (so \R\n never splits a CRLF) and a lone \n, \r, \v, \f, NEL, LS or PS also matches. \R is rune-aware (it carries the multi-byte NEL/LS/PS members), so like \p{…} it decodes a whole code point and is rejected inside a fixed-width lookbehind.
Subexpression calls \g<…> are implemented: \g<name>, \g<n> (absolute group number), the relative forms \g<+n> / \g<-n>, and \g<0> (recurse the whole pattern). A call is a true re-execution of the referenced group's sub-pattern at the current position, and it re-captures into that group's slot, so the most recent execution wins — except that a group recursing into itself keeps its outermost binding, exactly as Onigmo/Ruby does (the engine saves and restores the open groups' captures across a call). Calls may be recursive and mutually recursive, so the balanced-parentheses idiom \A(?<bal>\((?:[^()]|\g<bal>)*\))\z works. Forward references (calling a group defined later) resolve in a post-parse pass. Recursion is bounded by a hard depth cap plus the step budget, so a pathological or non-terminating grammar (e.g. \A\g<0>\z, which Onigmo rejects statically) fails deterministically rather than exhausting the Go stack. A call has data/recursion-dependent width, so — like a backreference — it is rejected inside a fixed-width lookbehind (a documented divergence: MRI accepts the simple one-char case).
ReDoS hardening (Phase 4) is in: for a pattern without a backreference or a subexpression call the VM memoizes the (instruction, position) split states it has explored and never re-explores one, so catastrophic patterns such as (a+)+$ run in polynomial rather than exponential time while producing the identical leftmost-first match. A deterministic step budget (and, for calls, the recursion-depth cap) remains as the backstop, joined by an optional wall-clock timeout (Ruby's Regexp.timeout equivalent): re.WithTimeout(d) returns a concurrency-safe copy that aborts a match exceeding d of real time, reporting no match. A pathological pattern is bounded by whichever of the budget or the deadline it reaches first.
A transparent prefilter (Phase 4) accelerates the search. The start-position pass analyses the compiled program's leading path for a \A anchor, a required literal prefix, or a first-byte set — including the union over a leading alternation (foo|bar, a*b) — and uses it to skip start positions that cannot begin a match (a strings.Index or byte-set scan instead of invoking the VM at every offset). A second pass extracts a required interior literal — a fixed substring that must appear somewhere in every match even when the pattern has no anchor or leading literal (the foo of \d+foo\d+) — by walking the program's mandatory spine, rejects a whole haystack lacking it with a single strings.Contains, and bounds the scan on the right at the literal's last occurrence (no match can begin past it). Every candidate either pass yields is still verified by the VM, so results are byte-identical to an unfiltered scan. The start-position scan also reuses a single capture buffer across the offsets it tries rather than reallocating at each — the VM never writes that base buffer in place, so the reuse is behaviour-preserving — and a representative benchmark suite (bench_test.go) measures the literal/alternation/anchored/backtracking/ subexpression-call/multibyte workloads and the prefilter fast paths against a forced-slow baseline.
With these, the standalone engine roadmap (Phases 0–4) is complete. The full supported-feature list and the deliberately documented out-of-scope boundaries (full/special case folding, the \p{…} category slice, encodings beyond UTF-8/ASCII-8BIT, byte rather than character offsets, lenient invalid-UTF-8 decoding, and variable-width lookbehind) are collected in the "Engine status: complete" section of docs/plan-regexp.md. The Ruby Regexp/MatchData surface and the replacement DSL (Phase 5) live in the downstream go-embedded-ruby adapter that consumes this module, not here.
See docs/plan-regexp.md for the full roadmap.
Index ¶
- Constants
- type Encoding
- type MatchData
- func (m *MatchData) Begin(i int) int
- func (m *MatchData) End(i int) int
- func (m *MatchData) IndexOfName(name string) int
- func (m *MatchData) NGroups() int
- func (m *MatchData) Post() string
- func (m *MatchData) Pre() string
- func (m *MatchData) Str(i int) string
- func (m *MatchData) StrName(name string) string
- type Regexp
- func (re *Regexp) Encoding() Encoding
- func (re *Regexp) Match(s string) *MatchData
- func (re *Regexp) MatchAt(s string, pos int) *MatchData
- func (re *Regexp) MatchBounds(s string) (begin, end int, ok bool)
- func (re *Regexp) MatchBoundsAt(s string, pos int) (begin, end int, ok bool)
- func (re *Regexp) MatchString(s string) bool
- func (re *Regexp) String() string
- func (re *Regexp) Timeout() time.Duration
- func (re *Regexp) WithTimeout(d time.Duration) *Regexp
Constants ¶
const ( // UTF8 is the default encoding: the dot and byte-oriented classes advance by // a whole UTF-8 code point. UTF8 = compile.UTF8 // ASCII8BIT is Ruby's binary (/n) encoding: every atom advances one byte. ASCII8BIT = compile.ASCII8BIT )
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Encoding ¶
Encoding selects how the byte-oriented input-advancing atoms — the dot (`.`) and a byte-oriented character class — traverse the input, the way Ruby's Regexp#encoding governs matching on a UTF-8 vs a binary (ASCII-8BIT) string.
In UTF8 (the default) the dot and byte-oriented classes advance by a whole UTF-8 code point, so `/./` matches a complete multi-byte character (it matches "é" as one unit, exactly as MRI does on a UTF-8 string) and `[^a]` consumes a whole character rather than a single byte. In ASCII8BIT (Ruby's /n binary encoding) every atom advances one byte, and Unicode case-folding (/i) and \p{…} properties operate per byte (ASCII-only). Match offsets are byte offsets in both modes.
type MatchData ¶
type MatchData struct {
// contains filtered or unexported fields
}
MatchData holds the result of a successful match: the byte spans of the whole match (group 0) and of each capturing group.
func (*MatchData) Begin ¶
Begin returns the byte offset of the start of group i, or -1 if the group did not participate in the match. Group 0 is the whole match. An out-of-range index returns -1.
func (*MatchData) End ¶
End returns the byte offset just past the end of group i, or -1 if the group did not participate. An out-of-range index returns -1.
func (*MatchData) IndexOfName ¶
IndexOfName returns the 1-based group index for a named capture, or -1 if no group has that name.
func (*MatchData) NGroups ¶
NGroups returns the number of capturing groups, not counting group 0 (the whole match).
type Regexp ¶
type Regexp struct {
// contains filtered or unexported fields
}
Regexp is a compiled regular expression, safe for concurrent use by multiple goroutines. A Regexp is immutable once compiled; WithTimeout returns a copy carrying a wall-clock match limit rather than mutating the receiver, so a shared Regexp stays concurrency-safe. The heavy matcher state lives behind the shared *machine, built lazily on first match; the copy WithTimeout returns shares that machine, so a timeout variant never rebuilds it.
func Compile ¶
Compile parses a pattern and returns a compiled Regexp in the default UTF-8 encoding, or an error if the pattern is malformed.
func CompileEnc ¶
CompileEnc is Compile with an explicit input encoding (see Encoding). UTF8 makes the dot and byte-oriented classes advance by a whole code point; ASCII8BIT makes every atom advance one byte, matching Ruby's /n.
func (*Regexp) Encoding ¶
Encoding returns the input encoding the Regexp matches under (Ruby's Regexp#encoding equivalent): UTF8 by default, ASCII8BIT for a binary pattern. It is answered from the stored encoding and does not trigger the deferred machine build.
func (*Regexp) Match ¶
Match searches s for the leftmost match and returns a *MatchData, or nil if there is no match. The search scans start positions left to right and, at the first position that matches, returns the greedy leftmost-first match (Ruby / Onigmo semantics).
If the Regexp carries a timeout (see WithTimeout) and the search exceeds it, or the internal step budget is exhausted, Match returns nil — a pathological pattern is bounded rather than allowed to run unboundedly.
func (*Regexp) MatchAt ¶
MatchAt attempts a match anchored exactly at byte offset pos in s, with \G bound to pos. It does not scan forward: it matches at pos or returns nil. The whole string s stays visible to the matcher, so the line/text anchors (^, \A) and lookbehind see the real prefix s[:pos] — exactly the semantics a StringScanner-style tokenizer needs (a Rouge RegexLexer scanning a buffer in place). Group offsets in the returned MatchData are absolute into s.
This is the faithful primitive for cursor-anchored lexing: pattern ^ matches only at a genuine line start, not merely at pos. A timeout or exhausted step budget yields nil, as with Match.
func (*Regexp) MatchBounds ¶
MatchBounds is the allocation-free, bounds-only form of Match: it scans s left to right for the leftmost match and returns its whole-match [begin, end) byte span, without building a MatchData or extracting submatches. It is the primitive for a forward StringScanner scan_until / skip_until whose captured text the caller ignores. On the lazy-NFA subset the search is the linear-time NFA on pooled state (no per-call allocation); otherwise it falls to the backtracking VM (bounded by the step budget). The span is identical to Match(s).Begin(0)/End(0).
func (*Regexp) MatchBoundsAt ¶
MatchBoundsAt is the allocation-free, bounds-only form of MatchAt: it reports the whole match's [begin, end) byte span for a match anchored exactly at pos (begin == pos on success), without building a MatchData or extracting submatches. It is the primitive for the cursor-anchored StringScanner ops that need only a length or a yes/no — skip(/…/), match?(/…/), an anchored scan whose captured text the caller ignores — mirroring Ruby's StringScanner#skip / #match?, which likewise return an integer rather than a MatchData. When the program is in the lazy-NFA subset (no backreference, call, lookaround, atomic group, or over-large bounded loop) the span is found by the linear-time NFA on pooled state, so a tokenizing loop over such a pattern allocates nothing per call; otherwise it falls to the backtracking VM. The span is identical to MatchAt(s, pos).Begin(0)/End(0). pos out of range yields ok == false.
func (*Regexp) MatchString ¶
MatchString reports whether s contains a match of the regular expression. When the program is in the lazy-NFA subset (no backreference, call, lookaround, atomic group, or over-large bounded loop) the is-match question is answered by the linear-time NFA simulation rather than the backtracking VM — one step per input position — even for a pattern with capturing groups, since whether a match exists never depends on which text the groups captured. The backtracking VM is used only for programs outside the subset.
func (*Regexp) Timeout ¶
Timeout returns the wall-clock limit applied to a single match, or zero if no limit is set.
func (*Regexp) WithTimeout ¶
WithTimeout returns a copy of the Regexp that aborts any single match taking longer than d of wall-clock time (Ruby's Regexp.timeout equivalent), returning no match. A non-positive d clears the limit. The copy shares the compiled program with the receiver, which is left unchanged, so a Regexp can be shared across goroutines and given per-use timeouts without data races.
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
ast
Package ast holds the abstract syntax tree node types for the regular expression grammar.
|
Package ast holds the abstract syntax tree node types for the regular expression grammar. |
|
charset
Package charset classifies a single Unicode code point against the property names this engine recognises for the \p{…} / \P{…} construct.
|
Package charset classifies a single Unicode code point against the property names this engine recognises for the \p{…} / \P{…} construct. |
|
compile
Package compile lowers a syntax AST into the flat instruction program that the backtracking VM executes.
|
Package compile lowers a syntax AST into the flat instruction program that the backtracking VM executes. |
|
syntax
Package syntax holds the scanner and recursive-descent parser that turn an Onigmo/Ruby regular-expression pattern into an abstract syntax tree (the node types live in the sibling ast package).
|
Package syntax holds the scanner and recursive-descent parser that turn an Onigmo/Ruby regular-expression pattern into an abstract syntax tree (the node types live in the sibling ast package). |
|
vm
Package vm executes a compiled program against an input using explicit backtracking with greedy, leftmost-first semantics (as in Ruby/Onigmo).
|
Package vm executes a compiled program against an input using explicit backtracking with greedy, leftmost-first semantics (as in Ruby/Onigmo). |
