diff --git a/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp b/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp index e649eaf56..5ef623697 100644 --- a/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp +++ b/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp @@ -1656,8 +1656,35 @@ class OwnedReturnConsumptionPass (mlir::isa_and_nonnull(slot) && slot->hasAttr(OWNED_LOCAL_ATTR_NAME))); }; + // ...or some branch yields a record literal it built whose field holds a temporary it made + // (`c ? { start: this.pos, dir: new V(1) } : ...`): the branch gives the temporary back before + // it ends, so the receiver's retain after the `ts.If` - which retains the fields - comes too + // late, and the field is freed under it. The branch retains it first. Not under -mm=own, + // which settles a literal's fields itself. + auto yieldsLiteralOfBranchTemporary = [&](mlir_ts::ResultOp resultOp) { + auto loadOp = producerOf(resultOp->getOperand(0)).getDefiningOp(); + auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + if (compileOptions.memoryModel == MemoryModelOwn || !varOp || + varOp->getParentRegion() != resultOp->getParentRegion()) + { + return false; + } + + return llvm::any_of(varOp->getUsers(), [&](mlir::Operation *user) { + auto propertyRefOp = mlir::dyn_cast(user); + return propertyRefOp && llvm::any_of(propertyRefOp->getUsers(), [&](mlir::Operation *fieldUser) { + auto storeOp = mlir::dyn_cast(fieldUser); + auto stored = storeOp && storeOp.getReference() == propertyRefOp.getResult() + ? producerOf(storeOp.getValue()) + : mlir::Value(); + return stored && stored.getDefiningOp() && ::typescript::mayTakeOverReference(stored) && + stored.getDefiningOp()->getParentRegion() == resultOp->getParentRegion(); + }); + }); + }; + if (llvm::none_of(yields, carriesOwned) && llvm::none_of(yields, borrowsBranchTemporary) && - llvm::none_of(yields, readsNamedObjectField)) + llvm::none_of(yields, readsNamedObjectField) && llvm::none_of(yields, yieldsLiteralOfBranchTemporary)) { return; } diff --git a/tslang/lib/TypeScript/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h index 23a6c9f5c..dbfe3b070 100644 --- a/tslang/lib/TypeScript/OwnershipFacts.h +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -70,6 +70,11 @@ constexpr int64_t OWN_INTERFACE_FIELD_POSITION = -3; // The position of a captured variable's cell holding a string, assigned: the old string goes, and a // string owns nothing else, so only a borrow that is a string may be it (spec 52) constexpr int64_t OWN_CAPTURED_STRING_POSITION = -4; +// The position of a captured variable's cell holding a block (an object, an array), assigned: the old +// value goes with all it owns, and only the cell owns it, so only a borrow read through a captured +// variable's cell may be under it. A call given anything read through a variable a closure assigns +// is an error where it is made (checkArgumentsAssigned), so no parameter of the callee is under it. +constexpr int64_t OWN_CAPTURED_CELL_POSITION = -5; // On a function whose body relies on a fact its callers cannot all know (a parameter it keeps, a // result that borrows): why, for the error the body gets instead. #define OWN_FACTS_LOST_ATTR_NAME "__own_facts_lost" diff --git a/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp b/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp index 151c8e412..bbced418e 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp @@ -29,6 +29,13 @@ namespace own_inference } } + // what a view of it shows (`f(n, ...)` with `n` given as an interface): the value read + for (auto depth = 0; depth < 8 && isView(argument.getDefiningOp()) && !argument.getDefiningOp(); + depth++) + { + argument = argument.getDefiningOp()->getOperand(0); + } + auto *read = argument.getDefiningOp(); auto local = mlir::dyn_cast_or_null(slotOf(argument)); if (!read || !local || !ownsHeap(argument) || isHandle(argument)) @@ -48,12 +55,31 @@ namespace own_inference return (storeOp && storeOp.getReference() == local.getResult()) || (releaseSlotOp && releaseSlotOp.getSlot() == local.getResult()); }); + // ...or the call itself, when it may run a closure that gives a captured object a new value + // (OWN_CAPTURED_CELL_POSITION): its callee's borrow of the argument would not see that + auto places = callDropFields(call); + if (!write && assignedByClosure && places && llvm::any_of(*places, [](auto &place) { + return place.first == OWN_CAPTURED_CELL_POSITION; + })) + { + write = call; + } + if (!write) { continue; } - if (reporting()) + if (reporting() && write == call) + { + auto name = varName(local); + auto callee = calleeName(call); + call->emitError("'") << name << "' is lent to '" << (callee.empty() ? "a function" : callee) + << "', which may run a closure that gives '" << name + << "' a new value; -mm=own cannot prove it outlives the call"; + signalPassFailure(); + } + else if (reporting()) { auto name = varName(local); auto callee = calleeName(call); diff --git a/tslang/lib/TypeScript/OwnershipInferenceClosures.cpp b/tslang/lib/TypeScript/OwnershipInferenceClosures.cpp index ac842603b..e87d8bcb3 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceClosures.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceClosures.cpp @@ -525,6 +525,18 @@ namespace own_inference return; } + // a callee that may overwrite an element (`p[0] = v`) gives back the old one, which is not + // the array's to give: the borrowed value is freed under its owner, and the new one is + // lost with the array's shallow release. So is a callee that may destroy anything + auto elementSlot = mlir_ts::RefType::get(mlir::cast(result.getType()).getElementType()); + auto places = callDropFields(call); + if (!callNoDrops(call) && (!places || llvm::any_of(*places, [&](auto &place) { + return place.first == -1 && place.second == elementSlot; + }))) + { + return; + } + // rc's references for the elements go: the array borrows them, for as long as the call setAttrTracked(arrayOp, OWN_BORROWING_ARRAY_ATTR_NAME); fieldBorrowRetains.insert(retains.begin(), retains.end()); diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index 80dd0ab57..f5389d21f 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -56,7 +56,8 @@ namespace own_inference } // Does nothing `value` gives - its own value, a field read out of it, a call's result that may be it - leave - // `block`? Then the last of them that uses it is collected into `last`. A scalar is not followed: it is a copy. + // `block` (the function, when null)? Then the last of them that uses it is collected into `last` (in `block` + // only). A scalar is not followed: it is a copy. static bool staysInBlock(mlir::Value value, mlir::Block *block, mlir::Operation *&last, int depth = 0) { auto isScalar = [](mlir::Type type) { @@ -75,12 +76,12 @@ namespace own_inference for (auto *user : value.getUsers()) { - if (user->getBlock() != block) + if (block && user->getBlock() != block) { return false; } - if (!last || last->isBeforeInBlock(user)) + if (block && (!last || last->isBeforeInBlock(user))) { last = user; } @@ -95,12 +96,12 @@ namespace own_inference { for (auto *refUser : user->getResult(0).getUsers()) { - if (refUser->getBlock() != block) + if (block && refUser->getBlock() != block) { return false; } - if (last->isBeforeInBlock(refUser)) + if (block && last->isBeforeInBlock(refUser)) { last = refUser; } @@ -115,10 +116,17 @@ namespace own_inference continue; } - // an argument: the result may be it, or borrowed from it - auto callIndirectOp = mlir::dyn_cast(user); - if ((mlir::isa(user) || callIndirectOp) && - (!callIndirectOp || callIndirectOp.getCallee() != value)) + // a method of it (`i.item.get()`): the method bound to it, its object and its function, which only + // the call below uses + if (mlir::isa(user) && + user->getNumResults() == 1 && follow(user->getResult(0))) + { + continue; + } + + // an argument, or the method called: the result may be it, or borrowed from it + if (mlir::isa(user)) { for (auto result : user->getResults()) { @@ -161,18 +169,117 @@ namespace own_inference continue; } + mlir::OpBuilder builder(loadOp->getContext()); + auto release = [&](mlir::Operation *at) { + auto releaseOp = builder.create(loadOp.getLoc(), loadOp.getResult()); + releaseOp->setAttr(OWN_CELL_READ_ATTR_NAME, builder.getUnitAttr()); + }; + mlir::Operation *last = nullptr; - if (!staysInBlock(loadOp.getResult(), loadOp->getBlock(), last) || !last || - last->hasTrait()) + if (staysInBlock(loadOp.getResult(), loadOp->getBlock(), last) && last && + !last->hasTrait()) { + builder.setInsertionPointAfter(last); + release(last); continue; } - mlir::OpBuilder builder(last->getContext()); - builder.setInsertionPointAfter(last); - auto releaseOp = builder.create(loadOp.getLoc(), loadOp.getResult()); - releaseOp->setAttr(OWN_CELL_READ_ATTR_NAME, builder.getUnitAttr()); + // ...or put in a `let` (`let x = i.item`) that is only read, across blocks too, and nothing read + // out of it leaves the function: freed at the function's returns, when the read reaches them all + // and the object it reads through is a parameter's, which outlives them + auto exits = localHeldToExits(loadOp); + for (auto *exit : exits) + { + builder.setInsertionPoint(exit); + release(exit); + } + + // ...else a getter's new value would be lost: returned, stored, or kept where nothing frees it. Only + // a field some class of this module fills with accessors has one; its value cannot leave its read + auto refOp = loadOp.getReference().getDefiningOp(); + if (!exits.empty() || !refOp) + { + continue; + } + + auto fieldAttr = mlir::dyn_cast(refOp.getIdentifier()); + auto filler = fieldAttr ? accessorFiller(fieldAttr.getValue()) : llvm::StringRef(); + if (!filler.empty()) + { + loadOp.emitError("'") + << fieldAttr.getValue() << "' is read through an interface, and '" << filler + << "' fills it with a getter, whose new value -mm=own frees only where it is read: it cannot " + "be returned, kept or outlive its block; read the field where it is used"; + signalPassFailure(); + } + } + } + + llvm::StringRef OwnershipInferencePass::accessorFiller(llvm::StringRef field) + { + // the cell of a class's accessors for an interface's field: `...vtbl..accessor` + auto suffix = (llvm::Twine(".") + field + ".accessor").str(); + auto module = getFunction()->getParentOfType(); + llvm::StringRef found; + module.walk([&](mlir_ts::GlobalOp globalOp) { + auto name = globalOp.getSymName(); + auto vtbl = name.find("..vtbl."); + if (vtbl != llvm::StringRef::npos && name.ends_with(suffix) && vtbl + 6 + suffix.size() == name.size()) + { + // the class: up to the interface's name + auto head = name.take_front(vtbl); + found = head.take_front(head.rfind('.') == llvm::StringRef::npos ? head.size() : head.rfind('.')); + return mlir::WalkResult::interrupt(); + } + + return mlir::WalkResult::advance(); + }); + + return found; + } + + llvm::SmallVector OwnershipInferencePass::localHeldToExits(mlir_ts::LoadOp read) + { + auto result = read.getResult(); + auto varOp = result.hasOneUse() ? mlir::dyn_cast(*result.getUsers().begin()) : mlir_ts::VariableOp(); + if (!varOp || varOp.getInitializer() != result || varOp.getCaptured().value_or(false)) + { + return {}; + } + + // only read, and what is read stays in the function + for (auto *user : varOp->getUsers()) + { + mlir::Operation *ignored = nullptr; + auto slotRead = mlir::dyn_cast(user); + if (!mlir::isa(user) && + (!slotRead || !staysInBlock(slotRead.getResult(), /*block=*/nullptr, ignored))) + { + return {}; + } + } + + // read through a parameter: `i.item` of an interface parameter `i` + auto refOp = read.getReference().getDefiningOp(); + auto objectRead = refOp ? refOp->getOperand(0).getDefiningOp() : mlir_ts::LoadOp(); + auto objectSlot = objectRead ? objectRead.getReference().getDefiningOp() : mlir_ts::VariableOp(); + auto index = -1; + if (!objectSlot || !isParameterSlot(objectSlot, index)) + { + return {}; } + + // every return, after the read + auto f = getFunction(); + mlir::DominanceInfo dominanceInfo(f); + llvm::SmallVector exits; + auto all = true; + f.walk([&](mlir_ts::ReturnInternalOp returnOp) { + exits.push_back(returnOp); + all = all && dominanceInfo.properlyDominates(read.getOperation(), returnOp.getOperation()); + }); + + return all ? exits : llvm::SmallVector(); } bool OwnershipInferencePass::analyze(llvm::SetVector &toErase) diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.h b/tslang/lib/TypeScript/OwnershipInferencePass.h index 9c389f5bb..414e585f0 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.h +++ b/tslang/lib/TypeScript/OwnershipInferencePass.h @@ -84,6 +84,11 @@ class OwnershipInferencePass // what is borrowed from it stay in its block: a release after their last use, for the getter's path only. One // nothing uses - what an assignment `i.f = v` leaves of the place it wrote - goes. void releaseCellReads(); + // The returns of the function, where a read through a parameter's interface field put in a `let` that is + // only read is released (releaseCellReads); none when the read does not reach them all or may leave. + llvm::SmallVector localHeldToExits(mlir_ts::LoadOp read); + // The class of this module that fills an interface's field of this name with accessors, if any. + llvm::StringRef accessorFiller(llvm::StringRef field); // The analysis of one function; true when it passed. In a dry run it erases nothing and strips // no facts. diff --git a/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp b/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp index 4c26f15ae..43f988945 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp @@ -732,6 +732,18 @@ namespace own_inference return !notString; } + // a captured object assigned frees what only its cell owns: a chain read through + // a captured variable's cell may be under it, nothing else (OWN_CAPTURED_CELL_POSITION) + if (place.first == OWN_CAPTURED_CELL_POSITION) + { + return llvm::any_of(chain.roots, [](auto &root) { + auto loadOp = root.first.template getDefiningOp(); + auto through = loadOp ? loadOp.getReference() : mlir::Value(); + return isCellVariable(root.first) || isLoadedCell(root.first) || + (through && (isCellVariable(through) || isLoadedCell(through))); + }); + } + return overwritesChain(place.first, place.second, chain); }); } diff --git a/tslang/lib/TypeScript/OwnershipInferenceRecords.cpp b/tslang/lib/TypeScript/OwnershipInferenceRecords.cpp index d708f9a71..147df9933 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceRecords.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceRecords.cpp @@ -467,7 +467,8 @@ namespace own_inference continue; } - if (auto branchOp = mlir::dyn_cast(user)) + // (not a call in a `try`, a `ts.Invoke`: a branch too, but its operands are the call's) + if (auto branchOp = mlir::dyn_cast(user); branchOp && !isCall(user)) { if (auto argument = branchOp.getSuccessorBlockArgument(use.getOperandNumber())) { @@ -519,6 +520,21 @@ namespace own_inference continue; } + // a callee that may overwrite a field of what it is given (`p.v = ...`) gives back + // the old value, which may be one the record borrows - as a write of the field here + // would + if (pending.record && !isCalleeOperand(use) && !callNoDrops(user)) + { + auto places = callDropFields(user); + if (places && llvm::any_of(*places, [](auto &place) { + return place.first >= 0 || place.first == OWN_INTERFACE_FIELD_POSITION; + })) + { + keep(user); + continue; + } + } + // a result that borrows it - a plain borrow (`id(r)`), or a borrowing record that // holds it - points where it points auto args = callArgs(user); diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp index 1fcc41db3..e56862755 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp @@ -1508,6 +1508,12 @@ namespace own_signature // a captured string assigned (`each += v` in a callback): it frees that string only drops.add({OWN_CAPTURED_STRING_POSITION, slot.getType()}); } + else if (isLoadedCell(slot) && !touchesHandle(op)) + { + // a captured object assigned (`last = new Box()` in a callback): it frees what the cell + // owns, which only a read through a captured variable may borrow + drops.add({OWN_CAPTURED_CELL_POSITION, slot.getType()}); + } else if (slot.getDefiningOp() || isLoadedCell(slot) || (!isPlace(slot) && touchesHandle(op) && !isCountedReadSlot(slot))) { diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index e31523c15..5c9b6a2b4 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -614,6 +614,7 @@ tslang_add_test(NAME test-compile-00-class-accessor-implements-field COMMAND tes tslang_add_test(NAME test-compile-00-object-literal-contextual-lambda COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_literal_contextual_lambda.ts") tslang_add_test(NAME test-compile-own-00-object-literal-contextual-lambda COMMAND test-runner -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_literal_contextual_lambda.ts") tslang_add_test(NAME test-compile-00-interface-field-getter-owned COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_field_getter_owned.ts") +tslang_add_test(NAME test-compile-00-conditional-record-literal-temporary COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00conditional_record_literal_temporary.ts") tslang_add_test(NAME test-compile-00-call-arg-param-field-global COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00call_arg_param_field_global.ts") tslang_add_test(NAME test-compile-00-class-accessor-untyped-setter COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor_untyped_setter.ts") tslang_add_test(NAME test-compile-00-class-super COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_super.ts") @@ -1254,6 +1255,7 @@ tslang_add_test(NAME test-jit-00-class-accessor-implements-field COMMAND test-ru tslang_add_test(NAME test-jit-00-object-literal-contextual-lambda COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_literal_contextual_lambda.ts") tslang_add_test(NAME test-jit-own-00-object-literal-contextual-lambda COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_literal_contextual_lambda.ts") tslang_add_test(NAME test-jit-00-interface-field-getter-owned COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_field_getter_owned.ts") +tslang_add_test(NAME test-jit-00-conditional-record-literal-temporary COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00conditional_record_literal_temporary.ts") tslang_add_test(NAME test-jit-00-call-arg-param-field-global COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00call_arg_param_field_global.ts") tslang_add_test(NAME test-jit-00-class-accessor-untyped-setter COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor_untyped_setter.ts") tslang_add_test(NAME test-jit-00-class-super COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_super.ts") @@ -2269,6 +2271,7 @@ set(TSLANG_CORPUS 00class_accessor_implements_field.ts 00object_literal_contextual_lambda.ts 00interface_field_getter_owned.ts + 00conditional_record_literal_temporary.ts 00call_arg_param_field_global.ts 00class_accessor_untyped_setter.ts 00class_accessor.ts @@ -4090,7 +4093,7 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o own_callback_shapes own_callback_extended own_callback_interface own_generic_specializations own_callback_cast own_callback_interface_slot own_equal_strings_into_any own_any_copies own_array_copies own_string_pushed_twice own_rest_takes own_shares_elements - own_rest_keeps_declared) + own_rest_keeps_declared own_interface_getter_let own_callback_assigns_capture) 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. @@ -4129,7 +4132,7 @@ foreach(keeps_mm rc gc none) own_captured_string_callback own_callback_and_call own_callback_captured own_literal_closures own_literal_variable_closures own_temporary_field_take own_union_narrowed_calls own_named_field_take own_let_field_take own_literal_captures own_literal_object_captures own_literal_interface_captures own_literal_let_captures own_interface_generic_method own_back_reference own_field_function_result own_callback_shapes own_callback_extended own_callback_interface own_generic_specializations own_callback_cast own_callback_interface_slot own_equal_strings_into_any - own_rest_keeps_declared) + own_rest_keeps_declared own_interface_getter_let own_callback_assigns_capture) 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() @@ -4195,6 +4198,13 @@ add_test(NAME test-own-caught-value-call set_tests_properties(test-own-caught-value-call PROPERTIES PASS_REGULAR_EXPRESSION "ts.Func" FAIL_REGULAR_EXPRESSION "error|Stack dump") +# A read through an interface field a getter may fill, put in a `let` that is only read, is freed at the +# function's returns under own, on the getter's path only; no run can see the leak, so the IR is read. +add_test(NAME test-own-interface-getter-let-released + COMMAND ${CMAKE_COMMAND} -DTSLANG=$ + "-DFILE=${PROJECT_SOURCE_DIR}/test/tester/own/own_interface_getter_let.ts" + -P "${CMAKE_CURRENT_SOURCE_DIR}/own-interface-getter-let.cmake") + # A rest parameter its function says it keeps (`@keeps`) is released once emptied under own, as an # inferred keep's is (spec 72); no run can see the leak, so the IR is read. add_test(NAME test-own-rest-keeps-declared-released @@ -4751,6 +4761,13 @@ set(own_error_cases "own_err_borrows_not_generator|'@borrows' of 'copyOf' names 'w', but it neither makes a generator whose state borrows its arguments nor returns one" "own_err_borrows_mixed_return|'@borrows' of 'pick' names 'w', but it neither makes a generator whose state borrows its arguments nor returns one" "own_err_marker_unknown_param|'@keeps' names 'd', which is not a parameter of this function" + "own_err_callback_assigns_receiver|:17:5: error: '[^']*' is lent to 'Bag[.]forEach', which may run a closure that gives '[^']*' a new value" + "own_err_callback_assigns_borrowed|:25:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_interface_arg_assigned_in_call|:27:11: error: '[^']*' is lent to 'f', but is given a new value before the call takes it" + "own_err_getter_read_returned|:19:12: error: 'item' is read through an interface, and 'Fresh' fills it with a getter" + "own_err_literal_element_overwritten|:20:24: error: '[^']*' borrows 'HG[.]v' and cannot be stored, returned or captured" + "own_err_record_field_overwritten|:23:11: error: '[^']*' borrows an element and cannot be stored, returned or captured" + "own_err_record_borrow_in_try|:31:14: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_marker_identifier_arg|:8:8: error: '@keeps' takes the names of parameters as strings: '@keeps[(]\"it\"[)]'" "own_err_marker_override|'Derived.set' overrides 'Base.set', and has to say the same '@keeps', '@borrows', '@lends' and '@record'" "own_err_lends_missing|'firstOf' could return a borrow or keep a parameter, but it can be called from another module. '@lends' or '@keeps' would say it in its signature" diff --git a/tslang/test/tester/own-interface-getter-let.cmake b/tslang/test/tester/own-interface-getter-let.cmake new file mode 100644 index 000000000..fb201b5fa --- /dev/null +++ b/tslang/test/tester/own-interface-getter-let.cmake @@ -0,0 +1,28 @@ +# Under own a read through an interface field put in a `let` that is only read (own_interface_getter_let.ts) is +# released at the function's returns on the getter's path (localHeldToExits). No run +# shows a leak, so the IR is read (--emit=mlir-affine): `heldInLet` releases the read before it returns. +cmake_minimum_required(VERSION 3.17.3) +execute_process(COMMAND "${TSLANG}" --emit=mlir-affine --no-default-lib -mm=own "${FILE}" + OUTPUT_VARIABLE out ERROR_VARIABLE err RESULT_VARIABLE result) +# the IR is printed to stderr +string(APPEND out "${err}") +if(NOT result EQUAL 0) + message(FATAL_ERROR "tslang failed (${result}):\n${out}") +endif() + +# the body of `heldInLet`, from its `ts.Func` to the next one +string(FIND "${out}" "ts.Func @heldInLet " begin) +if(begin EQUAL -1) + message(FATAL_ERROR "no function `heldInLet` in:\n${out}") +endif() +string(SUBSTRING "${out}" ${begin} -1 body) +string(SUBSTRING "${body}" 1 -1 rest) +string(FIND "${rest}" "ts.Func @" end) +if(NOT end EQUAL -1) + math(EXPR end "${end} + 1") + string(SUBSTRING "${body}" 0 ${end} body) +endif() + +if(NOT body MATCHES "\"ts[.]Release\"[(]%[0-9]+[)] {__own_cell_read}") + message(FATAL_ERROR "`heldInLet` does not release the read through the interface field:\n${body}") +endif() diff --git a/tslang/test/tester/own/own_callback_assigns_capture.ts b/tslang/test/tester/own/own_callback_assigns_capture.ts new file mode 100644 index 000000000..6bc6e8fa9 --- /dev/null +++ b/tslang/test/tester/own/own_callback_assigns_capture.ts @@ -0,0 +1,49 @@ +// -mm=own: a callback that gives a captured object a new value (`last = new Box(b.n)`), run by a method +// that walks the array a field holds. The assignment was a drop of anything, so the method's borrow of +// each element was "used after it may be released". It frees what only the captured variable owns +// (OWN_CAPTURED_CELL_POSITION): a borrow read through a captured variable may be under it, nothing else. +// Values are read after churn() has reused anything freed too early. The same program under every model. +class Box { + constructor(public n: number) {} +} + +class Bag { + items: Box[] = [new Box(1), new Box(2), new Box(3)]; + + forEach(f: (b: Box) => void) { + for (const b of this.items) { + f(b); + churn(); + } + } +} + +function churn() { + const junk: Box[] = []; + for (let i = 0; i < 64; i++) { + junk.push(new Box(999)); + } +} + +function main() { + const bag = new Bag(); + + let total: number = 0; + bag.forEach(b => { total = total + b.n; }); + + // the last one, replaced on each call + let last = new Box(0); + bag.forEach(b => { last = new Box(b.n * 10); }); + churn(); + + // the largest, replaced on some calls only + let top = new Box(0); + bag.forEach(b => { if (b.n > top.n) top = new Box(b.n); }); + churn(); + + assert(total == 6, "total"); + assert(last.n == 30, "last"); + assert(top.n == 3, "top"); + assert(bag.items.length == 3 && bag.items[0].n == 1 && bag.items[2].n == 3, "items kept"); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_callback_assigns_borrowed.ts b/tslang/test/tester/own/own_err_callback_assigns_borrowed.ts new file mode 100644 index 000000000..c5a6e523a --- /dev/null +++ b/tslang/test/tester/own/own_err_callback_assigns_borrowed.ts @@ -0,0 +1,26 @@ +// -mm=own rejects: `inner` borrows a field of `last`, and a callback run across the call gives `last` a +// new value, freeing the field `inner` reads afterwards. +class Box { + constructor(public n: number) {} +} + +class Pair { + inner: Box; + constructor(n: number) { this.inner = new Box(n); } +} + +class Bag { + items: Box[] = [new Box(1), new Box(2)]; + + forEach(f: (b: Box) => void) { + for (const b of this.items) f(b); + } +} + +function main() { + const bag = new Bag(); + let last = new Pair(0); + const inner = last.inner; + bag.forEach(b => { last = new Pair(b.n); }); + print(inner.n, last.inner.n); +} diff --git a/tslang/test/tester/own/own_err_callback_assigns_receiver.ts b/tslang/test/tester/own/own_err_callback_assigns_receiver.ts new file mode 100644 index 000000000..ffbc6453e --- /dev/null +++ b/tslang/test/tester/own/own_err_callback_assigns_receiver.ts @@ -0,0 +1,19 @@ +// -mm=own rejects: `bag` is lent to `forEach`, which walks the array `bag` holds while it runs a callback +// that gives `bag` a new value - freeing the array under the walk. +class Box { + constructor(public n: number) {} +} + +class Bag { + items: Box[] = [new Box(1), new Box(2)]; + + forEach(f: (b: Box) => void) { + for (const b of this.items) f(b); + } +} + +function main() { + let bag = new Bag(); + bag.forEach(b => { bag = new Bag(); }); + print(bag.items.length); +} diff --git a/tslang/test/tester/own/own_err_getter_read_returned.ts b/tslang/test/tester/own/own_err_getter_read_returned.ts new file mode 100644 index 000000000..739a39c58 --- /dev/null +++ b/tslang/test/tester/own/own_err_getter_read_returned.ts @@ -0,0 +1,25 @@ +// -mm=own rejects: a read through an interface's field that a class fills with a getter, returned. On +// the getter's path the read is a new value only the read's own release frees; returned, it would be a +// borrow of `i` to the caller, and leak (one Box per call). A field no class fills with accessors is a +// borrow on every path, and may be returned. +class Box { + constructor(public n: number) {} +} + +interface I { + item: Box; +} + +class Fresh implements I { + get item() { return new Box(1); } + set item(x: Box) {} +} + +function pick(i: I) { + return i.item; +} + +function main() { + const f: I = new Fresh(); + print(pick(f).n); +} diff --git a/tslang/test/tester/own/own_err_interface_arg_assigned_in_call.ts b/tslang/test/tester/own/own_err_interface_arg_assigned_in_call.ts new file mode 100644 index 000000000..9cdb9483e --- /dev/null +++ b/tslang/test/tester/own/own_err_interface_arg_assigned_in_call.ts @@ -0,0 +1,28 @@ +// -mm=own rejects: `n` is lent to `f` as an interface, and a later argument gives `n` a new value, +// freeing the object before the call reads it (fuzz seed 77055). Given as itself, the same program was +// rejected; seen through the interface view the check missed it, and the call read freed memory. +class V { + constructor(public x: number) {} +} + +class N { + v: V; + constructor(x: number) { this.v = new V(x); } +} + +interface IX { + v: V; +} + +function mk(i: number) { + return new N(i); +} + +function f(p: IX, q: N) { + return p.v.x + q.v.x; +} + +function main() { + let n = mk(8); + print(f(n, (n = mk(3), mk(5)))); +} diff --git a/tslang/test/tester/own/own_err_literal_element_overwritten.ts b/tslang/test/tester/own/own_err_literal_element_overwritten.ts new file mode 100644 index 000000000..1a8bdba27 --- /dev/null +++ b/tslang/test/tester/own/own_err_literal_element_overwritten.ts @@ -0,0 +1,21 @@ +// -mm=own rejects: an array literal that borrows its element (`[HG.v]`) given to a function that +// overwrites an element (`p[0] = ...`). The overwrite would free the global's value, which the array +// never owned, and the new element would be lost with the array. +class V { + constructor(public x: number) {} +} + +class H { + v: V = new V(1); +} + +const HG = new H(); + +function replaceFirst(p: V[]) { + p[0] = new V(3); + return p[0].x; +} + +function main() { + print(replaceFirst([HG.v]), HG.v.x); +} diff --git a/tslang/test/tester/own/own_err_record_borrow_in_try.ts b/tslang/test/tester/own/own_err_record_borrow_in_try.ts new file mode 100644 index 000000000..207b80db3 --- /dev/null +++ b/tslang/test/tester/own/own_err_record_borrow_in_try.ts @@ -0,0 +1,36 @@ +// -mm=own rejects: an object literal that borrows an element of `n` (`{ v: n.items[0] }`) given to a +// call whose later argument gives `n` a new value, which frees the element the literal borrows. Inside +// a `try` the calls are invokes, and the literal's use by the call was not seen. +class V { + constructor(public x: number) {} +} + +class N { + items: V[]; + constructor(x: number) { + this.items = [new V(x)]; + } +} + +interface IV { + v: V; +} + +function mk(x: number) { + return new N(x); +} + +function read(p: IV, q: N) { + return p.v.x + q.items[0].x; +} + +function main() { + let n = mk(7); + let s = 0; + try { + s += read({ v: n.items[0] }, (n = mk(1), n)); + } finally { + } + + print(s); +} diff --git a/tslang/test/tester/own/own_err_record_field_overwritten.ts b/tslang/test/tester/own/own_err_record_field_overwritten.ts new file mode 100644 index 000000000..8fac8663d --- /dev/null +++ b/tslang/test/tester/own/own_err_record_field_overwritten.ts @@ -0,0 +1,24 @@ +// -mm=own rejects: an object literal that borrows its field (`{ v: n.items[0] }`) given to a function +// that overwrites that field through its interface (`p.v = ...`). The overwrite would free the +// element, which the literal never owned. +class V { + constructor(public x: number) {} +} + +class N { + items: V[] = [new V(1)]; +} + +interface IV { + v: V; +} + +function replace(p: IV) { + p.v = new V(8); + return p.v.x; +} + +function main() { + const n = new N(); + print(replace({ v: n.items[0] }), n.items[0].x); +} diff --git a/tslang/test/tester/own/own_interface_getter_let.ts b/tslang/test/tester/own/own_interface_getter_let.ts new file mode 100644 index 000000000..57be81dec --- /dev/null +++ b/tslang/test/tester/own/own_interface_getter_let.ts @@ -0,0 +1,55 @@ +// -mm=own: a read through an interface's field that a getter may fill, put in a `let` that is only read - +// across a loop, out of the read's block - is freed at the function's returns, on the getter's path only +// (OwnershipInferencePass::localHeldToExits). It leaked before: two million calls kept two million Boxes +// (280 MB, 4.3 MB now). Through a plain field the same read is a borrow, and is not freed. Values are read +// after churn() has reused anything freed too early. +class Box { + data: number[] = [1, 2, 3]; + constructor(public n: number) {} +} + +interface I { + item: Box; +} + +class Fresh implements I { + private k = 0; + get item() { this.k = this.k + 1; return new Box(10 * this.k); } + set item(x: Box) { this.k = x.n; } +} + +class Plain implements I { + item = new Box(2); +} + +function churn() { + const junk: Box[] = []; + for (let i = 0; i < 64; i++) { + junk.push(new Box(999)); + } +} + +function heldInLet(i: I) { + let x = i.item; + let total: number = 0; + for (let k = 0; k < 3; k++) { + churn(); + total = total + x.data.length; + } + + return total + x.n; +} + +function main() { + const f: I = new Fresh(); + const p: I = new Plain(); + let s: number = 0; + for (let k = 0; k < 4; k++) { + s = s + heldInLet(f) + heldInLet(p); + churn(); + } + + assert(s == (9 + 10) + (9 + 20) + (9 + 30) + (9 + 40) + 4 * (9 + 2), "sum"); + assert(p.item.n == 2 && p.item.data.length == 3, "plain item kept"); + print("done."); +} diff --git a/tslang/test/tester/tests/00conditional_record_literal_temporary.ts b/tslang/test/tester/tests/00conditional_record_literal_temporary.ts new file mode 100644 index 000000000..e7e14603f --- /dev/null +++ b/tslang/test/tester/tests/00conditional_record_literal_temporary.ts @@ -0,0 +1,42 @@ +// A conditional between two object literals, one of which borrows a field (`{ start: this.pos, ... }`), +// each holding an object the branch makes (`new V(i)`). Under rc the branch gave its new object back +// before the receiver after the conditional retained the literal's fields, so the field was freed under +// it (heap corruption). The branch now retains the literal it yields. Each value is read after churn() +// has reused anything freed too early. +class V { + constructor(public x: number) {} +} + +interface Ray { + start: V; + dir: V; +} + +function churn() { + const keep: V[] = []; + for (let i = 0; i < 200; i++) keep.push(new V(77777)); + return keep.length; +} + +class Cam { + pos: V = new V(3); + + pick(i: number) { + const t: Ray = i == 1 ? { start: this.pos, dir: new V(i) } : { start: new V(i), dir: new V(i + 10) }; + churn(); + return t.start.x + t.dir.x; + } +} + +function main() { + const c = new Cam(); + let s = 0; + for (let i = 0; i < 4; i++) { + s += c.pick(i); + churn(); + assert(c.pos.x == 3, "pos"); + } + + assert(s == 0 + 10 + (3 + 1) + 2 + 12 + 3 + 13, "sum"); + print("done."); +}