⚡ Bolt: Implement fast path for lexer keyword lookup - #39
Conversation
Generates an optimized switch statement matching keywords by length and case-insensitively, avoiding the overhead of map lookup even when the map lookup allocates no memory. Evaluated benchmarks show ~2.5x speedup for lookups. Fallback to `keywords` map is retained for safety. Added `gen.go` to re-generate when keywords map changes. Co-authored-by: srimon12 <33979603+srimon12@users.noreply.github.com>
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
📝 WalkthroughWalkthroughAdds a ChangesLexer fast keyword lookup
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (4)
internal/lexer/gen_lookup.go (2)
16-45: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winAdd a sync guard so the committed
lookup_fast.gocan't silently drift fromkeywords.Since
lookup_fast.gois checked in andlookupKeywordonly falls back to thekeywordsmap when the fast path misses, a stale generated file would not cause a hard failure — it would just silently bypass the optimization (or, worse, return a wrong kind if a mapping ever changed). A cheap table-driven test that assertslookupKeywordFastagrees with thekeywordsmap for every entry would catch drift in CI.func TestLookupKeywordFastInSync(t *testing.T) { for kw, want := range keywords { if got, ok := lookupKeywordFast(kw); !ok || got != want { t.Errorf("lookupKeywordFast(%q) = (%v,%v); want (%v,true)", kw, got, ok, want) } } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/lexer/gen_lookup.go` around lines 16 - 45, The committed fast keyword lookup can drift from the source keywords without failing, so add a sync guard in tests to keep them aligned. Create a table-driven test around lookupKeywordFast that iterates over the keywords map and asserts each entry returns the same TokenKind and a true ok result, using the existing lookupKeywordFast and keywords symbols so CI catches mismatches if the generated lookup_fast.go falls out of sync.
28-45: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueParsing is brittle (as the comment notes); consider keying off the keyword quotes.
breakon the first line containing}andSplitN(line, ":", 2)happen to work for the current map, but any inline comment, a}in a trailing comment, or a reformatting would silently truncate or mis-parse the keyword set. Gating on lines that actually start with a quoted key (e.g. requirekwto be non-empty and the line to be a"...": ...,entry) would make regeneration more robust.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/lexer/gen_lookup.go` around lines 28 - 45, The keyword map parsing in gen_lookup.go is too brittle because it stops on any line containing “}” and accepts any split on “:”, which can mis-parse or truncate the keyword list. Update the parsing logic in the lookup generation flow (the map scan around inMap and keywords append) to only consume lines that actually begin with a quoted keyword entry, and only terminate when you reach the real map closing brace, so inline comments or reformatting do not affect regeneration.internal/lexer/lookup_fast.go (2)
4-313: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoffThe fast path is a linear scan within each length bucket, not a "perfect switch."
Despite the PR/code wording, each
casedoes a sequential chain ofequalsCaseInsensitivecalls (e.g. case 6 walks ~24 comparisons worst case). It's still likely faster than a map hash on short keys, but if the benchmark gain matters, a secondaryswitch s[0](uppercased) inside each length bucket would cut the comparisons to ~1. This is a generator change ingen_lookup.go, not a hand-edit here (file isDO NOT EDIT).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/lexer/lookup_fast.go` around lines 4 - 313, The current fast path in lookupKeywordFast is only a linear chain of equalsCaseInsensitive checks per length bucket, so update the generator instead of hand-editing this file. In gen_lookup.go, add a secondary dispatch by the first character (normalized for case) inside each length-based bucket so lookupKeywordFast can narrow candidates before calling equalsCaseInsensitive, which reduces the worst-case comparisons while preserving TokenKind mappings. Keep the generated lookupKeywordFast signature and all existing keyword/token names intact.
315-329: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
equalsCaseInsensitiveduplicateshasPrefixCaseInsensitiveinlexer.go.Both implement the same ASCII uppercase-and-compare loop; the only difference is the leading length check (already guaranteed here by the
switch len(s)). Worth consolidating onto one helper to avoid two divergent copies of the case-folding rule — though since this file is generated, the dedup would have to come from the generator.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/lexer/lookup_fast.go` around lines 315 - 329, The ASCII case-folding logic in equalsCaseInsensitive duplicates hasPrefixCaseInsensitive, so consolidate both through a single shared helper in the generator that performs the uppercase-and-compare loop. Update the generated lexer code path that emits equalsCaseInsensitive and hasPrefixCaseInsensitive to call the same reusable implementation, keeping the existing len(s) guard where needed and ensuring only one source of truth for the case-insensitive comparison rules.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/lexer/benchmark_test.go`:
- Around line 22-25: The tokenize benchmark is ignoring both return values from
Tokenize, so it can benchmark an error path instead of real tokenization work.
Update the benchmark loop in the benchmark function to capture the tokens and
error from Tokenize, keep the token result alive so it is not optimized away,
and call b.Fatal on any error inside the loop to ensure the benchmark stays on
the success path.
In `@internal/lexer/lookup_test.go`:
- Around line 7-31: The benchmarks in lookup_test.go are discarding the
TokenKind and bool results from lookupKeyword and lookupKeywordFast, which can
skew the measurements and hide invalid keyword misses. Update
BenchmarkLookupKeyword, BenchmarkLookupKeywordAlloc, and
BenchmarkLookupKeywordFast to assign the returned values to package-level sink
variables, and add validation that panics or fails the benchmark if ok is false.
Use the existing benchmark functions and the lookupKeyword/lookupKeywordFast
symbols to keep the fix localized.
---
Nitpick comments:
In `@internal/lexer/gen_lookup.go`:
- Around line 16-45: The committed fast keyword lookup can drift from the source
keywords without failing, so add a sync guard in tests to keep them aligned.
Create a table-driven test around lookupKeywordFast that iterates over the
keywords map and asserts each entry returns the same TokenKind and a true ok
result, using the existing lookupKeywordFast and keywords symbols so CI catches
mismatches if the generated lookup_fast.go falls out of sync.
- Around line 28-45: The keyword map parsing in gen_lookup.go is too brittle
because it stops on any line containing “}” and accepts any split on “:”, which
can mis-parse or truncate the keyword list. Update the parsing logic in the
lookup generation flow (the map scan around inMap and keywords append) to only
consume lines that actually begin with a quoted keyword entry, and only
terminate when you reach the real map closing brace, so inline comments or
reformatting do not affect regeneration.
In `@internal/lexer/lookup_fast.go`:
- Around line 4-313: The current fast path in lookupKeywordFast is only a linear
chain of equalsCaseInsensitive checks per length bucket, so update the generator
instead of hand-editing this file. In gen_lookup.go, add a secondary dispatch by
the first character (normalized for case) inside each length-based bucket so
lookupKeywordFast can narrow candidates before calling equalsCaseInsensitive,
which reduces the worst-case comparisons while preserving TokenKind mappings.
Keep the generated lookupKeywordFast signature and all existing keyword/token
names intact.
- Around line 315-329: The ASCII case-folding logic in equalsCaseInsensitive
duplicates hasPrefixCaseInsensitive, so consolidate both through a single shared
helper in the generator that performs the uppercase-and-compare loop. Update the
generated lexer code path that emits equalsCaseInsensitive and
hasPrefixCaseInsensitive to call the same reusable implementation, keeping the
existing len(s) guard where needed and ensuring only one source of truth for the
case-insensitive comparison rules.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: a6e32caa-bb4f-4755-8275-aef290956002
📒 Files selected for processing (7)
.jules/bolt.mdinternal/lexer/benchmark_test.gointernal/lexer/gen.gointernal/lexer/gen_lookup.gointernal/lexer/lexer.gointernal/lexer/lookup_fast.gointernal/lexer/lookup_test.go
| b.ResetTimer() | ||
| for i := 0; i < b.N; i++ { | ||
| lex.Tokenize(query) | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Keep the tokenize benchmark on the success path.
Tokenize returns ([]Token, error), but this loop drops both values. If the sample query ever starts failing, the benchmark will still pass and may report a faster error path instead of real tokenization work. Sink the tokens and b.Fatal on err inside the loop.
Proposed fix
package lexer
import (
"strings"
"testing"
)
+
+var benchmarkTokens []Token
func BenchmarkTokenize(b *testing.B) {
lex := &Lexer{}
query := `SELECT id, _score, VECTOR, VECTOR[my_vec], PAYLOAD, PAYLOAD.city,
PAYLOAD[location],
@@
b.ResetTimer()
for i := 0; i < b.N; i++ {
- lex.Tokenize(query)
+ tokens, err := lex.Tokenize(query)
+ if err != nil {
+ b.Fatal(err)
+ }
+ benchmarkTokens = tokens
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| b.ResetTimer() | |
| for i := 0; i < b.N; i++ { | |
| lex.Tokenize(query) | |
| } | |
| package lexer | |
| import ( | |
| "strings" | |
| "testing" | |
| ) | |
| var benchmarkTokens []Token | |
| func BenchmarkTokenize(b *testing.B) { | |
| lex := &Lexer{} | |
| query := `SELECT id, _score, VECTOR, VECTOR[my_vec], PAYLOAD, PAYLOAD.city, | |
| PAYLOAD[location], | |
| MAX(values), SUM(INT_TO_FLOAT(score) * values) FROM index_name | |
| WHERE payload.city = 'ABC' OR value = 1 OR city = 'XYZ' AND _score > 0 | |
| ORDER BY score DESC LIMIT 10` | |
| b.ResetTimer() | |
| for i := 0; i < b.N; i++ { | |
| tokens, err := lex.Tokenize(query) | |
| if err != nil { | |
| b.Fatal(err) | |
| } | |
| benchmarkTokens = tokens | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/lexer/benchmark_test.go` around lines 22 - 25, The tokenize
benchmark is ignoring both return values from Tokenize, so it can benchmark an
error path instead of real tokenization work. Update the benchmark loop in the
benchmark function to capture the tokens and error from Tokenize, keep the token
result alive so it is not optimized away, and call b.Fatal on any error inside
the loop to ensure the benchmark stays on the success path.
| func BenchmarkLookupKeyword(b *testing.B) { | ||
| b.ResetTimer() | ||
| for i := 0; i < b.N; i++ { | ||
| lookupKeyword("select") | ||
| lookupKeyword("where") | ||
| lookupKeyword("limit") | ||
| } | ||
| } | ||
|
|
||
| func BenchmarkLookupKeywordAlloc(b *testing.B) { | ||
| b.ReportAllocs() | ||
| b.ResetTimer() | ||
| for i := 0; i < b.N; i++ { | ||
| lookupKeyword("select") | ||
| } | ||
| } | ||
|
|
||
| func BenchmarkLookupKeywordFast(b *testing.B) { | ||
| b.ResetTimer() | ||
| for i := 0; i < b.N; i++ { | ||
| lookupKeywordFast("select") | ||
| lookupKeywordFast("where") | ||
| lookupKeywordFast("limit") | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Sink and validate the lookup results in these benchmarks.
All three benchmarks discard (TokenKind, bool). That makes the numbers easier to skew, and it also lets a broken fast-path/miss path benchmark as if it were a valid keyword hit. Store the results in package-level sinks and fail if ok is false.
Proposed fix
package lexer
import (
"testing"
)
+
+var (
+ benchmarkKeywordKind TokenKind
+ benchmarkKeywordOK bool
+)
func BenchmarkLookupKeyword(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
- lookupKeyword("select")
- lookupKeyword("where")
- lookupKeyword("limit")
+ benchmarkKeywordKind, benchmarkKeywordOK = lookupKeyword("select")
+ if !benchmarkKeywordOK {
+ b.Fatal("select was not recognized")
+ }
+ benchmarkKeywordKind, benchmarkKeywordOK = lookupKeyword("where")
+ if !benchmarkKeywordOK {
+ b.Fatal("where was not recognized")
+ }
+ benchmarkKeywordKind, benchmarkKeywordOK = lookupKeyword("limit")
+ if !benchmarkKeywordOK {
+ b.Fatal("limit was not recognized")
+ }
}
}
func BenchmarkLookupKeywordAlloc(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
- lookupKeyword("select")
+ benchmarkKeywordKind, benchmarkKeywordOK = lookupKeyword("select")
+ if !benchmarkKeywordOK {
+ b.Fatal("select was not recognized")
+ }
}
}
func BenchmarkLookupKeywordFast(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
- lookupKeywordFast("select")
- lookupKeywordFast("where")
- lookupKeywordFast("limit")
+ benchmarkKeywordKind, benchmarkKeywordOK = lookupKeywordFast("select")
+ if !benchmarkKeywordOK {
+ b.Fatal("select was not recognized")
+ }
+ benchmarkKeywordKind, benchmarkKeywordOK = lookupKeywordFast("where")
+ if !benchmarkKeywordOK {
+ b.Fatal("where was not recognized")
+ }
+ benchmarkKeywordKind, benchmarkKeywordOK = lookupKeywordFast("limit")
+ if !benchmarkKeywordOK {
+ b.Fatal("limit was not recognized")
+ }
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| func BenchmarkLookupKeyword(b *testing.B) { | |
| b.ResetTimer() | |
| for i := 0; i < b.N; i++ { | |
| lookupKeyword("select") | |
| lookupKeyword("where") | |
| lookupKeyword("limit") | |
| } | |
| } | |
| func BenchmarkLookupKeywordAlloc(b *testing.B) { | |
| b.ReportAllocs() | |
| b.ResetTimer() | |
| for i := 0; i < b.N; i++ { | |
| lookupKeyword("select") | |
| } | |
| } | |
| func BenchmarkLookupKeywordFast(b *testing.B) { | |
| b.ResetTimer() | |
| for i := 0; i < b.N; i++ { | |
| lookupKeywordFast("select") | |
| lookupKeywordFast("where") | |
| lookupKeywordFast("limit") | |
| } | |
| } | |
| var ( | |
| benchmarkKeywordKind TokenKind | |
| benchmarkKeywordOK bool | |
| ) | |
| func BenchmarkLookupKeyword(b *testing.B) { | |
| b.ResetTimer() | |
| for i := 0; i < b.N; i++ { | |
| benchmarkKeywordKind, benchmarkKeywordOK = lookupKeyword("select") | |
| if !benchmarkKeywordOK { | |
| b.Fatal("select was not recognized") | |
| } | |
| benchmarkKeywordKind, benchmarkKeywordOK = lookupKeyword("where") | |
| if !benchmarkKeywordOK { | |
| b.Fatal("where was not recognized") | |
| } | |
| benchmarkKeywordKind, benchmarkKeywordOK = lookupKeyword("limit") | |
| if !benchmarkKeywordOK { | |
| b.Fatal("limit was not recognized") | |
| } | |
| } | |
| } | |
| func BenchmarkLookupKeywordAlloc(b *testing.B) { | |
| b.ReportAllocs() | |
| b.ResetTimer() | |
| for i := 0; i < b.N; i++ { | |
| benchmarkKeywordKind, benchmarkKeywordOK = lookupKeyword("select") | |
| if !benchmarkKeywordOK { | |
| b.Fatal("select was not recognized") | |
| } | |
| } | |
| } | |
| func BenchmarkLookupKeywordFast(b *testing.B) { | |
| b.ResetTimer() | |
| for i := 0; i < b.N; i++ { | |
| benchmarkKeywordKind, benchmarkKeywordOK = lookupKeywordFast("select") | |
| if !benchmarkKeywordOK { | |
| b.Fatal("select was not recognized") | |
| } | |
| benchmarkKeywordKind, benchmarkKeywordOK = lookupKeywordFast("where") | |
| if !benchmarkKeywordOK { | |
| b.Fatal("where was not recognized") | |
| } | |
| benchmarkKeywordKind, benchmarkKeywordOK = lookupKeywordFast("limit") | |
| if !benchmarkKeywordOK { | |
| b.Fatal("limit was not recognized") | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/lexer/lookup_test.go` around lines 7 - 31, The benchmarks in
lookup_test.go are discarding the TokenKind and bool results from lookupKeyword
and lookupKeywordFast, which can skew the measurements and hide invalid keyword
misses. Update BenchmarkLookupKeyword, BenchmarkLookupKeywordAlloc, and
BenchmarkLookupKeywordFast to assign the returned values to package-level sink
variables, and add validation that panics or fails the benchmark if ok is false.
Use the existing benchmark functions and the lookupKeyword/lookupKeywordFast
symbols to keep the fix localized.
💡 What: Replaced the lexer's
lookupKeywordmap lookup with a generatedlookupKeywordFastfunction that performs length-basedswitchcomparisons.🎯 Why: The lexer performs keyword lookups very frequently. A map lookup involves hashing, even when optimized by Go to avoid memory allocations
map[string(byte_slice)]. Using a perfectly sized static switch statement drastically reduces lookup overhead.📊 Impact: Reduces lookup latency by roughly 2.5x. Benchmarks improved from ~74ns to ~29ns per operation. Overall
Tokenizeimproved.🔬 Measurement: Verify by running
cd internal/lexer && go test -bench=BenchmarkLookupKeyword.PR created automatically by Jules for task 2134823455075652399 started by @srimon12
Summary by CodeRabbit
Performance Improvements
Tests