diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index b64143faf..843071c2a 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -1701,22 +1701,28 @@ class MLIRGenImpl return varOp && varOp->hasAttr(OWNED_LOCAL_ATTR_NAME); } - // An interface or a bound method made over a value read out of such a local: `v` seen - // through `I` in `const i: I = v`, or `this` of `const m = v.get`. - static bool viewsLocalSlot(mlir::Value value) + // The value an interface or a bound method is made over: `v` seen through `I` in + // `const i: I = v`, or `this` of `const m = v.get`. + static mlir::Value viewedValue(mlir::Value value) { auto *definingOp = value ? value.getDefiningOp() : nullptr; if (auto newInterfaceOp = dyn_cast_or_null(definingOp)) { - return readsLocalSlot(newInterfaceOp.getThisVal()); + return newInterfaceOp.getThisVal(); } if (isa_and_nonnull(definingOp)) { - return readsLocalSlot(definingOp->getOperand(0)); + return definingOp->getOperand(0); } - return false; + return mlir::Value(); + } + + // An interface or a bound method made over a value read out of such a local. + static bool viewsLocalSlot(mlir::Value value) + { + return readsLocalSlot(viewedValue(value)); } // A value read out of a field, through any casts: what `h.c` is in `const c = h.c`. @@ -1881,8 +1887,10 @@ class MLIRGenImpl // it holds the block as `b` would. // // Under rc, so does a const read out of a field (`const c = h.c`) or an element (`const c = - // a[0]`, `const [c] = a`): folded, `h.c = d` or `a[0] = d` freed what `c` still read. Under - // own the folded read is a borrow, and a use after the overwrite is an error there. + // a[0]`, `const [c] = a`): folded, `h.c = d` or `a[0] = d` freed what `c` still read. So + // does an interface or a bound method made over a field read (`const i: I = h.v`, + // `const m = h.v.get`), which holds the block as `c` would. Under own the folded read is a + // borrow, and a use after the overwrite is an error there. // // And a const record whose fields own blocks (`const o = { s: p.child }`): the literal // hands its fields over unretained - its receiver is the one that retains them, as a @@ -1901,7 +1909,8 @@ class MLIRGenImpl || MLIRTypeHelper::isConstTupleHoldingArray(type) || ((isa(type) || MLIRTypeHelper::isSharedHandleType(type) || ((readsLocalSlot(initial) || viewsLocalSlot(initial)) && mth.ownsHeapMemory(location, type)) - || (compileOptions.isRefCounted() && (readsField(initial) || readsElement(initial)) + || (compileOptions.isRefCounted() + && (readsField(initial) || readsElement(initial) || readsField(viewedValue(initial))) && mth.ownsHeapMemory(location, type)) || (readsLiteralSlot(mth, location, initial) && isa(type) && mth.ownsHeapMemory(location, type)) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index d72970f33..f01294fbd 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -390,6 +390,7 @@ tslang_add_test(NAME test-compile-00-parse-number-range COMMAND test-runner "${P tslang_add_test(NAME test-compile-00-global-argument-held COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_argument_held.ts") tslang_add_test(NAME test-compile-00-narrowed-assign-other-member COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00narrowed_assign_other_member.ts") tslang_add_test(NAME test-compile-00-bound-method-count COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00bound_method_count.ts") +tslang_add_test(NAME test-compile-00-const-view-of-field COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_view_of_field.ts") tslang_add_test(NAME test-compile-00-const-record-owned-fields COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_record_owned_fields.ts") tslang_add_test(NAME test-compile-00-generator-temporaries COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00generator_temporaries.ts") tslang_add_test(NAME test-compile-00-array-length-valid COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_length_valid.ts") @@ -963,6 +964,7 @@ tslang_add_test(NAME test-jit-00-parse-number-range COMMAND test-runner -jit "${ tslang_add_test(NAME test-jit-00-global-argument-held COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_argument_held.ts") tslang_add_test(NAME test-jit-00-narrowed-assign-other-member COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00narrowed_assign_other_member.ts") tslang_add_test(NAME test-jit-00-bound-method-count COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00bound_method_count.ts") +tslang_add_test(NAME test-jit-00-const-view-of-field COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_view_of_field.ts") tslang_add_test(NAME test-jit-00-const-record-owned-fields COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_record_owned_fields.ts") tslang_add_test(NAME test-jit-00-generator-temporaries COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00generator_temporaries.ts") tslang_add_test(NAME test-jit-00-array-length-valid COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_length_valid.ts") @@ -1926,6 +1928,7 @@ set(TSLANG_CORPUS 00global_argument_held.ts 00narrowed_assign_other_member.ts 00bound_method_count.ts + 00const_view_of_field.ts 00const_record_owned_fields.ts 00generator_temporaries.ts 00array_length_valid.ts diff --git a/tslang/test/tester/tests/00const_view_of_field.ts b/tslang/test/tester/tests/00const_view_of_field.ts new file mode 100644 index 000000000..67f6ea1a6 --- /dev/null +++ b/tslang/test/tester/tests/00const_view_of_field.ts @@ -0,0 +1,63 @@ +// A `const` interface or bound method made over a field read keeps the object alive when the field +// is given a new value, as a `let` does. Folded, the const was the view itself and held no +// reference of its own: under rc `h.v = mkv(..)` freed the object `m()` and `i.get()` then ran on. +// Each call follows allocations of the same size, so a freed object is reused. +interface I { + get(): number; +} + +class W { + constructor(public x: number) {} +} + +class V { + items: number[] = [1, 2, 3]; + constructor(public w: W) {} + + get() { + return this.w.x; + } +} + +class H { + v: V = mkv(0); +} + +function mkv(n: number) { + return new V(new W(n)); +} + +function churn(): number { + let s: number = 0; + const a: V[] = []; + for (let i = 0; i < 64; i++) a.push(mkv(1000 + i)); + for (const e of a) s += e.w.x; + return s; +} + +function boundMethod(n: number) { + const h = new H(); + h.v = mkv(n); + const m = h.v.get; + h.v = mkv(n + 1); + churn(); + return m() == n && h.v.w.x == n + 1; +} + +function interfaceView(n: number) { + const h = new H(); + h.v = mkv(n); + const i: I = h.v; + h.v = mkv(n + 1); + churn(); + return i.get() == n && h.v.w.x == n + 1; +} + +function main() { + for (let n = 0; n < 20; n++) { + assert(boundMethod(n), "a const bound method over a field"); + assert(interfaceView(n), "a const interface over a field"); + } + + print("done."); +}