Skip to content

Suppress the unscanned-compile report only for a compile that scans - #409

Merged
typeless merged 1 commit into
mainfrom
wip/357-scannable-prefix-dep-flags
Aug 17, 2026
Merged

Suppress the unscanned-compile report only for a compile that scans#409
typeless merged 1 commit into
mainfrom
wip/357-scannable-prefix-dep-flags

Conversation

@typeless

Copy link
Copy Markdown
Owner

What was wrong

The unscanned-compile diagnostic exists to say when a compile's headers go unrecorded. Its
suppression — "this command writes its own depfile, so it needs no scan" — asked whether a depfile
flag appeared anywhere in the rule's command text, and was consulted per rule, before any scan
was built.

So the diagnostic was defeatable by placement rather than meaning. Any word spelling a depfile
flag silenced the report for every object the rule declared, and the failure was double-silent: no
scan, no depfile, and no report that either was missing.

The part the issue did not anticipate

Re-spelling the predicate over the scannable prefix closes the trailing and empty-prefix vectors,
but leaves the bug alive in miniature. scannable_prefix deliberately continues past
scan-transparent invocations (echo, true, :) so the walk can reach a later compile;
build_dep_scans then skips exactly those, for want of a compiler index. Reading the prefix alone
therefore still counts a flag that no scan could ever have been built from:

: a.c |> echo -MD && gcc -c a.c -o a.o && cp a.o b.o |> a.o b.o

answered "carries a depfile flag" — a program that cannot write a depfile suppressing a compile that
carries no flag. b.o is left uncovered and unreported.

Both predicates now gate on the same condition build_dep_scans gates on: the invocation must be
the compile itself. The two read the identical set of invocations, so they cannot disagree.

Also in this change

The word test moves from the command text to the tokenized words. Matching -M inside the text
required a manual check that the preceding character was a space; over words that is the
tokenizer's job, and dropping it is what retires the string_utils include here. clang_cl gets
the same shape, so the two scanners state the rule the same way rather than in two spellings.

Spec

REQ-SCAN-REPORT-UNSCANNED now reads "no invocation a scan would have been built from carries a
depfile flag" in place of "carries no depfile flag anywhere in its text", and the section's "two
classes escape the report" is down to one — the compile-and-link case, which is unrelated and
still open.

Three discharge tests, pinning both directions: a flag outside the scannable prefix suppresses
nothing, a flag inside it still suppresses, and an E2E scenario for the object left uncovered.

Verification

RED quoted for every leg, including the three second-round tests that failed against the first fix.
The E2E leg was authored after that fix, so it was proved discriminating by restoring the
pre-tightening scanner, observing the failure, and putting it back — verified by sha256, with no
git checkout and no stash.

Gate Result
build 0
format 0
tidy 0
iwyu 0
spec-check 0
make test 0 carried onto the #397 base (838 cases, 32 shards)

On a main base make test reports the pre-existing test_builder.cpp failure from a read-only
/tmp on the development machine — the #397 defect, fixed by PR #406, in a file this change does
not touch. CI runners have a writable /tmp.

Fixes #357

The suppression asked whether a depfile flag appeared anywhere in the rule's
command text. It was consulted per rule, before the scans were built, so any
word spelling a depfile flag silenced the report for every object the rule
declared — including a word in an invocation that no scan would ever have
been built from.

That made the diagnostic defeatable by placement rather than by meaning, and
the failure was double-silent: no scan, no depfile, and no report that either
was missing. The tool whose one job is to say when headers go unrecorded had a
blind spot a stray word could open.

Both scanners now ask the question over the invocations build_dep_scans draws
from, gated the way build_dep_scans gates them: the invocation must be the
compile itself. Reading only the scannable prefix is not sufficient on its own
— the prefix deliberately continues past scan-transparent invocations so the
walk can reach a later compile, and build_dep_scans then skips exactly those
for want of a compiler index. Without the compiler gate,

    echo -MD && gcc -c a.c -o a.o && cp a.o b.o

still answered true: a program that cannot write a depfile suppressing a
compile that carries no flag, leaving b.o uncovered and unreported. The two
predicates now read the identical set of invocations, so they cannot disagree.

The word test moves with it. Matching -M inside the command text needed a
manual check that the preceding character was a space; over tokenized words
that check is the tokenizer's job and is gone, which is what retires the
string_utils include here.

The requirement said "carries no depfile flag anywhere in its text" and now
says "no invocation a scan would have been built from carries a depfile flag",
and the two classes that escaped the report are down to one.

Verified: RED quoted for every leg, including the three second-round tests that
failed against the first fix. The E2E leg authored after the fix was proved
discriminating by restoring the pre-tightening scanner, observing the failure,
and putting it back, verified by sha256. build, format, tidy, iwyu, spec-check
all exit 0; make test is green when carried onto the #397 base and fails on a
main base only at the pre-existing read-only-/tmp failure in test_builder.cpp,
which this change does not touch.

Fixes #357
@github-actions

Copy link
Copy Markdown

PR metrics

Performance (gcc example, Linux)

Workload Instructions CPU time Page faults D1 miss LL miss Wall Peak RSS
parse 1693 M (-0.3%) 0.66 s 14.9 k 0.7% 0.1% 0.654 s 33.8 MB (-0.2MB)
dry-run 2308 M (-2.5%) 0.74 s 16.3 k 0.8% 0% 0.745 s 39 MB

Deterministic signals: instructions (cachegrind-simulated instruction reads — exact across runs, no PMU needed), page faults, peak RSS, and the cachegrind D1/LL miss rates. CPU time is user+sys from time(1).

Internal statistics (gcc example, up-to-date dry run)

Metric Value
Tupfiles parsed 24
Commands 3545
Commands scheduled 0
Files checked 5834
Files changed 0
Files in index 6102
Graph edges 381415
Index size (bytes) 7683630
Implicit deps 344126
Hash computations 0 (-100.0%)
Hashes skipped (stat cache) 5833 (+3.5%)
Stat calls 5885
Parse time (ms) 567.1 (+1.6%)
Total time (ms) 728.4 (+1.6%)
Runner CPU AMD EPYC 7763 64-Core Processor

Counters from putup -n --stat on the fully-built gcc example (up-to-date dry run): deterministic work measures — a jump in commands scheduled, hash computations, or stat calls is a real behavior change, not noise. Timings are the minimum over repeated runs, compared only against a baseline from the same CPU model; the counters are the regression signal.

Binary size (Linux)

Binary .text .data .bss File
putup 565.7 KB (+0.1%) 2.3 KB 98.7 KB 671 KB

Code churn (whole codebase, last 30d)

Files Lines written Still present Churned Churn rate
114 9622 7960 1662 17.3%

Of the lines written across the codebase in the last 30 days, how many are already gone — work that was written and then discarded or rewritten inside the same window. This is the state of the tree including this PR, not a measure of the PR itself. Only code we write is counted: tests, examples, vendored and generated files, CI plumbing and prose are excluded. 4371 lines were deleted in the window in total, most of them older than it.

Where the churn is
File Lines written then discarded
src/cli/cmd_build.cpp 592
src/graph/scanners/clang_cl.cpp 297
src/graph/scanners/gcc.cpp 116
src/cli/context.cpp 82
src/core/layout.cpp 69
src/graph/scanners/dep_words.cpp 64
src/graph/builder.cpp 62
src/platform/file_io-posix.cpp 42
src/cli/strict_checks.cpp 36
src/cli/cmd_clean.cpp 35

Test coverage (lines)

Overall Median file Min file Max file
88.1% 96.5% (+0.1pp) 14.7% include/pup/parser/token.hpp 100.0% include/pup/core/arena.hpp

102 files · 16608/18845 lines covered

Deltas vs main@26ddb6105.

Updated for 4ff2d32

@typeless
typeless merged commit 6b09482 into main Aug 17, 2026
13 checks passed
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.

reports_own_deps tests the whole command text, so an unrelated -MD word silences the unscanned-compile diagnostic

1 participant