diff --git a/docs/c-cpp-header-import.md b/docs/c-cpp-header-import.md index fad30b929..a73f0edd8 100644 --- a/docs/c-cpp-header-import.md +++ b/docs/c-cpp-header-import.md @@ -217,8 +217,8 @@ C is a 90%-mechanical translation; C++ is not. Realistic phasing: | 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 +interfaces: a `@cpp` class calls a C++ object's virtual methods through its own vtable, and its +non-virtual ones by their symbols (see [cpp-classes.md](cpp-classes.md)). Templates, exceptions and STL types still need the wrapper route. The C++ ABI reality (mangling differences, MSVC vs Itanium, RTTI, std::string diff --git a/docs/cppvtable-classes.md b/docs/cpp-classes.md similarity index 53% rename from docs/cppvtable-classes.md rename to docs/cpp-classes.md index 8bcf8e6bc..0f8663561 100644 --- a/docs/cppvtable-classes.md +++ b/docs/cpp-classes.md @@ -1,28 +1,31 @@ -# `@cppvtable` classes — calling C++ virtual methods directly +# `@cpp` classes — calling C++ 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: +A class decorated with `@cpp` is laid over objects made by C++. tslang calls its virtual methods +through the C++ object's own vtable, and its non-virtual methods by their symbols, 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 +@cpp 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 + @slot(0) _destructor(): void; + @slot(1) AddRef(): u32; + @slot(2) Release(): u32; + @slot(3) GetRefCount(): u32; } -@cppvtable +@cpp class NvDevice extends NvResource { - // ... slots 4..38 ... - getGraphicsAPI(): u8; // slot 39 + @slot(39) getGraphicsAPI(): u8; // slots 4..38 aren't declared: nothing calls them } const device = Donut_GetNvrhiDevice(app); // an Opaque from C/C++ print(device.getGraphicsAPI()); ``` +(`@cpp` was called `@cppvtable` before `@slot` existed, and numbered the methods by their order. +The old name is an error now.) + ## How it maps A tslang class already matches a C++ single-inheritance object on 64-bit targets in the @@ -34,14 +37,25 @@ following ways: 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. +What `@cpp` changes is what is virtual, and where. In an ordinary class every method is virtual, +and the vtable has `.instanceOf` in slot 0, `.new`, a slot for each implemented interface, and +slots for its static fields (`.rtti`, `.size`). A `@cpp` class has none of these, and a method is +virtual only as in C++: + +| Method | What it is | +| --- | --- | +| `@slot(N) name(...): T;` | a C++ virtual method: slot N of the vtable, counted from 0 with the base classes' slots | +| `name(...): T;` overriding a base class's `@slot` method | the same slot, as in C++ (`@slot` may be repeated, with the same number) | +| `@linkname("symbol") name(...): T;` | a C++ non-virtual method, called by its (mangled) symbol as `symbol(this, args...)` | +| `name(...): T { ... }` with no `@slot`, overriding nothing | a method of TypeScript's own, called directly; not in the vtable | + +The slots may be given in any order, and a slot no method names is skipped: declare only the +methods you call. Two methods in one slot are an error. -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. (A class -whose methods have bodies is the other way round: see "Implementing C++ methods".) +The class gets no vtable, constructor, RTTI or allocation of its own. It is a declaration: its C++ +methods have no bodies, and an `abstract` class with `abstract` methods (each with its `@slot`) +reads the same. (A class whose virtual methods have bodies is the other way round: see +"Implementing C++ methods".) Fields declared on the class are the C++ object's fields after the vtable pointer, laid out like a tuple's. @@ -49,22 +63,26 @@ 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. +it is. A wrong slot calls a different C++ method with no error at all, which is why these are +errors and not warnings. | Not allowed | Why | | --- | --- | -| a constructor or a field initializer, in a class none of whose methods has a body | the objects and their code are C++'s (a method with a body makes it a class implementing C++ methods, see below) | -| static members, `get`/`set` accessors, `accessor` fields | they would take vtable slots, and accessor slots don't follow declaration order | +| a method without a body and without `@slot` or `@linkname`, overriding nothing | it's neither a slot nor a symbol: a forgotten `@slot` | +| an `abstract` method without `@slot` | it is virtual, so it needs its slot | +| `@slot` on a method of an ordinary class | an ordinary class's vtable is tslang's own | +| a constructor or a field initializer, in a class none of whose virtual methods has a body | the objects and their code are C++'s (see "Implementing C++ methods") | +| static members, `get`/`set` accessors, `accessor` fields | they would take vtable slots | | `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 | +| `extends` an ordinary class, or an ordinary class extending a `@cpp` 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 method returning a tuple, on a target other than MSVC and Itanium x86-64 / AArch64 | each ABI returns records its own way (see below) | -| on Itanium, a method returning a trivial tuple other than one or two 8-byte integer or pointer fields | SysV / AAPCS64 pack it into registers by its fields, which a tuple isn't (see below) | +| a virtual method returning a tuple, on a target other than MSVC and Itanium x86-64 / AArch64 | each ABI returns records its own way (see below) | +| on Itanium, a virtual method returning a trivial tuple other than one or two 8-byte integer or pointer fields | SysV / AAPCS64 pack it into registers by its fields, which a tuple isn't (see below) | +| a non-virtual C++ method returning a tuple | not supported yet: its symbol would have to be declared with the hidden result pointer | -A generic or static class can't be `@cppvtable` either. +A generic or static class can't be `@cpp` either. ## Writing a binding @@ -72,21 +90,25 @@ A generic or static class can't be `@cppvtable` either. with `cl /c /Zs /d1reportSingleClassLayout file.cpp`; clang (Itanium) with `clang++ -S -emit-llvm -o /dev/null -Xclang -fdump-vtable-layouts file.cpp`, for a class whose vtable the file emits (it defines the first virtual method that isn't inline), where slot N is - entry N + 2 (entries 0 and 1 are the offset to top and the RTTI). Declare unused slots with - placeholder names (`_slot17(): void;`) so the methods you call keep their numbers. + entry N + 2 (entries 0 and 1 are the offset to top and the RTTI). The numbers differ between + the two ABIs (the destructor, below), so a binding for both is two files, picked by the target + (`..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. 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 + 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. +- **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). +- **Struct returned by value.** A C++ virtual method that returns a class or struct by value (for example `nvrhi::RefCountPtr` or `nvrhi::Object`) returns a record. Declare its return type as a tuple with the record's layout: ```ts type RefCountPtr = [pointer: Opaque | null]; // nvrhi::RefCountPtr @cppnontrivial // it has a destructor - createBuffer(desc: Ref): RefCountPtr; // const handle = device.createBuffer(Ref(desc)) + @slot(17) createBuffer(desc: Ref): RefCountPtr; // const handle = device.createBuffer(Ref(desc)) ``` MSVC returns a record from an instance method through a hidden pointer, whatever its size. The @@ -112,35 +134,36 @@ A generic or static class can't be `@cppvtable` either. The record isn't destroyed: a `RefCountPtr` the method returns holds a reference, which the caller gives back with `Release()`. A method stored in a variable and called later takes the - hidden pointer as an argument; call it directly. tslang pre-alpha94 and earlier need the hidden - pointer declared as a parameter: `createBuffer(ret: Ref, desc: Ref): Opaque`. + hidden pointer as an argument; call it directly. - **Narrow parameters.** A `bool` is a `boolean`, an `uint8_t` (or an `enum class : uint8_t`) a - `u8`, an `int16_t` an `s16`: a call of a `@cppvtable` method extends an argument narrower than - 32 bits as a C++ caller does (`zeroext`, `signext` for a signed type), which clang's code relies on. + `u8`, an `int16_t` an `s16`: a call of a C++ method extends an argument narrower than 32 bits as + a C++ caller does (`zeroext`, `signext` for a signed type), which clang's code relies on. - **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. +- **Methods of your own.** A method with a body and no `@slot` is TypeScript's: a helper over the + C++ methods, compiled as an ordinary function taking `this`. ## Implementing C++ methods -A `@cppvtable` class whose methods have bodies implements C++ methods: C++ calls them through the -object's vtable. An example is a callback interface such as `nvrhi::IMessageCallback`. Declare the C++ interface -as above, and extend it: +A `@cpp` class whose virtual methods have bodies implements C++ methods: C++ calls them through +the object's vtable. An example is a callback interface such as `nvrhi::IMessageCallback`. Declare +the C++ interface as above, and extend it: ```ts -@cppvtable +@cpp class IMessageCallback { // nvrhi::IMessageCallback (MSVC) - _destructor(): void; // slot 0 - message(severity: u8, text: string): void; + @slot(0) _destructor(): void; + @slot(1) message(severity: u8, text: string): void; } -@cppvtable +@cpp class MessageLog extends IMessageCallback { errors = 0; - message(severity: u8, text: string): void { + message(severity: u8, text: string): void { // overrides slot 1 if (severity >= 2) { this.errors++; } @@ -150,11 +173,11 @@ class MessageLog extends IMessageCallback { const log = new MessageLog(); // pass log to C++ ``` -- **The vtable** is the C++ layout and nothing else: the slots in order, with no `.instanceOf`, - interface or static slots. `new` points the object's first word at it. A method with a body - overrides the base's slot of the same name. A slot left to a method of the C++ interface (a - method with no body, such as the destructor) is null, as a pure virtual's would be: C++ calling - it crashes. A method that matches no base slot appends a slot, as in C++. +- **The vtable** is the C++ layout and nothing else: the slots, with no `.instanceOf`, interface + or static slots. `new` points the object's first word at it. A method with a body overrides the + base's slot of the same name. A slot left to a method of the C++ interface (a method with no + body, such as the destructor), or one no method names, is null, as a pure virtual's would be: + C++ calling it crashes. A new virtual method takes the slot its `@slot` gives it. - **The object** is allocated by tslang: the vtable pointer, the C++ interface's fields, then the class's own fields, laid out like a tuple's. It may have a constructor and field initializers. - **Calls from C++** come in as C++ calls them: `this` first, then the arguments. A narrow result @@ -172,8 +195,11 @@ const log = new MessageLog(); // pass log to C++ ## 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. + `abstract` against a vtable made by hand, and `tests/00cpp_slot.ts` slots out of order, skipped + slots and a method of TypeScript's own. Both are registered for AOT and JIT. - `tslang/test/tester/cppvtable/*.ts` holds one compile error per rule above. - `test-jit-cppvtable-abi` checks against code from a real C++ compiler. `cppvtable/abi_host.cpp` is built as a shared library by the host's compiler (MSVC, or g++/clang). `abi_msvc.ts` or `abi_itanium.ts` then: - - calls its object's methods, including struct returns and narrow arguments; - - implements its `Callback` interface (`Counter`), which C++ calls back. + - calls its object's virtual methods, including struct returns and narrow arguments, and a + non-virtual one by its mangled symbol; + - implements its `Callback` interface (`Counter`), which C++ calls back, with a method of + TypeScript's own beside the overrides. diff --git a/tslang/include/TypeScript/Defines.h b/tslang/include/TypeScript/Defines.h index 9d27d85fc..cc4e24f7e 100644 --- a/tslang/include/TypeScript/Defines.h +++ b/tslang/include/TypeScript/Defines.h @@ -193,18 +193,23 @@ #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" -// A @cppvtable method's C++ return type has a destructor or a copy constructor (see +#define CPP_VTABLE "cpp" +// the old name of @cpp, an error now: unknown, it would leave the class an ordinary one +#define CPP_VTABLE_OLD_NAME "cppvtable" +// A @cpp method's C++ return type has a destructor or a copy constructor (see // mlirGenClassCppVTableHiddenReturn) #define CPP_NON_TRIVIAL "cppnontrivial" +// @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" // 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) #define SRET_ARG_ATTR_NAME "ts.sret_arg" -// On a call: it calls a C++ method (of a @cppvtable class), whose arguments narrower than 32 bits +// On a call: it calls a C++ method (of a @cpp class), whose arguments narrower than 32 bits // LowerToLLVM marks zeroext / signext, as a C++ caller extends them (clang relies on it) #define CPP_CALL_ATTR_NAME "ts.cpp_call" -// On a function: a C++ method implemented in tslang (a method with a body of a @cppvtable class), +// On a function: a C++ method implemented in tslang (a method with a body of a @cpp class), // called by C++, whose results narrower than 32 bits LowerToLLVM extends as C++ callers expect #define CPP_METHOD_ATTR_NAME "ts.cpp_method" diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h b/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h index 1549a9f17..5465ae9a4 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h @@ -151,13 +151,18 @@ 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 + // A @cpp method returning a C++ record: the type it is declared with (funcType is the // one its slot is called with, the hidden result pointer added, see // mlirGenClassCppVTableHiddenReturn); null for any other method. mlir_ts::FunctionType declaredFuncType; - // @cppnontrivial: the C++ record a @cppvtable method returns has a destructor or a copy + // @cppnontrivial: the C++ record a @cpp method returns has a destructor or a copy // constructor, which decides how the Itanium ABI returns it bool cppNonTrivialReturn = false; + // @slot(N): the vtable slot of a @cpp method, -1 without @slot (a non-virtual method, or + // one overriding a base class's slot by name) + int cppSlot = -1; + // a slot of a @cpp class's vtable that no method names (ClassInfo::getVirtualTable) + bool isCppSlotGap = false; }; struct GenericMethodInfo @@ -785,16 +790,19 @@ struct ClassInfo bool isPublic; bool isDynamicImport; bool hasRTTI; - // `@cppvtable`: a binding to C++ objects (MSVC/Itanium single inheritance) made elsewhere. The + // `@cpp`: 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; - // a @cppvtable class with methods that have bodies: C++ methods implemented here, so C++ can + // a @cpp class with methods that have bodies: C++ methods implemented here, so C++ can // call them through its objects. It has a vtable (the C++ one's layout: the slots and nothing // else, a slot left to a declared C++ method null), may have a constructor and field // initializers, and `.new`; still no RTTI. tslang allocates its objects. bool isCppVTableImplementation = false; + // two @cpp methods in one slot, or an override's @slot not its base's: found by + // getVirtualTable, which can't report it, and reported by its caller + std::string cppSlotConflict; ProcessingStages processingAtEvaluation; ProcessingStages processing; // the module `processing` was reached in: a discovery module is thrown away with what it holds @@ -929,7 +937,7 @@ struct ClassInfo } } - // do vtable for current class (a @cppvtable class implements nothing, see isCppVTable) + // do vtable for current class (a @cpp class implements nothing, see isCppVTable) for (auto &implement : implements) { if (interfaceSlots.contains(implement.interface->fullName)) @@ -973,12 +981,53 @@ struct ClassInfo // found method - override the inherited slot (name and type together: // the overriding function's type must win over the inherited one) 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 " + + std::to_string(method.cppSlot); + } + vtable[index].methodInfo.funcName = method.funcName; vtable[index].methodInfo.funcType = method.funcType; method.virtualIndex = index; method.isVirtual = true; vtable[index].methodInfo.isAbstract = method.isAbstract; } + else if (method.isVirtual && isCppVTable && method.cppSlot >= 0) + { + // @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; + while (vtable.size() <= index) + { + MethodInfo gap; + gap.name = ".slot" + std::to_string(vtable.size()); + gap.funcType = mlir_ts::FunctionType::get(classType.getContext(), {}, {}, false); + gap.isStatic = false; + gap.isVirtual = true; + gap.isAbstract = false; + gap.virtualIndex = vtable.size(); + gap.orderWeight = 0; + gap.accessLevel = mlir_ts::AccessLevel::Public; + gap.isCppSlotGap = true; + vtable.push_back({gap, false}); + } + + if (!vtable[index].methodInfo.isCppSlotGap) + { + if (cppSlotConflict.empty()) + { + cppSlotConflict = "'" + method.name + "' and '" + vtable[index].methodInfo.name + "' are both in slot " + + std::to_string(index); + } + + continue; + } + + method.virtualIndex = index; + methodSlots[method.name] = index; + vtable[index] = {method, false}; + } else if (method.isVirtual) { method.virtualIndex = vtable.size(); diff --git a/tslang/include/TypeScript/TargetInfo.h b/tslang/include/TypeScript/TargetInfo.h index 4f5f21213..3769b8f32 100644 --- a/tslang/include/TypeScript/TargetInfo.h +++ b/tslang/include/TypeScript/TargetInfo.h @@ -29,12 +29,12 @@ struct TargetInfo 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. + // convention a class method is called with, so a @cpp 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 + // convention of a @cpp method declared to return a tuple. The Itanium ABI passes it // before `this`, and only for some records. bool hiddenReturnPointerAfterThis = false; diff --git a/tslang/lib/TypeScript/DeclarationPrinter.cpp b/tslang/lib/TypeScript/DeclarationPrinter.cpp index 0abf56bb2..743155506 100644 --- a/tslang/lib/TypeScript/DeclarationPrinter.cpp +++ b/tslang/lib/TypeScript/DeclarationPrinter.cpp @@ -653,6 +653,15 @@ namespace typescript newline(); } + // the slot of a @cpp class's virtual method: without it the importer takes the method + // for a non-virtual one + if (method.cppSlot >= 0) + { + os.indent(4); + os << "@" CPP_SLOT "(" << method.cppSlot << ")"; + newline(); + } + os.indent(4); if (method.accessLevel == mlir_ts::AccessLevel::Protected) @@ -669,7 +678,7 @@ namespace typescript os << "abstract "; } - // a @cppvtable method returning a record: as declared, the importer lowers it again + // a @cpp method returning a record: as declared, the importer lowers it again auto funcType = method.declaredFuncType ? method.declaredFuncType : method.funcType; printMethod( method.isStatic, diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index e77719b1a..5efe68599 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -2136,7 +2136,7 @@ struct SymbolCallInternalOpLowering : public TsLlvmPattern virtualTable; newClassPtr->getVirtualTable(virtualTable); - // a @cppvtable class is never constructed, typed or allocated here: its objects come from C++; + // a @cpp class is never constructed, typed or allocated here: its objects come from C++; // unless it implements C++ methods, which it does in an object of its own auto hasOwnRuntimeMembers = !newClassPtr->isStatic && (!newClassPtr->isCppVTable || newClassPtr->isCppVTableImplementation); @@ -254,6 +254,21 @@ namespace mlirgen return {mlir::failure(), ""}; } + // the slots of a @cpp class's methods, each where its @slot(N) puts it: two in one slot is + // an error (getVirtualTable finds it, here it's reported) + if (newClassPtr->isCppVTable) + { + llvm::SmallVector cppVirtualTable; + newClassPtr->cppSlotConflict.clear(); + newClassPtr->getVirtualTable(cppVirtualTable); + if (!newClassPtr->cppSlotConflict.empty()) + { + emitError(location) << "@" CPP_SLOT ": " << newClassPtr->cppSlotConflict; + setProcessingState(newClassPtr, ProcessingStages::ErrorInMembers, genContext); + return {mlir::failure(), ""}; + } + } + // generate vtable for interfaces in base class if (mlir::failed(mlirGenClassBaseInterfaces(location, newClassPtr, classGenContext))) { @@ -382,7 +397,7 @@ namespace mlirgen // 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 + // methods"). A @cpp 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. @@ -399,10 +414,12 @@ namespace mlirgen // CPP_METHOD_ATTR_NAME, and can't return a record yet (its body would have to write it through // the hidden pointer). mlir::LogicalResult MLIRGenImpl::mlirGenClassCppVTableHiddenReturn(mlir::Location location, ClassInfo::TypePtr newClassPtr, - mlir_ts::FuncOp funcOp, bool nonTrivial, bool hasBody) + mlir_ts::FuncOp funcOp, bool nonTrivial, bool hasBody, + bool isVirtual) { auto funcName = funcOp.getName(); - if (!newClassPtr->isCppVTable) + // a method with a body that isn't virtual is TypeScript's, not a C++ method + if (!newClassPtr->isCppVTable || (hasBody && !isVirtual)) { if (nonTrivial) { @@ -418,6 +435,10 @@ namespace mlirgen { funcOp->setAttr(CPP_METHOD_ATTR_NAME, builder.getUnitAttr()); } + else + { + cppMethodsWithoutBody.insert(funcName); + } auto method = llvm::find_if(newClassPtr->methods, [&](auto &method) { return method.funcName == funcName; }); if (method == newClassPtr->methods.end() || method->declaredFuncType) @@ -443,6 +464,12 @@ namespace mlirgen return mlir::failure(); } + if (!isVirtual) + { + emitError(location) << "a C++ non-virtual method (a @" CPP_VTABLE " class's, without @" CPP_SLOT ") can't return a struct yet"; + return mlir::failure(); + } + auto resultType = funcType.getResult(0); auto &targetInfo = compileOptions.targetInfo; auto beforeThis = false; @@ -522,13 +549,19 @@ namespace mlirgen }); } - // A @cppvtable class only names the slots of a vtable made by C++, so everything that would add + // A @cpp 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 (MLIRHelper::hasDecorator(classDeclarationAST, CPP_VTABLE_OLD_NAME)) + { + emitError(location) << "@" CPP_VTABLE_OLD_NAME " is now @" CPP_VTABLE ", and a virtual method needs @" CPP_SLOT "(N)"; + return mlir::failure(); + } + if (!newClassPtr->isCppVTable) { // an ordinary class can't add to a C++ vtable @@ -542,6 +575,15 @@ namespace mlirgen } } + for (auto &classMember : classDeclarationAST->members) + { + if (MLIRHelper::hasDecorator(classMember, CPP_SLOT)) + { + emitError(loc(classMember)) << "@" CPP_SLOT " is for a method of a @" CPP_VTABLE " class"; + return mlir::failure(); + } + } + return mlir::success(); } @@ -607,9 +649,45 @@ namespace mlirgen continue; } - // a method is a slot; with a body, it implements a C++ method (isCppVTableImplementation) + // a method with @slot(N) is a C++ virtual method, as is one overriding a base class's + // slot; with a body, it implements it (isCppVTableImplementation). Without @slot, a + // method with a body is a TypeScript method of the class, and one without is a C++ + // non-virtual method, called by its symbol. if (classMember == SyntaxKind::MethodDeclaration) { + auto slot = getCppSlot(classMember, genContext); + if (slot == -2) + { + error(memberLocation) << "@" CPP_SLOT " takes the slot number, an integer from 0"; + continue; + } + + auto methodName = getCppMethodName(classMember, genContext); + auto overrides = isCppBaseSlot(newClassPtr, methodName); + 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 + << " class needs @" CPP_SLOT "(N), its vtable slot"; + } + else if (!hasBody && !newClassPtr->isImport && !declarationMode) + { + auto hasLinkName = false; + iterateDecorators(classMember, genContext, [&](StringRef name, SmallVector args) { + hasLinkName |= name == DLL_NAME && !args.empty(); + }); + + if (!hasLinkName) + { + error(memberLocation) << "'" << 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 "(\"...\")"; + } + } + } + continue; } @@ -648,6 +726,81 @@ namespace mlirgen return failed ? mlir::failure() : mlir::success(); } + // @slot(N) of a class member: N; -1 without @slot, -2 when its argument isn't a slot number + int MLIRGenImpl::getCppSlot(ClassElement classMember, const GenContext &genContext) + { + auto slot = -1; + iterateDecorators(classMember, genContext, [&](StringRef name, SmallVector args) { + if (name != CPP_SLOT) + { + return; + } + + unsigned value; + if (args.size() != 1 || args.front().getAsInteger(10, value) || value > (unsigned)std::numeric_limits::max()) + { + slot = -2; + return; + } + + slot = static_cast(value); + }); + + return slot; + } + + // Whether a base class of a @cpp class has a virtual method named methodName: a method of the + // class with that name overrides it, as in C++, without @slot of its own + bool MLIRGenImpl::isCppBaseSlot(ClassInfo::TypePtr classInfo, StringRef methodName) + { + for (auto &base : classInfo->baseClasses) + { + if (llvm::any_of(base->methods, [&](auto &method) { return method.name == methodName && method.isVirtual && !method.isStatic; }) + || isCppBaseSlot(base, methodName)) + { + return true; + } + } + + return false; + } + + std::string MLIRGenImpl::getCppMethodName(ClassElement classMember, const GenContext &genContext) + { + std::string methodName; + std::string propertyName; + if (mlir::failed(getMethodNameOrPropertyName(false, classMember.as(), methodName, propertyName, genContext))) + { + return std::string(); + } + + return methodName; + } + + // A method of a @cpp class is a C++ virtual method (a vtable slot) with @slot(N), when abstract, + // or when it overrides a base class's slot by name; otherwise it isn't virtual + bool MLIRGenImpl::isCppVirtualMethod(ClassInfo::TypePtr classInfo, ClassElement classMember, const GenContext &genContext) + { + if (classMember != SyntaxKind::MethodDeclaration || hasModifier(classMember, SyntaxKind::StaticKeyword)) + { + return false; + } + + return getCppSlot(classMember, genContext) != -1 || hasModifier(classMember, SyntaxKind::AbstractKeyword) + || isCppBaseSlot(classInfo, getCppMethodName(classMember, genContext)); + } + + // A @cpp class implements C++ methods when one of its virtual methods has a body; a body of a + // method that isn't virtual is TypeScript's, and leaves the class a binding to C++ objects + bool MLIRGenImpl::isCppVTableImplementation(ClassLikeDeclaration classDeclarationAST, ClassInfo::TypePtr classInfo, + const GenContext &genContext) + { + return llvm::any_of(classDeclarationAST->members, [&](ClassElement member) { + return member == SyntaxKind::MethodDeclaration && member.as()->body + && isCppVirtualMethod(classInfo, member, genContext); + }); + } + mlir::LogicalResult MLIRGenImpl::mlirGenClassCheckIfDeclaration(mlir::Location location, ClassLikeDeclaration classDeclarationAST, ClassInfo::TypePtr newClassPtr, const GenContext &genContext) @@ -722,6 +875,16 @@ namespace mlirgen } } + // with the base classes known: a method with a body implements a C++ method when it + // overrides a base's slot, which the guess made with the class (any method with a body) + // couldn't tell from a method of TypeScript's own + if (newClassPtr->isCppVTable && newClassPtr->isCppVTableImplementation + && !isCppVTableImplementation(classDeclarationAST, newClassPtr, genContext)) + { + newClassPtr->isCppVTableImplementation = false; + newClassPtr->isDeclaration = true; + } + #if ENABLE_RTTI if (newClassPtr->isCppVTable) { @@ -2474,6 +2637,18 @@ genContext); return false; } + // a slot no method names (@slot skipped it), or one named by a method of + // this class without a body + if (vtRecord.methodInfo.isCppSlotGap) + { + return true; + } + + if (cppMethodsWithoutBody.contains(symbolName)) + { + return true; + } + std::function findOwner = [&](ClassInfo::TypePtr cls) -> ClassInfo::TypePtr { if (llvm::any_of(cls->methods, [&](auto &m) { return m.funcName == symbolName; })) @@ -2602,8 +2777,11 @@ genContext); // an overload signature of a method or a constructor, as for a function: the implementation // that follows it is the member + // (a method of a @cpp class without a body is a C++ method, a class implementing some has + // methods of both kinds, and none is overloaded) auto isAmbientMember = newClassPtr->isDeclaration || newClassPtr->isImport - || (classMember->flags & NodeFlags::Ambient) == NodeFlags::Ambient; + || (classMember->flags & NodeFlags::Ambient) == NodeFlags::Ambient + || (newClassPtr->isCppVTable && classMember == SyntaxKind::MethodDeclaration); if (!funcLikeDeclaration->body && !classMethodMemberInfo.isAbstract && !isAmbientMember && (classMember == SyntaxKind::MethodDeclaration || classMember == SyntaxKind::Constructor)) { @@ -2622,6 +2800,14 @@ genContext); assert (!classMethodMemberInfo.methodName.empty()); + // a method of a @cpp class is virtual only as a C++ virtual method (isCppVirtualMethod) + if (newClassPtr->isCppVTable && !classMethodMemberInfo.isConstructor && !classMethodMemberInfo.isStatic) + { + classMethodMemberInfo.cppSlot = std::max(getCppSlot(classMember, genContext), -1); + classMethodMemberInfo.isVirtual = isCppVirtualMethod(newClassPtr, classMember, genContext); + classMethodMemberInfo.isForceVirtual = classMethodMemberInfo.isVirtual; + } + // update access based on name if (StringRef(classMethodMemberInfo.getName()).starts_with("#")) { accessLevel = mlir_ts::AccessLevel::Private; @@ -2692,7 +2878,8 @@ genContext); if (!classMethodMemberInfo.isConstructor && !classMethodMemberInfo.isStatic && mlir::failed(mlirGenClassCppVTableHiddenReturn(loc(funcLikeDeclaration), newClassPtr, funcOp, MLIRHelper::hasDecorator(funcLikeDeclaration, CPP_NON_TRIVIAL), - static_cast(funcLikeDeclaration->body)))) + static_cast(funcLikeDeclaration->body), + classMethodMemberInfo.isVirtual))) { return mlir::failure(); } diff --git a/tslang/lib/TypeScript/MLIRGenExpressions.cpp b/tslang/lib/TypeScript/MLIRGenExpressions.cpp index 8c7532eab..546bb34b0 100644 --- a/tslang/lib/TypeScript/MLIRGenExpressions.cpp +++ b/tslang/lib/TypeScript/MLIRGenExpressions.cpp @@ -1075,7 +1075,7 @@ 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 + // a @cpp method returning a record: the arguments follow its hidden result pointer if (cppVTableHiddenReturnType(funcResult)) { offsetArgs = 2; diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 33e763319..c4b942158 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -5336,7 +5336,7 @@ class MLIRGenImpl return mlir::success(); } - // The record a call of funcRef returns through a hidden pointer: funcRef is a @cppvtable method + // The record a call of funcRef returns through a hidden pointer: funcRef is a @cpp method // declared to return a tuple (mlirGenClassCppVTableHiddenReturn); null for any other callee. mlir::Type cppVTableHiddenReturnType(mlir::Value funcRef) { @@ -5363,7 +5363,8 @@ class MLIRGenImpl return false; } - // Whether a call of funcRef calls a C++ method: a method of a @cppvtable class + // Whether a call of funcRef calls a C++ method: a method of a @cpp class, virtual (a slot) or not + // (called by its symbol) bool isCppVTableMethod(mlir::Value funcRef) { if (auto virtualSymbolRef = funcRef.getDefiningOp()) @@ -5371,10 +5372,15 @@ class MLIRGenImpl return cppVTableMethods.contains(virtualSymbolRef.getIdentifier()); } + if (auto symbolRef = funcRef.getDefiningOp()) + { + return cppVTableMethods.contains(symbolRef.getIdentifier()); + } + return false; } - // A @cppvtable class's objects are C++ objects: their slot 0 is a C++ method (the destructor), + // A @cpp 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) @@ -11294,7 +11300,8 @@ class MLIRGenImpl } mlir::LogicalResult mlirGenClassCppVTableHiddenReturn(mlir::Location location, ClassInfo::TypePtr newClassPtr, - mlir_ts::FuncOp funcOp, bool nonTrivial, bool hasBody); + mlir_ts::FuncOp funcOp, bool nonTrivial, bool hasBody, + bool isVirtual); bool isCppRecordReturnedInIntegerRegisters(mlir_ts::TupleType recordType); @@ -11302,6 +11309,17 @@ class MLIRGenImpl ClassLikeDeclaration classDeclarationAST, ClassInfo::TypePtr newClassPtr, const GenContext &genContext); + int getCppSlot(ClassElement classMember, const GenContext &genContext); + + bool isCppBaseSlot(ClassInfo::TypePtr classInfo, StringRef methodName); + + std::string getCppMethodName(ClassElement classMember, const GenContext &genContext); + + bool isCppVirtualMethod(ClassInfo::TypePtr classInfo, ClassElement classMember, const GenContext &genContext); + + bool isCppVTableImplementation(ClassLikeDeclaration classDeclarationAST, ClassInfo::TypePtr classInfo, + const GenContext &genContext); + mlir::LogicalResult mlirGenClassCheckIfDeclaration(mlir::Location location, ClassLikeDeclaration classDeclarationAST, ClassInfo::TypePtr newClassPtr, const GenContext &genContext); @@ -11849,11 +11867,17 @@ class MLIRGenImpl accessLevel, dllName }); + methodInfos.back().cppSlot = cppSlot; } else { methodInfos[methodIndex].orderWeight = orderWeight; methodInfos[methodIndex].accessLevel = accessLevel; + if (newClassPtr->isCppVTable) + { + methodInfos[methodIndex].cppSlot = cppSlot; + methodInfos[methodIndex].isVirtual = isAbstract || isVirtual; + } if (!dllName.empty()) { methodInfos[methodIndex].dllName = dllName; @@ -11947,6 +11971,8 @@ class MLIRGenImpl bool isForceVirtual; bool isVirtual; mlir_ts::AccessLevel accessLevel; + // @slot(N) of a method of a @cpp class (MethodInfo::cppSlot) + int cppSlot = -1; mlir_ts::FuncOp funcOp; }; @@ -14166,12 +14192,15 @@ class MLIRGenImpl llvm::ScopedHashTable fullNameClassesMap; - // the @cppvtable methods returning a C++ record (by function name): the record's type + // the @cpp methods returning a C++ record (by function name): the record's type llvm::StringMap cppVTableHiddenReturns; // those of them whose hidden pointer goes before `this` (the Itanium ABI) llvm::StringSet<> cppVTableHiddenReturnsBeforeThis; - // the methods of @cppvtable classes (by function name), C++ methods + // the methods of @cpp classes (by function name), C++ methods llvm::StringSet<> cppVTableMethods; + // the methods of @cpp classes without a body: a slot one names in a class implementing C++ + // methods is null, as a pure virtual method's + llvm::StringSet<> cppMethodsWithoutBody; llvm::ScopedHashTable fullNameGenericClassesMap; diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 750f53e8e..f39ea3c92 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -420,8 +420,9 @@ 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) +# 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-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") @@ -955,6 +956,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-cpp-slot COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00cpp_slot.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") @@ -2715,24 +2717,32 @@ 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 +# A @cpp 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. One whose methods have bodies implements # C++ methods (cppvtable/abi_*.ts' Counter), with limits of its own. 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 implementing a C\\+\\+ method \\(a @cppvtable class's, with a body\\) can't return a struct yet" - "export_implementation|a @cppvtable class implementing C\\+\\+ methods can't be exported" - "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 and fields" - "field_initializer|a field of a @cppvtable class can't have an initializer") + "instanceof|instanceof can't be used with a @cpp class" + "box_any|can't cast 'Base' to 'any': a @cpp class's objects are C\\+\\+ objects" + "unbox_any|can't cast 'any' to 'Base': a @cpp class's objects are C\\+\\+ objects" + "to_interface|can't cast 'Base' to 'IFirst': a @cpp class's objects are C\\+\\+ objects" + "extends_plain|the @cpp class 'Base' can only extend a @cpp class, not 'Plain'" + "plain_extends|class 'Plain' can't extend the @cpp class 'Base': it has to be @cpp too" + "implements|a @cpp class can't implement an interface" + "method_body|a method implementing a C\\+\\+ method \\(a @cpp class's, with a body\\) can't return a struct yet" + "export_implementation|a @cpp class implementing C\\+\\+ methods can't be exported" + "static_member|a @cpp class can't have static members" + "constructor|a @cpp class can't have a constructor" + "get_accessor|a @cpp class can only have methods and fields" + "field_initializer|a field of a @cpp class can't have an initializer" + "slot_old_name|@cppvtable is now @cpp, and a virtual method needs @slot\\(N\\)" + "slot_missing|'second' has no @slot, so it isn't virtual" + "slot_abstract|the abstract method 'first' of a @cpp class needs @slot\\(N\\), its vtable slot" + "slot_conflict|@slot: 'second' and 'first' are both in slot 1" + "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" + "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}") list(GET cppvtable_error 0 cppvtable_name) @@ -2753,7 +2763,7 @@ add_test(NAME test-compile-cppvtable-memory-model-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" + PROPERTIES PASS_REGULAR_EXPRESSION "memory_model\\.ts:3:1: error: a @cpp 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 @@ -2776,7 +2786,7 @@ add_test(NAME test-compile-cppvtable-hidden-return-target "${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, or the Itanium ABI on x86-64 or AArch64" + PROPERTIES PASS_REGULAR_EXPRESSION "hidden_return_target\\.ts:7:5: error: a method of a @cpp class returning a struct needs the MSVC ABI, or the Itanium ABI on x86-64 or AArch64" FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") # x86-64 Itanium: a trivial record tslang can't return as the SysV classification does, unmarked add_test(NAME test-compile-cppvtable-itanium-record @@ -2784,14 +2794,14 @@ add_test(NAME test-compile-cppvtable-itanium-record "${PROJECT_SOURCE_DIR}/test/tester/cppvtable/itanium_record.ts" -o "${CMAKE_CURRENT_BINARY_DIR}/cppvtable-itanium-record.obj") set_tests_properties(test-compile-cppvtable-itanium-record - PROPERTIES PASS_REGULAR_EXPRESSION "itanium_record\\.ts:7:5: error: on this target a method of a @cppvtable class returns a struct in registers by its fields.*mark the method @cppnontrivial" + PROPERTIES PASS_REGULAR_EXPRESSION "itanium_record\\.ts:7:5: error: on this target a method of a @cpp class returns a struct in registers by its fields.*mark the method @cppnontrivial" FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") add_test(NAME test-compile-cppvtable-nontrivial-no-struct COMMAND $ --emit=obj --no-default-lib -mm=none "${PROJECT_SOURCE_DIR}/test/tester/cppvtable/nontrivial_no_struct.ts" -o "${CMAKE_CURRENT_BINARY_DIR}/cppvtable-nontrivial-no-struct.obj") set_tests_properties(test-compile-cppvtable-nontrivial-no-struct - PROPERTIES PASS_REGULAR_EXPRESSION "nontrivial_no_struct\\.ts:[0-9]+:[0-9]+: error: @cppnontrivial is for a method of a @cppvtable class returning a struct" + PROPERTIES PASS_REGULAR_EXPRESSION "nontrivial_no_struct\\.ts:[0-9]+:[0-9]+: error: @cppnontrivial is for a method of a @cpp class returning a struct" FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") # The Itanium calls' argument attributes, on any host: the hidden result pointer `sret` (X8 on @@ -2813,7 +2823,7 @@ foreach(cppvtable_itanium_triple x86_64-unknown-linux-gnu aarch64-unknown-linux- FAIL_REGULAR_EXPRESSION "error:|Stack dump|Assertion failed") endforeach() -# A @cppvtable class calling the methods of an object C++ made (cppvtable/abi_host.cpp), built by +# A @cpp class calling the methods of an object C++ made (cppvtable/abi_host.cpp), built by # the host's compiler: its vtable, and the way its methods return records, are that compiler's ABI - # MSVC's (abi_msvc.ts), or the Itanium one of g++ and clang on x86-64 and AArch64 (abi_itanium.ts) - # not a vtable made by hand in tslang, which follows tslang's own idea of the ABI. @@ -2840,7 +2850,7 @@ add_test(NAME test-compile-cppvtable-thiscall-target "${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" + PROPERTIES PASS_REGULAR_EXPRESSION "memory_model\\.ts:3:1: error: a @cpp 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 diff --git a/tslang/test/tester/cppvtable/abi_host.cpp b/tslang/test/tester/cppvtable/abi_host.cpp index ab89248a8..215ea7f79 100644 --- a/tslang/test/tester/cppvtable/abi_host.cpp +++ b/tslang/test/tester/cppvtable/abi_host.cpp @@ -1,4 +1,4 @@ -// A C++ object whose methods a @cppvtable class calls (abi_msvc.ts, abi_itanium.ts): built by the +// A C++ object whose methods a @cpp class calls (abi_msvc.ts, abi_itanium.ts): built by the // host's C++ compiler, so its vtable and the way its methods return records are that compiler's // ABI - MSVC's, or the Itanium one of g++ and clang - not a vtable made by hand in tslang. @@ -6,8 +6,10 @@ #ifdef _WIN32 #define ABI_EXPORT extern "C" __declspec(dllexport) +#define ABI_MEMBER_EXPORT __declspec(dllexport) #else #define ABI_EXPORT extern "C" __attribute__((visibility("default"))) +#define ABI_MEMBER_EXPORT __attribute__((visibility("default"))) #endif // non-trivial, as nvrhi::RefCountPtr: the Itanium ABI returns it through a pointer before `this` @@ -44,9 +46,16 @@ 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; } + // non-virtual: called by its symbol, ?scaled@Methods@@QEAAHH@Z or _ZN7Methods6scaledEi + ABI_MEMBER_EXPORT int scaled(int x); int value = 7; }; +int Methods::scaled(int x) +{ + return x * 100 + 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 diff --git a/tslang/test/tester/cppvtable/abi_itanium.ts b/tslang/test/tester/cppvtable/abi_itanium.ts index 4380c4649..b8834deff 100644 --- a/tslang/test/tester/cppvtable/abi_itanium.ts +++ b/tslang/test/tester/cppvtable/abi_itanium.ts @@ -9,31 +9,34 @@ type Two = [a: u64, b: Opaque | null]; declare function abi_object(): Opaque; declare function abi_is_storage(p: Opaque | null, index: s32): s32; -@cppvtable +@cpp class Methods { value: s32; - completeDestructor(): void; // slot 0 - deletingDestructor(): void; // slot 1 + @slot(0) completeDestructor(): void; + @slot(1) deletingDestructor(): void; @cppnontrivial - make(x: s32): Ptr; // slot 2 - obj(x: s32): Obj; // slot 3 - two(x: s32): Two; // slot 4 - plain(a: s32, b: s32): s32; // slot 5 - narrow(b: boolean, u: u8, s: s8): s32; // slot 6 + @slot(2) make(x: s32): Ptr; + @slot(3) obj(x: s32): Obj; + @slot(4) two(x: s32): Two; + @slot(5) plain(a: s32, b: s32): s32; + @slot(6) narrow(b: boolean, u: u8, s: s8): s32; + // a C++ non-virtual method (no @slot), called by its symbol + @linkname("_ZN7Methods6scaledEi") + scaled(x: s32): s32; } // A C++ interface (abi_host.cpp's Callback) implemented here: C++ calls Counter's methods through the // vtable tslang makes for it -@cppvtable +@cpp class Callback { - completeDestructor(): void; // slot 0 - deletingDestructor(): void; // slot 1 - message(severity: u8, text: string): void; // slot 2 - count(): s32; // slot 3 - isEven(x: s32): boolean; // slot 4 + @slot(0) completeDestructor(): void; + @slot(1) deletingDestructor(): void; + @slot(2) message(severity: u8, text: string): void; + @slot(3) count(): s32; + @slot(4) isEven(x: s32): boolean; } -@cppvtable +@cpp class Counter extends Callback { total: s32 = 0; unexpected = 0; @@ -56,6 +59,11 @@ class Counter extends Callback { isEven(x: s32): boolean { return x % 2 == 0; } + + // TypeScript's own (no @slot, overriding nothing): not in the vtable C++ calls through + doubled(): s32 { + return this.count() * 2; + } } declare function abi_call_callback(callback: Opaque): s32; @@ -75,12 +83,14 @@ function main() { assert(two.a == 0x1234567890 + 7 && abi_is_storage(two.b, 9) == 1, "a trivial 16-byte record"); 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 counter = new Counter(5); assert(abi_call_callback(counter) == 10 && counter.unexpected == 0, "C++ calls the methods implemented here"); assert(abi_callback_is_even(counter, 4) == 1 && abi_callback_is_even(counter, 7) == 0, "a bool result"); const callback: Callback = counter; assert(callback.count() == 10, "and tslang through the C++ interface"); + assert(counter.doubled() == 20, "a method of TypeScript's own"); print("done."); } diff --git a/tslang/test/tester/cppvtable/abi_msvc.ts b/tslang/test/tester/cppvtable/abi_msvc.ts index 0a188d13d..4cebe6b15 100644 --- a/tslang/test/tester/cppvtable/abi_msvc.ts +++ b/tslang/test/tester/cppvtable/abi_msvc.ts @@ -8,29 +8,32 @@ type Two = [a: u64, b: Opaque | null]; declare function abi_object(): Opaque; declare function abi_is_storage(p: Opaque | null, index: s32): s32; -@cppvtable +@cpp class Methods { value: s32; - destructor(): void; // slot 0: the scalar deleting destructor + @slot(0) destructor(): void; // the scalar deleting destructor @cppnontrivial - make(x: s32): Ptr; // slot 1 - obj(x: s32): Obj; // slot 2 - two(x: s32): Two; // slot 3 - plain(a: s32, b: s32): s32; // slot 4 - narrow(b: boolean, u: u8, s: s8): s32; // slot 5 + @slot(1) make(x: s32): Ptr; + @slot(2) obj(x: s32): Obj; + @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 + @linkname("?scaled@Methods@@QEAAHH@Z") + scaled(x: s32): s32; } // A C++ interface (abi_host.cpp's Callback) implemented here: C++ calls Counter's methods through the // vtable tslang makes for it -@cppvtable +@cpp class Callback { - destructor(): void; // slot 0 - message(severity: u8, text: string): void; // slot 1 - count(): s32; // slot 2 - isEven(x: s32): boolean; // slot 3 + @slot(0) destructor(): void; + @slot(1) message(severity: u8, text: string): void; + @slot(2) count(): s32; + @slot(3) isEven(x: s32): boolean; } -@cppvtable +@cpp class Counter extends Callback { total: s32 = 0; unexpected = 0; @@ -53,6 +56,11 @@ class Counter extends Callback { isEven(x: s32): boolean { return x % 2 == 0; } + + // TypeScript's own (no @slot, overriding nothing): not in the vtable C++ calls through + doubled(): s32 { + return this.count() * 2; + } } declare function abi_call_callback(callback: Opaque): s32; @@ -72,12 +80,14 @@ function main() { assert(two.a == 0x1234567890 + 7 && abi_is_storage(two.b, 9) == 1, "a trivial 16-byte record"); 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 counter = new Counter(5); assert(abi_call_callback(counter) == 10 && counter.unexpected == 0, "C++ calls the methods implemented here"); assert(abi_callback_is_even(counter, 4) == 1 && abi_callback_is_even(counter, 7) == 0, "a bool result"); const callback: Callback = counter; assert(callback.count() == 10, "and tslang through the C++ interface"); + assert(counter.doubled() == 20, "a method of TypeScript's own"); print("done."); } diff --git a/tslang/test/tester/cppvtable/box_any.ts b/tslang/test/tester/cppvtable/box_any.ts index 4b5251f44..11ba603cc 100644 --- a/tslang/test/tester/cppvtable/box_any.ts +++ b/tslang/test/tester/cppvtable/box_any.ts @@ -1,7 +1,7 @@ // An `any` is unboxed by asking the object what it is, which a C++ object can't answer. -@cppvtable +@cpp class Base { - first(): s32; + @slot(0) first(): s32; } function main() { diff --git a/tslang/test/tester/cppvtable/constructor.ts b/tslang/test/tester/cppvtable/constructor.ts index ec7a974b7..e31428092 100644 --- a/tslang/test/tester/cppvtable/constructor.ts +++ b/tslang/test/tester/cppvtable/constructor.ts @@ -1,8 +1,8 @@ -// A @cppvtable class's objects are made by C++. -@cppvtable +// A @cpp class's objects are made by C++. +@cpp class Base { constructor() {} - first(): s32; + @slot(0) first(): s32; } function main() { diff --git a/tslang/test/tester/cppvtable/export_implementation.ts b/tslang/test/tester/cppvtable/export_implementation.ts index 4071aa6b2..253cd5645 100644 --- a/tslang/test/tester/cppvtable/export_implementation.ts +++ b/tslang/test/tester/cppvtable/export_implementation.ts @@ -1,11 +1,11 @@ // A class implementing C++ methods isn't exported: an importer would get its declaration only, with // the methods but not the vtable they are in. -@cppvtable +@cpp export class Base { - first(): s32; + @slot(0) first(): s32; } -@cppvtable +@cpp export class Implementation extends Base { first(): s32 { return 1; } } diff --git a/tslang/test/tester/cppvtable/extends_plain.ts b/tslang/test/tester/cppvtable/extends_plain.ts index d22d4ce0d..6ad5b7d0f 100644 --- a/tslang/test/tester/cppvtable/extends_plain.ts +++ b/tslang/test/tester/cppvtable/extends_plain.ts @@ -1,11 +1,11 @@ -// A @cppvtable class extending an ordinary class would have the ordinary class's slots first. +// A @cpp class extending an ordinary class would have the ordinary class's slots first. class Plain { m(): s32 { return 1; } } -@cppvtable +@cpp class Base extends Plain { - first(): s32; + @slot(0) first(): s32; } function main() { diff --git a/tslang/test/tester/cppvtable/field_initializer.ts b/tslang/test/tester/cppvtable/field_initializer.ts index d46e050b7..56e4485c6 100644 --- a/tslang/test/tester/cppvtable/field_initializer.ts +++ b/tslang/test/tester/cppvtable/field_initializer.ts @@ -1,8 +1,8 @@ -// A field initializer is code run by a constructor, which a @cppvtable class doesn't have. -@cppvtable +// A field initializer is code run by a constructor, which a @cpp class doesn't have. +@cpp class Base { value: s32 = 1; - first(): s32; + @slot(0) first(): s32; } function main() { diff --git a/tslang/test/tester/cppvtable/get_accessor.ts b/tslang/test/tester/cppvtable/get_accessor.ts index ae1d4c100..a02aaa540 100644 --- a/tslang/test/tester/cppvtable/get_accessor.ts +++ b/tslang/test/tester/cppvtable/get_accessor.ts @@ -1,8 +1,8 @@ // An accessor is a generated method, and its slot wouldn't follow the declaration order. -@cppvtable +@cpp class Base { get size(): s32; - first(): s32; + @slot(0) first(): s32; } function main() { diff --git a/tslang/test/tester/cppvtable/hidden_return_target.ts b/tslang/test/tester/cppvtable/hidden_return_target.ts index 69b79d022..34728c693 100644 --- a/tslang/test/tester/cppvtable/hidden_return_target.ts +++ b/tslang/test/tester/cppvtable/hidden_return_target.ts @@ -2,9 +2,9 @@ // through on the stack there, and the callee pops it, which isn't done. type Pair = [first: s32, second: s32]; -@cppvtable +@cpp class Methods { - pair(x: s32): Pair; + @slot(0) pair(x: s32): Pair; } function main() { diff --git a/tslang/test/tester/cppvtable/implements.ts b/tslang/test/tester/cppvtable/implements.ts index 449d10c07..3ffb5fce6 100644 --- a/tslang/test/tester/cppvtable/implements.ts +++ b/tslang/test/tester/cppvtable/implements.ts @@ -3,9 +3,9 @@ interface IFirst { first(): s32; } -@cppvtable +@cpp class Base implements IFirst { - first(): s32; + @slot(0) first(): s32; } function main() { diff --git a/tslang/test/tester/cppvtable/instanceof.ts b/tslang/test/tester/cppvtable/instanceof.ts index 6a7efb7c7..5f9343c5c 100644 --- a/tslang/test/tester/cppvtable/instanceof.ts +++ b/tslang/test/tester/cppvtable/instanceof.ts @@ -1,7 +1,7 @@ -// `instanceof` asks slot 0, which is a C++ method in a @cppvtable class's vtable. -@cppvtable +// `instanceof` asks slot 0, which is a C++ method in a @cpp class's vtable. +@cpp class Base { - first(): s32; + @slot(0) first(): s32; } class Plain {} diff --git a/tslang/test/tester/cppvtable/itanium_record.ts b/tslang/test/tester/cppvtable/itanium_record.ts index 1c05d1329..fb44a8baf 100644 --- a/tslang/test/tester/cppvtable/itanium_record.ts +++ b/tslang/test/tester/cppvtable/itanium_record.ts @@ -2,9 +2,9 @@ // which a tuple of them isn't; without @cppnontrivial it is an error, not a wrong call. type Pair = [first: s32, second: s32]; -@cppvtable +@cpp class Methods { - pair(x: s32): Pair; + @slot(0) pair(x: s32): Pair; } function main() { diff --git a/tslang/test/tester/cppvtable/memory_model.ts b/tslang/test/tester/cppvtable/memory_model.ts index 462627735..76939473e 100644 --- a/tslang/test/tester/cppvtable/memory_model.ts +++ b/tslang/test/tester/cppvtable/memory_model.ts @@ -1,8 +1,8 @@ // 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 +@cpp class Base { - first(): s32; + @slot(0) first(): s32; } function call(b: Base) { diff --git a/tslang/test/tester/cppvtable/method_body.ts b/tslang/test/tester/cppvtable/method_body.ts index 29946f598..dfdc47a93 100644 --- a/tslang/test/tester/cppvtable/method_body.ts +++ b/tslang/test/tester/cppvtable/method_body.ts @@ -2,12 +2,12 @@ // struct yet: its body would have to write it through the hidden pointer. type Pair = [first: s32, second: s32]; -@cppvtable +@cpp class Base { - pair(x: s32): Pair; + @slot(0) pair(x: s32): Pair; } -@cppvtable +@cpp class Implementation extends Base { pair(x: s32): Pair { return [x, x]; } } diff --git a/tslang/test/tester/cppvtable/nontrivial_no_struct.ts b/tslang/test/tester/cppvtable/nontrivial_no_struct.ts index 05d7c0159..498b1ac22 100644 --- a/tslang/test/tester/cppvtable/nontrivial_no_struct.ts +++ b/tslang/test/tester/cppvtable/nontrivial_no_struct.ts @@ -1,9 +1,9 @@ // @cppnontrivial says how a returned C++ record is returned: on a method returning a number it is // a mistake. -@cppvtable +@cpp class Methods { @cppnontrivial - count(): s32; + @slot(0) count(): s32; } function main() { diff --git a/tslang/test/tester/cppvtable/nonvirtual_struct.ts b/tslang/test/tester/cppvtable/nonvirtual_struct.ts new file mode 100644 index 000000000..ffaea26cc --- /dev/null +++ b/tslang/test/tester/cppvtable/nonvirtual_struct.ts @@ -0,0 +1,12 @@ +// A C++ non-virtual method returning a record isn't supported yet: its symbol would have to be +// declared with the hidden result pointer. +type Pair = [a: Opaque | null, b: Opaque | null]; + +@cpp +class Base { + @linkname("?pair@Base@@QEAA?AUPair@@XZ") + pair(): Pair; +} + +function main() { +} diff --git a/tslang/test/tester/cppvtable/plain_extends.ts b/tslang/test/tester/cppvtable/plain_extends.ts index 0c47abd71..7c01ddc3c 100644 --- a/tslang/test/tester/cppvtable/plain_extends.ts +++ b/tslang/test/tester/cppvtable/plain_extends.ts @@ -1,7 +1,7 @@ -// An ordinary class extending a @cppvtable class would add slots C++ doesn't have. -@cppvtable +// An ordinary class extending a @cpp class would add slots C++ doesn't have. +@cpp class Base { - first(): s32; + @slot(0) first(): s32; } class Plain extends Base { diff --git a/tslang/test/tester/cppvtable/slot_abstract.ts b/tslang/test/tester/cppvtable/slot_abstract.ts new file mode 100644 index 000000000..575880fed --- /dev/null +++ b/tslang/test/tester/cppvtable/slot_abstract.ts @@ -0,0 +1,8 @@ +// An abstract method is virtual: its slot has to be given. +@cpp +abstract class Base { + abstract first(): s32; +} + +function main() { +} diff --git a/tslang/test/tester/cppvtable/slot_argument.ts b/tslang/test/tester/cppvtable/slot_argument.ts new file mode 100644 index 000000000..a0fcb255c --- /dev/null +++ b/tslang/test/tester/cppvtable/slot_argument.ts @@ -0,0 +1,8 @@ +// @slot takes the slot number. +@cpp +class Base { + @slot("first") first(): s32; +} + +function main() { +} diff --git a/tslang/test/tester/cppvtable/slot_conflict.ts b/tslang/test/tester/cppvtable/slot_conflict.ts new file mode 100644 index 000000000..0bae6e7cb --- /dev/null +++ b/tslang/test/tester/cppvtable/slot_conflict.ts @@ -0,0 +1,9 @@ +// Two methods in one slot. +@cpp +class Base { + @slot(1) first(): s32; + @slot(1) second(): s32; +} + +function main() { +} diff --git a/tslang/test/tester/cppvtable/slot_missing.ts b/tslang/test/tester/cppvtable/slot_missing.ts new file mode 100644 index 000000000..53c001121 --- /dev/null +++ b/tslang/test/tester/cppvtable/slot_missing.ts @@ -0,0 +1,10 @@ +// A method of a @cpp class without @slot isn't virtual, and one without a body is then a C++ +// non-virtual method, which needs its symbol: a forgotten @slot is an error, not a wrong call. +@cpp +class Base { + @slot(0) first(): s32; + second(): s32; +} + +function main() { +} diff --git a/tslang/test/tester/cppvtable/slot_old_name.ts b/tslang/test/tester/cppvtable/slot_old_name.ts new file mode 100644 index 000000000..15921e486 --- /dev/null +++ b/tslang/test/tester/cppvtable/slot_old_name.ts @@ -0,0 +1,8 @@ +// @cppvtable is the old name of @cpp: unknown, it would leave the class an ordinary one. +@cppvtable +class Base { + @slot(0) first(): s32; +} + +function main() { +} diff --git a/tslang/test/tester/cppvtable/slot_override.ts b/tslang/test/tester/cppvtable/slot_override.ts new file mode 100644 index 000000000..dab80f8fe --- /dev/null +++ b/tslang/test/tester/cppvtable/slot_override.ts @@ -0,0 +1,13 @@ +// A method overriding a base class's slot keeps that slot: @slot naming another is an error. +@cpp +class Base { + @slot(0) first(): s32; +} + +@cpp +class Derived extends Base { + @slot(2) first(): s32; +} + +function main() { +} diff --git a/tslang/test/tester/cppvtable/slot_plain_class.ts b/tslang/test/tester/cppvtable/slot_plain_class.ts new file mode 100644 index 000000000..e00cd7ce8 --- /dev/null +++ b/tslang/test/tester/cppvtable/slot_plain_class.ts @@ -0,0 +1,9 @@ +// @slot is for a method of a @cpp class: an ordinary class has a vtable of tslang's own. +class Plain { + @slot(0) first(): s32 { + return 1; + } +} + +function main() { +} diff --git a/tslang/test/tester/cppvtable/static_member.ts b/tslang/test/tester/cppvtable/static_member.ts index 95897ab9e..3f70fe50b 100644 --- a/tslang/test/tester/cppvtable/static_member.ts +++ b/tslang/test/tester/cppvtable/static_member.ts @@ -1,8 +1,8 @@ // A static member would take a vtable slot. -@cppvtable +@cpp class Base { static count: s32; - first(): s32; + @slot(0) first(): s32; } function main() { diff --git a/tslang/test/tester/cppvtable/to_interface.ts b/tslang/test/tester/cppvtable/to_interface.ts index 846ee9ca5..c20b2178e 100644 --- a/tslang/test/tester/cppvtable/to_interface.ts +++ b/tslang/test/tester/cppvtable/to_interface.ts @@ -1,7 +1,7 @@ // An interface over a class object is found through the object's vtable, which is the C++ one. -@cppvtable +@cpp class Base { - first(): s32; + @slot(0) first(): s32; } interface IFirst { diff --git a/tslang/test/tester/cppvtable/unbox_any.ts b/tslang/test/tester/cppvtable/unbox_any.ts index 364641ca5..52ad855dc 100644 --- a/tslang/test/tester/cppvtable/unbox_any.ts +++ b/tslang/test/tester/cppvtable/unbox_any.ts @@ -1,7 +1,7 @@ -// Unboxing an `any` to a @cppvtable class would ask the object what it is. -@cppvtable +// Unboxing an `any` to a @cpp class would ask the object what it is. +@cpp class Base { - first(): s32; + @slot(0) first(): s32; } function main() { diff --git a/tslang/test/tester/tests/00cpp_slot.ts b/tslang/test/tester/tests/00cpp_slot.ts new file mode 100644 index 000000000..7ed4db6e1 --- /dev/null +++ b/tslang/test/tester/tests/00cpp_slot.ts @@ -0,0 +1,58 @@ +// @slot(N): a method of a @cpp class is a C++ virtual method, slot N of the vtable (a base class's +// slots counted), whatever the declaration order; the slots no method names are skipped. A method +// without @slot isn't virtual: with a body it is a method of TypeScript's own (a C++ non-virtual +// method, without a body, is called by its symbol: cppvtable/abi_*.ts). The "C++ object" is made +// by hand, as in 00cppvtable_class.ts. + +@cpp +class Base { + value: s32; + @slot(3) fourth(x: s32): s32; // slots 0 and 2 are never called: not declared + @slot(1) second(): s32; + + // TypeScript's own: not in the vtable + twice(): s32 { + return this.second() * 2; + } + +} + +@cpp +class Derived extends Base { + @slot(5) sixth(): s32; // after the base's slots, slot 4 skipped +} + +type ObjectData = [vtable: Opaque, value: s32]; + +function slotUnused(self: Opaque): s32 { + return -1; +} + +function slot1(self: Opaque): s32 { + return 10; +} + +function slot3(self: Opaque, x: s32): s32 { + return Deref(>self).value * x; +} + +function slot5(self: Opaque): s32 { + return 60; +} + +function main() { + let vtable: Opaque[] = [slotUnused, slot1, slotUnused, slot3, slotUnused, slot5]; + let data: ObjectData = [Ref(vtable[0]), 7]; + const object = Ref(data); + + const base = object; + assert(base.second() == 10, "slot 1"); + assert(base.fourth(3) == 21, "slot 3, declared first"); + assert(base.twice() == 20, "a method of TypeScript's own calls a slot"); + + const derived = object; + assert(derived.sixth() == 60, "slot 5 of a derived class"); + assert(derived.fourth(2) == 14, "a base slot through a derived class"); + + print("done."); +} diff --git a/tslang/test/tester/tests/00cppvtable_class.ts b/tslang/test/tester/tests/00cppvtable_class.ts index 562abfdc8..3df89eea5 100644 --- a/tslang/test/tester/tests/00cppvtable_class.ts +++ b/tslang/test/tester/tests/00cppvtable_class.ts @@ -1,25 +1,24 @@ -// 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 @cpp class is laid over an object made elsewhere (C++): its first word is the vtable, a method +// with @slot(N) is slot N of it (a base class's slots counted), 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 +@cpp class Base { value: s32; // the object's first field, after the vtable pointer - first(): s32; // slot 0 - second(x: s32): s32; // slot 1 + @slot(0) first(): s32; + @slot(1) second(x: s32): s32; } -@cppvtable +@cpp class Derived extends Base { - third(x: s32, y: s32): s32; // slot 2 + @slot(2) third(x: s32, y: s32): s32; } // `abstract` reads the same -@cppvtable +@cpp abstract class AbstractBase { - abstract first(): s32; // slot 0 - abstract second(x: s32): s32; // slot 1 + @slot(0) abstract first(): s32; + @slot(1) abstract second(x: s32): s32; } type ObjectData = [vtable: Opaque, value: s32]; diff --git a/tslang/test/tester/tests/00cppvtable_hidden_return.ts b/tslang/test/tester/tests/00cppvtable_hidden_return.ts index 598ff1ff1..39547ed37 100644 --- a/tslang/test/tester/tests/00cppvtable_hidden_return.ts +++ b/tslang/test/tester/tests/00cppvtable_hidden_return.ts @@ -1,4 +1,4 @@ -// A @cppvtable method declared to return a tuple returns a C++ record (a struct, a class such as +// A @cpp 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. @@ -8,17 +8,17 @@ 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 +@cpp 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 + @slot(0) pair(x: s32): Pair; + @slot(1) triple(): Triple; + @slot(2) plain(x: s32, y: s32): s32; // a number, returned as before } -@cppvtable +@cpp class DerivedMethods extends Methods { - otherPair(x: s32): Pair; // slot 3 + @slot(3) otherPair(x: s32): Pair; } type ObjectData = [vtable: Opaque, value: s32]; diff --git a/tslang/test/tester/tests/export_cppvtable_class.ts b/tslang/test/tester/tests/export_cppvtable_class.ts index 540a3debd..5bfedd11e 100644 --- a/tslang/test/tester/tests/export_cppvtable_class.ts +++ b/tslang/test/tester/tests/export_cppvtable_class.ts @@ -1,19 +1,25 @@ 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(). + // A @cpp class exported from one module and used from another: the declaration embedded + // in the binary (__decls) keeps the decorators, @cpp and each method's @slot, so the importer + // has the same slots - with no `.instanceOf` - and both modules call the same C++ method for + // second(). - @cppvtable + @cpp export class Base { value: s32; - first(): s32; // slot 0 - second(x: s32): s32; // slot 1 + @slot(0) first(): s32; + @slot(1) second(x: s32): s32; + + // TypeScript's own, not a slot: the importer calls this module's function + twice(x: s32): s32 { + return this.second(x) * 2; + } } - @cppvtable + @cpp export class Derived extends Base { - third(x: s32, y: s32): s32; // slot 2 + @slot(2) third(x: s32, y: s32): s32; } // called through this module's numbering diff --git a/tslang/test/tester/tests/export_cppvtable_hidden_return.ts b/tslang/test/tester/tests/export_cppvtable_hidden_return.ts index ec0dfc66e..31fa0c5fb 100644 --- a/tslang/test/tester/tests/export_cppvtable_hidden_return.ts +++ b/tslang/test/tester/tests/export_cppvtable_hidden_return.ts @@ -1,22 +1,22 @@ namespace M { - // A @cppvtable method returning a record (00cppvtable_hidden_return.ts), exported from one + // A @cpp 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, and @cppnontrivial, so the importer passes the hidden result pointer too. // MSVC only (it ignores @cppnontrivial, which the Itanium ABI needs). export type Pair = [first: s32, second: s32]; - @cppvtable + @cpp export class Base { value: s32; - first(): s32; // slot 0 + @slot(0) first(): s32; } - @cppvtable + @cpp export class Derived extends Base { @cppnontrivial - pair(x: s32): Pair; // slot 1: a record, returned through a pointer after `this` + @slot(1) pair(x: s32): Pair; // a record, returned through a pointer after `this` } // called through this module's numbering diff --git a/tslang/test/tester/tests/import_cppvtable_class.ts b/tslang/test/tester/tests/import_cppvtable_class.ts index e1c43f52b..afb4191a2 100644 --- a/tslang/test/tester/tests/import_cppvtable_class.ts +++ b/tslang/test/tester/tests/import_cppvtable_class.ts @@ -28,6 +28,7 @@ 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"); + assert(derived.twice(1) == 66, "a method of TypeScript's own, from the exporting module"); print("done."); } diff --git a/tslang/test/tester/verify-ownership.cmake b/tslang/test/tester/verify-ownership.cmake index 820bf28c1..c112d560d 100644 --- a/tslang/test/tester/verify-ownership.cmake +++ b/tslang/test/tester/verify-ownership.cmake @@ -35,7 +35,8 @@ set(not_compilable_alone import_vars.ts # written for `-nostrictnull` raytrace-0.ts - # a @cppvtable class is an error under rc and own (cppvtable/memory_model.ts) + # a @cpp class is an error under rc and own (cppvtable/memory_model.ts) + 00cpp_slot.ts 00cppvtable_class.ts 00cppvtable_hidden_return.ts export_cppvtable_class.ts diff --git a/tslang/test/tester/x86-exclusions.cmake b/tslang/test/tester/x86-exclusions.cmake index d06c80b16..386c4a28f 100644 --- a/tslang/test/tester/x86-exclusions.cmake +++ b/tslang/test/tester/x86-exclusions.cmake @@ -7,6 +7,7 @@ set(TSLANG_X86_EXCLUDED test-compile-internals + test-compile-00-cpp-slot test-compile-00-cppvtable-class test-compile-00-cppvtable-hidden-return test-compile-shared-export-import-cppvtable-class @@ -15,14 +16,16 @@ set(TSLANG_X86_EXCLUDED test-compile-none-corpus-internals ) +set(TSLANG_X86_EXCLUDED_REASON_test-compile-00-cpp-slot + "a @cpp class is an error on i686: MSVC member functions are __thiscall there") set(TSLANG_X86_EXCLUDED_REASON_test-compile-00-cppvtable-class - "a @cppvtable class is an error on i686: MSVC member functions are __thiscall there") + "a @cpp 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") + "a @cpp 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") + "a @cpp 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") + "a @cpp 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).