From c8f5bd321e04e46bcb8f148367b9a4acaa69bea6 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 23 Oct 2026 09:25:41 +0100 Subject: [PATCH] rc: an interface-typed global initialized with a class object holds it `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 --- .../TypeScript/OwnedReturnConsumptionPass.cpp | 7 +- tslang/test/tester/CMakeLists.txt | 9 ++ .../tester/own/own_global_interface_fresh.ts | 50 ++++++++++ .../tests/00global_interface_initializer.ts | 97 +++++++++++++++++++ 4 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 tslang/test/tester/own/own_global_interface_fresh.ts create mode 100644 tslang/test/tester/tests/00global_interface_initializer.ts diff --git a/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp b/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp index a2ad4ea4b..d775c882c 100644 --- a/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp +++ b/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp @@ -694,7 +694,8 @@ class OwnedReturnConsumptionPass // The initializer region ends in `ts.GlobalResult`, a terminator, so a value given to it // straight is never released as a discarded temporary (ยง9.30): `let g = mk()` keeps the call's // +1. A value given to it through a view of the global's type is: `ts.Cast` to `C | null` or - // `Shared | null`, `ts.OptionalValue` to `C | undefined`, `ts.CreateUnionInstance` - the view + // `Shared | null`, `ts.OptionalValue` to `C | undefined`, `ts.CreateUnionInstance`, + // `ts.NewInterface` over a class object for an interface-typed global (`let g: I = mk()`) - the view // is the one use the producer has, and it is releasable, so the producer's reference was given // back at the end of the region and the global was born at count 0, pointing at freed memory // (#459; own spec 23.1 for handles). @@ -739,6 +740,10 @@ class OwnedReturnConsumptionPass { value = createUnionInstanceOp.getIn(); } + else if (auto newInterfaceOp = mlir::dyn_cast(definingOp)) + { + value = newInterfaceOp.getThisVal(); + } else { break; diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 9661bd9c4..f556c1f45 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -399,6 +399,7 @@ tslang_add_test(NAME test-compile-00-array-length-valid COMMAND test-runner "${P 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") tslang_add_test(NAME test-compile-00-string-view-copy COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_view_copy.ts") tslang_add_test(NAME test-compile-00-global-copy-counted COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_copy_counted.ts") +tslang_add_test(NAME test-compile-00-global-interface-initializer COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_interface_initializer.ts") tslang_add_test(NAME test-compile-00-c-string-copy COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00c_string_copy.ts") tslang_add_test(NAME test-compile-00-const-field-read COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_field_read.ts") tslang_add_test(NAME test-compile-00-const-element-read COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_element_read.ts") @@ -978,6 +979,7 @@ tslang_add_test(NAME test-jit-00-array-length-valid COMMAND test-runner -jit "${ 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") tslang_add_test(NAME test-jit-00-string-view-copy COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_view_copy.ts") tslang_add_test(NAME test-jit-00-global-copy-counted COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_copy_counted.ts") +tslang_add_test(NAME test-jit-00-global-interface-initializer COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_interface_initializer.ts") tslang_add_test(NAME test-jit-00-c-string-copy COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00c_string_copy.ts") tslang_add_test(NAME test-jit-00-const-field-read COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_field_read.ts") tslang_add_test(NAME test-jit-00-const-element-read COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_element_read.ts") @@ -1947,6 +1949,7 @@ set(TSLANG_CORPUS 00const_interface_of_local.ts 00string_view_copy.ts 00global_copy_counted.ts + 00global_interface_initializer.ts 00c_string_copy.ts 00const_field_read.ts 00const_element_read.ts @@ -3546,6 +3549,12 @@ foreach(kept_mm own rc none gc) 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") endforeach() +# An interface-typed global given a fresh class object owns it; the same program under every model. +foreach(iface_mm own rc none gc) + 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") + 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") +endforeach() + # A `let` given a new value after its value moved owns the new one (#528); the same program under # rc and gc (not none: nothing is freed there, and the loops would hold about 800 MB). foreach(reassign_mm rc gc) diff --git a/tslang/test/tester/own/own_global_interface_fresh.ts b/tslang/test/tester/own/own_global_interface_fresh.ts new file mode 100644 index 000000000..1b3c69bc2 --- /dev/null +++ b/tslang/test/tester/own/own_global_interface_fresh.ts @@ -0,0 +1,50 @@ +// An interface-typed global given a fresh class object (`let a: I = mk()`, `let a: I = new V()`) +// owns it. The object reaches the global through a view, `ts.NewInterface`, which hid it from the +// global's initializer: the call's or the `new`'s reference was given back as a discarded +// temporary at the end of the initializer, and the global was born pointing at freed memory +// (own and rc). Each read follows allocations of the same size, so a freed block is reused. +interface I { + x: number; + tag: string; +} + +class V implements I { + tag = ""; + constructor(public x: number) { + this.tag = "v" + x; + } +} + +class Junk { + a = -1; + b = -2; + tag = "junk"; +} + +function churn() { + let keep: Junk[] = []; + for (let i = 0; i < 1000; i++) keep.push(new Junk()); + return keep.length; +} + +function mk(n: number) { + return new V(n); +} + +let fromCall: I = mk(7); +let fromNew: I = new V(8); +const constFromCall: I = mk(9); + +function main() { + churn(); + assert(fromCall.x == 7 && fromCall.tag == "v7", "a call's result"); + assert(fromNew.x == 8 && fromNew.tag == "v8", "a new object"); + assert(constFromCall.x == 9 && constFromCall.tag == "v9", "a const made by a call"); + + fromCall = new V(17); + fromNew = mk(18); + churn(); + assert(fromCall.x == 17 && fromCall.tag == "v17", "the call's global reassigned"); + assert(fromNew.x == 18 && fromNew.tag == "v18", "the new object's global reassigned"); + print("done."); +} diff --git a/tslang/test/tester/tests/00global_interface_initializer.ts b/tslang/test/tester/tests/00global_interface_initializer.ts new file mode 100644 index 000000000..228748e5d --- /dev/null +++ b/tslang/test/tester/tests/00global_interface_initializer.ts @@ -0,0 +1,97 @@ +// An interface-typed global initialized with a class object (`let a: I = K.w`, `let a: I = mk()`) +// holds the object through a view, `ts.NewInterface`. Under rc that view hid the object from the +// global's initializer: a read of another global, a field or an element took no count, so giving +// either side a new value freed the block the other still read; and a call's or a `new`'s +// reference was given back as a discarded temporary at the end of the initializer, so the global +// was born pointing at freed memory. Each read follows allocations of the same size, so a freed +// block is reused. +interface I { + x: number; + tag: string; +} + +class V implements I { + tag = ""; + constructor(public x: number) { + this.tag = "v" + x; + } +} + +class H { + v = new V(3); +} + +class K { + static w = new V(1); +} + +class Junk { + a = -1; + b = -2; + tag = "junk"; +} + +function churn() { + let keep: Junk[] = []; + for (let i = 0; i < 1000; i++) keep.push(new Junk()); + return keep.length; +} + +function mk(n: number) { + return new V(n); +} + +let G = new V(2); +const h = new H(); +const arr = [new V(4)]; + +// the interface global is given a new value: the source keeps its block +let aStatic: I = K.w; +let aGlobal: I = G; +let aField: I = h.v; +let aElement: I = arr[0]; + +// the source is given a new value: the interface global keeps its block +let G2 = new V(5); +const h2 = new H(); +const arr2 = [new V(6)]; +let bGlobal: I = G2; +const bConst: I = G2; +let bField: I = h2.v; +let bElement: I = arr2[0]; + +// a fresh object: the global takes its reference +let cCall: I = mk(7); +let cNew: I = new V(8); + +function main() { + churn(); + assert(cCall.x == 7 && cCall.tag == "v7", "a call's result"); + assert(cNew.x == 8 && cNew.tag == "v8", "a new object"); + + aStatic = new V(11); + aGlobal = new V(12); + aField = new V(13); + aElement = new V(14); + churn(); + assert(K.w.x == 1 && K.w.tag == "v1", "a static field read"); + assert(G.x == 2 && G.tag == "v2", "a global read"); + assert(h.v.x == 3 && h.v.tag == "v3", "a field read"); + assert(arr[0].x == 4 && arr[0].tag == "v4", "an element read"); + + G2 = new V(15); + h2.v = new V(16); + arr2[0] = new V(17); + churn(); + assert(bGlobal.x == 5 && bGlobal.tag == "v5", "a global read, the global reassigned"); + assert(bConst.x == 5 && bConst.tag == "v5", "a const of a global, the global reassigned"); + assert(bField.x == 3 && bField.tag == "v3", "a field read, the field reassigned"); + assert(bElement.x == 6 && bElement.tag == "v6", "an element read, the element reassigned"); + + cCall = new V(18); + cNew = new V(19); + churn(); + assert(cCall.x == 18 && cNew.x == 19, "the fresh objects' globals reassigned"); + assert(aStatic.x == 11 && aGlobal.x == 12 && aField.x == 13 && aElement.x == 14, "the new values"); + print("done."); +}