Skip to content

perf(ast/cmp): avoid re-tokenizing when comparing token slices#325

Open
DonIsaac wants to merge 4 commits into
mainfrom
don/up/ast-comparator
Open

perf(ast/cmp): avoid re-tokenizing when comparing token slices#325
DonIsaac wants to merge 4 commits into
mainfrom
don/up/ast-comparator

Conversation

@DonIsaac

@DonIsaac DonIsaac commented Oct 11, 2025

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • Bug Fixes

    • Improved reliability of duplicate-case detection in switch statements, reducing false positives and missed matches.
  • Refactor

    • Optimized internal comparison to use token-based fast paths when available, improving analysis accuracy and performance for identifier-heavy code.
    • Harmonized comparison internals without changing user workflows.

@coderabbitai

coderabbitai Bot commented Oct 11, 2025

Copy link
Copy Markdown

Walkthrough

Refactors AstComparator into a stateful comparator with an optional tokens field, converts eql into a method, and updates the duplicate_case rule to construct and use an AstComparator{ .ast = ast, .tokens = ctx.tokens() }; adds a branch-hinted early return when a switch is missing.

Changes

Cohort / File(s) Summary
AstComparator refactor
src/visit/AstComparator.zig
Adds tokens: ?*const TokenSlice = null to AstComparator; converts eql to a method pub fn eql(self: *const AstComparator, a, b) bool; introduces tokensEql/mainTokensEql/maybeTokensEql with a comptime skip_tag_check flag; updates internal call sites to use token-aware comparisons and preserves fallbacks when tokens are absent; adjusts imports and type aliases.
Linter rule adjustment
src/linter/rules/duplicate_case.zig
Replaces direct AstComparator.eql calls with an instance-based call using AstComparator{ .ast = ast, .tokens = ctx.tokens() }; adds a branch-hinted block for the early return when a switch statement is missing.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant LR as LinterRule:duplicate_case
  participant AC as AstComparator
  participant AST as AST
  participant TOK as Tokens?

  LR->>AC: construct { ast, tokens = ctx.tokens() }
  LR->>AC: eql(nodeA, nodeB)
  activate AC
  AC->>AC: eqlInner(nodeA,nodeB)
  alt tokens available
    AC->>TOK: tokensEql(..., skip_tag_check=false)
    TOK-->>AC: equal / unequal
  else tokens absent
    AC->>AST: source/token-slice based compare (fallback)
    AST-->>AC: equal / unequal
  end
  AC-->>LR: bool result
  deactivate AC

  note over LR: If switch missing → branch-hinted early return
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

C-enhancement

Poem

Hop hop! I sniff the token trail,
I compare each node without fail.
If no switch—I'll quickly bail,
With a hinted hop and tidy tail. 🥕🐇

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title concisely and accurately summarizes the primary change by indicating a performance optimization in the AST comparator to avoid re-tokenizing token slices, using a conventional prefix and clear language without extraneous information.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch don/up/ast-comparator

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 and usage tips.

@github-actions github-actions Bot added the A-linter Area - linter and lint rules label Oct 11, 2025

@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: 1

🧹 Nitpick comments (2)
src/linter/rules/duplicate_case.zig (1)

100-101: Consider hoisting comparator creation outside the loop.

The comparator is currently created for every pair of cases being compared. Since it's just a lightweight struct containing pointers and the tokens don't change, you could hoist the creation to line 94 (before the outer loop) for a minor efficiency gain:

 const cases = switchStmt.ast.cases;
+const comparator = AstComparator{ .ast = ast, .tokens = ctx.tokens() };

 // check each case statement against each other
 for (cases, 0..) |case, i| {
     for ((i + 1)..cases.len) |j| {
         const other = cases[j];
         const a = ast.fullSwitchCase(case) orelse unreachable;
         const b = ast.fullSwitchCase(other) orelse unreachable;
-        const comparator = AstComparator{ .ast = ast, .tokens = ctx.tokens() };
         if (comparator.eql(a.ast.target_expr, b.ast.target_expr)) {
src/visit/AstComparator.zig (1)

228-253: Excellent optimization! Consider updating the TODO comment.

The refactored tokensEql function successfully implements the optimization described in the TODO comment when tokens are provided. The comptime skip_tag_check parameter is a nice touch that allows callers to avoid redundant tag comparisons.

However, the TODO comment at line 229 now appears outdated since the optimization is implemented in the new code path (lines 230-243). Consider either:

  1. Removing the TODO entirely, or
  2. Moving it inside the else branch (line 245) to clarify it only applies to the fallback path

Apply this diff to remove the TODO:

 fn tokensEql(self: *const AstComparator, a: Ast.TokenIndex, b: Ast.TokenIndex, comptime skip_tag_check: bool) bool {
-    // TODO: source tokens from TokenList.Slice to avoid re-parsing
     if (self.tokens) |tokens| {

Or move it to the fallback branch:

 fn tokensEql(self: *const AstComparator, a: Ast.TokenIndex, b: Ast.TokenIndex, comptime skip_tag_check: bool) bool {
-    // TODO: source tokens from TokenList.Slice to avoid re-parsing
     if (self.tokens) |tokens| {
         const left = tokens.get(a);
         const right = tokens.get(b);
         if (comptime !skip_tag_check) {
             if (left.tag != right.tag) return false;
             // we're comparing keywords or operators; no need to compare source text
             // (an `orelse` token will always be "orelse")
             if (left.tag.lexeme()) |_| return true;
         }

         const left_slice = self.ast.source[left.loc.start..left.loc.end];
         const right_slice = self.ast.source[right.loc.start..right.loc.end];
         return mem.eql(u8, left_slice, right_slice);
     }

+    // TODO: source tokens from TokenList.Slice to avoid re-parsing
     const tok_tags: []Token.Tag = self.ast.tokens.items(.tag);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8b55356 and e2901f3.

📒 Files selected for processing (2)
  • src/linter/rules/duplicate_case.zig (2 hunks)
  • src/visit/AstComparator.zig (7 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test (windows-latest)
🔇 Additional comments (3)
src/linter/rules/duplicate_case.zig (1)

88-91: LGTM! Branch hint is correctly applied.

The .likely hint on the early return is correct. Since this rule runs on every AST node and most nodes are not switch statements, the common path is indeed returning early when fullSwitch returns null.

src/visit/AstComparator.zig (2)

13-18: LGTM! Clean conversion to instance method.

The refactor from a static function to an instance method with early-return optimization is well-structured and aligns with the new stateful design.


93-97: LGTM! Debug assertions appropriately verify token types.

The debug assertions ensure that field access tokens are identifiers, and passing skip_tag_check = true to tokensEql avoids redundant checks since the assertions already validated the token types.

Comment thread src/visit/AstComparator.zig
@codecov

codecov Bot commented Oct 12, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.30435% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.61%. Comparing base (e481d11) to head (776d9d0).
⚠️ Report is 33 commits behind head on main.

Files with missing lines Patch % Lines
src/visit/AstComparator.zig 89.47% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #325      +/-   ##
==========================================
- Coverage   91.67%   91.61%   -0.06%     
==========================================
  Files          91       91              
  Lines        7809     7816       +7     
==========================================
+ Hits         7159     7161       +2     
- Misses        650      655       +5     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@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: 0

🧹 Nitpick comments (1)
src/visit/AstComparator.zig (1)

228-253: Excellent optimization! Remove outdated TODO.

The token comparison optimization is well-implemented:

  • Uses pre-tokenized token locations when available to avoid re-parsing
  • Early return for lexemes (line 237) is correct since keywords/operators have fixed text
  • Falls back gracefully when tokens are unavailable
  • The comptime skip_tag_check parameter enables further optimization by callers

However, the TODO comment at line 229 is now outdated since the implementation below does exactly what the TODO suggests.

Apply this diff to remove the outdated TODO:

-    // TODO: source tokens from TokenList.Slice to avoid re-parsing
     if (self.tokens) |tokens| {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e2901f3 and 776d9d0.

📒 Files selected for processing (1)
  • src/visit/AstComparator.zig (7 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Collect and upload test coverage
🔇 Additional comments (4)
src/visit/AstComparator.zig (4)

5-11: LGTM! Well-documented performance optimization.

The optional tokens field is a sound design choice that provides backwards compatibility while enabling the performance optimization described in the PR objectives. The documentation clearly explains the rationale.


13-13: LGTM! Breaking change aligns with PR objectives.

Converting eql to an instance method is the correct design for a stateful comparator. This change also fixes what appears to be a type error in the previous signature where the second parameter was typed as bool instead of Node.Index.


93-97: LGTM! Good use of debug assertions.

The debug assertions verify token types while the skip_tag_check=true parameter avoids redundant tag comparisons in production. This is a sound optimization strategy.


274-279: LGTM! Necessary imports for the new functionality.

The added imports and type aliases support the token optimization feature and improve code readability.

@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: 0

♻️ Duplicate comments (1)
src/visit/AstComparator.zig (1)

5-11: LGTM: Performance optimization is well-documented.

The optional tokens field and its documentation clearly explain the performance benefit of avoiding re-tokenization. The field is properly typed as optional with a sensible default.

Note: The typo on line 8 ("presert" → "preserve") was already flagged in a previous review.

🧹 Nitpick comments (2)
src/visit/AstComparator.zig (2)

155-155: LGTM: Tag check is performed for variable identifiers.

The call to tokensEql with skip_tag_check: false ensures tag validation is performed. The function correctly identifies the variable name token at main_token + 1 (the token after var/const).

For consistency with eqlFieldAccess (lines 93-96), consider adding debug assertions to verify the tokens are identifiers:

     // Compare identifier names
     const left_main_token = self.ast.nodes.items(.main_token)[a];
     const right_main_token = self.ast.nodes.items(.main_token)[b];
+    if (util.IS_DEBUG) {
+        std.debug.assert(self.ast.tokens.items(.tag)[left_main_token + 1] == .identifier);
+        std.debug.assert(self.ast.tokens.items(.tag)[right_main_token + 1] == .identifier);
+    }
     if (!self.tokensEql(left_main_token + 1, right_main_token + 1, false)) return false;

228-253: LGTM: Correct implementation of optimized token comparison.

The implementation correctly handles both the optimized path (when tokens is provided) and the fallback path (when re-tokenization is needed). Key observations:

  • Token-based path: Uses pre-computed token locations to avoid re-tokenization, includes lexeme optimization when tags are verified equal
  • Fallback path: Uses tokenSlice with tag checks when tokens unavailable
  • Lexeme optimization: Correctly placed inside the !skip_tag_check block (line 237), ensuring tags are verified equal before skipping text comparison for keyword/operator tokens

The skip_tag_check parameter allows callers to skip redundant tag verification when they've already validated token types (e.g., via debug assertions in eqlFieldAccess).

Consider adding a doc comment to explain the skip_tag_check parameter:

+/// Compares two tokens for equality. When `skip_tag_check` is true, the caller
+/// must ensure both tokens are of compatible types (typically verified via debug assertions).
 fn tokensEql(self: *const AstComparator, a: Ast.TokenIndex, b: Ast.TokenIndex, comptime skip_tag_check: bool) bool {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e2901f3 and 776d9d0.

📒 Files selected for processing (1)
  • src/visit/AstComparator.zig (7 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Collect and upload test coverage
🔇 Additional comments (5)
src/visit/AstComparator.zig (5)

13-19: LGTM: Clean conversion to instance method.

The conversion of eql to an instance method is straightforward and maintains the existing branch hint optimization for the early return path.


93-97: LGTM: Debug assertions ensure safe optimization.

The debug assertions verify that both tokens are identifiers before calling tokensEql with skip_tag_check: true. This ensures correctness in debug builds while enabling the performance optimization in release builds.


211-214: LGTM: Correct delegation to token comparison.

The function correctly delegates to tokensEql with skip_tag_check: false, which is appropriate since main tokens can represent various token types (identifiers, literals, etc.).


216-220: LGTM: Correct handling of optional tokens.

The function properly handles the null cases and delegates to tokensEql with skip_tag_check: false for non-null tokens.


273-280: LGTM: Imports properly support the new functionality.

The import additions are appropriate:

  • util provides IS_DEBUG for conditional debug assertions
  • Semantic provides the updated Ast type and TokenList.Slice
  • Type aliases are correctly updated to reference Semantic.Ast and related types

All imports are utilized in the implementation.

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

Labels

A-linter Area - linter and lint rules

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant