Skip to content
This repository was archived by the owner on Aug 2, 2026. It is now read-only.

⚡ Bolt: Lexer keyword lookup optimization - #41

Open
srimon12 wants to merge 1 commit into
mainfrom
perf/lexer-keyword-lookup-246312254983283493
Open

⚡ Bolt: Lexer keyword lookup optimization#41
srimon12 wants to merge 1 commit into
mainfrom
perf/lexer-keyword-lookup-246312254983283493

Conversation

@srimon12

@srimon12 srimon12 commented Jul 3, 2026

Copy link
Copy Markdown
Owner

💡 What: Replaced the map[string]TokenKind lookup in the Lexer with an auto-generated case-insensitive switch statement (lookupKeywordFast). Added a go generate script so maintainers can still edit the map as the source of truth.

🎯 Why: The previous lookup mechanism caused runtime string allocations and hashing overhead. Replacing this with a structured switch statement matching string lengths and characters (via bitwise operators to coerce lowercase) ensures the lookups do not allocate.

📊 Impact:
Lexer Simple benchmark: ~491ns to ~393ns (20% faster)
Parser Simple benchmark: ~851ns to ~753ns (11% faster)

🔬 Measurement:
Run go test -bench . -benchmem ./internal/parser/ and compare BenchmarkLex_Simple and BenchmarkParse_Simple results.


PR created automatically by Jules for task 246312254983283493 started by @srimon12

Summary by CodeRabbit

  • Performance
    • Improved keyword recognition in the lexer, reducing memory allocations and speeding up tokenization.
    • Added automated generation for the optimized keyword lookup to keep matching fast and consistent.

Replaced map-based keyword lookup with an auto-generated length-and-character-based switch statement to eliminate string allocations and hashing overhead during lexical analysis.

Co-authored-by: srimon12 <33979603+srimon12@users.noreply.github.com>
@google-labs-jules

Copy link
Copy Markdown

👋 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 @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a Go generator (generate.go) that parses the lexer's keyword map and emits a generated file (keyword_lookup.go) containing lookupKeywordFast, a switch-based case-insensitive keyword matcher. lexer.go is updated to use this fast lookup instead of the previous map-based lookupKeyword, and .jules/bolt.md documents the change.

Changes

Fast Keyword Lookup

Layer / File(s) Summary
Keyword lookup generator tool
internal/lexer/generate.go
New build-ignored generator reads the keywords map from lexer.go, groups keywords by length and first character, generates lookupKeywordFast source with case-insensitive per-character checks, writes keyword_lookup.go, and runs gofmt.
Generated fast lookup implementation
internal/lexer/keyword_lookup.go
New generated file implements lookupKeywordFast(s string) (TokenKind, bool) using a length-based switch (2–12) with nested first-character and per-character case-insensitive matches, returning TokenKind or (0, false).
Wire lookupKeywordFast into lexer
internal/lexer/lexer.go, .jules/bolt.md
Adds a //go:generate directive, replaces the readIdentifier keyword-resolution call from lookupKeyword to lookupKeywordFast, and documents the optimization in bolt.md.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Developer
  participant generate.go
  participant lexer.go
  participant keyword_lookup.go

  Developer->>generate.go: go run generate.go
  generate.go->>lexer.go: read keywords map literal
  generate.go->>generate.go: group keywords by length and first char
  generate.go->>keyword_lookup.go: write lookupKeywordFast source
  generate.go->>keyword_lookup.go: gofmt -w
  lexer.go->>keyword_lookup.go: call lookupKeywordFast(word)
  keyword_lookup.go-->>lexer.go: return TokenKind, matched
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: optimizing lexer keyword lookup.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/lexer-keyword-lookup-246312254983283493

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
internal/lexer/generate.go (1)

20-43: 📐 Maintainability & Code Quality | 🔵 Trivial

Fragile text-based parsing of the keywords map.

Extracting the map via bytes.Index/line-splitting/SplitN(":") is brittle: it assumes one entry per line, no trailing comments, and an exact literal signature for the map declaration. Any future inline comment ("ID": TokenKindId, // ambiguous) or reformatted map would either silently mis-parse the value (leaving stray text appended to kindStr, likely surfacing only as a compile error in the generated file) or corrupt the extracted keyword set without a clear diagnostic. Since this is the single source of truth feeding the runtime lookup, consider parsing with go/parser/go/ast to walk the actual ast.CompositeLit for keywords, which is robust to formatting/comments and gives you typed key/value pairs directly.

🤖 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/generate.go` around lines 20 - 43, The keyword extraction in
generate.go is doing fragile text parsing of the keywords map, so it can break
or misread entries when formatting, comments, or the declaration shape changes.
Update the logic in the generator to locate and read the keywords composite
literal via go/parser and go/ast instead of bytes.Index, Split, and SplitN, and
derive the key/value pairs directly from the ast.CompositeLit for keywords. Keep
the existing byLength grouping, but populate it from typed AST nodes so the
generator remains stable across formatting and inline comments.
🤖 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/generate.go`:
- Around line 20-21: The marker lookup in generate.go can panic if "var keywords
= map[string]TokenKind{" is not present, because bytes.Index may return -1
before slicing content[startIdx:]. Update the keyword extraction logic around
the startIdx/endIdx calculation to validate that both markers are found before
using them, and return a clear error from the generator instead of slicing
invalid bounds.
- Around line 14-125: Add a drift/parity check between the source keywords map
and the generated fast lookup so manual edits can’t silently desync
tokenization. Use the existing generate flow in generate.go and the lexer-side
symbols keywords and lookupKeywordFast to create either a test that iterates
every keyword entry and verifies the generated lookup returns the same
TokenKind, or a CI check that runs go generate ./... and fails on a dirty diff.
Ensure the check is wired where it will run automatically with the lexer tests.

---

Nitpick comments:
In `@internal/lexer/generate.go`:
- Around line 20-43: The keyword extraction in generate.go is doing fragile text
parsing of the keywords map, so it can break or misread entries when formatting,
comments, or the declaration shape changes. Update the logic in the generator to
locate and read the keywords composite literal via go/parser and go/ast instead
of bytes.Index, Split, and SplitN, and derive the key/value pairs directly from
the ast.CompositeLit for keywords. Keep the existing byLength grouping, but
populate it from typed AST nodes so the generator remains stable across
formatting and inline comments.
🪄 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: ad709c91-fde0-4ee8-9dee-c612f61c1eb1

📥 Commits

Reviewing files that changed from the base of the PR and between 7843d01 and 095df4f.

📒 Files selected for processing (4)
  • .jules/bolt.md
  • internal/lexer/generate.go
  • internal/lexer/keyword_lookup.go
  • internal/lexer/lexer.go

Comment on lines +14 to +125
func main() {
content, err := os.ReadFile("lexer.go")
if err != nil {
panic(err)
}

startIdx := bytes.Index(content, []byte("var keywords = map[string]TokenKind{"))
endIdx := bytes.Index(content[startIdx:], []byte("}")) + startIdx

mapLines := bytes.Split(content[startIdx:endIdx], []byte("\n"))

byLength := make(map[int]map[string]string)
for _, line := range mapLines[1:] {
lineStr := strings.TrimSpace(string(line))
if lineStr == "" || strings.HasPrefix(lineStr, "//") {
continue
}
parts := strings.SplitN(lineStr, ":", 2)
if len(parts) != 2 {
continue
}
kwStr := strings.Trim(strings.TrimSpace(parts[0]), `"`)
kindStr := strings.TrimSuffix(strings.TrimSpace(parts[1]), ",")

l := len(kwStr)
if byLength[l] == nil {
byLength[l] = make(map[string]string)
}
byLength[l][kwStr] = kindStr
}

var buf bytes.Buffer
buf.WriteString(`// Code generated by go generate; DO NOT EDIT.
package lexer

func lookupKeywordFast(s string) (TokenKind, bool) {
if len(s) == 0 {
return 0, false
}
switch len(s) {
`)

var lengths []int
for l := range byLength {
lengths = append(lengths, l)
}
sort.Ints(lengths)

for _, l := range lengths {
buf.WriteString(fmt.Sprintf("\tcase %d:\n", l))
buf.WriteString("\t\tswitch s[0] | 0x20 {\n")

byFirstChar := make(map[byte]map[string]string)
for kw, kind := range byLength[l] {
fc := kw[0] | 0x20
if byFirstChar[fc] == nil {
byFirstChar[fc] = make(map[string]string)
}
byFirstChar[fc][kw] = kind
}

var chars []int
for c := range byFirstChar {
chars = append(chars, int(c))
}
sort.Ints(chars)

for _, cInt := range chars {
c := byte(cInt)
buf.WriteString(fmt.Sprintf("\t\tcase '%c':\n", c))

var sortedKws []string
for kw := range byFirstChar[c] {
sortedKws = append(sortedKws, kw)
}
sort.Strings(sortedKws)

for _, kw := range sortedKws {
kind := byFirstChar[c][kw]
buf.WriteString("\t\t\tif ")
for i := 1; i < len(kw); i++ {
if i > 1 {
buf.WriteString(" && ")
}
lc := kw[i] | 0x20
if lc >= 'a' && lc <= 'z' {
buf.WriteString(fmt.Sprintf("(s[%d] | 0x20) == '%c'", i, lc))
} else {
buf.WriteString(fmt.Sprintf("s[%d] == '%c'", i, kw[i]))
}
}
if len(kw) == 1 {
buf.WriteString("true")
}
buf.WriteString(" {\n")
buf.WriteString(fmt.Sprintf("\t\t\t\treturn %s, true\n", kind))
buf.WriteString("\t\t\t}\n")
}
}
buf.WriteString("\t\t}\n")
}

buf.WriteString("\t}\n")
buf.WriteString("\treturn 0, false\n}\n")

err = os.WriteFile("keyword_lookup.go", buf.Bytes(), 0644)
if err != nil {
panic(err)
}

exec.Command("gofmt", "-w", "keyword_lookup.go").Run()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for existing parity test or CI drift-check for the generated keyword lookup
rg -n 'lookupKeywordFast|keywords\b' --type=go internal/lexer
rg -n 'go generate' -g '*.yml' -g '*.yaml' -g 'Makefile' .
fd . .github/workflows 2>/dev/null | xargs -I{} grep -l 'go generate' {} 2>/dev/null

Repository: srimon12/qql-go

Length of output: 587


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== relevant files =='
git ls-files internal/lexer internal/parser .github/workflows Makefile | sed -n '1,200p'

echo
echo '== lexer files outline =='
ast-grep outline internal/lexer/lexer.go --view expanded
echo
ast-grep outline internal/lexer/generate.go --view expanded
echo
ast-grep outline internal/lexer/keyword_lookup.go --view expanded

echo
echo '== search for tests / regeneration / drift checks =='
rg -n 'lookupKeywordFast|keywords\b|go generate|generated by go generate|keyword_lookup.go' internal .github/workflows Makefile -g '*.go' -g '*.yml' -g '*.yaml' -g 'Makefile'

echo
echo '== workflow files mentioning generate =='
fd -a . .github/workflows 2>/dev/null | while read -r f; do
  echo "--- $f"
  rg -n 'go generate|git diff|keyword_lookup|lexer' "$f" || true
done

echo
echo '== focused lexer snippets =='
sed -n '1,120p' internal/lexer/lexer.go
echo
sed -n '1,120p' internal/lexer/keyword_lookup.go
echo
sed -n '1,220p' internal/lexer/generate.go

Repository: srimon12/qql-go

Length of output: 2992


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== lexer test file outline =='
ast-grep outline internal/lexer/lexer_test.go --view expanded

echo
echo '== lexer tests mentioning keywords / lookup =='
rg -n 'lookupKeywordFast|keywords|go generate|generated by go generate' internal/lexer/lexer_test.go internal/lexer/*.go

echo
echo '== CI workflow snippets =='
sed -n '1,220p' .github/workflows/ci.yml
echo
sed -n '1,220p' .github/workflows/release.yml

Repository: srimon12/qql-go

Length of output: 8879


Add a drift check for keyword generation The lexer tests cover current cases, but there’s still no explicit parity check between keywords and lookupKeywordFast. A manual edit to keywords can desync the committed generated file and silently change tokenization. Add a test that compares every entry against the generated lookup, or fail CI when go generate ./... produces a diff.

🤖 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/generate.go` around lines 14 - 125, Add a drift/parity check
between the source keywords map and the generated fast lookup so manual edits
can’t silently desync tokenization. Use the existing generate flow in
generate.go and the lexer-side symbols keywords and lookupKeywordFast to create
either a test that iterates every keyword entry and verifies the generated
lookup returns the same TokenKind, or a CI check that runs go generate ./... and
fails on a dirty diff. Ensure the check is wired where it will run automatically
with the lexer tests.

Comment on lines +20 to +21
startIdx := bytes.Index(content, []byte("var keywords = map[string]TokenKind{"))
endIdx := bytes.Index(content[startIdx:], []byte("}")) + startIdx

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard against marker-not-found before slicing.

If "var keywords = map[string]TokenKind{" isn't found (e.g., after reformatting lexer.go), bytes.Index returns -1, and content[startIdx:] panics with an unhelpful "slice bounds out of range" instead of a clear error.

🛡️ Proposed fix
 	startIdx := bytes.Index(content, []byte("var keywords = map[string]TokenKind{"))
+	if startIdx == -1 {
+		panic("could not find keywords map declaration in lexer.go")
+	}
 	endIdx := bytes.Index(content[startIdx:], []byte("}")) + startIdx
📝 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.

Suggested change
startIdx := bytes.Index(content, []byte("var keywords = map[string]TokenKind{"))
endIdx := bytes.Index(content[startIdx:], []byte("}")) + startIdx
startIdx := bytes.Index(content, []byte("var keywords = map[string]TokenKind{"))
if startIdx == -1 {
panic("could not find keywords map declaration in lexer.go")
}
endIdx := bytes.Index(content[startIdx:], []byte("}")) + startIdx
🤖 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/generate.go` around lines 20 - 21, The marker lookup in
generate.go can panic if "var keywords = map[string]TokenKind{" is not present,
because bytes.Index may return -1 before slicing content[startIdx:]. Update the
keyword extraction logic around the startIdx/endIdx calculation to validate that
both markers are found before using them, and return a clear error from the
generator instead of slicing invalid bounds.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant