diff --git a/docs/own-memory-model.md b/docs/own-memory-model.md index 397a2f675..ba9a65611 100644 --- a/docs/own-memory-model.md +++ b/docs/own-memory-model.md @@ -748,9 +748,12 @@ a reference gives it back on every path out of a function. gives an object in `t` a second owner, which is an error. A callback your function only calls, 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))`) is known by the functions its callers pass; one stored in a field, returned, or - captured in a closure that is itself kept in a local is not. A callback may assign the numbers - and strings it captures (`total += v.n`). `copyWithin` works on + => 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 + 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 object in two elements, an error by design. - Iterator helpers work, the lazy ones (`map`, `filter`, `take`, `drop`) taking their source over 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 9acd5bf98..65ea1738e 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 @@ -5007,5 +5007,55 @@ churning between steps, under own, rc, gc and none; own emits no counting. Negat `own_err_callback_captured_drops`, whose callback replaces a global array that the walk's element may be part of. -Not here: a closure held in a local and called (`const call = (v) => f(v); call(v)`), and a -callback stored into a field or returned, keep their families open. +Not here, then: a closure held in a local and called, and a callback stored into a field or +returned (§55, §56). + +## 55. A callback stored into a class's field, 2026-10-28 + +`class Walker { constructor(private f: (v: Item) => void) {} walk(a) { ... this.f(a[i]) ... } }` +was `borrows an element but is used here after it may be released or overwritten`: a call through a +field reached any function, and the constructor's parameter, stored, had no family (§43). + +- **A field's family** (`collectFieldFamilies`) is kept under its class's constructor and `-1 -` the + field's position (`fieldFamilyKey`), which no parameter has. Its members are the functions stored + into the field by name; its sources, the callback parameters given on into it (`this.f = f` in a + constructor or a method) and what functions returning one returned (§56) (`takeFamilyValue`). + Each read of the field is only called, tested, or given on (`onlyCalledValue`), and the field's + own `ts.ReleaseSlot` and `ts.RetainSlot` are its overwrites. +- **The class** is closed: no other module builds it (`callableFromOutside` of its constructor), no + class extends it (another class's storage would hold the field at another position), and no + object of it becomes an interface, `any`, `object`, `unknown` or a union, through which its fields + are reached by no `ts.PropertyRef` of its own. +- **A parameter stored into the field** gives it on (`onlyCalledValue` takes the store, and rc's + retain for it, as a forward; `parameterOf` names the field's family), so the constructor's + parameter is closed while the field's family is. +- **A call through the field** (`resolveCallback`) resolves through its family. + +## 56. A callback a function returns, 2026-10-28 + +`function above(n) { return n > 1 ? (v) => v.n > 1 : (v) => v.n > 0; }`, called through what it +returned or given on (`count(items, above(0))`), reached any function. + +- **A result family** (`collectResultFamilies`) of each function whose one result is a function value + is kept under the function and `RESULT_FAMILY`. What it returns - through the result slot and the + block arguments branches merge - is each a function by name, a callback parameter given on, or + what another such function returned; anything else opens it. A member's symbol that only flows to + the return (`flowsToReturn`) is passed to the family, not escaped. +- **A value given to a family** (`takeFamilyValue`, shared by parameter, field and result families) + may be a call of such a function, or a local declared from one and never assigned + (`givenResult`): the result family is its source. +- **A call through it** - the call's result itself, or such a local - resolves through the result + family (`resolveCallback` now takes the callee value). + +With §54, a closure held in a local and called (`const call = (v) => f(v); call(v)`) compiles: the +local is a view of the closure, called by name with its box. + +`own_callback_shapes` walks an array a class holds through a closure in a local, a callback stored +into a field by a constructor and by a method, and callbacks a function returns, called through a +local and given on, reading each element again after the call and churning between steps, under +own, rc, gc and none; own emits no counting. Negatives: `own_err_callback_field_drops` and +`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. diff --git a/tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp b/tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp index 9c65a0f85..7ce05f7ad 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp @@ -30,7 +30,8 @@ namespace own_signature } // A read of a callback parameter: the function a call goes through, tested for being given - // (`if (f)`), unwrapped, or an argument of a call that gives it on (`forwards`) + // (`if (f)`), unwrapped, or an argument of a call or stored into a class's field, either of + // which gives it on (`forwards`) static bool onlyCalledValue(mlir::Value value, llvm::SmallVectorImpl &forwards, int depth = 0) { if (depth > 8) @@ -72,6 +73,26 @@ namespace own_signature continue; } + // rc's count for a store of it + if (mlir::isa(user)) + { + continue; + } + + // stored into a class's field: given on to the field's family (spec 55) + if (auto storeOp = mlir::dyn_cast(user)) + { + auto fieldOp = storeOp.getReference().getDefiningOp(); + if (storeOp.getValue() != use.get() || !fieldOp || + !mlir::isa(fieldOp->getOperand(0).getType())) + { + return false; + } + + forwards.push_back(&use); + continue; + } + // the callee, or one argument: never both, never twice auto called = calleeValue(user) == use.get(); if (!isCall(user) || llvm::count(callArgs(user), use.get()) + (called ? 1 : 0) != 1) @@ -88,6 +109,62 @@ namespace own_signature return true; } + // 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(); + + // Through what unwraps or makes optional a function value, or casts it between function types + static mlir::Value unwrapFunction(mlir::Value value) + { + for (auto depth = 0; value && depth < 8; ++depth) + { + auto *def = value.getDefiningOp(); + if (auto valueOp = mlir::dyn_cast_or_null(def)) + { + value = valueOp.getIn(); + } + else if (auto optionalOp = mlir::dyn_cast_or_null(def)) + { + value = optionalOp.getIn(); + } + else if (auto castOp = mlir::dyn_cast_or_null(def); castOp && isFunctionValue(castOp.getType())) + { + value = castOp.getIn(); + } + else + { + break; + } + } + + return value; + } + + // A local declared from `value` and never assigned nor captured: each read of it is `value` + static mlir::Value initializerOf(mlir_ts::VariableOp varOp) + { + if (!varOp || !varOp.getInitializer() || varOp.getCaptured().value_or(false) || + llvm::any_of(varOp->getUsers(), [](mlir::Operation *user) { return mlir::isa(user); })) + { + return {}; + } + + return varOp.getInitializer(); + } + + // The function whose call made `value` - directly, or read out of a local declared from it - when + // it returns a function value: its result family is the value's (spec 56) + static mlir::FlatSymbolRefAttr givenResult(mlir::Value value) + { + value = unwrapFunction(value); + if (auto loadOp = value ? value.getDefiningOp() : mlir_ts::LoadOp()) + { + value = unwrapFunction(initializerOf(loadOp.getReference().getDefiningOp())); + } + + auto *call = value ? value.getDefiningOp() : nullptr; + return call && isCall(call) && call->getNumResults() == 1 ? directCallee(call) : mlir::FlatSymbolRefAttr(); + } + // The caller's parameter `value` gives on: a read of its slot, unwrapped, made optional or cast // between function types. Its function, or null. static mlir_ts::FuncOp givenParam(mlir::Value value, int &index) @@ -360,6 +437,275 @@ namespace own_signature return true; } + // Is `use` a way of a value to its function's return only: the return, a store into the slot + // only returned, a cast between function types or a branch to a block argument that is? + static bool flowsToReturn(mlir::OpOperand &use, int depth = 0) + { + if (depth > 8) + { + return false; + } + + auto *user = use.getOwner(); + if (mlir::isa(user)) + { + return true; + } + + if (auto storeOp = mlir::dyn_cast(user)) + { + auto slot = storeOp.getReference().getDefiningOp(); + return storeOp.getValue() == use.get() && slot && !slot.getInitializer() && !slot.getCaptured().value_or(false) && + llvm::all_of(slot->getUsers(), [&](mlir::Operation *slotUser) { + if (auto otherStore = mlir::dyn_cast(slotUser)) + { + return otherStore.getReference() == slot.getResult(); + } + + auto loadOp = mlir::dyn_cast(slotUser); + return loadOp && llvm::all_of(loadOp->getUsers(), [](mlir::Operation *loadUser) { + return mlir::isa(loadUser); + }); + }); + } + + auto flowsOn = [&](mlir::Value next) { + return llvm::all_of(next.getUses(), [&](mlir::OpOperand &nextUse) { return flowsToReturn(nextUse, depth + 1); }); + }; + + if (auto castOp = mlir::dyn_cast(user); castOp && isFunctionValue(castOp.getType())) + { + return flowsOn(castOp.getResult()); + } + + if (auto branchOp = mlir::dyn_cast(user)) + { + auto passed = false; + for (auto [index, successor] : llvm::enumerate(user->getSuccessors())) + { + auto operands = branchOp.getSuccessorOperands(index); + for (auto [position, operand] : llvm::enumerate(operands.getForwardedOperands())) + { + if (operand != use.get()) + { + continue; + } + + passed = true; + if (!flowsOn(successor->getArgument(operands.getProducedOperandCount() + position))) + { + return false; + } + } + } + + return passed; + } + + return false; + } + + bool OwnershipSignaturePass::takeFamilyValue(CallbackFamily &family, mlir::Value value, mlir::Operation *at) + { + auto shift = 0; + if (auto refOp = passedFunction(value, shift)) + { + auto member = functions.lookup(refOp.getIdentifier()); + if (!member || member.isDeclaration() || callableFromOutside(member) || declaresFacts(member)) + { + return false; + } + + auto symbol = member.getSymNameAttr(); + auto same = llvm::find_if(family.members, [&](const CallbackMember &known) { return known.symbol == symbol; }); + if (same != family.members.end() && same->shift != shift) + { + return false; + } + + if (same == family.members.end()) + { + family.members.push_back({symbol, shift}); + } + + family.passing.push_back(refOp); + return true; + } + + // an optional callback not given: no function + if (mlir::isa_and_nonnull(value.getDefiningOp()) && + mlir::isa(value.getType())) + { + return true; + } + + // a callback parameter of the function `at` is in, given on: the functions passed for it + auto position = -1; + if (auto source = givenParam(value, position); source && source->isAncestor(at)) + { + family.sources.push_back({source, position}); + return true; + } + + // what a function that returns a function value returned (spec 56) + if (auto callee = givenResult(value)) + { + if (auto producer = functions.lookup(callee.getValue())) + { + family.sources.push_back({producer.getOperation(), RESULT_FAMILY}); + return true; + } + } + + return false; + } + + void OwnershipSignaturePass::collectResultFamilies() + { + for (auto &entry : functions) + { + auto funcOp = entry.second; + auto results = funcOp.getFunctionType().getResults(); + if (funcOp.isDeclaration() || results.size() != 1 || !isCallbackType(results.front())) + { + continue; + } + + CallbackFamily family; + llvm::SmallVector work(returnedValues(funcOp)); + llvm::DenseSet seen; + while (family.closed && !work.empty()) + { + auto value = work.pop_back_val(); + if (!seen.insert(value).second) + { + continue; + } + + // merged from branches: each value passed for the block's argument + if (auto argument = mlir::dyn_cast(value); argument && !argument.getOwner()->isEntryBlock()) + { + llvm::SmallVector merged; + family.closed = mergedInto(argument, merged); + work.append(merged.begin(), merged.end()); + continue; + } + + family.closed = takeFamilyValue(family, value, funcOp); + } + + callbackFamilies[{funcOp.getOperation(), RESULT_FAMILY}] = std::move(family); + } + } + + std::pair OwnershipSignaturePass::fieldFamilyKey(mlir::Value ref) + { + 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(); + if (!constructor) + { + return {nullptr, -1}; + } + + return {constructor.getOperation(), -1 - static_cast(fieldOp.getPosition())}; + } + + void OwnershipSignaturePass::collectFieldFamilies( + mlir::ModuleOp module, llvm::function_ref(mlir::OpOperand &)> parameterOf) + { + // the fields of a callback type, by class and position + 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; + module.walk([&](mlir::Operation *op) { + if (auto fieldOp = mlir::dyn_cast(op)) + { + auto refType = mlir::dyn_cast(fieldOp.getType()); + auto key = fieldFamilyKey(fieldOp.getResult()); + if (key.first && refType && isCallbackType(refType.getElementType())) + { + fields[key].push_back(fieldOp); + } + + return; + } + + auto reachedOtherwise = llvm::any_of(op->getResultTypes(), [](mlir::Type type) { + return mlir::isa(type); + }); + for (auto operand : op->getOperands()) + { + if (reachedOtherwise && mlir::isa(operand.getType())) + { + escaping.insert(operand.getType()); + } + } + }); + + for (auto &[key, refs] : fields) + { + auto constructor = mlir::cast(key.first); + auto classType = mlir::cast(refs.front()->getOperand(0).getType()); + 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(); + }); + + for (auto fieldOp : refs) + { + for (auto &use : fieldOp->getUses()) + { + auto *user = use.getOwner(); + if (!family.closed) + { + break; + } + + if (mlir::isa(user)) + { + continue; + } + + // read only to be called, or given on + if (auto loadOp = mlir::dyn_cast(user)) + { + llvm::SmallVector forwards; + family.closed = onlyCalledValue(loadOp.getResult(), forwards); + for (auto *forward : forwards) + { + auto target = parameterOf(*forward); + family.closed = family.closed && target.first; + family.forwards.push_back(target); + } + + continue; + } + + // given a function by name, a callback parameter of the function that stores it, or + // what a function returning one returned + auto storeOp = mlir::dyn_cast(user); + if (!storeOp || storeOp.getReference() != fieldOp.getResult()) + { + family.closed = false; + break; + } + + family.closed = takeFamilyValue(family, storeOp.getValue(), storeOp); + } + } + + callbackFamilies[key] = std::move(family); + } + } + void OwnershipSignaturePass::collectCallbackFamilies(mlir::ModuleOp module) { capturedCallbacks.clear(); @@ -501,6 +847,19 @@ namespace own_signature // operand's position; null when the call reaches no one function or `use` is not an argument. auto parameterOf = [&](mlir::OpOperand &use) -> std::pair { auto *call = use.getOwner(); + // ...or the result of the function it is returned from (spec 56) + if (flowsToReturn(use)) + { + auto funcOp = call->getParentOfType(); + return {funcOp ? funcOp.getOperation() : nullptr, RESULT_FAMILY}; + } + + // ...or the field of a class it is stored into (spec 55) + if (auto storeOp = mlir::dyn_cast(call)) + { + return fieldFamilyKey(storeOp.getReference()); + } + if (!isCall(call)) { return {nullptr, -1}; @@ -557,55 +916,20 @@ namespace own_signature break; } + // a function by name, an optional one not given, the caller's own callback + // parameter given on, or what a function returning one returned auto args = callArgs(user); auto arg = static_cast(index) < args.size() ? args[index] : mlir::Value(); - auto shift = 0; - auto refOp = arg ? passedFunction(arg, shift) : mlir_ts::SymbolRefOp(); - if (!refOp && arg) - { - // an optional callback not given: no function - if (mlir::isa_and_nonnull(arg.getDefiningOp()) && - mlir::isa(arg.getType())) - { - continue; - } - - // the caller's own callback parameter, given on: the functions passed for it - auto position = -1; - if (auto source = givenParam(arg, position); source && source->isAncestor(user)) - { - family.sources.push_back({source, position}); - continue; - } - } - - auto member = refOp ? functions.lookup(refOp.getIdentifier()) : mlir_ts::FuncOp(); - if (!member || member.isDeclaration() || callableFromOutside(member) || declaresFacts(member)) - { - family.closed = false; - break; - } - - auto symbol = member.getSymNameAttr(); - auto same = llvm::find_if(family.members, [&](const CallbackMember &known) { return known.symbol == symbol; }); - if (same != family.members.end() && same->shift != shift) - { - family.closed = false; - break; - } - - if (same == family.members.end()) - { - family.members.push_back({symbol, shift}); - } - - family.passing.push_back(refOp); + family.closed = arg && takeFamilyValue(family, arg, user); } callbackFamilies[{funcOp, index}] = std::move(family); } } + collectFieldFamilies(module, parameterOf); + collectResultFamilies(); + // A member's symbol used anywhere else - a call through another value, a field, a vtable, a // global - reaches callers this cannot see; so does one passed to a family that is not closed. // A parameter given on is closed only while the parameters it is given from and on to are: @@ -716,13 +1040,30 @@ namespace own_signature } } - void OwnershipSignaturePass::resolveCallback(mlir_ts::LoadOp loadOp, Callees &callees) + void OwnershipSignaturePass::resolveCallback(mlir::Value callee, Callees &callees) { - auto varOp = loadOp.getReference().getDefiningOp(); - auto funcOp = loadOp->getParentOfType(); + auto loadOp = callee.getDefiningOp(); + auto producer = givenResult(callee); + if (!loadOp && !producer) + { + return; + } + + auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + auto funcOp = callee.getParentRegion()->getParentOfType(); auto index = -1; std::pair key{funcOp.getOperation(), -1}; - if (varOp && funcOp && isParameterSlot(varOp, index)) + if (producer && functions.lookup(producer.getValue())) + { + // what a function that returns a function value returned, called as it is or through a + // local declared from it (spec 56) + key = {functions.lookup(producer.getValue()).getOperation(), RESULT_FAMILY}; + } + else if (!loadOp) + { + return; + } + else if (varOp && funcOp && isParameterSlot(varOp, index)) { key.second = index; } @@ -734,6 +1075,11 @@ namespace own_signature // the captured parameter's cell, read where it is declared (spec 54) key.second = static_cast(argument.getArgNumber()); } + else if (auto fieldKey = fieldFamilyKey(loadOp.getReference()); fieldKey.first) + { + // a class's field of a callback type (spec 55) + key = fieldKey; + } else if (auto cellRead = loadOp.getReference().getDefiningOp()) { // read out of a closure's capture box: the parameter that field holds the cell of diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.h b/tslang/lib/TypeScript/OwnershipSignaturePass.h index 0992ba9c0..f2266e006 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.h +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.h @@ -840,6 +840,24 @@ class OwnershipSignaturePass : public mlir::PassWrapper, std::pair> capturedCallbacks; + // Takes `value`, given to `family` at `at`, in: a function by name (a member), an optional one not + // given, a callback parameter of the function `at` is in (a source), or what a function returning + // a function value returned (its result family, a source, spec 56). False for anything else. + bool takeFamilyValue(CallbackFamily &family, mlir::Value value, mlir::Operation *at); + + // The families of what each function returning a function value returns (spec 56) + void collectResultFamilies(); + + // The family key of a class's field `ref` points at: its class's constructor, and -1 - the + // field's position, which no parameter has (spec 55). Null when the class has no constructor. + std::pair fieldFamilyKey(mlir::Value ref); + + // The families of the class fields of a callback type: the functions stored into each by name, + // and the callback parameters given on into it, when every read of it is only called or given on + // and no object of the class is reached but as itself (spec 55) + void collectFieldFamilies(mlir::ModuleOp module, + llvm::function_ref(mlir::OpOperand &)> parameterOf); + // The closure function a capture box is bound to, when every box it is bound to holds `cell` in // field `position`. mlir_ts::FuncOp closureFunctionOf(mlir::Value box, mlir::Value cell, int32_t position); @@ -856,8 +874,9 @@ class OwnershipSignaturePass : public mlir::PassWrapper(def)) + else if (mlir::isa_and_nonnull(def) || (def && isCall(def))) { - resolveCallback(loadOp, callees); + // a callback parameter, field or local; or what a call returned (spec 56) + resolveCallback(callee, callees); return; } else if (auto valueOp = mlir::dyn_cast_or_null(def); valueOp && valueOp.getIn().getDefiningOp()) { // an optional callback, called once tested - resolveCallback(valueOp.getIn().getDefiningOp(), callees); + resolveCallback(valueOp.getIn(), callees); return; } else if (auto boundOp = mlir::dyn_cast_or_null(def)) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 313d4dd21..b16119051 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -3941,7 +3941,8 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o own_generator_this_param own_copy_within_returned_param own_swap_elements 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_captured_string_callback own_callback_and_call own_callback_captured + own_callback_shapes) 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. @@ -3977,7 +3978,8 @@ foreach(keeps_mm rc gc none) own_copy_within_returned_param own_swap_elements 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_captured_string_callback own_callback_and_call own_callback_captured + own_callback_shapes) 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() @@ -4675,7 +4677,9 @@ set(own_error_cases "own_err_optional_record_field_kept|:15:12: error: '[^']*' borrows the result of 'findIn' and cannot be stored, returned or captured" "own_err_generator_take_used_after|:14:9: error: '[^']*' borrows a field and cannot be stored, returned or captured" "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_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") 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_shapes.ts b/tslang/test/tester/own/own_callback_shapes.ts new file mode 100644 index 000000000..649c3e741 --- /dev/null +++ b/tslang/test/tester/own/own_callback_shapes.ts @@ -0,0 +1,111 @@ +// Callbacks known by the functions that can reach them, in four shapes: a closure kept in a local +// and called (spec 54), a callback stored into a class's field - by the constructor or by a method +// - and called through it (spec 55), and one a function returns, called through a local or given on +// (spec 56). 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)); +} + +// a closure in a local, over the callback parameter +function walkLocal(a: Item[], f: (v: Item) => void) { + const call = (v: Item) => f(v); + let s = 0; + for (let i = 0; i < a.length; i++) { + const v = a[i]; + call(v); + s += v.n; + } + + return s; +} + +let seen = 0; + +// a callback in a field +class Walker { + constructor(private f: (v: Item) => void) {} + + use(f: (v: Item) => void) { + this.f = f; + } + + walk(a: Item[]) { + let s = 0; + for (let i = 0; i < a.length; i++) { + const v = a[i]; + this.f(v); + s += v.n; + } + + return s; + } +} + +// a callback returned +function above(n: number) { + return n > 1 ? (v: Item) => v.n > 1 : (v: Item) => v.n > 0; +} + +function count(a: Item[], f: (v: Item) => boolean) { + let c = 0; + for (let i = 0; i < a.length; i++) { + const v = a[i]; + if (f(v)) c++; + c += v.n * 10; + } + + return c; +} + +class Bag { + constructor(private data: Item[] = []) {} + + local() { + let seen = 0; + return walkLocal(this.data, (v) => { + seen++; + }) * 10 + seen; + } + + // the callbacks count into a global: a closure that captures, stored into an object, escapes + field() { + seen = 0; + const w = new Walker((v) => { + seen++; + }); + const first = w.walk(this.data); + w.use((v) => { + seen += 2; + }); + return (first + w.walk(this.data)) * 100 + seen; + } + + returned() { + const big = above(2); + let c = 0; + for (const v of this.data) if (big(v)) c++; + return count(this.data, above(0)) * 10 + c; + } +} + +function main() { + let ok = 0; + for (let i = 0; i < 300; i++) { + const b = new Bag([new Item(1), new Item(5), new Item(2)]); + if (b.local() == 83) ok++; + churn(); + if (b.field() == 1609) ok++; + churn(); + if (b.returned() == 832) ok++; + churn(); + } + + assert(ok == 300 * 3, "ok"); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_callback_field_drops.ts b/tslang/test/tester/own/own_err_callback_field_drops.ts new file mode 100644 index 000000000..75d91bf75 --- /dev/null +++ b/tslang/test/tester/own/own_err_callback_field_drops.ts @@ -0,0 +1,29 @@ +// -mm=own rejects: the function stored into `Walker`'s field (spec 55) 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(private 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 main() { + pool = [new Item(7)]; + const w = new Walker((v) => { + pool = []; + }); + w.walk([new Item(1)]); + print(pool.length); +} diff --git a/tslang/test/tester/own/own_err_callback_returned_drops.ts b/tslang/test/tester/own/own_err_callback_returned_drops.ts new file mode 100644 index 000000000..edb2e47ae --- /dev/null +++ b/tslang/test/tester/own/own_err_callback_returned_drops.ts @@ -0,0 +1,30 @@ +// -mm=own rejects: one of the functions `pick` returns (spec 56) replaces a global array, releasing +// what it held. `walk` calls what `pick` returned 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[] = []; + +function pick(clear: boolean) { + return clear + ? (v: Item) => { + pool = []; + } + : (v: Item) => {}; +} + +function walk(a: Item[], f: (v: Item) => void) { + for (let i = 0; i < a.length; i++) { + const v = a[i]; + f(v); + print(v.n); + } +} + +function main() { + pool = [new Item(7)]; + walk([new Item(1)], pick(true)); + print(pool.length); +}