Skip to content

Fix cross-module class extends crash and add regression coverage#274

Merged
ASDAlexander77 merged 1 commit into
mainfrom
fix/cross-module-class-extends-crash
Jul 21, 2026
Merged

Fix cross-module class extends crash and add regression coverage#274
ASDAlexander77 merged 1 commit into
mainfrom
fix/cross-module-class-extends-crash

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Summary

  • ClassMethodAccess's isDynamicImport branches (hit when a derived class' base class lives in another module) fed an unchecked, possibly-null Value into CreateBoundFunctionOp whenever the target method's global couldn't be resolved by name — an intermittent, heap-layout-sensitive access violation. It was masked whenever a debugger happened to be attached (ProcDump/WinDbg), which is why it initially looked like a flaky Heisenbug rather than a deterministic bug.
  • Fix: a null-check that turns the failure into a clean compile error instead of a crash, plus a theModule.lookupSymbol fallback — compiler-synthesized methods (e.g. .instanceOf, which every class gets) are registered as real FuncOps rather than the dlsym-style global variables that regular imported/declared methods use, so the original single-mechanism lookup could never find them.
  • Adds the project's first regression coverage for cross-module class extends: basic 2-level extends, a 3-level chain, and extends+implements combined (mirroring the interface-extends coverage added in Fix cross-module diamond-extends interface silently corrupting data #268-Extend interface extends coverage to 3-target extends clauses #270). The non-shared-lib variants pass and are enabled.

Known issue (documented, not fixed here)

The -shared (AOT and JIT) variants of the same class hierarchies hit a separate, deeper pre-existing bug: .instanceOf isn't always reliably synthesized/registered when a class is compiled across a real DLL boundary (order-dependent on discovery/partial-resolve pass sequencing). Two targeted fix attempts were tried and reverted:

  • Registering the dynamic-import global under its full namespaced name broke unrelated plain cross-module function calls (that registration path is shared with non-class functions, which need the short name).
  • Deferring .instanceOf/.new synthesis until a non-speculative compiler pass caused an infinite loop in unrelated same-module class tests (their own discovery retry loop can also always run under allowPartialResolve).

Both were reverted in favor of leaving a clean, documented known issue rather than risking a worse regression. The 5 affected -shared test entries are commented out in CMakeLists.txt with a full explanation.

Test plan

  • Full ctest suite (761 tests): 760/761 passing — the sole failure (unittest-MLIRGenTests, a tuple bracket-vs-brace printing mismatch) is pre-existing and unrelated to this change.
  • Verified the original crash no longer reproduces under any tested invocation pattern (manual JIT repro, test-runner harness).
  • Verified reverting each risky fix attempt restores prior green state (no infinite loops, no unrelated regressions).

🤖 Generated with Claude Code

ClassMethodAccess's isDynamicImport branches (called for a class whose base
lives in another module) fed an unchecked, possibly-null Value into
CreateBoundFunctionOp when the target method's global variable couldn't be
resolved by name - an intermittent, heap-layout-sensitive access violation
(masked whenever a debugger happened to be attached, which made it look like
a flaky Heisenbug). Adds a null-check that turns the failure into a clean
compile error, plus a theModule.lookupSymbol fallback since compiler-
synthesized methods (e.g. .instanceOf) are registered as real FuncOps rather
than the dlsym-style global variables regular imported methods use.

Adds the project's first regression coverage for cross-module class
inheritance: basic 2-level extends, a 3-level chain, and extends+implements
combined. The non-shared-lib variants pass and are enabled; the -shared
(AOT/JIT) variants hit a separate, deeper pre-existing bug in when
.instanceOf gets synthesized for a class compiled across a real DLL boundary
- left disabled with a detailed comment rather than patched further, since
two targeted fix attempts each introduced a worse regression (an infinite
loop in unrelated same-module class tests).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 89eb986 into main Jul 21, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the fix/cross-module-class-extends-crash branch July 21, 2026 20:57
ASDAlexander77 added a commit that referenced this pull request Jul 22, 2026
…vestigation (#276)

* Fix CRT assert dialogs blocking debug tslang.exe runs; document cross-module .instanceOf investigation

_CrtSetReportFile(_CRT_ASSERT, ...) has no effect unless the report mode for
that category is also set to _CRTDBG_MODE_FILE - without it, the default mode
(_CRTDBG_MODE_WNDW) still pops a blocking "Assertion failed" MessageBox on
every assert(), which looks like a hang in a non-interactive session. One
missing _CrtSetReportMode call fixes it.

Also documents a three-attempt investigation into the disabled cross-module
`-shared` class-extends tests (PR #274's known issue): root-caused why
.instanceOf can't be resolved (two real bugs found and fixed in isolation),
but every attempt to fix it regressed unrelated tests project-wide (70+ tests,
then 10 more on a narrower retry) via a fragile scoped-hash-table interaction
that isn't safe to patch blindly. All three attempts reverted; the actual fix
is left to a future, more careful session per the recommendation in the new
doc.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

* Fix cross-module class extends under -shared: inline dlsym resolution + decl-printer fixes (#277)

Makes a derived class extending a base from a dynamically imported module
(-shared, real DLL boundary) work end-to-end - all six formerly disabled
tests (basic/multilevel/diamond x compile/JIT) now pass and are enabled.
Three co-operating fixes, each exposing the next:

1. ClassMethodAccess (isDynamicImport instance branch): when a member has
   neither a locally defined FuncOp (bodyless declarations are now explicitly
   excluded - they lower to unlinkable external references) nor a registered
   dlsym-global, resolve it in place via SearchForAddressOfSymbolOp + cast -
   no global registration at all, sidestepping the fullNameGlobalsMap scope
   fragility that sank the previous session's three attempts (see
   docs/cross-module-dynamic-import-instanceof-design.md, now updated with
   the resolution in section 10).

2. mlirGenClassVirtualTableDefinition: vtable slots whose symbols are owned
   by a dynamic-import base (inherited virtual methods; .rtti/.size statics
   under ADD_STATIC_MEMBERS_TO_VTABLE) emit runtime symbol resolution instead
   of constant SymbolRefOp - a link-time address of a DLL-resident symbol
   does not exist without an import library. GlobalOpLowering already routes
   such initializers through the __cctor path.

3. DeclarationPrinter: the extends clause printed the class's OWN name
   instead of the base's (unused loop variable - "class B extends M.B"),
   which made the importer build a self-cycle in baseClasses and
   stack-overflow in getVirtualTable's unguarded recursion; and the synthetic
   base-class storage field (memory layout, not a source member) leaked into
   the printed decl as a real field, shifting every subsequent field's offset
   in the importer - the DLL's methods saw b=22 while the importer read
   c.b==0 from one slot past it.

Full suite green: 767/767 (761 prior + 6 newly enabled).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
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.

1 participant