From df5e8cbd1cea73ed0586dfffe4b982dc8588107e Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 11 Oct 2026 17:52:05 +0100 Subject: [PATCH] @cppvtable classes: call C++ virtual methods through the object's own vtable A class decorated with @cppvtable is laid over objects made by C++. Its methods are the C++ vtable's slots from 0 in declaration order, with a base class's slots first. The class gets no .instanceOf, .new, interface or static-field slots, no RTTI, and no vtable, constructor or allocation of its own. A tslang class already keeps its vtable pointer at offset 0 and calls slot N as fn(this, args), which is the MSVC x64 member-function convention, so this is all that was missing to bind single-inheritance C++ interfaces such as nvrhi::IDevice. A slot off by one calls a different C++ method with no error, so these are compile errors: - method bodies, constructors, field initializers; - static members, accessors; - implements, and mixing with ordinary classes through extends; - instanceof, and casts to or from any or an interface (each asks the object through slot 0, which is a C++ method); - rc/own memory models (they write a count before the object); - 32-bit x86 MSVC targets (__thiscall). The declaration printer keeps @cppvtable, so an exported class re-imports with the same slots. Tests: - tests/00cppvtable_class.ts: slots, fields, extends and abstract, against a vtable made by hand (AOT and JIT); - export/import_cppvtable_class.ts: the same slots across modules; - cppvtable/*.ts: one compile error per rule. Docs: docs/cppvtable-classes.md. Co-Authored-By: Claude Opus 5.5 --- docs/c-cpp-header-import.md | 5 + docs/cppvtable-classes.md | 95 +++++++++++ tslang/include/TypeScript/Defines.h | 2 + .../TypeScript/MLIRLogic/MLIRGenStore.h | 20 ++- tslang/include/TypeScript/TargetInfo.h | 5 + tslang/lib/TypeScript/DeclarationPrinter.cpp | 8 + tslang/lib/TypeScript/MLIRGenCast.cpp | 9 ++ tslang/lib/TypeScript/MLIRGenClasses.cpp | 152 +++++++++++++++++- tslang/lib/TypeScript/MLIRGenImpl.h | 34 ++++ tslang/test/tester/CMakeLists.txt | 59 +++++++ tslang/test/tester/cppvtable/box_any.ts | 11 ++ tslang/test/tester/cppvtable/constructor.ts | 10 ++ tslang/test/tester/cppvtable/extends_plain.ts | 13 ++ .../tester/cppvtable/field_initializer.ts | 10 ++ tslang/test/tester/cppvtable/get_accessor.ts | 10 ++ tslang/test/tester/cppvtable/implements.ts | 13 ++ tslang/test/tester/cppvtable/instanceof.ts | 13 ++ tslang/test/tester/cppvtable/memory_model.ts | 14 ++ tslang/test/tester/cppvtable/method_body.ts | 9 ++ tslang/test/tester/cppvtable/plain_extends.ts | 13 ++ tslang/test/tester/cppvtable/static_member.ts | 10 ++ tslang/test/tester/cppvtable/to_interface.ts | 15 ++ tslang/test/tester/cppvtable/unbox_any.ts | 11 ++ tslang/test/tester/tests/00cppvtable_class.ts | 61 +++++++ .../tester/tests/export_cppvtable_class.ts | 23 +++ .../tester/tests/import_cppvtable_class.ts | 33 ++++ tslang/test/tester/verify-ownership.cmake | 6 +- tslang/test/tester/x86-exclusions.cmake | 7 + 28 files changed, 658 insertions(+), 13 deletions(-) create mode 100644 docs/cppvtable-classes.md create mode 100644 tslang/test/tester/cppvtable/box_any.ts create mode 100644 tslang/test/tester/cppvtable/constructor.ts create mode 100644 tslang/test/tester/cppvtable/extends_plain.ts create mode 100644 tslang/test/tester/cppvtable/field_initializer.ts create mode 100644 tslang/test/tester/cppvtable/get_accessor.ts create mode 100644 tslang/test/tester/cppvtable/implements.ts create mode 100644 tslang/test/tester/cppvtable/instanceof.ts create mode 100644 tslang/test/tester/cppvtable/memory_model.ts create mode 100644 tslang/test/tester/cppvtable/method_body.ts create mode 100644 tslang/test/tester/cppvtable/plain_extends.ts create mode 100644 tslang/test/tester/cppvtable/static_member.ts create mode 100644 tslang/test/tester/cppvtable/to_interface.ts create mode 100644 tslang/test/tester/cppvtable/unbox_any.ts create mode 100644 tslang/test/tester/tests/00cppvtable_class.ts create mode 100644 tslang/test/tester/tests/export_cppvtable_class.ts create mode 100644 tslang/test/tester/tests/import_cppvtable_class.ts diff --git a/docs/c-cpp-header-import.md b/docs/c-cpp-header-import.md index 9a6dc6832..3364f0074 100644 --- a/docs/c-cpp-header-import.md +++ b/docs/c-cpp-header-import.md @@ -203,6 +203,11 @@ C is a 90%-mechanical translation; C++ is not. Realistic phasing: | C++ 1 | Free functions, non-template classes with methods (no virtuals crossing the boundary), overloads | Bind by **mangled name**: clang provides Itanium/MSVC mangled symbols (`clang::MangleContext`); emit `declare` per overload with the mangled name attached (`@linkname("?foo@@YAHH@Z")`, an alias of `@dllname`, makes the emitted `FuncOp` symbol differ from the TS-visible name). Methods become functions taking `this: Reference` first. Constructors/destructors bound explicitly; caller manages object memory (`GC_malloc_atomic` for PODs / explicit new-delete pair bindings). | 2–4 weeks after A | | C++ 2 | Templates, virtual dispatch, exceptions, STL types | **Do not bind directly.** Generate a flattened `extern "C"` wrapper `.cpp` (tsbindgen emits both the wrapper and its `.d.ts`), compiled with the system C++ compiler and linked via existing `-obj=`. Explicit template instantiations only; exceptions caught at the wrapper boundary and converted to error codes (C++ exceptions must never unwind into tslang frames — different personality functions, especially in JIT where Win64 unwind info is custom, see `docs/jit-gc-static-roots.md` context). | 3–6 weeks, per-need | +Virtual dispatch has since become bindable without a wrapper for single-inheritance +interfaces: a `@cppvtable` class calls a C++ object's methods through its own vtable (see +[cppvtable-classes.md](cppvtable-classes.md)). Templates, exceptions and STL types still need +the wrapper route. + The C++ ABI reality (mangling differences, MSVC vs Itanium, RTTI, std::string layout) is why every successful system (Swift, Rust `cxx` crate, Zig) either uses clang as the importer *and* restricts the surface, or generates C wrappers. The diff --git a/docs/cppvtable-classes.md b/docs/cppvtable-classes.md new file mode 100644 index 000000000..c0ba48b55 --- /dev/null +++ b/docs/cppvtable-classes.md @@ -0,0 +1,95 @@ +# `@cppvtable` classes — calling C++ virtual methods directly + +A class decorated with `@cppvtable` is laid over objects made by C++. tslang calls the methods +through the C++ object's own vtable, so a C++ interface such as NVRHI's `nvrhi::IDevice` can be +used from TypeScript without a flat `extern "C"` wrapper per method: + +```ts +@cppvtable +class NvResource { + _destructor(): void; // slot 0: the C++ virtual destructor, declared only to keep the count + AddRef(): u32; // slot 1 + Release(): u32; // slot 2 + GetRefCount(): u32; // slot 3 +} + +@cppvtable +class NvDevice extends NvResource { + // ... slots 4..38 ... + getGraphicsAPI(): u8; // slot 39 +} + +const device = Donut_GetNvrhiDevice(app); // an Opaque from C/C++ +print(device.getGraphicsAPI()); +``` + +## How it maps + +A tslang class already matches a C++ single-inheritance object on 64-bit targets in the +following ways: + +- the vtable pointer is the object's first word; +- a method call loads slot N of that table and calls it as `fn(this, args...)`, with no hidden + parameters, in the target's C calling convention (on x64 Windows that is the MSVC member + function convention); +- a base class's slots come first, and a base object is the prefix of a derived one. + +What `@cppvtable` changes is what the class adds to the vtable. An ordinary class has +`.instanceOf` in slot 0, `.new`, a slot for each implemented interface, and slots for its static +fields (`.rtti`, `.size`). A `@cppvtable` class has none of these. Its methods are the vtable's +slots from 0, in declaration order, with a base class's slots first. + +The class gets no vtable, constructor, RTTI or allocation of its own. It is a declaration: its +methods have no bodies, and an `abstract` class with `abstract` methods reads the same. + +Fields declared on the class are the C++ object's fields after the vtable pointer, laid out like +a tuple's. + +## Rules + +The compiler rejects anything that would add a slot, run code of its own, or ask the object what +it is. A slot number off by one calls a different C++ method with no error at all, which is why +these are errors and not warnings. + +| Not allowed | Why | +| --- | --- | +| a method with a body, a constructor, a field initializer | the objects and their code are C++'s | +| static members, `get`/`set` accessors, `accessor` fields | they would take vtable slots, and accessor slots don't follow declaration order | +| `implements` | an implemented interface takes a slot | +| `extends` an ordinary class, or an ordinary class extending a `@cppvtable` one | the ordinary class's slots (`.instanceOf`, `.rtti`, ...) would shift the numbering | +| `instanceof`, a cast to or from `any`, a cast to an interface | each asks the object through slot 0, which is a C++ method | +| `-mm=rc` / `-mm=own` | retains and releases write a count in the word before an object, inside the C++ object | +| a 32-bit x86 MSVC target | C++ member functions are `__thiscall` there (`this` in ECX) | + +A generic or static class can't be `@cppvtable` either. + +## Writing a binding + +- **Slot numbers.** Take them from the compiler, not by counting: MSVC prints a class's vtable + with `cl /c /Zs /d1reportSingleClassLayout file.cpp`. Declare unused slots with + placeholder names (`_slot17(): void;`) so the methods you call keep their numbers. +- **The destructor** is one slot under MSVC (the scalar deleting destructor) and two under the + Itanium ABI. Don't call it: release objects the way the C++ library does (`Release()`). +- **Overloads.** tslang methods are identified by name, so every C++ overload needs its own + TypeScript name. MSVC groups overloads in the vtable, and not always in declaration order: + check the slot numbers in the layout report. +- **Struct returned by value.** A C++ method that returns a class or struct by value (for + example `nvrhi::RefCountPtr`) returns it through a hidden pointer. Under MSVC that pointer + is passed after `this`, and the method returns it. Declare the method with that parameter: + + ```ts + createBuffer(ret: Ref, desc: Ref): Opaque; // RefCountPtr + ``` + + The pointer it writes holds a reference, which the caller gives back with `Release()`. +- **Structs by reference** (`const BufferDesc&`) are a `Ref` to a named tuple with the C++ + layout. That layout depends on the C++ build: `std::string`, for example, is 32 bytes with the + release CRT and 40 with the debug CRT. +- **Casting.** `opaqueValue` is a plain pointer cast with no check: the object must + really be one. + +## Tests + +- `tslang/test/tester/tests/00cppvtable_class.ts` checks the slots, fields, `extends` and + `abstract` against a vtable made by hand. It is registered for AOT and JIT. +- `tslang/test/tester/cppvtable/*.ts` holds one compile error per rule above. diff --git a/tslang/include/TypeScript/Defines.h b/tslang/include/TypeScript/Defines.h index 20eb53c15..1d89d1b43 100644 --- a/tslang/include/TypeScript/Defines.h +++ b/tslang/include/TypeScript/Defines.h @@ -192,6 +192,8 @@ #define DLL_IMPORT "dllimport" #define DLL_NAME "dllname" #define LINK_NAME "linkname" +// A class laid over an existing C++ object: its vtable is the C++ one (see ClassInfo::isCppVTable) +#define CPP_VTABLE "cppvtable" #if __LP64__ #define TRAMPOLINE_SIZE 48 diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h b/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h index 8cc25a074..019bd7a76 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h @@ -778,6 +778,11 @@ struct ClassInfo bool isPublic; bool isDynamicImport; bool hasRTTI; + // `@cppvtable`: a binding to C++ objects (MSVC/Itanium single inheritance) made elsewhere. The + // instance is the C++ object, its first word the C++ vtable, and the class's methods are that + // vtable's slots from 0 in declaration order, a base class's first: no `.instanceOf`, `.new`, + // interface or static-field slots, no RTTI, and no vtable or constructor of its own. + bool isCppVTable; ProcessingStages processingAtEvaluation; ProcessingStages processing; // the module `processing` was reached in: a discovery module is thrown away with what it holds @@ -788,7 +793,7 @@ struct ClassInfo ClassInfo() : isDeclaration(false), hasNew(false), hasConstructor(false), constructorAccessLevel(mlir_ts::AccessLevel::Public), hasInitializers(false), hasStaticConstructor(false), hasStaticInitializers(false), hasVirtualTable(false), isAbstract(false), isExport(false), isImport(false), - isPublic(false), isDynamicImport(false), hasRTTI(false), + isPublic(false), isDynamicImport(false), hasRTTI(false), isCppVTable(false), processingAtEvaluation(ProcessingStages::NotSet), processing(ProcessingStages::NotSet) { } @@ -901,7 +906,7 @@ struct ClassInfo // (mlirGenInstanceOfOpaque). A root class puts it there ahead of its interfaces, and a // derived class overrides it in place. Before this, a root class that implemented an // interface kept the interface's vtable in slot 0, and `anyValue` called it. - if (baseClasses.empty()) + if (baseClasses.empty() && !isCppVTable) { auto instanceOf = std::find_if(methods.begin(), methods.end(), [](auto &method) { return method.name == INSTANCEOF_NAME; }); @@ -912,7 +917,7 @@ struct ClassInfo } } - // do vtable for current class + // do vtable for current class (a @cppvtable class implements nothing, see isCppVTable) for (auto &implement : implements) { if (interfaceSlots.contains(implement.interface->fullName)) @@ -943,6 +948,11 @@ struct ClassInfo { continue; } +#else + if (method.isStatic && isCppVTable) + { + continue; + } #endif auto it = methodSlots.find(method.name); @@ -966,8 +976,8 @@ struct ClassInfo } #ifdef ADD_STATIC_MEMBERS_TO_VTABLE - // static fields - for (auto &staticField : staticFields) + // static fields (a C++ vtable has none) + for (auto &staticField : isCppVTable ? llvm::MutableArrayRef() : llvm::MutableArrayRef(staticFields)) { staticField.virtualIndex = vtable.size(); vtable.push_back({staticField, false}); diff --git a/tslang/include/TypeScript/TargetInfo.h b/tslang/include/TypeScript/TargetInfo.h index 12121550c..418a8d499 100644 --- a/tslang/include/TypeScript/TargetInfo.h +++ b/tslang/include/TypeScript/TargetInfo.h @@ -28,6 +28,10 @@ struct TargetInfo // Whether tslang.exe - an x64 process - can execute this target's code in-process. bool supportsInProcessJit = true; + // MSVC C++ member functions are __thiscall on 32-bit x86 (`this` in ECX), not the plain C + // convention a class method is called with, so a @cppvtable class can't call them. + bool memberCallsUseThiscall = false; + static TargetInfo fromTriple(const llvm::Triple &target, const llvm::Triple &host) { TargetInfo info; @@ -50,6 +54,7 @@ struct TargetInfo info.stdcallDecoratesCxxThrow = msvc && x86_32; info.supportsInProcessJit = target.getArch() == host.getArch() && target.getOS() == host.getOS(); + info.memberCallsUseThiscall = msvc && x86_32; return info; } diff --git a/tslang/lib/TypeScript/DeclarationPrinter.cpp b/tslang/lib/TypeScript/DeclarationPrinter.cpp index 0bfeb5bcf..e9b4ee94d 100644 --- a/tslang/lib/TypeScript/DeclarationPrinter.cpp +++ b/tslang/lib/TypeScript/DeclarationPrinter.cpp @@ -429,6 +429,14 @@ namespace typescript printNamespaceBegin(classType->elementNamespace); printBeforeDeclaration(); + // as layout-relevant as `abstract` below: without it the importer numbers the slots of an + // ordinary class, `.instanceOf` first + if (classType->isCppVTable) + { + os << "@" CPP_VTABLE; + newline(); + } + // `abstract` is layout-relevant, not just semantic: mlirGenClassNew skips // synthesizing the `.new` method for abstract classes, so dropping the // modifier makes the reimporting module insert a `.new` vtable slot the diff --git a/tslang/lib/TypeScript/MLIRGenCast.cpp b/tslang/lib/TypeScript/MLIRGenCast.cpp index e050ca1d9..2c6713628 100644 --- a/tslang/lib/TypeScript/MLIRGenCast.cpp +++ b/tslang/lib/TypeScript/MLIRGenCast.cpp @@ -699,6 +699,15 @@ namespace mlirgen LLVM_DEBUG(llvm::dbgs() << "\n!! cast " << valueType << "\n -> " << type << "\n";); + // see isCppVTableClass: `any` is unboxed and an interface made by asking the object + if ((isa(valueType) && isCppVTableClass(type)) + || ((isa(type) || isa(type)) && isCppVTableClass(valueType))) + { + emitError(location) << "can't cast '" << to_print(valueType) << "' to '" << to_print(type) + << "': a @" CPP_VTABLE " class's objects are C++ objects, with no type information"; + return mlir::failure(); + } + if (mlir::failed(verifyCastPreconditions(location, type, valueType, disableStrictNullCheck))) { return mlir::failure(); diff --git a/tslang/lib/TypeScript/MLIRGenClasses.cpp b/tslang/lib/TypeScript/MLIRGenClasses.cpp index 8f6da5547..0e2c6b7f2 100644 --- a/tslang/lib/TypeScript/MLIRGenClasses.cpp +++ b/tslang/lib/TypeScript/MLIRGenClasses.cpp @@ -177,6 +177,12 @@ namespace mlirgen setProcessingState(newClassPtr, ProcessingStages::ProcessedStorageClass, genContext); + if (mlir::failed(mlirGenClassCppVTableCheck(location, classDeclarationAST, newClassPtr, classGenContext))) + { + setProcessingState(newClassPtr, ProcessingStages::ErrorInStorageClass, genContext); + return {mlir::failure(), ""}; + } + // if it is ClassExpression we need to know if it is declaration mlirGenClassCheckIfDeclaration(location, classDeclarationAST, newClassPtr, classGenContext); @@ -200,13 +206,16 @@ namespace mlirgen llvm::SmallVector virtualTable; newClassPtr->getVirtualTable(virtualTable); - if (!newClassPtr->isStatic) + // a @cppvtable class is never constructed, typed or allocated here: its objects come from C++ + auto hasOwnRuntimeMembers = !newClassPtr->isStatic && !newClassPtr->isCppVTable; + + if (hasOwnRuntimeMembers) { mlirGenClassDefaultConstructor(classDeclarationAST, newClassPtr, classGenContext); } #ifdef ENABLE_RTTI - if (!newClassPtr->isStatic) + if (hasOwnRuntimeMembers) { // INFO: .instanceOf must be first element in VTable for Cast Any mlirGenClassInstanceOfMethod(classDeclarationAST, newClassPtr, classGenContext); @@ -215,14 +224,14 @@ namespace mlirgen #if ENABLE_TYPED_GC auto enabledGC = compileOptions.needsGCRuntime(); - if (enabledGC && !newClassPtr->isStatic) + if (enabledGC && hasOwnRuntimeMembers) { mlirGenClassTypeBitmap(location, newClassPtr, classGenContext); mlirGenClassTypeDescriptorField(location, newClassPtr, classGenContext); } #endif - if (!newClassPtr->isStatic) + if (hasOwnRuntimeMembers) { mlirGenClassNew(classDeclarationAST, newClassPtr, classGenContext); } @@ -261,7 +270,7 @@ namespace mlirgen } } - if (!newClassPtr->isStatic) + if (hasOwnRuntimeMembers) { if (mlir::failed(mlirGenClassVirtualTableDefinition(location, newClassPtr, classGenContext))) { @@ -342,6 +351,15 @@ namespace mlirgen newClassPtr->isDynamicImport = true; } } + + // a binding to C++ objects: declared (its methods have no bodies), with a vtable + // field even with no `abstract` method (see ClassInfo::isCppVTable) + if (name == CPP_VTABLE) + { + newClassPtr->isCppVTable = true; + newClassPtr->isDeclaration = true; + newClassPtr->hasVirtualTable = true; + } }); getClassesMap().insert({namePtr, newClassPtr}); @@ -351,6 +369,122 @@ namespace mlirgen return newClassPtr; } + // A @cppvtable class only names the slots of a vtable made by C++, so everything that would add + // a slot, a field initializer or code of its own is an error, and so is a hierarchy mixing it + // with ordinary classes: a slot number off by one calls the wrong C++ method, with no error. + mlir::LogicalResult MLIRGenImpl::mlirGenClassCppVTableCheck(mlir::Location location, + ClassLikeDeclaration classDeclarationAST, + ClassInfo::TypePtr newClassPtr, const GenContext &genContext) + { + if (!newClassPtr->isCppVTable) + { + // an ordinary class can't add to a C++ vtable + for (auto &base : newClassPtr->baseClasses) + { + if (base->isCppVTable) + { + emitError(location) << "class '" << newClassPtr->fullName << "' can't extend the @" CPP_VTABLE " class '" + << base->fullName << "': it has to be @" CPP_VTABLE " too"; + return mlir::failure(); + } + } + + return mlir::success(); + } + + auto failed = false; + auto error = [&](mlir::Location errorLocation) -> mlir::InFlightDiagnostic { + failed = true; + return emitError(errorLocation); + }; + + if (compileOptions.tracksOwnership()) + { + // retains and releases write a count in the word before an object, inside a C++ object + error(location) << "a @" CPP_VTABLE " class needs the gc or none memory model"; + } + + if (compileOptions.targetInfo.memberCallsUseThiscall) + { + error(location) << "a @" CPP_VTABLE " class can't be used on this target: its C++ methods are __thiscall"; + } + + if (newClassPtr->isStatic) + { + error(location) << "a static class can't be @" CPP_VTABLE; + } + + if (classDeclarationAST->typeParameters.size() > 0) + { + error(location) << "a generic class can't be @" CPP_VTABLE; + } + + for (auto &base : newClassPtr->baseClasses) + { + if (!base->isCppVTable) + { + error(location) << "the @" CPP_VTABLE " class '" << newClassPtr->fullName << "' can only extend a @" CPP_VTABLE + << " class, not '" << base->fullName << "'"; + } + } + + for (auto &heritageClause : classDeclarationAST->heritageClauses) + { + if (heritageClause->token == SyntaxKind::ImplementsKeyword) + { + error(loc(heritageClause)) << "a @" CPP_VTABLE " class can't implement an interface"; + } + } + + for (auto &classMember : classDeclarationAST->members) + { + auto memberLocation = loc(classMember); + if (hasModifier(classMember, SyntaxKind::StaticKeyword)) + { + error(memberLocation) << "a @" CPP_VTABLE " class can't have static members"; + continue; + } + + if (classMember == SyntaxKind::MethodDeclaration) + { + if (classMember.as()->body) + { + error(memberLocation) << "a method of a @" CPP_VTABLE " class is a C++ vtable slot: it can't have a body"; + } + + continue; + } + + if (classMember == SyntaxKind::PropertyDeclaration) + { + // a field of the C++ object, after its vtable pointer + auto propertyDeclaration = classMember.as(); + if (propertyDeclaration->initializer) + { + error(memberLocation) << "a field of a @" CPP_VTABLE " class can't have an initializer"; + } + + if (hasModifier(classMember, SyntaxKind::AccessorKeyword)) + { + error(memberLocation) << "a @" CPP_VTABLE " class can't have accessor fields"; + } + + continue; + } + + if (classMember == SyntaxKind::Constructor) + { + error(memberLocation) << "a @" CPP_VTABLE " class can't have a constructor: its objects are made by C++"; + continue; + } + + // accessors are generated methods: their slots wouldn't follow the declaration order + error(memberLocation) << "a @" CPP_VTABLE " class can only have methods without bodies and fields"; + } + + return failed ? mlir::failure() : mlir::success(); + } + mlir::LogicalResult MLIRGenImpl::mlirGenClassCheckIfDeclaration(mlir::Location location, ClassLikeDeclaration classDeclarationAST, ClassInfo::TypePtr newClassPtr, const GenContext &genContext) @@ -426,7 +560,11 @@ namespace mlirgen } #if ENABLE_RTTI - if (newClassPtr->isDynamicImport) + if (newClassPtr->isCppVTable) + { + // no RTTI: a C++ object can't answer `instanceof` + } + else if (newClassPtr->isDynamicImport) { mlirGenCustomRTTIDynamicImport(location, classDeclarationAST, newClassPtr, genContext); } @@ -437,7 +575,7 @@ namespace mlirgen } #endif - if (!newClassPtr->isStatic) + if (!newClassPtr->isStatic && !newClassPtr->isCppVTable) { mlirGenClassSizeStaticField(location, classDeclarationAST, newClassPtr, genContext); } diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 2d242308b..63aac409e 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -5335,6 +5335,30 @@ class MLIRGenImpl return mlir::success(); } + // A @cppvtable class's objects are C++ objects: their slot 0 is a C++ method (the destructor), + // not `.instanceOf`, and the class has no RTTI, so nothing may ask one what it is - not + // `instanceof`, nor boxing into `any` (unboxed by asking), nor an interface (made by asking). + bool isCppVTableClass(mlir::Type type) + { + if (auto classType = dyn_cast(type)) + { + auto classInfo = getClassInfoByFullName(classType.getName().getValue()); + return classInfo && classInfo->isCppVTable; + } + + if (auto optionalType = dyn_cast(type)) + { + return isCppVTableClass(optionalType.getElementType()); + } + + if (auto unionType = dyn_cast(type)) + { + return llvm::any_of(unionType.getTypes(), [&](mlir::Type member) { return isCppVTableClass(member); }); + } + + return false; + } + // A failure here has to be a failure, not a null value: ValueOrLogicalResult takes a null // mlir::Value as success with no value, and an `if` whose condition has no value quietly // generates nothing - `if (a instanceof C) return a;` in the generated `___unbox` lost its @@ -5559,6 +5583,12 @@ class MLIRGenImpl rightType = mth.wideStorageType(rightType); + if (isCppVTableClass(rightType) || isCppVTableClass(resultLeftfType)) + { + emitError(location, "instanceof can't be used with a @" CPP_VTABLE " class: its objects are C++ objects, with no type information"); + return mlir::failure(); + } + #ifdef ENABLE_RTTI if (auto classType = dyn_cast(rightType)) { @@ -11171,6 +11201,10 @@ class MLIRGenImpl return mlir::failure(); } + mlir::LogicalResult mlirGenClassCppVTableCheck(mlir::Location location, + ClassLikeDeclaration classDeclarationAST, + ClassInfo::TypePtr newClassPtr, const GenContext &genContext); + mlir::LogicalResult mlirGenClassCheckIfDeclaration(mlir::Location location, ClassLikeDeclaration classDeclarationAST, ClassInfo::TypePtr newClassPtr, const GenContext &genContext); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index ea2c210bf..e8302c4b6 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -418,6 +418,8 @@ tslang_add_test(NAME test-compile-00-optional-loose-null COMMAND test-runner "${ tslang_add_test(NAME test-compile-00-union-nullable-array COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_nullable_array.ts") tslang_add_test(NAME test-compile-00-union-nullable-value COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_nullable_value.ts") tslang_add_test(NAME test-compile-00-opaque-nullable-compare COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00opaque_nullable_compare.ts") +# not in TSLANG_CORPUS: a @cppvtable class is an error under rc (cppvtable/memory_model.ts) +tslang_add_test(NAME test-compile-00-cppvtable-class COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00cppvtable_class.ts") tslang_add_test(NAME test-compile-00-union-value-equality COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_value_equality.ts") tslang_add_test(NAME test-compile-00-strict-equals-mismatched-types COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00strict_equals_mismatched_types.ts") tslang_add_test(NAME test-compile-00-any-number-compare COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00any_number_compare.ts") @@ -947,6 +949,7 @@ tslang_add_test(NAME test-jit-00-optional-loose-null COMMAND test-runner -jit "$ tslang_add_test(NAME test-jit-00-union-nullable-array COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_nullable_array.ts") tslang_add_test(NAME test-jit-00-union-nullable-value COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_nullable_value.ts") tslang_add_test(NAME test-jit-00-opaque-nullable-compare COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00opaque_nullable_compare.ts") +tslang_add_test(NAME test-jit-00-cppvtable-class COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00cppvtable_class.ts") tslang_add_test(NAME test-jit-00-union-value-equality COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_value_equality.ts") tslang_add_test(NAME test-jit-00-strict-equals-mismatched-types COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00strict_equals_mismatched_types.ts") tslang_add_test(NAME test-jit-00-any-number-compare COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00any_number_compare.ts") @@ -1464,6 +1467,7 @@ tslang_add_test(NAME test-compile-shared-export-import-class-implements-interfac # minimal deterministic regression pair for the vtable-slot-shift bug above (single-level abstract, # describe() called twice before any area() call so XMM0 luck can't hide a regression) tslang_add_test(NAME test-compile-shared-export-import-class-abstract-virtual-dispatch COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_abstract_virtual_dispatch.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_abstract_virtual_dispatch.ts") +tslang_add_test(NAME test-compile-shared-export-import-cppvtable-class COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_cppvtable_class.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_cppvtable_class.ts") # FIXED: see the matching test-compile-export-import-class-generic comment above (2026-07-22). tslang_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") tslang_add_test(NAME test-compile-shared-export-import-function-generic COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_function_generic.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_function_generic.ts") @@ -1546,6 +1550,7 @@ tslang_add_test(NAME test-jit-shared-export-import-class-structural-interface CO # FIXED 2026-07-23 (was disabled): see the matching test-compile-shared-export-import-class-implements-interface-abstract comment above (DeclarationPrinter dropped `abstract`, shifting the reimported vtable's slots). tslang_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") tslang_add_test(NAME test-jit-shared-export-import-class-abstract-virtual-dispatch COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_abstract_virtual_dispatch.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_abstract_virtual_dispatch.ts") +tslang_add_test(NAME test-jit-shared-export-import-cppvtable-class COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_cppvtable_class.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_cppvtable_class.ts") # FIXED: see the matching test-compile-export-import-class-generic comment above (2026-07-22). tslang_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") tslang_add_test(NAME test-jit-shared-export-import-function-generic COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_function_generic.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_function_generic.ts") @@ -2680,6 +2685,60 @@ add_test(NAME test-compile-interface-cast-empty set_tests_properties(test-compile-interface-cast-empty PROPERTIES FAIL_REGULAR_EXPRESSION "error:|Stack dump|Assertion failed") +# A @cppvtable class names the slots of a vtable made by C++ (tests/00cppvtable_class.ts), so what +# would add a slot, run code of its own or ask the object what it is is an error: a slot off by one +# would call the wrong C++ method, with no error at all. +set(cppvtable_errors + "instanceof|instanceof can't be used with a @cppvtable class" + "box_any|can't cast 'Base' to 'any': a @cppvtable class's objects are C\\+\\+ objects" + "unbox_any|can't cast 'any' to 'Base': a @cppvtable class's objects are C\\+\\+ objects" + "to_interface|can't cast 'Base' to 'IFirst': a @cppvtable class's objects are C\\+\\+ objects" + "extends_plain|the @cppvtable class 'Base' can only extend a @cppvtable class, not 'Plain'" + "plain_extends|class 'Plain' can't extend the @cppvtable class 'Base': it has to be @cppvtable too" + "implements|a @cppvtable class can't implement an interface" + "method_body|a method of a @cppvtable class is a C\\+\\+ vtable slot: it can't have a body" + "static_member|a @cppvtable class can't have static members" + "constructor|a @cppvtable class can't have a constructor" + "get_accessor|a @cppvtable class can only have methods without bodies and fields" + "field_initializer|a field of a @cppvtable class can't have an initializer") +foreach(cppvtable_error ${cppvtable_errors}) + string(REPLACE "|" ";" cppvtable_error "${cppvtable_error}") + list(GET cppvtable_error 0 cppvtable_name) + list(GET cppvtable_error 1 cppvtable_message) + add_test(NAME test-compile-cppvtable-${cppvtable_name} + COMMAND $ --emit=obj --no-default-lib -mm=none + "${PROJECT_SOURCE_DIR}/test/tester/cppvtable/${cppvtable_name}.ts" + -o "${CMAKE_CURRENT_BINARY_DIR}/cppvtable-${cppvtable_name}.obj") + set_tests_properties(test-compile-cppvtable-${cppvtable_name} + PROPERTIES PASS_REGULAR_EXPRESSION "${cppvtable_name}\\.ts:[0-9]+:[0-9]+: error: ${cppvtable_message}" + FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") +endforeach() + +# Retains and releases would write a count in the word before a C++ object: rc is an error, none +# compiles. +add_test(NAME test-compile-cppvtable-memory-model-rc + COMMAND $ --emit=obj --no-default-lib -mm=rc + "${PROJECT_SOURCE_DIR}/test/tester/cppvtable/memory_model.ts" + -o "${CMAKE_CURRENT_BINARY_DIR}/cppvtable-memory-model-rc.obj") +set_tests_properties(test-compile-cppvtable-memory-model-rc + PROPERTIES PASS_REGULAR_EXPRESSION "memory_model\\.ts:3:1: error: a @cppvtable class needs the gc or none memory model" + FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") +add_test(NAME test-compile-cppvtable-memory-model-none + COMMAND $ --emit=obj --no-default-lib -mm=none + "${PROJECT_SOURCE_DIR}/test/tester/cppvtable/memory_model.ts" + -o "${CMAKE_CURRENT_BINARY_DIR}/cppvtable-memory-model-none.obj") +set_tests_properties(test-compile-cppvtable-memory-model-none + PROPERTIES FAIL_REGULAR_EXPRESSION "error:|Stack dump|Assertion failed") + +# MSVC member functions are __thiscall on 32-bit x86, which a class method call isn't. +add_test(NAME test-compile-cppvtable-thiscall-target + COMMAND $ --emit=obj --no-default-lib -mm=none -mtriple=i686-pc-windows-msvc + "${PROJECT_SOURCE_DIR}/test/tester/cppvtable/memory_model.ts" + -o "${CMAKE_CURRENT_BINARY_DIR}/cppvtable-thiscall-target.obj") +set_tests_properties(test-compile-cppvtable-thiscall-target + PROPERTIES PASS_REGULAR_EXPRESSION "memory_model\\.ts:3:1: error: a @cppvtable class can't be used on this target: its C\\+\\+ methods are __thiscall" + FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") + # An error inside a cast function the compiler generates (here: null in a union cast to string) is # reported where the cast is - line 6 - and not at the generated text's offset taken as a position # in the user's file, which put it inside the comment on line 1. diff --git a/tslang/test/tester/cppvtable/box_any.ts b/tslang/test/tester/cppvtable/box_any.ts new file mode 100644 index 000000000..4b5251f44 --- /dev/null +++ b/tslang/test/tester/cppvtable/box_any.ts @@ -0,0 +1,11 @@ +// An `any` is unboxed by asking the object what it is, which a C++ object can't answer. +@cppvtable +class Base { + first(): s32; +} + +function main() { + const b = 0; + const a: any = b; + print(a); +} diff --git a/tslang/test/tester/cppvtable/constructor.ts b/tslang/test/tester/cppvtable/constructor.ts new file mode 100644 index 000000000..ec7a974b7 --- /dev/null +++ b/tslang/test/tester/cppvtable/constructor.ts @@ -0,0 +1,10 @@ +// A @cppvtable class's objects are made by C++. +@cppvtable +class Base { + constructor() {} + first(): s32; +} + +function main() { + print("done."); +} diff --git a/tslang/test/tester/cppvtable/extends_plain.ts b/tslang/test/tester/cppvtable/extends_plain.ts new file mode 100644 index 000000000..d22d4ce0d --- /dev/null +++ b/tslang/test/tester/cppvtable/extends_plain.ts @@ -0,0 +1,13 @@ +// A @cppvtable class extending an ordinary class would have the ordinary class's slots first. +class Plain { + m(): s32 { return 1; } +} + +@cppvtable +class Base extends Plain { + first(): s32; +} + +function main() { + print("done."); +} diff --git a/tslang/test/tester/cppvtable/field_initializer.ts b/tslang/test/tester/cppvtable/field_initializer.ts new file mode 100644 index 000000000..d46e050b7 --- /dev/null +++ b/tslang/test/tester/cppvtable/field_initializer.ts @@ -0,0 +1,10 @@ +// A field initializer is code run by a constructor, which a @cppvtable class doesn't have. +@cppvtable +class Base { + value: s32 = 1; + first(): s32; +} + +function main() { + print("done."); +} diff --git a/tslang/test/tester/cppvtable/get_accessor.ts b/tslang/test/tester/cppvtable/get_accessor.ts new file mode 100644 index 000000000..ae1d4c100 --- /dev/null +++ b/tslang/test/tester/cppvtable/get_accessor.ts @@ -0,0 +1,10 @@ +// An accessor is a generated method, and its slot wouldn't follow the declaration order. +@cppvtable +class Base { + get size(): s32; + first(): s32; +} + +function main() { + print("done."); +} diff --git a/tslang/test/tester/cppvtable/implements.ts b/tslang/test/tester/cppvtable/implements.ts new file mode 100644 index 000000000..449d10c07 --- /dev/null +++ b/tslang/test/tester/cppvtable/implements.ts @@ -0,0 +1,13 @@ +// An interface takes a slot in an ordinary class's vtable. +interface IFirst { + first(): s32; +} + +@cppvtable +class Base implements IFirst { + first(): s32; +} + +function main() { + print("done."); +} diff --git a/tslang/test/tester/cppvtable/instanceof.ts b/tslang/test/tester/cppvtable/instanceof.ts new file mode 100644 index 000000000..6a7efb7c7 --- /dev/null +++ b/tslang/test/tester/cppvtable/instanceof.ts @@ -0,0 +1,13 @@ +// `instanceof` asks slot 0, which is a C++ method in a @cppvtable class's vtable. +@cppvtable +class Base { + first(): s32; +} + +class Plain {} + +function main() { + const p = new Plain(); + const b = p; + print(b instanceof Plain); +} diff --git a/tslang/test/tester/cppvtable/memory_model.ts b/tslang/test/tester/cppvtable/memory_model.ts new file mode 100644 index 000000000..462627735 --- /dev/null +++ b/tslang/test/tester/cppvtable/memory_model.ts @@ -0,0 +1,14 @@ +// Compiled with -mm=rc: retains and releases would write the count in the word before a C++ +// object. The same file compiles with -mm=none. +@cppvtable +class Base { + first(): s32; +} + +function call(b: Base) { + print(b.first()); +} + +function main() { + print("done."); +} diff --git a/tslang/test/tester/cppvtable/method_body.ts b/tslang/test/tester/cppvtable/method_body.ts new file mode 100644 index 000000000..7b65af3ce --- /dev/null +++ b/tslang/test/tester/cppvtable/method_body.ts @@ -0,0 +1,9 @@ +// A method of a @cppvtable class is a slot of the C++ vtable, not code of its own. +@cppvtable +class Base { + first(): s32 { return 1; } +} + +function main() { + print("done."); +} diff --git a/tslang/test/tester/cppvtable/plain_extends.ts b/tslang/test/tester/cppvtable/plain_extends.ts new file mode 100644 index 000000000..0c47abd71 --- /dev/null +++ b/tslang/test/tester/cppvtable/plain_extends.ts @@ -0,0 +1,13 @@ +// An ordinary class extending a @cppvtable class would add slots C++ doesn't have. +@cppvtable +class Base { + first(): s32; +} + +class Plain extends Base { + second(): s32 { return 2; } +} + +function main() { + print("done."); +} diff --git a/tslang/test/tester/cppvtable/static_member.ts b/tslang/test/tester/cppvtable/static_member.ts new file mode 100644 index 000000000..95897ab9e --- /dev/null +++ b/tslang/test/tester/cppvtable/static_member.ts @@ -0,0 +1,10 @@ +// A static member would take a vtable slot. +@cppvtable +class Base { + static count: s32; + first(): s32; +} + +function main() { + print("done."); +} diff --git a/tslang/test/tester/cppvtable/to_interface.ts b/tslang/test/tester/cppvtable/to_interface.ts new file mode 100644 index 000000000..846ee9ca5 --- /dev/null +++ b/tslang/test/tester/cppvtable/to_interface.ts @@ -0,0 +1,15 @@ +// An interface over a class object is found through the object's vtable, which is the C++ one. +@cppvtable +class Base { + first(): s32; +} + +interface IFirst { + first(): s32; +} + +function main() { + const b = 0; + const i: IFirst = b; + print(i.first()); +} diff --git a/tslang/test/tester/cppvtable/unbox_any.ts b/tslang/test/tester/cppvtable/unbox_any.ts new file mode 100644 index 000000000..364641ca5 --- /dev/null +++ b/tslang/test/tester/cppvtable/unbox_any.ts @@ -0,0 +1,11 @@ +// Unboxing an `any` to a @cppvtable class would ask the object what it is. +@cppvtable +class Base { + first(): s32; +} + +function main() { + const a: any = 1; + const b = a; + print(b.first()); +} diff --git a/tslang/test/tester/tests/00cppvtable_class.ts b/tslang/test/tester/tests/00cppvtable_class.ts new file mode 100644 index 000000000..562abfdc8 --- /dev/null +++ b/tslang/test/tester/tests/00cppvtable_class.ts @@ -0,0 +1,61 @@ +// A @cppvtable class is laid over an object made elsewhere (C++): its first word is the vtable, the +// class's methods are that vtable's slots from 0 in declaration order, a base class's first, and +// each is called as fn(this, args). The "C++ object" here is made by hand: a table of functions and +// a tuple whose first field points at it. + +@cppvtable +class Base { + value: s32; // the object's first field, after the vtable pointer + first(): s32; // slot 0 + second(x: s32): s32; // slot 1 +} + +@cppvtable +class Derived extends Base { + third(x: s32, y: s32): s32; // slot 2 +} + +// `abstract` reads the same +@cppvtable +abstract class AbstractBase { + abstract first(): s32; // slot 0 + abstract second(x: s32): s32; // slot 1 +} + +type ObjectData = [vtable: Opaque, value: s32]; + +function slot0(self: Opaque): s32 { + return 10; +} + +function slot1(self: Opaque, x: s32): s32 { + return Deref(>self).value + x; +} + +function slot2(self: Opaque, x: s32, y: s32): s32 { + return x * y; +} + +function main() { + let vtable: Opaque[] = [slot0, slot1, slot2]; + let data: ObjectData = [Ref(vtable[0]), 32]; + const object = Ref(data); + + const base = object; + assert(base.value == 32, "the first field follows the vtable pointer"); + assert(base.first() == 10, "slot 0"); + assert(base.second(5) == 37, "slot 1 gets this"); + + const derived = object; + assert(derived.first() == 10, "a base method keeps its slot"); + assert(derived.third(6, 7) == 42, "a derived method follows the base's slots"); + + const asBase: Base = derived; + assert(asBase.second(1) == 33, "a derived object is a base object"); + + const abstractBase = object; + assert(abstractBase.first() == 10, "abstract slot 0"); + assert(abstractBase.second(8) == 40, "abstract slot 1"); + + print("done."); +} diff --git a/tslang/test/tester/tests/export_cppvtable_class.ts b/tslang/test/tester/tests/export_cppvtable_class.ts new file mode 100644 index 000000000..540a3debd --- /dev/null +++ b/tslang/test/tester/tests/export_cppvtable_class.ts @@ -0,0 +1,23 @@ +namespace M { + + // A @cppvtable class exported from one module and used from another: the declaration embedded + // in the binary (__decls) keeps the decorator, so the importer numbers the slots the same way + // - from 0, with no `.instanceOf` - and both modules call the same C++ method for second(). + + @cppvtable + export class Base { + value: s32; + first(): s32; // slot 0 + second(x: s32): s32; // slot 1 + } + + @cppvtable + export class Derived extends Base { + third(x: s32, y: s32): s32; // slot 2 + } + + // called through this module's numbering + export function callSecond(object: Base, x: s32): s32 { + return object.second(x); + } +} diff --git a/tslang/test/tester/tests/import_cppvtable_class.ts b/tslang/test/tester/tests/import_cppvtable_class.ts new file mode 100644 index 000000000..e1c43f52b --- /dev/null +++ b/tslang/test/tester/tests/import_cppvtable_class.ts @@ -0,0 +1,33 @@ +import './export_cppvtable_class' + +// The "C++ object" made by hand, as in 00cppvtable_class.ts: a table of functions and a tuple +// whose first field points at it. + +type ObjectData = [vtable: Opaque, value: s32]; + +function slot0(self: Opaque): s32 { + return 10; +} + +function slot1(self: Opaque, x: s32): s32 { + return Deref(>self).value + x; +} + +function slot2(self: Opaque, x: s32, y: s32): s32 { + return x * y; +} + +function main() { + let vtable: Opaque[] = [slot0, slot1, slot2]; + let data: ObjectData = [Ref(vtable[0]), 32]; + const object = Ref(data); + + const derived = object; + assert(derived.value == 32, "the first field follows the vtable pointer"); + assert(derived.first() == 10, "slot 0"); + assert(derived.second(5) == 37, "slot 1"); + assert(derived.third(6, 7) == 42, "slot 2"); + assert(M.callSecond(derived, 8) == 40, "the exporting module calls the same slot"); + + print("done."); +} diff --git a/tslang/test/tester/verify-ownership.cmake b/tslang/test/tester/verify-ownership.cmake index 16a83f452..4880fa930 100644 --- a/tslang/test/tester/verify-ownership.cmake +++ b/tslang/test/tester/verify-ownership.cmake @@ -34,7 +34,11 @@ set(not_compilable_alone 00switch_state.ts import_vars.ts # written for `-nostrictnull` - raytrace-0.ts) + raytrace-0.ts + # a @cppvtable class is an error under rc and own (cppvtable/memory_model.ts) + 00cppvtable_class.ts + export_cppvtable_class.ts + import_cppvtable_class.ts) file(GLOB corpus "${TESTS_DIR}/*.ts") list(SORT corpus) diff --git a/tslang/test/tester/x86-exclusions.cmake b/tslang/test/tester/x86-exclusions.cmake index ed2bb7797..85e3562cd 100644 --- a/tslang/test/tester/x86-exclusions.cmake +++ b/tslang/test/tester/x86-exclusions.cmake @@ -7,10 +7,17 @@ set(TSLANG_X86_EXCLUDED test-compile-internals + test-compile-00-cppvtable-class + test-compile-shared-export-import-cppvtable-class test-compile-rc-corpus-internals test-compile-none-corpus-internals ) +set(TSLANG_X86_EXCLUDED_REASON_test-compile-00-cppvtable-class + "a @cppvtable class is an error on i686: MSVC member functions are __thiscall there") +set(TSLANG_X86_EXCLUDED_REASON_test-compile-shared-export-import-cppvtable-class + "a @cppvtable class is an error on i686: MSVC member functions are __thiscall there") + # internals.ts, under each model: its first assert, on `inline_asm("xor $0, $0", "=r,r", v1)`, # fails at i686 (0xC0000409 before the first print). set(TSLANG_X86_EXCLUDED_REASON_test-compile-internals