Skip to content

Dep scan forfeits a whole command when a harmless prefix precedes the compiler #352

Description

@typeless

Found during the #345 investigation. Rewritten 2026-08-12: the original title and body said the refusal was silent, and described the fix as skipping leading separators in a matcher that read word_ids[0]. Both are stale — see "What changed" below. The defect that remains is the narrowness of recognition, unchanged in substance since filing.

What remains

putup forfeits the dependency scan for an entire command when it cannot recognize every one of that command's invocations as a compile it can reproduce. A harmless prefix — one that provably cannot change what the compile reads — costs the rule all of its header dependencies.

The live shapes, all reproducible today:

: hidden.c |> echo building %f && gcc -c %f -o %o |> %B.o   # test-pinned, see below
: foo.c    |> FOO=1 gcc -c %f -o %o |> %B.o                  # env assignment before the compiler
: foo.c    |> ; gcc -c %f -o %o |> %B.o                      # empty leading invocation

None of these can affect what the preprocessor reads, and all three forfeit the scan.

Current mechanism

every_invocation_is_a_compile (src/graph/scanners/gcc.cpp:126-134) requires compiler_index to succeed on every invocation. compiler_index (gcc.cpp:109-121) accepts the compiler at position 0, or behind exactly one recognized wrapper — nothing else. echo, FOO=1, and the empty span produced by a leading separator all fail it.

split_invocations (src/graph/scanners/dep_words.cpp:135-151) emits an empty invocation for an operator with nothing before it, and its comment asserts that this "is a command no scan can reproduce". That assertion is the narrow half of this issue stated as a code comment: an invocation that runs nothing cannot change the directory, the environment, or what a compile reads.

matches_clang_cl_compile carries the identical shape in driver_index (src/graph/scanners/clang_cl.cpp:121-134) and must move in lockstep.

The fix, as 276baf2 scoped it

The matcher fix — recognizing a compile behind a harmless prefix — is tracked in #352 and lands next, as an allowlist of prefix words that provably cannot change what the compile reads.

Still the right shape, with one update: the allowlist now belongs inside compiler_index, applied per invocation, rather than at the head of the whole command. The empty-invocation case is separable and simpler — an invocation with no words runs nothing, so it can be skipped rather than allowlisted.

The allowlist is convention-lagging by construction: a prefix nobody has added is a rule that still forfeits its scan. That is acceptable only because the lag is now loud (below), and the diagnostic is what measures it.

What changed since filing

The refusal is declared, not silent. 276baf2ee shipped the reporter. putup parse names each rule producing an object no scan covers —

no dependency scan for '<obj>': putup recognizes no compile it can reproduce in this command, so any header this rule reads goes unrecorded. Harmless if it preprocesses none — an assembler, a copy, a partial link.
src/cli/strict_checks.cpp:195-198

— and a build prints a one-line count (src/cli/cmd_build.cpp:2206-2212). Pinned by Scenario: A compile-shaped rule with no dependency scan is reported (test/unit/test_e2e.cpp:11162), whose fixture uses the echo … && gcc -c shape above. So the 103 objects are declared, not hidden; the harm is the missing dependency edges, not the concealment.

The criterion moved and got stricter, not wider. PR #400 (2bf0cce0c, 0e7907602, closing #356) made invocations a nameable unit and asks the criterion once per invocation through one enforcement point. It deliberately narrowed what is scanned — a command is scanned only if all of its invocations are recognized compiles — because a scan reproduces one invocation from the rule's directory with the rest stripped, so anything it cannot account for could have changed the state the compile ran in. This issue's fix widens recognition within that criterion; it does not relax it.

The 103 figure is stale. It was measured before #400 tightened the criterion, so the current count is at least that and probably higher. Re-measure with putup parse before using it as a baseline.

Boundaries

Recognition narrowness (this issue) is not the same defect as scan granularity. #355 is that a recognized multi-invocation command folds every invocation's sources under the first invocation's flags, which redirects rather than narrows the header search. Its full fix — one scan node per compile invocation — is being filed separately as its own issue and is gated on widening the unscanned reporter, not on this.

Related: #355 (granularity), #356 (closed by #400, the criterion), #357 (self-depfiling commands the reporter skips).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions