From 13e00b80c1b624911fd7971189f933752a582bf9 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 11 Oct 2026 20:10:43 +0100 Subject: [PATCH 1/3] @cppvtable: methods returning a record through MSVC's hidden pointer MSVC returns a record (struct or class, RefCountPtr included) from a C++ instance method through a hidden pointer. The pointer is passed after `this` and returned as well (clang's MicrosoftCXXABI::classifyReturnType). A @cppvtable method declared to return a tuple now uses that convention: - its slot is called as (this, Ref, args...) -> Ref; - a call passes a local of T and reads it back, so the call's value is the record; - DeclarationPrinter keeps the declared type, so an importing module lowers it again. Before this, the hidden pointer had to be declared as a parameter. The Itanium ABI passes the pointer before `this`, and only for some records, so on a non-MSVC target such a method is an error (TargetInfo::hiddenReturnPointerAfterThis). Tests: - 00cppvtable_hidden_return.ts covers 8- and 24-byte records and a scalar slot next to them, compiled and JIT; - export/import_cppvtable_class cover a record returned across modules; - cppvtable/hidden_return_target.ts checks the non-MSVC error. Co-Authored-By: Claude Opus 5.5 --- docs/cppvtable-classes.md | 22 ++++++-- .../TypeScript/MLIRLogic/MLIRGenStore.h | 4 ++ tslang/include/TypeScript/TargetInfo.h | 7 +++ tslang/lib/TypeScript/DeclarationPrinter.cpp | 6 ++- tslang/lib/TypeScript/MLIRGenAccessCall.cpp | 22 ++++++++ tslang/lib/TypeScript/MLIRGenClasses.cpp | 52 +++++++++++++++++++ tslang/lib/TypeScript/MLIRGenExpressions.cpp | 5 ++ tslang/lib/TypeScript/MLIRGenImpl.h | 22 ++++++++ tslang/test/tester/CMakeLists.txt | 12 +++++ .../tester/cppvtable/hidden_return_target.ts | 12 +++++ .../tester/tests/00cppvtable_hidden_return.ts | 52 +++++++++++++++++++ .../tester/tests/export_cppvtable_class.ts | 7 +++ .../tester/tests/import_cppvtable_class.ts | 15 +++++- tslang/test/tester/verify-ownership.cmake | 1 + tslang/test/tester/x86-exclusions.cmake | 3 ++ 15 files changed, 235 insertions(+), 7 deletions(-) create mode 100644 tslang/test/tester/cppvtable/hidden_return_target.ts create mode 100644 tslang/test/tester/tests/00cppvtable_hidden_return.ts diff --git a/docs/cppvtable-classes.md b/docs/cppvtable-classes.md index c0ba48b55..d50e794db 100644 --- a/docs/cppvtable-classes.md +++ b/docs/cppvtable-classes.md @@ -60,6 +60,7 @@ these are errors and not warnings. | `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 method returning a tuple, on a non-MSVC target | the Itanium ABI returns records otherwise (see below) | A generic or static class can't be `@cppvtable` either. @@ -74,14 +75,27 @@ A generic or static class can't be `@cppvtable` either. 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: + example `nvrhi::RefCountPtr` or `nvrhi::Object`) returns a record. Declare its return type + as a tuple with the record's layout: ```ts - createBuffer(ret: Ref, desc: Ref): Opaque; // RefCountPtr + type RefCountPtr = [pointer: Opaque | null]; // nvrhi::RefCountPtr + createBuffer(desc: Ref): RefCountPtr; // const handle = device.createBuffer(Ref(desc)) ``` - The pointer it writes holds a reference, which the caller gives back with `Release()`. + MSVC returns a record from an instance method through a hidden pointer, whatever its size. The + pointer is passed after `this`, and the method returns it as well. A call to such a method + passes a local for the result and reads it back, so the call's value is the record. This is + clang's `MicrosoftCXXABI::classifyReturnType`: "MSVC always returns structs indirectly from C++ + instance methods". A method returning a class (`getLayout(): IBindingLayout`, a C++ pointer) or + a number still returns it in a register. + + The record isn't destroyed: a `RefCountPtr` the method returns holds a reference, which the + caller gives back with `Release()`. The Itanium ABI passes the pointer before `this`, and only + for some records, so on a non-MSVC target such a method is an error. A method stored in a + variable and called later takes the hidden pointer as its first argument; call it directly. + tslang pre-alpha94 and earlier need the hidden pointer declared as a parameter: + `createBuffer(ret: Ref, desc: Ref): Opaque`. - **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. diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h b/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h index 019bd7a76..09dd23aa2 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h @@ -151,6 +151,10 @@ struct MethodInfo mlir_ts::AccessLevel accessLevel; // @dllname: exported symbol name, empty when the method keeps funcName std::string dllName; + // A @cppvtable method returning a C++ record: the type it is declared with (funcType is the + // one its slot is called with, the hidden result pointer after `this`, see + // mlirGenClassCppVTableHiddenReturn); null for any other method. + mlir_ts::FunctionType declaredFuncType; }; struct GenericMethodInfo diff --git a/tslang/include/TypeScript/TargetInfo.h b/tslang/include/TypeScript/TargetInfo.h index 418a8d499..a44108fce 100644 --- a/tslang/include/TypeScript/TargetInfo.h +++ b/tslang/include/TypeScript/TargetInfo.h @@ -32,6 +32,12 @@ struct TargetInfo // convention a class method is called with, so a @cppvtable class can't call them. bool memberCallsUseThiscall = false; + // MSVC returns a C++ record (struct, class) from an instance method through a hidden pointer, + // passed after `this` and returned in turn (clang's MicrosoftCXXABI::classifyReturnType): the + // convention of a @cppvtable method declared to return a tuple. The Itanium ABI passes it + // before `this`, and only for some records. + bool hiddenReturnPointerAfterThis = false; + static TargetInfo fromTriple(const llvm::Triple &target, const llvm::Triple &host) { TargetInfo info; @@ -55,6 +61,7 @@ struct TargetInfo info.supportsInProcessJit = target.getArch() == host.getArch() && target.getOS() == host.getOS(); info.memberCallsUseThiscall = msvc && x86_32; + info.hiddenReturnPointerAfterThis = msvc; return info; } diff --git a/tslang/lib/TypeScript/DeclarationPrinter.cpp b/tslang/lib/TypeScript/DeclarationPrinter.cpp index e9b4ee94d..78c7a5638 100644 --- a/tslang/lib/TypeScript/DeclarationPrinter.cpp +++ b/tslang/lib/TypeScript/DeclarationPrinter.cpp @@ -662,11 +662,13 @@ namespace typescript os << "abstract "; } + // a @cppvtable method returning a record: as declared, the importer lowers it again + auto funcType = method.declaredFuncType ? method.declaredFuncType : method.funcType; printMethod( method.isStatic, method.name, - method.funcType.getParams(), - method.funcType.getNumResults() > 0 ? method.funcType.getResult(0) : mlir::Type(), + funcType.getParams(), + funcType.getNumResults() > 0 ? funcType.getResult(0) : mlir::Type(), classType->classType); newline(); diff --git a/tslang/lib/TypeScript/MLIRGenAccessCall.cpp b/tslang/lib/TypeScript/MLIRGenAccessCall.cpp index e77c08c66..52631faf9 100644 --- a/tslang/lib/TypeScript/MLIRGenAccessCall.cpp +++ b/tslang/lib/TypeScript/MLIRGenAccessCall.cpp @@ -1751,6 +1751,28 @@ namespace mlirgen getFunctionType(calledBoundFuncType.getInputs(), calledBoundFuncType.getResults(), calledBoundFuncType.isVarArg()); auto thisValue = builder.create(location, calledFuncType.getInput(0), funcRefValue); auto unboundFuncRefValue = builder.create(location, calledFuncType, funcRefValue); + + // a @cppvtable method returning a record (mlirGenClassCppVTableHiddenReturn): it + // writes it to a local, passed after `this`, which is the call's value + if (auto resultType = cppVTableHiddenReturnType(funcRefValue)) + { + auto resultSlot = builder.create(location, mlir_ts::RefType::get(resultType), + mlir::Value(), builder.getBoolAttr(false), + builder.getIndexAttr(0)); + SmallVector slotOperands; + slotOperands.push_back(resultSlot); + slotOperands.append(operands.begin(), operands.end()); + auto callResult = mlirGenCallFunction(location, calledFuncType, unboundFuncRefValue, thisValue, slotOperands, genContext); + if (mlir::failed(callResult)) + { + value = mlir::failure(); + return; + } + + value = V(builder.create(location, resultType, resultSlot)); + return; + } + value = mlirGenCallFunction(location, calledFuncType, unboundFuncRefValue, thisValue, operands, genContext); }) .Case([&](auto calledExtentFuncType) { diff --git a/tslang/lib/TypeScript/MLIRGenClasses.cpp b/tslang/lib/TypeScript/MLIRGenClasses.cpp index 0e2c6b7f2..621982dd6 100644 --- a/tslang/lib/TypeScript/MLIRGenClasses.cpp +++ b/tslang/lib/TypeScript/MLIRGenClasses.cpp @@ -369,6 +369,53 @@ namespace mlirgen return newClassPtr; } + // MSVC returns a C++ record (struct, class - RefCountPtr too) from an instance method through + // a hidden pointer, passed after `this` and returned in turn (clang's + // MicrosoftCXXABI::classifyReturnType: "MSVC always returns structs indirectly from C++ instance + // methods"). A @cppvtable method declared to return a tuple returns that record: its slot is + // called as (this, Ref, args...) -> Ref, the type kept in the MethodInfo, and a call + // (mlirGenCall) passes a local of T and reads it back. The declared type stays for + // DeclarationPrinter, so an importer lowers it again. + mlir::LogicalResult MLIRGenImpl::mlirGenClassCppVTableHiddenReturn(mlir::Location location, ClassInfo::TypePtr newClassPtr, + StringRef funcName) + { + if (!newClassPtr->isCppVTable) + { + return mlir::success(); + } + + auto method = llvm::find_if(newClassPtr->methods, [&](auto &method) { return method.funcName == funcName; }); + if (method == newClassPtr->methods.end() || method->declaredFuncType) + { + return mlir::success(); + } + + auto funcType = method->funcType; + if (funcType.getNumResults() != 1 || !isa(funcType.getResult(0)) || funcType.getNumInputs() < 1) + { + return mlir::success(); + } + + if (!compileOptions.targetInfo.hiddenReturnPointerAfterThis) + { + emitError(location) << "a method of a @" CPP_VTABLE " class returning a struct needs the MSVC ABI, which returns it " + << "through a pointer after 'this'"; + return mlir::failure(); + } + + auto resultType = funcType.getResult(0); + auto resultRefType = mlir_ts::RefType::get(resultType); + SmallVector inputs; + inputs.push_back(funcType.getInput(0)); + inputs.push_back(resultRefType); + inputs.append(funcType.getInputs().begin() + 1, funcType.getInputs().end()); + + method->declaredFuncType = funcType; + method->funcType = getFunctionType(inputs, {resultRefType}, funcType.isVarArg()); + cppVTableHiddenReturns[funcName] = resultType; + return mlir::success(); + } + // 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. @@ -2402,6 +2449,11 @@ genContext); classMethodMemberInfo.setFuncOp(funcOp); if (classMethodMemberInfo.registerClassMethodMember(loc(funcLikeDeclaration), orderWeight, accessLevel)) { + if (mlir::failed(mlirGenClassCppVTableHiddenReturn(loc(funcLikeDeclaration), newClassPtr, funcOp.getName()))) + { + return mlir::failure(); + } + funcLikeDeclaration->processed = true; return mlir::success(); } diff --git a/tslang/lib/TypeScript/MLIRGenExpressions.cpp b/tslang/lib/TypeScript/MLIRGenExpressions.cpp index 3a2ce3843..f76a1d590 100644 --- a/tslang/lib/TypeScript/MLIRGenExpressions.cpp +++ b/tslang/lib/TypeScript/MLIRGenExpressions.cpp @@ -1075,6 +1075,11 @@ namespace mlirgen SmallVector operands; auto offsetArgs = isa(funcType) || isa(funcType) ? 1 : 0; + // a @cppvtable method returning a record: the arguments follow its hidden result pointer + if (cppVTableHiddenReturnType(funcResult)) + { + offsetArgs = 2; + } if (mlir::failed(mlirGenOperands(callExpression->arguments, operands, funcResult.getType(), genContext, offsetArgs, noReceiverTypesForGenericCall))) { return mlir::failure(); diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 63aac409e..ffe77d6c8 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -5335,6 +5335,22 @@ class MLIRGenImpl return mlir::success(); } + // The record a call of funcRef returns through a hidden pointer: funcRef is a @cppvtable method + // declared to return a tuple (mlirGenClassCppVTableHiddenReturn); null for any other callee. + mlir::Type cppVTableHiddenReturnType(mlir::Value funcRef) + { + if (auto virtualSymbolRef = funcRef.getDefiningOp()) + { + auto it = cppVTableHiddenReturns.find(virtualSymbolRef.getIdentifier()); + if (it != cppVTableHiddenReturns.end()) + { + return it->second; + } + } + + return mlir::Type(); + } + // 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). @@ -11201,6 +11217,9 @@ class MLIRGenImpl return mlir::failure(); } + mlir::LogicalResult mlirGenClassCppVTableHiddenReturn(mlir::Location location, ClassInfo::TypePtr newClassPtr, + StringRef funcName); + mlir::LogicalResult mlirGenClassCppVTableCheck(mlir::Location location, ClassLikeDeclaration classDeclarationAST, ClassInfo::TypePtr newClassPtr, const GenContext &genContext); @@ -14038,6 +14057,9 @@ class MLIRGenImpl llvm::ScopedHashTable fullNameClassesMap; + // the @cppvtable methods returning a C++ record (by function name): the record's type + llvm::StringMap cppVTableHiddenReturns; + llvm::ScopedHashTable fullNameGenericClassesMap; llvm::ScopedHashTable fullNameInterfacesMap; diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index e8302c4b6..c0c141eda 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -420,6 +420,7 @@ tslang_add_test(NAME test-compile-00-union-nullable-value COMMAND test-runner "$ 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-cppvtable-hidden-return COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00cppvtable_hidden_return.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") @@ -950,6 +951,7 @@ tslang_add_test(NAME test-jit-00-union-nullable-array COMMAND test-runner -jit " 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-cppvtable-hidden-return COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00cppvtable_hidden_return.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") @@ -2730,6 +2732,16 @@ add_test(NAME test-compile-cppvtable-memory-model-none set_tests_properties(test-compile-cppvtable-memory-model-none PROPERTIES FAIL_REGULAR_EXPRESSION "error:|Stack dump|Assertion failed") +# A method returning a record (tests/00cppvtable_hidden_return.ts) is MSVC's: the Itanium ABI +# returns a record through a pointer before `this`, and only some records. +add_test(NAME test-compile-cppvtable-hidden-return-target + COMMAND $ --emit=obj --no-default-lib -mm=none -mtriple=x86_64-unknown-linux-gnu + "${PROJECT_SOURCE_DIR}/test/tester/cppvtable/hidden_return_target.ts" + -o "${CMAKE_CURRENT_BINARY_DIR}/cppvtable-hidden-return-target.obj") +set_tests_properties(test-compile-cppvtable-hidden-return-target + PROPERTIES PASS_REGULAR_EXPRESSION "hidden_return_target\\.ts:7:5: error: a method of a @cppvtable class returning a struct needs the MSVC ABI" + FAIL_REGULAR_EXPRESSION "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 diff --git a/tslang/test/tester/cppvtable/hidden_return_target.ts b/tslang/test/tester/cppvtable/hidden_return_target.ts new file mode 100644 index 000000000..b03dc526d --- /dev/null +++ b/tslang/test/tester/cppvtable/hidden_return_target.ts @@ -0,0 +1,12 @@ +// Compiled for Linux: the Itanium ABI returns a record from a method otherwise than MSVC, through +// a pointer before `this`, and only some records. +type Pair = [first: s32, second: s32]; + +@cppvtable +class Methods { + pair(x: s32): Pair; +} + +function main() { + print("done."); +} diff --git a/tslang/test/tester/tests/00cppvtable_hidden_return.ts b/tslang/test/tester/tests/00cppvtable_hidden_return.ts new file mode 100644 index 000000000..a7864604c --- /dev/null +++ b/tslang/test/tester/tests/00cppvtable_hidden_return.ts @@ -0,0 +1,52 @@ +// A @cppvtable method declared to return a tuple returns a C++ record (a struct, a class such as +// RefCountPtr) by value. MSVC returns a record from an instance method through a hidden pointer, +// passed after `this` and returned in turn, so the slot is called as fn(this, &result, args) and the +// call's value is what fn wrote there. The "C++ methods" here are written in that shape. + +declare function memcpy(dst: Opaque, src: Opaque, count: index): Opaque; + +type Pair = [first: s32, second: s32]; // 8 bytes: still a hidden pointer, as an instance method's +type Triple = [a: u64, b: u64, c: u64]; // 24 bytes + +@cppvtable +class Methods { + value: s32; + pair(x: s32): Pair; // slot 0 + triple(): Triple; // slot 1 + plain(x: s32, y: s32): s32; // slot 2: a number, returned as before +} + +type ObjectData = [vtable: Opaque, value: s32]; + +function slotPair(self: Opaque, result: Ref, x: s32): Ref { + let value: Pair = [x + Deref(>self).value, x * 2]; + memcpy(result, Ref(value), sizeof()); + return result; +} + +function slotTriple(self: Opaque, result: Ref): Ref { + let value: Triple = [1, 2, 0x123456789]; + memcpy(result, Ref(value), sizeof()); + return result; +} + +function slotPlain(self: Opaque, x: s32, y: s32): s32 { + return x * y; +} + +function main() { + let vtable: Opaque[] = [slotPair, slotTriple, slotPlain]; + let data: ObjectData = [Ref(vtable[0]), 100]; + const methods = Ref(data); + + const pair = methods.pair(5); + assert(pair.first == 105 && pair.second == 10, "a record returned through the hidden pointer"); + + const triple = methods.triple(); + assert(triple.a == 1 && triple.b == 2 && triple.c == 0x123456789, "a bigger record"); + + assert(methods.plain(6, 7) == 42, "a number is still returned in a register, after them"); + assert(methods.pair(1).second == 2, "a record read from the call's value"); + + print("done."); +} diff --git a/tslang/test/tester/tests/export_cppvtable_class.ts b/tslang/test/tester/tests/export_cppvtable_class.ts index 540a3debd..4853610ff 100644 --- a/tslang/test/tester/tests/export_cppvtable_class.ts +++ b/tslang/test/tester/tests/export_cppvtable_class.ts @@ -11,13 +11,20 @@ namespace M { second(x: s32): s32; // slot 1 } + export type Pair = [first: s32, second: s32]; + @cppvtable export class Derived extends Base { third(x: s32, y: s32): s32; // slot 2 + pair(x: s32): Pair; // slot 3: a record, returned through a pointer after `this` } // called through this module's numbering export function callSecond(object: Base, x: s32): s32 { return object.second(x); } + + export function callPair(object: Derived, x: s32): Pair { + return object.pair(x); + } } diff --git a/tslang/test/tester/tests/import_cppvtable_class.ts b/tslang/test/tester/tests/import_cppvtable_class.ts index e1c43f52b..8617abf51 100644 --- a/tslang/test/tester/tests/import_cppvtable_class.ts +++ b/tslang/test/tester/tests/import_cppvtable_class.ts @@ -5,6 +5,8 @@ import './export_cppvtable_class' type ObjectData = [vtable: Opaque, value: s32]; +declare function memcpy(dst: Opaque, src: Opaque, count: index): Opaque; + function slot0(self: Opaque): s32 { return 10; } @@ -17,8 +19,15 @@ function slot2(self: Opaque, x: s32, y: s32): s32 { return x * y; } +// returns a record as MSVC does: through the pointer after `this` +function slot3(self: Opaque, result: Ref, x: s32): Ref { + let value: M.Pair = [x, x + 1]; + memcpy(result, Ref(value), sizeof()); + return result; +} + function main() { - let vtable: Opaque[] = [slot0, slot1, slot2]; + let vtable: Opaque[] = [slot0, slot1, slot2, slot3]; let data: ObjectData = [Ref(vtable[0]), 32]; const object = Ref(data); @@ -28,6 +37,10 @@ function main() { 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"); + const pair = derived.pair(7); + assert(pair.first == 7 && pair.second == 8, "a record returned by a method of an imported class"); + const exportedPair = M.callPair(derived, 9); + assert(exportedPair.first == 9 && exportedPair.second == 10, "and by the exporting module"); print("done."); } diff --git a/tslang/test/tester/verify-ownership.cmake b/tslang/test/tester/verify-ownership.cmake index 4880fa930..479d4eacd 100644 --- a/tslang/test/tester/verify-ownership.cmake +++ b/tslang/test/tester/verify-ownership.cmake @@ -37,6 +37,7 @@ set(not_compilable_alone raytrace-0.ts # a @cppvtable class is an error under rc and own (cppvtable/memory_model.ts) 00cppvtable_class.ts + 00cppvtable_hidden_return.ts export_cppvtable_class.ts import_cppvtable_class.ts) diff --git a/tslang/test/tester/x86-exclusions.cmake b/tslang/test/tester/x86-exclusions.cmake index 85e3562cd..4468aa955 100644 --- a/tslang/test/tester/x86-exclusions.cmake +++ b/tslang/test/tester/x86-exclusions.cmake @@ -8,6 +8,7 @@ set(TSLANG_X86_EXCLUDED test-compile-internals test-compile-00-cppvtable-class + test-compile-00-cppvtable-hidden-return test-compile-shared-export-import-cppvtable-class test-compile-rc-corpus-internals test-compile-none-corpus-internals @@ -15,6 +16,8 @@ set(TSLANG_X86_EXCLUDED 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-00-cppvtable-hidden-return + "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") From 015a6f7cef580e829190945a9a9e45875a0b1aab Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 11 Oct 2026 20:14:32 +0100 Subject: [PATCH 2/3] @cppvtable hidden return: a base class's method called through a derived class Co-Authored-By: Claude Opus 5.5 --- .../tester/tests/00cppvtable_hidden_return.ts | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tslang/test/tester/tests/00cppvtable_hidden_return.ts b/tslang/test/tester/tests/00cppvtable_hidden_return.ts index a7864604c..598ff1ff1 100644 --- a/tslang/test/tester/tests/00cppvtable_hidden_return.ts +++ b/tslang/test/tester/tests/00cppvtable_hidden_return.ts @@ -16,6 +16,11 @@ class Methods { plain(x: s32, y: s32): s32; // slot 2: a number, returned as before } +@cppvtable +class DerivedMethods extends Methods { + otherPair(x: s32): Pair; // slot 3 +} + type ObjectData = [vtable: Opaque, value: s32]; function slotPair(self: Opaque, result: Ref, x: s32): Ref { @@ -34,8 +39,14 @@ function slotPlain(self: Opaque, x: s32, y: s32): s32 { return x * y; } +function slotOtherPair(self: Opaque, result: Ref, x: s32): Ref { + let value: Pair = [x, -x]; + memcpy(result, Ref(value), sizeof()); + return result; +} + function main() { - let vtable: Opaque[] = [slotPair, slotTriple, slotPlain]; + let vtable: Opaque[] = [slotPair, slotTriple, slotPlain, slotOtherPair]; let data: ObjectData = [Ref(vtable[0]), 100]; const methods = Ref(data); @@ -48,5 +59,13 @@ function main() { assert(methods.plain(6, 7) == 42, "a number is still returned in a register, after them"); assert(methods.pair(1).second == 2, "a record read from the call's value"); + // a base class's method called through a derived class + const derived = Ref(data); + const inherited = derived.pair(3); + assert(inherited.first == 103 && inherited.second == 6, "an inherited method returning a record"); + const other = derived.otherPair(4); + assert(other.first == 4 && other.second == -4, "and the derived class's own"); + assert(derived.plain(2, 3) == 6, "a number through the derived class"); + print("done."); } From c3efbc5f4c971b00c36919bbf642e5d2b8b4b13d Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 11 Oct 2026 20:40:26 +0100 Subject: [PATCH 3/3] Add tests for cppvtable hidden return scenarios and update exclusions --- tslang/test/tester/CMakeLists.txt | 11 +++++-- .../tester/tests/export_cppvtable_class.ts | 7 ---- .../tests/export_cppvtable_hidden_return.ts | 24 ++++++++++++++ .../tester/tests/import_cppvtable_class.ts | 15 +-------- .../tests/import_cppvtable_hidden_return.ts | 33 +++++++++++++++++++ tslang/test/tester/verify-ownership.cmake | 4 ++- tslang/test/tester/x86-exclusions.cmake | 3 ++ 7 files changed, 72 insertions(+), 25 deletions(-) create mode 100644 tslang/test/tester/tests/export_cppvtable_hidden_return.ts create mode 100644 tslang/test/tester/tests/import_cppvtable_hidden_return.ts diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index c0c141eda..ca4edd0c3 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -420,7 +420,6 @@ tslang_add_test(NAME test-compile-00-union-nullable-value COMMAND test-runner "$ 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-cppvtable-hidden-return COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00cppvtable_hidden_return.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") @@ -951,7 +950,6 @@ tslang_add_test(NAME test-jit-00-union-nullable-array COMMAND test-runner -jit " 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-cppvtable-hidden-return COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00cppvtable_hidden_return.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") @@ -2733,7 +2731,14 @@ set_tests_properties(test-compile-cppvtable-memory-model-none PROPERTIES FAIL_REGULAR_EXPRESSION "error:|Stack dump|Assertion failed") # A method returning a record (tests/00cppvtable_hidden_return.ts) is MSVC's: the Itanium ABI -# returns a record through a pointer before `this`, and only some records. +# returns a record through a pointer before `this`, and only some records, so it is an error +# anywhere else and its tests run on Windows only. +if (WIN32) + tslang_add_test(NAME test-compile-00-cppvtable-hidden-return COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00cppvtable_hidden_return.ts") + tslang_add_test(NAME test-jit-00-cppvtable-hidden-return COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00cppvtable_hidden_return.ts") + tslang_add_test(NAME test-compile-shared-export-import-cppvtable-hidden-return COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_cppvtable_hidden_return.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_cppvtable_hidden_return.ts") + tslang_add_test(NAME test-jit-shared-export-import-cppvtable-hidden-return COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_cppvtable_hidden_return.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_cppvtable_hidden_return.ts") +endif() add_test(NAME test-compile-cppvtable-hidden-return-target COMMAND $ --emit=obj --no-default-lib -mm=none -mtriple=x86_64-unknown-linux-gnu "${PROJECT_SOURCE_DIR}/test/tester/cppvtable/hidden_return_target.ts" diff --git a/tslang/test/tester/tests/export_cppvtable_class.ts b/tslang/test/tester/tests/export_cppvtable_class.ts index 4853610ff..540a3debd 100644 --- a/tslang/test/tester/tests/export_cppvtable_class.ts +++ b/tslang/test/tester/tests/export_cppvtable_class.ts @@ -11,20 +11,13 @@ namespace M { second(x: s32): s32; // slot 1 } - export type Pair = [first: s32, second: s32]; - @cppvtable export class Derived extends Base { third(x: s32, y: s32): s32; // slot 2 - pair(x: s32): Pair; // slot 3: a record, returned through a pointer after `this` } // called through this module's numbering export function callSecond(object: Base, x: s32): s32 { return object.second(x); } - - export function callPair(object: Derived, x: s32): Pair { - return object.pair(x); - } } diff --git a/tslang/test/tester/tests/export_cppvtable_hidden_return.ts b/tslang/test/tester/tests/export_cppvtable_hidden_return.ts new file mode 100644 index 000000000..e65119d3a --- /dev/null +++ b/tslang/test/tester/tests/export_cppvtable_hidden_return.ts @@ -0,0 +1,24 @@ +namespace M { + + // A @cppvtable method returning a record (00cppvtable_hidden_return.ts), exported from one + // module and called from another: the declaration embedded in the binary (__decls) keeps the + // declared tuple, so the importer passes the hidden result pointer too. MSVC only. + + export type Pair = [first: s32, second: s32]; + + @cppvtable + export class Base { + value: s32; + first(): s32; // slot 0 + } + + @cppvtable + export class Derived extends Base { + pair(x: s32): Pair; // slot 1: a record, returned through a pointer after `this` + } + + // called through this module's numbering + export function callPair(object: Derived, x: s32): Pair { + return object.pair(x); + } +} diff --git a/tslang/test/tester/tests/import_cppvtable_class.ts b/tslang/test/tester/tests/import_cppvtable_class.ts index 8617abf51..e1c43f52b 100644 --- a/tslang/test/tester/tests/import_cppvtable_class.ts +++ b/tslang/test/tester/tests/import_cppvtable_class.ts @@ -5,8 +5,6 @@ import './export_cppvtable_class' type ObjectData = [vtable: Opaque, value: s32]; -declare function memcpy(dst: Opaque, src: Opaque, count: index): Opaque; - function slot0(self: Opaque): s32 { return 10; } @@ -19,15 +17,8 @@ function slot2(self: Opaque, x: s32, y: s32): s32 { return x * y; } -// returns a record as MSVC does: through the pointer after `this` -function slot3(self: Opaque, result: Ref, x: s32): Ref { - let value: M.Pair = [x, x + 1]; - memcpy(result, Ref(value), sizeof()); - return result; -} - function main() { - let vtable: Opaque[] = [slot0, slot1, slot2, slot3]; + let vtable: Opaque[] = [slot0, slot1, slot2]; let data: ObjectData = [Ref(vtable[0]), 32]; const object = Ref(data); @@ -37,10 +28,6 @@ function main() { 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"); - const pair = derived.pair(7); - assert(pair.first == 7 && pair.second == 8, "a record returned by a method of an imported class"); - const exportedPair = M.callPair(derived, 9); - assert(exportedPair.first == 9 && exportedPair.second == 10, "and by the exporting module"); print("done."); } diff --git a/tslang/test/tester/tests/import_cppvtable_hidden_return.ts b/tslang/test/tester/tests/import_cppvtable_hidden_return.ts new file mode 100644 index 000000000..edcd4ce98 --- /dev/null +++ b/tslang/test/tester/tests/import_cppvtable_hidden_return.ts @@ -0,0 +1,33 @@ +import './export_cppvtable_hidden_return' + +// The "C++ object" made by hand, as in 00cppvtable_hidden_return.ts: a table of functions and a +// tuple whose first field points at it. + +type ObjectData = [vtable: Opaque, value: s32]; + +declare function memcpy(dst: Opaque, src: Opaque, count: index): Opaque; + +function slot0(self: Opaque): s32 { + return 10; +} + +// returns a record as MSVC does: through the pointer after `this` +function slot1(self: Opaque, result: Ref, x: s32): Ref { + let value: M.Pair = [x + Deref(>self).value, x + 1]; + memcpy(result, Ref(value), sizeof()); + return result; +} + +function main() { + let vtable: Opaque[] = [slot0, slot1]; + let data: ObjectData = [Ref(vtable[0]), 100]; + const derived = Ref(data); + + assert(derived.first() == 10, "slot 0"); + const pair = derived.pair(7); + assert(pair.first == 107 && pair.second == 8, "a record returned by a method of an imported class"); + const exportedPair = M.callPair(derived, 9); + assert(exportedPair.first == 109 && exportedPair.second == 10, "and by the exporting module"); + + print("done."); +} diff --git a/tslang/test/tester/verify-ownership.cmake b/tslang/test/tester/verify-ownership.cmake index 479d4eacd..820bf28c1 100644 --- a/tslang/test/tester/verify-ownership.cmake +++ b/tslang/test/tester/verify-ownership.cmake @@ -39,7 +39,9 @@ set(not_compilable_alone 00cppvtable_class.ts 00cppvtable_hidden_return.ts export_cppvtable_class.ts - import_cppvtable_class.ts) + import_cppvtable_class.ts + export_cppvtable_hidden_return.ts + import_cppvtable_hidden_return.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 4468aa955..d06c80b16 100644 --- a/tslang/test/tester/x86-exclusions.cmake +++ b/tslang/test/tester/x86-exclusions.cmake @@ -10,6 +10,7 @@ set(TSLANG_X86_EXCLUDED test-compile-00-cppvtable-class test-compile-00-cppvtable-hidden-return test-compile-shared-export-import-cppvtable-class + test-compile-shared-export-import-cppvtable-hidden-return test-compile-rc-corpus-internals test-compile-none-corpus-internals ) @@ -20,6 +21,8 @@ set(TSLANG_X86_EXCLUDED_REASON_test-compile-00-cppvtable-hidden-return "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") +set(TSLANG_X86_EXCLUDED_REASON_test-compile-shared-export-import-cppvtable-hidden-return + "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).