From 583ac9d9a5edeeac25e6fcf2bf1d9ea262d1a5fc Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Wed, 28 Oct 2026 15:49:58 +0000 Subject: [PATCH] -mm=own: a class family spans one class hierarchy, not every class with that slot A virtual call's family was every function at its vtable position under its method name, in any class. Two specializations of one generic class - Array and Array - were one family, though neither extends the other: `find` returns a borrow for one and a number for the other, the family disagreed, both lost their facts, and any program with both failed under own ("'this value' borrows a field and cannot be stored, returned or captured"). Families are now keyed by the root of the class's chain of bases as well (familyRoot, through the class storages, now collected before the vtables). A slot with a member whose root is not known stays one family, as before. Test: own_generic_specializations (Box and Box). Co-Authored-By: Claude Opus 5.5 --- .../lib/TypeScript/OwnershipSignaturePass.cpp | 3 +- .../lib/TypeScript/OwnershipSignaturePass.h | 13 ++++-- .../TypeScript/OwnershipSignatureResolve.cpp | 45 ++++++++++++++++++- tslang/test/tester/CMakeLists.txt | 6 ++- .../tester/own/own_generic_specializations.ts | 30 +++++++++++++ 5 files changed, 89 insertions(+), 8 deletions(-) create mode 100644 tslang/test/tester/own/own_generic_specializations.ts diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp index a3ba6424d..634b48af6 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp @@ -53,8 +53,9 @@ namespace own_signature findImmortalGlobals(module); moveIntoGlobals(module); rejectBorrowedGlobalInitializers(module); - collectClassVTables(module); + // the storages first: a vtable's family is that of its class's root (familyRoot) collectClassStorages(module); + collectClassVTables(module); collectInterfaceFamilies(module); fieldFunctions.collect(module); // after the class storages (`@cpp`) and the families (holdsFunction reads the interface vtables) diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.h b/tslang/lib/TypeScript/OwnershipSignaturePass.h index f2266e006..6212424de 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.h +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.h @@ -217,9 +217,10 @@ class OwnershipSignaturePass : public mlir::PassWrapper classNames; - // (vtable position, method name) -> the functions at that position under that name, in every - // class vtable of the module: what a virtual call through that slot may reach. - std::map, llvm::SmallVector> families; + // (vtable position, method name, root class) -> the functions at that position under that name, + // in every class vtable of the module whose class has that root (familyRoot): what a virtual + // call through that slot may reach. A slot with a member of an unknown root has one family, root "". + std::map, llvm::SmallVector> families; // Functions whose callers are not all known - a caller outside the module, or a reference to // the symbol that is not a call this pass resolves - and why, for the error in their body. @@ -630,6 +631,12 @@ class OwnershipSignaturePass : public mlir::PassWrapper.find` and `Array.find`, + // one generic's specializations, are not one family (they disagreed, and both lost their facts). + std::string familyRoot(llvm::StringRef className); + llvm::SmallVector familyOf(int64_t index, mlir::StringAttr identifier); // Adds `symbol`'s function to `callees`; false when it is not defined in this module. diff --git a/tslang/lib/TypeScript/OwnershipSignatureResolve.cpp b/tslang/lib/TypeScript/OwnershipSignatureResolve.cpp index b2d641881..15aa66232 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureResolve.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureResolve.cpp @@ -21,15 +21,46 @@ namespace own_signature } }); + // each slot's members by the root of the vtable's class; a slot one of whose members has an + // unknown root is one family, as every slot was before + std::map, llvm::SmallVector>> slots; for (auto &[name, entries] : vtables) { + auto root = familyRoot(name.drop_back(llvm::StringRef("..vtbl").size())); for (auto [position, symbol] : entries) { - families[{position, methodName(symbol.getValue())}].push_back(symbol); + slots[{position, methodName(symbol.getValue())}].push_back({root, symbol}); + } + } + + for (auto &[slot, members] : slots) + { + auto unknownRoot = llvm::any_of(members, [](auto &member) { return member.first.empty(); }); + for (auto &[root, symbol] : members) + { + families[{slot.first, slot.second, unknownRoot ? std::string() : root}].push_back(symbol); } } } + std::string OwnershipSignaturePass::familyRoot(llvm::StringRef className) + { + auto storage = classStorages.lookup(className); + for (auto depth = 0; storage && depth < 64; ++depth) + { + auto fields = storage.getFields(); + auto base = fields.empty() ? mlir_ts::ClassStorageType() : mlir::dyn_cast(fields.front().type); + if (!base) + { + return storage.getName().getValue().str(); + } + + storage = base; + } + + return std::string(); + } + mlir_ts::TupleType OwnershipSignaturePass::storageTupleOf(mlir::Type type) { return own_facts::storageTupleOf(&getContext(), type); @@ -88,8 +119,18 @@ namespace own_signature llvm::SmallVector OwnershipSignaturePass::familyOf(int64_t index, mlir::StringAttr identifier) { + // the family of the identifier's class's root, or the slot's one family of an unknown root llvm::SmallVector members; - if (auto found = families.find({index, methodName(identifier.getValue())}); found != families.end()) + auto name = methodName(identifier.getValue()); + auto className = + name.size() < identifier.getValue().size() ? identifier.getValue().drop_back(name.size() + 1) : llvm::StringRef(); + auto found = families.find({index, name, familyRoot(className)}); + if (found == families.end()) + { + found = families.find({index, name, std::string()}); + } + + if (found != families.end()) { members = found->second; } diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 7b1ea552c..f0800b4e4 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -3955,7 +3955,8 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o own_callback_forwarded own_optional_record_read own_param_default_kept own_literal_into_array own_generator_take own_rest_array_borrows own_string_copies own_captured_string_callback own_callback_and_call own_callback_captured - own_callback_shapes own_callback_extended own_callback_interface) + own_callback_shapes own_callback_extended own_callback_interface + own_generic_specializations) tslang_add_test(NAME test-jit-own-${own_test} COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/own/${own_test}.ts") tslang_add_test(NAME test-compile-own-${own_test} COMMAND test-runner -mm=own "${PROJECT_SOURCE_DIR}/test/tester/own/${own_test}.ts") # The promise: no counting at all in what own emits. @@ -3992,7 +3993,8 @@ foreach(keeps_mm rc gc none) own_optional_record_read own_param_default_kept own_literal_into_array own_generator_take own_rest_array_borrows own_string_copies own_captured_string_callback own_callback_and_call own_callback_captured - own_callback_shapes own_callback_extended own_callback_interface) + own_callback_shapes own_callback_extended own_callback_interface + own_generic_specializations) tslang_add_test(NAME test-jit-${keeps_mm}-${keeps_test} COMMAND test-runner -jit -mm=${keeps_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/${keeps_test}.ts") tslang_add_test(NAME test-compile-${keeps_mm}-${keeps_test} COMMAND test-runner -mm=${keeps_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/${keeps_test}.ts") endforeach() diff --git a/tslang/test/tester/own/own_generic_specializations.ts b/tslang/test/tester/own/own_generic_specializations.ts new file mode 100644 index 000000000..b2ed3bfa6 --- /dev/null +++ b/tslang/test/tester/own/own_generic_specializations.ts @@ -0,0 +1,30 @@ +// -mm=own: two specializations of one generic class are not one class family. `Box.find` +// returns a borrow of its field (through `pick`'s result) and `Box.find` a number; neither +// class extends the other, so a call through either reaches only its own. Grouped by vtable slot and +// method name alone, they "disagreed" and both lost their facts: with Box beside it, +// `Box.find` was "'this value' borrows a field and cannot be stored, returned or captured". +function pick(a: T[], f: (v: T) => boolean): T { + for (const v of a) { + if (f(v)) return v; + } + + return a[0]; +} + +class Box { + constructor(private data: T[]) {} + + find(f: (v: T) => boolean) { + return pick(this.data, f); + } +} + +function main() { + const s = new Box(["a", "bb"]); + const n = new Box([1, 2]); + + assert(s.find((v) => v.length == 2) == "bb"); + assert(n.find((v) => v > 1) == 2); + + print("done."); +}