A using-declaration re-exporting a namespace-scope constant produces two AbiSnapshot.constants entries for one declaration on the castxml backend — reported against real oneDAL headers. A per-snapshot, name-shape-based dedup was implemented, reviewed, found unsound in two independent ways, and reverted rather than shipped broken (Codex review, three rounds)¶
Index: Known gaps
Status (re-verified 2026-10-09 at 456989f): OPEN. The entry's own later verification (known-gaps.md ~4030-4068) found both backends now drop the alias spelling (a false negative), so the alias-identity gap remains open. There is no UsingShadowDecl threading in the code.
cpu.hpp opens a plain (non-inline)
namespace v1 { constexpr ... cpu_feature_map = ...; } and later does
using v1::cpu_feature_map; inside the enclosing detail namespace —
an ordinary C++ re-export pattern, not versioned-inline-namespace ABI
tagging (inline namespace v1 {}, which is the shape
diff_namespaces.py's _paired_stable_indices/
EXPERIMENTAL_REMOVED_WITHOUT_REPLACEMENT machinery already merges
aliases for — that machinery is function/type-scoped, cross-snapshot
(old vs. new), and keys on experimental/preview/v0 segments
specifically, so it neither targets nor would catch this same-snapshot
pattern even if it applied to constants). castxml's <Variable> XML
records the using-declaration as a second full element rather than a
reference to the original, so dumper_castxml.py's
_iter_public_constants() (the shared source for parse_constants()/
parse_constant_headers()) emits both detail::v1::cpu_feature_map
and detail::cpu_feature_map as independent entries in
AbiSnapshot.constants with the same value — distinct qualified-name
keys, not a key collision, so this is invisible to model.py's
existing first-wins duplicate-name dedup (function_map/
variable_map/type_by_name all dedup by identity — mangled name or
bare type name — under which a using-shadow naturally collapses onto
its target; AbiSnapshot.constants has no mangled-name equivalent).
Reported at real scale, not a one-off: 98 alias groups across 267
constants in the reporting library's own dump. The same castxml
behavior duplicates other declaration kinds too, with two different
outcomes: 1,477 of 6,009 mangled function names were duplicated the
same way, but that half is not a double-reporting bug —
model.py's mangled-name-keyed function_map/variable_map already
collapse a using-shadow function or variable onto its target for free
for the ABI/mangled-symbol question (a using-declaration doesn't change
what a symbol mangles to), so this is not a repeat of the constants
over-reporting problem above. It is a separate, undocumented
false-negative gap on the source/API side, narrower in scope than the
std::-specific STD_REEXPORT_REMOVED detector below (Codex review,
fresh evidence): because the diff is keyed on the unchanged mangled
symbol, a release that removes only the using re-export of a
library's own function or variable — not a std:: name, which is
the one case STD_REEXPORT_REMOVED already covers — while keeping the
real declaration and its export produces no finding at all, even
though a consumer that named the alias-qualified spelling no longer
compiles. This is the identical shape of gap the clang-side note below
documents for constants (an unchanged underlying identity hides a real,
source-visible alias removal), just reached from the opposite side
(castxml capturing the alias correctly here, the diff layer being the
one blind to it) — not attempted in this pass, and would need
detect_std_reexport_removed's general shape (matching declared
qualified names, not std::-specific) extended to a library's own
namespaces, which is its own scoped detector design, not a drive-by
extension of this entry. A duplicated bare type name (range
alongside v1::range) is a separate, still-open gap from the
type-dedup entry already documented above
(opaque-type suppression keyed by bare RecordType.name colliding
across namespaces on an accidentally-shared bare name) —
range/v1::range are two distinct qualified spellings of what may be
the same using-re-exported record, so today's exact-bare-name
first-wins dedup does not merge them either, and would need its own,
differently-shaped fix (threaded through RecordType identity, not
AbiSnapshot.constants), not attempted here.
Confirmed independently that the duplicate-entry shape is castxml-only
— but the direct-clang L2 backend is not clean either, it just fails in
the opposite direction. Verified directly against real Clang 18
(-Xclang -ast-dump=json) on a minimal repro of the same shape: a
using-declaration lowers to a UsingDecl node carrying the qualified
target name (v1::cpu_feature_map) but no init/value of its own,
immediately followed by an implicit, unnamed UsingShadowDecl
("isImplicit": true, no name key at all — confirmed by inspecting
the emitted JSON directly, not inferred). That UsingShadowDecl node
does carry real target identity — a target object with the
underlying VarDecl's own id, kind, name, and type (confirmed
by inspecting the real JSON: {"id": "0x...", "kind": "UsingShadowDecl",
"isImplicit": true, "target": {"id": "0x...", "kind": "VarDecl", "name":
"cpu_feature_map", "type": {"qualType": "const int"}}}) — so this is
identity Clang exposes and dumper_clang.py simply never reads, not
identity Clang lacks (Codex review, fresh evidence; an earlier revision
of this entry claimed the latter, which was wrong). _categorize() has
no branch matching either UsingDecl or UsingShadowDecl — its
VarDecl branch requires both kind == "VarDecl" and a non-empty
name, neither of which either using-related node satisfies — so both
are silently dropped, target included, and the re-exported spelling
never enters AbiSnapshot.constants at all on that backend.
This is complementary missing coverage, not a clean bill of health:
a release that removes only the using v1::cpu_feature_map; re-export
while keeping the real v1::cpu_feature_map definition breaks every
consumer of the re-exported spelling, but both the old and new
Clang-derived snapshots contain only the one underlying key —
_diff_constants() has nothing to compare and emits no
CONSTANT_REMOVED, a silent false negative. Castxml's two qualified
keys, over-reporting as they are for the case this entry is about, would
at least detect that removal correctly. So the two backends fail in
opposite directions on the same underlying gap (no alias-identity
evidence for constants) — over-reporting on castxml, under-reporting on
clang. Not verified against a live castxml run (no castxml binary
available in this environment to reproduce the XML shape directly) —
the castxml-side mechanism above is taken from the original report, not
independently re-derived from castxml's own output. Update (G31 Phase
C, later pass): now independently re-derived, against a real
conda-forge castxml 0.7.0 build, and the "opposite directions" framing
in this paragraph does not hold — see the "Option (b) closed" note at
the end of this entry, below, for the reproduction and its
implications.
Attempted fix, reverted (three review rounds): a
qualified_name_segments.dedup_versioned_namespace_alias_items()
helper collapsed one spelling onto the other within one already-
collected _iter_public_constants() item list, gated on BOTH (1) the
two qualified names differing by exactly one versioned-inline-namespace
segment (version_strip_segments — the same, already-trusted
structural check diff_namespaces.py already relies on for its own
alias merging) AND (2) the two values being byte-identical. Deliberately
narrower than the plain value-equality merge this same module's own
docstring already rejected after three earlier review rounds for a
different, cross-snapshot reason (that heuristic had no name evidence
at all and could merge unrelated same-valued declarations that never
even coexist together) — this attempt was same-snapshot only and added
a real structural name-shape gate. It was not enough. Two more rounds of
review found it unsound from two independent directions, and a third
round (after the second was "fixed") found the second fix was also
wrong, at which point it was reverted rather than iterated a fourth
time:
- Round 1 (P1): the first revision kept the shorter, version-
stripped alias and dropped the version-qualified original —
reasoned as "the spelling a consumer of the
usingre-export actually writes." This actively fabricated findings: a release that adds or removes only the re-export (the real declaration unchanged) went from one snapshot keepingdetail::v1::x(no alias present, nothing to strip) to the other collapsing down todetail::xinstead — two different surviving keys for one unchanged declaration, which_diff_constantsread as a spuriousCONSTANT_REMOVED+CONSTANT_ADDEDpair. In other words: this revision manufactured exactly the kind of double-reporting it was written to eliminate, just shaped as fabricated add/remove instead of a duplicatedCONSTANT_CHANGED— arguably worse, since a fabricatedCONSTANT_REMOVEDreads as BREAKING. - Round 2 (P1 again, on the "fix" for round 1): reversing the
direction — always keep the version-qualified spelling, always
drop the alias — looked sound (invariant to whether either side
happens to carry the re-export) and was reviewed as fixing round 1.
A further round then produced a concrete counterexample proving the
fixed direction was also wrong in general:
namespace detail { constexpr int x = 42; namespace v1 { using detail::x; } }is legal C++ where a using-declaration imports a name into a versioned-looking namespace rather than out of one — heredetail::x(the shorter spelling) is the real declaration anddetail::v1::x(the longer, version-qualified spelling) is the alias, the exact reverse of what round 1's fix assumed universally. "Always keep the longer/qualified spelling" reproduces round 1's failure mode for this reversed input shape instead. This is the structural finding that ended the attempt: qualified-name shape alone (segment count, or which segment looks version-tagged) cannot determine which of two spellings is the real declaration and which is the using-introduced alias — a using-declaration can legally go in either direction relative to a versioned-looking namespace segment, and_iter_public_constants()'s current(qualified_name, value, declaring_header)output carries no signal (declaration order, anartificial-style marker, a using-shadow/target back-reference) that distinguishes the two directions. No fixed, name-shape-based rule can be sound for both. - Round 3 (P2, on the value-equality gate itself, independent of
direction): even a correctly-directed rule would still merge two
genuinely independent declarations that happen to form a
version-alias-shaped name pair AND happen to share a value at
extraction time (e.g.
namespace detail { namespace v1 { constexpr int x = 42; } constexpr int x = 42; }— two unrelatedxs, nousinganywhere) — the same class of coincidence-driven risk this module's docstring already flags for the cross-snapshot merge it rejects, now shown to reach the narrower same-snapshot case too.
Given round 2 proves no per-snapshot, name-shape-based direction rule
can be sound, and a genuinely correct fix needs real using-shadow/
target identity evidence rather than name shape — continuing to patch
the heuristic's direction a third time was exactly the "one more
counterexample" pattern this file's own linkage-blind-removal and
type-identity entries above already warn against repeating. Reverted in
full (code, tests, changelog fragment) rather than shipped with a
known-unsound direction, per this file's own "known gaps over risky
reactive patches" convention — and per the same "attempted twice,
reverted twice" discipline already established here: a false BREAKING
finding fabricated by an unsound heuristic is a worse failure mode than
the pre-existing double-reporting it would have fixed, since the
original bug is merely noisy while a wrong-direction fabricated
CONSTANT_REMOVED blocks a release for nothing.
What "real identity evidence" actually means differs by backend, and
the two must not be conflated (Codex review, fresh evidence corrected
an earlier draft of this entry that did conflate them). On
direct-clang, the identity evidence demonstrably EXISTS today and is
simply unused: UsingShadowDecl.target (see above) names the exact
underlying VarDecl by AST id, so a sound clang-side fix is a real,
scoped option — capture the shadow, resolve target.id back to the
already-visited VarDecl, and re-register the alias under the
using-declaration's own enclosing-scope name, carrying its target's
identity forward rather than guessing from name shape. That is new
extraction work (_categorize() doesn't currently visit
UsingShadowDecl at all) and was not attempted in this pass, but it is
a materially different, better-founded starting point than the
name-shape heuristic this entry's earlier rounds tried and reverted. On
castxml, whether an equivalent target back-reference exists in its
<Variable> XML is genuinely unknown — not confirmed absent, not
confirmed present — since no castxml binary was available in this
environment to inspect real output for this construct; the reported
castxml duplication mechanism throughout this entry is taken from the
original report, not independently re-derived. A correct, full fix
needs one of: (a) the clang-side target.id threading described above,
landed and verified against the FP-rate/mutation-score gates before
trusting it; (b) inspecting real castxml XML for this construct to
determine whether it exposes anything equivalent, which this
environment cannot do; or (c) a genuinely different architecture that
resolves the ambiguity at diff time with both sides' full data available
simultaneously (mirroring how diff_namespaces.py's
_paired_stable_indices jointly builds paired OLD/NEW indices for
functions/types, gated on real identity) rather than destructively
dropping information from one side's snapshot in isolation — a
materially larger, cross-cutting change on its own, not a follow-up
patch to the same per-snapshot helper.
Option (b) closed (G31 Phase C, real conda-forge castxml build): a
real, policy-conformant castxml (0.7.0, conda-forge, within the
>=0.6.11,<0.8.0 range castxml_policy.py enforces, bundled Clang
20.1.8) is now available and was used to reproduce this construct
directly through the exact invocation shape _build_castxml_command
emits (--castxml-cc-gnu (g++ -x c)/--castxml-cc-gnu g++, matching
real header-dump usage) — the originally-hypothesized castxml
duplication mechanism does not reproduce. For namespace detail {
namespace v1 { constexpr int cpu_feature_map = 42; } using
v1::cpu_feature_map; }, real castxml emits exactly one <Variable>
element (context="_13", i.e. v1 — never detail), and that single
element's id is listed in both detail's and v1's <Namespace
members="..."> attribute — a shared reference, not two elements. There
is no <Using...>-shaped XML tag in castxml's schema at all (checked
against castxml's own --help), and forcing the value to be ODR-used
(&cpu_feature_map from an inline function) does not change this.
Since dumper_castxml.py's _variable_els/_iter_public_constants()
iterate the flat, once-per-XML-element id map (_build_id_map), not
either namespace's members list, they see this construct exactly once
too — _qualified_name() resolves it to detail::v1::cpu_feature_map
only; the alias spelling detail::cpu_feature_map never enters
AbiSnapshot.constants on this castxml version, at all. The identical
single-element behavior was independently confirmed for the sibling
type-dedup case this entry cross-references (struct range in v1,
using v1::range; in detail): one <Struct> element, shared between
both namespaces' members lists, never a duplicate. What this means
for the entry above: the "castxml over-reports (duplicate keys),
clang under-reports (missing alias)" asymmetry this entry documented
was written from an unverified report and does not hold for the
currently-supported castxml version range — for this exact construct,
both backends now demonstrably fail the same way (silent false
negative: the alias spelling is absent from the snapshot entirely,
matching the clang-side finding already confirmed above). This does not
retroactively invalidate the original report (98 alias groups across
267 constants, real oneDAL headers) — that scan may have used a
different castxml build, or the real duplication may come from a
different source construct entirely (e.g. two independent, textually
separate definitions sharing a value, rather than a language-level
using-declaration/using-directive) that this pass did not have the
original headers to reproduce against. It does mean: (1) the three
reverted name-shape-heuristic attempts documented above were correctly
reverted regardless of this finding (their unsoundness was proven by
counterexample, independent of whether castxml duplicates), so nothing
here reopens that question; (2) a future fix attempt should design for
the false-negative alias-identity gap confirmed symmetric on both
backends (option (a)'s UsingShadowDecl.target clang-side threading is
now the best-founded starting point, since it is real, already-present
AST identity, not a heuristic), rather than a castxml-side dedup this
evidence no longer motivates; (3) closing this for real still needs the
original large-scale report's exact source construct identified before
trusting any fix against it — not attempted here, since the oneDAL
headers that produced the original 98/267 count are not available in
this environment either.