Skip to content

feat: add include resolver, dependency graph, BFS scanner - #368

Merged
16bit-ykiko merged 18 commits into
mainfrom
feat/dependency-graph
Mar 28, 2026
Merged

16bit-ykiko merged 18 commits into
mainfrom
feat/dependency-graph

Conversation

@16bit-ykiko

@16bit-ykiko 16bit-ykiko commented Mar 25, 2026 •

Copy link
Copy Markdown
Member

Summary

Add a dependency graph scanner that discovers the full include-graph of a project at startup, without invoking the compiler. The scanner reads every source file from the compilation database, lexer-scans it for #include and import directives, resolves each include to a filesystem path, and transitively follows discovered headers using a wavefront BFS. The result is a DependencyGraph mapping every file to its direct includes (keyed by file and search-config pair), plus module-name-to-file mappings for C++20 modules.

This replaces per-file on-demand header discovery with a one-shot upfront scan, giving the server a complete picture of the project's dependency structure before any LSP request arrives.

Architecture

Core components

DependencyGraph (src/syntax/dependency_graph.h/.cpp)

  • Stores include edges as (path_id, config_id) -> [path_id...] using LLVM DenseMaps with a custom IncludeKey hash.
  • Bit 31 of each included path_id is a conditional flag (CONDITIONAL_FLAG = 0x80000000) indicating whether the include was inside an #if/#ifdef block.
  • get_all_includes() merges edges across all configs for a file, with unconditional edges winning over conditional ones.
  • Module interface units are tracked via module_name -> [path_id].

IncludeResolver (src/syntax/include_resolver.h/.cpp)

  • Resolves #include directives to filesystem paths using a directory listing cache (DirListingCache) instead of per-file stat() calls.
  • Each search directory is listed once via readdir() and the resulting filenames are stored in an llvm::StringSet. File existence checks become O(1) hash lookups.
  • For multi-component includes like "llvm/Support/raw_ostream.h", a two-stage strategy is used: first check if the top-level component (llvm) exists in the pre-listed directory, then only resolve the subdirectory on match.
  • Follows standard C/C++ include resolution order: includer directory first for quoted includes, then search paths from the appropriate start index, with #include_next support.
  • ResolvedSearchConfig pre-resolves all search directory StringMap entries to direct pointers, eliminating map lookups during hot resolution loops.

Wavefront BFS scanner (scan_dependency_graph() in dependency_graph.cpp)

  • Runs on a local eventide event loop with a libuv thread pool.
  • Wave 0: all source files from the CDB are read and lexer-scanned in parallel on worker threads.
  • Phase 1 (parallel): read file content + lexer scan for directives.
  • Phase 2 (serial): resolve each #include to a path, intern it in PathPool, build graph edges, and collect newly discovered files for the next wave.
  • Waves repeat until no new files are discovered.

Key optimizations

  1. Overlapped directory cache population: All unique search directories are listed via readdir() on worker threads concurrently with Wave 0's file scanning. The dir cache await happens before Phase 2, so the cost is max(dir_time, scan_time) rather than dir_time + scan_time.
  2. Scan prefetching: During Phase 2, newly discovered files are immediately queued for reading on the thread pool. By the time the next wave starts, most files are already read.
  3. Angled-include cache: Resolution results for <...> includes are cached per (config_id, header_name) since they don't depend on the includer's directory. This eliminates redundant directory searches for commonly included system/library headers.
  4. ScanCache: A persistent cache struct allows warm re-scans to skip file I/O, lexer scanning, config extraction, and include resolution entirely.

ScanReport provides detailed timing and accuracy metrics: per-wave breakdowns, per-phase wall-clock times, cumulative I/O statistics, readdir cache hit rates, and a list of unresolved includes.

Cross-platform fixes

MSVC/clang-cl option visibility (argument_parser.h/.cpp, command.cpp, search_config.cpp)

  • Added default_visibility() that detects cl.exe / clang-cl drivers (including versioned names like clang-cl-17) and returns an appropriate visibility mask.
  • When the driver is NOT clang-cl, the CLOption visibility group is excluded. This prevents Unix absolute paths like /Users/... from being misparsed as MSVC /U sers/... options on macOS/Linux.
  • The visibility mask is passed to ParseOneArg() and set before parsing in CompilationDatabase::load(), toolchain extraction, and extract_search_config().

Non-C-family file filtering (argument_parser.h/.cpp, command.cpp)

  • is_c_family_file() uses clang's types::lookupTypeForExtension to detect C/C++/ObjC/CUDA files.
  • CompilationDatabase::load() now skips .rc, .asm, .def, and other non-C-family entries that some build systems emit into compile_commands.json.

Toolchain option whitelist (argument_parser.cpp)

  • is_toolchain_option() defines a positive whitelist of flags (--target, --sysroot, -std=, -x, -nostdinc, etc.) that affect system include path discovery.
  • Toolchain extraction now uses this whitelist instead of a blacklist, ensuring only relevant flags reach the toolchain query.

Additional discarded options: OPT_UNKNOWN, OPT__DASH_DASH, OPT__SLASH_Fo, and OPT__SLASH_Fd are now filtered during argument parsing.

Server integration

  • MasterServer now holds a DependencyGraph member and calls scan_dependency_graph() during load_workspace(), immediately after loading the CDB.
  • A structured log message reports scan timing, file counts, edge counts, resolution accuracy, and wave count.
  • New public methods CompilationDatabase::intern_path() and get_entries() expose internals needed by the scanner.

Benchmark

benchmarks/scan_benchmark.cpp is a standalone CLI tool that:

  • Loads a compile_commands.json and runs scan_dependency_graph() N times (default 20) as true cold starts (CDB, PathPool, and DependencyGraph are rebuilt each iteration).
  • Prints a detailed report for the first run: file counts, include edges, resolution accuracy, per-wave phase breakdown, I/O statistics, and directory cache hit rates.
  • Prints min/avg/max summary across all runs.
  • Supports --export graph.json to dump the full dependency graph.
  • Includes CDB dedup diagnostics (context/canonical command histogram).

.github/workflows/benchmark.yml runs on PRs against main, on ubuntu-24.04 / macos-15 / windows-2025. It clones LLVM, generates a CDB with all major subprojects enabled, and runs 20 cold-start iterations.

Files changed

File Description
src/syntax/dependency_graph.{h,cpp} DependencyGraph class, ScanReport, ScanCache, wavefront BFS scanner
src/syntax/include_resolver.{h,cpp} DirListingCache, ResolvedSearchConfig, include resolution with multi-component path optimization
src/command/argument_parser.{h,cpp} Visibility mask, toolchain option whitelist, C-family file detection, versioned clang-cl handling
src/command/command.{h,cpp} Visibility mask wiring, non-C-family file skip, intern_path() / get_entries() APIs
src/command/search_config.cpp Set visibility mask before parsing search config
src/server/master_server.{h,cpp} DependencyGraph member, startup scan + log report
src/syntax/scan.cpp Pre-allocate includes vector, add StringSet.h include
benchmarks/scan_benchmark.cpp Standalone cold-start benchmark with detailed reporting and JSON export
.github/workflows/benchmark.yml CI workflow: build, clone LLVM, generate CDB, run 20 iterations on 3 platforms
tests/unit/syntax/ Unit + integration tests for dependency_graph, include_resolver, scan
CMakeLists.txt Add new sources to clice-core, add scan_benchmark target

Test plan

  • dependency_graph_tests.cpp: unit tests for module mapping, include edges (per-config, cross-config union, conditional flag semantics), file/module/edge counts
  • dependency_graph_tests.cpp: integration tests using TempDir with real files — single file, chain includes, diamond includes, conditional includes, #include_next, multi-config, unresolved includes
  • include_resolver_tests.cpp: quoted vs angled resolution order, multi-component path optimization, #include_next skipping, absolute paths, ../ relative paths, missing directories, empty search config
  • scan_tests.cpp: verify is_angled flag is correctly set for <...> vs "..." includes
  • CI benchmark validates build and scan complete without errors on all three platforms

🤖 Generated with Claude Code

@16bit-ykiko
16bit-ykiko force-pushed the feat/dependency-graph branch from 91d9b27 to 5ad5af7 Compare March 25, 2026 15:57
@coderabbitai

coderabbitai Bot commented Mar 25, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a wavefront BFS include dependency scanner and cache, include-resolution utilities, PathPool-based path interning and CompilationDatabase accessors, a benchmark CLI and CI workflow, server integration to run scans during workspace load, CMake/bench target, and extensive unit tests for scanning and resolution.

Changes

Cohort / File(s) Summary
Dependency Graph Core
src/syntax/dependency_graph.h, src/syntax/dependency_graph.cpp, tests/unit/syntax/dependency_graph_tests.cpp
New DependencyGraph, ScanReport, ScanCache, WaveEntry and scan_dependency_graph(...) implementation: async wavefront BFS scanner, caching layers, detailed metrics, and unit/integration tests.
Include Resolution
src/syntax/include_resolver.h, src/syntax/include_resolver.cpp, tests/unit/syntax/include_resolver_tests.cpp
New include-resolution API and implementation with DirListingCache, ResolvedSearchConfig, resolve_include() (supports normalization, #include_next, quoted/angled semantics) and filesystem-backed tests.
Benchmark & Build
.github/workflows/benchmark.yml, benchmarks/scan_benchmark.cpp, CMakeLists.txt
Adds GitHub Actions benchmark workflow (OS matrix), a scan_benchmark CLI with run/export options, and CMake target registration for scan_benchmark.
CompilationDatabase & Argument Parsing
src/command/command.h, src/command/command.cpp, src/command/argument_parser.h, src/command/argument_parser.cpp
Adds CompilationDatabase::intern_path() and get_entries(); introduces is_toolchain_option(unsigned) predicate and uses it when extracting toolchain flags.
Server integration
src/server/master_server.h, src/server/master_server.cpp
Replaces custom ServerPathPool with shared PathPool, adds DependencyGraph member, and invokes scan_dependency_graph() during workspace loading.
Scan internals & tests
src/syntax/scan.cpp, tests/unit/syntax/scan_tests.cpp
Minor micro-optimization (reserve includes capacity) and expanded tests asserting per-include is_angled and other scan properties.
New Tests
tests/unit/syntax/dependency_graph_tests.cpp, tests/unit/syntax/include_resolver_tests.cpp, tests/unit/syntax/scan_tests.cpp
Extensive unit and integration tests covering graph semantics, include resolution, cache reuse, deleted-file handling, quoted vs angled lookup, diamond/deep graphs, and scan metrics.

Sequence Diagram(s)

sequenceDiagram
    participant Caller as Caller
    participant API as scan_dependency_graph
    participant CDB as CompilationDatabase
    participant PathPool as PathPool
    participant Cache as ScanCache / DirListingCache
    participant Phase1 as Phase 1: Read & Lex
    participant Phase2 as Phase 2: Resolve & Build
    participant Graph as DependencyGraph

    Caller->>API: scan_dependency_graph(cdb, path_pool, graph, cache?)
    API->>CDB: read entries, group contexts
    API->>Cache: prewarm dir listings & context caches
    API->>Phase1: launch parallel reads & lex (use cache if available)
    Phase1->>Cache: produce/check ScanResult
    Phase1-->>API: return scan results for wave
    API->>Phase2: resolve includes using ResolvedSearchConfig
    Phase2->>Cache: resolve_include(...) using DirListingCache
    Phase2->>PathPool: intern resolved paths
    Phase2->>Graph: add edges (mark conditional/unconditional), register modules
    API->>API: assemble next wave until no work
    API-->>Caller: return ScanReport (timings, counts, unresolved)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I hopped through headers, dirs, and trees,
Waves chased includes with tiny, eager breeze,
I cached the listings, resolved each name,
Benchmarked the hops — a rabbit’s small game,
Graphs now bloom where once were only leaves.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main additions: include resolver, dependency graph, and BFS scanner, all of which are the primary focus of this PR with substantial code additions.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/dependency-graph

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@16bit-ykiko
16bit-ykiko changed the base branch from feat/search-config to main March 25, 2026 16:01

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🧹 Nitpick comments (18)
src/support/path_pool.h (1)

33-35: Consider adding bounds check or assertion in resolve().

If an invalid id is passed (e.g., from stale data or corruption), this will access out-of-bounds memory. Consider adding an assertion for debug builds:

llvm::StringRef resolve(std::uint32_t id) const {
    assert(id < paths.size() && "Invalid path ID");
    return paths[id];
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/support/path_pool.h` around lines 33 - 35, The resolve(std::uint32_t id)
method in path_pool.h currently indexes paths[id] without validation; add a
bounds check or debug assertion (e.g., assert(id < paths.size() && "Invalid path
ID")) before returning to prevent out-of-bounds access, or alternatively handle
the error path (throw or return an empty/optional llvm::StringRef) so resolve
and the paths container are protected from stale/corrupt IDs.
.github/workflows/benchmark.yml (2)

32-42: Consider caching LLVM clone to speed up CI.

Cloning llvm-project (even with --depth 1) and running CMake configure on it is time-consuming. Consider caching the clone between runs:

♻️ Example caching approach
- name: Cache LLVM clone
  uses: actions/cache@v4
  with:
    path: llvm-project
    key: llvm-project-${{ runner.os }}-depth1

- name: Clone LLVM
  if: steps.cache-llvm.outputs.cache-hit != 'true'
  run: git clone --depth 1 https://github.com/llvm/llvm-project.git
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/benchmark.yml around lines 32 - 42, Add a cache step
before the existing "Clone LLVM" step to persist the llvm-project directory
between runs using actions/cache@v4, then make the "Clone LLVM" step conditional
on the cache miss by checking the cache step's cache-hit output; specifically,
insert a "Cache LLVM clone" step that caches path llvm-project with a key like
llvm-project-${{ runner.os }}-depth1 and change the "Clone LLVM" step to run
only when that cache step reports cache-hit != 'true' so the "Generate CDB" step
(the cmake invocation) can reuse the cached clone when available.

46-47: Benchmark results are not persisted.

The benchmark output only appears in CI logs. Consider uploading results as artifacts or using a benchmarking action (e.g., benchmark-action/github-action-benchmark) to track performance over time.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/benchmark.yml around lines 46 - 47, The "Run benchmark"
step that executes "./build/RelWithDebInfo/bin/scan_benchmark --runs 20
llvm-build/compile_commands.json" currently only prints results to CI logs;
modify the workflow to persist results by saving the benchmark output to a file
(e.g., redirect stdout/stderr to a results file) and then upload that file as a
GitHub Actions artifact (add an "actions/upload-artifact" step referencing the
results filename), or replace/enhance the step with a benchmarking action such
as benchmark-action/github-action-benchmark to automatically record and track
metrics over time.
src/syntax/scan.cpp (1)

7-7: Unused include: llvm/ADT/StringSet.h

This header does not appear to be used anywhere in this file. No StringSet type is referenced.

🧹 Remove unused include
-#include "llvm/ADT/StringSet.h"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/syntax/scan.cpp` at line 7, Remove the unused header include
"llvm/ADT/StringSet.h" from src/syntax/scan.cpp — the file does not reference
the StringSet type anywhere; delete the line `#include` "llvm/ADT/StringSet.h" and
run a quick rebuild to ensure no missing symbols remain.
tests/unit/test/temp_dir.h (2)

26-28: Unchecked error from createUniqueDirectory.

If directory creation fails, root will be empty, and subsequent operations will silently fail or operate on unexpected paths. Consider at least asserting success in test code:

TempDir(llvm::StringRef prefix = "clice-test") {
    auto ec = llvm::sys::fs::createUniqueDirectory(prefix, root);
    assert(!ec && "Failed to create temp directory");
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/test/temp_dir.h` around lines 26 - 28, The TempDir constructor
calls llvm::sys::fs::createUniqueDirectory(prefix, root) but ignores the
returned error code so root may be empty on failure; modify the
TempDir(llvm::StringRef prefix) constructor to capture the returned
llvm::ErrorCode (or std::error_code) from createUniqueDirectory, assert or
handle failure (e.g., assert(!ec, "Failed to create temp directory") or log and
abort) and ensure root is only used when the call succeeds; reference TempDir,
createUniqueDirectory, and root when making the change.

59-67: Silent failure in touch() can hide test setup issues.

If file creation or writing fails, the test will proceed with missing/empty files, potentially causing confusing test failures. Consider asserting success:

🛠️ Proposed fix
 void touch(llvm::StringRef relative, llvm::StringRef content = "") {
     auto p = path(relative);
     llvm::sys::fs::create_directories(llvm::sys::path::parent_path(p));
     std::error_code ec;
     llvm::raw_fd_ostream out(p, ec);
-    if(!ec) {
-        out << content;
-    }
+    assert(!ec && "Failed to create file in TempDir");
+    out << content;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/test/temp_dir.h` around lines 59 - 67, The touch() helper
currently ignores filesystem errors and may silently produce missing/empty
files; update touch (and its use of path(), llvm::sys::fs::create_directories
and llvm::raw_fd_ostream) to detect and fail on errors: check the result of
create_directories and the std::error_code ec from llvm::raw_fd_ostream, and if
an error is present, fail the test (e.g., with an assertion or throwing a
test-fatal exception) including the path and ec.message(); also ensure the
output is properly flushed/closed after writing.
tests/unit/syntax/dependency_graph_tests.cpp (1)

373-385: Direct access to pool.cache for path lookup.

Line 375 accesses pool.cache[tmp.path("src/main.cpp")] to get the path ID. This assumes PathPool::cache is public and uses a map-like interface. If this is test-only access, consider adding a PathPool::lookup(path) method that returns std::optional<uint32_t> for cleaner test code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/syntax/dependency_graph_tests.cpp` around lines 373 - 385, The
test directly reads pool.cache[tmp.path("src/main.cpp")] which relies on
PathPool::cache being public; change the test to use a new PathPool lookup API
instead: add a PathPool::lookup(const std::string& path) returning
std::optional<uint32_t> (or use existing pool.lookup if present), call that with
tmp.path("src/main.cpp"), check the optional is set, then pass the contained id
to graph.get_includes; update the test to assert the lookup succeeded before
using the id and remove direct accesses to pool.cache.
benchmarks/scan_benchmark.cpp (2)

74-94: Direct access to path_pool.paths couples benchmark to internal structure.

The export function directly accesses path_pool.paths[id] and path_pool.paths.size(). If PathPool's internal representation changes, this will break.

Consider adding a public iteration API to PathPool if this pattern is needed elsewhere, or document that this benchmark has privileged access to internals.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@benchmarks/scan_benchmark.cpp` around lines 74 - 94, This loop in the
benchmark directly reads PathPool internals (path_pool.paths and its size),
coupling the benchmark to PathPool's representation; change the code to use a
public PathPool API (e.g., add and call methods like PathPool::size(),
PathPool::get_path(id) or an iterator begin()/end()) instead of accessing
path_pool.paths, update the loop to iterate via that API when building FileNode
(node.path and the includes pushed from path_pool.paths[raw_id].str()), and keep
use of graph.get_all_includes, DependencyGraph::PATH_ID_MASK, path_to_module,
FileNode and export_data.files intact so callers only touch PathPool's public
interface.

254-258: putenv usage is correct but consider setenv for clarity.

The static std::string env ensures the string persists for the program lifetime as required by putenv. However, setenv (POSIX) is generally preferred as it copies the string internally.

Note: setenv is not available on Windows, so if cross-platform support is needed, this putenv pattern with a static string is acceptable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@benchmarks/scan_benchmark.cpp` around lines 254 - 258, Replace the putenv
usage with setenv when available to let the runtime copy the string: check for
UV_THREADPOOL_SIZE via std::getenv as shown, compute pool_size, then call
setenv("UV_THREADPOOL_SIZE", std::to_string(pool_size).c_str(), 1) on POSIX;
retain the existing static std::string + putenv(env.data()) fallback for Windows
or when setenv is unavailable (use `#ifdef` _WIN32 or HAVE_SETENV guard). Update
the code around getenv/UV_THREADPOOL_SIZE/pool_size/env/putenv to prefer setenv
but fall back to the current putenv pattern for cross-platform compatibility.
src/command/search_config.cpp (1)

59-67: Prefix path concatenation assumes prefix includes trailing separator.

When -iprefix /usr/include (no trailing /) is followed by -iwithprefix lib, the result is /usr/includelib instead of /usr/include/lib. This matches clang's behavior (which also directly concatenates), but it may be surprising.

The test at line 432-433 in command_tests.cpp uses tmp.path("gcc/12/") with a trailing slash, which works correctly. Consider adding a test case without trailing slash to document this behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/command/search_config.cpp` around lines 59 - 67, The prefix concatenation
code (cases for ID::OPT_iprefix and ID::OPT_iwithprefix using make_absolute)
assumes the prefix already has a trailing separator and will produce
"/usr/includelib" when given "-iprefix /usr/include" + "-iwithprefix lib"; add a
unit test in the command_tests suite that constructs a prefix without a trailing
slash (e.g., tmp.path("gcc/12") or "/usr/include") and then invokes the code
path that parses OPT_iprefix + OPT_iwithprefix, asserting the produced path
equals a direct concatenation of prefix + arg (i.e., no added slash) so the
behavior is documented; update or add an assertion near the existing test that
currently uses tmp.path("gcc/12/") to include a sibling case without the
trailing "/" to capture this behavior.
src/command/toolchain_provider.cpp (1)

29-44: Consider adding -cxx-isystem and potentially -iframework to toolchain options if supporting these paths becomes necessary.

The is_toolchain_option filter captures key system path discovery flags. However, -cxx-isystem (C++-specific system directories) and -iframework (macOS frameworks) can affect system includes and are missing from the filter. A TODO comment in search_config.cpp already flags -cxx-isystem as a known gap, suggesting awareness of this limitation. These options are not currently used in the codebase, so this remains a low-priority enhancement for completeness rather than a bug fix.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/command/toolchain_provider.cpp` around lines 29 - 44, The
is_toolchain_option filter (function is_toolchain_option) omits C++-specific and
framework include flags; add cases for the corresponding enum values (e.g.,
ID::OPT_cxx_isystem and ID::OPT_iframework or whatever the project enum names
are) to return true so these flags are treated as toolchain options like
OPT_isysroot and OPT__sysroot_EQ; update the switch in is_toolchain_option to
include those enum entries and run a quick compile to ensure the enum names
match the codebase.
src/command/toolchain_provider.h (1)

15-20: Document lifetime requirements for StringRef members.

ToolchainQuery::file and directory are llvm::StringRef which don't own their data. Callers must ensure the referenced strings outlive the query execution. Consider adding a doc comment or using std::string if ownership is unclear.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/command/toolchain_provider.h` around lines 15 - 20, The ToolchainQuery
struct's members file and directory are llvm::StringRef and do not own their
data; update the code by documenting lifetime requirements on ToolchainQuery
(add a comment above struct ToolchainQuery stating that callers must ensure the
storage backing file and directory outlives the query execution) or, if
ownership is unclear, change those members to std::string to take ownership;
reference the ToolchainQuery struct and its file and directory members when
making the change so callers and implementors see the lifetime requirement or
new ownership semantics.
src/syntax/dependency_graph.cpp (3)

71-83: First-seen flag wins in get_all_includes — consider if this is intended.

When merging includes across configs, deduplication uses raw_id but stores the original flagged id. If a header is included conditionally in config A and unconditionally in config B, only the first-encountered flag is preserved. If the intent is to track "ever unconditional," this logic should be adjusted.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/syntax/dependency_graph.cpp` around lines 71 - 83, get_all_includes
currently deduplicates by raw_id (using PATH_ID_MASK) but keeps the first-seen
full id (including any conditional/unconditional flags), so an unconditional
include seen after a conditional one won't upgrade the stored flag; change the
merge logic in get_all_includes to detect when a later id has a "stronger" flag
(e.g., unconditional) and replace the previously stored id in result (and update
seen bookkeeping) instead of keeping the first one; locate the loop over
fc_it->second and the use of seen, result, IncludeKey and PATH_ID_MASK and
implement a compare/replace step that preserves the strongest include flag for
each raw_id.

631-643: Phase 3 timing is always zero — remove dead code.

phase3_end is assigned the same value as phase2_end (line 632), so p3 is always 0. The comment at line 478 confirms Phase 2 and 3 were merged. Consider removing phase3_end, p3, and report.phase3_ms to reduce confusion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/syntax/dependency_graph.cpp` around lines 631 - 643, The code calculates
a redundant phase3 timing: phase3_end is set equal to phase2_end so p3 is always
zero; remove the dead variables and update the report accumulation to reflect
only two phases. Specifically, delete phase3_end, the computation of p3, and the
accumulation report.phase3_ms += p3; keep the existing computations for p1 and
p2 that use wave_start and phase1_end/phase2_end and only add p1 to
report.phase1_ms and p2 to report.phase2_ms (symbols to edit: phase2_end,
phase3_end, p1, p2, p3, report.phase3_ms, report.phase2_ms, wave_start,
phase1_end).

334-352: Consider avoiding copy of initial_wave on warm runs.

Line 336 copies the entire initial_wave vector. For large codebases this adds overhead. If the cached wave is only consumed (not reused across multiple scans), a move or reference could eliminate the copy.

♻️ Potential optimization
     const bool have_initial_wave_cache = ext_cache && !ext_cache->initial_wave.empty();
     if(have_initial_wave_cache) {
-        current_wave = ext_cache->initial_wave;
+        current_wave = std::move(ext_cache->initial_wave);
         for(auto& entry: current_wave) {
             scanned_files.try_emplace(entry.path_id, entry.found_dir_idx);
         }
+        // Rebuild initial_wave at end if cache needs to persist across scans
     } else {

Note: This requires restoring initial_wave after the scan if the cache should be reusable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/syntax/dependency_graph.cpp` around lines 334 - 352, The code currently
copies ext_cache->initial_wave into current_wave causing unnecessary allocation;
change the logic in the branch that checks have_initial_wave_cache to move the
cached vector into current_wave (e.g., use std::move or swap with current_wave)
and then populate scanned_files from the moved current_wave entries; if
ext_cache->initial_wave must remain reusable afterwards, move it into a local
temporary for the scan and then restore it (copy or move back) after the scan
completes. Ensure you only modify the block that handles ext_cache/initial_wave
and keep the rest of current_wave/scanned_files usage unchanged.
src/syntax/dependency_graph.h (1)

134-141: phase3_ms is always zero — consider removing or documenting.

The implementation merged Phase 2 and 3, making phase3_ms always zero. Either remove this field or update the comment to indicate it's reserved for future use.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/syntax/dependency_graph.h` around lines 134 - 141, The field phase3_ms is
always zero because Phase 2 and 3 were merged; either remove the phase3_ms
member declaration to avoid dead state or explicitly mark it as reserved: update
its comment to "reserved for future use (always zero — phases merged)" and keep
the field for ABI/serialization compatibility; also search for any
uses/serialization of phase3_ms and remove or adapt them accordingly.
src/command/command.cpp (1)

907-949: Consider extracting context lookup helper to reduce duplication.

The pattern of looking up CompilationInfo* from (path_id, context) appears in lookup(), lookup_search_config(), and resolve_toolchain_entries(). Extracting a helper like find_compilation_info(path_id, context) would reduce duplication.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/command/command.cpp` around lines 907 - 949, The code duplicates the
logic that finds a CompilationInfo for a given path_id and context in lookup(),
lookup_search_config(), and resolve_toolchain_entries(); extract that into a
helper like find_compilation_info(path_id, context) that traverses
self->files[path_id] and its linked list (checking cur->info.ptr == context) and
returns object_ptr<CompilationInfo> (or nullptr). Replace the duplicated blocks
in resolve_toolchain_entries (and similarly update lookup() and
lookup_search_config()) to call find_compilation_info(path_id, context), then
use the returned info for the arguments/directory logic.
src/syntax/include_resolver.cpp (1)

118-123: Normalization detection may miss edge cases.

The check for "\\." (backslash-dot) only catches .\ patterns but not standalone . as a path component without a following slash (e.g., foo\.bar wouldn't need normalization but foo\.\bar would). However, this is a minor edge case unlikely to occur in practice.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/syntax/include_resolver.cpp` around lines 118 - 123, The normalization
detection in the needs_normalize computation misses cases where a dot component
follows a path separator (e.g., "\." or "/.") or is a standalone component;
update the boolean expression that computes needs_normalize (using filename and
is_simple) to detect any path-separator + "."/ ".." sequences instead of just
"\\.", for example by checking for "/." or "\\." or "/.." or "\\.." or testing
for filename == "."/ ".." or using a small loop to scan for a path separator
followed by '.' (so dot components like "\." and "\.." are correctly flagged for
normalization).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/command/command.cpp`:
- Around line 772-776: The code currently assumes the last element of cached
(copied into arguments) is the temp source file appended during query_toolchain
and unconditionally calls arguments.pop_back(); instead of blindly popping,
validate the invariant: check the last argument from cached (or
arguments.back()) matches the expected temp-source pattern (e.g., filename
suffix/prefix or a sentinel token created by query_toolchain) before removing
it, and if it does not match, log or assert (or avoid removal) to avoid
discarding a legitimate compiler flag; also add a brief comment documenting that
query_toolchain appends a temp source and why the check is necessary.

In `@src/server/master_server.cpp`:
- Around line 197-199: scan_dependency_graph blocks the main event loop (it
calls loop.run()), causing load_workspace (which is scheduled on the main loop)
to block; fix by making the call non-blocking: either run scan_dependency_graph
on a background thread/worker (e.g., std::async/std::thread or a thread-pool
task) and then post the completed dependency_graph back to the main loop, or
refactor scan_dependency_graph into a coroutine-friendly/yielding version that
does not call loop.run() internally; ensure you update the call site in
load_workspace to await or receive the result asynchronously and protect shared
state (dependency_graph, cdb, updates, path_pool) for thread-safety.

In `@src/syntax/dependency_graph.cpp`:
- Around line 45-51: The method DependencyGraph::set_includes currently always
pushes config_id into file_configs[path_id], causing duplicate entries when
set_includes is called multiple times for the same (path_id, config_id); modify
set_includes to guard insertion by checking whether config_id is already present
in file_configs[path_id] (e.g., use std::find or equivalent on the SmallVector
before push_back) or otherwise deduplicate after insertion so
file_configs[path_id] contains each config_id only once while leaving the
includes[key] assignment as-is.

---

Nitpick comments:
In @.github/workflows/benchmark.yml:
- Around line 32-42: Add a cache step before the existing "Clone LLVM" step to
persist the llvm-project directory between runs using actions/cache@v4, then
make the "Clone LLVM" step conditional on the cache miss by checking the cache
step's cache-hit output; specifically, insert a "Cache LLVM clone" step that
caches path llvm-project with a key like llvm-project-${{ runner.os }}-depth1
and change the "Clone LLVM" step to run only when that cache step reports
cache-hit != 'true' so the "Generate CDB" step (the cmake invocation) can reuse
the cached clone when available.
- Around line 46-47: The "Run benchmark" step that executes
"./build/RelWithDebInfo/bin/scan_benchmark --runs 20
llvm-build/compile_commands.json" currently only prints results to CI logs;
modify the workflow to persist results by saving the benchmark output to a file
(e.g., redirect stdout/stderr to a results file) and then upload that file as a
GitHub Actions artifact (add an "actions/upload-artifact" step referencing the
results filename), or replace/enhance the step with a benchmarking action such
as benchmark-action/github-action-benchmark to automatically record and track
metrics over time.

In `@benchmarks/scan_benchmark.cpp`:
- Around line 74-94: This loop in the benchmark directly reads PathPool
internals (path_pool.paths and its size), coupling the benchmark to PathPool's
representation; change the code to use a public PathPool API (e.g., add and call
methods like PathPool::size(), PathPool::get_path(id) or an iterator
begin()/end()) instead of accessing path_pool.paths, update the loop to iterate
via that API when building FileNode (node.path and the includes pushed from
path_pool.paths[raw_id].str()), and keep use of graph.get_all_includes,
DependencyGraph::PATH_ID_MASK, path_to_module, FileNode and export_data.files
intact so callers only touch PathPool's public interface.
- Around line 254-258: Replace the putenv usage with setenv when available to
let the runtime copy the string: check for UV_THREADPOOL_SIZE via std::getenv as
shown, compute pool_size, then call setenv("UV_THREADPOOL_SIZE",
std::to_string(pool_size).c_str(), 1) on POSIX; retain the existing static
std::string + putenv(env.data()) fallback for Windows or when setenv is
unavailable (use `#ifdef` _WIN32 or HAVE_SETENV guard). Update the code around
getenv/UV_THREADPOOL_SIZE/pool_size/env/putenv to prefer setenv but fall back to
the current putenv pattern for cross-platform compatibility.

In `@src/command/command.cpp`:
- Around line 907-949: The code duplicates the logic that finds a
CompilationInfo for a given path_id and context in lookup(),
lookup_search_config(), and resolve_toolchain_entries(); extract that into a
helper like find_compilation_info(path_id, context) that traverses
self->files[path_id] and its linked list (checking cur->info.ptr == context) and
returns object_ptr<CompilationInfo> (or nullptr). Replace the duplicated blocks
in resolve_toolchain_entries (and similarly update lookup() and
lookup_search_config()) to call find_compilation_info(path_id, context), then
use the returned info for the arguments/directory logic.

In `@src/command/search_config.cpp`:
- Around line 59-67: The prefix concatenation code (cases for ID::OPT_iprefix
and ID::OPT_iwithprefix using make_absolute) assumes the prefix already has a
trailing separator and will produce "/usr/includelib" when given "-iprefix
/usr/include" + "-iwithprefix lib"; add a unit test in the command_tests suite
that constructs a prefix without a trailing slash (e.g., tmp.path("gcc/12") or
"/usr/include") and then invokes the code path that parses OPT_iprefix +
OPT_iwithprefix, asserting the produced path equals a direct concatenation of
prefix + arg (i.e., no added slash) so the behavior is documented; update or add
an assertion near the existing test that currently uses tmp.path("gcc/12/") to
include a sibling case without the trailing "/" to capture this behavior.

In `@src/command/toolchain_provider.cpp`:
- Around line 29-44: The is_toolchain_option filter (function
is_toolchain_option) omits C++-specific and framework include flags; add cases
for the corresponding enum values (e.g., ID::OPT_cxx_isystem and
ID::OPT_iframework or whatever the project enum names are) to return true so
these flags are treated as toolchain options like OPT_isysroot and
OPT__sysroot_EQ; update the switch in is_toolchain_option to include those enum
entries and run a quick compile to ensure the enum names match the codebase.

In `@src/command/toolchain_provider.h`:
- Around line 15-20: The ToolchainQuery struct's members file and directory are
llvm::StringRef and do not own their data; update the code by documenting
lifetime requirements on ToolchainQuery (add a comment above struct
ToolchainQuery stating that callers must ensure the storage backing file and
directory outlives the query execution) or, if ownership is unclear, change
those members to std::string to take ownership; reference the ToolchainQuery
struct and its file and directory members when making the change so callers and
implementors see the lifetime requirement or new ownership semantics.

In `@src/support/path_pool.h`:
- Around line 33-35: The resolve(std::uint32_t id) method in path_pool.h
currently indexes paths[id] without validation; add a bounds check or debug
assertion (e.g., assert(id < paths.size() && "Invalid path ID")) before
returning to prevent out-of-bounds access, or alternatively handle the error
path (throw or return an empty/optional llvm::StringRef) so resolve and the
paths container are protected from stale/corrupt IDs.

In `@src/syntax/dependency_graph.cpp`:
- Around line 71-83: get_all_includes currently deduplicates by raw_id (using
PATH_ID_MASK) but keeps the first-seen full id (including any
conditional/unconditional flags), so an unconditional include seen after a
conditional one won't upgrade the stored flag; change the merge logic in
get_all_includes to detect when a later id has a "stronger" flag (e.g.,
unconditional) and replace the previously stored id in result (and update seen
bookkeeping) instead of keeping the first one; locate the loop over
fc_it->second and the use of seen, result, IncludeKey and PATH_ID_MASK and
implement a compare/replace step that preserves the strongest include flag for
each raw_id.
- Around line 631-643: The code calculates a redundant phase3 timing: phase3_end
is set equal to phase2_end so p3 is always zero; remove the dead variables and
update the report accumulation to reflect only two phases. Specifically, delete
phase3_end, the computation of p3, and the accumulation report.phase3_ms += p3;
keep the existing computations for p1 and p2 that use wave_start and
phase1_end/phase2_end and only add p1 to report.phase1_ms and p2 to
report.phase2_ms (symbols to edit: phase2_end, phase3_end, p1, p2, p3,
report.phase3_ms, report.phase2_ms, wave_start, phase1_end).
- Around line 334-352: The code currently copies ext_cache->initial_wave into
current_wave causing unnecessary allocation; change the logic in the branch that
checks have_initial_wave_cache to move the cached vector into current_wave
(e.g., use std::move or swap with current_wave) and then populate scanned_files
from the moved current_wave entries; if ext_cache->initial_wave must remain
reusable afterwards, move it into a local temporary for the scan and then
restore it (copy or move back) after the scan completes. Ensure you only modify
the block that handles ext_cache/initial_wave and keep the rest of
current_wave/scanned_files usage unchanged.

In `@src/syntax/dependency_graph.h`:
- Around line 134-141: The field phase3_ms is always zero because Phase 2 and 3
were merged; either remove the phase3_ms member declaration to avoid dead state
or explicitly mark it as reserved: update its comment to "reserved for future
use (always zero — phases merged)" and keep the field for ABI/serialization
compatibility; also search for any uses/serialization of phase3_ms and remove or
adapt them accordingly.

In `@src/syntax/include_resolver.cpp`:
- Around line 118-123: The normalization detection in the needs_normalize
computation misses cases where a dot component follows a path separator (e.g.,
"\." or "/.") or is a standalone component; update the boolean expression that
computes needs_normalize (using filename and is_simple) to detect any
path-separator + "."/ ".." sequences instead of just "\\.", for example by
checking for "/." or "\\." or "/.." or "\\.." or testing for filename == "."/
".." or using a small loop to scan for a path separator followed by '.' (so dot
components like "\." and "\.." are correctly flagged for normalization).

In `@src/syntax/scan.cpp`:
- Line 7: Remove the unused header include "llvm/ADT/StringSet.h" from
src/syntax/scan.cpp — the file does not reference the StringSet type anywhere;
delete the line `#include` "llvm/ADT/StringSet.h" and run a quick rebuild to
ensure no missing symbols remain.

In `@tests/unit/syntax/dependency_graph_tests.cpp`:
- Around line 373-385: The test directly reads
pool.cache[tmp.path("src/main.cpp")] which relies on PathPool::cache being
public; change the test to use a new PathPool lookup API instead: add a
PathPool::lookup(const std::string& path) returning std::optional<uint32_t> (or
use existing pool.lookup if present), call that with tmp.path("src/main.cpp"),
check the optional is set, then pass the contained id to graph.get_includes;
update the test to assert the lookup succeeded before using the id and remove
direct accesses to pool.cache.

In `@tests/unit/test/temp_dir.h`:
- Around line 26-28: The TempDir constructor calls
llvm::sys::fs::createUniqueDirectory(prefix, root) but ignores the returned
error code so root may be empty on failure; modify the TempDir(llvm::StringRef
prefix) constructor to capture the returned llvm::ErrorCode (or std::error_code)
from createUniqueDirectory, assert or handle failure (e.g., assert(!ec, "Failed
to create temp directory") or log and abort) and ensure root is only used when
the call succeeds; reference TempDir, createUniqueDirectory, and root when
making the change.
- Around line 59-67: The touch() helper currently ignores filesystem errors and
may silently produce missing/empty files; update touch (and its use of path(),
llvm::sys::fs::create_directories and llvm::raw_fd_ostream) to detect and fail
on errors: check the result of create_directories and the std::error_code ec
from llvm::raw_fd_ostream, and if an error is present, fail the test (e.g., with
an assertion or throwing a test-fatal exception) including the path and
ec.message(); also ensure the output is properly flushed/closed after writing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 44c435ab-928a-4ae3-9d3a-90dbb4895da2

📥 Commits

Reviewing files that changed from the base of the PR and between f7a8d10 and 5ad5af7.

📒 Files selected for processing (22)
  • .github/workflows/benchmark.yml
  • CMakeLists.txt
  • benchmarks/scan_benchmark.cpp
  • src/command/command.cpp
  • src/command/command.h
  • src/command/search_config.cpp
  • src/command/search_config.h
  • src/command/toolchain_provider.cpp
  • src/command/toolchain_provider.h
  • src/server/master_server.cpp
  • src/server/master_server.h
  • src/support/path_pool.h
  • src/syntax/dependency_graph.cpp
  • src/syntax/dependency_graph.h
  • src/syntax/include_resolver.cpp
  • src/syntax/include_resolver.h
  • src/syntax/scan.cpp
  • tests/unit/compile/command_tests.cpp
  • tests/unit/syntax/dependency_graph_tests.cpp
  • tests/unit/syntax/include_resolver_tests.cpp
  • tests/unit/syntax/scan_tests.cpp
  • tests/unit/test/temp_dir.h

Comment thread src/command/command.cpp Outdated
Comment thread src/server/master_server.cpp Outdated
Comment thread src/syntax/dependency_graph.cpp
16bit-ykiko and others added 2 commits March 27, 2026 13:18
Introduce the search-config extraction pipeline that models clang's
four-segment include directory layout (Quoted → Angled → System → After),
a ToolchainProvider cache for `clang++ -###` queries, and a PathPool
intern table for lightweight file-path identity.  Also adds a shared
TempDir test helper and ExtractSearchConfig unit tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduce IncludeResolver for fast header-path resolution using
DirListingCache (readdir-based), DependencyGraph with BFS wavefront
scanning (Phase 1: parallel read+scan, Phase 2: include resolution),
and integrate scan_dependency_graph into MasterServer::load_workspace.
Adds benchmark harness and comprehensive unit tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@16bit-ykiko
16bit-ykiko force-pushed the feat/dependency-graph branch from 5ad5af7 to 30f974b Compare March 27, 2026 05:34

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (3)
tests/unit/syntax/dependency_graph_tests.cpp (1)

371-371: Consider using find() instead of operator[] to avoid silent insertion.

pool.cache[tmp.path("src/main.cpp")] will insert a default entry (value 0) if the key doesn't exist, which could mask test failures if the path wasn't properly interned during scanning. Using find() or lookup() would make the test fail explicitly if the path is missing.

Suggested improvement
-    auto includes = graph.get_includes(pool.cache[tmp.path("src/main.cpp")], 0);
+    auto it = pool.cache.find(tmp.path("src/main.cpp"));
+    ASSERT_TRUE(it != pool.cache.end());
+    auto includes = graph.get_includes(it->second, 0);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/syntax/dependency_graph_tests.cpp` at line 371, The test currently
uses pool.cache[tmp.path("src/main.cpp")] which will insert a default entry if
the key is missing; change the access to use a non-mutating lookup (e.g.,
pool.cache.find(...) or pool.lookup(...) depending on API) and assert that the
iterator/lookup result exists before calling graph.get_includes; specifically,
locate the call that constructs includes (the line calling graph.get_includes
with pool.cache[tmp.path("src/main.cpp")], 0) and replace the operator[] usage
with a find()/lookup and explicit failure/assertion if the path is not found so
the test fails loudly when the path wasn't interned.
benchmarks/scan_benchmark.cpp (2)

249-255: Prefer setenv() over putenv() for safer environment variable handling.

putenv() does not copy the string—it expects the passed buffer to remain valid and unmodified for the program's lifetime. While the static std::string ensures persistence, setenv() is safer and more portable as it copies the value internally.

Suggested fix
     // Set UV_THREADPOOL_SIZE if not already set.
     // Use at least libuv's default (4) so low-core CI runners don't regress.
     if(!std::getenv("UV_THREADPOOL_SIZE")) {
         auto pool_size = std::max(hw_threads, 4u);
-        static std::string env = "UV_THREADPOOL_SIZE=" + std::to_string(pool_size);
-        putenv(env.data());
+        setenv("UV_THREADPOOL_SIZE", std::to_string(pool_size).c_str(), 0);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@benchmarks/scan_benchmark.cpp` around lines 249 - 255, Replace the use of
putenv for setting UV_THREADPOOL_SIZE with setenv to avoid relying on
caller-owned buffers; after computing pool_size (from std::max(hw_threads, 4u))
call setenv("UV_THREADPOOL_SIZE", <stringified pool_size>, 1) so the C library
copies the value and you don’t need the static std::string env; remove the
static env variable and the putenv(env.data()) call and ensure you pass the
stringified pool_size as the second argument to setenv and overwrite any
existing value.

280-284: Guard against division by zero when CDB is empty.

If count is 0, unique_contexts.size() will also be 0, resulting in 0.0/0.0 which produces nan in the output.

Suggested fix
-        std::println("Context dedup: {} files -> {} unique contexts ({:.1f}x reduction)",
-                     count,
-                     unique_contexts.size(),
-                     static_cast<double>(count) / unique_contexts.size());
+        if(!unique_contexts.empty()) {
+            std::println("Context dedup: {} files -> {} unique contexts ({:.1f}x reduction)",
+                         count,
+                         unique_contexts.size(),
+                         static_cast<double>(count) / unique_contexts.size());
+        } else {
+            std::println("Context dedup: 0 files -> 0 unique contexts");
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@benchmarks/scan_benchmark.cpp` around lines 280 - 284, The print currently
divides by unique_contexts.size() which can be zero; modify the code around the
std::println call to guard the division by computing a safe reduction value
(e.g., double reduction = unique_contexts.empty() ? 0.0 :
static_cast<double>(count) / unique_contexts.size()) and then use that reduction
in the message (or print "N/A" when appropriate); refer to the variables count
and unique_contexts and the std::println invocation to locate where to make this
change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@benchmarks/scan_benchmark.cpp`:
- Around line 249-255: Replace the use of putenv for setting UV_THREADPOOL_SIZE
with setenv to avoid relying on caller-owned buffers; after computing pool_size
(from std::max(hw_threads, 4u)) call setenv("UV_THREADPOOL_SIZE", <stringified
pool_size>, 1) so the C library copies the value and you don’t need the static
std::string env; remove the static env variable and the putenv(env.data()) call
and ensure you pass the stringified pool_size as the second argument to setenv
and overwrite any existing value.
- Around line 280-284: The print currently divides by unique_contexts.size()
which can be zero; modify the code around the std::println call to guard the
division by computing a safe reduction value (e.g., double reduction =
unique_contexts.empty() ? 0.0 : static_cast<double>(count) /
unique_contexts.size()) and then use that reduction in the message (or print
"N/A" when appropriate); refer to the variables count and unique_contexts and
the std::println invocation to locate where to make this change.

In `@tests/unit/syntax/dependency_graph_tests.cpp`:
- Line 371: The test currently uses pool.cache[tmp.path("src/main.cpp")] which
will insert a default entry if the key is missing; change the access to use a
non-mutating lookup (e.g., pool.cache.find(...) or pool.lookup(...) depending on
API) and assert that the iterator/lookup result exists before calling
graph.get_includes; specifically, locate the call that constructs includes (the
line calling graph.get_includes with pool.cache[tmp.path("src/main.cpp")], 0)
and replace the operator[] usage with a find()/lookup and explicit
failure/assertion if the path is not found so the test fails loudly when the
path wasn't interned.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 47c9f6ee-4574-4c3a-adda-9c20655ff849

📥 Commits

Reviewing files that changed from the base of the PR and between 5ad5af7 and 30f974b.

📒 Files selected for processing (15)
  • .github/workflows/benchmark.yml
  • CMakeLists.txt
  • benchmarks/scan_benchmark.cpp
  • src/command/command.cpp
  • src/command/command.h
  • src/server/master_server.cpp
  • src/server/master_server.h
  • src/syntax/dependency_graph.cpp
  • src/syntax/dependency_graph.h
  • src/syntax/include_resolver.cpp
  • src/syntax/include_resolver.h
  • src/syntax/scan.cpp
  • tests/unit/syntax/dependency_graph_tests.cpp
  • tests/unit/syntax/include_resolver_tests.cpp
  • tests/unit/syntax/scan_tests.cpp
✅ Files skipped from review due to trivial changes (1)
  • src/syntax/include_resolver.h
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/syntax/scan.cpp
  • src/command/command.cpp
  • src/server/master_server.cpp
  • CMakeLists.txt
  • src/server/master_server.h
  • .github/workflows/benchmark.yml
  • src/syntax/dependency_graph.cpp

The blacklist approach (exclude discarded/user_content/codegen) let
warning flags, optimization levels, and other semantics-only options
into the toolchain cache key. On LLVM's CDB (9300 files, 1236 contexts)
this produced 53 unique keys instead of 6, causing 9x more
BuildCompilation calls during prewarm — 736ms vs 45ms on Linux,
and a timeout on Windows CI.

Restore the original whitelist (is_toolchain_option) that only includes
options affecting system path discovery: -target, -isysroot, --sysroot,
-stdlib=, -gcc-toolchain, -nostdinc, -std=, -x.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@benchmarks/scan_benchmark.cpp`:
- Around line 249-250: The code reads auto runs = *opts.runs and then calls
vector::reserve with that value which can wrap if runs is non-positive; validate
that runs > 0 immediately after reading opts.runs and return or error out if
not, then cast runs once to size_t (e.g. size_t runs_sz =
static_cast<size_t>(runs)) and use runs_sz for the subsequent reserve() calls
(also update the other places mentioned around lines 297-300 where reserve() is
called) to avoid negative-to-unsigned wraparound and huge allocations.

In `@src/command/argument_parser.cpp`:
- Around line 118-134: is_toolchain_option() currently omits flags (e.g.,
-resource-dir, -m32/-m64, -arch, -nobuiltininc, -nostdlibinc and any other
toolchain-affecting args) that must be included in the cache-key built by
extract_toolchain_flags() and read by query_toolchain_cached(); update
is_toolchain_option() to include these specific options or, better, replace the
hard-coded switch with logic that derives the whitelist from Clang's option
groups / the ToolChain implementation (i.e., consult the same option categories
used by Clang/ToolChain to decide which flags affect cc1 and resource/header
discovery) so all flags that affect computed cc1 args are consistently included
in the cache key (refer to is_toolchain_option, extract_toolchain_flags, and
query_toolchain_cached to locate and verify correctness).

In `@src/syntax/dependency_graph.cpp`:
- Around line 325-327: The visited-set usage is wrong: scanned_files currently
keys only by path_id so a header scanned for one config will be skipped for
other configs; change the visit/cache scheme to use an IncludeKey-style key that
contains path_id and config_id (and include found_dir_idx in the key if you need
exact `#include_next` semantics) so the graph traversal calls like
graph.set_includes(header, other_config, ...) get enqueued for each config; keep
the path-stable scan_results caching by path_id for lexed data but switch
scanned_files (and the similar logic around the other occurrence at lines
~604-606) to record visits by the composite IncludeKey instead of plain path_id.
- Around line 271-273: The include_cache currently maps to only path_id, which
loses found_dir_idx on warm hits; change the cache value type used at the
local_include_cache/ext_cache->include_cache site to store the full resolution
tuple (path_id plus found_dir_idx) — e.g., a small struct or packed uint64 — and
update all lookup/insert sites (the variables include_cache, the code that
enqueues into scanned_files and next_wave, and the other affected blocks around
the later include-resolution logic) to read both path_id and found_dir_idx from
the cache so warm hits reuse the original found_dir_idx (so `#include_next`
resumes from the correct search directory). Ensure producers write both values
into the cache and consumers extract both values when pushing to scanned_files
and next_wave.
- Around line 683-695: The public function scan_dependency_graph currently
constructs its own et::event_loop, schedules scan_impl and blocks on loop.run,
which blocks the caller's event loop; instead remove the private loop and expose
an asynchronous entry point: change scan_dependency_graph to not run a local
loop but either (A) accept an external et::event_loop or executor and schedule
scan_impl on it, returning immediately (or returning a future/promise that
completes with ScanReport), or (B) spawn the scan onto a background
thread/worker and invoke a provided callback with the ScanReport; update callers
(e.g., MasterServer::load_workspace) to schedule or await the returned future
rather than block, and keep symbols scan_dependency_graph, scan_impl, ScanReport
and the loop scheduling logic consistent while eliminating loop.run from the
public API.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 979afd29-6426-4854-ad04-2f9fb3aa79ba

📥 Commits

Reviewing files that changed from the base of the PR and between 30f974b and 6bc6a1c.

📒 Files selected for processing (7)
  • benchmarks/scan_benchmark.cpp
  • src/command/argument_parser.cpp
  • src/command/argument_parser.h
  • src/command/command.cpp
  • src/syntax/dependency_graph.cpp
  • src/syntax/dependency_graph.h
  • tests/unit/syntax/dependency_graph_tests.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/syntax/dependency_graph.h

Comment thread benchmarks/scan_benchmark.cpp
Comment thread src/command/argument_parser.cpp
Comment thread src/syntax/dependency_graph.cpp Outdated
Comment thread src/syntax/dependency_graph.cpp
Comment thread src/syntax/dependency_graph.cpp
Print unique canonicals, patches, directories, and sample entries
to help diagnose macOS dedup regression (9894/9895 unique contexts
vs 1316/9882 on Ubuntu).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (4)
benchmarks/scan_benchmark.cpp (4)

388-392: Lambda mutates input vector as a side effect.

The stats lambda sorts the input vector in-place. While the vectors aren't used afterward, mutating inputs through a reference parameter is a subtle side effect that could cause issues if the code evolves.

♻️ Take by value to avoid mutation
-        auto stats = [](std::vector<std::int64_t>& v) {
+        auto stats = [](std::vector<std::int64_t> v) {
             std::ranges::sort(v);
             auto sum = std::accumulate(v.begin(), v.end(), std::int64_t{0});
             return std::tuple{v.front(), sum / static_cast<std::int64_t>(v.size()), v.back()};
         };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@benchmarks/scan_benchmark.cpp` around lines 388 - 392, The stats lambda
currently takes its parameter by reference and sorts the vector in-place,
causing an unintended side effect; change the signature of the stats lambda to
take the vector by value (e.g., std::vector<std::int64_t> v) so the sort and
subsequent accumulate operate on a copy and do not mutate the caller's data,
leaving the rest of the logic in the lambda (std::ranges::sort, std::accumulate,
returning min/avg/max) unchanged.

254-258: putenv usage is correct but consider setenv for clarity on POSIX.

The static std::string ensures the buffer persists for the process lifetime, which putenv requires. However, putenv stores the pointer directly and callers may modify the buffer through subsequent putenv calls on the same variable.

For a benchmark tool this is acceptable. On POSIX systems, setenv("UV_THREADPOOL_SIZE", std::to_string(pool_size).c_str(), 0) would be cleaner as it copies the value internally, but putenv works cross-platform.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@benchmarks/scan_benchmark.cpp` around lines 254 - 258, Replace the current
putenv-based UV_THREADPOOL_SIZE setting to use POSIX setenv when available:
instead of relying on static std::string env and putenv(env.data()), call
setenv("UV_THREADPOOL_SIZE", std::to_string(pool_size).c_str(), 0) (or use an
`#ifdef` to fall back to the existing putenv approach on non-POSIX platforms).
Update the logic around getenv, pool_size, and env so the value is copied
internally (avoiding reliance on env.data() lifetime) and only use putenv as the
fallback.

279-302: Pointer-based uniqueness relies on string interning; comment is misleading.

Line 293 says "Count unique patches by hashing their content," but the code compares const char* pointers, not string contents. This works correctly only if the compilation database interns all strings (same content → same address).

If interning is incomplete, identical strings at different addresses will be counted as distinct, inflating the unique count. For diagnostic purposes this may be acceptable if you're specifically measuring interning effectiveness. Otherwise, consider string-based comparison.

♻️ Content-based comparison if needed
-        std::set<const char*> unique_dirs;
+        std::set<std::string_view> unique_dirs;
         ...
-        // Count unique patches by hashing their content.
-        std::set<std::vector<const char*>> unique_patches;
+        // Count unique patches by their string content.
+        std::set<std::vector<std::string_view>> unique_patches;
         for(auto& entry: cdb.get_entries()) {
             unique_patches.insert(
-                std::vector<const char*>(entry.info->patch.begin(), entry.info->patch.end()));
+                std::vector<std::string_view>(entry.info->patch.begin(), entry.info->patch.end()));
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@benchmarks/scan_benchmark.cpp` around lines 279 - 302, The current
unique_patches logic collects std::vector<const char*> (from entry.info->patch)
which only compares pointer identities (string interning assumption) rather than
content; replace the pointer-based set with a content-based representation
(e.g., build std::vector<std::string> or std::string_view-backed vector for each
entry or compute a combined hash of the concatenated patch strings) and insert
that into unique_patches so equality is determined by actual patch text; update
the unique_patches declaration and the insert call that references
entry.info->patch to construct and store content-based vectors instead of
vectors of const char*.

92-95: Consider adding bounds validation for raw_id.

The raw_id is extracted by masking with PATH_ID_MASK (31 bits). While scan_dependency_graph should only produce valid IDs, a corrupted graph entry could cause out-of-bounds access. For a benchmark tool this is acceptable, but a defensive check would improve robustness.

🛡️ Optional defensive check
         for(auto flagged_id: inc_ids) {
             auto raw_id = flagged_id & DependencyGraph::PATH_ID_MASK;
+            if(raw_id >= path_pool.paths.size()) {
+                std::println(stderr, "Warning: invalid path_id {} in graph", raw_id);
+                continue;
+            }
             node.includes.push_back(path_pool.paths[raw_id].str());
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@benchmarks/scan_benchmark.cpp` around lines 92 - 95, The loop extracts raw_id
by masking with DependencyGraph::PATH_ID_MASK and then indexes
path_pool.paths[raw_id] without verifying raw_id is within
path_pool.paths.size(); add a defensive bounds check (e.g., if(raw_id >=
path_pool.paths.size()) continue or log+continue) before pushing into
node.includes to avoid out-of-bounds access in case of corrupted entries; update
the loop around inc_ids where raw_id is computed and use the guarded access to
path_pool.paths and its .str() call.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@benchmarks/scan_benchmark.cpp`:
- Around line 388-392: The stats lambda currently takes its parameter by
reference and sorts the vector in-place, causing an unintended side effect;
change the signature of the stats lambda to take the vector by value (e.g.,
std::vector<std::int64_t> v) so the sort and subsequent accumulate operate on a
copy and do not mutate the caller's data, leaving the rest of the logic in the
lambda (std::ranges::sort, std::accumulate, returning min/avg/max) unchanged.
- Around line 254-258: Replace the current putenv-based UV_THREADPOOL_SIZE
setting to use POSIX setenv when available: instead of relying on static
std::string env and putenv(env.data()), call setenv("UV_THREADPOOL_SIZE",
std::to_string(pool_size).c_str(), 0) (or use an `#ifdef` to fall back to the
existing putenv approach on non-POSIX platforms). Update the logic around
getenv, pool_size, and env so the value is copied internally (avoiding reliance
on env.data() lifetime) and only use putenv as the fallback.
- Around line 279-302: The current unique_patches logic collects
std::vector<const char*> (from entry.info->patch) which only compares pointer
identities (string interning assumption) rather than content; replace the
pointer-based set with a content-based representation (e.g., build
std::vector<std::string> or std::string_view-backed vector for each entry or
compute a combined hash of the concatenated patch strings) and insert that into
unique_patches so equality is determined by actual patch text; update the
unique_patches declaration and the insert call that references entry.info->patch
to construct and store content-based vectors instead of vectors of const char*.
- Around line 92-95: The loop extracts raw_id by masking with
DependencyGraph::PATH_ID_MASK and then indexes path_pool.paths[raw_id] without
verifying raw_id is within path_pool.paths.size(); add a defensive bounds check
(e.g., if(raw_id >= path_pool.paths.size()) continue or log+continue) before
pushing into node.includes to avoid out-of-bounds access in case of corrupted
entries; update the loop around inc_ids where raw_id is computed and use the
guarded access to path_pool.paths and its .str() call.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c81358bb-cb6b-4cb1-8062-98b279360819

📥 Commits

Reviewing files that changed from the base of the PR and between 6bc6a1c and c9a7b34.

📒 Files selected for processing (1)
  • benchmarks/scan_benchmark.cpp

Show full per-arg diff between two entries sharing the same canonical
but different patches, to identify what causes per-file uniqueness
on macOS/Windows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
benchmarks/scan_benchmark.cpp (2)

92-95: Consider adding bounds check for raw_id before array access.

While raw_id should always be valid if the graph was built correctly with the same PathPool, a defensive bounds check prevents crashes from data corruption or logic bugs elsewhere.

🛡️ Suggested defensive check
         for(auto flagged_id: inc_ids) {
             auto raw_id = flagged_id & DependencyGraph::PATH_ID_MASK;
+            if(raw_id >= path_pool.paths.size()) {
+                std::println(stderr, "Warning: invalid include id {} in graph", raw_id);
+                continue;
+            }
             node.includes.push_back(path_pool.paths[raw_id].str());
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@benchmarks/scan_benchmark.cpp` around lines 92 - 95, The loop that extracts
raw_id from flagged_id using DependencyGraph::PATH_ID_MASK then accesses
path_pool.paths[raw_id] is unsafe; add a defensive bounds check on raw_id
against path_pool.paths.size() before pushing to node.includes to avoid
out-of-range access. Locate the loop that iterates over inc_ids and the
variables flagged_id, raw_id, node.includes and if raw_id is invalid either skip
that entry (and optionally log or count the corruption) or push a safe
placeholder, ensuring no direct access to path_pool.paths[raw_id] when raw_id >=
path_pool.paths.size().

254-258: putenv is POSIX-specific; consider setenv or cross-platform alternative.

putenv works on POSIX systems but Windows uses _putenv or _putenv_s. Since this benchmark might run on Windows (mentioned in PR objectives for macOS/Windows path uniqueness), consider using a cross-platform approach.

🔧 Cross-platform alternative
     if(!std::getenv("UV_THREADPOOL_SIZE")) {
         auto pool_size = std::max(hw_threads, 4u);
-        static std::string env = "UV_THREADPOOL_SIZE=" + std::to_string(pool_size);
-        putenv(env.data());
+#ifdef _WIN32
+        _putenv_s("UV_THREADPOOL_SIZE", std::to_string(pool_size).c_str());
+#else
+        setenv("UV_THREADPOOL_SIZE", std::to_string(pool_size).c_str(), 0);
+#endif
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@benchmarks/scan_benchmark.cpp` around lines 254 - 258, The code uses putenv
to set UV_THREADPOOL_SIZE which is POSIX-specific; replace it with a
cross-platform branch that uses setenv on POSIX and _putenv_s on Windows: build
the pool_size string as before, then inside an `#ifdef` _WIN32 use
_putenv_s("UV_THREADPOOL_SIZE", value.c_str()); else use
setenv("UV_THREADPOOL_SIZE", value.c_str(), 1); (remove the static env hack) and
include the needed headers; modify the block around pool_size/putenv to use
these platform-specific calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@benchmarks/scan_benchmark.cpp`:
- Around line 269-275: Check the result of CompilationDatabase::load (the
variable count) after calling cdb.load(cdb_path) and bail out if it indicates
failure (e.g., count == 0) instead of continuing; update the code around
CompilationDatabase, cdb.load, cdb_path and the subsequent std::println call to
detect an empty/failed load, print a clear error mentioning cdb_path and the
load failure, and exit or return a non-zero status so the benchmark does not run
on an empty database.

---

Nitpick comments:
In `@benchmarks/scan_benchmark.cpp`:
- Around line 92-95: The loop that extracts raw_id from flagged_id using
DependencyGraph::PATH_ID_MASK then accesses path_pool.paths[raw_id] is unsafe;
add a defensive bounds check on raw_id against path_pool.paths.size() before
pushing to node.includes to avoid out-of-range access. Locate the loop that
iterates over inc_ids and the variables flagged_id, raw_id, node.includes and if
raw_id is invalid either skip that entry (and optionally log or count the
corruption) or push a safe placeholder, ensuring no direct access to
path_pool.paths[raw_id] when raw_id >= path_pool.paths.size().
- Around line 254-258: The code uses putenv to set UV_THREADPOOL_SIZE which is
POSIX-specific; replace it with a cross-platform branch that uses setenv on
POSIX and _putenv_s on Windows: build the pool_size string as before, then
inside an `#ifdef` _WIN32 use _putenv_s("UV_THREADPOOL_SIZE", value.c_str()); else
use setenv("UV_THREADPOOL_SIZE", value.c_str(), 1); (remove the static env hack)
and include the needed headers; modify the block around pool_size/putenv to use
these platform-specific calls.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6565571b-c2eb-4b25-ac7e-ecbb9026269e

📥 Commits

Reviewing files that changed from the base of the PR and between c9a7b34 and d73743d.

📒 Files selected for processing (1)
  • benchmarks/scan_benchmark.cpp

Comment thread benchmarks/scan_benchmark.cpp
16bit-ykiko and others added 13 commits March 27, 2026 16:22
On macOS, source file paths starting with /Users/... are
misinterpreted by clang's option parser as the MSVC-style /U
(undefine) flag. This caused the source file path to leak into
per-file patch args, making every CompilationInfo unique and
destroying context deduplication (9895 unique contexts instead
of ~1300).

Detect the misparse: if an option with "/" prefix + its value
reconstructs to an absolute path, discard it as a file path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…lity mask

On macOS/Linux, clang's option parser interprets Unix absolute paths
like /Users/... as MSVC-style /U sers/... (undefine macro), breaking
context deduplication (9895/9896 unique contexts instead of ~1300).

Use ParseOneArg's Visibility parameter to exclude CLOption-tagged
options when the driver is not cl.exe. This is the proper solution
instead of the hacky path-reconstruction check.

Also clean up verbose diagnostic code in scan_benchmark.cpp.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Temporarily dump canonical command details when dedup is poor (>200
unique canonicals) to identify what per-file arg leaks into canonical
on Windows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
clang-cl.exe needs CLOption visibility just like cl.exe, otherwise
MSVC-style /TP, /EHs, /Oi options get misparsed and leak per-file
garbage into canonical commands, breaking dedup on Windows (9811
unique canonicals instead of ~76).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Skip .rc (Windows resource), .asm, .def and other non-C-family
entries that build systems like CMake emit into compile_commands.json.
Uses clang::driver::types to check file extensions.

This removes ~450 cmake_llvm_rc and ml64 entries on Windows that
were polluting the canonical dedup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
On Windows with clang-cl, /Fd (PDB path) varies per-target and --
is parsed as OPT_UNKNOWN. Both leak into canonical, breaking dedup.

Also discard /Fo (MSVC output file) for consistency with -o.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
On Windows with clang-cl, CMake generates compile_commands.json entries
ending with `-- <source-file>`. The `--` option is parsed as
OPT__DASH_DASH (KIND_REMAINING_ARGS), consuming the source file path
as its value. Both leaked into the canonical command, making every
entry unique (1.0x dedup, 9807 unique canonicals out of 9812 entries).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Deduplicate config_id in DependencyGraph::set_includes to prevent
  accumulation on repeated scans
- Store found_dir_idx in include_cache (CachedInclude struct) so that
  #include_next resolves correctly for cache-hit headers
- Validate --runs > 0 in benchmark to prevent reserve() overflow
- Add ".\\" check in needs_normalize for Windows #include paths

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the same header is included across multiple configs — unconditionally
in one, conditionally in another — the merge now correctly strips the
CONDITIONAL_FLAG. Previously, whichever config was iterated first won,
producing non-deterministic results.

Also add TODOs for future improvements:
- ScanCache generation counter for safe partial invalidation
- DirListingCache per-directory invalidation and case-insensitive FS note
- pop_back() assertion for temp source file invariant
- Test coverage: circular includes, warm ScanCache, #include_next edge cases

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…-by-zero

- default_visibility: strip trailing version digits (e.g. clang-cl-17)
  before checking for cl/clang-cl, matching driver_family() logic.
  Previously versioned clang-cl drivers lost all MSVC-style options.
- resolve_dir: log readdir failures at DEBUG level instead of silently
  caching an empty directory listing.
- benchmark: guard against division by zero when CDB has no entries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Log dependency scan summary (timing, file counts, edge counts,
resolution accuracy) at INFO level during workspace load.
Warn when there are unresolved includes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@16bit-ykiko
16bit-ykiko merged commit f8a3914 into main Mar 28, 2026
17 checks passed
@16bit-ykiko
16bit-ykiko deleted the feat/dependency-graph branch March 28, 2026 09:40
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