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
Original file line number Diff line number Diff line change
Expand Up @@ -873,12 +873,15 @@ AOT, `measure.ps1`, at `-O3` and `-O1` (identical within 1%), in MB:
| program | gc | rc | none | own |
| --- | --- | --- | --- | --- |
| `own_param_kept` | 6.5 | 4.8 | 1657.7 | 4.8 |
| `own_getter_borrow` | 6.5 | 12.6 | 1653.3 | 4.8 |
| `own_getter_borrow` | 6.5 | 4.8 (was 12.6) | 1653.3 | 4.8 |
| `own_any_box` | 6.5 | 4.8 | 558.9 | 4.8 |
| `own_call_no_drops` | 6.5 | 4.8 | 1100.7 | 4.8 |

rc climbs in `own_getter_borrow`: it never releases a getter's result used as a temporary
(`h.cc.x`: the `ts.CallInternal` result has no `ts.Release`). Own has no reference to give back.
rc climbed in `own_getter_borrow`: it never released a getter's result used as a temporary
(`h.cc.x`). The getter retains its result like any function, but `OwnedReturnConsumptionPass`
only settled `ts.CallIndirect`, and a getter read is an accessor op until the affine lowering.
The pass now classifies accessor reads the way it classifies calls (`rc_getter_temporary.ts`).
Own has no reference to give back.
§14.5's `const a: any = new C(i)` loop reads 4.6 MB under all four models, at both levels. `none`
does not grow, so LLVM removes the allocation and that program shows nothing about reclamation.

Expand Down Expand Up @@ -939,11 +942,6 @@ of 3226.
exported, so the virtual call to `area` has candidates this module cannot see, and it may drop
(`own_err_exported_virtual_call` is the same shape). The error does not yet say why: the note
on lost facts is attached to escapes and second references, not to drops.
- rc: a getter's result used as a temporary is never released (§15.3).
- Every model: `<B>anyValue` segfaults when `B` implements an interface.
`mlirGenInstanceOfOpaque` calls vtable slot 0 as `..instanceOf`, but such a class keeps the
interface's vtable there (`B..vtbl = {I, .instanceOf, ...}`). Fixing it changes the vtable
layout, so it is left for its own PR.
- From §14.6: a read reached through an interface, borrow chains through assigned borrowers,
moves after the last use of every borrower, and a temporary taken by a `let` and consumed
elsewhere.
145 changes: 144 additions & 1 deletion tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "mlir/Pass/Pass.h"
#include "mlir/Interfaces/SideEffectInterfaces.h"

#include "TypeScript/TypeScriptDialect.h"
#include "TypeScript/TypeScriptOps.h"
Expand All @@ -10,6 +11,7 @@
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/DenseSet.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/TypeSwitch.h"
#include "llvm/Support/Debug.h"

#define DEBUG_TYPE "pass"
Expand Down Expand Up @@ -183,6 +185,49 @@ class OwnedReturnConsumptionPass
callOp->setAttr(OWNED_RESULT_ATTR_NAME, mlir::UnitAttr::get(&getContext()));
});

// A getter read is a call as well - `h.cc` becomes `H.get_cc(h)` in the affine lowering,
// and the getter retains what it returns like any other function - so its result is
// settled the same way. Left out, `h.cc.x` kept the getter's reference and nobody gave
// it back: every temporary read through a getter leaked.
module.walk([&](mlir::Operation *op) {
if (!isGetterRead(op) || op->hasAttr(OWNED_RESULT_ATTR_NAME))
{
return;
}

auto result = op->getResult(0);
if (!mth.ownsHeapMemory(op->getLoc(), result.getType()))
{
return;
}

// Assigning through an accessor builds a getter read first and a setter after it
// (MLIRGen's assignment rebuilds the op with the value to set); the read is left with
// no use, for the canonicalizer to delete. A release would be a use, and would keep a
// call the program never made.
if (!hasLiveUse(result))
{
return;
}

if (anyUnclassifiedOwningReturn &&
!getterReturnsOwned(op, returnsOwned, methodsByMemberName, vtableSlots))
{
return;
}

auto retains = findReceiverRetains(result);
if (!retains.empty())
{
op->setAttr(OWNED_RESULT_ATTR_NAME, mlir::UnitAttr::get(&getContext()));
op->setAttr(OWNED_RESULT_CONSUMED_ATTR_NAME, mlir::UnitAttr::get(&getContext()));
toErase.append(retains.begin(), retains.end());
return;
}

op->setAttr(OWNED_RESULT_ATTR_NAME, mlir::UnitAttr::get(&getContext()));
});

for (auto *op : toErase)
{
op->erase();
Expand Down Expand Up @@ -698,7 +743,14 @@ class OwnedReturnConsumptionPass
return false;
}

auto identifier = virtualRefOp.getIdentifier();
return everyOverrideReturnsOwned(virtualRefOp.getIdentifier(), returnsOwned, methodsByMemberName);
}

// Does every method that can be in the slot `identifier` was written against return owned?
static bool everyOverrideReturnsOwned(mlir::StringRef identifier,
const llvm::DenseSet<mlir::StringRef> &returnsOwned,
const llvm::StringMap<llvm::SmallVector<mlir::StringRef>> &methodsByMemberName)
{
auto dot = identifier.rfind('.');
if (dot == mlir::StringRef::npos || dot + 1 >= identifier.size())
{
Expand Down Expand Up @@ -755,6 +807,15 @@ class OwnedReturnConsumptionPass
return false;
}

return interfaceMemberReturnsOwned(interfaceRefOp, returnsOwned, vtableSlots);
}

// Does every implementation in the vtables of the interface `interfaceRefOp` reads return
// owned?
static bool interfaceMemberReturnsOwned(mlir_ts::InterfaceSymbolRefOp interfaceRefOp,
const llvm::DenseSet<mlir::StringRef> &returnsOwned,
const llvm::StringMap<llvm::DenseMap<int64_t, mlir::StringRef>> &vtableSlots)
{
auto interfaceType = dyn_cast<mlir_ts::InterfaceType>(interfaceRefOp.getInterfaceVal().getType());
if (!interfaceType)
{
Expand Down Expand Up @@ -786,6 +847,88 @@ class OwnedReturnConsumptionPass
return sawCandidate;
}

// `h.cc`, `h[i]` through a `get` accessor: the ops that call a getter and have its result.
static bool isGetterRead(mlir::Operation *op)
{
return op->getNumResults() == 1 &&
mlir::isa<mlir_ts::AccessorOp, mlir_ts::ThisAccessorOp, mlir_ts::ThisIndirectAccessorOp,
mlir_ts::ThisIndexAccessorOp, mlir_ts::ThisIndirectIndexAccessorOp,
mlir_ts::BoundIndirectAccessorOp, mlir_ts::BoundIndirectIndexAccessorOp>(op);
}

// Is anything going to read this value? A use that is itself dead - a `ts.Cast` of it that
// nothing reads - is not one: the canonicalizer deletes the pair.
static bool hasLiveUse(mlir::Value value)
{
for (auto *user : value.getUsers())
{
if (!mlir::isOpTriviallyDead(user))
{
return true;
}
}

return false;
}

// The getter a getter read calls, by the same rules as calleeNameOf and the two
// candidate-set questions: named outright, a virtual slot (every method of that member
// name), or an interface slot (what the vtables put there). A getter given as anything
// else - `ts.Undef` when there is only a setter - is unclassified.
static bool getterReturnsOwned(mlir::Operation *op, const llvm::DenseSet<mlir::StringRef> &returnsOwned,
const llvm::StringMap<llvm::SmallVector<mlir::StringRef>> &methodsByMemberName,
const llvm::StringMap<llvm::DenseMap<int64_t, mlir::StringRef>> &vtableSlots)
{
auto named = [&](mlir::FlatSymbolRefAttr getter) {
return getter && returnsOwned.contains(getter.getValue());
};

auto throughValue = [&](mlir::Value getter) {
auto *definingOp = getter.getDefiningOp();
if (!definingOp)
{
return false;
}

if (auto symbolRefOp = mlir::dyn_cast<mlir_ts::SymbolRefOp>(definingOp))
{
return returnsOwned.contains(symbolRefOp.getIdentifier());
}

if (auto thisSymbolRefOp = mlir::dyn_cast<mlir_ts::ThisSymbolRefOp>(definingOp))
{
return returnsOwned.contains(thisSymbolRefOp.getIdentifier());
}

if (auto virtualSymbolRefOp = mlir::dyn_cast<mlir_ts::VirtualSymbolRefOp>(definingOp))
{
return everyOverrideReturnsOwned(virtualSymbolRefOp.getIdentifier(), returnsOwned,
methodsByMemberName);
}

if (auto thisVirtualSymbolRefOp = mlir::dyn_cast<mlir_ts::ThisVirtualSymbolRefOp>(definingOp))
{
return everyOverrideReturnsOwned(thisVirtualSymbolRefOp.getIdentifier(), returnsOwned,
methodsByMemberName);
}

if (auto interfaceRefOp = mlir::dyn_cast<mlir_ts::InterfaceSymbolRefOp>(definingOp))
{
return interfaceMemberReturnsOwned(interfaceRefOp, returnsOwned, vtableSlots);
}

return false;
};

return llvm::TypeSwitch<mlir::Operation *, bool>(op)
.Case<mlir_ts::AccessorOp, mlir_ts::ThisAccessorOp, mlir_ts::ThisIndexAccessorOp>(
[&](auto accessorOp) { return named(accessorOp.getGetAccessorAttr()); })
.Case<mlir_ts::ThisIndirectAccessorOp, mlir_ts::ThisIndirectIndexAccessorOp,
mlir_ts::BoundIndirectAccessorOp, mlir_ts::BoundIndirectIndexAccessorOp>(
[&](auto accessorOp) { return throughValue(accessorOp.getGetAccessor()); })
.Default([](mlir::Operation *) { return false; });
}

// Is `vtableName` a vtable for `interfaceName`? Both shapes spell the interface as a whole
// dot-separated component: `Sphere.Thing..vtbl` for a class that implements it,
// `Thing.19585545..vtbl` for an object literal that satisfies it. A class's own vtable,
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 @@ -2402,6 +2402,11 @@ set_tests_properties(test-own-inference-erases-birth-takes
PROPERTIES PASS_REGULAR_EXPRESSION "ts.Release"
FAIL_REGULAR_EXPRESSION "ts\\.Retain|error|Stack dump")

# A getter read is settled like a call: its temporary released, its receiver taking it over.
foreach(getter_mm rc gc none)
tslang_add_test(NAME test-jit-${getter_mm}-getter-temporary COMMAND test-runner -jit -mm=${getter_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/rc_getter_temporary.ts")
tslang_add_test(NAME test-compile-${getter_mm}-getter-temporary COMMAND test-runner -mm=${getter_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/rc_getter_temporary.ts")
endforeach()
# Under rc, a call through an interface carries the result its implementation retained: no
# `ts.CallIndirect` returning a C through an interface slot is left without `__owned_result`
# (`__owned_result_named` alone only says it initialises a `const`).
Expand Down
82 changes: 82 additions & 0 deletions tslang/test/tester/own/rc_getter_temporary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// -mm=rc: a getter hands back a reference like any other function, so a getter read is settled
// like a call - a temporary is released at the end of its block, a receiver takes it over. Before,
// `h.cc.x` kept the getter's reference and nothing gave it back. Every shape of getter read is
// here, each read after a `churn()` that would reuse a block freed too early: a class getter
// returning a field and a fresh object, an override, a static getter, `super`'s, an interface
// property implemented by a getter, one taken by a `let`, passed to a call, and assigned through.
class C {
v: number[] = [];
constructor(public x: number) {}
}

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

interface HasC {
get cc(): C;
}

class H implements HasC {
c: C = new C(1);
get cc() {
return this.c;
}

set cc(v: C) {
this.c = v;
}

get fresh() {
return new C(2);
}

static s: C = new C(3);
static get sc() {
return H.s;
}
}

class K extends H {
get cc() {
return super.cc;
}

get fresh() {
return new C(4);
}
}

function xOf(c: C) {
return c.x;
}

function main() {
let t: number = 0;
for (let i = 0; i < 1000; i++) {
const h = new H();
const k: H = new K();
const f: HasC = h;

t += h.cc.x + h.fresh.x + k.cc.x + k.fresh.x + H.sc.x + f.cc.x;
churn();

let b = h.cc;
const g = k.fresh;
churn();
t += b.x + g.x + xOf(h.cc) + xOf(k.fresh);

h.cc = new C(5);
h.cc.x = h.cc.x + 1;
churn();
t += h.cc.x + h.cc.v.length + b.x;
}

assert(t == 29000);
print("done.");
}
Loading