42 Commits

Author SHA1 Message Date
47f88c817f Fixed typo 2025-02-09 15:14:17 -05:00
835d495990 Removed capitalization for error message (staticcheck) 2025-02-09 09:14:45 -05:00
76e0170cb9 Removed unused function 2025-02-09 09:13:52 -05:00
d172a58258 Throw error if match isn't found but test.result has >0 elements 2025-02-09 09:13:29 -05:00
7231169270 Removed unused functions 2025-02-09 09:13:03 -05:00
e546f01c20 Removed redundant return (staticcheck) 2025-02-09 09:12:55 -05:00
b7467a00f1 Removed priorityQueue (unused) 2025-02-09 09:07:43 -05:00
c6ad4caa0d Removed a bunch of unused code (let's go!!!) 2025-02-09 09:06:40 -05:00
6334435b83 Updated tests since the engine uses Perl matching instead of POSIX matching; added tests for FindStringSubmatch 2025-02-09 09:01:42 -05:00
78fb5606dd Use new definition of Reg 2025-02-09 08:59:16 -05:00
eddd2ae700 Updated documentation 2025-02-09 08:58:58 -05:00
c577064977 Added string field to Reg, that contains the expression string; wrote method to return the string 2025-02-09 08:58:46 -05:00
d4e3942d27 Added Match() and FindStringSubmatch(); removed old code; updated comments 2025-02-09 08:58:09 -05:00
f15a5cae34 Store all states visited in a single run of 'addStateToList()' in a slice 2025-02-08 16:07:01 -05:00
62ca1a872a Made zeroLengthMatchState() return a pointer; reduced the number of comparisons performd by nfaState.equals 2025-02-08 16:06:14 -05:00
99230b49de Use new function signature for zeroLengthMatchState() 2025-02-08 16:05:35 -05:00
22ead83625 Fixed assertion matching 2025-02-07 16:19:36 -05:00
3604486a9b Used Pike's algorithm (an extension to Thompson's algorithm) (see Russ Cox's 2nd article); I think I almost have a working PCRE-style engine 2025-02-07 16:06:45 -05:00
052de55826 question() now returns 2 values 2025-02-07 16:04:46 -05:00
d2ad0d95a8 Modified question operator so that it doesn't create an unnecessary zero-state 2025-02-07 16:04:26 -05:00
ccf3b3b299 More progress on implementing PCRE matching 2025-02-06 22:08:56 -05:00
1d4f695f8f Wrote function to check if a state is in an nfaState, based on the Equals function 2025-02-06 22:06:51 -05:00
8534174ea1 Use pointers instead of values 2025-02-06 22:06:22 -05:00
ed4ffde64e REFACTOR NEEDED: Added another special case; insert instead of appending into currentStates 2025-02-05 22:51:55 -05:00
fbc9bea9fb Commented out unused functions; use new nfaState parameters 2025-02-05 22:23:31 -05:00
cca8c7cda2 Got rid of transitions parameter, changed how kleene state is processed
I replaced the transition parameter for nfaState, replacing it with a
single nfaState pointer. This is because any non-alternation state will
only have one next state, so the map was just added complexity.

I changed alternation processing - instead of having their own dedicated
fields, they just use the new 'next' parameter, and another one called
'splitState'.

I also changed the kleene state processing to remove the unecessary
empty state in the right-side alternation (it actually messed up my
matching).
2025-02-05 22:20:28 -05:00
858e535fba Continued implementing Thompson's algorithm 2025-02-05 18:01:36 -05:00
7c62ba6bfd Started implementing Thompson's algorithm for matching, because the old one was completely backtracking (so it would enter infinite loops on something like '(a*)*' )
The git diff claims that a ton of code was changed, but most of it was just indentation changes.
2025-02-05 12:21:12 -05:00
d4e8cb74fd Replaced pointer to nfaState with nfaState 2025-02-05 11:32:20 -05:00
3ce611d121 More work towards implementing PCRE matching 2025-02-04 14:09:24 -05:00
e0253dfaf3 Change kleene() to an alternation-style construct 2025-02-04 14:09:04 -05:00
753e973d82 Started rewrite of matching algorithm, got concatenation and alternation done, kleene and zero-state stuff is next 2025-02-03 22:01:52 -05:00
5563a70568 Reverse the order in which I pop states for alternation, because this messes with the left branch-right branch thing 2025-02-03 21:59:41 -05:00
de0d7345a8 Store left and right branches of alternation separately 2025-02-03 21:59:05 -05:00
ad273b0c68 Trying to emulate backtracking by using string pointers within threads (something similar to rsc's 2nd regexp article) 2025-02-03 16:50:11 -05:00
e167cdb2cb Fixed mistake in test output 2025-02-03 16:49:30 -05:00
1fd48ae614 Store the current string pointer as a 'thread variable' (allows us to simulate backtracking) 2025-02-03 16:49:10 -05:00
09812956ac Disable all optimizations 2025-02-03 16:48:09 -05:00
fbc9dfcc95 Trying something out; we'll see if it works 2025-02-03 16:47:53 -05:00
bc32e0cb76 Started working on converting to PCRE matching rules (prefer left branch of alternation) 2025-02-03 14:06:14 -05:00
ad0f7d0178 Added new state fields to tell if a state is a question or alternation 2025-02-03 14:05:53 -05:00
4e597f8eb1 Implemented a priority-queue to use while matching 2025-02-03 14:05:30 -05:00
9 changed files with 431 additions and 490 deletions

View File

@@ -6,8 +6,8 @@ fmt:
vet: fmt
go vet ./...
buildLib: vet
go build -gcflags="-N -l" ./...
go build -gcflags="all=-N -l" ./...
buildCmd: buildLib
go build -C cmd/ -gcflags="-N -l" -o re ./...
go build -C cmd/ -gcflags="all=-N -l" -o re ./...
test: buildCmd
go test -v ./...

View File

@@ -16,7 +16,6 @@ func (s *uniq_arr[T]) add(vals ...T) {
s.backingMap[item] = struct{}{}
}
}
return
}
func (s uniq_arr[T]) contains(val T) bool {

View File

@@ -12,18 +12,24 @@ var notDotChars []rune
// A Reg represents the result of compiling a regular expression. It contains
// the startState of the NFA representation of the regex, and the number of capturing
// groups in the regex.
// groups in the regex. It also contains the expression string.
type Reg struct {
start *nfaState
numGroups int
str string
}
// numSubexp eturns the number of sub-expressions in the given [Reg]. This is equivalent
// NumSubexp returns the number of sub-expressions in the given [Reg]. This is equivalent
// to the number of capturing groups.
func (r Reg) NumSubexp() int {
return r.numGroups
}
// String returns the string used to compile the expression.
func (r Reg) String() string {
return r.str
}
const concatRune rune = 0xF0001
// Flags for shuntingYard - control its behavior
@@ -816,13 +822,12 @@ func thompson(re []postfixNode) (Reg, error) {
// In these cases, we will return an NFA with 1 state, with an assertion that is always true.
if len(re) == 0 {
start := zeroLengthMatchState()
nfa = append(nfa, &start)
nfa = append(nfa, start)
}
for _, c := range re {
if c.nodetype == characterNode || c.nodetype == assertionNode {
stateToAdd := nfaState{}
stateToAdd.transitions = make(map[int][]*nfaState)
if c.allChars {
stateToAdd.allChars = true
if len(c.except) != 0 {
@@ -934,7 +939,6 @@ func thompson(re []postfixNode) (Reg, error) {
s.isEmpty = true
s.output = make([]*nfaState, 0)
s.output = append(s.output, s)
s.transitions = make(map[int][]*nfaState)
// LPAREN nodes are just added normally
if c.nodetype == lparenNode {
numGroups++
@@ -966,7 +970,7 @@ func thompson(re []postfixNode) (Reg, error) {
s.groupNum = lparenNode.groupNum
to_add := concatenate(lparenNode, s)
nfa = append(nfa, to_add)
} else if middleNode.groupBegin && len(middleNode.transitions) == 0 { // The middle node is a lone lparen - something like '(())', and I'm looking at the first rparen
} else if middleNode.groupBegin && middleNode.numTransitions() == 0 { // The middle node is a lone lparen - something like '(())', and I'm looking at the first rparen
nfa = append(nfa, lparenNode) // I shouldn't have popped this out, because it is not involved in the current capturing group
s.groupNum = middleNode.groupNum // In this case, the 'middle' node is actually an lparen
to_add := concatenate(middleNode, s)
@@ -989,7 +993,8 @@ func thompson(re []postfixNode) (Reg, error) {
if c.nodetype == charclassNode { // A Character class consists of all the nodes in it, alternated
// Map the list of nodes to a list of states, each state containing the contents of a specific node
states := funcMap(c.nodeContents, func(node postfixNode) *nfaState {
s := newState()
s := &nfaState{}
s.output = append(s.output, s)
nodeContents := node.contents
if caseInsensitive {
nodeContents = slices.Concat(funcMap(nodeContents, func(r rune) []rune {
@@ -1003,7 +1008,7 @@ func thompson(re []postfixNode) (Reg, error) {
return n.contents
})...)
}
return &s
return s
})
// Reduce the list of states down to a single state by alternating them
toAdd := funcReduce(states, func(s1 *nfaState, s2 *nfaState) *nfaState {
@@ -1030,14 +1035,14 @@ func thompson(re []postfixNode) (Reg, error) {
if err != nil {
return Reg{}, fmt.Errorf("error applying kleene star")
}
stateToAdd, err := kleene(*s1)
stateToAdd, err := kleene(s1)
if err != nil {
return Reg{}, err
}
nfa = append(nfa, stateToAdd)
case plusNode: // a+ is equivalent to aa*
s1 := mustPop(&nfa)
s2, err := kleene(*s1)
s2, err := kleene(s1)
if err != nil {
return Reg{}, err
}
@@ -1048,7 +1053,10 @@ func thompson(re []postfixNode) (Reg, error) {
if err != nil {
return Reg{}, fmt.Errorf("error applying question operator")
}
s2 := question(s1)
s2, err := question(s1)
if err != nil {
return Reg{}, err
}
nfa = append(nfa, s2)
case pipeNode:
// A pipe operator doesn't actually need either operand to be present. If an operand isn't present,
@@ -1059,21 +1067,21 @@ func thompson(re []postfixNode) (Reg, error) {
// '|a'
// '^a|'
// '^|a'
s1, err1 := pop(&nfa)
s2, err2 := pop(&nfa)
if err2 != nil || (s2.groupBegin && len(s2.transitions) == 0) { // Doesn't exist, or its just an LPAREN
s2, err1 := pop(&nfa)
s1, err2 := pop(&nfa)
if err2 != nil || (s2.groupBegin && s2.numTransitions() == 0) { // Doesn't exist, or its just an LPAREN
if err2 == nil { // Roundabout way of saying that this node existed, but it was an LPAREN, so we append it back
nfa = append(nfa, s2)
}
tmp := zeroLengthMatchState()
s2 = &tmp
s2 = tmp
}
if err1 != nil || (s1.groupBegin && len(s1.transitions) == 0) { // Doesn't exist, or its just an LPAREN
if err1 != nil || (s1.groupBegin && s1.numTransitions() == 0) { // Doesn't exist, or its just an LPAREN
if err1 == nil { // See above for explanation
nfa = append(nfa, s1)
}
tmp := zeroLengthMatchState()
s1 = &tmp
s1 = tmp
}
s3 := alternate(s1, s2)
nfa = append(nfa, s3)
@@ -1100,14 +1108,18 @@ func thompson(re []postfixNode) (Reg, error) {
stateToAdd = concatenate(stateToAdd, cloneState(poppedState))
}
if c.endReps == infinite_reps { // Case 3
s2, err := kleene(*poppedState)
s2, err := kleene(poppedState)
if err != nil {
return Reg{}, err
}
stateToAdd = concatenate(stateToAdd, s2)
} else { // Case 2
for i := c.startReps; i < c.endReps; i++ {
stateToAdd = concatenate(stateToAdd, question(cloneState(poppedState)))
tmp, err := question(cloneState(poppedState))
if err != nil {
return Reg{}, fmt.Errorf("error processing bounded repetition")
}
stateToAdd = concatenate(stateToAdd, tmp)
}
}
nfa = append(nfa, stateToAdd)
@@ -1117,9 +1129,13 @@ func thompson(re []postfixNode) (Reg, error) {
return Reg{}, fmt.Errorf("invalid regex")
}
verifyLastStates(nfa)
lastState := newState()
lastState.isLast = true
return Reg{nfa[0], numGroups}, nil
concatenate(nfa[0], &lastState)
// The string is empty here, because we add it in Compile()
return Reg{nfa[0], numGroups, ""}, nil
}
@@ -1137,10 +1153,11 @@ func Compile(re string, flags ...ReFlag) (Reg, error) {
if err != nil {
return Reg{}, fmt.Errorf("error compiling regex: %w", err)
}
reg.str = re
return reg, nil
}
// MustCompile panicks if Compile returns an error. They are identical in all other respects.
// MustCompile panics if Compile returns an error. They are identical in all other respects.
func MustCompile(re string, flags ...ReFlag) Reg {
reg, err := Compile(re, flags...)
if err != nil {

View File

@@ -4,6 +4,8 @@ Package regex implements regular expression search, using a custom non-bracktrac
The engine relies completely on UTF-8 codepoints. As such, it is capable of matching characters
from other languages, emojis and symbols.
The API and regex syntax are largely compatible with that of the stdlib's [regexp], with a few key differences (see 'Key Differences with regexp').
The full syntax is specified below.
# Syntax
@@ -55,8 +57,8 @@ POSIX classes (inside normal character classes):
Composition:
def Match d, followed by e, followed by f
x|y Match x or y (prefer longer one)
xy|z Match xy or z
x|y Match x or y (prefer x)
xy|z Match xy or z (prefer xy)
Repitition (always greedy, preferring more):
@@ -94,10 +96,11 @@ Lookarounds:
Numeric ranges:
<x-y> Match any number from x to y (inclusive) (x and y must be positive numbers)
\<x Match a literal '<' followed by x
# Key Differences with regexp
The engine and the API differ from [regexp] in a number of ways, some of them very subtle.
The engine and the API differ from [regexp] in a few ways, some of them very subtle.
The key differences are mentioned below.
1. Greediness:
@@ -132,7 +135,7 @@ Rather than using primitives for return values, my engine defines two types that
values: a [Group] represents a capturing group, and a [Match] represents a list of groups.
[regexp] specifies a regular expression that gives a list of all the matching functions that it supports. The
equivalent expression for this engine is:
equivalent expression for this engine is shown below. Note that 'Index' is the default.
Find(All)?(String)?(Submatch)?
@@ -140,7 +143,7 @@ equivalent expression for this engine is:
If a function contains 'All' it returns all matches instead of just the leftmost one.
If a function contains 'String' it returns the matched text, rather than the indices.
If a function contains 'String' it returns the matched text, rather than the index in the string.
If a function contains 'Submatch' it returns the match, including all submatches found by
capturing groups.
@@ -156,5 +159,20 @@ and the input string:
The 0th group would contain 'xy' and the 1st group would contain 'y'. Any matching function without 'Submatch' in its name
returns the 0-group.
# Feature Differences
The following features from [regexp] are (currently) NOT supported:
1. Named capturing groups
2. Non-greedy operators
3. Unicode character classes
4. Embedded flags (flags are passed as arguments to [Compile])
5. Literal text with \Q ... \E
The following features are not available in [regexp], but are supported in my engine:
1. Lookarounds
2. Numeric ranges
The goal is to shorten the first list, and expand the second.
*/
package regex

View File

@@ -2,7 +2,6 @@ package regex
import (
"fmt"
"sort"
)
// A Match represents a match found by the regex in a given string.
@@ -14,7 +13,7 @@ import (
// See [Reg.FindSubmatch] for an example.
type Match []Group
// a Group represents a group. It contains the start index and end index of the match
// a Group represents a capturing group. It contains the start and index of the group.
type Group struct {
StartIdx int
EndIdx int
@@ -29,17 +28,6 @@ func newMatch(size int) Match {
return toRet
}
// Returns the number of valid groups in the match
func (m Match) numValidGroups() int {
numValid := 0
for _, g := range m {
if g.StartIdx >= 0 && g.EndIdx >= 0 {
numValid++
}
}
return numValid
}
// Returns a string containing the indices of all (valid) groups in the match
func (m Match) String() string {
var toRet string
@@ -58,7 +46,7 @@ func (idx Group) String() string {
return fmt.Sprintf("%d\t%d", idx.StartIdx, idx.EndIdx)
}
// Returns whether a group is valid (ie. whether it matched any text). It
// IsValid returns whether a group is valid (ie. whether it matched any text). It
// simply ensures that both indices of the group are >= 0.
func (g Group) IsValid() bool {
return g.StartIdx >= 0 && g.EndIdx >= 0
@@ -69,85 +57,8 @@ func getZeroGroup(m Match) Group {
return m[0]
}
// takeZeroState takes the 0-state (if such a transition exists) for all states in the
// given slice. It returns the resulting states. If any of the resulting states is a 0-state,
// the second ret val is true.
// If a state begins or ends a capturing group, its 'thread' is updated to contain the correct index.
func takeZeroState(states []*nfaState, numGroups int, idx int) (rtv []*nfaState, isZero bool) {
for _, state := range states {
if len(state.transitions[epsilon]) > 0 {
for _, s := range state.transitions[epsilon] {
if s.threadGroups == nil {
s.threadGroups = newMatch(numGroups + 1)
}
copy(s.threadGroups, state.threadGroups)
if s.groupBegin {
s.threadGroups[s.groupNum].StartIdx = idx
// openParenGroups = append(openParenGroups, s.groupNum)
}
if s.groupEnd {
s.threadGroups[s.groupNum].EndIdx = idx
// closeParenGroups = append(closeParenGroups, s.groupNum)
}
}
rtv = append(rtv, state.transitions[epsilon]...)
}
}
for _, state := range rtv {
if len(state.transitions[epsilon]) > 0 {
return rtv, true
}
}
return rtv, false
}
// zeroMatchPossible returns true if a zero-length match is possible
// from any of the given states, given the string and our position in it.
// It uses the same algorithm to find zero-states as the one inside the loop,
// so I should probably put it in a function.
func zeroMatchPossible(str []rune, idx int, numGroups int, states ...*nfaState) bool {
zeroStates, isZero := takeZeroState(states, numGroups, idx)
tempstates := make([]*nfaState, 0, len(zeroStates)+len(states))
tempstates = append(tempstates, states...)
tempstates = append(tempstates, zeroStates...)
num_appended := 0 // number of unique states addded to tempstates
for isZero == true {
zeroStates, isZero = takeZeroState(tempstates, numGroups, idx)
tempstates, num_appended = uniqueAppend(tempstates, zeroStates...)
if num_appended == 0 { // break if we haven't appended any more unique values
break
}
}
for _, state := range tempstates {
if state.isEmpty && (state.assert == noneAssert || state.checkAssertion(str, idx)) && state.isLast {
return true
}
}
return false
}
// Prunes the slice by removing overlapping indices.
func pruneIndices(indices []Match) []Match {
// First, sort the slice by the start indices
sort.Slice(indices, func(i, j int) bool {
return indices[i][0].StartIdx < indices[j][0].StartIdx
})
toRet := make([]Match, 0, len(indices))
current := indices[0]
for _, idx := range indices[1:] {
// idx doesn't overlap with current (starts after current ends), so add current to result
// and update the current.
if idx[0].StartIdx >= current[0].EndIdx {
toRet = append(toRet, current)
current = idx
} else if idx[0].EndIdx > current[0].EndIdx {
// idx overlaps, but it is longer, so update current
current = idx
}
}
// Add last state
toRet = append(toRet, current)
return toRet
func copyThread(to *nfaState, from nfaState) {
to.threadGroups = append([]Group{}, from.threadGroups...)
}
// Find returns the 0-group of the leftmost match of the regex in the given string.
@@ -160,6 +71,12 @@ func (regex Reg) Find(str string) (Group, error) {
return getZeroGroup(match), nil
}
// Match returns a boolean value, indicating whether the regex found a match in the given string.
func (regex Reg) Match(str string) bool {
_, err := regex.Find(str)
return err == nil
}
// FindAll returns a slice containing all the 0-groups of the regex in the given string.
// A 0-group represents the match without any submatches.
func (regex Reg) FindAll(str string) []Group {
@@ -195,7 +112,37 @@ func (regex Reg) FindSubmatch(str string) (Match, error) {
}
}
// FindAllString is the 'all' version of FindString.
// FindStringSubmatch is the 'string' version of [FindSubmatch]. It returns a slice of strings,
// where the string at index i contains the text matched by the i-th capturing group.
// The 0-th index represents the entire match.
// An empty string at index n could mean:
// ,
// 1. Group n did not find a match
// 2. Group n found a zero-length match
//
// A return value of nil indicates no match.
func (regex Reg) FindStringSubmatch(str string) []string {
matchStr := make([]string, regex.numGroups+1)
match, err := regex.FindSubmatch(str)
if err != nil {
return nil
}
nonEmptyMatchFound := false
for i := range match {
if match[i].IsValid() {
matchStr[i] = str[match[i].StartIdx:match[i].EndIdx]
nonEmptyMatchFound = true
} else {
matchStr[i] = ""
}
}
if nonEmptyMatchFound == false {
return nil
}
return matchStr
}
// FindAllString is the 'all' version of [FindString].
// It returns a slice of strings containing the text of all matches of
// the regex in the given string.
func (regex Reg) FindAllString(str string) []string {
@@ -240,18 +187,52 @@ func (regex Reg) FindAllSubmatch(str string) []Match {
indices = append(indices, matchIdx)
}
}
if len(indices) > 0 {
return pruneIndices(indices)
}
return indices
}
func addStateToList(str []rune, idx int, list []nfaState, state nfaState, threadGroups []Group, visited []nfaState) []nfaState {
if stateExists(list, state) || stateExists(visited, state) {
return list
}
visited = append(visited, state)
if state.isKleene || state.isQuestion {
copyThread(state.splitState, state)
list = addStateToList(str, idx, list, *state.splitState, threadGroups, visited)
copyThread(state.next, state)
list = addStateToList(str, idx, list, *state.next, threadGroups, visited)
return list
}
if state.isAlternation {
copyThread(state.next, state)
list = addStateToList(str, idx, list, *state.next, threadGroups, visited)
copyThread(state.splitState, state)
list = addStateToList(str, idx, list, *state.splitState, threadGroups, visited)
return list
}
state.threadGroups = append([]Group{}, threadGroups...)
if state.assert != noneAssert {
if state.checkAssertion(str, idx) {
copyThread(state.next, state)
return addStateToList(str, idx, list, *state.next, state.threadGroups, visited)
}
}
if state.groupBegin {
state.threadGroups[state.groupNum].StartIdx = idx
return addStateToList(str, idx, list, *state.next, state.threadGroups, visited)
}
if state.groupEnd {
state.threadGroups[state.groupNum].EndIdx = idx
return addStateToList(str, idx, list, *state.next, state.threadGroups, visited)
}
return append(list, state)
}
// Helper for FindAllMatches. Returns whether it found a match, the
// first Match it finds, and how far it got into the string ie. where
// the next search should start from.
//
// Might return duplicates or overlapping indices, so care must be taken to prune the resulting array.
func findAllSubmatchHelper(start *nfaState, str []rune, offset int, numGroups int) (bool, Match, int) {
// Base case - exit if offset exceeds string's length
if offset > len(str) {
@@ -260,20 +241,9 @@ func findAllSubmatchHelper(start *nfaState, str []rune, offset int, numGroups in
}
resetThreads(start)
// Hold a list of match indices for the current run. When we
// can no longer find a match, the match with the largest range is
// chosen as the match for the entire string.
// This allows us to pick the longest possible match (which is how greedy matching works).
// COMMENT ABOVE IS CURRENTLY NOT UP-TO-DATE
tempIndices := newMatch(numGroups + 1)
foundPath := false
startIdx := offset
endIdx := offset
currentStates := make([]*nfaState, 0)
tempStates := make([]*nfaState, 0) // Used to store states that should be used in next loop iteration
i := offset // Index in string
startingFrom := i // Store starting index
currentStates := make([]nfaState, 0)
nextStates := make([]nfaState, 0)
i := offset // Index in string
// If the first state is an assertion, makes sure the assertion
// is true before we do _anything_ else.
@@ -283,182 +253,41 @@ func findAllSubmatchHelper(start *nfaState, str []rune, offset int, numGroups in
return false, []Group{}, i
}
}
// Increment until we hit a character matching the start state (assuming not 0-state)
if start.isEmpty == false {
for i < len(str) && !start.contentContains(str, i) {
i++
}
startIdx = i
startingFrom = i
i++ // Advance to next character (if we aren't at a 0-state, which doesn't match anything), so that we can check for transitions. If we advance at a 0-state, we will never get a chance to match the first character
}
start.threadGroups = newMatch(numGroups + 1)
// Check if the start state begins a group - if so, add the start index to our list
if start.groupBegin {
start.threadGroups[start.groupNum].StartIdx = i
// tempIndices[start.groupNum].startIdx = i
}
currentStates = append(currentStates, start)
// Main loop
for i < len(str) {
foundPath = false
zeroStates := make([]*nfaState, 0)
// Keep taking zero-states, until there are no more left to take
// Objective: If any of our current states have transitions to 0-states, replace them with the 0-state. Do this until there are no more transitions to 0-states, or there are no more unique 0-states to take.
zeroStates, isZero := takeZeroState(currentStates, numGroups, i)
tempStates = append(tempStates, zeroStates...)
num_appended := 0
for isZero == true {
zeroStates, isZero = takeZeroState(tempStates, numGroups, i)
tempStates, num_appended = uniqueAppend(tempStates, zeroStates...)
if num_appended == 0 { // Break if we haven't appended any more unique values
break
}
}
currentStates, _ = uniqueAppend(currentStates, tempStates...)
tempStates = nil
// Take any transitions corresponding to current character
numStatesMatched := 0 // The number of states which had at least 1 match for this round
assertionFailed := false // Whether or not an assertion failed for this round
lastStateInList := false // Whether or not a last state was in our list of states
var lastStatePtr *nfaState = nil // Pointer to the last-state, if it was found
lastLookaroundInList := false // Whether or not a last state (that is a lookaround) was in our list of states
for _, state := range currentStates {
matches, numMatches := state.matchesFor(str, i)
if numMatches > 0 {
numStatesMatched++
tempStates = append(tempStates, matches...)
foundPath = true
for _, m := range matches {
if m.threadGroups == nil {
m.threadGroups = newMatch(numGroups + 1)
}
copy(m.threadGroups, state.threadGroups)
}
}
if numMatches < 0 {
assertionFailed = true
}
if state.isLast {
if state.isLookaround() {
lastLookaroundInList = true
}
lastStateInList = true
lastStatePtr = state
}
}
if assertionFailed && numStatesMatched == 0 { // Nothing has matched and an assertion has failed
// If I'm being completely honest, I'm not sure why I have to check specifically for a _lookaround_
// state. The explanation below is my attempt to explain this behavior.
// If you replace 'lastLookaroundInList' with 'lastStateInList', one of the test cases fails.
//
// One of the states in our list was a last state and a lookaround. In this case, we
// don't abort upon failure of the assertion, because we have found
// another path to a final state.
// Even if the last state _was_ an assertion, we can use the previously
// saved indices to find a match.
if lastLookaroundInList {
break
} else {
if i == startingFrom {
i++
}
return false, []Group{}, i
}
}
// Check if we can find a state in our list that is:
// a. A last-state
// b. Empty
// c. Doesn't assert anything
for _, s := range currentStates {
if s.isLast && s.isEmpty && s.assert == noneAssert {
lastStatePtr = s
lastStateInList = true
}
}
if lastStateInList { // A last-state was in the list of states. add the matchIndex to our MatchIndex list
for j := 1; j < numGroups+1; j++ {
tempIndices[j] = lastStatePtr.threadGroups[j]
}
endIdx = i
tempIndices[0] = Group{startIdx, endIdx}
}
// Check if we can find a zero-length match
if foundPath == false {
if ok := zeroMatchPossible(str, i, numGroups, currentStates...); ok {
if tempIndices[0].IsValid() == false {
tempIndices[0] = Group{startIdx, startIdx}
}
}
// If we haven't moved in the string, increment the counter by 1
// to ensure we don't keep trying the same string over and over.
// if i == startingFrom {
startIdx++
// i++
// }
if tempIndices.numValidGroups() > 0 && tempIndices[0].IsValid() {
if tempIndices[0].StartIdx == tempIndices[0].EndIdx { // If we have a zero-length match, we have to shift the index at which we start. Otherwise we keep looking at the same paert of the string over and over.
return true, tempIndices, tempIndices[0].EndIdx + 1
} else {
return true, tempIndices, tempIndices[0].EndIdx
}
}
return false, []Group{}, startIdx
}
currentStates = make([]*nfaState, len(tempStates))
copy(currentStates, tempStates)
tempStates = nil
i++
}
// End-of-string reached. Go to any 0-states, until there are no more 0-states to go to. Then check if any of our states are in the end position.
// This is the exact same algorithm used inside the loop, so I should probably put it in a function.
zeroStates, isZero := takeZeroState(currentStates, numGroups, i)
tempStates = append(tempStates, zeroStates...)
num_appended := 0 // Number of unique states addded to tempStates
for isZero == true {
zeroStates, isZero = takeZeroState(tempStates, numGroups, i)
tempStates, num_appended = uniqueAppend(tempStates, zeroStates...)
if num_appended == 0 { // Break if we haven't appended any more unique values
start.threadGroups[0].StartIdx = i
currentStates = addStateToList(str, i, currentStates, *start, start.threadGroups, nil)
var match Match = nil
for idx := i; idx <= len(str); idx++ {
if len(currentStates) == 0 {
break
}
}
for currentStateIdx := 0; currentStateIdx < len(currentStates); currentStateIdx++ {
currentState := currentStates[currentStateIdx]
currentStates = append(currentStates, tempStates...)
tempStates = nil
if currentState.threadGroups == nil {
currentState.threadGroups = newMatch(numGroups + 1)
currentState.threadGroups[0].StartIdx = idx
}
for _, state := range currentStates {
// Only add the match if the start index is in bounds. If the state has an assertion,
// make sure the assertion checks out.
if state.isLast && i <= len(str) {
if state.assert == noneAssert || state.checkAssertion(str, i) {
for j := 1; j < numGroups+1; j++ {
tempIndices[j] = state.threadGroups[j]
if currentState.isLast {
currentState.threadGroups[0].EndIdx = idx
match = append([]Group{}, currentState.threadGroups...)
break
} else if !currentState.isAlternation && !currentState.isKleene && !currentState.isQuestion && !currentState.groupBegin && !currentState.groupEnd { // Normal character or assertion
if currentState.contentContains(str, idx) {
nextStates = addStateToList(str, idx+1, nextStates, *currentState.next, currentState.threadGroups, nil)
}
endIdx = i
tempIndices[0] = Group{startIdx, endIdx}
}
}
currentStates = append([]nfaState{}, nextStates...)
nextStates = nil
}
if tempIndices.numValidGroups() > 0 {
if tempIndices[0].StartIdx == tempIndices[0].EndIdx { // If we have a zero-length match, we have to shift the index at which we start. Otherwise we keep looking at the same paert of the string over and over.
return true, tempIndices, tempIndices[0].EndIdx + 1
} else {
return true, tempIndices, tempIndices[0].EndIdx
if match != nil {
if offset == match[0].EndIdx {
return true, match, match[0].EndIdx + 1
}
return true, match, match[0].EndIdx
}
if startIdx == startingFrom { // Increment starting index if we haven't moved in the string. Prevents us from matching the same part of the string over and over.
startIdx++
}
return false, []Group{}, startIdx
return false, []Group{}, i + 1
}

View File

@@ -48,49 +48,6 @@ func isNormalChar(c rune) bool {
return !slices.Contains(specialChars, c)
}
// Ensure that the given elements are only appended to the given slice if they
// don't already exist. Returns the new slice, and the number of unique items appended.
func uniqueAppend[T comparable](slc []T, items ...T) ([]T, int) {
num_appended := 0
for _, item := range items {
if !slices.Contains(slc, item) {
slc = append(slc, item)
num_appended++
}
}
return slc, num_appended
}
func uniqueAppendFunc[T any](slc []T, fn func(T, T) bool, items ...T) ([]T, int) {
toRet := make([]T, len(slc))
num_appended := 0
copy(toRet, slc)
for _, item := range items {
itemExists := false
for _, val := range slc {
if fn(item, val) {
itemExists = true
}
}
if !itemExists {
toRet = append(toRet, item)
num_appended++
}
}
return toRet, num_appended
}
// Returns true only if all the given elements are equal
func allEqual[T comparable](items ...T) bool {
first := items[0]
for _, item := range items {
if item != first {
return false
}
}
return true
}
// Map function - convert a slice of T to a slice of V, based on a function
// that maps a T to a V
func funcMap[T, V any](slc []T, fn func(T) V) []V {

View File

@@ -25,21 +25,25 @@ const (
)
type nfaState struct {
content stateContents // Contents of current state
isEmpty bool // If it is empty - Union operator and Kleene star states will be empty
isLast bool // If it is the last state (acept state)
output []*nfaState // The outputs of the current state ie. the 'outward arrows'. A union operator state will have more than one of these.
transitions map[int][]*nfaState // Transitions to different states (maps a character (int representation) to a _list of states. This is useful if one character can lead multiple states eg. ab|aa)
isKleene bool // Identifies whether current node is a 0-state representing Kleene star
assert assertType // Type of assertion of current node - NONE means that the node doesn't assert anything
allChars bool // Whether or not the state represents all characters (eg. a 'dot' metacharacter). A 'dot' node doesn't store any contents directly, as it would take up too much space
except []rune // Only valid if allChars is true - match all characters _except_ the ones in this block. Useful for inverting character classes.
lookaroundRegex string // Only for lookaround states - Contents of the regex that the lookaround state holds
lookaroundNFA *nfaState // Holds the NFA of the lookaroundRegex - if it exists
lookaroundNumCaptureGroups int // Number of capturing groups in lookaround regex if current node is a lookaround
groupBegin bool // Whether or not the node starts a capturing group
groupEnd bool // Whether or not the node ends a capturing group
groupNum int // Which capturing group the node starts / ends
content stateContents // Contents of current state
isEmpty bool // If it is empty - Union operator and Kleene star states will be empty
isLast bool // If it is the last state (acept state)
output []*nfaState // The outputs of the current state ie. the 'outward arrows'. A union operator state will have more than one of these.
// transitions map[int][]*nfaState // Transitions to different states (maps a character (int representation) to a _list of states. This is useful if one character can lead multiple states eg. ab|aa)
next *nfaState // The next state (not for alternation or kleene states)
isKleene bool // Identifies whether current node is a 0-state representing Kleene star
isQuestion bool // Identifies whether current node is a 0-state representing the question operator
isAlternation bool // Identifies whether current node is a 0-state representing an alternation
splitState *nfaState // Only for alternation states - the 'other' branch of the alternation ('next' is the first)
assert assertType // Type of assertion of current node - NONE means that the node doesn't assert anything
allChars bool // Whether or not the state represents all characters (eg. a 'dot' metacharacter). A 'dot' node doesn't store any contents directly, as it would take up too much space
except []rune // Only valid if allChars is true - match all characters _except_ the ones in this block. Useful for inverting character classes.
lookaroundRegex string // Only for lookaround states - Contents of the regex that the lookaround state holds
lookaroundNFA *nfaState // Holds the NFA of the lookaroundRegex - if it exists
lookaroundNumCaptureGroups int // Number of capturing groups in lookaround regex if current node is a lookaround
groupBegin bool // Whether or not the node starts a capturing group
groupEnd bool // Whether or not the node ends a capturing group
groupNum int // Which capturing group the node starts / ends
// The following properties depend on the current match - I should think about resetting them for every match.
zeroMatchFound bool // Whether or not the state has been used for a zero-length match - only relevant for zero states
threadGroups []Group // Assuming that a state is part of a 'thread' in the matching process, this array stores the indices of capturing groups in the current thread. As matches are found for this state, its groups will be copied over.
@@ -68,8 +72,9 @@ func cloneStateHelper(stateToClone *nfaState, cloneMap map[*nfaState]*nfaState)
isEmpty: stateToClone.isEmpty,
isLast: stateToClone.isLast,
output: make([]*nfaState, len(stateToClone.output)),
transitions: make(map[int][]*nfaState),
isKleene: stateToClone.isKleene,
isQuestion: stateToClone.isQuestion,
isAlternation: stateToClone.isAlternation,
assert: stateToClone.assert,
zeroMatchFound: stateToClone.zeroMatchFound,
allChars: stateToClone.allChars,
@@ -87,20 +92,18 @@ func cloneStateHelper(stateToClone *nfaState, cloneMap map[*nfaState]*nfaState)
clone.output[i] = cloneStateHelper(s, cloneMap)
}
}
for k, v := range stateToClone.transitions {
clone.transitions[k] = make([]*nfaState, len(v))
for i, s := range v {
if s == stateToClone {
clone.transitions[k][i] = clone
} else {
clone.transitions[k][i] = cloneStateHelper(s, cloneMap)
}
}
}
if stateToClone.lookaroundNFA == stateToClone {
clone.lookaroundNFA = clone
}
clone.lookaroundNFA = cloneStateHelper(stateToClone.lookaroundNFA, cloneMap)
if stateToClone.splitState == stateToClone {
clone.splitState = clone
}
clone.splitState = cloneStateHelper(stateToClone.splitState, cloneMap)
if stateToClone.next == stateToClone {
clone.next = clone
}
clone.next = cloneStateHelper(stateToClone.next, cloneMap)
return clone
}
@@ -111,16 +114,20 @@ func resetThreads(start *nfaState) {
}
func resetThreadsHelper(state *nfaState, visitedMap map[*nfaState]bool) {
if state == nil {
return
}
if _, ok := visitedMap[state]; ok {
return
}
// Assuming it hasn't been visited
state.threadGroups = nil
visitedMap[state] = true
for _, v := range state.transitions {
for _, nextState := range v {
resetThreadsHelper(nextState, visitedMap)
}
if state.isAlternation {
resetThreadsHelper(state.next, visitedMap)
resetThreadsHelper(state.splitState, visitedMap)
} else {
resetThreadsHelper(state.next, visitedMap)
}
}
@@ -176,7 +183,7 @@ func (s nfaState) checkAssertion(str []rune, idx int) bool {
strToMatch = string(runesToMatch)
}
regComp := Reg{startState, s.lookaroundNumCaptureGroups}
regComp := Reg{startState, s.lookaroundNumCaptureGroups, s.lookaroundRegex}
matchIndices := regComp.FindAll(strToMatch)
numMatchesFound := 0
@@ -207,6 +214,9 @@ func (s nfaState) contentContains(str []rune, idx int) bool {
if s.assert != noneAssert {
return s.checkAssertion(str, idx)
}
if idx >= len(str) {
return false
}
if s.allChars {
return !slices.Contains(slices.Concat(notDotChars, s.except), str[idx]) // Return true only if the index isn't a 'notDotChar', or isn't one of the exception characters for the current node.
}
@@ -218,74 +228,84 @@ func (s nfaState) isLookaround() bool {
return s.assert == plaAssert || s.assert == plbAssert || s.assert == nlaAssert || s.assert == nlbAssert
}
func (s nfaState) numTransitions() int {
if s.next == nil && s.splitState == nil {
return 0
}
if s.next == nil || s.splitState == nil {
return 1
}
return 2
}
// Returns the matches for the character at the given index of the given string.
// Also returns the number of matches. Returns -1 if an assertion failed.
func (s nfaState) matchesFor(str []rune, idx int) ([]*nfaState, int) {
// Assertions can be viewed as 'checks'. If the check fails, we return
// an empty array and 0.
// If it passes, we treat it like any other state, and return all the transitions.
if s.assert != noneAssert {
if s.checkAssertion(str, idx) == false {
return make([]*nfaState, 0), -1
}
}
listTransitions := s.transitions[int(str[idx])]
for _, dest := range s.transitions[int(anyCharRune)] {
if !slices.Contains(slices.Concat(notDotChars, dest.except), str[idx]) {
// Add an allChar state to the list of matches if:
// a. The current character isn't a 'notDotChars' character. In single line mode, this includes newline. In multiline mode, it doesn't.
// b. The current character isn't the state's exception list.
listTransitions = append(listTransitions, dest)
}
}
numTransitions := len(listTransitions)
return listTransitions, numTransitions
}
//func (s nfaState) matchesFor(str []rune, idx int) ([]*nfaState, int) {
// // Assertions can be viewed as 'checks'. If the check fails, we return
// // an empty array and 0.
// // If it passes, we treat it like any other state, and return all the transitions.
// if s.assert != noneAssert {
// if s.checkAssertion(str, idx) == false {
// return make([]*nfaState, 0), -1
// }
// }
// listTransitions := s.transitions[int(str[idx])]
// for _, dest := range s.transitions[int(anyCharRune)] {
// if !slices.Contains(slices.Concat(notDotChars, dest.except), str[idx]) {
// // Add an allChar state to the list of matches if:
// // a. The current character isn't a 'notDotChars' character. In single line mode, this includes newline. In multiline mode, it doesn't.
// // b. The current character isn't the state's exception list.
// listTransitions = append(listTransitions, dest)
// }
// }
// numTransitions := len(listTransitions)
// return listTransitions, numTransitions
//}
// verifyLastStatesHelper performs the depth-first recursion needed for verifyLastStates
func verifyLastStatesHelper(st *nfaState, visited map[*nfaState]bool) {
if len(st.transitions) == 0 {
st.isLast = true
return
}
// if len(state.transitions) == 1 && len(state.transitions[state.content]) == 1 && state.transitions[state.content][0] == state { // Eg. a*
if len(st.transitions) == 1 { // Eg. a*
var moreThanOneTrans bool // Dummy variable, check if all the transitions for the current's state's contents have a length of one
for _, c := range st.content {
if len(st.transitions[c]) != 1 || st.transitions[c][0] != st {
moreThanOneTrans = true
}
}
st.isLast = !moreThanOneTrans
}
if st.isKleene { // A State representing a Kleene Star has transitions going out, which loop back to it. If all those transitions point to the same (single) state, then it must be a last state
transitionDests := make([]*nfaState, 0)
for _, v := range st.transitions {
transitionDests = append(transitionDests, v...)
}
if allEqual(transitionDests...) {
st.isLast = true
return
}
}
if visited[st] == true {
return
}
visited[st] = true
for _, states := range st.transitions {
for i := range states {
if states[i] != st {
verifyLastStatesHelper(states[i], visited)
}
}
}
}
//func verifyLastStatesHelper(st *nfaState, visited map[*nfaState]bool) {
// if st.numTransitions() == 0 {
// st.isLast = true
// return
// }
// // if len(state.transitions) == 1 && len(state.transitions[state.content]) == 1 && state.transitions[state.content][0] == state { // Eg. a*
// if st.numTransitions() == 1 { // Eg. a*
// var moreThanOneTrans bool // Dummy variable, check if all the transitions for the current's state's contents have a length of one
// for _, c := range st.content {
// if len(st.transitions[c]) != 1 || st.transitions[c][0] != st {
// moreThanOneTrans = true
// }
// }
// st.isLast = !moreThanOneTrans
// }
//
// if st.isKleene { // A State representing a Kleene Star has transitions going out, which loop back to it. If all those transitions point to the same (single) state, then it must be a last state
// transitionDests := make([]*nfaState, 0)
// for _, v := range st.transitions {
// transitionDests = append(transitionDests, v...)
// }
// if allEqual(transitionDests...) {
// st.isLast = true
// return
// }
// }
// if visited[st] == true {
// return
// }
// visited[st] = true
// for _, states := range st.transitions {
// for i := range states {
// if states[i] != st {
// verifyLastStatesHelper(states[i], visited)
// }
// }
// }
//}
// verifyLastStates enables the 'isLast' flag for the leaf nodes (last states)
func verifyLastStates(start []*nfaState) {
verifyLastStatesHelper(start[0], make(map[*nfaState]bool))
}
//func verifyLastStates(start []*nfaState) {
// verifyLastStatesHelper(start[0], make(map[*nfaState]bool))
//}
// Concatenates s1 and s2, returns the start of the concatenation.
func concatenate(s1 *nfaState, s2 *nfaState) *nfaState {
@@ -293,73 +313,84 @@ func concatenate(s1 *nfaState, s2 *nfaState) *nfaState {
return s2
}
for i := range s1.output {
for _, c := range s2.content { // Create transitions for every element in s1's content to s2'
s1.output[i].transitions[c], _ = uniqueAppend(s1.output[i].transitions[c], s2)
}
s1.output[i].next = s2
}
s1.output = s2.output
return s1
}
func kleene(s1 nfaState) (*nfaState, error) {
func kleene(s1 *nfaState) (*nfaState, error) {
if s1.isEmpty && s1.assert != noneAssert {
return nil, fmt.Errorf("previous token is not quantifiable")
}
toReturn := &nfaState{}
toReturn.transitions = make(map[int][]*nfaState)
toReturn.content = newContents(epsilon)
toReturn.isEmpty = true
toReturn.isAlternation = true
toReturn.content = newContents(epsilon)
toReturn.splitState = s1
// toReturn := &nfaState{}
// toReturn.transitions = make(map[int][]*nfaState)
// toReturn.content = newContents(epsilon)
toReturn.isKleene = true
toReturn.output = append(toReturn.output, toReturn)
toReturn.output = append([]*nfaState{}, toReturn)
for i := range s1.output {
for _, c := range toReturn.content {
s1.output[i].transitions[c], _ = uniqueAppend(s1.output[i].transitions[c], toReturn)
}
}
for _, c := range s1.content {
toReturn.transitions[c], _ = uniqueAppend(toReturn.transitions[c], &s1)
s1.output[i].next = toReturn
}
// for _, c := range s1.content {
// toReturn.transitions[c], _ = uniqueAppend(toReturn.transitions[c], &s1)
// }
//toReturn.kleeneState = &s1
return toReturn, nil
}
func alternate(s1 *nfaState, s2 *nfaState) *nfaState {
toReturn := &nfaState{}
toReturn.transitions = make(map[int][]*nfaState)
// toReturn.transitions = make(map[int][]*nfaState)
toReturn.output = append(toReturn.output, s1.output...)
toReturn.output = append(toReturn.output, s2.output...)
// Unique append is used here (and elsewhere) to ensure that,
// for any given transition, a state can only be mentioned once.
// For example, given the transition 'a', the state 's1' can only be mentioned once.
// This would lead to multiple instances of the same set of match indices, since both
// 's1' states would be considered to match.
for _, c := range s1.content {
toReturn.transitions[c], _ = uniqueAppend(toReturn.transitions[c], s1)
}
for _, c := range s2.content {
toReturn.transitions[c], _ = uniqueAppend(toReturn.transitions[c], s2)
}
// // Unique append is used here (and elsewhere) to ensure that,
// // for any given transition, a state can only be mentioned once.
// // For example, given the transition 'a', the state 's1' can only be mentioned once.
// // This would lead to multiple instances of the same set of match indices, since both
// // 's1' states would be considered to match.
// for _, c := range s1.content {
// toReturn.transitions[c], _ = uniqueAppend(toReturn.transitions[c], s1)
// }
// for _, c := range s2.content {
// toReturn.transitions[c], _ = uniqueAppend(toReturn.transitions[c], s2)
// }
toReturn.content = newContents(epsilon)
toReturn.isEmpty = true
toReturn.isAlternation = true
toReturn.next = s1
toReturn.splitState = s2
return toReturn
}
func question(s1 *nfaState) *nfaState { // Use the fact that ab? == a(b|)
s2 := &nfaState{}
s2.transitions = make(map[int][]*nfaState)
s2.content = newContents(epsilon)
s2.output = append(s2.output, s2)
s2.isEmpty = true
s3 := alternate(s1, s2)
return s3
func question(s1 *nfaState) (*nfaState, error) { // Use the fact that ab? == a(b|)
if s1.isEmpty && s1.assert != noneAssert {
return nil, fmt.Errorf("previous token is not quantifiable")
}
toReturn := &nfaState{}
toReturn.isEmpty = true
toReturn.isAlternation = true
toReturn.isQuestion = true
toReturn.content = newContents(epsilon)
toReturn.splitState = s1
toReturn.output = append([]*nfaState{}, toReturn)
toReturn.output = append(toReturn.output, s1.output...)
// s2.transitions = make(map[int][]*nfaState)
return toReturn, nil
}
// Creates and returns a new state with the 'default' values.
func newState() nfaState {
ret := nfaState{
output: make([]*nfaState, 0),
transitions: make(map[int][]*nfaState),
output: make([]*nfaState, 0),
// transitions: make(map[int][]*nfaState),
assert: noneAssert,
except: append([]rune{}, 0),
lookaroundRegex: "",
@@ -371,10 +402,40 @@ func newState() nfaState {
}
// Creates and returns a state that _always_ has a zero-length match.
func zeroLengthMatchState() nfaState {
start := newState()
func zeroLengthMatchState() *nfaState {
start := &nfaState{}
start.content = newContents(epsilon)
start.isEmpty = true
start.assert = alwaysTrueAssert
start.output = append([]*nfaState{}, start)
return start
}
func (s nfaState) equals(other nfaState) bool {
return s.isEmpty == other.isEmpty &&
s.isLast == other.isLast &&
slices.Equal(s.output, other.output) &&
slices.Equal(s.content, other.content) &&
s.next == other.next &&
s.isKleene == other.isKleene &&
s.isQuestion == other.isQuestion &&
s.isAlternation == other.isAlternation &&
s.splitState == other.splitState &&
s.assert == other.assert &&
s.allChars == other.allChars &&
slices.Equal(s.except, other.except) &&
s.lookaroundNFA == other.lookaroundNFA &&
s.groupBegin == other.groupBegin &&
s.groupEnd == other.groupEnd &&
s.groupNum == other.groupNum &&
slices.Equal(s.threadGroups, other.threadGroups)
}
func stateExists(list []nfaState, s nfaState) bool {
for i := range list {
if list[i].equals(s) {
return true
}
}
return false
}

View File

@@ -109,7 +109,7 @@ func range2regex(start int, end int) (string, error) {
startSlc := intToSlc(rg.start)
endSlc := intToSlc(rg.end)
if len(startSlc) != len(endSlc) {
return "", fmt.Errorf("Error parsing numeric range")
return "", fmt.Errorf("error parsing numeric range")
}
for i := range startSlc {
if startSlc[i] == endSlc[i] {

View File

@@ -528,7 +528,7 @@ var groupTests = []struct {
}{
{"(a)(b)", nil, "ab", []Match{[]Group{{0, 2}, {0, 1}, {1, 2}}}},
{"((a))(b)", nil, "ab", []Match{[]Group{{0, 2}, {0, 1}, {0, 1}, {1, 2}}}},
{"(0)", nil, "ab", []Match{[]Group{}}},
{"(0)", nil, "ab", []Match{}},
{"(a)b", nil, "ab", []Match{[]Group{{0, 2}, {0, 1}}}},
{"a(b)", nil, "ab", []Match{[]Group{{0, 2}, {1, 2}}}},
{"(a|b)", nil, "ab", []Match{[]Group{{0, 1}, {0, 1}}, []Group{{1, 2}, {1, 2}}}},
@@ -538,9 +538,8 @@ var groupTests = []struct {
{"(a+)(aa)", nil, "aaaa", []Match{[]Group{{0, 4}, {0, 2}, {2, 4}}}},
{"(aaaa)|(aaaa)", nil, "aaaa", []Match{[]Group{{0, 4}, {0, 4}, {-1, -1}}}},
{"(aaa)|(aaaa)", nil, "aaaa", []Match{[]Group{{0, 4}, {-1, -1}, {0, 4}}}},
{"(aaa)|(aaaa)", nil, "aaaa", []Match{[]Group{{0, 4}, {-1, -1}, {0, 4}}}},
{"(aaaa)|(aaa)", nil, "aaaa", []Match{[]Group{{0, 4}, {0, 4}, {-1, -1}}}},
{"(a)|(aa)", nil, "aa", []Match{[]Group{{0, 2}, {-1, -1}, {0, 2}}}},
{"(a)|(aa)", nil, "aa", []Match{[]Group{{0, 1}, {0, 1}}, []Group{{1, 2}, {1, 2}}}},
{"(a?)a?", nil, "b", []Match{[]Group{{0, 0}, {0, 0}}, []Group{{1, 1}, {1, 1}}}},
{"(a?)a?", nil, "ab", []Match{[]Group{{0, 1}, {0, 1}}, []Group{{1, 1}, {1, 1}}, []Group{{2, 2}, {2, 2}}}},
{"(a?)a?", nil, "aa", []Match{[]Group{{0, 2}, {0, 1}}, []Group{{2, 2}, {2, 2}}}},
@@ -578,7 +577,7 @@ var groupTests = []struct {
{`(bc+d$|ef*g.|h?i(j|k))`, nil, `bcdd`, []Match{}},
{`(bc+d$|ef*g.|h?i(j|k))`, nil, `reffgz`, []Match{[]Group{{1, 6}, {1, 6}}}},
{`(((((((((a)))))))))`, nil, `a`, []Match{[]Group{{0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}}}},
{`(((((((((a)))))))))\41`, nil, `a`, []Match{[]Group{{0, 2}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}}}},
{`(((((((((a)))))))))\41`, nil, `a!`, []Match{[]Group{{0, 2}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}}}},
{`(.*)c(.*)`, nil, `abcde`, []Match{[]Group{{0, 5}, {0, 2}, {3, 5}}}},
{`\((.*), (.*)\)`, nil, `(a, b)`, []Match{[]Group{{0, 6}, {1, 2}, {4, 5}}}},
@@ -633,7 +632,7 @@ var groupTests = []struct {
{`(bc+d$|ef*g.|h?i(j|k))`, []ReFlag{RE_CASE_INSENSITIVE}, `BCDD`, []Match{}},
{`(bc+d$|ef*g.|h?i(j|k))`, []ReFlag{RE_CASE_INSENSITIVE}, `reffgz`, []Match{[]Group{{1, 6}, {1, 6}}}},
{`(((((((((a)))))))))`, []ReFlag{RE_CASE_INSENSITIVE}, `A`, []Match{[]Group{{0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}}}},
{`(((((((((a)))))))))\41`, []ReFlag{RE_CASE_INSENSITIVE}, `A`, []Match{[]Group{{0, 2}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}}}},
{`(((((((((a)))))))))\41`, []ReFlag{RE_CASE_INSENSITIVE}, `A!`, []Match{[]Group{{0, 2}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}}}},
{`(.*)c(.*)`, []ReFlag{RE_CASE_INSENSITIVE}, `ABCDE`, []Match{[]Group{{0, 5}, {0, 2}, {3, 5}}}},
{`\((.*), (.*)\)`, []ReFlag{RE_CASE_INSENSITIVE}, `(A, B)`, []Match{[]Group{{0, 6}, {1, 2}, {4, 5}}}},
{`(a)(b)c|ab`, []ReFlag{RE_CASE_INSENSITIVE}, `AB`, []Match{[]Group{{0, 2}}}},
@@ -701,7 +700,7 @@ func TestFind(t *testing.T) {
if len(test.result) == 0 {
return // Manually pass the test, because this is the expected behavior
} else {
t.Errorf("Wanted no match Got %v\n", groupIndex)
t.Errorf("Wanted %v Got no matches\n", test.result)
}
} else {
if groupIndex != test.result[0] {
@@ -743,7 +742,7 @@ func TestFindString(t *testing.T) {
foundString := regComp.FindString(test.str)
if len(test.result) == 0 {
if foundString != "" {
t.Errorf("Expected no match got %v\n", foundString)
t.Errorf("Wanted no match got %v\n", foundString)
}
} else {
expectedString := test.str[test.result[0].StartIdx:test.result[0].EndIdx]
@@ -791,11 +790,68 @@ func TestFindSubmatch(t *testing.T) {
}
}
match, err := regComp.FindSubmatch(test.str)
if err != nil {
if len(test.result) != 0 {
t.Errorf("Wanted %v got no match\n", test.result[0])
}
} else if len(test.result) == 0 {
t.Errorf("Wanted no match got %v\n", match)
}
for i := range match {
if match[i].IsValid() {
if test.result[0][i] != match[i] {
t.Errorf("Wanted %v Got %v\n", test.result[0], match)
}
} else {
if i < len(test.result) && test.result[0][i].IsValid() {
t.Errorf("Wanted %v Got %v\n", test.result[0], match)
}
}
}
})
}
}
func TestFindStringSubmatch(t *testing.T) {
for _, test := range groupTests {
t.Run(test.re+" "+test.str, func(t *testing.T) {
regComp, err := Compile(test.re, test.flags...)
if err != nil {
if test.result != nil {
panic(err)
}
}
matchStr := regComp.FindStringSubmatch(test.str)
if matchStr == nil {
if len(test.result) != 0 {
expectedStr := funcMap(test.result[0], func(g Group) string {
if g.IsValid() {
return test.str[g.StartIdx:g.EndIdx]
} else {
return ""
}
})
t.Errorf("Wanted %v got no match\n", expectedStr)
}
} else if len(test.result) == 0 {
t.Errorf("Wanted no match got %v\n", matchStr)
} else {
expectedStr := funcMap(test.result[0], func(g Group) string {
if g.IsValid() {
return test.str[g.StartIdx:g.EndIdx]
} else {
return ""
}
})
for i, groupStr := range matchStr {
if groupStr == "" {
if i < len(expectedStr) && expectedStr[i] != "" {
t.Errorf("Wanted %v Got %v\n", expectedStr, matchStr)
}
} else {
if expectedStr[i] != groupStr {
t.Errorf("Wanted %v Got %v\n", expectedStr, matchStr)
}
}
}
}
})
@@ -817,6 +873,10 @@ func TestFindAllSubmatch(t *testing.T) {
if test.result[i][j] != matchIndices[i][j] {
t.Errorf("Wanted %v Got %v\n", test.result, matchIndices)
}
} else {
if i < len(test.result) && j < len(test.result[i]) && test.result[i][j].IsValid() {
t.Errorf("Wanted %v Got %v\n", test.result, matchIndices)
}
}
}
}