Skip to content

Add C/C++ bundle support with preprocessor include graph analysis - #71

Open
ChristopherTuring wants to merge 3 commits into
mainfrom
claude/wonderful-wright-q54z92
Open

ChristopherTuring wants to merge 3 commits into
mainfrom
claude/wonderful-wright-q54z92

Conversation

@ChristopherTuring

Copy link
Copy Markdown
Collaborator

This PR adds support for bundling C and C++ projects by analyzing their preprocessor include graphs without compilation.

Summary

Introduces a new C/C++ loader (stasis/src/loaders/c.js) that builds bundles from C/C++ source files by:

  • Scanning source files for #include, #import, #include_next, and #embed directives
  • Resolving includes using GCC/Clang search semantics (quoted includes in includer's directory, then -iquote, -I, -isystem, -idirafter directories)
  • Automatically bundling each header's implementation file (source of same name beside it or in mirrored src/ directory)
  • Handling conditional compilation (#if/#ifdef/#ifndef) to track live, maybe, and dead code regions
  • Supporting compile_commands.json for per-translation-unit include search paths
  • Detecting known projects (Node.js, V8, OpenSSL, libuv, zlib, ICU, etc.) and their include directory layouts

Key Changes

  • New loader module (stasis/src/loaders/c.js, ~916 lines):

    • scanIncludes(): Parses C/C++ source respecting translation phase 4 (line splicing, comment/literal skipping)
    • collectCBundle(): Builds the include graph with GCC-style path resolution
    • loadCompileCommands(): Reads compile_commands.json for per-file include flags
    • detectProjects(): Identifies known projects and their include structures
    • Support for C/C++ format classification and entry detection
  • Bundle command integration (stasis/src/cmd/bundle.js):

    • Added buildCBundle() function to create C/C++ bundles
    • Integrated with existing bundle infrastructure (workspace bucketing, format tracking)
    • Support for --include-dirs and --compile-commands CLI flags
    • Automatic naming: c-bundle for C projects, cpp-bundle for C++ projects
  • Comprehensive test suite (tests/c-loader.test.js, ~445 lines):

    • Tests for include scanning (directives, forms, conditional compilation, include guards)
    • Tests for path resolution (quoted vs angle includes, #include_next)
    • Tests for compile command parsing and database loading
    • Integration tests for bundle collection with various project layouts
  • Test fixtures (tests/fixtures/c-bundle/):

    • Basic C project with headers and implementation files
    • C++ project with include directory layout
    • Conditional compilation scenarios
    • #embed directive support
  • Documentation updates:

    • Updated doc/file-formats.md to document C/C++ bundle format and resolution
    • Updated doc/bundle.md to mention C/C++ bundle support
    • Updated CLI help in stasis/bin/stasis.js

Notable Implementation Details

  • Conditional compilation handling: Tracks #if/#ifdef/#ifndef nesting with three states (live/maybe/dead) to include directives that might be compiled in some configurations
  • Include guard detection: Recognizes #ifndef X / #define X patterns as include guards, treating their body as unconditional
  • Raw string literals: Properly skips C++11 raw strings (R"delim(...)") which can contain unescaped quotes and backslashes
  • Known projects database: Hardcoded list of common C/C++ projects with their marker files and include directory structures for accurate resolution without build configuration
  • Header implementation pairing: Automatically discovers and bundles implementation files (.c/.cpp) for bundled headers, enabling complete dependency tracking
  • Edges keyed under "c": All C/C++ imports use the same condition key regardless of individual file format, simplifying the import graph structure

https://claude.ai/code/session_01Hg9tYjaNiR19bvkZUJP8hH

claude added 3 commits October 9, 2026 03:30
`stasis bundle` takes C/C++ entries (.c/.cc/.cpp/.cxx/.c++, or a header
.h/.hh/.hpp/.hxx/.h++) and walks their `#include`/`#import`/`#include_next`/
`#embed` lines as GCC and Clang search for them: the includer's directory for
"x", then -iquote, -I, -isystem, -idirafter. Directives are found past
comments, literals (raw strings, digit separators) and line splices; `#if 0`
code is skipped, an include under any other conditional is followed but not
required, and a file's include guard is no condition.

Headers hold code, so an include lands on the header, and a bundled header
pulls in its implementation file (same name beside it, or in the src/
mirroring its include/): main.c -> util.h -> util.c. That edge is a guess,
so nothing an implementation file lacks is fatal.

The search path comes from --include-dirs (-I for every unit) or
--compile-commands (a compile_commands.json: each unit's own -I/-iquote/
-isystem/-idirafter/--embed-dir/-include). A quoted include the tree holds
above its includer stops the build, naming the -I it needs; one found
nowhere (a system header in quotes, a generated one) is reported; <x> found
nowhere is a system header. Nothing outside the root is read; a symlink out
of it and a .env are refused. Files of a GitHub git submodule are bucketed
as a `github` dependency, as Solidity's are; edges are keyed under "c",
files tagged c/cpp/c-header/cpp-header, #embed files as resources.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hg9tYjaNiR19bvkZUJP8hH
…therwise

A header's implementation is found by name (util.h -> util.c), which misses
libraries whose sources aren't named after their headers: from Node.js's
src/node_main.cc, 78 of its 221 sources and nearly all of V8 and OpenSSL
weren't reached. A small table of known links, matched from a library's
root wherever it is vendored, fills that in: Node.js's embedder API
(node.h -> src/api/*), its bindings (every source registering one through
NODE_BINDING_CONTEXT_AWARE_INTERNAL/NODE_BINDING_PER_ISOLATE_INIT) and the
headers it implements across files; V8's API (include/v8*.h -> src/api/*);
OpenSSL's subsystems, TLS and providers; libuv, uvwasi, c-ares, llhttp,
nghttp2/nghttp3/ngtcp2, brotli, zstd, zlib and ICU.

From node_main.cc the bundle now reaches 8,807 files instead of 1,587: 218
of Node.js's 221 sources (the rest only the build picks), all of libuv,
1,054 of OpenSSL's 1,164 and 869 of V8's 1,264 sources.

An implementation edge is now keyed by the file's path from the header's
directory (`impl ../src/api/api.cc`), as two links of one header can share
a name (libuv's src/unix/fs.c and src/win/fs.c).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hg9tYjaNiR19bvkZUJP8hH
…eir builds scope them

A tree vendoring projects stasis knows -- Node.js and its deps/, V8 and its
third_party/, OpenSSL, libuv, ICU, zlib, c-ares, nghttp2/nghttp3/ngtcp2,
Brotli, Zstandard and some 20 more -- is now searched with no -I given. A
directory holding a project's marker files is its root; each project has its
own -I directories and those it exports (from Node.js's gyp files). A unit's
search path is its innermost project's own directories, then the exports of
every other project but those around it, nearest first. So Node.js's sources
find deps/zlib and V8's its own third_party/zlib, and ICU's "util.h" is ICU's:
a single merged -I list resolved those to the wrong copy. Node.js's
deps/openssl wraps OpenSSL's tree with its own directories; where a project
carries configurations for several platforms, Linux x86-64's is taken.

--include-dirs come first and a compile command replaces it for the units it
lists. `stasis bundle` says which projects it found. Bundling a Node.js
checkout from src/node_main.cc with no flag reaches 8,949 files: every file
the hand-built merged list did, and 104 more.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hg9tYjaNiR19bvkZUJP8hH
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-09T07:19:50.966965Z 56b3679 PR opened
🔒 Security Review ✅ Completed 2026-10-09T07:22:50.221448Z 56b3679 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@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: 56b367948c

ℹ️ 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 stasis/src/loaders/c.js
for (const { flag, path, dir } of ctx.forced) {
const spec = `${flag} ${path}`
let result = isAbsolute(path) ? probe(makeDir(baseDir, realBase, path), '.') : probe(dir, path)
if (result === null && !isAbsolute(path)) result = resolveInclude(node, { directive: 'include', form: 'quote', path })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Skip the source directory for forced includes

When a compile command uses -include or -imacros, the named file is absent from the command's working directory, and a same-named file exists beside the translation unit, this fallback resolves to that source-local file. GCC specifies that the working directory is searched instead of the main source's directory, followed by the remainder of the quote search chain (GCC preprocessor options); calling resolveInclude() here reintroduces the source directory and can silently record and bundle a different header from the one the compiler uses.

Useful? React with 👍 / 👎.

Comment thread stasis/src/loaders/c.js
if (edge.result?.refused !== undefined) {
missing.push({ spec: edge.spec, from: node.file, reason: edge.result.refused })
seen.add(id)
} else if (edge.result === null && (edge.forced || (edge.inc.form === 'quote' && edge.inc.directive !== 'include_next'))) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fail when a live embed resource is absent

When an unconditional #embed "asset" cannot be resolved and no ancestor directory happens to contain that path, this generic quoted-reference branch classifies it as merely unfound, so buildCBundle emits a warning and produces a supposedly complete bundle without the required bytes. Unlike a quoted header, an embed has no implicit system-header fallback—GCC searches only the source directory and --embed-dir directories (GCC binary resource inclusion)—so an unresolved live #embed must be added to missing.

Useful? React with 👍 / 👎.

Comment thread stasis/src/loaders/c.js
Comment on lines +65 to +67
function evalCondition(expr) {
const m = /^\(*\s*(\d+)[uUlL]*\s*\)*$/u.exec(expr.trim())
return m ? Number(m[1]) !== 0 : undefined

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recognize non-decimal integer conditions

For an always-true condition written as a hexadecimal or binary integer literal, such as #if 0x1, this decimal-only regex returns undefined and marks the branch as maybe. Consequently a live unresolved include in that branch is neither required nor even reported by the definite-edge pass, allowing an incomplete bundle to succeed; parse the standard integer literal bases rather than only \d+.

Useful? React with 👍 / 👎.

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