Skip to content

ruff_cpp_spo: tag for-header slots (for_init/for_cond/for_inc) from the real ; tokens - #116

Merged
AdaWorldAPI merged 3 commits into
mainfrom
claude/ladybug-transcoding-plan-q5zbrs
Sep 8, 2026
Merged

AdaWorldAPI merged 3 commits into
mainfrom
claude/ladybug-transcoding-plan-q5zbrs

Conversation

@AdaWorldAPI

@AdaWorldAPI AdaWorldAPI commented Sep 8, 2026 •

Copy link
Copy Markdown
Owner

Summary

The ordered ore stream (ruff_cpp_spo::events) could not tell which of a ForStmt's header children was the initializer, the condition, or the increment. libclang exposes the header children positionally and omits absent slots, so for (;; ++i) and for (i = 0;;) both present a single header child, and a downstream consumer that wants to rotate the increment after the body (the control-normalized view used by the behavioral-motif lab) has nothing to key on.

This change tokenizes the for header, finds the two depth-1 ; separators, and tags every event whose offset falls inside a slot with for_init / for_cond / for_inc — only when the event carries no other control tag, so if / switch / loop tags inside the header are never overwritten. A header whose depth-1 ; count is not exactly two is left untagged rather than guessed.

Nothing else in the stream changes: the raw event sequence, scope kinds, anchors, and every existing control tag are untouched.

Review round

Three review findings landed on the first commit, and addressing the P1 exposed a fourth defect that the same review's P2 had warned about from the other side. All four are fixed; the threads carry the detail.

  1. P1 — macro-definition separator offsets. A loop emitted whole by a function-like macro tokenized from the macro definition, so all three slots fell through to for_inc. Measured on the corpus: 126 bad for_inc tags and zero bad for_init / for_cond, exactly the skew the finding predicted.
  2. P2 — braced ; in the header. A lambda body in init counted its own ; as a separator. Fixed with a brace-depth counter beside the paren counter.
  3. P2 — tokenize the header only. The whole ForStmt extent was tokenized, re-lexing descendant bodies once per ancestor. The range is now for keyword to body start.
  4. Introduced by the P1 fix, then fixed. Bounding the header by the body cursor's raw location is unsafe when the body is itself a macro invocation: that location is a macro-expansion location, libclang tokenizes such a range to a single token, and an ordinary header loses all three slots. This untagged 32 real loops (for (...) LOG(ERROR) << x; in re2, for (...) TDEFL_PUT_BITS(...); in miniz). Both endpoints are now materialised as file locations in the for keyword's own file via File::get_offset_location.

Corpus gate

Re-harvest of ladybug src/ (1,081 TUs), pre-fix against head:

pre-fix head
rows 646,411 646,411
for_init 3,053 3,053
for_cond 6,748 6,748
for_inc 2,880 2,754

100 tags removed across 31 methods, 0 added, 0 moved. 30 of the 31 are macro loops in third_party/roaring_bitmap/roaring.c; the 31st is mbedTLS' SHIFT_AND_PLACE. Every decline is a genuinely macro-emitted header; no non-macro loop declines.

Test Plan

  • Eleven unit tests in events.rs covering full headers, bare for (;;), missing cond / missing inc, comma operators, nested parens in cond, a ; inside a char literal, while and range-for never receiving slot tags, and the three macro shapes: header from a macro (untagged), body from a macro (tagged), header and body from one macro (untagged).
  • Each new test is disable-verified — the guard it names is removed and the test goes red. The one range-validity precondition that no fixture can reach is labelled as a precondition in the source rather than presented as a guard.
  • LIBCLANG_PATH=/usr/lib/llvm-18/lib cargo test -p ruff_cpp_spo --features libclang: 70 lib tests + 6 example tests pass.
  • cargo clippy -p ruff_cpp_spo --features libclang --all-targets -- -D warnings clean; cargo fmt -p ruff_cpp_spo -- --check clean.
  • prek could not run in this environment (the installed uv rejects the repo's exclude-newer = "P7D" setting and refuses the lockfile under --locked), so the fmt / clippy / test gates above were run directly. CI's prek run caught one thing they cannot: the typos hook rejected mis-tagged in a doc comment, now reworded.

🤖 Generated with Claude Code

https://claude.ai/code/session_011JRoXmYPrE8KubXVrDB71L

Summary by CodeRabbit

  • New Features

    • Improved analysis of C++ for loops by distinguishing initialization, condition, and increment sections.
    • Correctly handles omitted sections, nested expressions, commas, literals, and lambda initializers.
  • Bug Fixes

    • Prevents incorrect classifications for macro-generated or unparseable loop headers.
    • Preserves existing behavior for while, do, and range-based for loops.

…kens

The ordered ore stream could not tell which of a ForStmt's header
children was the initializer, the condition, or the increment: libclang
exposes the ForStmt's children positionally and omits absent slots, so
`for (;; ++i)` and `for (i = 0;;)` both present a single header child.
Tokenize the ForStmt's own extent, find the two depth-1 `;` separators,
and tag every event whose offset falls inside a slot with `for_init` /
`for_cond` / `for_inc` when it carries no other control tag. Headers
whose depth-1 `;` count is not exactly two (macro-generated headers) are
left untagged rather than guessed.

Re-harvest on ladybug `src/` is row-identical (646,411 events); slot tag
counts: for_init 3,053 / for_cond 6,748 / for_inc 2,880; 9 headers
declined. Eight unit tests; six of them fail with the slot arm disabled.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011JRoXmYPrE8KubXVrDB71L
@coderabbitai

coderabbitai Bot commented Sep 8, 2026 •

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Essentials

Run ID: 1d945367-1d8a-4328-a46b-71cf7c1c27b0

📥 Commits

Reviewing files that changed from the base of the PR and between a59a3fd and 34ef4dd.

📒 Files selected for processing (1)
  • crates/ruff_cpp_spo/src/events.rs

Included review availability: 1 review is currently available. Your included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour.


📝 Walkthrough

Walkthrough

The change classifies C++ for-loop header events as initialization, condition, or increment events. It uses token boundaries, ignores nested separators and literals, preserves existing annotations, and leaves unsupported or invalid headers unchanged.

Changes

For-loop header classification

Layer / File(s) Summary
Header token detection
crates/ruff_cpp_spo/src/events.rs
The classifier finds exactly two top-level semicolons in tokenized for headers. It rejects invalid, macro-spelled, and un-tokenizable headers.
Event annotation wiring
crates/ruff_cpp_spo/src/events.rs
The loop traversal applies for_init, for_cond, and for_inc annotations to newly emitted events. Other loop types retain existing behavior.
Classification fixtures and tests
crates/ruff_cpp_spo/src/events.rs
Tests cover omitted clauses, commas, nested calls, literals, macros, lambdas, range-based loops, and while loops.

Priority: ⬇️ Low — Defer the for-loop tagging change because it is a contained parser enhancement with no stated external urgency or broader product impact.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: ⚪ Minimal · up to 34ef4

This change adds positional init, condition, and increment tags for valid C++ for-loop headers while preserving existing annotations and leaving unsupported headers untagged. No current merge-blocking risk remains.

Suggested reviewers: claude

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 18 functions across 1 files.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: tagging for-header slots from real semicolon tokens.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

A rabbit sorts the loop’s three tracks
Init hops forward, condition checks backs
Increment nudges the clock ahead
Nested commas stay in their shed
Clear tokens guide each springy tread

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

@cursor

cursor Bot commented Sep 8, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_a96c6b14-052f-4713-ad76-537a2be23165)

@AdaWorldAPI
AdaWorldAPI marked this pull request as ready for review September 8, 2026 13:41
@cursor

cursor Bot commented Sep 8, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_7d37b8f3-a680-4c2d-abe5-bcd77e7c1495)

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 572a7cffc6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/ruff_cpp_spo/src/events.rs Resolved
Comment thread crates/ruff_cpp_spo/src/events.rs Resolved
Comment thread crates/ruff_cpp_spo/src/events.rs Resolved
…skip braced `;`, tokenize the header only

Three findings from the Codex review of #116, each reproduced as a failing
test before the fix:

1. A loop emitted whole by a function-like macro (`#define LOOP(I,C,N)
   for (I; C; N)`) tokenizes as the macro DEFINITION's tokens, so the two `;`
   matched while the head-child anchors pointed at the expansion site and
   every event fell through to `for_inc`. Every separator must now lie
   strictly between the `for` keyword's expansion-site offset and the
   body's; a header spelled elsewhere yields `None` (untagged), never a
   mis-tag.
2. A `;` inside a braced expression in the header (a lambda body in the
   init clause) counted as a separator, so a valid header returned `None`.
   Brace depth is tracked; only `;` at paren depth 1 and brace depth 0 count.
3. The whole `ForStmt` extent was tokenized (including the body, re-lexed
   once per enclosing loop). Only the header range — `for` keyword to the
   body's first token — is tokenized now.

Gate on the ladybug corpus (`ore-full-v3` vs `ore-full-v2`): 646,411 rows,
byte-identical in every column except `control`; no tag added or moved;
158 tags removed in 34 methods, all in `third_party/roaring_bitmap`
(132 `for_inc`, 15 `for_cond`, 11 `for_init`) — the finding-1 corruption,
now declined. Slot counts: for_init 3,053→3,042, for_cond 6,748→6,733,
for_inc 2,880→2,748.

Two new tests (`for_macro_emitted_loop_gets_no_slot_tags`,
`for_lambda_in_init_still_tags_all_three_slots`); each fails with its guard
removed and passes with it. 68 lib + 6 example tests, clippy -D warnings,
fmt clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011JRoXmYPrE8KubXVrDB71L
… untag it

The macro-rejection fix in 15624df over-rejected. It bounded the header by
the body cursor's raw location, and when the body is itself a macro
invocation that location is a macro-expansion location: libclang tokenizes
such a range to a single token (measured, ntok=1), so the separator count is
never two and every slot tag on an ordinary header is silently dropped.

Measured on the ladybug corpus (1,081 TUs, 646,411 rows): 158 tags in 34
methods went away, but only 31 of those methods are genuinely macro-emitted.
The other three are ordinary headers whose body is a macro call —
`for (...) LOG(ERROR) << x;` in two re2 methods and
`for (...) TDEFL_PUT_BITS(...);` in miniz.

Both range endpoints are now materialised as file locations in the `for`
keyword's own file via `File::get_offset_location`, which keeps the
header-only range (no ancestor re-lexes a descendant body) and makes it
well-formed whatever the body is. That also subsumes the per-separator
offset check, which is deleted rather than left as an unfalsifiable guard:
for a wholly macro-emitted loop the materialised range spans the CALL SITE,
whose tokens carry no `;` at header depth, so the count is not two and the
header is rejected without ever reading a definition offset.

Gate, v2 (pre-15624df) against this commit, same corpus, same extractor
otherwise: 646,411 rows both sides; 100 tags removed across 31 methods,
0 added, 0 moved; `for_init` and `for_cond` counts unchanged at 3,053 and
6,748; `for_inc` 2,880 -> 2,754. The 126 removed `for_inc` tags are exactly
the macro-emitted separators that used to fall through to that slot, which
is the failure the review named. 30 of the 31 methods are macro loops in
`third_party/roaring_bitmap/roaring.c`, the 31st is mbedTLS'
`SHIFT_AND_PLACE`; no non-macro loop declines.

Tests: `for_with_a_macro_body_still_tags_all_three_slots` (disable-verified
against the raw-location endpoints) and
`for_emitted_whole_with_its_body_gets_no_slot_tags`, which pins the shape
`LOOP` does not cover — header and body from one object-like macro. The
range-validity precondition is labelled as such, since no fixture reaches it.

Also reword a doc comment so the `typos` hook stops failing on `mis-tagged`.

70 lib + 6 example tests pass; clippy -D warnings and fmt clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011JRoXmYPrE8KubXVrDB71L
@cursor

cursor Bot commented Sep 8, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_4737c418-d1cd-4b5e-bc0e-b538ed02c767)

@AdaWorldAPI
AdaWorldAPI merged commit e2055ae into main Sep 8, 2026
34 of 35 checks passed
AdaWorldAPI pushed a commit that referenced this pull request Oct 14, 2026
…skip braced `;`, tokenize the header only

Three findings from the Codex review of #116, each reproduced as a failing
test before the fix:

1. A loop emitted whole by a function-like macro (`#define LOOP(I,C,N)
   for (I; C; N)`) tokenizes as the macro DEFINITION's tokens, so the two `;`
   matched while the head-child anchors pointed at the expansion site and
   every event fell through to `for_inc`. Every separator must now lie
   strictly between the `for` keyword's expansion-site offset and the
   body's; a header spelled elsewhere yields `None` (untagged), never a
   mis-tag.
2. A `;` inside a braced expression in the header (a lambda body in the
   init clause) counted as a separator, so a valid header returned `None`.
   Brace depth is tracked; only `;` at paren depth 1 and brace depth 0 count.
3. The whole `ForStmt` extent was tokenized (including the body, re-lexed
   once per enclosing loop). Only the header range — `for` keyword to the
   body's first token — is tokenized now.

Gate on the ladybug corpus (`ore-full-v3` vs `ore-full-v2`): 646,411 rows,
byte-identical in every column except `control`; no tag added or moved;
158 tags removed in 34 methods, all in `third_party/roaring_bitmap`
(132 `for_inc`, 15 `for_cond`, 11 `for_init`) — the finding-1 corruption,
now declined. Slot counts: for_init 3,053→3,042, for_cond 6,748→6,733,
for_inc 2,880→2,748.

Two new tests (`for_macro_emitted_loop_gets_no_slot_tags`,
`for_lambda_in_init_still_tags_all_three_slots`); each fails with its guard
removed and passes with it. 68 lib + 6 example tests, clippy -D warnings,
fmt clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011JRoXmYPrE8KubXVrDB71L
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants