Skip to content

Fix C/C++ frontend defects: sizeof operands, C++ extensions, catch handlers, AST cache key - #190

Merged
prabhu merged 12 commits into
mainfrom
feat/edg-cpp
Oct 1, 2026
Merged

prabhu merged 12 commits into
mainfrom
feat/edg-cpp

Conversation

@prabhu

@prabhu prabhu commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

C/C++ frontend fixes found while comparing c2cpg with a fully resolving C++ front end. One commit per fix, each with regression tests.

Frontend (c2cpg)

  • sizeof and friends. sizeof(char *) was emitted as sizeof(char): the type operand lost its abstract declarator. alignof, typeid, typeof and sizeof... were all emitted as <operator>.sizeOf, so passes that read sizeof as a byte size also read alignments, type ids and pack counts. Each now has its own operator (<operator>.alignOf, <operator>.typeId, <operator>.typeOf, <operator>.parameterPackSize), the GNU unary forms (__alignof__ x, noexcept(...), &&label) no longer fall through to <operator>.unknown, and the new operators carry no-flow semantics like sizeof.
  • File extensions. Only .c, .cc and .cpp were translation units, so .cxx and .c++ sources were never parsed, and .hh headers were parsed as C. C++ sources now include .cxx, .c++ and .C; C++ headers include .hxx, .h++, .H, .ipp, .inl and .tcc; longer extensions also match in upper case.
  • C++ catch handlers. Every catch body was a direct child of the try with the same order, the exception parameter was dropped (a use of e resolved to nothing), and function-try-blocks lost their handlers. Handlers now sit in one block coded catch, each at its own position with its exception bound as a local, and a function-try-block wraps the function body in a try. The CFG reads a catch group's children as alternatives, which also fixes javasrc2cpg, where a second catch clause ran after the first instead of being entered from the try body. The uninitialised-read rule treats the new binding as initialised by the throw.
  • Macro handler. MacroHandler.scala held raw NUL bytes, so git treated it as binary. The expansion under a macro's INLINED call reused argument index 1, colliding with the first argument copy; it now takes the index after the last argument.
  • --no-include-auto-discovery was accepted but had no action.

AST cache

  • The cache key ignored the parser options. atom's header mode parses a project without function bodies and leaves its cache in <input>/.chen; a later full run over the same directory replayed those ASTs, so its graph had no method bodies and its usage slices were <empty> placeholders (cdxgen's C evidence step followed by evinse hits exactly this). Function bodies, inactive code, comments, image locations, trivial expressions, the C++ standard, include discovery and the project index now take part in the key.
  • Shared tables leaked across runs. Types, member layouts and constants registered by an earlier run in the same process were lent to the next graph as extra TYPE nodes; C2Cpg.createCpg now starts from empty tables. This also fixes CWarmRestoreTests failing whenever the AST cache tests ran first in the same JVM.

Docs

The c2cpg lesson listed MACRO_DEF/MACRO_EXPANSION nodes that do not exist, a --enable-ast-cache flag that is not accepted and a frontend-args syntax atom does not parse. It now documents the parsed extensions, the pass list, the macro census flags and how macro invocations are represented. The CDT notes say the vendored core bundle ships in both CDT 12.4 and 12.5.

Testing

  • c2cpg/test 1211, x2cpg/test 118, dataflowengineoss/test 53, javasrc2cpg/test 573: all pass.
  • Memory-safety corpus unchanged: medium recall 62.77% / precision 98.33%, low 75.53% / 95.95%, no findings on negative controls.
  • sbt scalafmtAll applied.

prabhu added 12 commits October 1, 2026 17:50
… and sizeof... apart

sizeof(char *) was emitted as sizeof(char), and alignof, typeid, typeof and
sizeof... all became <operator>.sizeOf, so passes that read sizeof as a byte
size also read alignments, type ids and pack counts. Each operator now has its
own name, the type operand keeps its abstract declarator, and the GNU unary
forms (__alignof__ expr, noexcept, &&label) no longer fall through to
<operator>.unknown.
Only .c, .cc and .cpp were translation units, so .cxx and .c++ sources were
never parsed, and .hh headers were parsed as C. C++ sources now include .cxx,
.c++ and .C, C++ headers include .hxx, .h++, .H, .ipp, .inl and .tcc, and
longer extensions also match in upper case. A .tcc file is now also a C-family
file for internal linkage.
The argument splitter compared characters against raw NUL literals, which made
git treat the file as binary. The expansion under a macro's INLINED call also
reused argument index 1, colliding with the first argument copy; it now takes
the index after the last argument, matching its order.
The hidden flag was accepted but had no action, so it could not undo an
earlier --with-include-auto-discovery.
Every C++ catch body was a direct child of the try with order 2, so siblings
shared one position, the exception parameter was dropped (a use of e in the
handler resolved to nothing) and function-try-blocks lost their handlers.

The handlers now sit in one block coded catch, numbered in source order, each
holding its exception as a local followed by its statements, and a function
try block wraps the function body in a try. The CFG reads the children of a
catch group as alternatives, which also fixes javasrc2cpg, where a second
catch clause ran after the first instead of being entered from the try body.
…acros

The lesson listed MACRO_DEF and MACRO_EXPANSION nodes that do not exist, a
--enable-ast-cache flag that is not accepted, a frontend-args syntax atom does
not parse and an include discovery step that finds system rather than project
paths. It now documents the parsed extensions, the full pass list, the macro
census flags and how macro invocations are represented. The CDT notes say the
vendored core bundle ships in both CDT 12.4 and 12.5.
The uninitialised-read rule saw the new local for catch (int e) as declared
and never assigned, and reported every read of e. The throw initialises it:
the rule now skips a handler block's own local. The handler's statements stay
in their body block, so the binding is the only local of the handler block;
the CFG reads that body block through, as it does the try body.
The cache was keyed on file content, include paths, defines and macro files
only. atom's header mode parses a project without function bodies and leaves
its cache in <input>/.chen; a later full run over the same directory replayed
those body-less ASTs, so its graph had no method bodies and its usage slices
were empty placeholders (cdxgen's C evidence step followed by evinse hits
exactly this). Function bodies, inactive code, comments, image locations,
trivial expressions, the C++ standard, include discovery and the project index
now take part in the key.
Types, member layouts and constants registered by an earlier run in the same
process that never drained them (a run that stopped early, or an AST pass used
on its own) were lent to the next graph as extra TYPE nodes. This also made the
warm-restore test fail whenever the AST cache tests ran first in the same JVM.
@prabhu
prabhu merged commit 608a2c9 into main Oct 1, 2026
10 checks passed
@prabhu
prabhu deleted the feat/edg-cpp branch October 1, 2026 21:15
@prabhu prabhu added the ai-auto label Oct 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant