diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRDeclarationPrinter.h b/tslang/include/TypeScript/MLIRLogic/MLIRDeclarationPrinter.h index 50564592c..57eb7a8b6 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRDeclarationPrinter.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRDeclarationPrinter.h @@ -21,7 +21,8 @@ namespace typescript void printVariableDeclaration(StringRef, NamespaceInfo::TypePtr, mlir::Type, bool); void print(StringRef, NamespaceInfo::TypePtr, mlir_ts::FunctionType); void print(ClassInfo::TypePtr); - void print(InterfaceInfo::TypePtr); + void print(InterfaceInfo::TypePtr); + void printGenericClass(NamespaceInfo::TypePtr, StringRef); protected: void newline(); diff --git a/tslang/lib/TypeScript/DeclarationPrinter.cpp b/tslang/lib/TypeScript/DeclarationPrinter.cpp index 90fe9a7e9..128fe681b 100644 --- a/tslang/lib/TypeScript/DeclarationPrinter.cpp +++ b/tslang/lib/TypeScript/DeclarationPrinter.cpp @@ -511,6 +511,20 @@ namespace typescript printNamespaceEnd(classType->elementNamespace); } + void MLIRDeclarationPrinter::printGenericClass(NamespaceInfo::TypePtr elementNamespace, StringRef rawDeclarationText) + { + // unlike print(ClassInfo::TypePtr) above, a generic class has no compiled body for + // any given instantiation to @dllimport against - each importing module + // instantiates it locally instead, so the caller passes the ORIGINAL source text + // (type parameters and method bodies intact) verbatim; this just supplies the + // matching namespace wrapper, deliberately WITHOUT printBeforeDeclaration()'s + // @dllimport marker. + printNamespaceBegin(elementNamespace); + os << rawDeclarationText; + newline(); + printNamespaceEnd(elementNamespace); + } + void MLIRDeclarationPrinter::print(InterfaceInfo::TypePtr interfaceType) { printNamespaceBegin(interfaceType->elementNamespace); diff --git a/tslang/lib/TypeScript/MLIRGenClasses.cpp b/tslang/lib/TypeScript/MLIRGenClasses.cpp index 899c23391..359d24400 100644 --- a/tslang/lib/TypeScript/MLIRGenClasses.cpp +++ b/tslang/lib/TypeScript/MLIRGenClasses.cpp @@ -16,6 +16,16 @@ namespace mlirgen auto fullNamePtr = getFullNamespaceName(namePtr); if (fullNameGenericClassesMap.count(fullNamePtr)) { + // already registered - but the registration itself typically happens during + // Stages::Discovering (before addGenericClassDeclarationToExport's + // isAddedToExport gate, which only actually emits once stage == + // Stages::SourceGeneration, will do anything) - retry the export step alone + // using the existing GenericClassInfo rather than skipping it entirely. + if (getExportModifier(classDeclarationAST)) + { + addGenericClassDeclarationToExport(fullNameGenericClassesMap.lookup(fullNamePtr)); + } + return mlir::success(); } @@ -40,6 +50,18 @@ namespace mlirgen getGenericClassesMap().insert({namePtr, newGenericClassPtr}); fullNameGenericClassesMap.insert(fullNamePtr, newGenericClassPtr); + // support dynamic loading: a generic class is never instantiated in this module + // if nothing here uses it concretely, so mlirGen(ClassLikeDeclaration) never + // reaches its own addClassDeclarationToExport call below (that only runs for a + // SPECIALIZED instantiation, gated on genContext.typeParamsWithArgs being + // non-empty) - the bare template needs to be exported here instead, the one + // place every generic declaration passes through regardless of whether it is + // ever instantiated locally. + if (getExportModifier(classDeclarationAST)) + { + addGenericClassDeclarationToExport(newGenericClassPtr); + } + return mlir::success(); } @@ -71,9 +93,27 @@ namespace mlirgen return {mlir::failure(), ""}; } - // do not process specialized class second time; if (isGenericClass && genContext.typeParamsWithArgs.size() > 0) { + // a concrete instantiation of a generic class (e.g. Box) reprocesses the + // SAME class declaration AST as the bare template (e.g. `export class Box`), + // so mlirGenClassInfo's isExport = getExportModifier(classDeclarationAST) above + // would otherwise mark this LOCAL, per-instantiation specialization as exported + // too - wrong on two counts: (1) semantically, a specialization materialized by + // whichever module instantiates it is local to that module, not a re-export of + // it (an importer using M.Box isn't re-exporting M.Box further); + // (2) mechanically, a multi-type-param instantiation's mangled name contains a + // raw comma (e.g. M.Pair..instanceOf), which is a + // metacharacter in the linker's `/EXPORT:name[,option]` directive syntax - lld + // rejects it outright ("invalid /export:") the moment anything in it is + // actually marked for export. The generic TEMPLATE's own declaration is already + // exported correctly and separately (see registerGenericClass / + // addGenericClassDeclarationToExport), which is the only export this class + // needs. + newClassPtr->isExport = false; + newClassPtr->isPublic = false; + + // do not process specialized class second time; // TODO: investigate why classType is provided already for class if (testProcessingState(newClassPtr, ProcessingStages::Processing, genContext)) { diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index ec886dd59..1789e5115 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -219,6 +219,7 @@ class MLIRGenImpl #endif mlir::LogicalResult createDeclarationExportGlobalVar(const GenContext &genContext); + mlir::LogicalResult createGenericClassDeclarationExportGlobalVar(const GenContext &genContext); bool isCodeStatment(SyntaxKind kind); @@ -10308,20 +10309,65 @@ class MLIRGenImpl { // already added return; - } + } exportedTypes.insert(newClassPtr->classType); addDependancyTypesToExport(newClassPtr->classType); SmallVector out; - llvm::raw_svector_ostream ss(out); + llvm::raw_svector_ostream ss(out); MLIRDeclarationPrinter dp(ss); dp.print(newClassPtr); declExports << ss.str().str(); } + void addGenericClassDeclarationToExport(GenericClassInfo::TypePtr genericClassInfo) + { + if (isAddedToExport(genericClassInfo->classType)) + { + // already added + return; + } + + // A generic class re-declared while re-importing another module's embedded + // declarations (see mlirGenImportSharedLib) still carries its own `export` keyword + // verbatim (it is the original source, copied as-is), so it reaches this same + // function a second time from inside the IMPORTER's own compile - but at that + // point `sourceFile` is the ambient/outer file (e.g. the importer's own .ts), not + // the "partial" buffer parsePartialStatements actually parsed this declaration + // from, so classDeclaration's pos/_end (offsets into THAT buffer) do not correspond + // to sourceFile->text at all and can exceed its length. Re-exporting a + // re-declaration one level removed like this is also simply wrong (the importer + // isn't meant to re-export M's generics further) - bounds-check and skip rather + // than let getTextOfNodeFromSourceText's substr throw std::out_of_range. + auto declEnd = static_cast(genericClassInfo->classDeclaration->_end); + if (declEnd > genericClassInfo->sourceFile->text.length()) + { + return; + } + + exportedTypes.insert(genericClassInfo->classType); + + // unlike a concrete class (printed above as a signature-only, @dllimport-marked + // declaration - the real body is already compiled into this module's DLL), a + // generic class has no compiled body for any given instantiation: each importing + // module instantiates it locally, on demand, exactly like a same-file usage would. + // So the FULL original source - type parameters and method bodies intact, no + // @dllimport marker - must be re-exported verbatim for parsePartialStatements to + // recompile per instantiation in the importer. + auto declText = convertWideToUTF8(getTextOfNodeFromSourceText( + genericClassInfo->sourceFile->text, genericClassInfo->classDeclaration.as(), true)); + + SmallVector out; + llvm::raw_svector_ostream ss(out); + MLIRDeclarationPrinter dp(ss); + dp.printGenericClass(genericClassInfo->elementNamespace, declText); + + genericDeclExports << ss.str().str(); + } + auto getNamespaceName() -> StringRef { return currentNamespace->name; @@ -10990,6 +11036,15 @@ class MLIRGenImpl bool declarationMode; std::stringstream declExports; + // generic class declarations must be re-imported as plain (non-".d.ts") source: unlike + // declExports (parsed with a "partial.d.ts" filename, which makes the parser mark + // everything ambient/external regardless of any per-declaration @dllimport marker - see + // createDeclarationExportGlobalVar/mlirGenImportSharedLib), a generic class has no + // compiled body for any instantiation to link against - the importer instantiates it + // locally, and needs its real method bodies to actually be compilable, not treated as + // external stubs. Kept in a separate stream/global so it can be re-parsed with a plain + // ".ts" filename instead. + std::stringstream genericDeclExports; mlir::SmallPtrSet exportCheckedDependenciesTypes; mlir::SmallPtrSet exportedTypes; diff --git a/tslang/lib/TypeScript/MLIRGenModule.cpp b/tslang/lib/TypeScript/MLIRGenModule.cpp index c5e6b30c4..8bcbac521 100644 --- a/tslang/lib/TypeScript/MLIRGenModule.cpp +++ b/tslang/lib/TypeScript/MLIRGenModule.cpp @@ -334,9 +334,49 @@ namespace mlirgen llvm::sys::path::append(path, llvm::sys::path::filename(mainSourceFileName)); llvm::sys::path::replace_extension(path, ".d.ts"); return createDependencyDeclarationFile(path, declText); -#else - return success(); -#endif +#else + return success(); +#endif + } + + mlir::LogicalResult MLIRGenImpl::createGenericClassDeclarationExportGlobalVar(const GenContext &genContext) + { + if (!genericDeclExports.rdbuf()->in_avail() || !compileOptions.embedExportDeclarations) + { + return mlir::success(); + } + + auto declText = genericDeclExports.str(); + + LLVM_DEBUG(llvm::dbgs() << "\n!! export generic class declaration: \n" << declText << "\n";); + + auto typeWithInit = [&](mlir::Location location, const GenContext &genContext) { + auto litValue = V(mlirGenStringValue(location, declText, true)); + return std::make_tuple(litValue.getType(), litValue, TypeProvided::No); + }; + + auto loc = mlir::UnknownLoc::get(builder.getContext()); + + VariableClass varClass = VariableType::Var; + varClass.isExport = true; + varClass.isPublic = true; + + // "generic" in the middle keeps the "__decls" prefix (so the existing + // symbol.starts_with(SHARED_LIB_DECLARATIONS_2UNDERSCORE) enumeration in + // mlirGenImportSharedLib still finds it) while staying distinguishable from the + // regular per-file "__decls__" global, so that call site can tell the + // two apart and parse each with the right file_d_ts flag. + std::string varName(SHARED_LIB_DECLARATIONS_2UNDERSCORE); + varName.append("_generic_"); + varName.append(llvm::sys::path::stem(llvm::sys::path::filename(mainSourceFileName))); + varName.append("_"); + varName.append(to_string(hash_value(mainSourceFileName))); + + auto varNameRef = StringRef(varName).copy(stringAllocator); + + registerVariable(loc, varNameRef, true, varClass, typeWithInit, genContext); + + return mlir::success(); } bool MLIRGenImpl::isCodeStatment(SyntaxKind kind) @@ -649,6 +689,11 @@ namespace mlirgen outputDiagnostics(postponedMessages, 1); return mlir::failure(); } + + if (mlir::failed(createGenericClassDeclarationExportGlobalVar(genContext))) { + outputDiagnostics(postponedMessages, 1); + return mlir::failure(); + } } clearTempModule(); @@ -879,15 +924,27 @@ namespace mlirgen LLVM_DEBUG(llvm::dbgs() << "\n!! Shared lib import: \n" << dataPtr << "\n";); { - MLIRLocationGuard vgLoc(overwriteLoc); + MLIRLocationGuard vgLoc(overwriteLoc); overwriteLoc = location; + // a generic class's declaration (see + // createGenericClassDeclarationExportGlobalVar) must NOT be parsed with + // the ".d.ts" filename convention used below for every other kind of + // declaration: that convention makes the parser mark everything ambient/ + // external regardless of any per-declaration @dllimport marker (see the + // comment on parsePartialStatements's file_d_ts parameter), which would + // make the generic's instantiated specializations wrongly look like + // external stubs with no compilable body - they need to be treated as + // ordinary, fully-compilable local source instead. + auto isGenericClassDecl = + declSymbol.starts_with(std::string(SHARED_LIB_DECLARATIONS_2UNDERSCORE) + "_generic_"); + auto importData = convertUTF8toWide(dataPtr); - if (mlir::failed(parsePartialStatements(importData, genContext, false, true))) + if (mlir::failed(parsePartialStatements(importData, genContext, false, !isGenericClassDecl))) { //assert(false); return mlir::failure(); - } + } } } else diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index eca6d87b9..0ed5578bd 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -854,20 +854,34 @@ add_test(NAME test-compile-export-import-class-extends-multilevel COMMAND test-r add_test(NAME test-compile-export-import-class-extends-implements-diamond COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_extends_implements_diamond.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_extends_implements_diamond.ts") add_test(NAME test-compile-export-import-class-abstract COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_abstract.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_abstract.ts") add_test(NAME test-compile-export-import-class-static COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_static.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_static.ts") -# Cross-module generic class instantiation: DISABLED, known issue found 2026-07-22. A generic -# class specialization instantiated in the IMPORTING module incorrectly inherits `export` -# (dllexport) linkage from the original `export class` declaration, even though the -# specialization is defined fresh, locally, in this module. For a multi-type-param generic -# (Pair) the resulting mangled name contains a raw comma -# (M.Pair..instanceOf), which is a linker-directive metacharacter - -# lld rejects it with "invalid /export:". Separately, the -shared tiers fail even earlier: -# once a compiled .dll/.lib for the exporting module already exists next to its .ts source, -# import resolution reads declarations back from that binary artifact instead of -# reparsing source, and that path has no representation for an uninstantiated generic -# template at all, so the whole namespace (M) fails to resolve ("can't resolve name: M"). -# Re-enable once both are fixed; test files are kept in the tree (export_class_generic.ts / -# import_class_generic.ts) for that follow-up. -# add_test(NAME test-compile-export-import-class-generic COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_generic.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_generic.ts") +# Cross-module generic class instantiation: FIXED 2026-07-22 (was disabled). Two independent +# bugs, both now fixed: +# (1) A generic class specialization instantiated in the IMPORTING module was incorrectly +# inheriting `export`/dllexport linkage from the original `export class` declaration, even +# though the specialization is a fresh, local instantiation. For a multi-type-param generic +# (Pair) the resulting mangled name contains a raw comma +# (M.Pair..instanceOf), a linker-directive metacharacter - lld +# rejected it with "invalid /export:". Fixed in mlirGen(ClassLikeDeclaration): a concrete +# instantiation now forces isExport/isPublic false regardless of the template's own +# modifiers (see the comment there). +# (2) The bare generic class declaration itself (`export class Box {...}`) never reached +# addClassDeclarationToExport at all (registerGenericClass's early-return path skipped it +# entirely), so nothing about it was ever embedded in the exporting module's __decls +# global - once a compiled .dll/.lib for the exporting module already existed next to its +# .ts source, import resolution reads declarations back from that binary artifact instead +# of reparsing source, and found nothing, so the whole namespace (M) failed to resolve +# ("can't resolve name: M"). Fixed by re-exporting the generic's own ORIGINAL source text +# (not a signature-only declaration like concrete classes get - a generic has no compiled +# body anywhere for the importer to link against, so it needs the real, recompilable +# source) via a new addGenericClassDeclarationToExport, embedded in ITS OWN +# "__decls_generic__" global (kept separate from the regular +# "__decls__" one) so it can be re-parsed with a plain ".ts" filename instead +# of ".d.ts" - re-parsing as ".d.ts" implicitly marks everything ambient/external +# regardless of any per-declaration @dllimport marker, which broke instantiation entirely +# (crashed the compiler with an unhandled std::out_of_range once the reimported +# declaration's own re-export attempt hit a stale source-file/AST-position mismatch). +# See class-generic-declaration-export-fix memory for the full account. +add_test(NAME test-compile-export-import-class-generic COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_generic.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_generic.ts") add_test(NAME test-compile-export-import-class-accessor COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_accessor.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_accessor.ts") add_test(NAME test-compile-export-import-class-implements-interface-multilevel COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_implements_interface_multilevel.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_implements_interface_multilevel.ts") add_test(NAME test-compile-export-import-class-implements-interface-optional COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_implements_interface_optional.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_implements_interface_optional.ts") @@ -935,8 +949,8 @@ add_test(NAME test-compile-shared-export-import-class-structural-interface COMMA # interface vtable slot, distinct from the link-time issue fixed above) - left for a dedicated # follow-up. # add_test(NAME test-compile-shared-export-import-class-implements-interface-abstract COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_implements_interface_abstract.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_implements_interface_abstract.ts") -# DISABLED: see the matching test-compile-export-import-class-generic comment above (known issue, 2026-07-22). -# add_test(NAME test-compile-shared-export-import-class-generic COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_generic.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_generic.ts") +# FIXED: see the matching test-compile-export-import-class-generic comment above (2026-07-22). +add_test(NAME test-compile-shared-export-import-class-generic COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_generic.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_generic.ts") # DISABLED: known issue found 2026-07-22 (separate from the super-accessor-call crash fixed # above). Under the -shared dynamic-import path, a base class's get/set accessors are not # resolvable at all from a derived class in another module ("Class member 'celsius' can't be @@ -979,8 +993,8 @@ add_test(NAME test-jit-shared-export-import-class-implements-interface-optional add_test(NAME test-jit-shared-export-import-class-structural-interface COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_structural_interface.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_structural_interface.ts") # DISABLED: see the matching test-compile-shared-export-import-class-implements-interface-abstract comment above (known issue, 2026-07-22, NOT fixed) - the JIT tier of the abstract variant hits the same this.area()-returns-0-via-interface bug. # add_test(NAME test-jit-shared-export-import-class-implements-interface-abstract COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_implements_interface_abstract.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_implements_interface_abstract.ts") -# DISABLED: see the matching test-compile-export-import-class-generic comment above (known issue, 2026-07-22). -# add_test(NAME test-jit-shared-export-import-class-generic COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_generic.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_generic.ts") +# FIXED: see the matching test-compile-export-import-class-generic comment above (2026-07-22). +add_test(NAME test-jit-shared-export-import-class-generic COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_generic.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_generic.ts") # DISABLED: see the matching test-compile-shared-export-import-class-accessor comment above (known issue, 2026-07-22). # add_test(NAME test-jit-shared-export-import-class-accessor COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_accessor.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_accessor.ts") add_test(NAME test-jit-shared-export-import-object-literal-with-interface COMMAND test-runner -jit -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_with_interface.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_with_interface.ts")