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
7 changes: 4 additions & 3 deletions docs/own-memory-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -749,9 +749,10 @@ a reference gives it back on every path out of a function.
tests (`compare && compare(a, b)`), hands on to another function (as `sort` hands its comparison
to a helper), names again (`const g = f`) or captures in a closure that calls it (`a.forEach((v)
=> f(v))`, also kept in a local) is known by the functions its callers pass. So is one stored in
a class's field (`constructor(private f: ...)`, `this.f = f`) and one a function returns. Not
when the class is extended, or one of its objects becomes an interface or `any`: then its
fields' callbacks may be any function. A callback may assign the numbers and strings it
a class's field (`constructor(private f: ...)`, `this.f = f`, also from a class extending it)
and one a function returns. Not when one of the class's objects, or of a class extending it,
becomes an interface or `any`, or another module may build or extend it: then its fields'
callbacks may be any function. A callback may assign the numbers and strings it
captures (`total += v.n`); one stored in an object may not capture locals, which would escape
with it. `copyWithin` works on
numbers and strings (a string is copied where it moves); over class instances it would leave one
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5057,5 +5057,27 @@ own, rc, gc and none; own emits no counting. Negatives: `own_err_callback_field_
`own_err_callback_returned_drops`, whose callbacks replace a global array the walk's element may be
part of.

Not here: a class whose object becomes an interface or `any`, or that another class extends, keeps
its fields' families open; so does a field given a callback any other way than those above.
Not here: a class whose object becomes an interface or `any` keeps its fields' families open; so
does a field given a callback any other way than those above. A class another class extends: §57.

## 57. A callback field of a class others extend, 2026-10-28

`class Sub extends Walker { ... }` kept `Walker`'s field families open (§55): a derived class's
object holds its base's storage as its first field, and reaches the field there (`ts.PropertyRef` of
a `!ts.ref<!ts.class_storage<@Walker>>`), where the family saw no access of its own.

- **The key** (`fieldFamilyKey`) is the same for the field reached in an object of the class and in
the base's storage of a derived class's object: the class's constructor and `-1 -` the position.
So a derived class's reads and stores of the field (`this.f(v)`, `this.f = drain`) are the
family's, and a parameter stored there gives it on (`onlyCalledValue` takes a store into a
storage's field as a forward).
- **Closed** while neither the class nor any class extending it (`extendsClass`, the chain of first
fields; one too deep to follow may) is built by another module, nor has an object become an
interface, `any`, `object`, `unknown` or a union. A class-to-class cast (`Sub` to `Walker`) keeps it.

`own_callback_extended` stores and calls the base's field from a derived class and one two levels
down, under own, rc, gc and none; own emits no counting. Negative: `own_err_callback_extended_drops`,
whose derived class stores a function that replaces a global array the walk's element may be part of.

Not here: a class whose object, or a derived class's, becomes an interface or `any` keeps its fields'
families open.
92 changes: 58 additions & 34 deletions tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,10 @@ namespace own_signature
if (auto storeOp = mlir::dyn_cast<mlir_ts::StoreOp>(user))
{
auto fieldOp = storeOp.getReference().getDefiningOp<mlir_ts::PropertyRefOp>();
auto refType = fieldOp ? mlir::dyn_cast<mlir_ts::RefType>(fieldOp->getOperand(0).getType()) : mlir_ts::RefType();
if (storeOp.getValue() != use.get() || !fieldOp ||
!mlir::isa<mlir_ts::ClassType>(fieldOp->getOperand(0).getType()))
!(mlir::isa<mlir_ts::ClassType>(fieldOp->getOperand(0).getType()) ||
(refType && mlir::isa<mlir_ts::ClassStorageType>(refType.getElementType()))))
{
return false;
}
Expand Down Expand Up @@ -112,6 +114,32 @@ namespace own_signature
// The index a function's result family is kept under: no parameter's, nor a field's (spec 56)
static constexpr int32_t RESULT_FAMILY = std::numeric_limits<int32_t>::min();

// Whether a class extends another: a base is the first field of a class's storage. One whose
// storage is not known here, or whose chain is too deep to follow, may.
static bool extendsClass(const llvm::StringMap<mlir_ts::ClassStorageType> &classStorages, llvm::StringRef derived,
llvm::StringRef base)
{
auto storage = classStorages.lookup(derived);
for (auto depth = 0; storage && depth < 64; ++depth)
{
auto fields = storage.getFields();
auto next = fields.empty() ? mlir_ts::ClassStorageType() : mlir::dyn_cast<mlir_ts::ClassStorageType>(fields.front().type);
if (!next)
{
return false;
}

if (next.getName().getValue() == base)
{
return true;
}

storage = next;
}

return true;
}

// Through what unwraps or makes optional a function value, or casts it between function types
static mlir::Value unwrapFunction(mlir::Value value)
{
Expand Down Expand Up @@ -600,9 +628,22 @@ namespace own_signature

std::pair<mlir::Operation *, int32_t> OwnershipSignaturePass::fieldFamilyKey(mlir::Value ref)
{
// of an object of the class, or of its storage where a derived class's object holds it
auto fieldOp = ref.getDefiningOp<mlir_ts::PropertyRefOp>();
auto classType = fieldOp ? mlir::dyn_cast<mlir_ts::ClassType>(fieldOp->getOperand(0).getType()) : mlir_ts::ClassType();
auto constructor = classType ? functions.lookup((classType.getName().getValue() + ".constructor").str()) : mlir_ts::FuncOp();
auto className = mlir::StringAttr();
if (auto classType = fieldOp ? mlir::dyn_cast<mlir_ts::ClassType>(fieldOp->getOperand(0).getType()) : mlir_ts::ClassType())
{
className = classType.getName().getAttr();
}
else if (auto refType = fieldOp ? mlir::dyn_cast<mlir_ts::RefType>(fieldOp->getOperand(0).getType()) : mlir_ts::RefType())
{
if (auto storageType = mlir::dyn_cast<mlir_ts::ClassStorageType>(refType.getElementType()))
{
className = storageType.getName().getAttr();
}
}

auto constructor = className ? functions.lookup((className.getValue() + ".constructor").str()) : mlir_ts::FuncOp();
if (!constructor)
{
return {nullptr, -1};
Expand All @@ -618,7 +659,7 @@ namespace own_signature
std::map<std::pair<mlir::Operation *, int32_t>, llvm::SmallVector<mlir_ts::PropertyRefOp>> fields;
// the classes whose objects go where their fields may be reached otherwise: cast to an
// interface, `any` or a union
llvm::DenseSet<mlir::Type> escaping;
llvm::StringSet<> escaping;
module.walk([&](mlir::Operation *op) {
if (auto fieldOp = mlir::dyn_cast<mlir_ts::PropertyRefOp>(op))
{
Expand All @@ -638,25 +679,28 @@ namespace own_signature
});
for (auto operand : op->getOperands())
{
if (reachedOtherwise && mlir::isa<mlir_ts::ClassType>(operand.getType()))
if (auto classType = mlir::dyn_cast<mlir_ts::ClassType>(operand.getType()); reachedOtherwise && classType)
{
escaping.insert(operand.getType());
escaping.insert(classType.getName().getValue());
}
}
});

for (auto &[key, refs] : fields)
{
auto constructor = mlir::cast<mlir_ts::FuncOp>(key.first);
auto classType = mlir::cast<mlir_ts::ClassType>(refs.front()->getOperand(0).getType());
auto className = constructor.getSymName().drop_back(llvm::StringRef(".constructor").size());
CallbackFamily family;

// a class another module may build, or another class extends (its storage's first field
// is this one's: the field is reached at another position there)
family.closed = !callableFromOutside(constructor) && !escaping.contains(classType) &&
llvm::none_of(classStorages, [&](auto &entry) {
auto storageFields = entry.second.getFields();
return !storageFields.empty() && storageFields.front().type == classType.getStorageType();
// neither the class nor one extending it (whose object holds the field in its base's
// storage, reached there by the same key) is built by another module, nor goes where its
// fields are reached otherwise
auto closedClass = [&](llvm::StringRef name) {
auto classConstructor = functions.lookup((name + ".constructor").str());
return classConstructor && !callableFromOutside(classConstructor) && !escaping.contains(name);
};
family.closed = closedClass(className) && llvm::all_of(classStorages, [&](auto &entry) {
return !extendsClass(classStorages, entry.first(), className) || closedClass(entry.first());
});

for (auto fieldOp : refs)
Expand Down Expand Up @@ -729,27 +773,7 @@ namespace own_signature
// name, of any class: another member overrides it when its class extends this one (a base is
// the first field of a class's storage), or may, when its storage is not known here.
auto classOf = [&](llvm::StringRef symbol) { return symbol.drop_back(methodName(symbol).size() + 1); };
auto extends = [&](llvm::StringRef derived, llvm::StringRef base) {
auto storage = classStorages.lookup(derived);
for (auto depth = 0; storage && depth < 64; ++depth)
{
auto fields = storage.getFields();
auto next = fields.empty() ? mlir_ts::ClassStorageType() : mlir::dyn_cast<mlir_ts::ClassStorageType>(fields.front().type);
if (!next)
{
return false;
}

if (next.getName().getValue() == base)
{
return true;
}

storage = next;
}

return true;
};
auto extends = [&](llvm::StringRef derived, llvm::StringRef base) { return extendsClass(classStorages, derived, base); };

auto notOverridden = [&](mlir_ts::FuncOp funcOp) {
auto symbol = funcOp.getSymNameAttr();
Expand Down
7 changes: 4 additions & 3 deletions tslang/test/tester/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3945,7 +3945,7 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o
own_callback_forwarded own_optional_record_read own_param_default_kept own_literal_into_array
own_generator_take own_rest_array_borrows own_string_copies
own_captured_string_callback own_callback_and_call own_callback_captured
own_callback_shapes)
own_callback_shapes own_callback_extended)
tslang_add_test(NAME test-jit-own-${own_test} COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/own/${own_test}.ts")
tslang_add_test(NAME test-compile-own-${own_test} COMMAND test-runner -mm=own "${PROJECT_SOURCE_DIR}/test/tester/own/${own_test}.ts")
# The promise: no counting at all in what own emits.
Expand Down Expand Up @@ -3982,7 +3982,7 @@ foreach(keeps_mm rc gc none)
own_optional_record_read own_param_default_kept own_literal_into_array
own_generator_take own_rest_array_borrows own_string_copies
own_captured_string_callback own_callback_and_call own_callback_captured
own_callback_shapes)
own_callback_shapes own_callback_extended)
tslang_add_test(NAME test-jit-${keeps_mm}-${keeps_test} COMMAND test-runner -jit -mm=${keeps_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/${keeps_test}.ts")
tslang_add_test(NAME test-compile-${keeps_mm}-${keeps_test} COMMAND test-runner -mm=${keeps_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/${keeps_test}.ts")
endforeach()
Expand Down Expand Up @@ -4682,7 +4682,8 @@ set(own_error_cases
"own_err_captured_string_drops|:20:5: error: '[^']*' borrows a field but is used here after it may be released or overwritten"
"own_err_callback_captured_drops|:14:9: error: '[^']*' borrows an element but is used here after it may be released or overwritten"
"own_err_callback_field_drops|:16:13: error: '[^']*' borrows an element but is used here after it may be released or overwritten"
"own_err_callback_returned_drops|:21:9: error: '[^']*' borrows an element but is used here after it may be released or overwritten")
"own_err_callback_returned_drops|:21:9: error: '[^']*' borrows an element but is used here after it may be released or overwritten"
"own_err_callback_extended_drops|:16:13: error: '[^']*' borrows an element but is used here after it may be released or overwritten")
foreach(own_error_case ${own_error_cases})
string(FIND "${own_error_case}" "|" own_error_sep)
string(SUBSTRING "${own_error_case}" 0 ${own_error_sep} own_error_name)
Expand Down
100 changes: 100 additions & 0 deletions tslang/test/tester/own/own_callback_extended.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// A callback stored into a class's field keeps its family when other classes extend the class
// (spec 57): a derived class reaches the field in its base's storage, by the same key, both to call
// it and to store into it. Each walk reads the element again after the call, so the call must
// release nothing of it. Each step churns, so an element freed too early reads garbage; every model
// runs it.
class Item {
constructor(public n: number) {}
}

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

let seen = 0;

function once(v: Item) {
seen++;
}

function twice(v: Item) {
seen += 2;
}

class Walker {
constructor(protected f: (v: Item) => void) {}

walk(a: Item[]) {
let s: number = 0;
for (let i = 0; i < a.length; i++) {
const v = a[i];
this.f(v);
s += v.n;
}

return s;
}
}

// calls the field and stores into it from the derived class
class Sub extends Walker {
constructor(f: (v: Item) => void) {
super(f);
}

use(f: (v: Item) => void) {
this.f = f;
}

each(a: Item[]) {
let s: number = 0;
for (const v of a) {
this.f(v);
s += v.n;
}

return s;
}
}

// two levels down
class Leaf extends Sub {
constructor() {
super(twice);
}

reset() {
this.f = once;
}
}

function run(a: Item[]) {
seen = 0;
const s = new Sub(once);
let r = s.walk(a) + s.each(a);
s.use(twice);
r += s.walk(a);
churn();

const l = new Leaf();
r += l.each(a);
l.reset();
r += l.walk(a);

const w = new Walker(twice);
r += w.walk(a);
return r * 100 + seen;
}

function main() {
let ok = 0;
for (let i = 0; i < 300; i++) {
const a = [new Item(1), new Item(5), new Item(2)];
if (run(a) == 4827) ok++;
churn();
}

assert(ok == 300, "ok");
print("done.");
}
46 changes: 46 additions & 0 deletions tslang/test/tester/own/own_err_callback_extended_drops.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// -mm=own rejects: a class extending `Walker` stores into its field (spec 57) a function that
// replaces a global array, releasing what it held. `walk` calls it with an element of an array that
// may be that global, and reads the element again after the call.
class Item {
constructor(public n: number) {}
}

let pool: Item[] = [];

class Walker {
constructor(protected f: (v: Item) => void) {}

walk(a: Item[]) {
for (let i = 0; i < a.length; i++) {
const v = a[i];
this.f(v);
print(v.n);
}
}
}

function show(v: Item) {
print(v.n);
}

function drain(v: Item) {
pool = [];
}

class Sub extends Walker {
constructor() {
super(show);
}

swap() {
this.f = drain;
}
}

function main() {
pool = [new Item(7)];
const s = new Sub();
s.swap();
s.walk([new Item(1)]);
print(pool.length);
}
Loading