diff --git a/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp b/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp index 65827c780..a2ad4ea4b 100644 --- a/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp +++ b/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp @@ -448,6 +448,81 @@ class OwnedReturnConsumptionPass return it != assigns.end() && it->getValue().contains(global); }; + // What a hold keeps of an argument: the value read before the call. Under rc a view of it is + // made at the call, after every argument is evaluated - after a later one may have given + // the global a new value - so the value read is the one behind it: the object a cast or an + // interface is made over (`f(G, (G = mk(), x))` with an interface parameter), or the values + // an object literal was given (`f({ v: G.v }, ..)`), which the literal holds with no count + // until it is boxed for the view, at the call too. + auto readsOf = [&](mlir::Operation *call, mlir::Value argument, llvm::SmallVectorImpl &reads) { + if (!compileOptions.isRefCounted()) + { + reads.push_back(argument); + return; + } + + auto value = argument; + for (;;) + { + if (auto castOp = value.getDefiningOp()) + { + value = castOp.getIn(); + } + else if (auto newInterfaceOp = value.getDefiningOp()) + { + value = newInterfaceOp.getThisVal(); + } + else + { + break; + } + } + + // an object literal boxed for an interface: stored into a new value_ref + auto newOp = value.getDefiningOp(); + if (newOp && mlir::isa(newOp.getInstance().getType())) + { + for (auto *user : newOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(user); + if (storeOp && storeOp.getReference() == newOp.getInstance()) + { + value = storeOp.getValue(); + break; + } + } + } + + // an object literal: a read of the slot it was built in, in the call's block + auto loadOp = value.getDefiningOp(); + auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + if (varOp && !varOp->hasAttr(OWNED_LOCAL_ATTR_NAME) && varOp->getBlock() == call->getBlock() && + mlir::isa(value.getType())) + { + for (auto *user : varOp->getUsers()) + { + auto fieldRef = mlir::dyn_cast(user); + if (!fieldRef) + { + continue; + } + + for (auto *fieldUser : fieldRef->getUsers()) + { + auto storeOp = mlir::dyn_cast(fieldUser); + if (storeOp && storeOp.getReference() == fieldRef.getResult()) + { + reads.push_back(storeOp.getValue()); + } + } + } + + return; + } + + reads.push_back(value); + }; + llvm::SmallVector> held; module.walk([&](mlir::Operation *op) { if (!own_facts::isCall(op) || op->hasTrait() || @@ -457,6 +532,7 @@ class OwnedReturnConsumptionPass } llvm::SmallPtrSet seen; + llvm::SmallVector reads; for (auto argument : own_facts::callArgs(op)) { // a method's object: `this` of the method bound to it @@ -469,6 +545,11 @@ class OwnedReturnConsumptionPass } } + readsOf(op, argument, reads); + } + + for (auto argument : reads) + { if (!mth.ownsHeapMemory(op->getLoc(), argument.getType()) || !seen.insert(argument).second) { continue; diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 242cace14..9661bd9c4 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -404,6 +404,7 @@ tslang_add_test(NAME test-compile-00-const-field-read COMMAND test-runner "${PRO tslang_add_test(NAME test-compile-00-const-element-read COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_element_read.ts") tslang_add_test(NAME test-compile-00-const-global-read COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_global_read.ts") tslang_add_test(NAME test-compile-00-global-argument-reassigned COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_argument_reassigned.ts") +tslang_add_test(NAME test-compile-00-global-argument-view COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_argument_view.ts") tslang_add_test(NAME test-compile-00-array-data-string-alias COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_data_string_alias.ts") tslang_add_test(NAME test-compile-00-arrays7 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array7.ts") tslang_add_test(NAME test-compile-00-arrays8-tuple-spread COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array8_tuple_spread.ts") @@ -982,6 +983,7 @@ tslang_add_test(NAME test-jit-00-const-field-read COMMAND test-runner -jit "${PR tslang_add_test(NAME test-jit-00-const-element-read COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_element_read.ts") tslang_add_test(NAME test-jit-00-const-global-read COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_global_read.ts") tslang_add_test(NAME test-jit-00-global-argument-reassigned COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_argument_reassigned.ts") +tslang_add_test(NAME test-jit-00-global-argument-view COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_argument_view.ts") tslang_add_test(NAME test-jit-00-array-data-string-alias COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_data_string_alias.ts") tslang_add_test(NAME test-jit-00-arrays7 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array7.ts") tslang_add_test(NAME test-jit-00-arrays8-tuple-spread COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array8_tuple_spread.ts") @@ -1950,6 +1952,7 @@ set(TSLANG_CORPUS 00const_element_read.ts 00const_global_read.ts 00global_argument_reassigned.ts + 00global_argument_view.ts 00array_data_string_alias.ts 00array7.ts 00array8_tuple_spread.ts diff --git a/tslang/test/tester/tests/00global_argument_view.ts b/tslang/test/tester/tests/00global_argument_view.ts new file mode 100644 index 000000000..2e065bd34 --- /dev/null +++ b/tslang/test/tester/tests/00global_argument_view.ts @@ -0,0 +1,109 @@ +// An argument read out of a global and given to a call as a view - an interface, a base class, an +// object literal holding it - stays alive when a later argument gives the global a new value: +// `check(G, (G = mk(2), 0))` with an interface parameter, `check({ c: G.c }, (G = mk(2), 0))`. +// The view is made at the call, after every argument is evaluated, so under rc a hold taken on the +// view (00global_argument_reassigned.ts) came after the later argument had freed the block, and an +// object literal held what it was given with no count until then. Each read follows allocations of +// the same size, so a freed block is reused. +class Base { + tag = ""; + constructor(public x: number) { + this.tag = "c" + x; + } +} + +class C extends Base { + v: number[] = []; +} + +class H { + c: C = mk(3); +} + +interface IC { + x: number; + tag: string; +} + +interface IHolder { + c: C; +} + +function mk(x: number): C { + const r = new C(x); + r.v.push(x); + return r; +} + +function churn() { + const junk: C[] = []; + for (let i = 0; i < 64; i++) { + const c = new C(999); + c.tag = "junk"; + c.v.push(99); + junk.push(c); + } +} + +let G: C = mk(1); +const GH = new H(); + +function thrower(): number { + throw "thrown"; +} + +function viaInterface(c: IC, n: number, x: number) { + churn(); + return c.x == x && c.tag == "c" + x; +} + +function viaBase(c: Base, n: number, x: number) { + churn(); + return c.x == x && c.tag == "c" + x; +} + +function viaHolder(h: IHolder, n: number, x: number) { + churn(); + return h.c.x == x && h.c.tag == "c" + x && h.c.v[0] == x; +} + +function viaRecord(h: { c: C }, n: number, x: number) { + churn(); + return h.c.x == x && h.c.tag == "c" + x && h.c.v[0] == x; +} + +function inLoop() { + for (let i = 0; i < 3; i++) { + const x = G.x; + assert(viaInterface(G, (G = mk(10 + i), 0), x), "an interface over a global, in a loop"); + const y = G.x; + assert(viaHolder({ c: G }, (G = mk(20 + i), 0), y), "an object literal holding a global, in a loop"); + } +} + +function inTryCatch() { + G = mk(30); + let caught = false; + try { + viaInterface(G, (G = mk(31), thrower()), 30); + } catch (e) { + caught = true; + } + + churn(); + assert(caught && G.x == 31 && G.tag == "c31", "a later argument throws after the assignment"); +} + +function main() { + assert(viaInterface(G, (G = mk(2), 0), 1), "an interface over a global"); + assert(viaBase(G, (G = mk(3), 0), 2), "a global as its base class"); + assert(viaInterface(GH.c, (GH.c = mk(4), 0), 3), "an interface over a global's field"); + assert(viaHolder({ c: G }, (G = mk(5), 0), 3), "an object literal holding a global"); + assert(viaHolder({ c: GH.c }, (GH.c = mk(6), 0), 4), "an object literal holding a global's field"); + assert(viaRecord({ c: G }, (G = mk(7), 0), 5), "an object literal given as a record"); + inLoop(); + inTryCatch(); + churn(); + assert(G.x == 31 && GH.c.x == 6, "the globals' last values"); + print("done."); +}