--lang c++ explicitness not threaded to remaining fan-out callers (l0_export_delta/appcompat); dump/compare fixed¶
Index: Known gaps
Status (re-verified 2026-10-09 at 456989f): PARTIAL. Closed for dump/compare CLI and the DumpRequest/CompareRequest API. lang_explicit is threaded through frontends/cli/commands/dump.py, compare.py, workflows/dump/{native,pe,macho}.py and workflows/artifact/*, and release_member_request.py now carries it too. The scan follow-up is moot (scan was deleted). appcompat and l0_export_delta are unverified. l0_export_delta.py takes a plain lang: str with no lang_explicit, so that follow-up looks still open.
cli_dump_helpers.py's ELF dump path (and service.py's two mirrors,
_dump_elf/PE-Mach-O's _header_graph_lang) all normalize the caller's
lang to lang if lang == "c" else None before calling dumper.dump() —
deliberately, per their own comment: "every format's own main pass
normalizes lang to only ever force a language explicitly requested,
letting auto-detection run otherwise (including for the default 'c++')",
so _attach_header_graph's own _clang_header_dump call computes the
identical cache key and hits the in-process AST memo instead of paying a
second clang invocation. That assumption — "an explicit --lang c++ and
auto-detection converge on the same result anyway, so unifying their cache
keys is free" — is false whenever the header itself is language-ambiguous:
_resolve_force_cpp's auto-detection (_detect_cpp_headers/
_detect_cpp20_headers/an explicit -std=c++NN) requires the header to
contain some C++-only syntax, but a plain POD struct with no such syntax
(struct Widget { int x; int y; }; — ordinary, real C++ code, e.g. a
value/DTO type) compiles as valid C too and auto-detects as C. Reproduced
end-to-end: abicheck dump lib.so -H widget.h --ast-frontend clang --lang
c++ silently parses widget.h in C mode for the primary snapshot
(confirmed via the raw clang AST: a plain RecordDecl, not
CXXRecordDecl, no definitionData at all) — with no error, warning, or
any user-visible sign the explicit --lang c++ was overridden — while the
same dump's internal _attach_header_graph pass, reached through a
different lang derivation one call removed
(abicheck/service.py:1281/abicheck/service.py:608's ELF branch shares
the identical lang if lang == "c" else None squash, so it too loses the
signal — the divergence traced here is specifically the ELF _dump_elf
helper's own second, ADR-050/G31-era call site,
abicheck/cli_dump_helpers.py:1717, versus _clang_header_dump's
documented "an explicit --lang c++/cpp always wins" contract at the
function it's calling into — the squash happens one layer above that
contract, defeating it before it ever runs). Concrete, silent correctness
cost: RecordType.is_standard_layout/is_trivially_copyable (and any
other clang-only, C++-semantic-only fact — _clang_record_type_traits
itself documents "a plain C RecordDecl... genuinely absent", the correct
behavior for C mode, just not the mode the user asked for) silently read
None instead of a real value for a header that would parse identically
either way except for these C++-only facts, with the user's own explicit
override having no effect. Not fixed here: the squashing is not an
oversight — it is a deliberate, twice-Codex-reviewed cache/memo-consistency
design (see abicheck/service.py:608's own comment) that this finding does
not invalidate for the common case (a header with real C++ syntax
auto-detects as C++ regardless, so squashing costs nothing there) — only
for the specific, real, silently-wrong case: an explicit --lang c++ on a
syntactically-ambiguous header. A correct fix needs the squash itself to
stop conflating "auto-detect, and it happens to reach the same verdict as
the default" with "explicitly override, must not be silently downgraded to
a guess" — e.g. resolving force_cpp once, upstream of all three call
sites and their _attach_header_graph mirrors, and threading the
resolved boolean (or an unsquashed lang plus a corrected memo key that
hashes the resolved language rather than the raw flag) through consistently
— a change to a shared cache-key contract three call sites and two
Codex-reviewed comments currently rely on, not a one-line fix at any single
site. No test in the repository currently exercises this path: every
existing is_standard_layout/is_trivially_copyable regression test
(tests/test_dumper_clang.py::test_parse_types_populates_standard_layout_and_trivially_copyable,
tests/test_diff_layout.py) hand-builds the parsed AST or RecordType
directly, bypassing dumper.dump()'s CLI-level --lang resolution
entirely — closing this gap should add an end-to-end regression case in
the shape of tests/test_clang_header_backend_integration.py's existing
siblings, verified against a real compiled library with an
intentionally-C-compatible C++ struct, once the fix itself is designed.
Closed for the abicheck dump ELF CLI path — scoped narrower than the
"resolve force_cpp once, upstream of all three call sites" design sketch
above (G31 continuation). Rather than a shared, force_cpp-boolean
cache-key contract spanning cli_dump_helpers.py, service.py's
_dump_elf, and service.py's PE/Mach-O _header_graph_lang, the actual
fix is narrower: only cli_dump_helpers.py's ELF dump CLI path had a
real internal divergence between its own primary pass (squashed) and its
own _attach_header_graph call (raw, unsquashed) — service.py's
_dump_elf and its _header_graph_lang computation already squash
consistently with each other (both feed the identical normalized value),
so compare's implicit-dump path and the Python service.run_dump API
were never the site of this specific divergence; PE/Mach-O's own primary
pass (_try_header_scoped_dump) never squashed at all. What was missing
everywhere is the one bit no string-normalization scheme can recover on
its own: whether --lang was genuinely given on the command line, since
Click's own default for --lang (LANG_DEFAULT, cli_options.py) is the
identical string "c++" a real --lang c++ produces. dump_cmd now
resolves this once via Click's own parameter-source tracking
(click.get_current_context().get_parameter_source("lang") ==
click.core.ParameterSource.COMMANDLINE) and threads a new
lang_explicit: bool keyword parameter through perform_elf_dump, which
derives one _effective_lang (the real lang when explicit or "c",
else None) and passes that identical value to both the primary
dump() call and the _attach_header_graph call, instead of the primary
pass's own one-off squash and the graph pass's raw pass-through. This also
fixes a second, previously-undocumented half of the same divergence in the
other direction: on a plain default invocation (no --lang, still
lang="c++"), the header-graph pass previously force-parsed C++
unconditionally (since a non-empty lang was always treated as explicit
by _resolve_force_cpp), while the primary pass correctly auto-detected —
so even a default, no-flags dump --ast-frontend clang could already
silently disagree with itself between its own primary snapshot and its
own embedded header-graph. Verified end-to-end against a real compiled
library with an intentionally-C-compatible POD struct (struct Widget {
int x; int y; };, exactly this entry's own repro shape) through the real
abicheck dump CLI, not a hand-built AST or RecordType — see
tests/test_clang_header_backend_integration.py::
test_cli_dump_explicit_lang_cpp_forces_cpp_mode_on_ambiguous_header.
Closed for service.run_dump/DumpRequest/CompareRequest in a later
pass, once a real conda-forge castxml build (0.7.0, within the
>=0.6.11,<0.8.0 policy range — castxml_policy.py) was available in this
environment to verify against, alongside clang 18. Rather than the
lang: str | None = None tri-state default this entry originally
sketched — a public-API shape change with a wide, hard-to-verify blast
radius across every resolve_input/run_dump caller (compare, scan,
appcompat, l0_export_delta, ...), most of which still legitimately pass
a concrete, Click-defaulted lang string that must keep auto-detecting —
the actual fix is the same additive lang_explicit: bool = False
parameter the CLI fix above already established, generalized one layer
up: service.resolve_input/run_dump/_dump_elf/_dump_pe/
_dump_macho and service_header_scoped._try_header_scoped_dump all gain
it (default False, a no-op — every existing caller's behavior is
bit-for-bit unchanged), DumpRequest.lang_explicit/
CompareRequest.lang_explicit carry it on the typed API, and
service_dump_pipeline.run_dump_request/service_compare_pipeline.
resolve_compare_request thread it through resolve_side_snapshot (their
one shared per-side resolution function) to service.resolve_input. The
dump/compare CLIs resolve it the identical way dump_cmd already did
— compare_cmd mirrors the established _frontend_explicit/
_nostdinc_explicit ctx.get_parameter_source(...)==COMMANDLINE pattern
already used one function over in cli_compare_helpers._embed_inline_
source_side for --ast-frontend/--nostdinc, extended to --lang and
threaded through cli_resolve._resolve_compare_snapshots into
CompareRequest. The whole-snapshot disk cache key
(workflows/dump/cache/key.py's _dump_cache_extra_key/api.py's cached_run_dump) folds
lang_explicit in too — the identical lang string now legitimately
resolves to two different parsed ASTs depending on it, so a cache entry
for one must never serve the other. scan/appcompat/the
release/set-input fan-out/l0_export_delta are not touched by this
pass — they still pass their own already-resolved, Click-defaulted lang
string with lang_explicit defaulted False, so they keep their
pre-existing (unfixed, but also not regressed) behavior; wiring each is
the identical mechanical pattern applied here, left for its own follow-up
rather than expanding this pass's verified surface further. Verified
end-to-end against the same real, intentionally-C-compatible POD struct
through service.resolve_input, DumpRequest/run_dump_request,
CompareRequest/resolve_compare_request, and the real compare CLI
(Click parameter-source spy) — see
tests/test_clang_header_backend_integration.py::
test_dump_request_and_compare_request_lang_explicit_forces_cpp_mode and
tests/test_service_dump_cache.py's
test_differs_by_lang_explicit/test_lang_explicit_reaches_run_dump_and_keys_separately.