fix(prism): stop treating C++ digit separators as char-literal openers - #1761
Conversation
C++14+ allows a single quote inside numeric literals (512'000, 1'000'000'000, 0xDE'AD'BE'EF). The generic literal shield's single-quote branch is unbounded, so a separator quote was read as the start of a char literal and paired with the next stray quote anywhere later in the file -- swallowing every real // and /* */ comment in between as one giant "literal". Comments leaked into the code stream, coding_loc was inflated, and the detector saw comment text. Give C++ its own bounded shield (same 10-char bound squid-protocol#1302 already applies to the recursive_block family) and consume digit-separator quotes as their own alternative so they can never open a span. JS/PHP single-quoted strings keep the unbounded shared pattern -- they legitimately span many chars. Regenerated both golden master fixtures: cpp/godot/editor_node.cpp now strips its "512'000; // 500 KB" comment, so Coding LOC drops and Documentation LOC rises there. Verified with the golden crucible in full-precision and zero-dependency modes, plus 6 new regression tests.
|
Closes #1718. |
squid-protocol
left a comment
There was a problem hiding this comment.
Thanks for chasing this down, Ruiming — the digit-separator root cause and the editor_node.cpp before/after are a clean diagnosis, and I like that the fix stays scoped to C++ instead of touching the shared SHIELD_PATTERN that JS/PHP rely on.
CI is currently red on two checks that look like real substance rather than flakiness, though, so I don't think this is mergeable yet:
tree-sitter-accuracy-audit fails with a measured C++ regression:
=== cpp ===
MISSING CLASSES IN godot/variant.h: {'HashMapComparatorDefault<Variant>', 'is_zero_constructible<Variant>'}
tree_sitter_accuracy_audit: 2 regression(s) against the baseline:
found_functions: 1376 -> 1350 (higher is better, this got worse)
args_exact_match: 1202 -> 1186 (higher is better, this got worse)
godot/variant.h is a different file from the one this PR targets, and it's now losing two template-specialization classes it used to detect correctly, plus 26 fewer functions found across the C++ corpus overall. That's at odds with the PR description's claim that "No other file in the corpus changed."
crucible-audit (zero-dependency) also fails, while crucible-audit (full-precision) passes — mismatches show up in unrelated files across many languages (python/airflow, dart/flutter, php/laravel, zig, swift...), which reads as global rank/blast-radius ripple rather than direct C++ damage, but it's still a real CI gate failure. Combined with the PR saying "Golden crucible passes in both full-precision and zero-dependency modes" locally, this looks like it could be the stale-venv footgun CLAUDE.md's Differential Scan section calls out (PR #579/#723): a crucible_check.py venv whose editable install silently resolves to a different checkout gives a false "zero diff" pass locally while CI correctly fails. Worth confirming with python -c "import gitgalaxy; print(gitgalaxy.__file__)" inside whatever venv ran the local check.
For the func/args regression itself: I haven't nailed the exact trigger, but two things in CPP_LITERAL_MASK_PATTERN look like plausible candidates worth checking against variant.h directly:
[0-9a-fA-F]'[0-9a-fA-F]isn't anchored to an actual numeric-literal context — it's just three characters in that shape, so it could coincidentally fire near an ordinary hex-valued char literal ('a','f', etc.) sitting next to another hex-ish character, not just real512'000-style separators.- The
{0,10}bound on the char-literal branch — C++'s universal-character-name escapes (\uXXXX,\UXXXXXXXX, and C++23's\N{...}) can run longer than 10 characters between the quotes, so a real (if rare) literal could get clipped and desync the shield the same way the original unbounded version did, just in the other direction.
Could you re-run tree_sitter_accuracy_audit.py --all and crucible_check.py (fresh venv, not a reused one) and see what's actually happening in variant.h? Happy to look again once those are green.
Two same-day PRs (#1760, #1761) both ran crucible_check.py and the full test suite, checked off the PR template honestly, and still shipped regressions the Differential Scan's 80-repo corpus diff didn't cover -- caught only by manually running tree_sitter_accuracy_audit.py, which wasn't mentioned anywhere in CONTRIBUTING.md or the PR template. Co-authored-by: Joe Esquibel <squid-protocol@users.noreply.github.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…ral pass prism.py gained the C++ digit-separator shield in the previous commit, but the detector's own single-quote literal shielding still treated a separator quote as a char-literal opener and paired it with the next stray quote anywhere later in the file -- blanking real function bodies in between. Without this the tree-sitter accuracy audit regressed on the C++ corpus (found_functions 1376 -> 1350, args_exact_match 1202 -> 1186) and godot/variant.h lost its template-specialization classes. Apply the same bounded pattern (digit separator consumed as its own alternative, char-literal branch bounded to 10 chars) to the detector shield, matching prism.py's CPP_LITERAL_MASK_PATTERN. Regression test covers a separator followed by functions, a comment apostrophe, and a real char literal.
The prism + detector digit-separator shields stop separator quotes from pairing with a later apostrophe, so cpp output shifts: godot/editor_node.cpp's '512'000; // 500 KB' comment is no longer swallowed into a fake literal (Coding LOC drops, Documentation LOC rises) and cpp health/impact metrics move accordingly. Regenerated both fixtures via update_golden_master.py; the golden crucible test passes against them.
|
Chased this down - the prism shield was only half the fix. The detector has its own single-quote literal pass (_build_brace_safe_stream) that still treated a separator quote as a char-literal opener and paired it with the next stray apostrophe anywhere later in the file. Reproduced the exact CI regression locally against the pinned v1.0 corpus: with variant.h losing HashMapComparatorDefault and is_zero_constructible. Applied the same bounded pattern to the detector's shield (digit separator consumed as its own alternative, char-literal branch bounded to 10 chars, same shape as the prism mask). Re-ran the tree-sitter audit against the corpus: no regressions. Added a regression test (separator followed by functions, comment apostrophe, real char literal), reblessed both golden masters via update_golden_master.py, and the golden crucible test passes against them. Full detector suite is green. Tree-sitter audit and crucible should be green on CI now. |
The digit-separator shielding in prism.py and detector.py bounded the C++ char-literal branch to 10 chars (mirroring squid-protocol#1302's recursive_block bound). That's wide enough for classic escapes ('\u0041', '\U0001F600') but not for C++23 named character escapes (\N{LATIN CAPITAL LETTER A}), which can run far longer and contain braces. A real (if rare) literal would be clipped, leaving the shield desynced the same way the original unbounded version did, just in the other direction. Widen the C++-only bound to 64 chars -- comfortably covers every named escape, still far too short for any cross-file cascade. The recursive_block shield keeps its own 10-char bound, and JS/PHP single-quoted strings keep the unbounded shared pattern.
|
Thanks for the thorough review — and for the specific pointers on where to look. I went through each point; here's what I found. The two red checks were against the prism-only state, before the detector fix landed. Your review is pinned to commit On variant.h and the "26 fewer functions" — I believe that was the intermediate prism-only regression, now resolved, and the two variant.h classes are pre-existing misses. I checked the committed baseline first: it records Your two pattern concerns, addressed directly:
On the stale-venv footgun: you're right that the earlier local "zero diff" was misleading. The zero-dependency fixture in the first push had been regenerated with a subprocess The bound-hardening is in |
|
CI's fully green now and the fix itself checks out — thanks for the thorough follow-up on
Small optional one: |
…or-1718 # Conflicts: # tests/golden_master_audit.json # tests/golden_master_zero_dep_audit.json
squid-protocol
left a comment
There was a problem hiding this comment.
LGTM! I pushed the fixes and resolved the conflicts. Approving.
What
C++14+ lets you write a single quote inside numeric literals as a digit separator:
512'000,1'000'000'000,0xDE'AD'BE'EF. GitGalaxy's literal shields didn't know about that. Their single-quote branches are unbounded, so a separator quote was read as the start of a char literal and paired with the next stray quote anywhere later in the file — swallowing real//and/* */comments (prism) and blanking real function bodies (detector brace scan) in between.The fix (two layers)
prism.py —
CPP_LITERAL_MASK_PATTERNfor the genericstandard_blockstripper: consume digit separators as their own alternative ([0-9a-fA-F]'[0-9a-fA-F]) so they can never open a span, and bound the char-literal branch to 10 chars (same bound prism.py: _strip_nested_comments' unbounded single-quote shield swallows huge code spans (scala/rust/swift/haskell/scheme) #1302 already applies to therecursive_blockshield). Kept per-language because JS/PHP single-quoted strings legitimately span many characters and keep the unbounded shared pattern.detector.py —
_build_brace_safe_stream's own single-quote shield had the identical bug: a separator quote blanked every real function body between it and the next apostrophe, which showed up as a tree-sitter accuracy regression (cppfound_functions1376 → 1350) once prism stopped swallowing the comment that used to terminate the span. Same bounded pattern applied here, mirroring the existing rust/zig bound.Verification
test_prism_issue_1718.py,test_detector_issue_1718.py): far-away quote pairing, hex/multi-group separators, a comment apostrophe near a separator, real char literals still shielding,u8prefixed literals, JS/PHP strings staying whole, and detector brace-scan survival.Why the golden masters changed
Regenerated both fixtures via
tests/tools/update_golden_master.py. The diff is confined tocpp/godot/editor_node.cppand its derived aggregates: Coding LOC 7678 → 7665, Documentation LOC 441 → 454 (the// 500 KBcomment after512'000is now correctly stripped instead of being swallowed into a fake literal), plus the corresponding shifts in structural signatures and exposures. No other file in the corpus changed.Closes #1718.