Emit discardable-ODR variables via odr-use instead of UsedAttr in GetVariableOffset#1068
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1068 +/- ##
==========================================
+ Coverage 87.01% 87.04% +0.03%
==========================================
Files 23 23
Lines 6069 6084 +15
==========================================
+ Hits 5281 5296 +15
Misses 788 788
🚀 New features to boost your workflow:
|
…bleOffset ForceCodeGen forces deferred emission by planting a permanent __attribute__((used)) on the decl; every global emitted that way gets a WeakTrackingVH in codegen's llvm.used list. When such a global is deleted before emitUsed runs (ORC freeing a materialized PTU's IR), the handle nulls and release-built clang dereferences it unchecked — a process crash that accumulates with interpreter state rather than tracing to any one declaration (large reflection sweeps died; per-type bisection never converged, so the crash itself has no compact unit repro — root-caused via gdb). GetVariableOffset now emits GVA_DiscardableODR variables (the inline/constexpr class the crash traced to) by declaring an external-linkage odr-use of the qualified name instead: the definition flows through the regular deferred-decl path and leaves no used-list residue, no permanent attribute. Everything else keeps the UsedAttr path, deliberately: - Internal-linkage variables cannot be odr-used from a later PTU (the module-local symbol duplicates or goes missing); getting this wrong broke VariableReflection_GetVariableOffset (static int S) and cppyy's Lifeline::count lookup. - Available-externally definitions would not be emitted by a mere reference. - Template-specialization and anonymous/lambda spellings do not reliably round-trip as source (a printed LLONG_MIN non-type argument re-parses as an overflowing literal), and a parse-failing declaration poisons the incremental interpreter. An interpreter-level alternative — Undo(1) on parse failure to drop the failed PTU — was evaluated and rejected: cppyy depends on failed parses leaving side-effect declarations (an explicit-instantiation probe of a declared-but-undefined template is expected to fail and leave the specialization decl behind; test_templates test32). The dummy-name counter is deliberately plain, matching gWrapperSerial: the interpreter API is caller-serialized. ForceCodeGen's function path (GetFunctionAddress) still uses UsedAttr — same theoretical hazard, never observed. The test pins the mechanism: after an offset query on a discardable-ODR static, the decl carries no UsedAttr and later PTU modules carry no llvm.used, with a sweep-shaped regression net over interleaved absorbed parse failures. Co-developed-with-the-help-of: Claude Code (Fable 5, human in the loop)
916eeb4 to
79d2fdb
Compare
|
This entire approach of adding these attributes to force emission is sketchy to say the least. Can you ask Claude what alternatives we have including perhaps minimal reasonable modifications to clang. |
ya, i didn't love this at all either... felt pretty gross to me. let me iterate a bit and see what we might do upstream to address it. |
|
clang-tidy review says "All clean, LGTM! 👍" |
I did not mean your patch but the overall logic in this area. |
|
CI triage for the three red jobs — none traces to this diff:
The six cling compile failures from the first push are fixed (PTU white-box assertions now gated to clang-repl builds). |
|
Asked Claude to survey the design space; here's the grounded version (file/line refs against release/21.x). Why the current approach is sketchy (agreed): What CppInterOp can do without touching clang: essentially this PR — a source-level odr-use where the qualified name round-trips (falling back to Minimal clang modification #1 (tiny, worth doing regardless): null-skip deleted globals in Minimal clang modification #2 (the principled end-state): an emission API on Suggested sequencing: land this PR now as the crash fix — it helps every clang version that exists today, and CppInterOp's supported-version window means the eventual cleanup is a version gate rather than a revert (prefer |
|
Sounds like a good plan actually. |
ForceCodeGenforces deferred emission by planting a permanent__attribute__((used))on the decl. Every global emitted that way gets aWeakTrackingVHin codegen'sllvm.usedlist; when such a global is deleted beforeemitUsedruns (e.g. ORC freeing a materialized PTU's IR), the handle nulls and release-built clang dereferences it unchecked — SIGSEGV inConstantExpr::getPointerBitCastOrAddrSpaceCastunderCodeGenModule::Release()of a later incremental parse. The failure is cumulative interpreter state, not a culprit type: it only manifests after enough force-emitted statics plus JIT materialization cycles (we hit it on large reflection sweeps; per-type bisection never converged), so the regression test asserts the mechanism instead — noUsedAttrplanted, nollvm.usedresidue in later PTU modules — plus a sweep-shaped smoke loop.GetVariableOffsetnow emitsGVA_DiscardableODRvariables (the inline/constexpr class the crash traced to) by declaring an external-linkage odr-use of the qualified name: the definition flows through the regular deferred-decl path and leaves no used-list residue and no permanent attribute. Everything else deliberately stays on theUsedAttrpath:VariableReflection_GetVariableOffset(static int S) and cppyy'sLifeline::countlookup.LLONG_MINnon-type argument re-parses as an overflowing literal), and a parse-failing declaration poisons the incremental interpreter.Also evaluated and rejected:
Interpreter::Undo(1)on parse failure to drop failed PTUs — cppyy depends on failed parses leaving side-effect declarations (test_templatestest32's explicit-instantiation probe).Concurrency: the dummy-name counter is deliberately a plain
static unsigned, matching the file's existinggWrapperSerialidiom. The interpreter API is caller-serialized (the incremental parse either route drives is not thread-safe, and the file's only sync primitive is the process-initonce_flag); an atomic here would advertise a guarantee the surrounding function doesn't have. Worst case if raced anyway: a duplicate dummy name fails theDeclareand the caller falls back toForceCodeGen.Known residual, unchanged here:
ForceCodeGen's function path (GetFunctionAddress) still usesUsedAttr— same theoretical hazard, never observed.Independent of #1067 (both touch
GetVariableOffset; whichever lands second has a trivial test-file conflict).🤖 Done with the help of Claude Code (Fable 5, human in the loop)