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
77 changes: 77 additions & 0 deletions tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ class OwnedReturnConsumptionPass

retainYieldedRecords(mth, module);

holdGlobalArguments(mth, module);

own_facts::FieldFunctions fieldFunctionsOfModule;
const own_facts::FieldFunctions *fieldFunctions = nullptr;
if (compileOptions.isRefCounted())
Expand Down Expand Up @@ -291,6 +293,81 @@ class OwnedReturnConsumptionPass
});
}

// A call's argument read out of a global (`run(G)`, `run(G.c)`, `G.m()`) is borrowed like any
// other argument, and the callee may give the global a new value while it still uses the
// parameter (`function run(h: C) { G = mk(); print(h.x); }`): the global's release freed the
// block the parameter read. Under rc the caller holds a count across the call - retained right
// before it, released right after it. A call in a try body (`ts.Invoke`) ends its block, and is
// left as it was; so is a Shared<T> handle, whose count a program can read (`Shared.count`).
void holdGlobalArguments(MLIRTypeHelper &mth, mlir::ModuleOp module)
{
if (!compileOptions.isRefCounted())
{
return;
}

// a read out of a global, or out of a field or an element of one, through casts
auto readsGlobal = [](mlir::Value value) {
for (auto depth = 0; depth < 16 && value; depth++)
{
while (auto castOp = value.getDefiningOp<mlir_ts::CastOp>())
{
value = castOp.getIn();
}

auto loadOp = value.getDefiningOp<mlir_ts::LoadOp>();
auto *place = loadOp ? loadOp.getReference().getDefiningOp() : nullptr;
if (mlir::isa_and_nonnull<mlir_ts::AddressOfOp>(place))
{
return true;
}

value = mlir::isa_and_nonnull<mlir_ts::PropertyRefOp, mlir_ts::ElementRefOp>(place) ? place->getOperand(0)
: mlir::Value();
}

return false;
};

llvm::SmallVector<std::pair<mlir::Operation *, mlir::Value>> held;
module.walk([&](mlir::Operation *op) {
if (!own_facts::isCall(op) || op->hasTrait<mlir::OpTrait::IsTerminator>() ||
!op->getParentOfType<mlir::FunctionOpInterface>())
{
return;
}

llvm::SmallPtrSet<mlir::Value, 4> seen;
for (auto argument : own_facts::callArgs(op))
{
// a method's object: `this` of the method bound to it
if (auto getThisOp = argument.getDefiningOp<mlir_ts::GetThisOp>())
{
auto *boundOp = getThisOp->getOperand(0).getDefiningOp();
if (mlir::isa_and_nonnull<mlir_ts::ThisSymbolRefOp, mlir_ts::ThisVirtualSymbolRefOp>(boundOp))
{
argument = boundOp->getOperand(0);
}
}

if (mth.ownsHeapMemory(op->getLoc(), argument.getType()) &&
!MLIRTypeHelper::isSharedHandleType(argument.getType()) && readsGlobal(argument) &&
seen.insert(argument).second)
{
held.push_back({op, argument});
}
}
});

for (auto [op, argument] : held)
{
mlir::OpBuilder builder(op);
builder.create<mlir_ts::RetainOp>(op->getLoc(), argument);
builder.setInsertionPointAfter(op);
builder.create<mlir_ts::ReleaseOp>(op->getLoc(), argument);
}
}

// A global is born holding the reference its initializer carries.
//
// The initializer region ends in `ts.GlobalResult`, a terminator, so a value given to it
Expand Down
48 changes: 48 additions & 0 deletions tslang/lib/TypeScript/OwnershipSignaturePass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ class OwnershipSignaturePass : public mlir::PassWrapper<OwnershipSignaturePass,
findOpen(module);
computeFacts();
computeDrops();
rejectGlobalArgumentsDropped();
findBorrowingStates(module);
findBoundedForwarders();
findYieldedFields(module);
Expand Down Expand Up @@ -2395,6 +2396,53 @@ class OwnershipSignaturePass : public mlir::PassWrapper<OwnershipSignaturePass,
llvm::any_of(callees.funcs, [&](mlir_ts::FuncOp callee) { return !noDrops.contains(callee); });
}

// An argument read out of a global (`run(G)`, `G.m()`) is lent to the callee, and the callee may
// free it under its own parameter: `function run(h: C) { G = mk(); print(h.x); }`. The caller's
// borrow ends at the call that takes it, so nothing in the caller sees that, and in the callee
// the parameter is not known to be the global's. A call that may drop is an error for such an
// argument, when something assigns that global. (A field of a global given that way is a
// borrow of a place, which the inference already bounds by the call.)
void rejectGlobalArgumentsDropped()
{
for (auto &call : calls)
{
if (!callMayDrop(call.callees))
{
continue;
}

for (auto argument : callArgs(call.op))
{
// a method's object: `this` of the method bound to it
if (auto getThisOp = argument.getDefiningOp<mlir_ts::GetThisOp>())
{
auto *boundOp = getThisOp->getOperand(0).getDefiningOp();
if (mlir::isa_and_nonnull<mlir_ts::ThisSymbolRefOp, mlir_ts::ThisVirtualSymbolRefOp>(boundOp))
{
argument = boundOp->getOperand(0);
}
}

if (isHandle(argument) || !ownsHeap(call.op->getLoc(), argument.getType()))
{
continue;
}

auto loadOp = rootOf(argument).getDefiningOp<mlir_ts::LoadOp>();
auto global = loadOp ? loadOp.getReference().getDefiningOp<mlir_ts::AddressOfOp>() : mlir_ts::AddressOfOp();
if (!global || !writtenGlobals.contains(global.getGlobalName()))
{
continue;
}

call.op->emitError("a value read out of the global '")
<< global.getGlobalName()
<< "' is lent to a call that may release or overwrite it; -mm=own cannot prove it outlives the call";
signalPassFailure();
}
}
}

// ---- Calls in a global's initializer ----

// A global's region is no function: its calls get no facts pinned, and the inference never sees
Expand Down
5 changes: 5 additions & 0 deletions tslang/test/tester/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ tslang_add_test(NAME test-compile-00-param-assigned-owned COMMAND test-runner "$
tslang_add_test(NAME test-compile-00-string-empty-falsy COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_empty_falsy.ts")
tslang_add_test(NAME test-compile-00-string-concat-bounds COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_concat_bounds.ts")
tslang_add_test(NAME test-compile-00-parse-number-range COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00parse_number_range.ts")
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-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")
Expand Down Expand Up @@ -956,6 +957,7 @@ tslang_add_test(NAME test-jit-00-param-assigned-owned COMMAND test-runner -jit "
tslang_add_test(NAME test-jit-00-string-empty-falsy COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_empty_falsy.ts")
tslang_add_test(NAME test-jit-00-string-concat-bounds COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_concat_bounds.ts")
tslang_add_test(NAME test-jit-00-parse-number-range COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00parse_number_range.ts")
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-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")
Expand Down Expand Up @@ -1915,6 +1917,7 @@ set(TSLANG_CORPUS
00string_empty_falsy.ts
00string_concat_bounds.ts
00parse_number_range.ts
00global_argument_held.ts
00narrowed_assign_other_member.ts
00const_record_owned_fields.ts
00generator_temporaries.ts
Expand Down Expand Up @@ -3717,6 +3720,8 @@ set(own_error_cases
"own_err_global_borrowed_temporary|the initializer of global 'c' holds the result of 'id', which borrows its argument"
"own_err_global_kept_not_fresh|the initializer of global 'n' gives 'N.constructor' a value it keeps, but -mm=own cannot move it there"
"own_err_global_returned|borrows the global 'V.def' and cannot be stored, returned or captured"
"own_err_global_argument_assigned|a value read out of the global 'G' is lent to a call that may release or overwrite it"
"own_err_global_method_assigns|a value read out of the global 'G' is lent to a call that may release or overwrite it"
"own_err_global_let_call|borrows the global 'names', but -mm=own cannot prove the global keeps its value"
"own_err_global_from_global|the initializer of global 'k' reads a value another owner keeps"
"own_err_global_from_field|the initializer of global 'k' reads a value another owner keeps"
Expand Down
20 changes: 20 additions & 0 deletions tslang/test/tester/own/own_err_global_argument_assigned.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// -mm=own rejects: `run` borrows `G`'s object as `h`, then gives `G` a new value, which frees that
// object while `h` still reads it.
class C {
x = 5;
}

function mk(): C {
return new C();
}

let G: C = mk();

function run(h: C) {
G = mk();
print(h.x);
}

function main() {
run(G);
}
16 changes: 16 additions & 0 deletions tslang/test/tester/own/own_err_global_method_assigns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// -mm=own rejects: `G.m()` runs with `this` borrowed from `G`, and `m` gives `G` a new value, which
// frees the object `this` still is.
class C {
x = 5;

m() {
G = new C();
return this.x;
}
}

let G = new C();

function main() {
print(G.m());
}
61 changes: 61 additions & 0 deletions tslang/test/tester/tests/00global_argument_held.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// An argument read out of a global stays alive while the callee gives the global a new value. The
// argument is borrowed, so under rc the global's release freed the block the parameter still read.
// Each read follows allocations of the same size, so a freed block is reused.
// @strict-null false
class C {
x = 5;
tag = "";
v: number[] = [];

m() {
G = mk(7);
churn();
return this.x;
}
}

class H {
c: C = mk(3);
}

function mk(x: number): C {
const r = new C();
r.x = x;
r.tag = "c" + x;
r.v.push(x);
return r;
}

function churn() {
const junk: C[] = [];
for (let i = 0; i < 64; i++) {
const c = new C();
c.x = 999;
c.tag = "junk";
c.v.push(99);
junk.push(c);
}
}

let G: C = mk(1);
const GH = new H();

function run(h: C) {
G = mk(2);
churn();
return h.x == 1 && h.tag == "c1" && h.v[0] == 1;
}

function runField(c: C) {
GH.c = mk(4);
churn();
return c.x == 3 && c.tag == "c3";
}

function main() {
assert(run(G), "a global given to a function that assigns it");
assert(runField(GH.c), "a global's field given to a function that assigns it");
G = mk(6);
assert(G.m() == 6, "a method of a global that assigns it");
print("done.");
}
Loading