Files
Dmitry VerkhoturovandUmputun 09110c792f Bump backend Go modules to latest
Updates every backend dependency with a newer release available, and
tidies the example module alongside as any change to backend/go.mod
requires.
2026-08-19 03:39:11 -05:00

2362 lines
54 KiB
Go

package regexp2
import (
"bytes"
"fmt"
"math"
"slices"
"strconv"
"strings"
"time"
"unicode"
"github.com/dlclark/regexp2/v2/helpers"
"github.com/dlclark/regexp2/v2/syntax"
)
type Runner struct {
re *Regexp
code *syntax.Code
debug bool
Runtextstart int // starting point for search
Runtext []rune // text to search
Runtextpos int // current position in text
Runtextend int
// The backtracking stack. Opcodes use this to store data regarding
// what they have matched and where to backtrack to. Each "frame" on
// the stack takes the form of [CodePosition Data1 Data2...], where
// CodePosition is the position of the current opcode and
// the data values are all optional. The CodePosition can be negative, and
// these values (also called "back2") are used by the BranchMark family of opcodes
// to indicate whether they are backtracking after a successful or failed
// match.
// When we backtrack, we pop the CodePosition off the stack, set the current
// instruction pointer to that code position, and mark the opcode
// with a backtracking flag ("Back"). Each opcode then knows how to
// handle its own data.
runtrack []int
Runtrackpos int
// This stack is used to track text positions across different opcodes.
// For example, in /(a*b)+/, the parentheses result in a SetMark/CaptureMark
// pair. SetMark records the text position before we match a*b. Then
// CaptureMark uses that position to figure out where the capture starts.
// Opcodes which push onto this stack are always paired with other opcodes
// which will pop the value from it later. A successful match should mean
// that this stack is empty.
runstack []int
Runstackpos int
// The crawl stack is used to keep track of captures. Every time a group
// has a capture, we push its group number onto the runcrawl stack. In
// the case of a balanced match, we push BOTH groups onto the stack.
runcrawl []int
runcrawlpos int
runtrackcount int // count of states that may do backtracking
runmatch *Match // result object
ignoreTimeout bool
timeout time.Duration // timeout in milliseconds (needed for actual)
deadline fasttime
operator syntax.InstOp
codepos int
rightToLeft bool
caseInsensitive bool
}
// run searches for matches and can continue from the previous match.
//
// quick is usually false, but can be true to not return matches, just put it in caches.
// textstart is -1 to start at the "beginning" (depending on Right-To-Left), otherwise an index in input.
// previousMatchLength is -1 for an initial scan. A zero value advances the current scan position while
// preserving textstart for anchors such as \G.
// textInfo is nil for quick scans that do not need returned capture text metadata.
func (re *Regexp) run(quick bool, textstart, previousMatchLength int, input []rune, textInfo *matchText) (*Match, error) {
// get a cached runner
runner := re.getRunner()
defer re.putRunner(runner)
if textstart < 0 {
if re.RightToLeft() {
textstart = len(input)
} else {
textstart = 0
}
}
if quick && textInfo == nil && re.quickCode != nil {
runner.code = re.quickCode
}
return runner.scan(input, textInfo, textstart, previousMatchLength, quick, re.MatchTimeout)
}
// Scans the string to find the first match. Uses the Match object
// both to feed text in and as a place to store matches that come out.
//
// All the action is in the Go() method. Our
// responsibility is to load up the class members before
// calling Go.
//
// The optimizer can compute a set of candidate starting characters,
// and we could use a separate method Skip() that will quickly scan past
// any characters that we know can't match.
//
// The input slice is passed separately from matchText so quick scans can avoid
// allocating match metadata. When textInfo is nil, successful matches are only
// used as a boolean result and capture text is intentionally unavailable. If
// we collapsed down to just textInfo it would "escape" and hit the GC for fast
// scans without captures.
func (r *Runner) scan(rt []rune, textInfo *matchText, textstart, previousMatchLength int, quick bool, timeout time.Duration) (*Match, error) {
r.timeout = timeout
r.ignoreTimeout = (time.Duration(math.MaxInt64) == timeout)
r.debug = r.re.Debug()
r.Runtextstart = textstart
r.Runtext = rt
r.Runtextend = len(rt)
// Some internal callers use quick match tidying while still consuming
// capture data (notably replacement). Capture elision is only safe when no
// match text metadata was requested.
stoppos := r.Runtextend
bump := 1
if r.re.RightToLeft() {
bump = -1
stoppos = 0
}
r.Runtextpos = textstart
//initted := false
// setup our scanner functions
findFirstChar := r.re.findFirstChar
execute := r.re.execute
if quick && textInfo == nil && r.re.executeQuick != nil {
execute = r.re.executeQuick
}
if findFirstChar == nil {
findFirstChar = findFirstCharDefault
}
if execute == nil {
execute = executeDefault
}
minRequiredLength := 0
if r.code != nil && r.code.FindOptimizations != nil {
minRequiredLength = r.code.FindOptimizations.MinRequiredLength
}
r.initMatch(textInfo)
// An empty previous match must not be returned again. Keep Runtextstart at
// the previous match position for \G, but move the candidate scan position.
if previousMatchLength == 0 {
if r.Runtextpos == stoppos {
r.tidyMatch(true)
return nil, nil
}
r.Runtextpos += bump
}
r.startTimeoutWatch()
for {
if minRequiredLength > 0 {
if r.code.RightToLeft {
if r.Runtextpos < minRequiredLength {
r.tidyMatch(true)
return nil, nil
}
} else if r.Runtextend-r.Runtextpos < minRequiredLength {
r.tidyMatch(true)
return nil, nil
}
}
if r.debug {
//fmt.Printf("\nSearch content: %v\n", string(r.runtext))
fmt.Printf("\nSearch range: from 0 to %v\n", r.Runtextend)
fmt.Printf("Firstchar search starting at %v stopping at %v\n", r.Runtextpos, stoppos)
}
if findFirstChar(r) {
if !r.ignoreTimeout {
if err := r.CheckTimeout(); err != nil {
return nil, err
}
}
if r.debug {
fmt.Printf("Executing engine starting at %v\n\n", r.Runtextpos)
}
if err := execute(r); err != nil {
return nil, err
}
if r.runmatch.matchcount[0] > 0 {
// We'll return a match even if it touches a previous empty match
return r.tidyMatch(quick), nil
}
// reset state for another go
r.Runtrackpos = len(r.runtrack)
r.Runstackpos = len(r.runstack)
r.runcrawlpos = len(r.runcrawl)
}
// failure!
if r.Runtextpos == stoppos {
r.tidyMatch(true)
return nil, nil
}
// Recognize leading []* and various anchors, and bump on failure accordingly
// r.bump by one and start again
r.Runtextpos += bump
}
// We never get here
}
func executeDefault(r *Runner) error {
if err := r.goTo(0); err != nil {
return err
}
for {
if r.debug {
r.dumpState()
}
if !r.ignoreTimeout {
if err := r.CheckTimeout(); err != nil {
return err
}
}
switch r.operator {
case syntax.Stop:
return nil
case syntax.Nothing:
//noop
case syntax.Goto:
if err := r.goTo(r.operand(0)); err != nil {
return err
}
continue
case syntax.Dispatch:
// Dispatch only peeks at the next rune. It consumes it after finding a
// matching branch, so a failed dispatch leaves the input position alone.
if r.forwardchars() < 1 {
break
}
// Pick the next rune in the current execution direction.
pos := r.Runtextpos
if r.rightToLeft {
pos--
}
ch := r.Runtext[pos]
tableIndex := r.operand(0)
table := &r.code.Dispatches[tableIndex]
branch := -1
if ch >= 0 && ch < 128 {
if table.ASCII != nil {
// Larger dispatches use a direct ASCII branch lookup.
branch = int(table.ASCII[ch]) - 1
} else {
// Smaller dispatches use two compact ASCII bitmasks per set.
word := int(ch >> 6)
bit := uint64(1) << (ch & 63)
for i := range table.Sets {
if table.ASCIIMasks[i*2+word]&bit != 0 {
branch = i
break
}
}
}
} else {
// Non-ASCII runes fall back to the complete character sets.
for i, setIndex := range table.Sets {
if r.code.Sets[setIndex].CharIn(ch) {
branch = i
break
}
}
}
if branch < 0 {
break
}
// The selected branch starts with this rune, so consume it and jump
// directly to the rest of that branch.
r.Runtextpos += r.bump()
if err := r.goTo(table.Branches[branch]); err != nil {
return err
}
continue
case syntax.Testref:
if !r.runmatch.isMatched(r.operand(0)) {
break
}
r.advance(1)
continue
case syntax.Lazybranch:
r.trackPush1(r.textPos())
r.advance(1)
continue
case syntax.Lazybranch | syntax.Back:
r.trackPop()
r.textto(r.trackPeek())
if err := r.goTo(r.operand(0)); err != nil {
return err
}
continue
case syntax.Setmark:
r.stackPush(r.textPos())
r.trackPush()
r.advance(0)
continue
case syntax.Nullmark:
r.stackPush(-1)
r.trackPush()
r.advance(0)
continue
case syntax.Setmark | syntax.Back, syntax.Nullmark | syntax.Back:
r.stackPop()
case syntax.Getmark:
r.stackPop()
r.trackPush1(r.stackPeek())
r.textto(r.stackPeek())
r.advance(0)
continue
case syntax.Getmark | syntax.Back:
r.trackPop()
r.stackPush(r.trackPeek())
case syntax.Capturemark:
if r.operand(1) != -1 && !r.runmatch.isMatched(r.operand(1)) {
break
}
r.stackPop()
if r.operand(1) != -1 {
r.transferCapture(r.operand(0), r.operand(1), r.stackPeek(), r.textPos())
} else {
r.Capture(r.operand(0), r.stackPeek(), r.textPos())
}
r.trackPush1(r.stackPeek())
r.advance(2)
continue
case syntax.Capturemark | syntax.Back:
r.trackPop()
r.stackPush(r.trackPeek())
r.uncapture()
if r.operand(0) != -1 && r.operand(1) != -1 {
r.uncapture()
}
case syntax.Branchmark:
r.stackPop()
matched := r.textPos() - r.stackPeek()
if matched != 0 { // Nonempty match -> loop now
r.trackPush2(r.stackPeek(), r.textPos()) // Save old mark, textpos
r.stackPush(r.textPos()) // Make new mark
if err := r.goTo(r.operand(0)); err != nil { // Loop
return err
}
} else { // Empty match -> straight now
r.trackPushNeg1(r.stackPeek()) // Save old mark
r.advance(1) // Straight
}
continue
case syntax.Branchmark | syntax.Back:
r.trackPopN(2)
r.stackPop()
r.textto(r.trackPeekN(1)) // Recall position
r.trackPushNeg1(r.trackPeek()) // Save old mark
r.advance(1) // Straight
continue
case syntax.Branchmark | syntax.Back2:
r.trackPop()
r.stackPush(r.trackPeek()) // Recall old mark
// Backtrack
case syntax.Lazybranchmark:
{
// We hit this the first time through a lazy loop and after each
// successful match of the inner expression. It simply continues
// on and doesn't loop.
r.stackPop()
oldMarkPos := r.stackPeek()
if r.textPos() != oldMarkPos { // Nonempty match -> try to loop again by going to 'back' state
if oldMarkPos != -1 {
r.trackPush2(oldMarkPos, r.textPos()) // Save old mark, textpos
} else {
r.trackPush2(r.textPos(), r.textPos())
}
} else {
// The inner expression found an empty match, so we'll go directly to 'back2' if we
// backtrack. Don't touch the grouping stack here; instead, record the old mark and
// a flag indicating that backtracking doesn't need to pop a grouping stack frame.
r.trackPushNeg2(oldMarkPos, 0)
}
r.advance(1)
continue
}
case syntax.Lazybranchmark | syntax.Back:
// After the first time, Lazybranchmark | syntax.Back occurs
// with each iteration of the loop, and therefore with every attempted
// match of the inner expression. We'll try to match the inner expression,
// then go back to Lazybranchmark if successful. If the inner expression
// fails, we go to Lazybranchmark | syntax.Back2
r.trackPopN(2)
pos := r.trackPeekN(1)
r.trackPushNeg2(r.trackPeek(), 1) // Save old mark, note that we pushed a new mark
r.stackPush(pos) // Make new mark
r.textto(pos) // Recall position
if err := r.goTo(r.operand(0)); err != nil { // Loop
return err
}
continue
case syntax.Lazybranchmark | syntax.Back2:
// The lazy loop has failed. We'll do a true backtrack and
// start over before the lazy loop.
r.trackPopN(2)
oldMark := r.trackPeek()
needsPop := r.trackPeekN(1)
if needsPop != 0 {
r.stackPop()
}
r.stackPush(oldMark) // Recall old mark
case syntax.Setcount:
r.stackPush2(r.textPos(), r.operand(0))
r.trackPush()
r.advance(1)
continue
case syntax.Nullcount:
r.stackPush2(-1, r.operand(0))
r.trackPush()
r.advance(1)
continue
case syntax.Setcount | syntax.Back:
r.stackPopN(2)
case syntax.Nullcount | syntax.Back:
r.stackPopN(2)
case syntax.Branchcount:
// r.stackPush:
// 0: Mark
// 1: Count
r.stackPopN(2)
mark := r.stackPeek()
count := r.stackPeekN(1)
matched := r.textPos() - mark
if count >= r.operand(1) || (matched == 0 && count >= 0) { // Max loops or empty match -> straight now
r.trackPushNeg2(mark, count) // Save old mark, count
r.advance(2) // Straight
} else { // Nonempty match -> count+loop now
r.trackPush1(mark) // remember mark
r.stackPush2(r.textPos(), count+1) // Make new mark, incr count
if err := r.goTo(r.operand(0)); err != nil { // Loop
return err
}
}
continue
case syntax.Branchcount | syntax.Back:
// r.trackPush:
// 0: Previous mark
// r.stackPush:
// 0: Mark (= current pos, discarded)
// 1: Count
r.trackPop()
r.stackPopN(2)
if r.stackPeekN(1) > 0 { // Positive -> can go straight
r.textto(r.stackPeek()) // Zap to mark
r.trackPushNeg2(r.trackPeek(), r.stackPeekN(1)-1) // Save old mark, old count
r.advance(2) // Straight
continue
}
r.stackPush2(r.trackPeek(), r.stackPeekN(1)-1) // recall old mark, old count
case syntax.Branchcount | syntax.Back2:
// r.trackPush:
// 0: Previous mark
// 1: Previous count
r.trackPopN(2)
r.stackPush2(r.trackPeek(), r.trackPeekN(1)) // Recall old mark, old count
case syntax.Lazybranchcount:
// r.stackPush:
// 0: Mark
// 1: Count
r.stackPopN(2)
mark := r.stackPeek()
count := r.stackPeekN(1)
if count < 0 { // Negative count -> loop now
r.trackPushNeg1(mark) // Save old mark
r.stackPush2(r.textPos(), count+1) // Make new mark, incr count
if err := r.goTo(r.operand(0)); err != nil { // Loop
return err
}
} else { // Nonneg count -> straight now
r.trackPush3(mark, count, r.textPos()) // Save mark, count, position
r.advance(2) // Straight
}
continue
case syntax.Lazybranchcount | syntax.Back:
// r.trackPush:
// 0: Mark
// 1: Count
// 2: r.textPos
r.trackPopN(3)
mark := r.trackPeek()
textpos := r.trackPeekN(2)
if r.trackPeekN(1) < r.operand(1) && textpos != mark { // Under limit and not empty match -> loop
r.textto(textpos) // Recall position
r.stackPush2(textpos, r.trackPeekN(1)+1) // Make new mark, incr count
r.trackPushNeg1(mark) // Save old mark
if err := r.goTo(r.operand(0)); err != nil { // Loop
return err
}
continue
} else { // Max loops or empty match -> backtrack
r.stackPush2(r.trackPeek(), r.trackPeekN(1)) // Recall old mark, count
// backtrack
}
case syntax.Lazybranchcount | syntax.Back2:
// r.trackPush:
// 0: Previous mark
// r.stackPush:
// 0: Mark (== current pos, discarded)
// 1: Count
r.trackPop()
r.stackPopN(2)
r.stackPush2(r.trackPeek(), r.stackPeekN(1)-1) // Recall old mark, count
// Backtrack
case syntax.Setjump:
r.stackPush2(r.trackpos(), r.Crawlpos())
r.trackPush()
r.advance(0)
continue
case syntax.Setjump | syntax.Back:
r.stackPopN(2)
case syntax.Backjump:
// r.stackPush:
// 0: Saved trackpos
// 1: r.crawlpos
r.stackPopN(2)
r.trackto(r.stackPeek())
for r.Crawlpos() != r.stackPeekN(1) {
r.uncapture()
}
case syntax.Forejump:
// r.stackPush:
// 0: Saved trackpos
// 1: r.crawlpos
r.stackPopN(2)
r.trackto(r.stackPeek())
r.trackPush1(r.stackPeekN(1))
r.advance(0)
continue
case syntax.Forejump | syntax.Back:
// r.trackPush:
// 0: r.crawlpos
r.trackPop()
for r.Crawlpos() != r.trackPeek() {
r.uncapture()
}
case syntax.Bol:
if r.leftchars() > 0 && r.charAt(r.textPos()-1) != '\n' {
break
}
r.advance(0)
continue
case syntax.Eol:
if r.rightchars() > 0 && r.charAt(r.textPos()) != '\n' {
break
}
r.advance(0)
continue
case syntax.Boundary:
if !r.IsBoundary(r.textPos()) {
break
}
r.advance(0)
continue
case syntax.Nonboundary:
if r.IsBoundary(r.textPos()) {
break
}
r.advance(0)
continue
case syntax.ECMABoundary:
if !r.IsECMABoundary(r.textPos()) {
break
}
r.advance(0)
continue
case syntax.NonECMABoundary:
if r.IsECMABoundary(r.textPos()) {
break
}
r.advance(0)
continue
case syntax.Beginning:
if r.leftchars() > 0 {
break
}
r.advance(0)
continue
case syntax.Start:
if r.textPos() != r.textstart() {
break
}
r.advance(0)
continue
case syntax.EndZ:
rchars := r.rightchars()
if rchars > 1 {
break
}
// RE2 and EcmaScript define $ as "asserts position at the end of the string"
// PCRE/.NET adds "or before the line terminator right at the end of the string (if any)"
if (r.re.options & (RE2 | ECMAScript)) != 0 {
// RE2/Ecmascript mode
if rchars > 0 {
break
}
} else if rchars == 1 && r.charAt(r.textPos()) != '\n' {
// "regular" mode
break
}
r.advance(0)
continue
case syntax.End:
if r.rightchars() > 0 {
break
}
r.advance(0)
continue
case syntax.One:
if r.forwardchars() < 1 || r.forwardcharnext() != rune(r.operand(0)) {
break
}
r.advance(1)
continue
case syntax.Notone:
if r.forwardchars() < 1 || r.forwardcharnext() == rune(r.operand(0)) {
break
}
r.advance(1)
continue
case syntax.Set:
if r.forwardchars() < 1 || !r.code.Sets[r.operand(0)].CharIn(r.forwardcharnext()) {
break
}
r.advance(1)
continue
case syntax.Multi:
if !r.runematch(r.code.Strings[r.operand(0)]) {
break
}
r.advance(1)
continue
case syntax.Grapheme:
if !r.TryMatchGrapheme(r.rightToLeft) {
break
}
r.advance(0)
continue
case syntax.Ref:
capnum := r.operand(0)
if r.runmatch.isMatched(capnum) {
if !r.refmatch(r.runmatch.matchIndex(capnum), r.runmatch.matchLength(capnum)) {
break
}
} else {
if (r.re.options & ECMAScript) == 0 {
break
}
}
r.advance(1)
continue
case syntax.Onerep:
c := r.operand(1)
if r.forwardchars() < c {
break
}
ch := rune(r.operand(0))
for c > 0 {
if r.forwardcharnext() != ch {
goto BreakBackward
}
c--
}
r.advance(2)
continue
case syntax.Notonerep:
c := r.operand(1)
if r.forwardchars() < c {
break
}
ch := rune(r.operand(0))
for c > 0 {
if r.forwardcharnext() == ch {
goto BreakBackward
}
c--
}
r.advance(2)
continue
case syntax.Setrep:
c := r.operand(1)
if r.forwardchars() < c {
break
}
set := r.code.Sets[r.operand(0)]
for c > 0 {
if !set.CharIn(r.forwardcharnext()) {
goto BreakBackward
}
c--
}
r.advance(2)
continue
case syntax.Oneloop, syntax.Oneloopatomic:
c := r.operand(1)
if c > r.forwardchars() {
c = r.forwardchars()
}
ch := rune(r.operand(0))
i := c
for ; i > 0; i-- {
if r.forwardcharnext() != ch {
r.backwardnext()
break
}
}
if c > i && r.operator == syntax.Oneloop {
r.trackPush2(c-i-1, r.textPos()-r.bump())
}
r.advance(2)
continue
case syntax.Notoneloop, syntax.Notoneloopatomic:
c := r.operand(1)
if c > r.forwardchars() {
c = r.forwardchars()
}
ch := rune(r.operand(0))
i := c
for ; i > 0; i-- {
if r.forwardcharnext() == ch {
r.backwardnext()
break
}
}
if c > i && r.operator == syntax.Notoneloop {
r.trackPush2(c-i-1, r.textPos()-r.bump())
}
r.advance(2)
continue
case syntax.Setloop, syntax.Setloopatomic:
c := r.operand(1)
if c > r.forwardchars() {
c = r.forwardchars()
}
set := r.code.Sets[r.operand(0)]
i := c
for ; i > 0; i-- {
if !set.CharIn(r.forwardcharnext()) {
r.backwardnext()
break
}
}
if c > i && r.operator == syntax.Setloop {
r.trackPush2(c-i-1, r.textPos()-r.bump())
}
r.advance(2)
continue
case syntax.Oneloop | syntax.Back, syntax.Notoneloop | syntax.Back:
r.trackPopN(2)
i := r.trackPeek()
pos := r.trackPeekN(1)
r.textto(pos)
if i > 0 {
r.trackPush2(i-1, pos-r.bump())
}
r.advance(2)
continue
case syntax.Setloop | syntax.Back:
r.trackPopN(2)
i := r.trackPeek()
pos := r.trackPeekN(1)
r.textto(pos)
if i > 0 {
r.trackPush2(i-1, pos-r.bump())
}
r.advance(2)
continue
case syntax.Onelazy, syntax.Notonelazy:
c := r.operand(1)
if c > r.forwardchars() {
c = r.forwardchars()
}
if c > 0 {
r.trackPush2(c-1, r.textPos())
}
r.advance(2)
continue
case syntax.Setlazy:
c := r.operand(1)
if c > r.forwardchars() {
c = r.forwardchars()
}
if c > 0 {
r.trackPush2(c-1, r.textPos())
}
r.advance(2)
continue
case syntax.Onelazy | syntax.Back:
r.trackPopN(2)
pos := r.trackPeekN(1)
r.textto(pos)
if r.forwardcharnext() != rune(r.operand(0)) {
break
}
i := r.trackPeek()
if i > 0 {
r.trackPush2(i-1, pos+r.bump())
}
r.advance(2)
continue
case syntax.Notonelazy | syntax.Back:
r.trackPopN(2)
pos := r.trackPeekN(1)
r.textto(pos)
if r.forwardcharnext() == rune(r.operand(0)) {
break
}
i := r.trackPeek()
if i > 0 {
r.trackPush2(i-1, pos+r.bump())
}
r.advance(2)
continue
case syntax.Setlazy | syntax.Back:
r.trackPopN(2)
pos := r.trackPeekN(1)
r.textto(pos)
if !r.code.Sets[r.operand(0)].CharIn(r.forwardcharnext()) {
break
}
i := r.trackPeek()
if i > 0 {
r.trackPush2(i-1, pos+r.bump())
}
r.advance(2)
continue
case syntax.UpdateBumpalong:
// UpdateBumpalong should only exist in the code stream at such a point where the root
// of the backtracking stack contains the runtextpos from the start of this Go call. Replace
// that tracking value with the current runtextpos value if it's greater.
trackingpos := r.runtrack[len(r.runtrack)-1]
if trackingpos < r.Runtextpos {
r.runtrack[len(r.runtrack)-1] = r.Runtextpos
}
r.advance(0)
continue
default:
return fmt.Errorf("unknown state in regex runner: %v", r.operator)
}
BreakBackward:
;
// "break Backward" comes here:
if err := r.backtrack(); err != nil {
return err
}
}
}
// increase the size of stack and track storage
func (r *Runner) ensureStorage() error {
if r.Runstackpos < r.runtrackcount*4 {
doubleIntSlice(&r.runstack, &r.Runstackpos)
}
if r.Runtrackpos < r.runtrackcount*4 && !r.growTrack() {
return ErrBacktrackingStackLimit
}
return nil
}
func (r *Runner) ensureStack(plus int) {
if r.Runstackpos-plus < r.runtrackcount*4 {
doubleIntSlice(&r.runstack, &r.Runstackpos)
}
}
func doubleIntSlice(s *[]int, pos *int) {
oldLen := len(*s)
newS := make([]int, oldLen*2)
copy(newS[oldLen:], *s)
*pos += oldLen
*s = newS
}
// Save a number on the longjump unrolling stack
func (r *Runner) crawl(i int) {
if r.runcrawlpos == 0 {
doubleIntSlice(&r.runcrawl, &r.runcrawlpos)
}
r.runcrawlpos--
r.runcrawl[r.runcrawlpos] = i
}
// Remove a number from the longjump unrolling stack
func (r *Runner) popcrawl() int {
val := r.runcrawl[r.runcrawlpos]
r.runcrawlpos++
return val
}
// Get the height of the stack
func (r *Runner) Crawlpos() int {
return len(r.runcrawl) - r.runcrawlpos
}
func (r *Runner) advance(i int) {
r.codepos += (i + 1)
r.setOperator(r.code.Codes[r.codepos])
}
func (r *Runner) goTo(newpos int) error {
// when branching backward or in place, ensure storage
if newpos <= r.codepos {
if err := r.ensureStorage(); err != nil {
return err
}
}
r.setOperator(r.code.Codes[newpos])
r.codepos = newpos
return nil
}
func (r *Runner) textto(newpos int) {
r.Runtextpos = newpos
}
func (r *Runner) trackto(newpos int) {
r.Runtrackpos = len(r.runtrack) - newpos
}
func (r *Runner) textstart() int {
return r.Runtextstart
}
func (r *Runner) textPos() int {
return r.Runtextpos
}
// TryMatchGrapheme consumes one Unicode extended grapheme cluster in the
// runner's current direction. It is exported for regexp2cg-generated engines.
func (r *Runner) TryMatchGrapheme(rightToLeft bool) bool {
var boundary int
if rightToLeft {
boundary = syntax.PreviousGraphemeClusterBoundary(r.Runtext, r.Runtextpos)
} else {
boundary = syntax.NextGraphemeClusterBoundary(r.Runtext, r.Runtextpos)
}
if boundary < 0 {
return false
}
r.Runtextpos = boundary
return true
}
// push onto the backtracking stack
func (r *Runner) trackpos() int {
return len(r.runtrack) - r.Runtrackpos
}
func (r *Runner) growTrack() bool {
oldLen := len(r.runtrack)
newLen := oldLen * 2
if newLen == 0 {
newLen = 1
}
if limit := r.re.optimizations.MaxBacktrackingStackSize; limit >= 0 && newLen > limit {
newLen = limit
}
if newLen <= oldLen {
return false
}
newTrack := make([]int, newLen)
copy(newTrack[newLen-oldLen:], r.runtrack)
r.Runtrackpos += newLen - oldLen
r.runtrack = newTrack
return true
}
func (r *Runner) trackPush() {
r.Runtrackpos--
r.runtrack[r.Runtrackpos] = r.codepos
}
func (r *Runner) trackPush1(I1 int) {
r.Runtrackpos--
r.runtrack[r.Runtrackpos] = I1
r.Runtrackpos--
r.runtrack[r.Runtrackpos] = r.codepos
}
func (r *Runner) trackPush2(I1, I2 int) {
r.Runtrackpos--
r.runtrack[r.Runtrackpos] = I1
r.Runtrackpos--
r.runtrack[r.Runtrackpos] = I2
r.Runtrackpos--
r.runtrack[r.Runtrackpos] = r.codepos
}
func (r *Runner) trackPush3(I1, I2, I3 int) {
r.Runtrackpos--
r.runtrack[r.Runtrackpos] = I1
r.Runtrackpos--
r.runtrack[r.Runtrackpos] = I2
r.Runtrackpos--
r.runtrack[r.Runtrackpos] = I3
r.Runtrackpos--
r.runtrack[r.Runtrackpos] = r.codepos
}
func (r *Runner) trackPushNeg1(I1 int) {
r.Runtrackpos--
r.runtrack[r.Runtrackpos] = I1
r.Runtrackpos--
r.runtrack[r.Runtrackpos] = -r.codepos
}
func (r *Runner) trackPushNeg2(I1, I2 int) {
r.Runtrackpos--
r.runtrack[r.Runtrackpos] = I1
r.Runtrackpos--
r.runtrack[r.Runtrackpos] = I2
r.Runtrackpos--
r.runtrack[r.Runtrackpos] = -r.codepos
}
func (r *Runner) backtrack() error {
newpos := r.runtrack[r.Runtrackpos]
r.Runtrackpos++
if r.debug {
if newpos < 0 {
fmt.Printf(" Backtracking (back2) to code position %v\n", -newpos)
} else {
fmt.Printf(" Backtracking to code position %v\n", newpos)
}
}
if newpos < 0 {
newpos = -newpos
r.setOperator(r.code.Codes[newpos] | int(syntax.Back2))
} else {
r.setOperator(r.code.Codes[newpos] | int(syntax.Back))
}
// When branching backward, ensure storage
if newpos < r.codepos {
if err := r.ensureStorage(); err != nil {
return err
}
}
r.codepos = newpos
return nil
}
func (r *Runner) setOperator(op int) {
r.caseInsensitive = (0 != (op & int(syntax.Ci)))
r.rightToLeft = (0 != (op & int(syntax.Rtl)))
r.operator = syntax.InstOp(op & ^int(syntax.Rtl|syntax.Ci))
}
func (r *Runner) trackPop() {
r.Runtrackpos++
}
// pop framesize items from the backtracking stack
func (r *Runner) trackPopN(framesize int) {
r.Runtrackpos += framesize
}
// Technically we are actually peeking at items already popped. So if you want to
// get and pop the top item from the stack, you do
// r.trackPop();
// r.trackPeek();
func (r *Runner) trackPeek() int {
return r.runtrack[r.Runtrackpos-1]
}
// get the ith element down on the backtracking stack
func (r *Runner) trackPeekN(i int) int {
return r.runtrack[r.Runtrackpos-i-1]
}
// Push onto the grouping stack
func (r *Runner) stackPush(I1 int) {
r.Runstackpos--
r.runstack[r.Runstackpos] = I1
}
func (r *Runner) stackPush2(I1, I2 int) {
r.Runstackpos--
r.runstack[r.Runstackpos] = I1
r.Runstackpos--
r.runstack[r.Runstackpos] = I2
}
func (r *Runner) stackPop() {
r.Runstackpos++
}
// pop framesize items from the grouping stack
func (r *Runner) stackPopN(framesize int) {
r.Runstackpos += framesize
}
// Technically we are actually peeking at items already popped. So if you want to
// get and pop the top item from the stack, you do
// r.stackPop();
// r.stackPeek();
func (r *Runner) stackPeek() int {
return r.runstack[r.Runstackpos-1]
}
// get the ith element down on the grouping stack
func (r *Runner) stackPeekN(i int) int {
return r.runstack[r.Runstackpos-i-1]
}
func (r *Runner) operand(i int) int {
return r.code.Codes[r.codepos+i+1]
}
func (r *Runner) leftchars() int {
return r.Runtextpos
}
func (r *Runner) rightchars() int {
return r.Runtextend - r.Runtextpos
}
func (r *Runner) bump() int {
if r.rightToLeft {
return -1
}
return 1
}
func (r *Runner) forwardchars() int {
if r.rightToLeft {
return r.Runtextpos
}
return r.Runtextend - r.Runtextpos
}
func (r *Runner) forwardcharnext() rune {
var ch rune
if r.rightToLeft {
r.Runtextpos--
ch = r.Runtext[r.Runtextpos]
} else {
ch = r.Runtext[r.Runtextpos]
r.Runtextpos++
}
// move this to compile time for individual runes
/*if r.caseInsensitive {
return unicode.ToLower(ch)
}*/
return ch
}
func (r *Runner) runematch(str []rune) bool {
var pos int
c := len(str)
if !r.rightToLeft {
if r.Runtextend-r.Runtextpos < c {
return false
}
pos = r.Runtextpos + c
} else {
if r.Runtextpos-0 < c {
return false
}
pos = r.Runtextpos
}
if !r.caseInsensitive {
for c != 0 {
c--
pos--
if str[c] != r.Runtext[pos] {
return false
}
}
} else {
for c != 0 {
c--
pos--
if str[c] != unicode.ToLower(r.Runtext[pos]) {
return false
}
}
}
if !r.rightToLeft {
pos += len(str)
}
r.Runtextpos = pos
return true
}
func (r *Runner) refmatch(index, len int) bool {
var c, pos, cmpos int
if !r.rightToLeft {
if r.Runtextend-r.Runtextpos < len {
return false
}
pos = r.Runtextpos + len
} else {
if r.Runtextpos-0 < len {
return false
}
pos = r.Runtextpos
}
cmpos = index + len
c = len
if !r.caseInsensitive {
for c != 0 {
c--
cmpos--
pos--
if r.Runtext[cmpos] != r.Runtext[pos] {
return false
}
}
} else {
for c != 0 {
c--
cmpos--
pos--
if unicode.ToLower(r.Runtext[cmpos]) != unicode.ToLower(r.Runtext[pos]) {
return false
}
}
}
if !r.rightToLeft {
pos += len
}
r.Runtextpos = pos
return true
}
func (r *Runner) backwardnext() {
if r.rightToLeft {
r.Runtextpos++
} else {
r.Runtextpos--
}
}
func (r *Runner) charAt(j int) rune {
return r.Runtext[j]
}
func findFirstCharDefault(r *Runner) bool {
if 0 != (r.code.Anchors & (syntax.AnchorBeginning | syntax.AnchorStart | syntax.AnchorEndZ | syntax.AnchorEnd)) {
if !r.code.RightToLeft {
if (0 != (r.code.Anchors&syntax.AnchorBeginning) && r.Runtextpos > 0) ||
(0 != (r.code.Anchors&syntax.AnchorStart) && r.Runtextpos > r.Runtextstart) {
r.Runtextpos = r.Runtextend
return false
}
if 0 != (r.code.Anchors&syntax.AnchorEndZ) && r.Runtextpos < r.Runtextend-1 {
r.Runtextpos = r.Runtextend - 1
} else if 0 != (r.code.Anchors&syntax.AnchorEnd) && r.Runtextpos < r.Runtextend {
r.Runtextpos = r.Runtextend
}
} else {
if (0 != (r.code.Anchors&syntax.AnchorEnd) && r.Runtextpos < r.Runtextend) ||
(0 != (r.code.Anchors&syntax.AnchorEndZ) && (r.Runtextpos < r.Runtextend-1 ||
(r.Runtextpos == r.Runtextend-1 && r.charAt(r.Runtextpos) != '\n'))) ||
(0 != (r.code.Anchors&syntax.AnchorStart) && r.Runtextpos < r.Runtextstart) {
r.Runtextpos = 0
return false
}
if 0 != (r.code.Anchors&syntax.AnchorBeginning) && r.Runtextpos > 0 {
r.Runtextpos = 0
}
}
if r.code.BmPrefix != nil {
return r.code.BmPrefix.IsMatch(r.Runtext, r.Runtextpos, 0, r.Runtextend)
}
return true // found a valid start or end anchor
} else if r.code.BmPrefix != nil {
r.Runtextpos = r.code.BmPrefix.Scan(r.Runtext, r.Runtextpos, 0, r.Runtextend)
if r.Runtextpos == -1 {
if r.code.RightToLeft {
r.Runtextpos = 0
} else {
r.Runtextpos = r.Runtextend
}
return false
}
return true
}
if shouldUseFindFirstCharOptimized(r) {
if handled, found := findFirstCharOptimized(r); handled {
return found
}
}
if r.code.FcPrefix == nil {
return true
}
r.rightToLeft = r.code.RightToLeft
r.caseInsensitive = r.code.FcPrefix.CaseInsensitive
set := r.code.FcPrefix.PrefixSet
if set.IsSingleton() {
ch := set.SingletonChar()
for i := r.forwardchars(); i > 0; i-- {
if ch == r.forwardcharnext() {
r.backwardnext()
return true
}
}
} else {
for i := r.forwardchars(); i > 0; i-- {
n := r.forwardcharnext()
//fmt.Printf("%v in %v: %v\n", string(n), set.String(), set.CharIn(n))
if set.CharIn(n) {
r.backwardnext()
return true
}
}
}
return false
}
func shouldUseFindFirstCharOptimized(r *Runner) bool {
if r.code == nil || r.code.FindOptimizations == nil {
return false
}
opts := r.code.FindOptimizations
switch opts.FindMode {
case syntax.TrailingAnchor_FixedLength_LeftToRight_End,
syntax.LeadingString_OrdinalIgnoreCase_LeftToRight,
syntax.LeadingStrings_LeftToRight,
syntax.LeadingStrings_OrdinalIgnoreCase_LeftToRight,
syntax.FixedDistanceChar_LeftToRight,
syntax.FixedDistanceString_LeftToRight,
syntax.FixedDistanceSets_LeftToRight,
syntax.LiteralAfterLoop_LeftToRight,
syntax.RequiredLandmarkChain_LeftToRight:
return true
case syntax.LeadingSet_LeftToRight:
// General Unicode sets already have a direct fallback loop below.
// Large enumerated sets are also faster through the set's ASCII bitmap
// than through the linear IndexOfAny helper.
return len(opts.FixedDistanceSets) > 0 &&
((len(opts.FixedDistanceSets[0].Chars) > 0 && len(opts.FixedDistanceSets[0].Chars) <= 5) ||
opts.FixedDistanceSets[0].Range != nil)
default:
return false
}
}
func findFirstCharOptimized(r *Runner) (handled bool, found bool) {
if r.code == nil || r.code.FindOptimizations == nil {
return false, false
}
opts := r.code.FindOptimizations
switch opts.FindMode {
case syntax.NoSearch:
return false, false
case syntax.TrailingAnchor_FixedLength_LeftToRight_End:
return true, findTrailingFixedLengthEnd(r, opts.MinRequiredLength)
case syntax.LeadingString_LeftToRight:
return true, findLeadingStringLeftToRight(r, []rune(opts.LeadingPrefix), false)
case syntax.LeadingString_OrdinalIgnoreCase_LeftToRight:
return true, findLeadingStringLeftToRight(r, []rune(opts.LeadingPrefix), true)
case syntax.LeadingStrings_LeftToRight:
return true, findLeadingStringsLeftToRight(r, opts.LeadingPrefixesRunes, opts.LeadingPrefixFirstRunes, false)
case syntax.LeadingStrings_OrdinalIgnoreCase_LeftToRight:
return true, findLeadingStringsLeftToRight(r, opts.LeadingPrefixesRunes, opts.LeadingPrefixFirstRunes, true)
case syntax.LeadingSet_LeftToRight, syntax.FixedDistanceSets_LeftToRight:
return true, findFixedDistanceSetsLeftToRight(r, opts.FixedDistanceSets)
case syntax.FixedDistanceChar_LeftToRight:
return true, findFixedDistanceCharLeftToRight(r, opts.FixedDistanceLiteral.C, opts.FixedDistanceLiteral.Distance)
case syntax.FixedDistanceString_LeftToRight:
return true, findFixedDistanceStringLeftToRight(r, []rune(opts.FixedDistanceLiteral.S), opts.FixedDistanceLiteral.Distance)
case syntax.LiteralAfterLoop_LeftToRight:
return true, findLiteralAfterLoopLeftToRight(r, opts.LiteralAfterLoop)
case syntax.RequiredLandmarkChain_LeftToRight:
return true, findRequiredLandmarkChainLeftToRight(r, opts.LandmarkChain)
default:
return false, false
}
}
func findTrailingFixedLengthEnd(r *Runner, fixedLength int) bool {
start := r.Runtextend - fixedLength
if start < r.Runtextpos || start < 0 {
r.Runtextpos = r.Runtextend
return false
}
r.Runtextpos = start
return true
}
func findLeadingStringLeftToRight(r *Runner, prefix []rune, ignoreCase bool) bool {
if len(prefix) == 0 {
return true
}
search := r.Runtext[r.Runtextpos:]
var offset int
if ignoreCase {
if isASCIIRunes(prefix) {
offset = helpers.IndexOfIgnoreCaseAscii(search, prefix)
} else {
offset = helpers.IndexOfIgnoreCase(search, prefix)
}
} else {
offset = helpers.IndexOf(search, prefix)
}
if offset < 0 {
r.Runtextpos = r.Runtextend
return false
}
start := r.Runtextpos + offset
if !hasRequiredLengthAt(r, start) {
r.Runtextpos = r.Runtextend
return false
}
r.Runtextpos = start
return true
}
func findLeadingStringsLeftToRight(r *Runner, prefixes [][]rune, firstRunes []rune, ignoreCase bool) bool {
if len(prefixes) == 0 {
return false
}
// Unicode ordinal-ignore-case matching has more possible first-rune folds
// than a small precomputed set can safely represent. Keep its conservative
// position-by-position scan; the common case-sensitive path skips directly
// between possible first runes.
if ignoreCase || len(firstRunes) == 0 {
for start := r.Runtextpos; start <= latestPossibleStart(r); start++ {
for _, prefix := range prefixes {
if ignoreCase {
if helpers.StartsWithIgnoreCase(r.Runtext[start:], prefix) {
r.Runtextpos = start
return true
}
} else if helpers.StartsWith(r.Runtext[start:], prefix) {
r.Runtextpos = start
return true
}
}
}
r.Runtextpos = r.Runtextend
return false
}
latest := min(latestPossibleStart(r), r.Runtextend-1)
for searchAt := r.Runtextpos; searchAt <= latest; {
offset := indexOfAnyRunes(r.Runtext[searchAt:latest+1], firstRunes)
if offset < 0 {
break
}
start := searchAt + offset
first := r.Runtext[start]
for _, prefix := range prefixes {
if len(prefix) > 0 && prefix[0] == first && helpers.StartsWith(r.Runtext[start:], prefix) {
r.Runtextpos = start
return true
}
}
searchAt = start + 1
}
r.Runtextpos = r.Runtextend
return false
}
func indexOfAnyRunes(input, find []rune) int {
switch len(find) {
case 0:
return -1
case 1:
return helpers.IndexOfAny1(input, find[0])
case 2:
return helpers.IndexOfAny2(input, find[0], find[1])
case 3:
return helpers.IndexOfAny3(input, find[0], find[1], find[2])
default:
return helpers.IndexOfAny(input, find)
}
}
func findFixedDistanceCharLeftToRight(r *Runner, ch rune, distance int) bool {
searchStart := r.Runtextpos + distance
for searchStart < r.Runtextend {
offset := helpers.IndexOfAny1(r.Runtext[searchStart:], ch)
if offset < 0 {
r.Runtextpos = r.Runtextend
return false
}
literalIndex := searchStart + offset
start := literalIndex - distance
if start >= r.Runtextpos && hasRequiredLengthAt(r, start) {
r.Runtextpos = start
return true
}
if start > latestPossibleStart(r) {
break
}
searchStart = literalIndex + 1
}
r.Runtextpos = r.Runtextend
return false
}
func findFixedDistanceStringLeftToRight(r *Runner, literal []rune, distance int) bool {
if len(literal) == 0 {
return true
}
searchStart := r.Runtextpos + distance
for searchStart <= r.Runtextend-len(literal) {
offset := helpers.IndexOf(r.Runtext[searchStart:], literal)
if offset < 0 {
r.Runtextpos = r.Runtextend
return false
}
literalIndex := searchStart + offset
start := literalIndex - distance
if start >= r.Runtextpos && hasRequiredLengthAt(r, start) {
r.Runtextpos = start
return true
}
if start > latestPossibleStart(r) {
break
}
searchStart = literalIndex + 1
}
r.Runtextpos = r.Runtextend
return false
}
func findFixedDistanceSetsLeftToRight(r *Runner, sets []syntax.FixedDistanceSet) bool {
if len(sets) == 0 || sets[0].Set == nil {
return false
}
primary := sets[0]
searchStart := r.Runtextpos + primary.Distance
for searchStart < r.Runtextend {
offset := indexOfSet(r.Runtext[searchStart:], primary)
if offset < 0 {
r.Runtextpos = r.Runtextend
return false
}
charIndex := searchStart + offset
start := charIndex - primary.Distance
if start > latestPossibleStart(r) {
break
}
if start >= r.Runtextpos && hasRequiredLengthAt(r, start) && fixedDistanceSetsMatchAt(r, sets, start) {
r.Runtextpos = start
return true
}
searchStart = charIndex + 1
}
r.Runtextpos = r.Runtextend
return false
}
func findLiteralAfterLoopLeftToRight(r *Runner, literal *syntax.LiteralAfterLoop) bool {
if literal == nil || literal.LoopNode == nil || literal.LoopNode.Set == nil {
return false
}
searchStart := r.Runtextpos
for searchStart < r.Runtextend {
literalIndex := indexOfLiteralAfterLoop(r, literal, searchStart)
if literalIndex < 0 {
r.Runtextpos = r.Runtextend
return false
}
start := literalIndex
for start > r.Runtextpos && literal.LoopNode.Set.CharIn(r.Runtext[start-1]) {
start--
}
if hasRequiredLengthAt(r, start) {
r.Runtextpos = start
return true
}
searchStart = literalIndex + 1
}
r.Runtextpos = r.Runtextend
return false
}
func findRequiredLandmarkChainLeftToRight(r *Runner, chain *syntax.RequiredLandmarkChain) bool {
if chain == nil || chain.LeadingLoopSet == nil || len(chain.Landmarks) == 0 {
return false
}
for searchStart := r.Runtextpos; searchStart <= latestPossibleStart(r); {
first, ok := findNextRequiredLandmarkRunes(r.Runtext, searchStart, r.Runtextend, chain.Landmarks[0])
if !ok {
r.Runtextpos = r.Runtextend
return false
}
nextStart := first.End
for i := 1; i < len(chain.Landmarks); i++ {
landmark, ok := findNextRequiredLandmarkRunes(r.Runtext, nextStart, r.Runtextend, chain.Landmarks[i])
if !ok {
r.Runtextpos = r.Runtextend
return false
}
nextStart = landmark.End
}
candidate := first.Start
if candidate < r.Runtextpos {
candidate = r.Runtextpos
}
for candidate > r.Runtextpos && chain.LeadingLoopSet.CharIn(r.Runtext[candidate-1]) {
candidate--
}
if hasRequiredLengthAt(r, candidate) {
r.Runtextpos = candidate
return true
}
searchStart = first.CoreStart + 1
}
r.Runtextpos = r.Runtextend
return false
}
type requiredLandmarkMatch struct {
Start int
CoreStart int
End int
}
func findNextRequiredLandmarkRunes(input []rune, startAt, endAt int, landmark syntax.RequiredLandmark) (requiredLandmarkMatch, bool) {
for i := startAt; i < endAt; i++ {
for _, alt := range landmark.Alternatives {
if match, ok := requiredLandmarkAlternativeMatch(input, i, endAt, alt); ok {
return match, true
}
}
}
return requiredLandmarkMatch{}, false
}
func requiredLandmarkAlternativeMatch(input []rune, start, endAt int, alt syntax.RequiredLandmarkAlternative) (requiredLandmarkMatch, bool) {
if alt.RequireWhitespaceBefore &&
(start == 0 || alt.LeadingWhitespaceSet == nil || !alt.LeadingWhitespaceSet.CharIn(input[start-1])) {
return requiredLandmarkMatch{}, false
}
var end int
if len(alt.Literal) > 0 {
if start+len(alt.Literal) > endAt || !helpers.StartsWith(input[start:], alt.Literal) {
return requiredLandmarkMatch{}, false
}
end = start + len(alt.Literal)
} else if alt.Set != nil && alt.MinRepeat > 0 {
end = start
maxRepeat := alt.MaxRepeat
if maxRepeat <= 0 {
maxRepeat = alt.MinRepeat
}
for end < endAt && end-start < maxRepeat && alt.Set.CharIn(input[end]) {
end++
}
if end-start < alt.MinRepeat {
return requiredLandmarkMatch{}, false
}
} else {
return requiredLandmarkMatch{}, false
}
if alt.RequireWhitespaceAfter &&
(end >= endAt || alt.TrailingWhitespaceSet == nil || !alt.TrailingWhitespaceSet.CharIn(input[end])) {
return requiredLandmarkMatch{}, false
}
matchStart := start
for matchStart > 0 && alt.LeadingWhitespaceSet != nil && alt.LeadingWhitespaceSet.CharIn(input[matchStart-1]) {
matchStart--
}
return requiredLandmarkMatch{Start: matchStart, CoreStart: start, End: end}, true
}
func indexOfLiteralAfterLoop(r *Runner, literal *syntax.LiteralAfterLoop, searchStart int) int {
switch {
case literal.String != "":
needle := []rune(literal.String)
if literal.StringIgnoreCase {
var offset int
if isASCIIString(literal.String) {
offset = helpers.IndexOfIgnoreCaseAscii(r.Runtext[searchStart:], needle)
} else {
offset = helpers.IndexOfIgnoreCase(r.Runtext[searchStart:], needle)
}
if offset >= 0 {
return searchStart + offset
}
} else if offset := helpers.IndexOf(r.Runtext[searchStart:], needle); offset >= 0 {
return searchStart + offset
}
case len(literal.Chars) > 0:
if offset := helpers.IndexOfAny(r.Runtext[searchStart:], literal.Chars); offset >= 0 {
return searchStart + offset
}
default:
if offset := helpers.IndexOfAny1(r.Runtext[searchStart:], literal.Char); offset >= 0 {
return searchStart + offset
}
}
return -1
}
func isASCIIRunes(in []rune) bool {
for _, ch := range in {
if ch > unicode.MaxASCII {
return false
}
}
return true
}
func indexOfSet(chars []rune, set syntax.FixedDistanceSet) int {
if len(set.Chars) > 0 && !set.Negated {
return helpers.IndexOfAny(chars, set.Chars)
}
if len(set.Chars) > 0 && set.Negated {
return helpers.IndexOfAnyExcept(chars, set.Chars)
}
if set.Range != nil {
if set.Negated {
return helpers.IndexOfAnyExceptInRange(chars, set.Range.First, set.Range.Last)
}
return helpers.IndexOfAnyInRange(chars, set.Range.First, set.Range.Last)
}
return helpers.IndexFunc(chars, func(ch rune) bool {
return charInFixedDistanceSet(set, ch)
})
}
func fixedDistanceSetsMatchAt(r *Runner, sets []syntax.FixedDistanceSet, start int) bool {
for _, set := range sets {
index := start + set.Distance
if index < 0 || index >= r.Runtextend || !charInFixedDistanceSet(set, r.Runtext[index]) {
return false
}
}
return true
}
func charInFixedDistanceSet(set syntax.FixedDistanceSet, ch rune) bool {
if len(set.Chars) > 0 {
found := slices.Contains(set.Chars, ch)
if set.Negated {
return !found
}
return found
}
if set.Range != nil {
found := ch >= set.Range.First && ch <= set.Range.Last
if set.Negated {
return !found
}
return found
}
return set.Set != nil && set.Set.CharIn(ch)
}
func latestPossibleStart(r *Runner) int {
if r.code == nil || r.code.FindOptimizations == nil {
return r.Runtextend
}
minRequiredLength := r.code.FindOptimizations.MinRequiredLength
if minRequiredLength <= 0 {
return r.Runtextend
}
return r.Runtextend - minRequiredLength
}
func hasRequiredLengthAt(r *Runner, start int) bool {
return start >= 0 && start <= latestPossibleStart(r)
}
func (r *Runner) initMatch(textInfo *matchText) {
// Use a hashtable'ed Match object if the capture numbers are sparse
if r.runmatch == nil {
if r.re.caps != nil {
r.runmatch = newMatchSparse(r.re, r.re.caps, r.re.capsize, textInfo, r.Runtextstart)
} else {
r.runmatch = newMatch(r.re, r.re.capsize, textInfo, r.Runtextstart)
}
} else {
r.runmatch.reset(textInfo, r.Runtextstart)
}
// note we test runcrawl, because it is the last one to be allocated
// If there is an alloc failure in the middle of the three allocations,
// we may still return to reuse this instance, and we want to behave
// as if the allocations didn't occur. (we used to test _trackcount != 0)
if r.runcrawl != nil {
r.Runtrackpos = len(r.runtrack)
r.Runstackpos = len(r.runstack)
r.runcrawlpos = len(r.runcrawl)
return
}
r.initTrackCount()
tracksize := r.runtrackcount * 8
stacksize := r.runtrackcount * 8
if tracksize < 64 {
tracksize = 64
}
if limit := r.re.optimizations.MaxBacktrackingStackSize; limit >= 0 && tracksize > limit {
tracksize = limit
}
if stacksize < 32 {
stacksize = 32
}
r.runtrack = make([]int, tracksize)
r.Runtrackpos = tracksize
r.runstack = make([]int, stacksize)
r.Runstackpos = stacksize
r.runcrawl = make([]int, 32)
r.runcrawlpos = 32
}
func (r *Runner) tidyMatch(quick bool) *Match {
if !quick {
match := r.runmatch
r.runmatch = nil
match.tidy(r.Runtextpos)
return match
} else {
// send back our match -- it's not leaving the package, so it's safe to not clean it up
// this reduces allocs for frequent calls to the "IsMatch" bool-only functions
m := r.runmatch
if m == nil {
return nil
}
m.textpos = r.Runtextpos
if m.matchcount[0] > 0 {
interval := m.matches[0]
setCaptureFields(&m.Capture, interval[0], interval[1])
}
return m
}
}
// Capture captures a subexpression. Note that the
// capnum used here has already been mapped to a non-sparse
// index (by the code generator RegexWriter).
func (r *Runner) Capture(capnum, start, end int) {
if end < start {
T := end
end = start
start = T
}
r.crawl(capnum)
r.runmatch.addMatch(capnum, start, end-start)
}
// transferCapture captures a subexpression. Note that the
// capnum used here has already been mapped to a non-sparse
// index (by the code generator RegexWriter).
func (r *Runner) transferCapture(capnum, uncapnum, start, end int) {
var start2, end2 int
// these are the two intervals that are cancelling each other
if end < start {
T := end
end = start
start = T
}
start2 = r.runmatch.matchIndex(uncapnum)
end2 = start2 + r.runmatch.matchLength(uncapnum)
// The new capture gets the innermost defined interval
if start >= end2 {
end = start
start = end2
} else if end <= start2 {
start = start2
} else {
if end > end2 {
end = end2
}
if start2 > start {
start = start2
}
}
r.crawl(uncapnum)
r.runmatch.balanceMatch(uncapnum)
if capnum != -1 {
r.crawl(capnum)
r.runmatch.addMatch(capnum, start, end-start)
}
}
// revert the last capture
func (r *Runner) uncapture() {
capnum := r.popcrawl()
r.runmatch.removeMatch(capnum)
}
//debug
func (r *Runner) dumpState() {
back := ""
if r.operator&syntax.Back != 0 {
back = " Back"
}
if r.operator&syntax.Back2 != 0 {
back += " Back2"
}
fmt.Printf("Text: %v\nTrack: %v\nStack: %v\n %s%s\n\n",
r.textposDescription(),
r.stackDescription(r.runtrack, r.Runtrackpos),
r.stackDescription(r.runstack, r.Runstackpos),
r.code.OpcodeDescription(r.codepos),
back)
}
func (r *Runner) stackDescription(a []int, index int) string {
buf := &bytes.Buffer{}
fmt.Fprintf(buf, "%v/%v", len(a)-index, len(a))
if buf.Len() < 8 {
buf.WriteString(strings.Repeat(" ", 8-buf.Len()))
}
buf.WriteRune('(')
for i := index; i < len(a); i++ {
if i > index {
buf.WriteRune(' ')
}
buf.WriteString(strconv.Itoa(a[i]))
}
buf.WriteRune(')')
return buf.String()
}
func (r *Runner) textposDescription() string {
buf := &bytes.Buffer{}
buf.WriteString(strconv.Itoa(r.Runtextpos))
if buf.Len() < 8 {
buf.WriteString(strings.Repeat(" ", 8-buf.Len()))
}
if r.Runtextpos > 0 {
buf.WriteString(syntax.CharDescription(r.Runtext[r.Runtextpos-1]))
} else {
buf.WriteRune('^')
}
buf.WriteRune('>')
for i := r.Runtextpos; i < r.Runtextend; i++ {
buf.WriteString(syntax.CharDescription(r.Runtext[i]))
}
if buf.Len() >= 64 {
buf.Truncate(61)
buf.WriteString("...")
} else {
buf.WriteRune('$')
}
return buf.String()
}
// decide whether the pos
// at the specified index is a boundary or not. It's just not worth
// emitting inline code for this logic.
func (r *Runner) IsBoundary(index int) bool {
return (index > 0 && syntax.IsWordChar(r.Runtext[index-1])) !=
(index < r.Runtextend && syntax.IsWordChar(r.Runtext[index]))
}
func (r *Runner) IsECMABoundary(index int) bool {
return (index > 0 && syntax.IsECMAWordChar(r.Runtext[index-1])) !=
(index < r.Runtextend && syntax.IsECMAWordChar(r.Runtext[index]))
}
func (r *Runner) startTimeoutWatch() {
if r.ignoreTimeout {
return
}
r.deadline = makeDeadline(r.timeout)
}
func (r *Runner) CheckTimeout() error {
if r.ignoreTimeout || !r.deadline.reached() {
return nil
}
return fmt.Errorf("match timeout after %v on input `%v`", r.timeout, string(r.Runtext))
}
func (r *Runner) initTrackCount() {
if r.code != nil {
r.runtrackcount = r.code.TrackCount
}
}
// getRunner returns a runner to use for matching re.
func (re *Regexp) getRunner() *Runner {
if re.runnerPool == nil {
re.initCaches()
}
return re.runnerPool.Get().(*Runner)
}
// putRunner returns a runner to the re's pool cache.
func (re *Regexp) putRunner(r *Runner) {
r.Runtext = nil
r.code = re.code
if r.runmatch != nil {
r.runmatch.text = nil
}
re.runnerPool.Put(r)
}
func (r *Runner) LastIndexOfRune(startIndex int, endIndex int, find rune) int {
for i := endIndex - 1; i >= startIndex; i-- {
if r.Runtext[i] == find {
return i
}
}
return -1
}
// Undo captures until it reaches the specified capture position
func (r *Runner) UncaptureUntil(capturePos int) {
for r.Crawlpos() > capturePos {
r.uncapture()
}
}
func (r *Runner) StackPop() int {
//get it
val := r.runstack[r.Runstackpos]
// pop it
r.Runstackpos++
// return it
return val
}
// StackDepth returns the number of integer slots currently used by the
// generated engine's backtracking stack.
func (r *Runner) StackDepth() int {
return len(r.runstack) - r.Runstackpos
}
func (r *Runner) StackPush(val int) {
// check if we need to size up stack
r.ensureStack(1)
r.Runstackpos--
r.runstack[r.Runstackpos] = val
}
func (r *Runner) StackPush2(val1, val2 int) {
// check if we need to size up stack
r.ensureStack(2)
r.Runstackpos--
r.runstack[r.Runstackpos] = val1
r.Runstackpos--
r.runstack[r.Runstackpos] = val2
}
func (r *Runner) StackPush3(val1, val2, val3 int) {
// check if we need to size up stack
r.ensureStack(3)
r.Runstackpos--
r.runstack[r.Runstackpos] = val1
r.Runstackpos--
r.runstack[r.Runstackpos] = val2
r.Runstackpos--
r.runstack[r.Runstackpos] = val3
}
func (r *Runner) StackPush4(val1, val2, val3, val4 int) {
// check if we need to size up stack
r.ensureStack(4)
r.Runstackpos--
r.runstack[r.Runstackpos] = val1
r.Runstackpos--
r.runstack[r.Runstackpos] = val2
r.Runstackpos--
r.runstack[r.Runstackpos] = val3
r.Runstackpos--
r.runstack[r.Runstackpos] = val4
}
func (r *Runner) StackPush5(val1, val2, val3, val4, val5 int) {
// check if we need to size up stack
r.ensureStack(5)
r.Runstackpos--
r.runstack[r.Runstackpos] = val1
r.Runstackpos--
r.runstack[r.Runstackpos] = val2
r.Runstackpos--
r.runstack[r.Runstackpos] = val3
r.Runstackpos--
r.runstack[r.Runstackpos] = val4
r.Runstackpos--
r.runstack[r.Runstackpos] = val5
}
func (r *Runner) StackPushN(vals ...int) {
// check if we need to size up stack
r.ensureStack(len(vals))
for _, val := range vals {
r.Runstackpos--
r.runstack[r.Runstackpos] = val
}
}
func (r *Runner) IsMatched(cap int) bool {
return r.runmatch.isMatched(cap)
}
func (r *Runner) MatchLength(cap int) int {
return r.runmatch.matchLength(cap)
}
func (r *Runner) MatchIndex(cap int) int {
return r.runmatch.matchIndex(cap)
}