diff --git a/docs/own-memory-model.md b/docs/own-memory-model.md index db16814b9..c04af4b8a 100644 --- a/docs/own-memory-model.md +++ b/docs/own-memory-model.md @@ -742,9 +742,14 @@ a reference gives it back on every path out of a function. - Array methods that keep elements of the source in a new array (`filter` over class instances) give each such element a second owner, which is an error by design. Use `Shared` elements, or build the result from copies. -- `sort`, `reverse` and `copyWithin` work on arrays of numbers and strings (a string is copied - where it moves). Over class instances they are an error: a swap reads an element into a local - while its slot is overwritten, which -mm=own cannot follow yet; use `Shared` elements. +- `reverse` and `sort` work on arrays of numbers, strings and class instances. They exchange + elements with `[a[i], a[j]] = [a[j], a[i]]`, which moves nothing; swap two elements or two fields + the same way in your own code. A swap through a local (`let t = a[i]; a[i] = a[j]; a[j] = t`) + gives an object in `t` a second owner, which is an error. A callback your function only calls, + tests or hands on to another function (as `sort` hands its comparison to a helper) is known by + the functions its callers pass; one stored, captured or returned is not. `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 as in JavaScript: `const seq = fibonacci().map(f)`. Over an iterator that borrows its container (`s.values()` of a `Map` or a `Set`) a helper is bounded by the container too: 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 7e98fbe33..ca4129cb9 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 @@ -4675,3 +4675,105 @@ needs an exchange of two slots the analysis sees as moving nothing. return `this`, and a read returned directly and through a local, over strings built at run time, churning between steps, under own, rc, gc and none; AOT under own, 1000, 200000 and 400000 iterations peak at 4.0, 4.0 and 4.4 MB (a returned read copied would leak). + +## 42. An exchange of two places (`[p, q] = [q, p]`), 2026-10-27 + +The swap that `sort` and `reverse` over class instances need (§41, not there). A swap through a +local, `let v = a[i]; a[i] = a[j]; a[j] = v`, holds one element in `v` while its slot is +overwritten: for a string `v` is a copy, for an object a second owner, and the reads stored back +were "borrows an element and cannot be stored". Recognising the three stores in the inference +would mean proving, after the affine lowering and CSE, that nothing between them reads, writes or +releases either place. JavaScript has a statement for the exchange, and MLIRGen now gives it an op +of its own. + +- **`ts.Swap %first, %second`.** Two references of one type; each place takes the other's value. + Every value keeps exactly one place, so it is emitted with no `ts.Retain`, `ts.ReleaseSlot` or + drop in any model, and rc's counts are what they were. Both operands read and write memory, so + nothing folds or hoists it. The LLVM lowering loads both values before it stores either: a + place swapped with itself (`i == j`) keeps its value. +- **MLIRGen.** An expression statement `[p, q] = [q, p]` (`discardedAssignments`: the array the + assignment would give as its value is never built) whose two sides name the same two places + (`isSamePlaceExpression`: names, `this`, numbers, members and elements of those, `+ - *` and + unary `-` of those - nothing that runs code, so evaluating each target once is evaluating it + twice) and whose targets are each an element or a field of one type (`swappablePlace`: a + `ts.Load` through a `ts.ElementRef` or a `ts.PropertyRef`) is a `ts.Swap`. Anything else - a + local, a global, a captured variable, an accessor, an interface's optional field, places of two + types, the assignment's value used - takes the path it took before. +- **The inference.** A swap is one of the function's drops: it frees nothing, but a borrow read + out of either place reads a block the other place holds now, under another root perhaps, which + may end first (`[a[0], b[0]] = [b[0], a[0]]`, then `b` released). `dropsChain` drops a chain + through either place as an overwrite of it would (`overwritesChain`, an element by its type, + a field by its position). +- **The signature pass.** A swap through a place that reaches outside the function is an + overwrite of both places for its callers: `dropsInBody`, and `fieldDropsInBody` names the two. + Without it a function that only swaps would be `__own_no_drops`, and a caller's borrow of an + element would outlive the call that moved it. + +`own_swap_elements` reverses and sorts a `C[]` with generic functions written as the default +library's, swaps an element with itself, two fields, and elements of two arrays, and reverses and +sorts strings built at run time, churning between steps, under own, rc, gc and none; own emits no +counting. Negatives: `own_err_swap_borrow_used_after` (an element read before a swap, used after +it) and `own_err_swap_call_borrow_used_after` (the same across a call that swaps). + +DefaultLib: `reverse` and `selectionSort` exchange elements with `[a[i], a[j]] = [a[j], a[i]]`; +under own a string element is no longer copied out and back, and `reverse` over class instances +compiles and runs (`array_reverse_class`). + +Not here: `sort` over class instances. It gives its optional `callbackfn` on to `selectionSort`, +and a callback given on through a parameter is in no closed family (`collectCallbackFamilies` takes +only a function passed by name; `onlyCalledParam` refuses a parameter that is tested or passed on), +so `compare(a[i], a[j])` is a call that may release the two elements it is given - an error the +swap does not reach (`array_sort_class`, `// not own`). The same helper called with a function by +name compiles. §43 adds it. And `copyWithin` over class instances, which would leave one object in +two elements - an error by design. + +## 43. A callback given on through a parameter, 2026-10-27 + +`sort` over class instances (§42, not there). DefaultLib's `sort(this: T[], callbackfn?)` tests its +optional comparison and gives it on to `selectionSort(a, compare)`, which calls `compare(a[i], +a[j])`. A call through a parameter knows its callee's facts only from the family of functions its +callers pass (§33), and `collectCallbackFamilies` took a member only from a function passed by +name: `selectionSort`'s `compare` was given `sort`'s parameter, its family was open, and the call +was one that may release the two elements it is given. `sort`'s own `callbackfn` had no family +either, since `onlyCalledParam` refused a parameter that is tested or passed on, and an optional +function was not a function type. + +- **A parameter given on.** A callback parameter's type is a function value or an optional one. + Each read of its slot may be the callee of a call, tested (`ts.HasValue`, a `ts.Cast` to + boolean), unwrapped (`ts.Value`, a cast between function types) and then any of these, or one + argument of a call (`forwards`; never the callee and an argument of one call, never two + arguments). A forward names the parameter of the one function its call reaches (`parameterOf`: + a direct callee, or a method no other overrides, as `passesToClosed` resolved it). +- **Its family.** For each call of the function, the argument for the parameter is a function by + name (`passedFunction`, now through `ts.OptionalValue` as well), nothing (`ts.Undef` or + `ts.OptionalUndef` of an optional type: an optional callback not given, which passes no function), + or the caller's own callback parameter given on (`givenParam`: its slot's read, unwrapped, made + optional or cast) - a source: the functions passed for that parameter are passed for this one. + Anything else opens the family, as before. +- **The fixpoint.** The loop that opens a family whose member escapes now also opens one with a + source or a forward that is not a closed family, and merges a closed source's members (with + their shifts; a member at two shifts opens it) and its passing symbols into it, until nothing + changes. A family given on is so closed only when every family it is given from and to is: the + calls it gives its members to must resolve to the same functions. A function given to itself + (`eachDown(a, i - 1, f)`) is its own source. `passedOnly` follows a member's symbol through + `ts.OptionalValue`, so the function passed to `sort` does not escape where it is made optional. +- **An optional callback called.** A call whose callee is a `ts.Value` of a read of a callback + parameter's slot (`if (compare) { if (compare(a, b) > 0) ... }`) is resolved through the + parameter's family, as one through the read itself (`resolveCallback`). +- The families then meet facts as §33's do: `selectionSort`'s `compare` reaches `sort`'s default + comparison and every function passed to `sort`, and a call through it knows what each releases. + +`own_callback_forwarded` sorts an `Item[]` through an optional comparison given on (and with none +given), gives a callback on twice (`eachTwice` to `each`) and to the function itself, and calls +an optional comparison once tested, churning between steps, under own, rc, gc and none; own emits no counting. Negatives: +`own_err_callback_forwarded_drops` (the function passed to `sortIt` replaces a global array, so +`selectionSort`'s call with two elements is a use of them after they may be released) and +`own_err_callback_forwarded_keeps` (it keeps its argument in a global array, for good, which a call +through a value cannot make immortal). + +DefaultLib: `array_sort_class` compiles and runs under own, and so does `const_array_mutation`, +which sorts numbers with a comparison `sort` gives on. + +Not here: a callback stored, captured or returned is not given on, and keeps its family open. So +is one in `compare && compare(a, b)`: the `&&` is a `boolean | function` union that holds the +callback on the path that does not call it. diff --git a/tslang/include/TypeScript/TypeScriptOps.td b/tslang/include/TypeScript/TypeScriptOps.td index 5168f5176..37bb7874c 100644 --- a/tslang/include/TypeScript/TypeScriptOps.td +++ b/tslang/include/TypeScript/TypeScriptOps.td @@ -1349,7 +1349,21 @@ def TypeScript_StoreOp : TypeScript_Op<"Store", []> { }]; let arguments = (ins AnyType:$value, Arg:$reference); - + +} + +def TypeScript_SwapOp : TypeScript_Op<"Swap", [AllTypesMatch<["first", "second"]>]> { + let summary = "exchanges the values of two places"; + let description = [{ + `[p, q] = [q, p]` of two elements or fields: each place takes the other's value. Every value + keeps exactly one place, so nothing is retained, released or dropped, in any memory model + (own spec 42). + }]; + + let arguments = (ins + Arg:$first, + Arg:$second + ); } def TypeScript_LoadOp : TypeScript_Op<"Load", []> { diff --git a/tslang/lib/TypeScript/LowerToAffineLoops.cpp b/tslang/lib/TypeScript/LowerToAffineLoops.cpp index 70c91d423..e14606be7 100644 --- a/tslang/lib/TypeScript/LowerToAffineLoops.cpp +++ b/tslang/lib/TypeScript/LowerToAffineLoops.cpp @@ -2932,7 +2932,7 @@ void AddTsAffineLegalOps(ConversionTarget &target) mlir_ts::AddressOfOp, mlir_ts::ArithmeticBinaryOp, mlir_ts::ArithmeticUnaryOp, mlir_ts::AssertOp, mlir_ts::CastOp, mlir_ts::ConstantOp, mlir_ts::ElementRefOp, mlir_ts::PointerOffsetRefOp, mlir_ts::FuncOp, mlir_ts::GlobalOp, mlir_ts::GlobalResultOp, mlir_ts::DefaultOp, mlir_ts::HasValueOp, mlir_ts::ValueOp, mlir_ts::ValueOrDefaultOp, mlir_ts::NullOp, mlir_ts::ParseFloatOp, mlir_ts::ParseIntOp, mlir_ts::IsNaNOp, - mlir_ts::PrintOp, mlir_ts::ConvertFOp, mlir_ts::SizeOfOp, mlir_ts::TypeDescriptorOp, mlir_ts::RetainOp, mlir_ts::ReleaseOp, mlir_ts::RetainSlotOp, mlir_ts::ReleaseSlotOp, mlir_ts::RetainCellOp, mlir_ts::ReleaseCellOp, mlir_ts::StoreOp, mlir_ts::SymbolRefOp, mlir_ts::LengthOfOp, mlir_ts::SetLengthOfOp, + mlir_ts::PrintOp, mlir_ts::ConvertFOp, mlir_ts::SizeOfOp, mlir_ts::TypeDescriptorOp, mlir_ts::RetainOp, mlir_ts::ReleaseOp, mlir_ts::RetainSlotOp, mlir_ts::ReleaseSlotOp, mlir_ts::RetainCellOp, mlir_ts::ReleaseCellOp, mlir_ts::StoreOp, mlir_ts::SwapOp, mlir_ts::SymbolRefOp, mlir_ts::LengthOfOp, mlir_ts::SetLengthOfOp, mlir_ts::StringLengthOp, mlir_ts::StringResizeOp, mlir_ts::StringConcatOp, mlir_ts::StringCopyOp, mlir_ts::SharedNewOp, mlir_ts::SharedValueRefOp, mlir_ts::SharedCountOp, mlir_ts::StringCompareOp, mlir_ts::AnyCompareOp, mlir_ts::LoadOp, mlir_ts::LoadSaveOp, mlir_ts::NewOp, mlir_ts::CreateTupleOp, mlir_ts::DeconstructTupleOp, mlir_ts::CreateArrayOp, mlir_ts::NewEmptyArrayOp, mlir_ts::NewArrayOp, mlir_ts::DeleteOp, mlir_ts::PropertyRefOp, mlir_ts::InsertPropertyOp, diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index 3a6182d8f..4a40194db 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -5139,6 +5139,26 @@ struct StoreOpLowering : public TsLlvmPattern } }; +// Both values are read before either place is written, so a place swapped with itself keeps its +// value. The loads and stores are the dialect's own, lowered as any other. +struct SwapOpLowering : public TsLlvmPattern +{ + using TsLlvmPattern::TsLlvmPattern; + + LogicalResult matchAndRewrite(mlir_ts::SwapOp swapOp, Adaptor transformed, + ConversionPatternRewriter &rewriter) const final + { + auto loc = swapOp->getLoc(); + auto valueType = cast(swapOp.getFirst().getType()).getElementType(); + auto first = rewriter.create(loc, valueType, swapOp.getFirst()); + auto second = rewriter.create(loc, valueType, swapOp.getSecond()); + rewriter.create(loc, second, swapOp.getFirst()); + rewriter.create(loc, first, swapOp.getSecond()); + rewriter.eraseOp(swapOp); + return success(); + } +}; + struct ElementRefOpLowering : public TsLlvmPattern { using TsLlvmPattern::TsLlvmPattern; @@ -9151,7 +9171,7 @@ void TypeScriptToLLVMLoweringPass::runOnOperation() PointerOffsetRefOpLowering, LogicalBinaryOpLowering, NullOpLowering, NewOpLowering, CreateTupleOpLowering, DeconstructTupleOpLowering, CreateArrayOpLowering, NewEmptyArrayOpLowering, NewArrayOpLowering, ArrayPushOpLowering, ArrayPopOpLowering, ArrayUnshiftOpLowering, ArrayShiftOpLowering, ArraySpliceOpLowering, ArrayViewOpLowering, DeleteOpLowering, - ParseFloatOpLowering, ParseIntOpLowering, IsNaNOpLowering, PrintOpLowering, ConvertFOpLowering, StoreOpLowering, SizeOfOpLowering, TypeDescriptorOpLowering, RetainOpLowering, ReleaseOpLowering, RetainSlotOpLowering, ReleaseSlotOpLowering, RetainCellOpLowering, ReleaseCellOpLowering, ReleaseContentsOpLowering, MakeImmortalOpLowering, MarkImmortalCellOpLowering, + ParseFloatOpLowering, ParseIntOpLowering, IsNaNOpLowering, PrintOpLowering, ConvertFOpLowering, StoreOpLowering, SwapOpLowering, SizeOfOpLowering, TypeDescriptorOpLowering, RetainOpLowering, ReleaseOpLowering, RetainSlotOpLowering, ReleaseSlotOpLowering, RetainCellOpLowering, ReleaseCellOpLowering, ReleaseContentsOpLowering, MakeImmortalOpLowering, MarkImmortalCellOpLowering, InsertPropertyOpLowering, LengthOfOpLowering, SetLengthOfOpLowering, StringLengthOpLowering, StringResizeOpLowering, StringConcatOpLowering, StringCopyOpLowering, SharedNewOpLowering, SharedValueRefOpLowering, SharedCountOpLowering, StringCompareOpLowering, AnyCompareOpLowering, CharToStringOpLowering, UndefOpLowering, CopyStructOpLowering, MemoryCopyOpLowering, MemoryMoveOpLowering, diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index f703543d9..b7329ce57 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -7019,6 +7019,21 @@ class MLIRGenImpl if (leftExpression == SyntaxKind::ArrayLiteralExpression) { + if (discardedAssignments.contains(binaryExpressionAST.operator->())) + { + auto swapped = false; + if (mlir::failed(mlirGenSwap(location, leftExpression.as(), rightExpression, + swapped, genContext))) + { + return mlir::failure(); + } + + if (swapped) + { + return mlir::success(); + } + } + return mlirGenSaveLogicArray(location, leftExpression.as(), rightExpression, genContext); } @@ -7061,6 +7076,163 @@ class MLIRGenImpl return mlirGenSaveLogicOneItem(location, leftExpressionValue, rightExpressionValue, genContext); } + // The same place named twice: each part is a name, `this`, a number, a member or an element of + // one, or arithmetic of those - nothing that runs code or changes anything, so evaluating it once + // is evaluating it twice. + bool isSamePlaceExpression(Expression left, Expression right) + { + while (left == SyntaxKind::ParenthesizedExpression) + { + left = left.as()->expression; + } + + while (right == SyntaxKind::ParenthesizedExpression) + { + right = right.as()->expression; + } + + if (left == SyntaxKind::Identifier) + { + return right == SyntaxKind::Identifier && + left.as()->escapedText == right.as()->escapedText; + } + + if (left == SyntaxKind::ThisKeyword) + { + return right == SyntaxKind::ThisKeyword; + } + + if (left == SyntaxKind::NumericLiteral) + { + return right == SyntaxKind::NumericLiteral && left.as()->text == right.as()->text; + } + + if (left == SyntaxKind::PropertyAccessExpression) + { + if (right != SyntaxKind::PropertyAccessExpression) + { + return false; + } + + auto leftAccess = left.as(); + auto rightAccess = right.as(); + return !leftAccess->questionDotToken && !rightAccess->questionDotToken && + leftAccess->name == SyntaxKind::Identifier && rightAccess->name == SyntaxKind::Identifier && + leftAccess->name.as()->escapedText == rightAccess->name.as()->escapedText && + isSamePlaceExpression(leftAccess->expression, rightAccess->expression); + } + + if (left == SyntaxKind::ElementAccessExpression) + { + if (right != SyntaxKind::ElementAccessExpression) + { + return false; + } + + auto leftAccess = left.as(); + auto rightAccess = right.as(); + return !leftAccess->questionDotToken && !rightAccess->questionDotToken && + isSamePlaceExpression(leftAccess->expression, rightAccess->expression) && + isSamePlaceExpression(leftAccess->argumentExpression, rightAccess->argumentExpression); + } + + if (left == SyntaxKind::BinaryExpression) + { + if (right != SyntaxKind::BinaryExpression) + { + return false; + } + + auto leftBinary = left.as(); + auto rightBinary = right.as(); + auto op = (SyntaxKind)leftBinary->operatorToken; + return (op == SyntaxKind::PlusToken || op == SyntaxKind::MinusToken || op == SyntaxKind::AsteriskToken) && + op == (SyntaxKind)rightBinary->operatorToken && + isSamePlaceExpression(leftBinary->left, rightBinary->left) && + isSamePlaceExpression(leftBinary->right, rightBinary->right); + } + + if (left == SyntaxKind::PrefixUnaryExpression) + { + if (right != SyntaxKind::PrefixUnaryExpression) + { + return false; + } + + auto leftUnary = left.as(); + auto rightUnary = right.as(); + return leftUnary->_operator == SyntaxKind::MinusToken && rightUnary->_operator == SyntaxKind::MinusToken && + isSamePlaceExpression(leftUnary->operand, rightUnary->operand); + } + + return false; + } + + // The element or field a target names, when it is one a store writes as is: not a local, a + // global or a captured variable, whose slot the ownership inference follows by its stores, nor + // an interface's optional field, whose store sets a flag. + mlir::Value swappablePlace(mlir::Value targetValue) + { + auto loadOp = targetValue.getDefiningOp(); + if (!loadOp || loadOp->hasAttr(ATOMIC_ATTR_NAME) || loadOp->hasAttr(VOLATILE_ATTR_NAME)) + { + return mlir::Value(); + } + + auto reference = loadOp.getReference(); + if (!isa(reference.getType()) || + !isa_and_nonnull(reference.getDefiningOp())) + { + return mlir::Value(); + } + + return reference; + } + + // `[p, q] = [q, p]` of two elements or fields: an exchange, which moves nothing. Built through + // an array literal, each value would be read out of its place (under -mm=own a borrow, which + // may not be stored) and the literal would own a second reference to it. Only a statement's, + // whose value nothing reads: the swap builds no array to give. `swapped` stays false when the + // assignment is not such a swap. + mlir::LogicalResult mlirGenSwap(mlir::Location location, ArrayLiteralExpression arrayLiteralExpression, + Expression rightExpression, bool &swapped, const GenContext &genContext) + { + while (rightExpression == SyntaxKind::ParenthesizedExpression) + { + rightExpression = rightExpression.as()->expression; + } + + if (rightExpression != SyntaxKind::ArrayLiteralExpression) + { + return mlir::success(); + } + + auto &targets = arrayLiteralExpression->elements; + auto &sources = rightExpression.as()->elements; + if (targets.size() != 2 || sources.size() != 2 || !isSamePlaceExpression(targets[0], sources[1]) || + !isSamePlaceExpression(targets[1], sources[0])) + { + return mlir::success(); + } + + auto first = mlirGen(targets[0], genContext); + EXIT_IF_FAILED_OR_NO_VALUE(first) + auto second = mlirGen(targets[1], genContext); + EXIT_IF_FAILED_OR_NO_VALUE(second) + + auto firstPlace = swappablePlace(V(first)); + auto secondPlace = swappablePlace(V(second)); + if (!firstPlace || !secondPlace || firstPlace.getType() != secondPlace.getType()) + { + // the reads are dead: the assignment reads the places again + return mlir::success(); + } + + builder.create(location, firstPlace, secondPlace); + swapped = true; + return mlir::success(); + } + ValueOrLogicalResult mlirGenSaveLogicArray(mlir::Location location, ArrayLiteralExpression arrayLiteralExpression, Expression rightExpression, const GenContext &genContext) { @@ -16037,6 +16209,10 @@ class MLIRGenImpl // SupportedYieldScope. llvm::DenseMap supportedYields; + // The destructuring assignments a statement is made of, whose value nothing reads: such a + // `[p, q] = [q, p]` may be a swap (mlirGenSwap), which builds no array to give as its value. + llvm::DenseSet discardedAssignments; + private: std::string label; // `a: b: for (...)`: the outer labels of a chain that ends in a loop, each mapped to the innermost diff --git a/tslang/lib/TypeScript/MLIRGenStatements.cpp b/tslang/lib/TypeScript/MLIRGenStatements.cpp index a55bbb2a0..faaa8db1b 100644 --- a/tslang/lib/TypeScript/MLIRGenStatements.cpp +++ b/tslang/lib/TypeScript/MLIRGenStatements.cpp @@ -448,6 +448,7 @@ namespace mlirgen expression = expression.as()->expression; } + const void *discarded = nullptr; if (expression == SyntaxKind::BinaryExpression) { auto binaryExpression = expression.as(); @@ -456,9 +457,21 @@ namespace mlirgen { supportedYieldScope.mark(binaryExpression->right, true); } + + if (binaryExpression->operatorToken == SyntaxKind::EqualsToken + && binaryExpression->left == SyntaxKind::ArrayLiteralExpression) + { + discarded = expression.operator->(); + discardedAssignments.insert(discarded); + } } auto result = mlirGen(expressionStatementAST->expression, genContext); + if (discarded) + { + discardedAssignments.erase(discarded); + } + EXIT_IF_FAILED(result) return mlir::success(); } diff --git a/tslang/lib/TypeScript/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h index 6ba35f4d8..787f31d86 100644 --- a/tslang/lib/TypeScript/OwnershipFacts.h +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -684,6 +684,19 @@ inline bool sameSlot(mlir::Operation *a, mlir::Operation *b) return globalA && globalB && globalA.getGlobalName() == globalB.getGlobalName(); } +// A place a `ts.Swap` exchanges, as an overwrite of it names it: a field's position, or -1 for an +// element (spec 42). +inline int64_t swappedPosition(mlir::Value place) +{ + int64_t position = -1; + if (auto propertyRefOp = place.getDefiningOp()) + { + position = propertyRefOp.getPosition(); + } + + return position; +} + // The storage an op gives a new value to or releases: a store, `ts.ReleaseSlot`. inline mlir::Operation *slotWritten(mlir::Operation *op) { diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index 013099ca5..cb9edec2d 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -250,6 +250,11 @@ namespace own_inference drops.push_back(op); } } + else if (mlir::isa(op)) + { + // an exchange of two places, which moves their values (spec 42) + drops.push_back(op); + } else if (mlir::isa(op) || isCall(op)) diff --git a/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp b/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp index c01cd4519..bbe704af0 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp @@ -604,6 +604,15 @@ namespace own_inference return true; } + // An exchange frees nothing, but what a borrow of either place reads is the other place's + // now, under another root perhaps, which may end first (spec 42) + if (auto swapOp = mlir::dyn_cast(drop)) + { + return llvm::any_of(swapOp->getOperands(), [&](mlir::Value place) { + return overwritesChain(swappedPosition(place), place.getType(), chain); + }); + } + if (auto releaseSlotOp = mlir::dyn_cast(drop)) { // An assignment to a captured variable destroys its old value, which is reached only diff --git a/tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp b/tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp index 72b3c2687..f4b2c5b46 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp @@ -8,12 +8,118 @@ namespace own_signature { - bool OwnershipSignaturePass::isFunctionValueType(mlir::Type type) + static bool isFunctionValue(mlir::Type type) { return mlir::isa(type); } - bool OwnershipSignaturePass::onlyCalledParam(mlir::BlockArgument argument) + bool OwnershipSignaturePass::isFunctionValueType(mlir::Type type) + { + return isFunctionValue(type); + } + + // A callback parameter's type: a function value, or an optional one + static bool isCallbackType(mlir::Type type) + { + if (auto optionalType = mlir::dyn_cast(type)) + { + type = optionalType.getElementType(); + } + + return isFunctionValue(type); + } + + // 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`) + static bool onlyCalledValue(mlir::Value value, llvm::SmallVectorImpl &forwards, int depth = 0) + { + if (depth > 8) + { + return false; + } + + for (auto &use : value.getUses()) + { + auto *user = use.getOwner(); + if (mlir::isa(user)) + { + continue; + } + + if (auto castOp = mlir::dyn_cast(user)) + { + if (mlir::isa(castOp.getType())) + { + continue; + } + + if (!isFunctionValue(castOp.getType()) || + !onlyCalledValue(castOp.getResult(), forwards, depth + 1)) + { + return false; + } + + continue; + } + + if (auto valueOp = mlir::dyn_cast(user)) + { + if (!onlyCalledValue(valueOp.getResult(), forwards, depth + 1)) + { + return false; + } + + 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) + { + return false; + } + + if (!called) + { + forwards.push_back(&use); + } + } + + return true; + } + + // 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) + { + 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 + { + auto loadOp = mlir::dyn_cast_or_null(def); + auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + return varOp && isParameterSlot(varOp, index) ? varOp->getParentOfType() : mlir_ts::FuncOp(); + } + } + + return {}; + } + + bool OwnershipSignaturePass::onlyCalledParam(mlir::BlockArgument argument, llvm::SmallVectorImpl &forwards) { for (auto *user : argument.getUsers()) { @@ -32,20 +138,10 @@ namespace own_signature } auto loadOp = mlir::dyn_cast(slotUser); - if (!loadOp) + if (!loadOp || !onlyCalledValue(loadOp.getResult(), forwards)) { return false; } - - for (auto &use : loadOp->getUses()) - { - auto *call = use.getOwner(); - if (!isCall(call) || calleeValue(call) != use.get() || - llvm::is_contained(callArgs(call), use.get())) - { - return false; - } - } } } @@ -54,6 +150,11 @@ namespace own_signature mlir_ts::SymbolRefOp OwnershipSignaturePass::passedFunction(mlir::Value value, int32_t &shift) { + if (auto optionalOp = value.getDefiningOp()) + { + value = optionalOp.getIn(); + } + while (auto castOp = value.getDefiningOp()) { if (!isFunctionValueType(castOp.getIn().getType()) || !isFunctionValueType(castOp.getType())) @@ -96,12 +197,18 @@ namespace own_signature } auto castOp = mlir::dyn_cast(user); + auto optionalOp = mlir::dyn_cast(user); auto boundOp = mlir::dyn_cast(user); if (castOp && isFunctionValueType(castOp.getType()) && passedOnly(castOp.getResult(), passes, depth + 1)) { continue; } + if (optionalOp && passedOnly(optionalOp.getResult(), passes, depth + 1)) + { + continue; + } + if (boundOp && boundOp.getFunc() == use.get() && closureOfBox(boundOp.getThisVal()) == boundOp && passedOnly(boundOp.getResult(), passes, depth + 1)) { @@ -250,6 +357,27 @@ namespace own_signature llvm::all_of(users, [&](mlir::Operation *user) { return callsOf(funcOp, user, calls); }); }; + // The parameter of the one function a call reaches that `use` is the argument for, by its + // 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(); + if (!isCall(call)) + { + return {nullptr, -1}; + } + + auto callee = directCallee(call); + auto calleeOp = callee ? functions.lookup(callee.getValue()) : methodCalled(call); + auto args = callArgs(call); + if (!calleeOp || args.empty() || use.getOperandNumber() < args.getBeginOperandIndex() || + use.getOperandNumber() >= args.getBeginOperandIndex() + args.size()) + { + return {nullptr, -1}; + } + + return {calleeOp, static_cast(use.getOperandNumber() - args.getBeginOperandIndex())}; + }; + for (auto &entry : functions) { auto funcOp = entry.second; @@ -261,19 +389,50 @@ namespace own_signature for (auto argument : funcOp.getBody().front().getArguments()) { - if (!isFunctionValueType(argument.getType()) || !onlyCalledParam(argument)) + llvm::SmallVector forwards; + if (!isCallbackType(argument.getType()) || !onlyCalledParam(argument, forwards)) { continue; } auto index = static_cast(argument.getArgNumber()); CallbackFamily family; + for (auto *use : forwards) + { + auto target = parameterOf(*use); + family.closed = family.closed && target.first; + family.forwards.push_back(target); + } + for (auto *user : calls) { + if (!family.closed) + { + break; + } + auto args = callArgs(user); + auto arg = static_cast(index) < args.size() ? args[index] : mlir::Value(); auto shift = 0; - auto refOp = static_cast(index) < args.size() ? passedFunction(args[index], shift) - : mlir_ts::SymbolRefOp(); + 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)) { @@ -303,27 +462,61 @@ namespace own_signature // 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: + // the functions passed for those are its own, and the calls it gives them to must resolve. // Each family that has such a member is open, until nothing changes. + auto closedFamily = [&](std::pair key) -> CallbackFamily * { + auto found = callbackFamilies.find(key); + return found != callbackFamilies.end() && found->second.closed ? &found->second : nullptr; + }; + auto passesToClosed = [&](mlir::OpOperand &use) { - auto *call = use.getOwner(); - auto callee = directCallee(call); - auto calleeOp = callee ? functions.lookup(callee.getValue()) : (isCall(call) ? methodCalled(call) : mlir_ts::FuncOp()); - if (!calleeOp) - { - return false; - } + auto key = parameterOf(use); + return key.first && closedFamily(key); + }; - // the argument this use is, by its operand's position - auto args = callArgs(call); - if (args.empty() || use.getOperandNumber() < args.getBeginOperandIndex() || - use.getOperandNumber() >= args.getBeginOperandIndex() + args.size()) + // Takes in the members of the parameters given here: false when one is not closed, or gives + // a member with another shift. + auto mergeSources = [&](CallbackFamily &family, bool &changed) { + for (auto source : family.sources) { - return false; + auto *from = closedFamily(source); + if (!from) + { + return false; + } + + if (from == &family) + { + continue; + } + + for (auto &member : from->members) + { + auto same = llvm::find_if(family.members, [&](const CallbackMember &known) { return known.symbol == member.symbol; }); + if (same != family.members.end() && same->shift != member.shift) + { + return false; + } + + if (same == family.members.end()) + { + family.members.push_back(member); + changed = true; + } + } + + for (auto *refOp : from->passing) + { + if (!llvm::is_contained(family.passing, refOp)) + { + family.passing.push_back(refOp); + changed = true; + } + } } - auto index = static_cast(use.getOperandNumber() - args.getBeginOperandIndex()); - auto found = callbackFamilies.find({calleeOp, index}); - return found != callbackFamilies.end() && found->second.closed; + return true; }; for (auto changed = true; changed;) @@ -336,6 +529,14 @@ namespace own_signature continue; } + if (!mergeSources(family, changed) || + llvm::any_of(family.forwards, [&](auto target) { return !closedFamily(target); })) + { + family.closed = false; + changed = true; + continue; + } + for (auto &member : family.members) { auto memberOp = functions.lookup(member.symbol.getValue()); diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp index d7c2a24c8..e180d1ad9 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp @@ -1379,6 +1379,17 @@ namespace own_signature drops.add({position, slot.getType()}); } } + else if (auto swapOp = mlir::dyn_cast(op)) + { + // an exchange drops a borrow of either place as an overwrite of it would (spec 42) + for (auto place : swapOp->getOperands()) + { + if (reachesOutside(place, funcOp)) + { + drops.add({swappedPosition(place), place.getType()}); + } + } + } else if (mlir::isa(op)) { if (reachesOutside(op->getOperand(0), funcOp)) @@ -1579,6 +1590,11 @@ namespace own_signature { drops = reachesOutside(op->getOperand(0), funcOp); } + else if (auto swapOp = mlir::dyn_cast(op)) + { + // the caller's borrow of a place it exchanges reads another place's value (spec 42) + drops = reachesOutside(swapOp.getFirst(), funcOp) || reachesOutside(swapOp.getSecond(), funcOp); + } else if (mlir::isa(op)) { // through a handle, it may move the elements another handle's borrow reads (spec 23.3) diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.h b/tslang/lib/TypeScript/OwnershipSignaturePass.h index bd11d8a03..0a25bd580 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.h +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.h @@ -812,6 +812,10 @@ class OwnershipSignaturePass : public mlir::PassWrapper members; // the ops that name a member's symbol to pass it here llvm::SmallVector passing; + // the parameters of its callers they give on here (their members are this one's too), and the + // parameters of the calls it gives its own on to: each must be closed for this one to be + llvm::SmallVector, 1> sources; + llvm::SmallVector, 1> forwards; bool closed = true; }; @@ -825,15 +829,17 @@ class OwnershipSignaturePass : public mlir::PassWrapper &forwards); // The function a caller passes as `value`: its symbol, seen as a function value through casts - // between function types, or a closure over a capture box built of it. Null for anything else. + // between function types and made optional, or a closure over a capture box built of it. Null + // for anything else. static mlir_ts::SymbolRefOp passedFunction(mlir::Value value, int32_t &shift); // Does the function value `value` go only where `passes` allows - as an argument of a call - and - // through casts between function types, a closure built of it, and that closure's own retains + // through casts between function types, made optional, a closure built of it, and that closure's own retains // and releases? static bool passedOnly(mlir::Value value, llvm::function_ref passes, int depth = 0); diff --git a/tslang/lib/TypeScript/OwnershipSignatureResolve.cpp b/tslang/lib/TypeScript/OwnershipSignatureResolve.cpp index 6eb51c1fe..5f36e14d8 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureResolve.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureResolve.cpp @@ -319,6 +319,13 @@ namespace own_signature resolveCallback(loadOp, 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); + return; + } else if (auto boundOp = mlir::dyn_cast_or_null(def)) { // a function-typed field of an interface, called: `ts.CreateBoundFunction( diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 51234eeeb..193160d9d 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -3938,7 +3938,8 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o own_generator_method_this own_string_copy_call own_param_kept_some_paths own_method_callbacks own_loop_read_again own_length_drops_elements own_tuple_param_kept own_for_of_borrowed_iterator own_generator_keeps own_generator_keeps_bounded - own_generator_this_param own_copy_within_returned_param) + own_generator_this_param own_copy_within_returned_param own_swap_elements + own_callback_forwarded) 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. @@ -3967,10 +3968,11 @@ endforeach() # A generator whose `@keeps` makes its state own its arguments (spec 38), and one given a generator # that borrows, and one over its `this` parameter; strings copied within a parameter the result borrows; -# the same programs under every other model. +# elements and fields swapped; a callback given on through a parameter; the same programs under every +# other model. foreach(keeps_mm rc gc none) foreach(keeps_test own_generator_keeps own_generator_keeps_bounded own_generator_this_param - own_copy_within_returned_param) + own_copy_within_returned_param own_swap_elements own_callback_forwarded) 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() @@ -4645,7 +4647,11 @@ set(own_error_cases "own_err_generator_keeps_used_after|:15:11: error: '[^']*' is used here after its value was moved" "own_err_generator_keeps_bounded|:14:17: error: '[^']*' borrows its generator's arguments and cannot be stored, returned or captured" "own_err_generator_keeps_bounded_outlives|:20:11: error: '[^']*' borrows '[^']*' but is used here after '[^']*' is released or overwritten" - "own_err_generator_this_param_returned|:11:26: error: '[^']*' borrows its generator's arguments and cannot be stored, returned or captured") + "own_err_generator_this_param_returned|:11:26: error: '[^']*' borrows its generator's arguments and cannot be stored, returned or captured" + "own_err_swap_borrow_used_after|:11:11: error: '[^']*' borrows an element but is used here after it may be released or overwritten" + "own_err_swap_call_borrow_used_after|:15:11: error: '[^']*' borrows an element but is used here after it may be released or overwritten" + "own_err_callback_forwarded_drops|:15:17: error: 'this value' borrows an element but is used here after it may be released or overwritten" + "own_err_callback_forwarded_keeps|:32:18: error: '[^']*' keeps '#0' for good, but -mm=own cannot make every caller make it immortal: the function is called through a value") 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_forwarded.ts b/tslang/test/tester/own/own_callback_forwarded.ts new file mode 100644 index 000000000..d9bfe5f0a --- /dev/null +++ b/tslang/test/tester/own/own_callback_forwarded.ts @@ -0,0 +1,123 @@ +// A callback given on through a parameter (spec 43): the functions passed for the parameter it is +// given from are passed for the one it is given to, so a call through it - with two elements of an +// array the callback only reads - knows it releases nothing. An optional callback is tested for +// being given, and one not given passes nothing. Given on once, twice, and to the function itself; +// an optional one called itself. +// Each case churns between steps with blocks the size of the ones read, so a block freed by mistake +// is reused. +class Item { + v: number[] = []; + constructor(public key: int) {} +} + +function churn() { + const junk: Item[] = []; + for (let i = 0; i < 64; i++) { + const c = new Item(999); + c.v.push(99); + junk.push(c); + } +} + +function made(n: int): Item[] { + const r: Item[] = []; + for (let i = n - 1; i >= 0; i--) { + const c = new Item(i); + c.v.push(i); + r.push(c); + } + + return r; +} + +function selectionSort(a: T[], compare: (x: T, y: T) => int) { + const len = a.length; + for (let i = 0; i < len - 1; ++i) { + for (let j = i + 1; j < len; ++j) { + if (compare(a[i], a[j]) > 0) { + [a[i], a[j]] = [a[j], a[i]]; + } + } + } +} + +// as DefaultLib's sort: an optional comparison, given on when given +function sortIt(this: T[], callbackfn?: (x: T, y: T) => int) { + if (callbackfn) { + selectionSort(this, callbackfn); + } else { + selectionSort(this, (a: T, b: T) => 0); + } +} + +function each(a: T[], f: (v: T) => void) { + for (let i = 0; i < a.length; i++) { + f(a[i]); + } +} + +function eachTwice(a: T[], f: (v: T) => void) { + each(a, f); + each(a, f); +} + +function eachDown(a: T[], i: int, f: (v: T) => void) { + if (i < 0) { + return; + } + + f(a[i]); + eachDown(a, i - 1, f); +} + +// an optional callback called itself, once tested +function countDescents(a: T[], compare?: (x: T, y: T) => int) { + let n = 0; + for (let i = 0; i < a.length - 1; ++i) { + if (compare) { + if (compare(a[i], a[i + 1]) > 0) n++; + } + } + + return n; +} + +let total: number = 0; + +function add(c: Item) { + total += c.key + c.v[0]; +} + +function main() { + let ok = 0; + for (let round = 0; round < 1000; round++) { + const a = made(5); + a.sortIt((p: Item, q: Item) => p.key - q.key); + churn(); + if (a[0].key == 0 && a[2].key == 2 && a[4].key == 4 && a[4].v[0] == 4) ok++; + + const b = made(3); + b.sortIt(); + churn(); + if (b[0].key == 2 && b[2].key == 0) ok++; + + total = 0; + const c = made(4); + eachTwice(c, add); + churn(); + if (total == 24 && c[3].v[0] == 0) ok++; + + total = 0; + eachDown(c, 3, add); + churn(); + if (total == 12 && c[0].key == 3) ok++; + + const d = made(4); + const descents = countDescents(d, (p: Item, q: Item) => p.key - q.key) + countDescents(d); + churn(); + if (descents == 3 && d[3].v[0] == 0) ok++; + } + + assert(ok == 1000 * 5, "ok"); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_callback_forwarded_drops.ts b/tslang/test/tester/own/own_err_callback_forwarded_drops.ts new file mode 100644 index 000000000..50e0bf6a4 --- /dev/null +++ b/tslang/test/tester/own/own_err_callback_forwarded_drops.ts @@ -0,0 +1,38 @@ +// -mm=own rejects: `sortIt` gives its comparison on to `selectionSort` (spec 43), so the function +// passed to `sortIt` is the one `selectionSort` calls - and it replaces a global array, releasing +// what it held. `selectionSort` calls it with two elements of an array that may be that global, and +// reads them again after the call. +class Item { + constructor(public key: int) {} +} + +let pool: Item[] = []; + +function selectionSort(a: T[], compare: (x: T, y: T) => int) { + const len = a.length; + for (let i = 0; i < len - 1; ++i) { + for (let j = i + 1; j < len; ++j) { + if (compare(a[i], a[j]) > 0) { + [a[i], a[j]] = [a[j], a[i]]; + } + } + } +} + +function sortIt(this: T[], callbackfn?: (x: T, y: T) => int) { + if (callbackfn) { + selectionSort(this, callbackfn); + } else { + selectionSort(this, (a: T, b: T) => 0); + } +} + +function main() { + const items = [new Item(3), new Item(1), new Item(2)]; + pool = [new Item(7)]; + items.sortIt((p: Item, q: Item) => { + pool = []; + return p.key - q.key; + }); + print(items[0].key, pool.length); +} diff --git a/tslang/test/tester/own/own_err_callback_forwarded_keeps.ts b/tslang/test/tester/own/own_err_callback_forwarded_keeps.ts new file mode 100644 index 000000000..c1e126467 --- /dev/null +++ b/tslang/test/tester/own/own_err_callback_forwarded_keeps.ts @@ -0,0 +1,37 @@ +// -mm=own rejects: `sortIt` gives its comparison on to `selectionSort` (spec 43), so the function +// passed to `sortIt` is the one `selectionSort` calls - and it keeps its first argument for good, in +// a global array. Its callers must make that argument immortal, and `selectionSort` calls it through +// a value, which cannot. +class Item { + constructor(public key: int) {} +} + +let kept: Item[] = []; + +function selectionSort(a: T[], compare: (x: T, y: T) => int) { + const len = a.length; + for (let i = 0; i < len - 1; ++i) { + for (let j = i + 1; j < len; ++j) { + if (compare(a[i], a[j]) > 0) { + [a[i], a[j]] = [a[j], a[i]]; + } + } + } +} + +function sortIt(this: T[], callbackfn?: (x: T, y: T) => int) { + if (callbackfn) { + selectionSort(this, callbackfn); + } else { + selectionSort(this, (a: T, b: T) => 0); + } +} + +function main() { + const items = [new Item(3), new Item(1), new Item(2)]; + items.sortIt((p: Item, q: Item) => { + kept.push(p); + return p.key - q.key; + }); + print(items[0].key, kept.length); +} diff --git a/tslang/test/tester/own/own_err_swap_borrow_used_after.ts b/tslang/test/tester/own/own_err_swap_borrow_used_after.ts new file mode 100644 index 000000000..160e7d983 --- /dev/null +++ b/tslang/test/tester/own/own_err_swap_borrow_used_after.ts @@ -0,0 +1,12 @@ +// -mm=own rejects: an element read before a swap of its array's elements, used after it. The swap +// frees nothing, but what was read is held by another place now, which may end first (spec 42). +class C { + constructor(public x: number) {} +} + +function main() { + const a = [new C(1), new C(2)]; + const first = a[0]; + [a[0], a[1]] = [a[1], a[0]]; + print(first.x); +} diff --git a/tslang/test/tester/own/own_err_swap_call_borrow_used_after.ts b/tslang/test/tester/own/own_err_swap_call_borrow_used_after.ts new file mode 100644 index 000000000..39f8811c4 --- /dev/null +++ b/tslang/test/tester/own/own_err_swap_call_borrow_used_after.ts @@ -0,0 +1,16 @@ +// -mm=own rejects: an element read before a call that swaps its array's elements, used after the +// call. The callee frees nothing, but it moves what was read to another place (spec 42). +class C { + constructor(public x: number) {} +} + +function swapFirst(a: C[]) { + [a[0], a[1]] = [a[1], a[0]]; +} + +function main() { + const a = [new C(1), new C(2)]; + const first = a[0]; + swapFirst(a); + print(first.x); +} diff --git a/tslang/test/tester/own/own_swap_elements.ts b/tslang/test/tester/own/own_swap_elements.ts new file mode 100644 index 000000000..88473ad79 --- /dev/null +++ b/tslang/test/tester/own/own_swap_elements.ts @@ -0,0 +1,130 @@ +// `[p, q] = [q, p]` of two elements or fields is an exchange (spec 42): each value keeps exactly one +// place, so nothing is retained, released or dropped, and under -mm=own an array of class instances +// can be reversed and sorted in place - through a local, `let v = a[i]`, the swap would make `v` a +// second owner. Reversed and sorted generically too, over strings built at run time. Each case churns +// between steps with blocks the size of the ones swapped, so a block freed by mistake is reused. +class C { + v: number[] = []; + constructor(public x: number) {} +} + +class Pair { + constructor(public a: C, public b: C) {} +} + +function churn() { + const junk: C[] = []; + for (let i = 0; i < 64; i++) { + const c = new C(999); + c.v.push(99); + junk.push(c); + } +} + +function made(n: int): C[] { + const r: C[] = []; + for (let i = 0; i < n; i++) { + const c = new C(i); + c.v.push(i); + r.push(c); + } + + return r; +} + +function strings(n: int): string[] { + const r: string[] = []; + for (let i = 0; i < n; i++) r.push("s" + i); + return r; +} + +function reverseAll(this: T[]) { + const shift = this.length - 1; + const size = this.length >> 1; + for (let i = 0; i < size; i++) { + [this[i], this[shift - i]] = [this[shift - i], this[i]]; + } + + return this; +} + +function selectionSort(a: T[], compare: (value1: T, value2: T) => int) { + const len = a.length; + for (let i = 0; i < len - 1; ++i) { + for (let j = i + 1; j < len; ++j) { + if (compare(a[i], a[j]) > 0) { + [a[i], a[j]] = [a[j], a[i]]; + } + } + } +} + +function byX(p: C, q: C): int { + return p.x == q.x ? 0 : p.x > q.x ? 1 : -1; +} + +function byText(p: string, q: string): int { + return p == q ? 0 : p > q ? 1 : -1; +} + +function inOrder(a: C[], first: number, step: number) { + for (let i = 0; i < a.length; i++) { + if (a[i].x != first + i * step || a[i].v[0] != a[i].x) return false; + } + + return true; +} + +function main() { + let ok = 0; + for (let i = 0; i < 1000; i++) { + // reversed in place, an odd and an even length; the result is the array it was given + const a = made(5); + const same = a.reverseAll(); + churn(); + if (inOrder(same, 4, -1)) ok++; + + const b = made(4); + b.reverseAll(); + churn(); + if (inOrder(b, 3, -1)) ok++; + + // sorted with a comparison + const c = made(6); + c.reverseAll(); + [c[1], c[4]] = [c[4], c[1]]; + selectionSort(c, byX); + churn(); + if (inOrder(c, 0, 1)) ok++; + + // a place swapped with itself keeps its value + const k = 2; + [c[k], c[2]] = [c[2], c[k]]; + churn(); + if (inOrder(c, 0, 1)) ok++; + + // two fields + const p = new Pair(new C(1), new C(2)); + [p.a, p.b] = [p.b, p.a]; + churn(); + if (p.a.x == 2 && p.b.x == 1) ok++; + + // elements of two arrays, each of which then frees what it holds + const d = made(2); + const e = made(3); + [d[0], e[2]] = [e[2], d[0]]; + churn(); + if (d[0].x == 2 && e[2].x == 0 && d[1].x == 1) ok++; + + // strings, generically + const s = strings(5); + s.reverseAll(); + churn(); + selectionSort(s, byText); + churn(); + if (s[0] == "s0" && s[2] == "s2" && s[4] == "s4") ok++; + } + + assert(ok == 1000 * 7, "ok"); + print("done."); +}