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
Original file line number Diff line number Diff line change
Expand Up @@ -5068,6 +5068,7 @@ part of.

Not here: a class whose object becomes an interface or `any` keeps its fields' families open; so
does a field given a callback any other way than those above. A class another class extends: §57.
An interface: §58; `any` and a union: §59.

## 57. A callback field of a class others extend, 2026-10-28

Expand All @@ -5089,7 +5090,7 @@ down, under own, rc, gc and none; own emits no counting. Negative: `own_err_call
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 `any` keeps its fields' families open;
an interface: §58.
an interface: §58; `any`: §59.

## 58. A class seen through an interface, 2026-10-28

Expand Down Expand Up @@ -5120,4 +5121,45 @@ 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.
method is called on its class. Both: §59.

## 59. Casts to `any` and calls through an interface, 2026-10-28

Two families stayed open that need not: a class's callback field once an object of the class became
`any`, a union, `unknown` or `object` (§55-§58), and a callback parameter of a method an interface's
vtable holds (§58).

- **A class's fields after a cast to `any`, a union, `unknown` or `object`.** Every way back to the
fields is a cast to the class: `<Walker>x` unboxes, `u instanceof Walker` narrows
(`ts.GetValueFromUnion`), and the field is a `ts.PropertyRef` of the class again, which the family
sees here. Another module can cast back only to a class it names, and a module that names the class
can build it too (`callableFromOutside` of its constructor, which opens the family already). `any`
to an interface uses only the vtables this module made for the class (`castAnyToInterface`, §58);
`any` to a tuple or an object type reads no field of a class (a class object read as one crashes
in every model), and `object` cannot be cast back to a class at all. So the cast no longer opens
the family (`collectFieldFamilies` drops `escaping`).
- **A method an interface's vtable holds** (`<Class>.<Interface>..vtbl`) is called through the
interface's slot. When every slot that holds it is closed (`interfaceFamilies`, spec 31: no other
module sees the interface, every value of it is made from a vtable this module reads) and it is not
`interfaceReached`, its callers are every call through those slots (`interfaceCallsOf`): a
`ts.InterfaceSymbolRef` of the slot, not optional, bound and called with its `this` first, as a
method bound to its object is (`boundCalls`). Their arguments join its parameters' families with
those of its direct calls. Any other use of the slot's value opens the family, as before.
- **A value given on through an interface's slot** reaches each member of the slot: `parameterOf`
returns the parameter of each (`FamilyTargets`), and the value is given to a closed family only
when all of them are. A callback parameter given on (`e.each(a, f)` in a function taking `f`) is
forwarded to each, and a function given by name passes to each.

`own_callback_cast` walks through a class cast to `any` and back, and to a union and narrowed back,
its callback field stored by the constructor and by a method, under own, rc, gc and none; own emits no
counting. `own_callback_interface_slot` calls two classes' methods through an interface, directly, and
with a caller's callback parameter given on through the interface. Negatives:
`own_err_callback_cast_field_drops` (a draining callback stored into a field of a class cast to `any`
and back), `own_err_callback_interface_slot_drops` (a callback given only through the interface still
counts for a direct call that lends the global array it replaces) and
`own_err_callback_interface_slot_targets` (a callback parameter given on through the slot reaches
both classes).

Not here: a function-typed field of an object literal typed by its receiver (`{ w: x => x + 1 }` given
for `{ w: (x: number) => number }`) is a method bound to the literal object, which nothing owns; own
rejects the call that receives it.
240 changes: 164 additions & 76 deletions tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions tslang/lib/TypeScript/OwnershipSignaturePass.h
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,9 @@ class OwnershipSignaturePass : public mlir::PassWrapper<OwnershipSignaturePass,

// (function, parameter index) -> the functions its callers pass there
std::map<std::pair<mlir::Operation *, int32_t>, CallbackFamily> callbackFamilies;

// the families a value given on reaches: one, or one per member of an interface's slot (spec 59)
using FamilyTargets = llvm::SmallVector<std::pair<mlir::Operation *, int32_t>, 1>;
// the members whose families disagreed (meetFacts), for the error in their bodies
llvm::DenseSet<mlir::Operation *> demotedCallbacks;
// uses of a member's symbol that only pass it to a closed family: no escape (findOpen)
Expand Down Expand Up @@ -861,9 +864,9 @@ class OwnershipSignaturePass : public mlir::PassWrapper<OwnershipSignaturePass,

// The families of the class fields of a callback type: the functions stored into each by name,
// and the callback parameters given on into it, when every read of it is only called or given on
// and no object of the class is reached but as itself (spec 55)
// and no other module builds the class (spec 55, 59)
void collectFieldFamilies(mlir::ModuleOp module,
llvm::function_ref<std::pair<mlir::Operation *, int32_t>(mlir::OpOperand &)> parameterOf);
llvm::function_ref<FamilyTargets(mlir::OpOperand &)> parameterOf);

// The closure function a capture box is bound to, when every box it is bound to holds `cell` in
// field `position`.
Expand Down
9 changes: 6 additions & 3 deletions tslang/test/tester/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3974,7 +3974,7 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o
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_generic_specializations)
own_generic_specializations own_callback_cast own_callback_interface_slot)
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 @@ -4012,7 +4012,7 @@ foreach(keeps_mm rc gc none)
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_generic_specializations)
own_generic_specializations own_callback_cast own_callback_interface_slot)
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 @@ -4715,7 +4715,10 @@ set(own_error_cases
"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_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")
"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"
"own_err_callback_cast_field_drops|:16:13: error: '[^']*' borrows an element but is used here after it may be released or overwritten"
"own_err_callback_interface_slot_drops|:41: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_slot_targets|:47:12: 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
90 changes: 90 additions & 0 deletions tslang/test/tester/own/own_callback_cast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// A callback stored into a class's field keeps its family when the class's objects are cast to `any`
// or to a union (spec 59): each way back is a cast to the class, whose reads of the field are the
// family's again. 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;
}

class Walker {
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;
}
}

class Other {
walk(a: Item[]): number {
return 0;
}
}

function viaAny(x: any, a: Item[]) {
const w = <Walker>x;
return w.walk(a);
}

function viaUnion(u: Walker | Other, a: Item[]) {
return u instanceof Walker ? u.walk(a) : -1;
}

function testAny(a: Item[]) {
seen = 0;
const w = new Walker(once);
let r: number = w.walk(a);
churn();
w.use(twice);
r += w.walk(a) + viaAny(w, a);
return r * 100 + seen;
}

function testUnion(a: Item[]) {
seen = 0;
const w = new Walker(once);
let r: number = w.walk(a);
churn();
w.use(twice);
r += w.walk(a) + viaUnion(w, a);
return r * 100 + seen;
}

function main() {
let ok = 0;
for (let i = 0; i < 300; i++) {
const a = [new Item(1), new Item(5), new Item(2)];
if (testAny(a) == 2415) ok++;
churn();
if (testUnion(a) == 2415) ok++;
churn();
}

assert(ok == 600, "ok");
print("done.");
}
86 changes: 86 additions & 0 deletions tslang/test/tester/own/own_callback_interface_slot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// A method an interface's vtable holds takes its callback parameter's family from every call through
// the interface's slot as well as from its direct calls (spec 59): the interface is closed, so no
// other module calls through it. Two classes fill the slot; a caller's own callback parameter is given
// on through the interface to both. 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 Each {
each(a: Item[], f: (v: Item) => void): number;
}

class Forward implements Each {
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;
}
}

class Backward implements Each {
each(a: Item[], f: (v: Item) => void): number {
let s: number = 0;
for (let i = a.length - 1; i >= 0; i--) {
const v = a[i];
f(v);
s += v.n * 10;
}

return s;
}
}

function viaEach(e: Each, a: Item[]) {
return e.each(a, once);
}

function giveOn(e: Each, a: Item[], f: (v: Item) => void) {
return e.each(a, f);
}

function test(a: Item[]) {
seen = 0;
const fw = new Forward();
const bw = new Backward();
let r: number = fw.each(a, twice) + bw.each(a, once);
churn();
r += viaEach(fw, a) + viaEach(bw, a);
churn();
r += giveOn(fw, a, twice) + giveOn(bw, a, once);
return r * 100 + seen;
}

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) == 26424) ok++;
churn();
}

assert(ok == 300, "ok");
print("done.");
}
32 changes: 32 additions & 0 deletions tslang/test/tester/own/own_err_callback_cast_field_drops.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// -mm=own rejects: the function stored into `Walker`'s field replaces a global array, releasing what
// it held. The class is cast to `any` and back before `walk` calls it (spec 59), with an element of an
// array that may be that global, and reads the element again after the call.
class Item {
constructor(public n: number) {}
}

let pool: Item[] = [];

class Walker {
constructor(private f: (v: Item) => void) {}

walk(a: Item[]) {
for (let i = 0; i < a.length; i++) {
const v = a[i];
this.f(v);
print(v.n);
}
}
}

function viaAny(x: any) {
(<Walker>x).walk([new Item(1)]);
}

function main() {
pool = [new Item(7)];
viaAny(new Walker((v) => {
pool = [];
}));
print(pool.length);
}
42 changes: 42 additions & 0 deletions tslang/test/tester/own/own_err_callback_interface_slot_drops.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// -mm=own rejects: a callback given through an interface's slot (spec 59) is in the family of the
// method's parameter for its direct calls too. `drain` replaces the global array, releasing what it
// held, so the direct call that lends that array may have it released under it.
class Item {
constructor(public n: number) {}
}

let pool: Item[] = [];

interface Each {
each(a: Item[], f: (v: Item) => void): number;
}

class Walker implements Each {
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 once(v: Item) {}

function drain(v: Item) {
pool = [];
}

function viaEach(e: Each) {
return e.each([new Item(1)], drain);
}

function main() {
pool = [new Item(7)];
const w = new Walker();
print(viaEach(w));
print(w.each(pool, once));
}
Loading
Loading