Skip to content

Fix cross-module generic class export/import (4 co-operating bugs)#280

Merged
ASDAlexander77 merged 1 commit into
mainfrom
fix/generic-class-cross-module-export
Jul 22, 2026
Merged

Fix cross-module generic class export/import (4 co-operating bugs)#280
ASDAlexander77 merged 1 commit into
mainfrom
fix/generic-class-cross-module-export

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Summary

A generic class's bare template declaration (export class Box<T> {...}) never reached the exporting module's declaration-export text at all: mlirGen(ClassLikeDeclaration) returns early into registerGenericClass whenever processing the template (as opposed to a concrete instantiation), before ever calling addClassDeclarationToExport. This left namespaces containing only generic classes completely unresolvable from an importing module once a compiled .dll/.lib already existed ("can't resolve name: M").

Four co-operating bugs fixed:

  1. Never exported at all - fixed by exporting the generic's own ORIGINAL source text (not a signature-only declaration like concrete classes get, since a generic has no compiled body anywhere for an importer to link against - it needs the real, recompilable source) via a new addGenericClassDeclarationToExport/printGenericClass.
  2. .d.ts-mode ambient bleed - the existing declaration-reimport mechanism parses with a "partial.d.ts" filename, which implicitly marks everything ambient/external regardless of any per-declaration @dllimport marker - fine for every other declaration kind (bodies already compiled elsewhere) but fatal for a generic's real body. Fixed by giving generic declarations their own separate embedded global ("__decls_generic_<file>_<hash>"), reparsed with a plain .ts filename instead.
  3. Crash on reimport - reimporting the generic's own text re-declares it a second time inside the importer, which re-attempts the export call with a stale source-file/AST-position pairing, throwing an unhandled std::out_of_range. This surfaced as a blocked "Microsoft Visual C++ Runtime Library" dialog - near-zero CPU usage, looked exactly like a hang from the outside (confirmed via EnumWindows + a ProcDump capture rather than assumed). Fixed with a bounds-check that skips the redundant re-export instead of crashing.
  4. isExport bleeding into instantiations - a concrete generic instantiation (e.g. Pair<number,string>) was inheriting its template's export modifier, and the mangled name's comma broke the linker's /EXPORT: directive syntax outright (lld: invalid /export:). Fixed by forcing isExport/isPublic false on any class reached via a generic specialization's codegen path.

Re-enables all 3 tiers (compile / -shared / -jit -shared) of export_class_generic.ts/import_class_generic.ts, previously disabled in CMakeLists.txt.

Test plan

  • ctest -C Debug -R "class-generic" - all tiers pass
  • Full suite: ctest -C Debug -j8 - 789/789 passed, no regressions

A generic class's bare template declaration never reached the module's
declaration-export text at all: mlirGen(ClassLikeDeclaration) returns
early into registerGenericClass whenever processing the template (vs.
a concrete instantiation), before ever calling
addClassDeclarationToExport. Fixed by exporting the generic's own
ORIGINAL source text (not a signature-only declaration like concrete
classes get, since a generic has no compiled body anywhere for an
importer to link against - it needs the real, recompilable source)
via a new addGenericClassDeclarationToExport/printGenericClass.

That exposed two more bugs once generics started actually reaching
the reimport path:
- The existing declaration-reimport mechanism parses with a
  "partial.d.ts" filename, which implicitly marks everything ambient/
  external regardless of any per-declaration @dllimport marker - fine
  for every other declaration kind (bodies already compiled elsewhere)
  but fatal for a generic's real body. Fixed by giving generic
  declarations their own separate embedded global
  ("__decls_generic_<file>_<hash>"), reparsed with a plain ".ts"
  filename instead.
- Reimporting the generic's own text re-declares it a second time
  inside the importer, which re-attempts the export call with a
  stale source-file/AST-position pairing, throwing an unhandled
  std::out_of_range (surfaced as a blocked "Microsoft Visual C++
  Runtime Library" dialog - near-zero CPU, looked like a hang from
  the outside). Fixed with a bounds-check that skips the redundant
  re-export instead of crashing.

Also fixes a related, separately-triggered bug: a concrete generic
instantiation (e.g. Pair<number,string>) was inheriting its
template's `export` modifier, and the mangled name's comma broke the
linker's /EXPORT: directive syntax outright. Fixed by forcing
isExport/isPublic false on any class reached via a generic
specialization's codegen path.

Re-enables all 3 tiers (compile / -shared / -jit-shared) of
export_class_generic.ts/import_class_generic.ts, previously disabled.
Full suite: 789/789, no regressions.
@ASDAlexander77
ASDAlexander77 merged commit bb27386 into main Jul 22, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the fix/generic-class-cross-module-export branch July 22, 2026 18:45
ASDAlexander77 added a commit that referenced this pull request Jul 23, 2026
Last remaining item from decls-cross-module-declaration-mechanism.md's
unverified list (after generic classes/PR #280, generic functions/PR
#285, generic interfaces/PR #286): genericTypeAliasMap's registration
path in mlirGen(TypeAliasDeclaration) never called any export function
for the generic branch - only the non-generic else-branch calls
addTypeDeclarationToExport. Confirmed with a cross-module repro: a
generic type alias failed cross-module resolution under -shared.

Unlike the other three generic declaration kinds, there is no
dedicated GenericTypeAliasInfo struct tracking sourceFile/
elementNamespace for a generic type alias - genericTypeAliasMap's
value is just {typeParams, typeNode}, since resolution
(getTypeByTypeReference / resolveGenericTypeInNamespace) is pure
compile-time type substitution on the stored TypeNode, never
re-invoking mlirGen on the whole declaration the way a class/function/
interface instantiation does. So instead of adding a new Info struct,
addGenericTypeAliasDeclarationToExport takes the AST node and
namespace directly and is called inline from the one registration
site, gated on `stage == Stages::SourceGeneration` (a given top-level
TypeAliasDeclaration is visited exactly once per stage, so this is
sufficient dedup - no "already registered" early-return branch exists
here to retry from, unlike the class/function/interface fixes).

Tested with both a single- and two-type-param alias (Box<T>,
Pair<A,B>) to check for the classes/functions' export-bleed-on-
instantiation bug; neither triggered it, matching generic interfaces'
outcome - a type alias has no compiled body or runtime entity at all,
so there's nothing for an instantiation to wrongly dllexport in the
first place. All 3 tiers passed on the first attempt, no follow-up fix
needed - the simplest of the four generic-declaration-kind fixes.

New export_type_alias_generic.ts/import_type_alias_generic.ts
cross-module test pair (compile, -shared compile, -jit -shared tiers).
802/802 green. This closes out the generic cross-module export arc.

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