From 6d83561b22fb4578646fd64144b1edaaa81833d1 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Tue, 13 Oct 2026 20:02:13 +0100 Subject: [PATCH] @cpp classes: C++ overloads under one name, picked by the arguments A @cpp class can declare a C++ overload set under the C++ name, each method with its own @slot (or @linkname for a non-virtual one). Before, a second declaration of a name was "redefinition of symbol", and tsbindgen wrote `scale_2`, `scale_3`, which quietly changed meaning when a header added an overload above them. - mlirGenClassCppOverloadNames renames an overload after the first to `name#N` (`#` is in no identifier), once the base classes are known: a method in the slot of a base class's overload takes that overload's name, which is how it overrides it. - resolveCppOverload: at the call, after the arguments are generated, the overload the arguments fit best is picked from the class and its bases, as C++ does: the argument's own type, then an integer for an integer or a float for a float (`number` is f64), then any other conversion. No fit, or two equal fits, is an error listing them. - An override of an overloaded base method without @slot is an error, and so is a @slot overload with the parameters of a base method in another slot (that method, overridden in the wrong slot). - DeclarationPrinter prints the declared name; the importer renames again. Diagnostics print the declared name too. - tsbindgen keeps the C++ name for overloads (a field of the name still gives `_2`). Tested against real C++ (cppvtable/abi_host.cpp): virtual overloads in MSVC's reversed order, non-virtual ones by symbol, and C++ calling overloads implemented in tslang. TSLANG_ISSUES.md 5. Co-Authored-By: Claude Opus 5.5 --- README.md | 4 +- TSLANG_ISSUES.md | 8 +- docs/cpp-classes.md | 17 +- tslang/include/TsClangImporter/BindingModel.h | 2 +- tslang/include/TypeScript/Defines.h | 2 + .../TypeScript/MLIRLogic/MLIRGenStore.h | 23 +- tslang/lib/TsClangImporter/HeaderParser.cpp | 15 +- tslang/lib/TypeScript/DeclarationPrinter.cpp | 4 +- tslang/lib/TypeScript/MLIRGenAccessCall.cpp | 234 ++++++++++++++++++ tslang/lib/TypeScript/MLIRGenClasses.cpp | 140 ++++++++++- tslang/lib/TypeScript/MLIRGenImpl.h | 8 + tslang/test/bindgen/bindgen_cpp_test.ts | 2 +- tslang/test/bindgen/fixture_cpp.hpp | 2 +- tslang/test/tester/CMakeLists.txt | 10 +- tslang/test/tester/cppvtable/abi_host.cpp | 21 ++ tslang/test/tester/cppvtable/abi_itanium.ts | 31 +++ tslang/test/tester/cppvtable/abi_msvc.ts | 33 ++- .../tester/cppvtable/overload_ambiguous.ts | 10 + .../tester/cppvtable/overload_no_match.ts | 10 + .../tester/cppvtable/overload_override.ts | 14 ++ tslang/test/tester/tests/00cpp_overloads.ts | 50 ++++ .../tester/tests/export_cppvtable_class.ts | 8 + .../tester/tests/import_cppvtable_class.ts | 13 +- tslang/test/tester/verify-ownership.cmake | 1 + .../TsClangImporter/CppClassTest.cpp | 10 +- 25 files changed, 646 insertions(+), 26 deletions(-) create mode 100644 tslang/test/tester/cppvtable/overload_ambiguous.ts create mode 100644 tslang/test/tester/cppvtable/overload_no_match.ts create mode 100644 tslang/test/tester/cppvtable/overload_override.ts create mode 100644 tslang/test/tester/tests/00cpp_overloads.ts diff --git a/README.md b/README.md index 2c9a0f43c..a8a0dc84e 100644 --- a/README.md +++ b/README.md @@ -713,7 +713,7 @@ type Handle = [p: Opaque | null]; class Shape { // geo::Shape @slot(1) area(): s32; @slot(3) scale(by: s32): void; - @slot(2) scale_2(by: f64): void; + @slot(2) scale(by: f64): void; @cppnontrivial @slot(4) handle(): [p: Opaque | null]; @linkname("?id@Shape@geo@@QEAAHH@Z") id(x: s32): s32; @@ -727,7 +727,7 @@ declare function make_square(side: s32): Square; ``` - **Slots** are clang's: the destructor (slot 0 under MSVC, 0 and 1 under the Itanium ABI) and methods that aren't public are slots left out. A method overriding a base's is not declared again: the `@cpp` class's override keeps the base's slot. -- **Overloads** get `_2`, `_3`, ... in declaration order, whatever order the ABI puts their slots in. +- **Overloads** keep the C++ name, each with its own slot (or symbol), in whatever order the ABI puts the slots: a call picks the overload by its arguments, as C++ does (`shape.scale(2)`, `shape.scale(0.5)`). A method named as a field gets `_2`, `_3`, ... - **A pointer or a reference** to an object of a bound class is the class (`| null` for a pointer); a reference to a struct is a `Reference` to its tuple. - **A record returned by value** is a tuple with its layout, `@cppnontrivial` when it has a destructor or a copy constructor. - **Fields** are declared when they are where a `@cpp` class puts them (after the vtable pointer and the base's storage). diff --git a/TSLANG_ISSUES.md b/TSLANG_ISSUES.md index 8fbafc5d8..b0f7044a0 100644 --- a/TSLANG_ISSUES.md +++ b/TSLANG_ISSUES.md @@ -13,7 +13,7 @@ https://github.com/ASDAlexander77/native_ts_graphics | 2 | The error for (1) pointed at line 1, column 1 of the base class | bug | **moot**: nothing synthesized fails any more; an explicit bad `super()` is reported at the call | | 3 | `u64` `<<` shifted by the count modulo 32 | bug | **fixed** | | 4 | `Ref` field with a `null` initializer rejected | question | **answered**: declare it `Ref \| null` | -| 5 | `@cppvtable` overload naming (C++ overloads need distinct TypeScript names) | open | **addressed** on branch `cppvtable-slot`: `@cpp` + `@slot(N)` (docs/cpp-classes.md) | +| 5 | `@cppvtable` (now `@cpp`) overload naming (C++ overloads need distinct TypeScript names) | open | **fixed**: `@slot(N)` (#561), slots from tsbindgen (#562), and overloads under one name, picked by the arguments (this PR) | | 6 | C `string` return values keep the raw `char*` (no copy) | open | **fixed**: copied at the call (this PR; `Opaque` is the raw pointer) | | 7 | `Opaque` compared with `Opaque \| null` crashed the compiler | - | **fixed** by #536 (re-checked with the original drawScene shape) | | 8 | A ternary of number literals in arithmetic truncated to the other operand's integer type | - | **fixed** by #560 (still wrong on pre-alpha97) | @@ -112,6 +112,12 @@ interface method need distinct TypeScript names, and the MSVC slot order of over hand (`cl /d1reportSingleClassLayout`). A way to name the C++ overload (or a tool that prints the slot map) would remove that manual step. See `docs/cppvtable-classes.md`. Not re-checked. +**Fixed**: `@cppvtable` is now `@cpp`, and each virtual method names its slot with `@slot(N)` (#561), which +tsbindgen takes from clang (#562), MSVC's reversed overload order included. C++ overloads are now declared +under the C++ name, each with its own `@slot` or `@linkname`, and a call picks one by its arguments as C++ +does (`methods.over(5)`, `methods.over(2.5)`); no fit, or two equal fits, is an error listing the +overloads. tsbindgen no longer writes `_2`, `_3` (docs/cpp-classes.md). + ### 6. C `string` return values are not copied A C/C++ function declared as returning `string` hands tslang its raw `char*`, with no copy and no diff --git a/docs/cpp-classes.md b/docs/cpp-classes.md index 0609edbfe..b405dcb41 100644 --- a/docs/cpp-classes.md +++ b/docs/cpp-classes.md @@ -100,9 +100,20 @@ Writing one by hand: (`..ts`). - **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, with its own `@slot`. MSVC groups overloads in the vtable, and not always in - declaration order: take the numbers from the layout report. +- **Overloads.** Declare each C++ overload under the C++ name, with its own `@slot` (or + `@linkname` for a non-virtual one). A call picks the overload from its arguments, at compile + time as C++ does: an argument of the parameter's own type fits best, then an integer for an + integer or a float for a float, then any other conversion. A call that fits none, or fits two + as well, is an error that lists the overloads. MSVC groups overloads in the vtable, in reverse + declaration order: take the numbers from the layout report (tsbindgen does). + + ```ts + @slot(8) over(x: s32): s32; + @slot(7) over(x: f64): s32; + @slot(6) over(a: s32, b: s32): s32; // methods.over(5), methods.over(2.5), methods.over(6, 7) + ``` + + A derived class overriding one overload names its slot: by name alone it would be ambiguous. - **Non-virtual methods** are bound by their mangled symbol: `@linkname("?scaled@Methods@@QEAAHH@Z")` under MSVC, `@linkname("_ZN7Methods6scaledEi")` under the Itanium ABI. The symbol has to be exported by the C++ library (`__declspec(dllexport)` on the method or the class). diff --git a/tslang/include/TsClangImporter/BindingModel.h b/tslang/include/TsClangImporter/BindingModel.h index b5d86b066..6e4a7b5b2 100644 --- a/tslang/include/TsClangImporter/BindingModel.h +++ b/tslang/include/TsClangImporter/BindingModel.h @@ -88,7 +88,7 @@ enum class DeclKind // A method of a C++ class bound as a @cpp class. struct Method { - std::string name; // the TS name: the C++ name, `_2`, `_3`, ... for later overloads + std::string name; // the TS name: the C++ name (an overload too), `_2`, ... when a field has it std::string cppName; // the C++ name, for messages int slot = -1; // a virtual method's vtable slot; -1 for a non-virtual one std::string symbol; // a non-virtual method's mangled symbol diff --git a/tslang/include/TypeScript/Defines.h b/tslang/include/TypeScript/Defines.h index cc4e24f7e..4699a7730 100644 --- a/tslang/include/TypeScript/Defines.h +++ b/tslang/include/TypeScript/Defines.h @@ -202,6 +202,8 @@ // @slot(N) on a method of a @cpp class: a C++ virtual method, slot N of the vtable (counted // from 0, a base class's slots included); a method without it isn't virtual (see MethodInfo::cppSlot) #define CPP_SLOT "slot" +// The name of a C++ overload after the first one declared, `write#1` (see mlirGenClassCppOverloadNames) +#define CPP_OVERLOAD_SEPARATOR "#" // On a call (CallIndirect, then CallInternal or Invoke): the index of the argument that is the hidden // pointer the callee returns a record through, which LowerToLLVM marks `sret` for the target to pass // it where the C++ ABI does (X8 on AArch64) diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h b/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h index 5465ae9a4..b80b93e06 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h @@ -983,7 +983,8 @@ struct ClassInfo auto index = it->second; if (isCppVTable && method.cppSlot >= 0 && (size_t)method.cppSlot != index && cppSlotConflict.empty()) { - cppSlotConflict = "'" + method.name + "' overrides slot " + std::to_string(index) + " of a base class, not slot " + cppSlotConflict = "'" + mlir::StringRef(method.name).split(CPP_OVERLOAD_SEPARATOR).first.str() + "' overrides slot " + + std::to_string(index) + " of a base class, not slot " + std::to_string(method.cppSlot); } @@ -998,6 +999,23 @@ struct ClassInfo // @slot(N): slot N, whatever the declaration order; the slots no method names // are gaps, which a class implementing C++ methods leaves null auto index = (size_t)method.cppSlot; + + // a C++ overload of a base class's method takes other parameters: with the same ones + // it is that method, overridden in the wrong slot + auto declaredName = mlir::StringRef(method.name).split(CPP_OVERLOAD_SEPARATOR).first; + auto params = method.funcType.getInputs().drop_front(method.funcType.getNumInputs() > 0 ? 1 : 0); + for (auto [slot, entry] : llvm::enumerate(vtable)) + { + auto &inherited = entry.methodInfo; + if (!entry.isInterfaceVTable && !entry.isStaticField && !inherited.isCppSlotGap && slot != index + && inherited.funcType && inherited.funcType.getNumInputs() > 0 + && mlir::StringRef(inherited.name).split(CPP_OVERLOAD_SEPARATOR).first == declaredName + && inherited.funcType.getInputs().drop_front() == params && cppSlotConflict.empty()) + { + cppSlotConflict = "'" + declaredName.str() + "' overrides slot " + std::to_string(slot) + + " of a base class, not slot " + std::to_string(index); + } + } while (vtable.size() <= index) { MethodInfo gap; @@ -1017,7 +1035,8 @@ struct ClassInfo { if (cppSlotConflict.empty()) { - cppSlotConflict = "'" + method.name + "' and '" + vtable[index].methodInfo.name + "' are both in slot " + cppSlotConflict = "'" + mlir::StringRef(method.name).split(CPP_OVERLOAD_SEPARATOR).first.str() + "' and '" + + mlir::StringRef(vtable[index].methodInfo.name).split(CPP_OVERLOAD_SEPARATOR).first.str() + "' are both in slot " + std::to_string(index); } diff --git a/tslang/lib/TsClangImporter/HeaderParser.cpp b/tslang/lib/TsClangImporter/HeaderParser.cpp index c6e3b95c6..aa9e9eea5 100644 --- a/tslang/lib/TsClangImporter/HeaderParser.cpp +++ b/tslang/lib/TsClangImporter/HeaderParser.cpp @@ -513,13 +513,17 @@ class Collector : public clang::RecursiveASTVisitor } llvm::StringSet<> inherited; + llvm::StringSet<> inheritedMethods; if (base) { inherited = classNames[decl.base]; + inheritedMethods = classMethodNames[decl.base]; } auto &usedNames = classNames[key]; usedNames = std::move(inherited); + auto &methodNames = classMethodNames[key]; + methodNames = std::move(inheritedMethods); addClassFields(decl, definition, base); for (auto &field : decl.fields) @@ -538,15 +542,18 @@ class Collector : public clang::RecursiveASTVisitor continue; } + // C++ overloads keep the C++ name, each with its own @slot or @linkname: tslang picks one + // by the arguments. A field of the name makes it `_2`, `_3`, ... Method bound; bound.cppName = method->getName().str(); bound.name = escapeReserved(bound.cppName); - for (auto overload = 2; usedNames.count(bound.name); ++overload) + for (auto suffix = 2; usedNames.count(bound.name) && !methodNames.count(bound.name); ++suffix) { - bound.name = escapeReserved(bound.cppName) + "_" + std::to_string(overload); + bound.name = escapeReserved(bound.cppName) + "_" + std::to_string(suffix); } usedNames.insert(bound.name); + methodNames.insert(bound.name); mapMethod(bound, method); decl.methods.push_back(std::move(bound)); } @@ -755,8 +762,10 @@ class Collector : public clang::RecursiveASTVisitor HeaderModel &model; llvm::StringSet<> keys; std::vector> pendingRecords; - // a @cpp class's TS member names, its base's included: a later overload gets `_2`, `_3`, ... + // a @cpp class's TS member names, its base's included: a method named as a field gets `_2`, ... llvm::StringMap> classNames; + // those that are methods: an overload keeps the name + llvm::StringMap> classMethodNames; // where a @cpp class's declared fields end, padded to its alignment, and that alignment (bits): // a derived class's fields follow llvm::DenseMap> fieldsEnd; diff --git a/tslang/lib/TypeScript/DeclarationPrinter.cpp b/tslang/lib/TypeScript/DeclarationPrinter.cpp index 743155506..1544e8cb2 100644 --- a/tslang/lib/TypeScript/DeclarationPrinter.cpp +++ b/tslang/lib/TypeScript/DeclarationPrinter.cpp @@ -680,9 +680,11 @@ namespace typescript // a @cpp method returning a record: as declared, the importer lowers it again auto funcType = method.declaredFuncType ? method.declaredFuncType : method.funcType; + // a C++ overload after the first is `name#N` here (mlirGenClassCppOverloadNames); the + // importer renames it again printMethod( method.isStatic, - method.name, + StringRef(method.name).split(CPP_OVERLOAD_SEPARATOR).first, funcType.getParams(), funcType.getNumResults() > 0 ? funcType.getResult(0) : mlir::Type(), classType->classType); diff --git a/tslang/lib/TypeScript/MLIRGenAccessCall.cpp b/tslang/lib/TypeScript/MLIRGenAccessCall.cpp index 45a2155a8..b552d3e99 100644 --- a/tslang/lib/TypeScript/MLIRGenAccessCall.cpp +++ b/tslang/lib/TypeScript/MLIRGenAccessCall.cpp @@ -1707,9 +1707,243 @@ namespace mlirgen return mlir::success(); } + if (!genContext.allowPartialResolve) + { + actualFuncRefValue = resolveCppOverload(location, actualFuncRefValue, operands, genContext); + if (!actualFuncRefValue) + { + return mlir::failure(); + } + } + return mlirGenCall(location, actualFuncRefValue, operands, genContext); } + // A call of a C++ overloaded method of a @cpp class (mlirGenClassCppOverloadNames): the member + // access found the first method of that name; this picks the one the arguments fit best, from + // the class and its bases, as C++ would at compile time. funcRefValue itself for any other + // callee; null, with an error, when no overload fits or two fit as well. + mlir::Value MLIRGenImpl::resolveCppOverload(mlir::Location location, mlir::Value funcRefValue, + mlir::ArrayRef operands, const GenContext &genContext) + { + mlir::Value thisValue; + StringRef identifier; + if (auto virtualRef = funcRefValue.getDefiningOp()) + { + thisValue = virtualRef.getThisVal(); + identifier = virtualRef.getIdentifier(); + } + else if (auto symbolRef = funcRefValue.getDefiningOp()) + { + thisValue = symbolRef.getThisVal(); + identifier = symbolRef.getIdentifier(); + } + + if (!thisValue || !isCppVTableMethod(funcRefValue)) + { + return funcRefValue; + } + + // a method found in a base class is called on the object cast to it: the overloads are those + // of the object's own class + if (auto castOp = thisValue.getDefiningOp()) + { + if (isa(castOp.getIn().getType())) + { + thisValue = castOp.getIn(); + } + } + + auto classType = dyn_cast(thisValue.getType()); + if (!classType) + { + return funcRefValue; + } + + auto classInfo = getClassInfoByFullName(classType.getName().getValue()); + if (!classInfo) + { + return funcRefValue; + } + + auto name = cppOverloadedName(identifier.rsplit('.').second); + + // the methods of that name, the most derived class first; an override (the same slot) hides + // the method it overrides + struct Candidate + { + ClassInfo::TypePtr owner; + int methodIndex; + }; + + SmallVector candidates; + llvm::SmallDenseSet slots; + llvm::StringSet<> names; + std::function collect = [&](ClassInfo::TypePtr owner) { + for (auto [index, method] : llvm::enumerate(owner->methods)) + { + if (method.isStatic || cppOverloadedName(method.name) != name) + { + continue; + } + + // an override has the name of the method it overrides (mlirGenClassCppOverloadNames), + // or its slot + auto hidden = !names.insert(method.name).second; + hidden |= method.cppSlot >= 0 && !slots.insert(method.cppSlot).second; + if (hidden) + { + continue; + } + + candidates.push_back({owner, static_cast(index)}); + } + + for (auto &base : owner->baseClasses) + { + collect(base); + } + }; + collect(classInfo); + + if (candidates.size() < 2) + { + return funcRefValue; + } + + // how well an argument fits a parameter: 3 its type, 2 an integer for an integer or a float + // for a float, 1 any other conversion, 0 none + auto fit = [&](mlir::Type argType, mlir::Type paramType) -> int { + if (argType == paramType) + { + return 3; + } + + auto literalType = dyn_cast(argType); + auto baseType = literalType ? literalType.getElementType() : argType; + // `number` is a C `double`, as f64 is + auto isDouble = [](mlir::Type type) { return isa(type) || type.isF64(); }; + if (baseType == paramType || (isDouble(baseType) && isDouble(paramType))) + { + return 3; + } + + // an integer for an integer, a float for a float, before a conversion between the two + // (C++ ranks a promotion or an integral conversion above a floating-integral one) + auto isFloat = [&](mlir::Type type) { return isDouble(type) || isa(type); }; + if ((isa(baseType) && isa(paramType)) || (isFloat(baseType) && isFloat(paramType))) + { + return 2; + } + + if ((isPromotableNumeric(baseType) && isPromotableNumeric(paramType)) || mth.canCastFromTo(location, baseType, paramType)) + { + return 1; + } + + return 0; + }; + + auto bestScore = -1; + SmallVector best; + for (auto &candidate : candidates) + { + auto &method = candidate.owner->methods[candidate.methodIndex]; + auto funcType = method.declaredFuncType ? method.declaredFuncType : method.funcType; + // input 0 is `this` + auto params = funcType.getInputs().drop_front(); + if (operands.size() > params.size()) + { + continue; + } + + auto score = 0; + auto fits = true; + for (auto [index, param] : llvm::enumerate(params)) + { + if (index >= operands.size()) + { + // a parameter left out has to take `undefined` + fits &= isa(param); + continue; + } + + auto argFit = fit(operands[index].getType(), param); + fits &= argFit > 0; + score += argFit; + } + + if (!fits) + { + continue; + } + + if (score > bestScore) + { + bestScore = score; + best.clear(); + } + + if (score == bestScore) + { + best.push_back(candidate); + } + } + + auto describe = [&](auto &diagnostic) { + for (auto &candidate : candidates) + { + auto &method = candidate.owner->methods[candidate.methodIndex]; + auto funcType = method.declaredFuncType ? method.declaredFuncType : method.funcType; + diagnostic << "\n " << name << "("; + for (auto [index, param] : llvm::enumerate(funcType.getInputs().drop_front())) + { + diagnostic << (index ? ", " : "") << to_print(param); + } + + diagnostic << ")"; + } + }; + + if (best.empty() || best.size() > 1) + { + auto diagnostic = emitError(location); + diagnostic << (best.empty() ? "no overload of '" : "the call of '") << name + << (best.empty() ? "' takes these arguments (" : "' is ambiguous: its arguments ("); + for (auto [index, operand] : llvm::enumerate(operands)) + { + auto type = operand.getType(); + if (auto literalType = dyn_cast(type)) + { + type = literalType.getElementType(); + } + + diagnostic << (index ? ", " : "") << to_print(type); + } + + diagnostic << (best.empty() ? "); it has:" : ") fit more than one of:"); + describe(diagnostic); + return mlir::Value(); + } + + auto chosen = best.front(); + auto &chosenMethod = chosen.owner->methods[chosen.methodIndex]; + if (chosenMethod.funcName == identifier) + { + return funcRefValue; + } + + // a base class's method takes the object cast to that class, as the member access does + auto resolved = ClassMethodAccess(chosen.owner, location, thisValue, chosen.methodIndex, chosen.owner != classInfo, + chosenMethod.accessLevel, genContext); + if (resolved && funcRefValue.use_empty()) + { + funcRefValue.getDefiningOp()->erase(); + } + + return resolved; + } + ValueOrLogicalResult MLIRGenImpl::NewClassInstanceOnStack(mlir::Location location, mlir_ts::ClassType classType, SmallVector &operands, const GenContext &genContext) { diff --git a/tslang/lib/TypeScript/MLIRGenClasses.cpp b/tslang/lib/TypeScript/MLIRGenClasses.cpp index 299ca740a..8d9444f87 100644 --- a/tslang/lib/TypeScript/MLIRGenClasses.cpp +++ b/tslang/lib/TypeScript/MLIRGenClasses.cpp @@ -177,6 +177,12 @@ namespace mlirgen setProcessingState(newClassPtr, ProcessingStages::ProcessedStorageClass, genContext); + // the base classes are known now, and no method has been named yet + if (newClassPtr->isCppVTable) + { + mlirGenClassCppOverloadNames(classDeclarationAST, newClassPtr, classGenContext); + } + if (mlir::failed(mlirGenClassCppVTableCheck(location, classDeclarationAST, newClassPtr, classGenContext))) { setProcessingState(newClassPtr, ProcessingStages::ErrorInStorageClass, genContext); @@ -394,6 +400,129 @@ namespace mlirgen return newClassPtr; } + // C++ overloads: a @cpp class may declare one name several times, each a method of its own (its + // own @slot or @linkname). Methods are known by name, so an overload is renamed `name#1`, + // `name#2`, ... - `#` is in no identifier - and a call of `name` picks one by its arguments + // (resolveCppOverload). DeclarationPrinter prints the name before the `#`. + // + // A method in the slot of a base class's overload takes that overload's name, which is how it + // overrides it; any other method whose name is taken - by an earlier declaration here, or, with + // a @slot of its own, by a base class's method - takes the next free `name#N`. Run again on the + // same declaration (discovery, an import), it leaves the names as they are. + void MLIRGenImpl::mlirGenClassCppOverloadNames(ClassLikeDeclaration classDeclarationAST, ClassInfo::TypePtr newClassPtr, + const GenContext &genContext) + { + struct BaseMethod + { + std::string name; + int slot; + }; + + llvm::StringMap> baseMethods; + std::function collect = [&](ClassInfo::TypePtr classInfo) { + for (auto &base : classInfo->baseClasses) + { + for (auto &method : base->methods) + { + if (!method.isStatic) + { + baseMethods[cppOverloadedName(method.name)].push_back({method.name, method.cppSlot}); + } + } + + collect(base); + } + }; + collect(newClassPtr); + + auto isBaseName = [&](StringRef name) { + auto it = baseMethods.find(cppOverloadedName(name)); + return it != baseMethods.end() + && llvm::any_of(it->second, [&](BaseMethod &method) { return method.name == name; }); + }; + + SmallVector methods; + llvm::StringSet<> ownNames; + for (auto &classMember : classDeclarationAST->members) + { + if (classMember == SyntaxKind::MethodDeclaration && !hasModifier(classMember, SyntaxKind::StaticKeyword) + && classMember.as()->name == SyntaxKind::Identifier) + { + auto methodDeclaration = classMember.as(); + methods.push_back(methodDeclaration); + auto name = MLIRHelper::getName(methodDeclaration->name); + if (name.find(CPP_OVERLOAD_SEPARATOR) != std::string::npos) + { + ownNames.insert(name); + } + } + } + + NodeFactory nf(NodeFactoryFlags::None); + auto rename = [&](MethodDeclaration methodDeclaration, StringRef newName) { + auto renamed = nf.createIdentifier(stows(newName.str())); + renamed->pos = methodDeclaration->name->pos; + renamed->_end = methodDeclaration->name->_end; + methodDeclaration->name = renamed; + }; + + for (auto methodDeclaration : methods) + { + auto name = MLIRHelper::getName(methodDeclaration->name); + if (name.find(CPP_OVERLOAD_SEPARATOR) != std::string::npos) + { + continue; + } + + auto slot = getCppSlot(methodDeclaration, genContext); + if (slot >= 0) + { + // overriding a base class's overload: its name + auto it = baseMethods.find(name); + if (it != baseMethods.end()) + { + auto overridden = llvm::find_if(it->second, [&](BaseMethod &method) { return method.slot == slot; }); + if (overridden != it->second.end()) + { + if (overridden->name != name) + { + rename(methodDeclaration, overridden->name); + } + + ownNames.insert(overridden->name); + continue; + } + } + } + + // a method without @slot keeps a base class's name: it overrides it by name, or is + // reported (mlirGenClassCppVTableCheck) + auto taken = ownNames.contains(name) || (slot >= 0 && isBaseName(name)); + if (!taken) + { + ownNames.insert(name); + continue; + } + + for (auto index = 1;; index++) + { + auto candidate = name + CPP_OVERLOAD_SEPARATOR + std::to_string(index); + if (!ownNames.contains(candidate) && !isBaseName(candidate)) + { + rename(methodDeclaration, candidate); + ownNames.insert(candidate); + break; + } + } + } + } + + // The name a C++ overload was declared with: `write#1` is `write` + StringRef MLIRGenImpl::cppOverloadedName(StringRef methodName) + { + return methodName.split(CPP_OVERLOAD_SEPARATOR).first; + } + // 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 @@ -664,12 +793,19 @@ namespace mlirgen auto methodName = getCppMethodName(classMember, genContext); auto overrides = isCppBaseSlot(newClassPtr, methodName); + if (slot < 0 && overrides && isCppBaseSlot(newClassPtr, (cppOverloadedName(methodName) + CPP_OVERLOAD_SEPARATOR "1").str())) + { + // by name, it would take the slot of the first of them + error(memberLocation) << "'" << cppOverloadedName(methodName) << "' is overloaded in a base class: an override " + << "names the slot it overrides, @" CPP_SLOT "(N)"; + continue; + } if (slot < 0 && !overrides) { auto hasBody = static_cast(classMember.as()->body); if (hasModifier(classMember, SyntaxKind::AbstractKeyword)) { - error(memberLocation) << "the abstract method '" << methodName << "' of a @" CPP_VTABLE + error(memberLocation) << "the abstract method '" << cppOverloadedName(methodName) << "' of a @" CPP_VTABLE << " class needs @" CPP_SLOT "(N), its vtable slot"; } else if (!hasBody && !newClassPtr->isImport && !declarationMode) @@ -681,7 +817,7 @@ namespace mlirgen if (!hasLinkName) { - error(memberLocation) << "'" << methodName << "' has no @" CPP_SLOT ", so it isn't virtual: give a " + error(memberLocation) << "'" << cppOverloadedName(methodName) << "' has no @" CPP_SLOT ", so it isn't virtual: give a " << "C++ virtual method its slot, @" CPP_SLOT "(N), or a non-virtual one its " << "symbol, @" LINK_NAME "(\"...\")"; } diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 5d7995bcf..bfacf7354 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -11395,6 +11395,14 @@ class MLIRGenImpl std::string getCppMethodName(ClassElement classMember, const GenContext &genContext); + void mlirGenClassCppOverloadNames(ClassLikeDeclaration classDeclarationAST, ClassInfo::TypePtr newClassPtr, + const GenContext &genContext); + + static StringRef cppOverloadedName(StringRef methodName); + + mlir::Value resolveCppOverload(mlir::Location location, mlir::Value funcRefValue, mlir::ArrayRef operands, + const GenContext &genContext); + bool isCppVirtualMethod(ClassInfo::TypePtr classInfo, ClassElement classMember, const GenContext &genContext); bool isCppVTableImplementation(ClassLikeDeclaration classDeclarationAST, ClassInfo::TypePtr classInfo, diff --git a/tslang/test/bindgen/bindgen_cpp_test.ts b/tslang/test/bindgen/bindgen_cpp_test.ts index ced941118..5a4b85fef 100644 --- a/tslang/test/bindgen/bindgen_cpp_test.ts +++ b/tslang/test/bindgen/bindgen_cpp_test.ts @@ -8,7 +8,7 @@ function main() { print(square.area()); // a virtual method, overridden in C++ square.scale(2); // scale(int32_t) print(square.area()); - square.scale_2(0.5); // scale(double), the second overload + square.scale(0.5); // scale(double): an overload, picked by the argument print(square.area()); const shape: Shape = square; diff --git a/tslang/test/bindgen/fixture_cpp.hpp b/tslang/test/bindgen/fixture_cpp.hpp index c8d7b3926..529336316 100644 --- a/tslang/test/bindgen/fixture_cpp.hpp +++ b/tslang/test/bindgen/fixture_cpp.hpp @@ -94,7 +94,7 @@ class FX_API Shape // defined (not pure): a pure virtual's slot would point at the C++ runtime's __cxa_pure_virtual virtual int32_t area() const; virtual void scale(int32_t by); - virtual void scale(double by); // an overload: scale_2 + virtual void scale(double by); // an overload: scale too, with its own @slot virtual Handle handle(); virtual Pair pair(int32_t x); virtual int32_t fits(const Extent &extent, const Extent *limit); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 1af824fa3..faa1832ee 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -426,6 +426,7 @@ tslang_add_test(NAME test-compile-00-opaque-nullable-compare COMMAND test-runner # not in TSLANG_CORPUS: a @cpp 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-cpp-slot COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00cpp_slot.ts") +tslang_add_test(NAME test-compile-00-cpp-overloads COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00cpp_overloads.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") @@ -966,6 +967,7 @@ tslang_add_test(NAME test-jit-00-union-nullable-value COMMAND test-runner -jit " 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-cpp-slot COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00cpp_slot.ts") +tslang_add_test(NAME test-jit-00-cpp-overloads COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00cpp_overloads.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") @@ -2771,6 +2773,9 @@ set(cppvtable_errors "slot_override|@slot: 'first' overrides slot 0 of a base class, not slot 2" "slot_argument|@slot takes the slot number, an integer from 0" "slot_plain_class|@slot is for a method of a @cpp class" + "overload_no_match|no overload of 'write' takes these arguments \\(string\\). it has:" + "overload_ambiguous|the call of 'write' is ambiguous: its arguments \\(s32, s32\\) fit more than one of:" + "overload_override|'write' is overloaded in a base class: an override names the slot it overrides, @slot\\(N\\)" "nonvirtual_struct|a C\\+\\+ non-virtual method \\(a @cpp class's, without @slot\\) can't return a struct yet") foreach(cppvtable_error ${cppvtable_errors}) string(REPLACE "|" ";" cppvtable_error "${cppvtable_error}") @@ -2843,12 +2848,13 @@ foreach(cppvtable_itanium_triple x86_64-unknown-linux-gnu aarch64-unknown-linux- PROPERTIES PASS_REGULAR_EXPRESSION "i1 zeroext %[0-9]+, i8 zeroext %[0-9]+, i8 signext %[0-9]+\\).*\\(ptr sret\\(\\{ ptr \\}\\) %[0-9]+, ptr %[0-9]+, i32 3\\)" FAIL_REGULAR_EXPRESSION "error:|Stack dump|Assertion failed") # C++ methods implemented here (abi_itanium.ts' Counter): the vtable is the C++ one's slots and - # nothing else, the two destructors left null, and a bool result is extended for C++ callers + # nothing else, the two destructors left null, a bool result is extended for C++ callers, and the + # two overloads of pick are in their slots, in declaration order add_test(NAME test-compile-cppvtable-itanium-implementation-${cppvtable_itanium_triple} COMMAND $ --emit=llvm --no-default-lib -mm=none -mtriple=${cppvtable_itanium_triple} "${PROJECT_SOURCE_DIR}/test/tester/cppvtable/abi_itanium.ts" -o -) set_tests_properties(test-compile-cppvtable-itanium-implementation-${cppvtable_itanium_triple} - PROPERTIES PASS_REGULAR_EXPRESSION "@Counter\\.\\.vtbl = private global \\{ ptr, ptr, ptr, ptr, ptr \\} \\{ ptr null, ptr null, ptr @Counter\\.message, ptr @Counter\\.count, ptr @Counter\\.isEven \\}.*define zeroext i1 @Counter\\.isEven" + PROPERTIES PASS_REGULAR_EXPRESSION "@Counter\\.\\.vtbl = private global \\{ ptr, ptr, ptr, ptr, ptr, ptr, ptr \\} \\{ ptr null, ptr null, ptr @Counter\\.message, ptr @Counter\\.count, ptr @Counter\\.isEven, ptr @Counter\\.pick, ptr @\"Counter\\.pick#1\" \\}.*define zeroext i1 @Counter\\.isEven" FAIL_REGULAR_EXPRESSION "error:|Stack dump|Assertion failed") endforeach() diff --git a/tslang/test/tester/cppvtable/abi_host.cpp b/tslang/test/tester/cppvtable/abi_host.cpp index 215ea7f79..e640eb7be 100644 --- a/tslang/test/tester/cppvtable/abi_host.cpp +++ b/tslang/test/tester/cppvtable/abi_host.cpp @@ -46,8 +46,15 @@ struct Methods virtual int plain(int a, int b) { return a * b + value; } // narrow arguments: clang's code takes them as extended to 32 bits by the caller virtual int narrow(bool b, uint8_t u, int8_t s) { return (b ? 1000 : 0) + u * 10 + s; } + // overloads: MSVC groups them in its vtable, in reverse declaration order; the Itanium ABI + // keeps the declaration order + virtual int over(int x) { return 1000 + x; } + virtual int over(double x) { return 2000 + (int)(x * 10); } + virtual int over(int a, int b) { return 3000 + a * b; } // non-virtual: called by its symbol, ?scaled@Methods@@QEAAHH@Z or _ZN7Methods6scaledEi ABI_MEMBER_EXPORT int scaled(int x); + // and its overload, ?scaled@Methods@@QEAAHN@Z or _ZN7Methods6scaledEd + ABI_MEMBER_EXPORT int scaled(double x); int value = 7; }; @@ -56,6 +63,11 @@ int Methods::scaled(int x) return x * 100 + value; } +int Methods::scaled(double x) +{ + return (int)(x * 1000) + value; +} + // implemented in tslang (abi_msvc.ts, abi_itanium.ts' Counter): C++ calls its methods through the // vtable tslang made, as NVRHI calls an application's nvrhi::IMessageCallback class Callback @@ -67,6 +79,9 @@ class Callback virtual void message(uint8_t severity, const char *text) = 0; virtual int count() = 0; virtual bool isEven(int x) = 0; + // overloads implemented in tslang + virtual int pick(int x) = 0; + virtual int pick(double x) = 0; }; // two messages, then the count @@ -77,6 +92,12 @@ ABI_EXPORT int abi_call_callback(Callback *callback) return callback->count(); } +// C++ picks the overload: pick(int), then pick(double) +ABI_EXPORT int abi_callback_pick(Callback *callback) +{ + return callback->pick(3) * 10000 + callback->pick(0.25); +} + // a bool result, as C++ reads it ABI_EXPORT int abi_callback_is_even(Callback *callback, int x) { diff --git a/tslang/test/tester/cppvtable/abi_itanium.ts b/tslang/test/tester/cppvtable/abi_itanium.ts index b8834deff..d876e653b 100644 --- a/tslang/test/tester/cppvtable/abi_itanium.ts +++ b/tslang/test/tester/cppvtable/abi_itanium.ts @@ -20,9 +20,15 @@ class Methods { @slot(4) two(x: s32): Two; @slot(5) plain(a: s32, b: s32): s32; @slot(6) narrow(b: boolean, u: u8, s: s8): s32; + // C++ overloads, each with its slot, in declaration order + @slot(7) over(x: s32): s32; + @slot(8) over(x: f64): s32; + @slot(9) over(a: s32, b: s32): s32; // a C++ non-virtual method (no @slot), called by its symbol @linkname("_ZN7Methods6scaledEi") scaled(x: s32): s32; + @linkname("_ZN7Methods6scaledEd") + scaled(x: f64): s32; } // A C++ interface (abi_host.cpp's Callback) implemented here: C++ calls Counter's methods through the @@ -34,6 +40,9 @@ class Callback { @slot(2) message(severity: u8, text: string): void; @slot(3) count(): s32; @slot(4) isEven(x: s32): boolean; + // C++ overloads, in declaration order + @slot(5) pick(x: s32): s32; + @slot(6) pick(x: f64): s32; } @cpp @@ -60,6 +69,15 @@ class Counter extends Callback { return x % 2 == 0; } + // overloads, each implementing its slot: C++ picks one, and so does tslang + @slot(5) pick(x: s32): s32 { + return x + this.start; + } + + @slot(6) pick(x: f64): s32 { + return (x * 100); + } + // TypeScript's own (no @slot, overriding nothing): not in the vtable C++ calls through doubled(): s32 { return this.count() * 2; @@ -68,6 +86,7 @@ class Counter extends Callback { declare function abi_call_callback(callback: Opaque): s32; declare function abi_callback_is_even(callback: Opaque, x: s32): s32; +declare function abi_callback_pick(callback: Opaque): s32; // arguments made from a wider value: the register's upper bits aren't zero function callNarrow(methods: Methods, big: s32): s32 { @@ -84,6 +103,16 @@ function main() { assert(methods.plain(6, 7) == 49, "a number"); assert(callNarrow(methods, 0x1234) == 1000 + 0x34 * 10 - 3, "narrow arguments, extended"); assert(methods.scaled(3) == 307, "a non-virtual method, by its symbol"); + const half: number = 0.5; + assert(methods.scaled(half) == 507, "its overload, picked by the argument"); + + // C++ overloads: one name, the slot picked by the arguments + const small: u8 = 4; + assert(methods.over(5) == 1005, "over(int)"); + assert(methods.over(small) == 1004, "over(int), from a u8"); + assert(methods.over(2.5) == 2025, "over(double)"); + assert(methods.over(half) == 2005, "over(double), from a number"); + assert(methods.over(6, 7) == 3042, "over(int, int)"); const counter = new Counter(5); assert(abi_call_callback(counter) == 10 && counter.unexpected == 0, "C++ calls the methods implemented here"); @@ -91,6 +120,8 @@ function main() { const callback: Callback = counter; assert(callback.count() == 10, "and tslang through the C++ interface"); assert(counter.doubled() == 20, "a method of TypeScript's own"); + assert(abi_callback_pick(counter) == 80025, "C++ calls the overloads implemented here"); + assert(counter.pick(1) == 6 && counter.pick(0.5) == 50, "and tslang picks them too"); print("done."); } diff --git a/tslang/test/tester/cppvtable/abi_msvc.ts b/tslang/test/tester/cppvtable/abi_msvc.ts index 4cebe6b15..9f85b18b8 100644 --- a/tslang/test/tester/cppvtable/abi_msvc.ts +++ b/tslang/test/tester/cppvtable/abi_msvc.ts @@ -18,9 +18,15 @@ class Methods { @slot(3) two(x: s32): Two; @slot(4) plain(a: s32, b: s32): s32; @slot(5) narrow(b: boolean, u: u8, s: s8): s32; - // a C++ non-virtual method (no @slot), called by its symbol + // C++ overloads, each with its slot: MSVC groups them in reverse declaration order + @slot(8) over(x: s32): s32; + @slot(7) over(x: f64): s32; + @slot(6) over(a: s32, b: s32): s32; + // a C++ non-virtual method (no @slot), called by its symbol, and its overload @linkname("?scaled@Methods@@QEAAHH@Z") scaled(x: s32): s32; + @linkname("?scaled@Methods@@QEAAHN@Z") + scaled(x: f64): s32; } // A C++ interface (abi_host.cpp's Callback) implemented here: C++ calls Counter's methods through the @@ -31,6 +37,9 @@ class Callback { @slot(1) message(severity: u8, text: string): void; @slot(2) count(): s32; @slot(3) isEven(x: s32): boolean; + // C++ overloads: MSVC groups them in reverse declaration order + @slot(5) pick(x: s32): s32; + @slot(4) pick(x: f64): s32; } @cpp @@ -57,6 +66,15 @@ class Counter extends Callback { return x % 2 == 0; } + // overloads, each implementing its slot: C++ picks one, and so does tslang + @slot(5) pick(x: s32): s32 { + return x + this.start; + } + + @slot(4) pick(x: f64): s32 { + return (x * 100); + } + // TypeScript's own (no @slot, overriding nothing): not in the vtable C++ calls through doubled(): s32 { return this.count() * 2; @@ -65,6 +83,7 @@ class Counter extends Callback { declare function abi_call_callback(callback: Opaque): s32; declare function abi_callback_is_even(callback: Opaque, x: s32): s32; +declare function abi_callback_pick(callback: Opaque): s32; // arguments made from a wider value: the register's upper bits aren't zero function callNarrow(methods: Methods, big: s32): s32 { @@ -81,6 +100,16 @@ function main() { assert(methods.plain(6, 7) == 49, "a number"); assert(callNarrow(methods, 0x1234) == 1000 + 0x34 * 10 - 3, "narrow arguments, extended"); assert(methods.scaled(3) == 307, "a non-virtual method, by its symbol"); + const half: number = 0.5; + assert(methods.scaled(half) == 507, "its overload, picked by the argument"); + + // C++ overloads: one name, the slot picked by the arguments + const small: u8 = 4; + assert(methods.over(5) == 1005, "over(int)"); + assert(methods.over(small) == 1004, "over(int), from a u8"); + assert(methods.over(2.5) == 2025, "over(double)"); + assert(methods.over(half) == 2005, "over(double), from a number"); + assert(methods.over(6, 7) == 3042, "over(int, int)"); const counter = new Counter(5); assert(abi_call_callback(counter) == 10 && counter.unexpected == 0, "C++ calls the methods implemented here"); @@ -88,6 +117,8 @@ function main() { const callback: Callback = counter; assert(callback.count() == 10, "and tslang through the C++ interface"); assert(counter.doubled() == 20, "a method of TypeScript's own"); + assert(abi_callback_pick(counter) == 80025, "C++ calls the overloads implemented here"); + assert(counter.pick(1) == 6 && counter.pick(0.5) == 50, "and tslang picks them too"); print("done."); } diff --git a/tslang/test/tester/cppvtable/overload_ambiguous.ts b/tslang/test/tester/cppvtable/overload_ambiguous.ts new file mode 100644 index 000000000..a6009cfd0 --- /dev/null +++ b/tslang/test/tester/cppvtable/overload_ambiguous.ts @@ -0,0 +1,10 @@ +// Two overloads the arguments fit as well: an error, as in C++. +@cpp +class Base { + @slot(0) write(x: s32, y: f64): s32; + @slot(1) write(x: f64, y: s32): s32; +} + +function main(b: Base) { + b.write(1, 1); +} diff --git a/tslang/test/tester/cppvtable/overload_no_match.ts b/tslang/test/tester/cppvtable/overload_no_match.ts new file mode 100644 index 000000000..4d6a456c5 --- /dev/null +++ b/tslang/test/tester/cppvtable/overload_no_match.ts @@ -0,0 +1,10 @@ +// A call of a C++ overloaded method takes the overload its arguments fit: one that fits none is an error. +@cpp +class Base { + @slot(0) write(x: s32): s32; + @slot(1) write(x: f64): s32; +} + +function main(b: Base) { + b.write("text"); +} diff --git a/tslang/test/tester/cppvtable/overload_override.ts b/tslang/test/tester/cppvtable/overload_override.ts new file mode 100644 index 000000000..fa9226ffc --- /dev/null +++ b/tslang/test/tester/cppvtable/overload_override.ts @@ -0,0 +1,14 @@ +// An override of a base class's C++ overload names its slot: by name it would take the first's. +@cpp +class Base { + @slot(0) write(x: s32): s32; + @slot(1) write(x: f64): s32; +} + +@cpp +class Derived extends Base { + write(x: f64): s32; +} + +function main() { +} diff --git a/tslang/test/tester/tests/00cpp_overloads.ts b/tslang/test/tester/tests/00cpp_overloads.ts new file mode 100644 index 000000000..060f884ea --- /dev/null +++ b/tslang/test/tester/tests/00cpp_overloads.ts @@ -0,0 +1,50 @@ +// C++ overloads: a @cpp class declares one name several times, each with its own @slot, and a call +// picks one by its arguments, as C++ does at compile time. A derived class overrides one by naming +// its slot, and adds overloads of its own. The "C++ object" is made by hand, as in 00cpp_slot.ts. + +@cpp +class Base { + value: s32; + @slot(1) write(x: s32): s32; + @slot(2) write(x: f64): s32; + @slot(3) write(x: s32, y: s32): s32; + @slot(4) name(): s32; +} + +@cpp +class Derived extends Base { + @slot(2) write(x: f64): s32; // overrides the f64 overload only + @slot(5) write(s: string): s32; // an overload of its own +} + +type ObjectData = [vtable: Opaque, value: s32]; + +function slot0(self: Opaque): s32 { return -1; } +function slot1(self: Opaque, x: s32): s32 { return 100 + x; } +function slot2(self: Opaque, x: number): s32 { return 200 + (x * 10); } +function slot3(self: Opaque, x: s32, y: s32): s32 { return 300 + x * y; } +function slot4(self: Opaque): s32 { return Deref(>self).value; } +function slot5(self: Opaque, s: string): s32 { return 500 + s.length; } + +function main() { + let vtable: Opaque[] = [slot0, slot1, slot2, slot3, slot4, slot5]; + let data: ObjectData = [Ref(vtable[0]), 7]; + + const base = Ref(data); + const small: u8 = 9; + const half: number = 0.5; + assert(base.write(5) == 105, "an integer literal: write(s32)"); + assert(base.write(small) == 109, "a u8: write(s32), an integer for an integer"); + assert(base.write(2.5) == 225, "a fraction: write(f64)"); + assert(base.write(half) == 205, "a number: write(f64)"); + assert(base.write(6, 7) == 342, "two arguments: write(s32, s32)"); + assert(base.name() == 7, "a method that isn't overloaded"); + + const derived = Ref(data); + assert(derived.write(5) == 105, "a base overload through a derived class"); + assert(derived.write(2.5) == 225, "the overridden overload, in its slot"); + assert(derived.write("abc") == 503, "the derived class's own overload"); + assert(derived.write(2, 3) == 306, "another base overload"); + + print("done."); +} diff --git a/tslang/test/tester/tests/export_cppvtable_class.ts b/tslang/test/tester/tests/export_cppvtable_class.ts index 5bfedd11e..186f0a6b4 100644 --- a/tslang/test/tester/tests/export_cppvtable_class.ts +++ b/tslang/test/tester/tests/export_cppvtable_class.ts @@ -10,6 +10,8 @@ namespace M { value: s32; @slot(0) first(): s32; @slot(1) second(x: s32): s32; + // a C++ overload: printed with its own name and slot, the importer renames it again + @slot(3) second(x: f64): s32; // TypeScript's own, not a slot: the importer calls this module's function twice(x: s32): s32 { @@ -20,10 +22,16 @@ namespace M { @cpp export class Derived extends Base { @slot(2) third(x: s32, y: s32): s32; + // and one of the derived class's own + @slot(4) second(x: s32, y: s32): s32; } // called through this module's numbering export function callSecond(object: Base, x: s32): s32 { return object.second(x); } + + export function callSecondDouble(object: Base, x: f64): 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 index afb4191a2..bdc459493 100644 --- a/tslang/test/tester/tests/import_cppvtable_class.ts +++ b/tslang/test/tester/tests/import_cppvtable_class.ts @@ -17,8 +17,16 @@ function slot2(self: Opaque, x: s32, y: s32): s32 { return x * y; } +function slot3(self: Opaque, x: number): s32 { + return (x * 10); +} + +function slot4(self: Opaque, x: s32, y: s32): s32 { + return x - y; +} + function main() { - let vtable: Opaque[] = [slot0, slot1, slot2]; + let vtable: Opaque[] = [slot0, slot1, slot2, slot3, slot4]; let data: ObjectData = [Ref(vtable[0]), 32]; const object = Ref(data); @@ -29,6 +37,9 @@ function main() { assert(derived.third(6, 7) == 42, "slot 2"); assert(M.callSecond(derived, 8) == 40, "the exporting module calls the same slot"); assert(derived.twice(1) == 66, "a method of TypeScript's own, from the exporting module"); + assert(derived.second(2.5) == 25, "a C++ overload, slot 3"); + assert(derived.second(9, 4) == 5, "the derived class's overload, slot 4"); + assert(M.callSecondDouble(derived, 1.5) == 15, "the exporting module picks the same overload"); print("done."); } diff --git a/tslang/test/tester/verify-ownership.cmake b/tslang/test/tester/verify-ownership.cmake index c112d560d..14e80aabb 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 @cpp class is an error under rc and own (cppvtable/memory_model.ts) 00cpp_slot.ts + 00cpp_overloads.ts 00cppvtable_class.ts 00cppvtable_hidden_return.ts export_cppvtable_class.ts diff --git a/tslang/unittests/TsClangImporter/CppClassTest.cpp b/tslang/unittests/TsClangImporter/CppClassTest.cpp index 7d85959ae..aedbae8bd 100644 --- a/tslang/unittests/TsClangImporter/CppClassTest.cpp +++ b/tslang/unittests/TsClangImporter/CppClassTest.cpp @@ -44,15 +44,15 @@ std::string cpp(const std::string &header, const std::string &target = windowsTa } // namespace -// MSVC: one destructor slot, and overloads grouped in reverse declaration order +// MSVC: one destructor slot, and overloads grouped in reverse declaration order, under one name TEST(CppClass, MsvcSlots) { auto out = cpp(shapes); EXPECT_THAT(out, HasSubstr("@cpp\nclass Shape { // geo::Shape\n" " @slot(1) area(): s32;\n" " @slot(4) scale(by: s32): void;\n" - " @slot(3) scale_2(by: f32): void;\n" - " @slot(2) scale_3(by: f64): void;\n" + " @slot(3) scale(by: f32): void;\n" + " @slot(2) scale(by: f64): void;\n" " @slot(6) last(): s32;\n" "}\n")); // the destructor (0) and the protected hidden() (5) are slots left out; overrides aren't @@ -68,8 +68,8 @@ TEST(CppClass, ItaniumSlots) auto out = cpp(shapes, linuxTarget); EXPECT_THAT(out, HasSubstr(" @slot(2) area(): s32;\n" " @slot(3) scale(by: s32): void;\n" - " @slot(4) scale_2(by: f32): void;\n" - " @slot(5) scale_3(by: f64): void;\n" + " @slot(4) scale(by: f32): void;\n" + " @slot(5) scale(by: f64): void;\n" " @slot(7) last(): s32;\n")); EXPECT_THAT(out, HasSubstr(" @slot(8) contains(other: Shape): boolean;\n")); }