From 87647a176e034ccea16438d33c8d1cfcd1d6f18d Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Wed, 28 Oct 2026 01:35:01 +0000 Subject: [PATCH] -mm=own: a callback field keeps its family when other classes extend the class A derived class reaches its base's field in the base's storage; that access now has the same family key, so the family stays closed while no class on the chain is built by another module or becomes an interface, any or a union (spec 57). Co-Authored-By: Claude Opus 5.5 --- docs/own-memory-model.md | 7 +- .../2026-09-24-own-memory-model-design.md | 26 ++++- .../OwnershipSignatureCallbacks.cpp | 92 ++++++++++------ tslang/test/tester/CMakeLists.txt | 7 +- .../test/tester/own/own_callback_extended.ts | 100 ++++++++++++++++++ .../own/own_err_callback_extended_drops.ts | 46 ++++++++ 6 files changed, 236 insertions(+), 42 deletions(-) create mode 100644 tslang/test/tester/own/own_callback_extended.ts create mode 100644 tslang/test/tester/own/own_err_callback_extended_drops.ts diff --git a/docs/own-memory-model.md b/docs/own-memory-model.md index ba9a65611..02456fad7 100644 --- a/docs/own-memory-model.md +++ b/docs/own-memory-model.md @@ -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 diff --git a/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md b/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md index 65ea1738e..1ff20b5f1 100644 --- a/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md +++ b/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md @@ -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>`), 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. diff --git a/tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp b/tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp index 7ce05f7ad..bc51aa3f1 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp @@ -83,8 +83,10 @@ namespace own_signature if (auto storeOp = mlir::dyn_cast(user)) { auto fieldOp = storeOp.getReference().getDefiningOp(); + auto refType = fieldOp ? mlir::dyn_cast(fieldOp->getOperand(0).getType()) : mlir_ts::RefType(); if (storeOp.getValue() != use.get() || !fieldOp || - !mlir::isa(fieldOp->getOperand(0).getType())) + !(mlir::isa(fieldOp->getOperand(0).getType()) || + (refType && mlir::isa(refType.getElementType())))) { return false; } @@ -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::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 &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(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) { @@ -600,9 +628,22 @@ namespace own_signature std::pair 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(); - auto classType = fieldOp ? mlir::dyn_cast(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(fieldOp->getOperand(0).getType()) : mlir_ts::ClassType()) + { + className = classType.getName().getAttr(); + } + else if (auto refType = fieldOp ? mlir::dyn_cast(fieldOp->getOperand(0).getType()) : mlir_ts::RefType()) + { + if (auto storageType = mlir::dyn_cast(refType.getElementType())) + { + className = storageType.getName().getAttr(); + } + } + + auto constructor = className ? functions.lookup((className.getValue() + ".constructor").str()) : mlir_ts::FuncOp(); if (!constructor) { return {nullptr, -1}; @@ -618,7 +659,7 @@ namespace own_signature std::map, llvm::SmallVector> fields; // the classes whose objects go where their fields may be reached otherwise: cast to an // interface, `any` or a union - llvm::DenseSet escaping; + llvm::StringSet<> escaping; module.walk([&](mlir::Operation *op) { if (auto fieldOp = mlir::dyn_cast(op)) { @@ -638,9 +679,9 @@ namespace own_signature }); for (auto operand : op->getOperands()) { - if (reachedOtherwise && mlir::isa(operand.getType())) + if (auto classType = mlir::dyn_cast(operand.getType()); reachedOtherwise && classType) { - escaping.insert(operand.getType()); + escaping.insert(classType.getName().getValue()); } } }); @@ -648,15 +689,18 @@ namespace own_signature for (auto &[key, refs] : fields) { auto constructor = mlir::cast(key.first); - auto classType = mlir::cast(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) @@ -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(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(); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 2e9f69774..bc9fbdc81 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -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. @@ -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() @@ -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) diff --git a/tslang/test/tester/own/own_callback_extended.ts b/tslang/test/tester/own/own_callback_extended.ts new file mode 100644 index 000000000..2201fb4f3 --- /dev/null +++ b/tslang/test/tester/own/own_callback_extended.ts @@ -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."); +} diff --git a/tslang/test/tester/own/own_err_callback_extended_drops.ts b/tslang/test/tester/own/own_err_callback_extended_drops.ts new file mode 100644 index 000000000..61e17651c --- /dev/null +++ b/tslang/test/tester/own/own_err_callback_extended_drops.ts @@ -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); +}