Prefilter indexer regexps with literal checks

Go's regexp backtracker dominated indexing. Each pattern now runs only
when a cheap necessary-condition check passes, cutting indexing of the
Go standard library source from 4.7s to 1.4s.
Adds BenchmarkIndexDirectory.
This commit is contained in:
Andy Dixon
2026-01-25 15:57:26 +00:00
parent 5816dea6b1
commit 62c363b031
2 changed files with 71 additions and 20 deletions
+50 -16
View File
@@ -57,26 +57,54 @@ type Snapshot struct {
released map[uint32]bool
}
type definitionPattern struct {
// Each pattern has a cheap literal prefilter that is a necessary condition for a match,
// so the (much slower) regexp only runs on candidate lines.
type pattern struct {
kind string
maybe func(line string) bool
re *regexp.Regexp
}
var definitionPatterns = []definitionPattern{
{"class", regexp.MustCompile(`(?:class|struct|enum|trait|interface|record)\s+([A-Za-z_][A-Za-z0-9_]*)`)},
{"function", regexp.MustCompile(`(?:fn|func|def|function)\s+([A-Za-z_][A-Za-z0-9_]*)`)},
{"function", regexp.MustCompile(`(?:public|private|protected|static|async|export|const|let|var|unsafe|pub|virtual|override|inline|final|synchronized|abstract|extern|\s)+\s*([A-Za-z_][A-Za-z0-9_]*)\s*\([^;]*\)\s*(?:\{|=>|throws)`)},
func containsAny(literals ...string) func(string) bool {
return func(line string) bool {
for _, literal := range literals {
if strings.Contains(line, literal) {
return true
}
}
return false
}
}
var importPatterns = []*regexp.Regexp{
regexp.MustCompile(`from\s+["']?([^"';\s]+)`),
regexp.MustCompile(`import\s*["']([^"']+)`),
regexp.MustCompile(`^\s*import\s+([A-Za-z_][A-Za-z0-9_.]*)\s*(?:;|$|\bas\b)`),
regexp.MustCompile(`import\s*\(\s*["']([^"']+)`),
regexp.MustCompile(`require\s*\(\s*["']([^"']+)`),
regexp.MustCompile(`#include\s*[<"]([^>"]+)`),
regexp.MustCompile(`(?:use|mod)\s+([A-Za-z_][A-Za-z0-9_:]*)`),
regexp.MustCompile(`using\s+([A-Za-z_][A-Za-z0-9_.]*)`),
// callWithBody needs "(", then ")", then "{", "=>" or "throws" after that ")".
func callWithBody(line string) bool {
open := strings.IndexByte(line, '(')
if open < 0 {
return false
}
close := strings.IndexByte(line[open:], ')')
if close < 0 {
return false
}
tail := line[open+close:]
return strings.Contains(tail, "{") || strings.Contains(tail, "=>") || strings.Contains(tail, "throws")
}
var definitionPatterns = []pattern{
{"class", containsAny("class", "struct", "enum", "trait", "interface", "record"), regexp.MustCompile(`(?:class|struct|enum|trait|interface|record)\s+([A-Za-z_][A-Za-z0-9_]*)`)},
{"function", containsAny("fn", "func", "def"), regexp.MustCompile(`(?:fn|func|def|function)\s+([A-Za-z_][A-Za-z0-9_]*)`)},
{"function", callWithBody, regexp.MustCompile(`(?:public|private|protected|static|async|export|const|let|var|unsafe|pub|virtual|override|inline|final|synchronized|abstract|extern|\s)+\s*([A-Za-z_][A-Za-z0-9_]*)\s*\([^;]*\)\s*(?:\{|=>|throws)`)},
}
var importPatterns = []pattern{
{"", containsAny("from"), regexp.MustCompile(`from\s+["']?([^"';\s]+)`)},
{"", containsAny("import"), regexp.MustCompile(`import\s*["']([^"']+)`)},
{"", containsAny("import"), regexp.MustCompile(`^\s*import\s+([A-Za-z_][A-Za-z0-9_.]*)\s*(?:;|$|\bas\b)`)},
{"", containsAny("import"), regexp.MustCompile(`import\s*\(\s*["']([^"']+)`)},
{"", containsAny("require"), regexp.MustCompile(`require\s*\(\s*["']([^"']+)`)},
{"", containsAny("#include"), regexp.MustCompile(`#include\s*[<"]([^>"]+)`)},
{"", containsAny("use", "mod"), regexp.MustCompile(`(?:use|mod)\s+([A-Za-z_][A-Za-z0-9_:]*)`)},
{"", containsAny("using"), regexp.MustCompile(`using\s+([A-Za-z_][A-Za-z0-9_.]*)`)},
}
var complexityTokens = []string{" if ", " for ", " while ", " match ", " switch ", "&&", "||"}
@@ -209,6 +237,9 @@ func parseFile(root, path string) (*FileRecord, string) {
}
if mayDefine(line) {
for _, pattern := range definitionPatterns {
if !pattern.maybe(line) {
continue
}
if m := pattern.re.FindStringSubmatchIndex(line); m != nil && m[2] >= 0 {
record.Symbols = append(record.Symbols, Symbol{strings.Clone(line[m[2]:m[3]]), pattern.kind, record.Lines, strings.Clone(truncateRunes(strings.TrimSpace(line), 180))})
break
@@ -216,8 +247,11 @@ func parseFile(root, path string) (*FileRecord, string) {
}
}
if mayImport(line) {
for _, re := range importPatterns {
if m := re.FindStringSubmatchIndex(line); m != nil && m[2] >= 0 {
for _, pattern := range importPatterns {
if !pattern.maybe(line) {
continue
}
if m := pattern.re.FindStringSubmatchIndex(line); m != nil && m[2] >= 0 {
record.Imports = append(record.Imports, strings.Clone(line[m[2]:m[3]]))
}
}
+19 -2
View File
@@ -4,6 +4,7 @@ import (
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
)
@@ -63,8 +64,8 @@ func TestResolvesDottedModulesAsInferred(t *testing.T) {
func TestExtractsNamedJavaScriptImportTarget(t *testing.T) {
var imports []string
for _, re := range importPatterns {
if m := re.FindStringSubmatch("import { render } from './renderer.js';"); m != nil {
for _, p := range importPatterns {
if m := p.re.FindStringSubmatch("import { render } from './renderer.js';"); m != nil {
imports = append(imports, m[1])
}
}
@@ -207,3 +208,19 @@ func TestEmptyDirectoryFails(t *testing.T) {
t.Fatal("expected error for empty directory")
}
}
// BenchmarkIndexDirectory indexes CODENAV_BENCH_DIR, defaulting to the Go standard library source.
func BenchmarkIndexDirectory(b *testing.B) {
dir := os.Getenv("CODENAV_BENCH_DIR")
if dir == "" {
dir = filepath.Join(runtime.GOROOT(), "src")
}
if _, err := os.Stat(dir); err != nil {
b.Skip("no benchmark corpus:", err)
}
for b.Loop() {
if _, err := indexDirectory(dir, func(string, int, int, string) {}); err != nil {
b.Fatal(err)
}
}
}