Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions docs/own-memory-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5079,5 +5079,36 @@ a `!ts.ref<!ts.class_storage<@Walker>>`), 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**
(`<Class>.<Interface>..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.<field>.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.
36 changes: 32 additions & 4 deletions tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<int32_t>::min();

// Whether an op is in the vtable a class's object is seen through as an interface
// (`<Class>.<Interface>..vtbl`), not its class's own (`<Class>..vtbl`): both start with the
// class's `..instanceOf`.
static bool inInterfaceVTable(mlir::Operation *op)
{
auto globalOp = op->getParentOfType<mlir_ts::GlobalOp>();
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<mlir_ts::ClassStorageType> &classStorages, llvm::StringRef derived,
Expand Down Expand Up @@ -657,8 +680,11 @@ namespace own_signature
{
// the fields of a callback type, by class and position
std::map<std::pair<mlir::Operation *, int32_t>, llvm::SmallVector<mlir_ts::PropertyRefOp>> 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 (`<Class>.<Interface>..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<mlir_ts::PropertyRefOp>(op))
Expand All @@ -674,7 +700,7 @@ namespace own_signature
}

auto reachedOtherwise = llvm::any_of(op->getResultTypes(), [](mlir::Type type) {
return mlir::isa<mlir_ts::InterfaceType, mlir_ts::AnyType, mlir_ts::UnionType, mlir_ts::UnknownType,
return mlir::isa<mlir_ts::AnyType, mlir_ts::UnionType, mlir_ts::UnknownType,
mlir_ts::ObjectType>(type);
});
for (auto operand : op->getOperands())
Expand Down Expand Up @@ -810,7 +836,9 @@ namespace own_signature
if (!mlir::isa<mlir_ts::ThisVirtualSymbolRefOp, mlir_ts::ThisSymbolRefOp>(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
Expand Down
8 changes: 5 additions & 3 deletions tslang/test/tester/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down
81 changes: 81 additions & 0 deletions tslang/test/tester/own/own_callback_interface.ts
Original file line number Diff line number Diff line change
@@ -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.");
}
47 changes: 47 additions & 0 deletions tslang/test/tester/own/own_err_callback_interface_field.ts
Original file line number Diff line number Diff line change
@@ -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));
}
35 changes: 35 additions & 0 deletions tslang/test/tester/own/own_err_callback_interface_param.ts
Original file line number Diff line number Diff line change
@@ -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));
}
Loading