Skip to content

fix(extraction): a damaged C #if group no longer turns into functions named if - #2940

Merged
colbymchenry merged 1 commit into
mainfrom
claude/pr-1736-damaged-if-groups
Oct 27, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/pr-1736-damaged-if-groups

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Summary

A C #if … #else … #endif group whose branches aren't whole statements no longer turns into functions named if, which used to swallow the rest of the function or the file. A new length-preserving pre-parse pass (blankCUnbalancedConditionalBranches, at the end of preParseCSource) removes such a group's directive lines. It keeps every branch when they still read as C side by side, and one branch when they don't. Block macros written in capitals in statement position, like ATOMIC_BLOCK(NVIC_PRIO_MAX) {, are now blanked like the lowercase iterator macros.

Root cause

tree-sitter-c parses each branch of a conditional group as a run of block items, so a group is harmless only when each branch is complete on its own. Three shapes in real C are not:

  • a branch that begins with else, an arm of an if-chain kept behind #ifdef;
  • a branch that ends with an if (…) header whose body follows the #endif, as in ST's HAL latency tables;
  • a branch whose braces don't balance, such as a function head that differs per build.

Each leaves a K&R-shaped if (…) { … } that the grammar reads as a function named if, nested in the enclosing function. The functions after it were filed under it or lost. On main, betaflight has 1,147 such phantom functions (named if or (…)).

What came from the contributor, and what we changed

From @danusha2345, kept: the pass, its three damage shapes plus expression fragments, the branch choice when one branch must go (the #else of a positive test, the first of #ifndef, the live one of #if 0), the ALL_CAPS statement macros, and the tests.

Ours, each with a unit test that fails without it (mutation-checked):

  1. A group on a flag whose branches each balance keeps every branch, and only its directive lines are blanked. Read in a row, such branches are still C: } else if (a) {…} else if (b) {…}, list entries, || x || y. Dropping one lost real code of builds the project uses:

    • betaflight's failsafe.c arms under #if ENABLE_RESCUE_PLAN, which is default-on in common_post.h;
    • adc_stm32h7xx.c's adcTagMap, whose inner MCU chain kept its #else #error branch.

    Unbalanced and literal groups keep one branch, as in the PR. The PR's first unit test now expects both arms of its balanced else-led group.

  2. The group is read on a copy with comments and literals masked. A brace inside a block comment that spans several lines counted as code. Lua's ** {====== banners made loadlib.c's platform group look damaged, so its dlopen, Windows and dyld implementations were dropped for the stub. On betaflight, the same blind spot dropped 19 CMSIS-DSP MVE functions.

  3. A line splice is read on the raw line. A \ at the end of a comment line inside a #define body still continues the #define; masking hid it, which made jemalloc's tsd.h include guard look damaged.

  4. Only a directive's own \ continuations are blanked with it. The PR blanked every directive line right under a collapsed group's #if/#else, so a kept branch lost a leading #include or #define.

  5. #if 1 keeps its first branch. It kept the dead #else.

  6. CHANGELOG rewritten without numbers, placed with the C++ misparse bullet.

Tried and dropped: keeping the feature branch of a flag the file #defines itself, the same-file evidence #2615 uses. Applied group by group, it broke dtoa's coordinated groups in jq: #ifdef Avoid_Underflow kept its branch while a later #ifndef Avoid_Underflow still kept its }, cutting jvp_strtod in half.

Interaction with #2615 (feature-macro variants). Both treat a flag nothing sets as off. #2615 decides at resolution which of several definitions a call reaches, using the file's own #defines and the project's build files. This pass decides at parse time, per file and with no project context, which branch of a damaged, unbalanced group the parser sees. The two can disagree only there; with (1), that is the 61 dropped feature branches on betaflight, about a third of them testing flags its target configs set. A balanced group now keeps both builds.

Validation

Main b6379e6d vs this (dump-graph; nodes keyed by file, kind, name and line, edges by site):

repo phantom functions removed real functions added enum members added notes
betaflight 49679ccd (4k C files) 257 40 701 spiInternalStartDMA, gyroDetect, processSmartPortTelemetry, esc4wayProcess, the CMSIS matrix routines; 244 locals that had leaked to file scope are gone
STM32H7 HAL 7e541d92 68 6 8 LL_SetFlashLatency, HAL_ADC_AnalogWDGConfig, HAL_RTCEx_SetWakeUpTimer
STM32G4 HAL a6001282 30 2 1
redis 7f440372 8 16 1 tre's tre_parse, jemalloc's je_mallocx, lua_cmsgpack; Lua's luaV_execute no longer "calls" a phantom tre_match::if
jq 49fbca12 1 11 strptime, gethex, jvp_dtoa, jv_number; main kept, the WIN32-only wmain dropped
leveldb (C++), gin (Go) byte-identical
  • Calls move to the real function. On betaflight, 746 calls that sat on a phantom or the file node are gone, and 770 now sit on real functions: 369 on restored ones and 401 on functions that existed but had lost them to a phantom.

  • Losses:

    • references from #if conditions inside an initializer, such as betaflight's #if USED_TIMERS & TIM_N(1) lines (12);
    • references to test macros that the ALL_CAPS statement-macro blank removes, such as redis's TEST("…") { (10);
    • a few calls in feature branches of unbalanced groups, such as CMSIS's DSP-extension half of arm_mat_inverse_f32. That function had no node at all on main.
  • The PR as written lost 19 real functions on betaflight, the AT32 CMSIS-DSP MVE variants, through the multi-line-comment blind spot; Lua's loadlib platform implementations on redis; betaflight's rescue-plan arms in failsafeUpdateState (8 calls); and adcTagMap. With (1)–(4), none are lost:

    betaflight main PR as written this
    failsafeUpdateState calls on lines 495–540 14 6 (the 8 calls in the #if ENABLE_RESCUE_PLAN arms lost) 14
    adcTagMap constant (adc_stm32h7xx.c) present, referenced by adcInit and adcFindTagMapEntry gone: its inner MCU chain kept #else #error MCU not defined, dropping every entry present, same references
  • Groups that must still keep one branch do, each pinned by a unit test:

    • #if 0 … #else … #endif keeps the live branch;
    • #if 1 keeps its first arm;
    • unbalanced function heads keep one: #ifdef WIN32 int umain(…) { #else int main(…) { #endif keeps main;
    • an unbalanced group keeps its kept branch's leading #include.

    In real code, jq keeps main and drops the WIN32 heads, and CMSIS's arm_mat_inverse_f32 keeps its generic branch.

  • Kernel parity (kernel-parity.mjs --lang c) on betaflight, the STM32H7 HAL, redis and jq: 0 files with diffs, on main and here. The kernel gets the pre-parsed bytes. Fewer files defer to wasm because more parse clean: betaflight 1,216 → 1,095, redis 169 → 150, jq 21 → 15, STM32H7 156 → 146. The STM32H7 HAL trips the >50% deferral backstop on both arms.

  • Cost. C preParse CPU over all of betaflight is 5.7 → 8.6 s (the PR as written: 8.4 s), and redis 1.2 → 1.6 s. Whole-index CPU, median of 3 interleaved: redis 41.2 vs 40.0 s, STM32H7 HAL 21.4 vs 21.9 s, which is noise.

  • Red→green. The PR's end-to-end test (a phantom (NVIC_PRIO_MAX) and if on main) fails on main and passes here. Each of our changes has a unit test that fails without it.

  • Full suite: 764 files passed, 6 skipped; 9,092 tests passed, 85 skipped, 0 failed, on 083b9cb8 (the same patch one rebase earlier: patch-id 4825eb99 on both; the rebase onto c3069670 brought only fix(cfnptr): a C function-pointer table links only to C and C++ functions #2935, the C function-pointer filter, and a CHANGELOG line elsewhere).

Lands #1736 by @danusha2345.

🤖 Generated with Claude Code

… named `if`

tree-sitter-c parses every branch of a `#if … #elif … #else … #endif`
group as a run of block items, so a group is harmless only when each
branch is complete on its own. Three shapes in real C are not: a branch
that begins with `else` (an arm of an if-chain kept behind `#ifdef`), a
branch that ends with a bare `if (…)` header whose body follows the
`#endif` (ST's HAL latency tables), and a branch whose braces don't
balance (a function head that differs per build). Each leaves a
K&R-shaped `if (…) { … }` that the grammar reads as a function named
`if`, filed inside the enclosing function, and the functions after it
were nested under it or lost. Separately, a block macro in capitals in
statement position (`ATOMIC_BLOCK(NVIC_PRIO_MAX) {`) was skipped by the
statement-macro blank, which matched lowercase iterator macros only.

blankCUnbalancedConditionalBranches runs at the end of preParseCSource,
after the directive restore, since it edits directive lines. A group
where a branch shows one of those shapes, or is a fragment of an
expression or a list (`|| (h4 == NULL)` inside an `if (…)`), loses its
directive lines, offsets kept. When a branch can't stand next to the
others, one branch is kept verbatim: the `#else` of a positive test
(`#ifdef X`, `#if defined(X)`), the first of `#ifndef`, the live one of
`#if 0`. The statement-macro blank also takes ALL_CAPS names (PascalCase
still excluded, for C++ constructors). Both engines see it: the kernel
gets the pre-parsed bytes from the TS route point.

Ours on top:
- A group on a flag whose branches each balance (braces and parens)
  keeps every branch, only its directive lines blanked: read in a row
  they are still C (`} else if (a) {…} else if (b) {…}`, list entries,
  `|| x || y`). Dropping one lost real code of builds the project uses:
  betaflight's failsafe.c arms under `#if ENABLE_RESCUE_PLAN` (default-on
  in common_post.h), and adc_stm32h7xx.c's adcTagMap, whose inner MCU
  chain kept its `#else #error` branch. Unbalanced and literal groups
  keep one branch, as above. The PR's first unit test now expects both
  arms of its balanced else-led group.
- The group is read on a copy with comments and literals masked. A brace
  inside a block comment over several lines counted as code, so Lua's
  `** {======` banners made loadlib.c's platform group look damaged, and
  its dlopen, Windows and dyld implementations were dropped for the stub;
  on betaflight it dropped 19 CMSIS-DSP MVE functions.
- A line splice is read on the raw line: a `\` at the end of a comment
  line inside a #define body still continues the #define (masking hid
  it, which made jemalloc's tsd.h include guard look damaged).
- Only a directive's own `\` continuations are blanked with it. The PR
  blanked every directive line right under a collapsed group's `#if` or
  `#else`, so a kept branch lost a leading `#include` or `#define`.
- `#if 1` keeps its first branch; it kept the dead `#else`.
- Unit tests for each, mutation-checked; CHANGELOG bullet rewritten
  without numbers.
- Tried and dropped: keeping the feature branch of a flag the file
  #defines itself (the same-file evidence PR #2615 uses). Applied per
  group, it broke dtoa's coordinated groups in jq (`#ifdef
  Avoid_Underflow` kept its branch while a later `#ifndef
  Avoid_Underflow` still kept its `}`), cutting jvp_strtod in half.

Validation, main b6379e6 vs this:
- Phantom functions (named if or (…)) removed / real functions added:
  betaflight 49679ccd 257 / 40 (spiInternalStartDMA, gyroDetect,
  processSmartPortTelemetry, esc4wayProcess, the CMSIS matrix
  routines), stm32h7xx_hal_driver 7e541d92 68 / 6 (LL_SetFlashLatency,
  HAL_ADC_AnalogWDGConfig), stm32g4xx_hal_driver a6001282 30 / 2, redis 7f440372 8 / 16 (tre_parse, je_mallocx,
  lua_cmsgpack), jq 49fbca12 1 / 11 (strptime, gethex, jvp_dtoa). No
  real function lost except jq's WIN32-only wmain. Enum members added:
  betaflight 701, stm32h7 8.
- Calls move to the real function: on betaflight, 746 calls that sat on
  a phantom or on the file node are gone, and 770 now sit on real
  functions (369 on restored ones, 401 on functions that existed but
  had lost them to a phantom). Lost: references from `#if`
  conditions inside an initializer (betaflight TIM_N x12), test-macro
  references (redis TEST x10), and a few calls in feature branches of
  unbalanced groups.
- leveldb (C++) and gin (Go) byte-identical.
- kernel-parity (betaflight, stm32h7, redis, jq): 0 files with diffs on
  main and here; fewer files defer to wasm (betaflight 1216 -> 1095,
  redis 169 -> 150, jq 21 -> 15, stm32h7 156 -> 146).
- Cost: C preParse CPU over betaflight 5.7 -> 8.6 s (the PR as written
  8.4 s), redis 1.2 -> 1.6 s.
  Whole-index CPU, median of 3 interleaved: redis 41.2 vs 40.0 s,
  stm32h7xx_hal_driver 21.4 vs 21.9 s (noise).

Original PR #1736 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry merged commit cbd9817 into main Oct 27, 2026
@colbymchenry
colbymchenry deleted the claude/pr-1736-damaged-if-groups branch October 27, 2026 22:25
danusha2345 pushed a commit to danusha2345/codegraph that referenced this pull request Oct 28, 2026
…mchenry#2972; 10 landed PRs yield to main; EXTRACTION_VERSION 56)

Main landed ten more of our PRs as our own commits with the maintainer's additions
(colbymchenry#2923 ← colbymchenry#1936, colbymchenry#2924 ← colbymchenry#1951, colbymchenry#2925 ← colbymchenry#1939, colbymchenry#2930 ← colbymchenry#2921, colbymchenry#2940 ← colbymchenry#1736,
colbymchenry#2942 ← colbymchenry#1947, colbymchenry#2950 ← colbymchenry#1952, colbymchenry#2961 ← colbymchenry#1954, colbymchenry#2965 ← colbymchenry#1942, colbymchenry#2972 ← colbymchenry#1933) and the
module-path half of colbymchenry#2308 (colbymchenry#2955). Where the integration carried our version, main's
wins: the enclosing-scope member pick and its tests, the Go and Java receiver typers,
the delegating-call self-loop rule, the duplicated-module copy confidence, the C #if
collapse, the module-path resolution of Rust paths, the trail/path hardening in
utils.ts. Kept beside main: colbymchenry#1875's Verilog hooks and status fields, colbymchenry#2839's
implementations command, colbymchenry#1923, colbymchenry#2922, colbymchenry#2258, colbymchenry#1761, colbymchenry#1583, the bare-call half of
colbymchenry#2308 (rust-use-bindings.ts). CHANGELOG is main's plus the open PRs' bullets.

EXTRACTION_VERSION 56: the C #if group handling (colbymchenry#2940) and the kernel change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
danusha2345 pushed a commit to danusha2345/codegraph that referenced this pull request Oct 28, 2026
…tests, colbymchenry#1947's cls exemption, locale-tiebreak test)

Our copies of the C #if tests colbymchenry#2940 superseded are dropped (one of them was main's
own test twice); the library-method rule no longer exempts a receiver named `cls`,
as colbymchenry#2942 landed it; ts-this-field-call.test.ts is main's (colbymchenry#2927 landed the same
code-unit tiebreak the integration carried from the earlier PR).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.

1 participant