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 @@ -111,6 +111,9 @@ unchanged.
under it (five default-library tests failed ahead of time). Counting the header's reference
restores what main does. A reallocating change (`push`, and PR 3's growth) still moves the
block under such a string, as it does on main (#481).
- Since #481 such a string is a view under rc, as under own: it takes no reference to the block,
and where it is kept (returned, stored, pushed, captured) the keeper takes a copy. The count
the header holds on its data block stays; nothing else adds one now.
- Slots `[length, capacity)` are never released (and, from PR 3, hold zero).
- `pop` and `shift` hand the removed element's reference to the caller, as today.
- **own.** The header is a single-owned block, like a class instance.
Expand Down
138 changes: 138 additions & 0 deletions tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
#include "TypeScript/MLIRLogic/MLIRTypeHelper.h"
#include "TypeScript/MLIRLogic/MLIROwnedReference.h"

#include "OwnershipFacts.h"

#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/DenseSet.h"
#include "llvm/ADT/SmallVector.h"
Expand Down Expand Up @@ -60,6 +62,8 @@ class OwnedReturnConsumptionPass
auto module = getOperation();
MLIRTypeHelper mth(module->getContext(), compileOptions);

copyEscapingStringViews(module);

llvm::DenseSet<mlir::StringRef> returnsOwned;
// Every method in the module, grouped by the name after the last dot: the candidate set
// for a virtual call, see virtualCallReturnsOwned.
Expand Down Expand Up @@ -411,6 +415,140 @@ class OwnedReturnConsumptionPass
});
}

// A string made over memory it does not own - `<string><Opaque>Ref(buffer[0])`, a cast out of
// `!ts.opaque` - is a view of that memory, not a block of its own (#481). Under rc a receiver
// used to retain it, counting whatever word sits in front of the pointer: the array's data
// block for `Ref(buffer[0])`, which then outlived the array and leaked once the array grew,
// and element data or another block's tail for `Ref(buffer[1])`. Now, as under own, a view
// keeps no count: where a receiver retains it (a return, a store into a field, an element or
// a global, a push, a capture), the receiver takes a copy of the string instead, made right
// there - after the bytes were written through the view (`sprintf_s(s, ...)`). A `let` that
// only ever holds a view borrows it; one that is assigned again starts from a copy, since the
// assignment releases what it held.
//
// A view still points into the array: used after the array grows or dies, it reads freed
// memory in every model, as a C pointer would. A view kept inside a tuple or record literal,
// or by a function it is passed to, is not copied.
void copyEscapingStringViews(mlir::ModuleOp module)
{
if (!compileOptions.isRefCounted())
{
return;
}

llvm::DenseSet<mlir::Operation *> viewLocals;
auto isView = [&](mlir::Value value) {
for (auto depth = 0; depth < 16 && value; depth++)
{
auto *definingOp = value.getDefiningOp();
if (auto castOp = mlir::dyn_cast_or_null<mlir_ts::CastOp>(definingOp))
{
if (mlir::isa<mlir_ts::OpaqueType>(castOp.getIn().getType()))
{
return true;
}

value = own_facts::isCopyableString(castOp.getIn().getType()) ? castOp.getIn() : mlir::Value();
}
else if (auto loadOp = mlir::dyn_cast_or_null<mlir_ts::LoadOp>(definingOp))
{
auto *slot = loadOp.getReference().getDefiningOp();
value = slot && viewLocals.contains(slot)
? mlir::cast<mlir_ts::VariableOp>(slot).getInitializer()
: mlir::Value();
}
else
{
return false;
}
}

return false;
};

// `let l = view`: borrowed when nothing assigns the variable again, a copy when something does.
// Collected first: the walk has already stepped to the op after a variable - its RetainSlot,
// which is erased below.
llvm::SmallVector<mlir_ts::VariableOp> variables;
module.walk([&](mlir_ts::VariableOp varOp) { variables.push_back(varOp); });
for (auto varOp : variables)
{
auto init = varOp.getInitializer();
if (!init || !own_facts::isCopyableString(init.getType()) || !isView(init) || varOp.getCaptured().value_or(false))
{
continue;
}

auto assignedAgain = llvm::any_of(varOp->getUsers(), [&](mlir::Operation *user) {
auto storeOp = mlir::dyn_cast<mlir_ts::StoreOp>(user);
return storeOp && storeOp.getReference() == varOp.getResult();
});

if (assignedAgain)
{
mlir::OpBuilder builder(varOp);
auto copy = builder.create<mlir_ts::StringCopyOp>(varOp.getLoc(), init.getType(), init);
varOp.getInitializerMutable().assign(copy);
continue;
}

llvm::SmallVector<mlir::Operation *> slotOps;
for (auto *user : varOp->getUsers())
{
if (mlir::isa<mlir_ts::RetainSlotOp, mlir_ts::ReleaseSlotOp>(user))
{
slotOps.push_back(user);
}
}

for (auto *slotOp : slotOps)
{
slotOp->erase();
}

varOp->removeAttr(OWNED_LOCAL_ATTR_NAME);
viewLocals.insert(varOp);
}

// a receiver's retain: MLIRGen puts it right in front of the receiver, with nothing but
// other counting between them (`ts.Retain(v); ts.ReleaseSlot(field); ts.Store(v, field)`)
llvm::SmallVector<mlir_ts::RetainOp> retains;
module.walk([&](mlir_ts::RetainOp retainOp) {
auto value = retainOp.getReference();
if (own_facts::isCopyableString(value.getType()) && isView(value))
{
retains.push_back(retainOp);
}
});

for (auto retainOp : retains)
{
auto value = retainOp.getReference();
auto *receiver = retainOp->getNextNode();
while (receiver && mlir::isa<mlir_ts::RetainOp, mlir_ts::ReleaseOp, mlir_ts::RetainSlotOp,
mlir_ts::ReleaseSlotOp>(receiver))
{
receiver = receiver->getNextNode();
}

if (!receiver || !llvm::is_contained(receiver->getOperands(), value))
{
continue;
}

mlir::OpBuilder builder(retainOp);
auto copy = builder.create<mlir_ts::StringCopyOp>(retainOp->getLoc(), value.getType(), value);
retainOp->setOperand(0, copy);
for (auto &operand : receiver->getOpOperands())
{
if (operand.get() == value)
{
operand.set(copy);
}
}
}
}

// An array or tuple built in a global's region (`const a = [1, 2]`, `const t = [1, [6, 7]]`, or
// a field of `const o = { items: [1, 2] }`) is born at count 0 under rc: a local takes its first
// count with RetainSlot, and nothing took one for the global, so the first local that read it
Expand Down
3 changes: 3 additions & 0 deletions tslang/test/tester/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ tslang_add_test(NAME test-compile-00-array-rest-copy COMMAND test-runner "${PROJ
tslang_add_test(NAME test-compile-00-arrays6 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array6.ts")
tslang_add_test(NAME test-compile-00-array-static-nested COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_static_nested.ts")
tslang_add_test(NAME test-compile-00-array-literal-copy COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_literal_copy.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-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")
Expand Down Expand Up @@ -850,6 +851,7 @@ tslang_add_test(NAME test-jit-00-array-rest-copy COMMAND test-runner -jit "${PRO
tslang_add_test(NAME test-jit-00-arrays6 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array6.ts")
tslang_add_test(NAME test-jit-00-array-static-nested COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_static_nested.ts")
tslang_add_test(NAME test-jit-00-array-literal-copy COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_literal_copy.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-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")
Expand Down Expand Up @@ -1681,6 +1683,7 @@ set(TSLANG_CORPUS
00array6.ts
00array_static_nested.ts
00array_literal_copy.ts
00string_view_copy.ts
00array_data_string_alias.ts
00array7.ts
00array8_tuple_spread.ts
Expand Down
86 changes: 86 additions & 0 deletions tslang/test/tester/tests/00string_view_copy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// A string made over an array's memory with `<string><Opaque>Ref(buffer[i])` is a view of that
// memory (#481). Under rc it keeps no count; where it is kept - returned, stored, pushed,
// captured - the keeper takes a copy, made after the bytes were written through the view.
declare function strcpy(dst: string, src: string): string;

class Box {
s: string = "";
}

let g = "";

function conv(value: string): string {
let buffer: char[] = [];
buffer.length = 50;
const s = <string><Opaque>Ref(buffer[0]);
strcpy(s, value);
return s;
}

function convLet(value: string): string {
let buffer: char[] = [];
buffer.length = 50;
let s = <string><Opaque>Ref(buffer[0]);
strcpy(s, value);
return s;
}

function tail(): string {
let buffer: char[] = [];
buffer.length = 50;
strcpy(<string><Opaque>Ref(buffer[0]), "xyz");
const s = <string><Opaque>Ref(buffer[1]);
return s;
}

function keep(b: Box, arr: string[]) {
let buffer: char[] = [];
buffer.length = 50;
const s = <string><Opaque>Ref(buffer[0]);
strcpy(s, "kept");
b.s = s;
g = s;
arr.push(s);
}

// a view is not a block: the word in front of it is element data, not a count
function frontBytes() {
let buffer: char[] = [];
buffer.length = 32;
strcpy(<string><Opaque>Ref(buffer[0]), "abcdefghijklmnop");
g = <string><Opaque>Ref(buffer[8]);
const whole = <string><Opaque>Ref(buffer[0]);
assert(whole == "abcdefghijklmnop", "the bytes in front of a view are not a count");
assert(g == "ijklmnop", "a copy of the view");
}

// the array grows after its view was kept: the keeper's copy does not hold the old block
function grows() {
let buffer: char[] = [];
buffer.length = 16;
const s = <string><Opaque>Ref(buffer[0]);
strcpy(s, "grows");
g = s;
for (let j = 0; j < 64; j++) buffer.push(<char>65);
}

function main() {
const a = conv("1.5");
const b = convLet("2.25");
const t = tail();
const box = new Box();
const arr: string[] = [];
keep(box, arr);

// reuse freed memory: a string left pointing at a freed buffer would show it
const fill: string[] = [];
for (let i = 0; i < 4096; i++) fill.push("filler-" + i);

assert(a == "1.5" && b == "2.25" && t == "yz", "returned");
assert(box.s == "kept" && g == "kept" && arr[0] == "kept", "kept");

frontBytes();
for (let i = 0; i < 3; i++) grows();

print("done.");
}
Loading