From cc8d15dd2c55b356607fe04d04ad285b1208d993 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Wed, 28 Oct 2026 09:21:20 +0000 Subject: [PATCH] -mm=own: a class seen through an interface keeps its field families; interface calls open parameter families An interface reaches a class's fields only through vtables built from the class, so a cast to an interface no longer opens the class's callback field families (any, unions, unknown and object still do). A method in an interface's vtable is called through ts.InterfaceSymbolRef with arguments its callback parameter families never saw; its vtable entry no longer counts as a seen call. Before, r.each(pool, drain) through an interface compiled with each's callback family closed and empty, so a call that freed pool's element was taken to release nothing (spec 58). Co-Authored-By: Claude Opus 5.5 --- docs/own-memory-model.md | 8 +- .../2026-09-24-own-memory-model-design.md | 35 +++++++- .../OwnershipSignatureCallbacks.cpp | 36 ++++++++- tslang/test/tester/CMakeLists.txt | 8 +- .../test/tester/own/own_callback_interface.ts | 81 +++++++++++++++++++ .../own/own_err_callback_interface_field.ts | 47 +++++++++++ .../own/own_err_callback_interface_param.ts | 35 ++++++++ 7 files changed, 238 insertions(+), 12 deletions(-) create mode 100644 tslang/test/tester/own/own_callback_interface.ts create mode 100644 tslang/test/tester/own/own_err_callback_interface_field.ts create mode 100644 tslang/test/tester/own/own_err_callback_interface_param.ts diff --git a/docs/own-memory-model.md b/docs/own-memory-model.md index 02456fad7..3a1aaf834 100644 --- a/docs/own-memory-model.md +++ b/docs/own-memory-model.md @@ -750,9 +750,11 @@ a reference gives it back on every path out of a function. to a helper), names again (`const g = f`) or captures in a closure that calls it (`a.forEach((v) => f(v))`, also kept in a local) is known by the functions its callers pass. So is one stored in a class's field (`constructor(private f: ...)`, `this.f = f`, also from a class extending it) - and one a function returns. Not when one of the class's objects, or of a class extending it, - becomes an interface or `any`, or another module may build or extend it: then its fields' - callbacks may be any function. A callback may assign the numbers and strings it + and one a function returns, also when the class's objects are seen through an interface. Not + when one of the class's objects, or of a class extending it, becomes `any` or a union, or another + module may build or extend it: then its fields' callbacks may be any function. Nor is a callback + parameter of a method an interface reaches: a call through the interface may pass any function, + from any module. A callback may assign the numbers and strings it captures (`total += v.n`); one stored in an object may not capture locals, which would escape with it. `copyWithin` works on numbers and strings (a string is copied where it moves); over class instances it would leave one diff --git a/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md b/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md index 1ff20b5f1..f3bd700ae 100644 --- a/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md +++ b/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md @@ -5079,5 +5079,36 @@ a `!ts.ref>`), where the family saw no access of its down, under own, rc, gc and none; own emits no counting. Negative: `own_err_callback_extended_drops`, whose derived class stores a function that replaces a global array the walk's element may be part of. -Not here: a class whose object, or a derived class's, becomes an interface or `any` keeps its fields' -families open. +Not here: a class whose object, or a derived class's, becomes `any` keeps its fields' families open; +an interface: §58. + +## 58. A class seen through an interface, 2026-10-28 + +`const r: Runner = new Walker(show); r.walk(a)` kept `Walker`'s field families open (§55): a class +whose object became an interface was taken to have its fields reached in ways this pass can't see. + +- **An interface reaches a class's fields only through a vtable built from the class** + (`...vtbl`), where the class is known: here, or in a module that names it + (`callableFromOutside` of its constructor, which opens the family). A field's slot is a + `ts.PropertyRef` of the class there (a use the family takes as reaching it otherwise, so it opens); + a function field's is a thunk (`..vtbl..thunk`) that loads it and calls it (a read only + called); storing through it is refused in every model ("saving to constant object"). A cast to + another interface (`castInterfaceToInterface`) remaps those slots. So a cast to an interface no + longer opens the class's fields' families; `any`, a union, `unknown` and `object`, from which + another module may cast an object back, still do. +- **A method in an interface's vtable** (`inInterfaceVTable`: a `..vtbl` global whose class's + `..instanceOf` names another `..vtbl`) is called through `ts.InterfaceSymbolRef` with arguments + its parameter families can't see, from any module the interface reaches: its use there no longer + counts as a call the family has seen (`callsOf`), so its callback parameters' families are open. + This was unsound before: `r.each(pool, drain)` through an interface compiled with `each`'s `f` + family closed and empty, so `f(v)` was taken to release nothing while `drain` replaced `pool`. + +`own_callback_interface` walks through a class seen as two interfaces, its field's callback stored by +the constructor and by a method called on the class, under own, rc, gc and none; own emits no +counting. Negatives: `own_err_callback_interface_param` (the unsound case above) and +`own_err_callback_interface_field` (a callback given through an interface's method and stored into +the field). + +Not here: a class whose object becomes `any`, a union, `unknown` or `object` keeps its fields' +families open; a callback parameter of a method in an interface's vtable is open even where the +method is called on its class. diff --git a/tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp b/tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp index bc51aa3f1..d9e9187d1 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp @@ -114,6 +114,29 @@ namespace own_signature // The index a function's result family is kept under: no parameter's, nor a field's (spec 56) static constexpr int32_t RESULT_FAMILY = std::numeric_limits::min(); + // Whether an op is in the vtable a class's object is seen through as an interface + // (`...vtbl`), not its class's own (`..vtbl`): both start with the + // class's `..instanceOf`. + static bool inInterfaceVTable(mlir::Operation *op) + { + auto globalOp = op->getParentOfType(); + if (!globalOp) + { + return false; + } + + auto inInterface = false; + globalOp.getInitializerRegion().walk([&](mlir_ts::SymbolRefOp symbolRefOp) { + auto symbol = symbolRefOp.getIdentifier(); + if (symbol.ends_with("..instanceOf")) + { + inInterface = globalOp.getSymName() != (symbol.drop_back(llvm::StringRef("..instanceOf").size()) + "..vtbl").str(); + } + }); + + return inInterface; + } + // Whether a class extends another: a base is the first field of a class's storage. One whose // storage is not known here, or whose chain is too deep to follow, may. static bool extendsClass(const llvm::StringMap &classStorages, llvm::StringRef derived, @@ -657,8 +680,11 @@ namespace own_signature { // the fields of a callback type, by class and position std::map, llvm::SmallVector> fields; - // the classes whose objects go where their fields may be reached otherwise: cast to an - // interface, `any` or a union + // the classes whose objects go where their fields may be reached otherwise: cast to `any`, + // a union, `unknown` or `object`, from which another module may cast them back. An interface + // reaches them only through a vtable built from the class (`...vtbl`), here + // or in a module that names it: a field's slot is a `ts.PropertyRef` here, a function + // field's a thunk that loads it and calls it, and another interface's cast remaps those slots. llvm::StringSet<> escaping; module.walk([&](mlir::Operation *op) { if (auto fieldOp = mlir::dyn_cast(op)) @@ -674,7 +700,7 @@ namespace own_signature } auto reachedOtherwise = llvm::any_of(op->getResultTypes(), [](mlir::Type type) { - return mlir::isa(type); }); for (auto operand : op->getOperands()) @@ -810,7 +836,9 @@ namespace own_signature if (!mlir::isa(user) || !notOverridden(funcOp) || !onlyCalled(user->getResult(0))) { - return isClassVTableEntry(user) && notOverridden(funcOp); + // a call through an interface (`ts.InterfaceSymbolRef`) passes what this family can't + // see, from any module the interface reaches + return isClassVTableEntry(user) && !inInterfaceVTable(user) && notOverridden(funcOp); } // `ts.CallInternal(ts.GetMethod(m), ts.GetThis(m), ...)`: its arguments are the method's diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index bc9fbdc81..b2ebf445c 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -3945,7 +3945,7 @@ 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_shapes own_callback_extended own_callback_interface) 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. @@ -3982,7 +3982,7 @@ 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_shapes own_callback_extended own_callback_interface) 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() @@ -4683,7 +4683,9 @@ set(own_error_cases "own_err_callback_captured_drops|:14:9: error: '[^']*' borrows an element but is used here after it may be released or overwritten" "own_err_callback_field_drops|:16:13: error: '[^']*' borrows an element but is used here after it may be released or overwritten" "own_err_callback_returned_drops|:21:9: error: '[^']*' borrows an element but is used here after it may be released or overwritten" - "own_err_callback_extended_drops|:16:13: error: '[^']*' borrows an element but is used here after it may be released or overwritten") + "own_err_callback_extended_drops|:16:13: error: '[^']*' borrows an element but is used here after it may be released or overwritten" + "own_err_callback_interface_param|:34:11: error: a value read out of the global 'pool' is lent to a call that may release or overwrite it" + "own_err_callback_interface_field|:46:11: error: a value read out of the global 'pool' is lent to a call that may release or overwrite it") foreach(own_error_case ${own_error_cases}) string(FIND "${own_error_case}" "|" own_error_sep) string(SUBSTRING "${own_error_case}" 0 ${own_error_sep} own_error_name) diff --git a/tslang/test/tester/own/own_callback_interface.ts b/tslang/test/tester/own/own_callback_interface.ts new file mode 100644 index 000000000..f9d7b4b98 --- /dev/null +++ b/tslang/test/tester/own/own_callback_interface.ts @@ -0,0 +1,81 @@ +// A callback stored into a class's field keeps its family when the class's objects are seen through +// an interface (spec 58): the interface reaches the class's fields only through vtables built from +// the class, here. Each walk reads the element again after the call, so the call must release +// nothing of it. Each step churns, so an element freed too early reads garbage; every model runs it. +class Item { + constructor(public n: number) {} +} + +function churn() { + const junk: Item[] = []; + for (let i = 0; i < 64; i++) junk.push(new Item(999)); +} + +let seen = 0; + +function once(v: Item) { + seen++; +} + +function twice(v: Item) { + seen += 2; +} + +interface Runner { + walk(a: Item[]): number; +} + +interface Named { + name: string; +} + +class Walker implements Runner, Named { + name = "w"; + + constructor(private f: (v: Item) => void) {} + + use(f: (v: Item) => void) { + this.f = f; + } + + walk(a: Item[]): number { + let s: number = 0; + for (let i = 0; i < a.length; i++) { + const v = a[i]; + this.f(v); + s += v.n; + } + + return s; + } +} + +function run(r: Runner, a: Item[]) { + return r.walk(a); +} + +function nameOf(n: Named) { + return n.name; +} + +function test(a: Item[]) { + seen = 0; + const w = new Walker(once); + let r = run(w, a); + churn(); + w.use(twice); + r += run(w, a) + w.walk(a); + return nameOf(w) == "w" ? r * 100 + seen : -1; +} + +function main() { + let ok = 0; + for (let i = 0; i < 300; i++) { + const a = [new Item(1), new Item(5), new Item(2)]; + if (test(a) == 2415) ok++; + churn(); + } + + assert(ok == 300, "ok"); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_callback_interface_field.ts b/tslang/test/tester/own/own_err_callback_interface_field.ts new file mode 100644 index 000000000..fb1914278 --- /dev/null +++ b/tslang/test/tester/own/own_err_callback_interface_field.ts @@ -0,0 +1,47 @@ +// -mm=own rejects: a callback given through an interface's method is stored into the class's field +// (spec 58), here one that replaces the global array `walk` walks, releasing what it held, while +// `walk` reads the element again after the call. +class Item { + constructor(public n: number) {} +} + +let pool: Item[] = []; + +interface Runner { + use(f: (v: Item) => void): void; + walk(a: Item[]): number; +} + +class Walker implements Runner { + constructor(private f: (v: Item) => void) {} + + use(f: (v: Item) => void) { + this.f = f; + } + + walk(a: Item[]): number { + let s: number = 0; + for (let i = 0; i < a.length; i++) { + const v = a[i]; + this.f(v); + s += v.n; + } + + return s; + } +} + +function show(v: Item) { + print(v.n); +} + +function drain(v: Item) { + pool = []; +} + +function main() { + pool = [new Item(7)]; + const r: Runner = new Walker(show); + r.use(drain); + print(r.walk(pool)); +} diff --git a/tslang/test/tester/own/own_err_callback_interface_param.ts b/tslang/test/tester/own/own_err_callback_interface_param.ts new file mode 100644 index 000000000..538567767 --- /dev/null +++ b/tslang/test/tester/own/own_err_callback_interface_param.ts @@ -0,0 +1,35 @@ +// -mm=own rejects: a method called through an interface may be given any callback (spec 58), here +// one that replaces the global array the method walks, releasing what it held, while the method +// reads the element again after the call. +class Item { + constructor(public n: number) {} +} + +let pool: Item[] = []; + +interface Runner { + each(a: Item[], f: (v: Item) => void): number; +} + +class Walker implements Runner { + each(a: Item[], f: (v: Item) => void): number { + let s: number = 0; + for (let i = 0; i < a.length; i++) { + const v = a[i]; + f(v); + s += v.n; + } + + return s; + } +} + +function drain(v: Item) { + pool = []; +} + +function main() { + pool = [new Item(7)]; + const r: Runner = new Walker(); + print(r.each(pool, drain)); +}