Skip to content

Commit 6c1f1d1

Browse files
rc: an interface-typed global initialized with a class object holds it (#656)
`let a: I = K.w` (a static field, another global, a field or an element) gave the global no count of its own: claimGlobalInitializers looked through casts and unions to the value a global is given, but not through `ts.NewInterface`, the view a class object takes for an interface-typed global. Giving either side a new value freed the block the other still read (rc printed 999 for 1). A call's or a `new`'s reference (`let a: I = mk()`, `let a: I = new V(8)`) was given back as a discarded temporary at the end of the initializer, so the global was born pointing at freed memory; under own too (`let a: I = mk()` read a freed block). The initializer now looks through `ts.NewInterface` to the object. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 1f53a05 commit 6c1f1d1

4 files changed

Lines changed: 162 additions & 1 deletion

File tree

‎tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -875,7 +875,8 @@ class OwnedReturnConsumptionPass
875875
// The initializer region ends in `ts.GlobalResult`, a terminator, so a value given to it
876876
// straight is never released as a discarded temporary (§9.30): `let g = mk()` keeps the call's
877877
// +1. A value given to it through a view of the global's type is: `ts.Cast` to `C | null` or
878-
// `Shared<T> | null`, `ts.OptionalValue` to `C | undefined`, `ts.CreateUnionInstance` - the view
878+
// `Shared<T> | null`, `ts.OptionalValue` to `C | undefined`, `ts.CreateUnionInstance`,
879+
// `ts.NewInterface` over a class object for an interface-typed global (`let g: I = mk()`) - the view
879880
// is the one use the producer has, and it is releasable, so the producer's reference was given
880881
// back at the end of the region and the global was born at count 0, pointing at freed memory
881882
// (#459; own spec 23.1 for handles).
@@ -920,6 +921,10 @@ class OwnedReturnConsumptionPass
920921
{
921922
value = createUnionInstanceOp.getIn();
922923
}
924+
else if (auto newInterfaceOp = mlir::dyn_cast<mlir_ts::NewInterfaceOp>(definingOp))
925+
{
926+
value = newInterfaceOp.getThisVal();
927+
}
923928
else
924929
{
925930
break;

‎tslang/test/tester/CMakeLists.txt‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,7 @@ tslang_add_test(NAME test-compile-00-array-length-valid COMMAND test-runner "${P
399399
tslang_add_test(NAME test-compile-00-const-interface-of-local COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_interface_of_local.ts")
400400
tslang_add_test(NAME test-compile-00-string-view-copy COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_view_copy.ts")
401401
tslang_add_test(NAME test-compile-00-global-copy-counted COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_copy_counted.ts")
402+
tslang_add_test(NAME test-compile-00-global-interface-initializer COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_interface_initializer.ts")
402403
tslang_add_test(NAME test-compile-00-c-string-copy COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00c_string_copy.ts")
403404
tslang_add_test(NAME test-compile-00-const-field-read COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_field_read.ts")
404405
tslang_add_test(NAME test-compile-00-const-element-read COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_element_read.ts")
@@ -983,6 +984,7 @@ tslang_add_test(NAME test-jit-00-array-length-valid COMMAND test-runner -jit "${
983984
tslang_add_test(NAME test-jit-00-const-interface-of-local COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_interface_of_local.ts")
984985
tslang_add_test(NAME test-jit-00-string-view-copy COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_view_copy.ts")
985986
tslang_add_test(NAME test-jit-00-global-copy-counted COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_copy_counted.ts")
987+
tslang_add_test(NAME test-jit-00-global-interface-initializer COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_interface_initializer.ts")
986988
tslang_add_test(NAME test-jit-00-c-string-copy COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00c_string_copy.ts")
987989
tslang_add_test(NAME test-jit-00-const-field-read COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_field_read.ts")
988990
tslang_add_test(NAME test-jit-00-const-element-read COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_element_read.ts")
@@ -1957,6 +1959,7 @@ set(TSLANG_CORPUS
19571959
00const_interface_of_local.ts
19581960
00string_view_copy.ts
19591961
00global_copy_counted.ts
1962+
00global_interface_initializer.ts
19601963
00c_string_copy.ts
19611964
00const_field_read.ts
19621965
00const_element_read.ts
@@ -3691,6 +3694,12 @@ foreach(kept_mm own rc none gc)
36913694
tslang_add_test(NAME test-compile-${kept_mm}-own-global-kept-argument COMMAND test-runner -mm=${kept_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_global_kept_argument.ts")
36923695
endforeach()
36933696

3697+
# An interface-typed global given a fresh class object owns it; the same program under every model.
3698+
foreach(iface_mm own rc none gc)
3699+
tslang_add_test(NAME test-jit-${iface_mm}-own-global-interface-fresh COMMAND test-runner -jit -mm=${iface_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_global_interface_fresh.ts")
3700+
tslang_add_test(NAME test-compile-${iface_mm}-own-global-interface-fresh COMMAND test-runner -mm=${iface_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_global_interface_fresh.ts")
3701+
endforeach()
3702+
36943703
# A `let` given a new value after its value moved owns the new one (#528); the same program under
36953704
# rc and gc (not none: nothing is freed there, and the loops would hold about 800 MB).
36963705
foreach(reassign_mm rc gc)
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// An interface-typed global given a fresh class object (`let a: I = mk()`, `let a: I = new V()`)
2+
// owns it. The object reaches the global through a view, `ts.NewInterface`, which hid it from the
3+
// global's initializer: the call's or the `new`'s reference was given back as a discarded
4+
// temporary at the end of the initializer, and the global was born pointing at freed memory
5+
// (own and rc). Each read follows allocations of the same size, so a freed block is reused.
6+
interface I {
7+
x: number;
8+
tag: string;
9+
}
10+
11+
class V implements I {
12+
tag = "";
13+
constructor(public x: number) {
14+
this.tag = "v" + x;
15+
}
16+
}
17+
18+
class Junk {
19+
a = -1;
20+
b = -2;
21+
tag = "junk";
22+
}
23+
24+
function churn() {
25+
let keep: Junk[] = [];
26+
for (let i = 0; i < 1000; i++) keep.push(new Junk());
27+
return keep.length;
28+
}
29+
30+
function mk(n: number) {
31+
return new V(n);
32+
}
33+
34+
let fromCall: I = mk(7);
35+
let fromNew: I = new V(8);
36+
const constFromCall: I = mk(9);
37+
38+
function main() {
39+
churn();
40+
assert(fromCall.x == 7 && fromCall.tag == "v7", "a call's result");
41+
assert(fromNew.x == 8 && fromNew.tag == "v8", "a new object");
42+
assert(constFromCall.x == 9 && constFromCall.tag == "v9", "a const made by a call");
43+
44+
fromCall = new V(17);
45+
fromNew = mk(18);
46+
churn();
47+
assert(fromCall.x == 17 && fromCall.tag == "v17", "the call's global reassigned");
48+
assert(fromNew.x == 18 && fromNew.tag == "v18", "the new object's global reassigned");
49+
print("done.");
50+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// An interface-typed global initialized with a class object (`let a: I = K.w`, `let a: I = mk()`)
2+
// holds the object through a view, `ts.NewInterface`. Under rc that view hid the object from the
3+
// global's initializer: a read of another global, a field or an element took no count, so giving
4+
// either side a new value freed the block the other still read; and a call's or a `new`'s
5+
// reference was given back as a discarded temporary at the end of the initializer, so the global
6+
// was born pointing at freed memory. Each read follows allocations of the same size, so a freed
7+
// block is reused.
8+
interface I {
9+
x: number;
10+
tag: string;
11+
}
12+
13+
class V implements I {
14+
tag = "";
15+
constructor(public x: number) {
16+
this.tag = "v" + x;
17+
}
18+
}
19+
20+
class H {
21+
v = new V(3);
22+
}
23+
24+
class K {
25+
static w = new V(1);
26+
}
27+
28+
class Junk {
29+
a = -1;
30+
b = -2;
31+
tag = "junk";
32+
}
33+
34+
function churn() {
35+
let keep: Junk[] = [];
36+
for (let i = 0; i < 1000; i++) keep.push(new Junk());
37+
return keep.length;
38+
}
39+
40+
function mk(n: number) {
41+
return new V(n);
42+
}
43+
44+
let G = new V(2);
45+
const h = new H();
46+
const arr = [new V(4)];
47+
48+
// the interface global is given a new value: the source keeps its block
49+
let aStatic: I = K.w;
50+
let aGlobal: I = G;
51+
let aField: I = h.v;
52+
let aElement: I = arr[0];
53+
54+
// the source is given a new value: the interface global keeps its block
55+
let G2 = new V(5);
56+
const h2 = new H();
57+
const arr2 = [new V(6)];
58+
let bGlobal: I = G2;
59+
const bConst: I = G2;
60+
let bField: I = h2.v;
61+
let bElement: I = arr2[0];
62+
63+
// a fresh object: the global takes its reference
64+
let cCall: I = mk(7);
65+
let cNew: I = new V(8);
66+
67+
function main() {
68+
churn();
69+
assert(cCall.x == 7 && cCall.tag == "v7", "a call's result");
70+
assert(cNew.x == 8 && cNew.tag == "v8", "a new object");
71+
72+
aStatic = new V(11);
73+
aGlobal = new V(12);
74+
aField = new V(13);
75+
aElement = new V(14);
76+
churn();
77+
assert(K.w.x == 1 && K.w.tag == "v1", "a static field read");
78+
assert(G.x == 2 && G.tag == "v2", "a global read");
79+
assert(h.v.x == 3 && h.v.tag == "v3", "a field read");
80+
assert(arr[0].x == 4 && arr[0].tag == "v4", "an element read");
81+
82+
G2 = new V(15);
83+
h2.v = new V(16);
84+
arr2[0] = new V(17);
85+
churn();
86+
assert(bGlobal.x == 5 && bGlobal.tag == "v5", "a global read, the global reassigned");
87+
assert(bConst.x == 5 && bConst.tag == "v5", "a const of a global, the global reassigned");
88+
assert(bField.x == 3 && bField.tag == "v3", "a field read, the field reassigned");
89+
assert(bElement.x == 6 && bElement.tag == "v6", "an element read, the element reassigned");
90+
91+
cCall = new V(18);
92+
cNew = new V(19);
93+
churn();
94+
assert(cCall.x == 18 && cNew.x == 19, "the fresh objects' globals reassigned");
95+
assert(aStatic.x == 11 && aGlobal.x == 12 && aField.x == 13 && aElement.x == 14, "the new values");
96+
print("done.");
97+
}

0 commit comments

Comments
 (0)