diff --git a/docs/own-memory-model.md b/docs/own-memory-model.md index c04af4b8a..ecb353362 100644 --- a/docs/own-memory-model.md +++ b/docs/own-memory-model.md @@ -754,9 +754,15 @@ a reference gives it back on every path out of a function. 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: `for (const v of s.values().take(2))` works, but the result cannot be returned or kept past `s`. - An array's `values()`, `keys()` and `[Symbol.iterator]()` borrow it in the same way; - `entries()` does not compile yet, its `[i, a[i]]` holding a borrowed element. - A closure that captures, given to two of them, is moved into the first. + An array's `values()`, `keys()`, `entries()` and `[Symbol.iterator]()` borrow it in the same + way. A closure that captures, given to two of them, is moved into the first. A `for...of` over a + generator, or a spread of one (`[...s.matchAll(re)]`), only borrows what each step yields: keeping + a yielded object or array (pushing it into another array) is an error for now. +- The `Array` class holds values, strings or `Shared` handles, as `Map` does: `new + Array()` works, and `new Array()` is a compile error that points at `Shared`. + `Array` and `Array>` do not compile yet. +- A parameter with a default (`constructor(private items: T[] = [])`) is kept like any other + parameter, whether the caller passes it or not. - `Shared` cycles are never freed. - There are no lifetime annotations: a borrow that the compiler cannot follow is an error, even when the program would be correct. The fix is usually a move, a fresh value or `Shared`. 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 ca4129cb9..b70c097cc 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 @@ -4777,3 +4777,75 @@ 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. + +## 44. A field read out of a borrowed tuple value, and a caught value, 2026-10-27 + +Two borrows that own refused although nothing could free what they read. + +- **A field of a tuple value.** A function returning `T | undefined` of a record (`find`) gives + the caller an optional whose payload is the tuple itself, and printing it reads each field with + `ts.ExtractProperty` out of the `ts.Value` (an in-place read goes through `ts.PropertyRef` + instead). `walkBorrowed` took the extraction for a keep. A field read this way borrows what the + tuple borrows: a number or a boolean keeps nothing, and a field that may point into a block is + walked on as a borrow of the same chain, so storing it is still refused. +- **A caught value.** A catch clause's variable is filled by `ts.SaveCatchVar` with what was thrown + and assigned nowhere else (`isCaughtValue`). A throw takes what it throws, since a borrow cannot + be thrown, so the value is the exception's until the catch ends: no global and no other local + holds it. A chain rooted at a read of it is treated as one this function owns when `dropsChain` + asks whether a call may free it, so only a call given something derived from it may. Before, + `const r = e; r instanceof Error` was refused: the virtual `instanceOf` call, whose + facts own cannot know, was taken to free the exception. + +`own_optional_record_read` prints records `find` returns (found and not), and calls a method and +`instanceof` on a caught value cast to its class, under own, rc, gc and none; own emits no +counting. `own_caught_value_call` (compiled only) gives the cast caught value to a declared +function, which has no facts, and reads it after the call. Negative: `own_err_optional_record_field_kept` pushes a class-typed field of a found +record into another array. A throw of an element, a field or a reassigned global stays refused, so +nothing but the exception holds what a catch reads. DefaultLib: `array_find2`, `array_find3` and `catch_library_error` compile and run under +own. + +## 45. A parameter with a default is kept as any parameter is, 2026-10-27 + +`constructor(private data: T[] = [])` (DefaultLib's `class Array`) was `takes a second +reference`: MLIRGen makes a parameter with a default `HasValue(p) ? Value(p) : `, merged +into the local the body reads, not a local made of the argument itself, so the signature pass found +no slot of the parameter and the store into the field kept a merge it could not follow. + +- **Its slot.** `isParameterSlot` now also takes a local whose initializer is a merge of the + parameter itself (seen through `ts.Value`) and values that are fresh or hold no block - the + default made here. `parameterSlotsOf` finds such a local from the parameter (through the + `ts.Value`'s branch), and `localOwnedParams`, `takenOnEveryPath` and the inference's + `parameterSlot` use it. Everything §33 does for a kept parameter then holds: the callers move the + argument in, a path that stores the slot's value moves it out, and a path that does not releases + the slot, which holds the argument given or the default, both this function's. +- **The argument left out** is a `ts.Undef` of the optional type: `isLiteral` takes it as the empty + optional `ts.OptionalUndef` is, so a caller "moves" nothing. + +`own_param_default_kept` constructs with and without the argument, keeping it on some paths only, +and a defaulted string beside a defaulted array, churning between steps, under own, rc, gc and none; +own emits no counting. A defaulted parameter that is not kept is unchanged: its default is made and +not released, as before. + +## 46. An object literal moved into an array, 2026-10-27 + +`[{ regex: /a/, order: [1, 2, 3] }, ...]` and `a.push({ name: "f" + i, quantity: i })` were `an +object literal that holds a value it owns is moved only into a local or returned as it is`: §27 +lets `recordRetainOf` take the literal's read into the result, an owning local or a box. An element +of an array literal (`ts.CreateArray`) and an item pushed (`ts.ArrayPush`) now take it too: the +array owns the tuple and what it holds, and rc's retain of the read is where the fresh values stored +into its fields move in, each value's own check erasing its release. The read still goes to one +place only. + +`own_literal_into_array` builds arrays of literals holding a fresh string, a new object and an array +literal, by an array literal and by push, churning between steps, under own, rc, gc and none; own +emits no counting, and the array's release is the only one of what the literals hold. DefaultLib: +`regexp_parse_examples` compiles and runs under own. + +## 47. A local's record for the debugger keeps nothing, 2026-10-27 + +Under `--di`, a borrowing local (the array an inner `for...of` walks, read out of the outer one's +element) has a `ts.DebugVariable` of its slot. `collectBorrowerUses` took any use of the slot that is +not a read for a keep: `'.a' borrows an element and cannot be stored`. It now skips it, as +`isBorrow` already skips a `ts.DebugVariable` of a value. `own_for_of_nested_di` (debug build, JIT) +sums the lengths of the strings of a `string[][]`. DefaultLib: `array_concat` and `owning_copies`, +which failed under own in the debug pass only, compile and run. diff --git a/tslang/lib/TypeScript/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h index 787f31d86..f1a971d8c 100644 --- a/tslang/lib/TypeScript/OwnershipFacts.h +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -476,7 +476,9 @@ inline bool isOwningVariable(mlir_ts::VariableOp varOp) // (`u: C | undefined = undefined`), hold no block at all. inline bool isLiteral(mlir::Operation *def) { - if (mlir::isa(def)) + // ...and so does `ts.Undef` of an optional: the argument left out for a parameter with a default + if (mlir::isa(def) || + (mlir::isa(def) && mlir::isa(def->getResult(0).getType()))) { return true; } @@ -1275,6 +1277,33 @@ inline bool isLoadedCell(mlir::Value value) return loadOp && mlir::isa(loadOp.getType()); } +// A read of a catch clause's variable, which ts.SaveCatchVar fills with what was thrown and +// nothing else assigns: the exception's value, not this function's nor any global's. +inline bool isCaughtValue(mlir::Value value) +{ + auto loadOp = value.getDefiningOp(); + auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + if (!varOp || varOp.getInitializer() || varOp.getCaptured().value_or(false)) + { + return false; + } + + auto saved = false; + for (auto *user : varOp->getUsers()) + { + if (mlir::isa(user)) + { + saved = true; + } + else if (!mlir::isa(user)) + { + return false; + } + } + + return saved; +} + // A captured variable's cell, in the function that declared it. inline bool isCellVariable(mlir::Value value) { @@ -1309,6 +1338,35 @@ inline bool isParameterSlot(mlir_ts::VariableOp varOp, int &index) { auto init = varOp.getInitializer(); auto argument = init ? mlir::dyn_cast(init) : mlir::BlockArgument(); + // a parameter with a default (`d: T[] = []`): its slot holds the argument given, or the default + // made here when none was - fresh, or holding no block - so it is the parameter's as an + // undefaulted one's is + if (argument && !argument.getOwner()->isEntryBlock()) + { + llvm::SmallVector merged; + mlir::BlockArgument given; + if (!mergedInto(argument, merged)) + { + return false; + } + + // each seen through its views: the argument given is the optional parameter itself + for (auto incoming : merged) + { + auto passed = mlir::dyn_cast(incoming); + if (passed && passed.getOwner()->isEntryBlock() && (!given || given == passed)) + { + given = passed; + } + else if (!isFresh(incoming) && !holdsNoBlock(incoming)) + { + return false; + } + } + + argument = given; + } + if (!argument || !argument.getOwner()->isEntryBlock() || !mlir::isa(argument.getOwner()->getParentOp()) || isOwningVariable(varOp) || varOp.getCaptured().value_or(false)) @@ -1329,6 +1387,48 @@ inline bool isParameterSlot(mlir_ts::VariableOp varOp, int &index) return true; } +// The slots of a function's parameter (isParameterSlot): the local made of it, or, for a parameter +// with a default, the local made of the argument given or the default. +inline llvm::SmallVector parameterSlotsOf(mlir::BlockArgument argument) +{ + llvm::SmallVector slots; + auto add = [&](mlir::Operation *user) { + auto varOp = mlir::dyn_cast(user); + auto index = -1; + if (varOp && isParameterSlot(varOp, index) && index == static_cast(argument.getArgNumber()) && + !llvm::is_contained(slots, varOp)) + { + slots.push_back(varOp); + } + }; + + for (auto *user : argument.getUsers()) + { + add(user); + if (!mlir::isa(user)) + { + continue; + } + + for (auto &use : user->getResult(0).getUses()) + { + auto branchOp = mlir::dyn_cast(use.getOwner()); + auto merged = branchOp ? branchOp.getSuccessorBlockArgument(use.getOperandNumber()) : std::nullopt; + if (!merged) + { + continue; + } + + for (auto *mergedUser : merged->getUsers()) + { + add(mergedUser); + } + } + } + + return slots; +} + // The object a method call is made on: `ts.GetThis` of a method bound to it - by its class, or by an // interface value whose method it is (`i.m`: the object the interface value is a view of, spec 31). // Null otherwise. diff --git a/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp b/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp index 0b2d8324b..711afaa31 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp @@ -235,6 +235,12 @@ namespace own_inference continue; } + // `--di`'s record of the local for the debugger names it and keeps nothing + if (mlir::isa(user)) + { + continue; + } + auto readOp = mlir::dyn_cast(user); if (!readOp) { @@ -391,6 +397,19 @@ namespace own_inference continue; } + // a field read out of a tuple value (`console.log(found)` of an optional record): + // a number keeps nothing, and a field that holds a block borrows it as the tuple does + if (mlir::isa(user)) + { + auto field = user->getResult(0); + if (mayPointInto(field) && seen.insert(field).second) + { + values.push_back({field, true, pending.kills}); + } + + continue; + } + llvm::SmallVector same; llvm::SmallVector renewedAt; if (passesOn(use, same, &renewedAt)) diff --git a/tslang/lib/TypeScript/OwnershipInferenceKeptParams.cpp b/tslang/lib/TypeScript/OwnershipInferenceKeptParams.cpp index 2d7c6c060..96b627877 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceKeptParams.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceKeptParams.cpp @@ -16,17 +16,8 @@ namespace own_inference return {}; } - for (auto *user : body.front().getArgument(index).getUsers()) - { - auto varOp = mlir::dyn_cast(user); - auto position = -1; - if (varOp && isParameterSlot(varOp, position) && position == index) - { - return varOp; - } - } - - return {}; + auto slots = parameterSlotsOf(body.front().getArgument(index)); + return slots.empty() ? mlir_ts::VariableOp() : slots.front(); } bool OwnershipInferencePass::isReleasedParam(mlir::Value slot) diff --git a/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp b/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp index e0f5ae309..4047003c7 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp @@ -156,6 +156,14 @@ namespace own_inference return; } + // an element of an array literal (`[{ re: /a/, order: [1, 2] }]`), or an item pushed: the + // array owns what the record held, and rc's retain of the read is where its values move in + auto pushOp = mlir::dyn_cast(user); + if (mlir::isa(user) || (pushOp && llvm::is_contained(pushOp.getItems(), used))) + { + return; + } + // `let a = { item: new C() }`: the read is what an owning local starts with, and rc's // `ts.RetainSlot` of that local is the record's retain if (auto ownerOp = mlir::dyn_cast(user)) diff --git a/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp b/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp index bbe704af0..0aa5dc191 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp @@ -662,7 +662,12 @@ namespace own_inference // a call given a closure may run it, and its box may hold anything auto givenClosure = llvm::any_of(drop->getOperands(), holdsCaptures); auto mayDrop = true; - if (chain.owned() && !givenClosure) + // A caught value is the exception's until the catch ends: a throw takes what it throws + // (a borrow cannot be thrown), so nothing a call reaches holds it but what it is given + auto caught = llvm::all_of(chain.roots, [&](auto &root) { + return root.second != Chain::NotOwned || isCaughtValue(root.first); + }); + if (caught && !givenClosure) { auto &fromRead = derivedFrom(borrowed ? borrowed : read->getResult(0)); auto sources = rootSources(chain); diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp index e180d1ad9..c7607bed4 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp @@ -828,15 +828,8 @@ namespace own_signature } auto kept = false; - for (auto *user : argument.getUsers()) + for (auto varOp : parameterSlotsOf(argument)) { - auto varOp = mlir::dyn_cast(user); - auto index = -1; - if (!varOp || !isParameterSlot(varOp, index)) - { - continue; - } - for (auto *slotUser : varOp.getResult().getUsers()) { if (auto loadOp = mlir::dyn_cast(slotUser)) @@ -892,15 +885,8 @@ namespace own_signature { // the statements that keep the parameter, as decideParam sees a move llvm::SmallVector takers; - for (auto *user : body.front().getArgument(index).getUsers()) + for (auto varOp : parameterSlotsOf(body.front().getArgument(index))) { - auto varOp = mlir::dyn_cast(user); - auto position = -1; - if (!varOp || !isParameterSlot(varOp, position)) - { - continue; - } - for (auto *slotUser : varOp.getResult().getUsers()) { if (auto loadOp = mlir::dyn_cast(slotUser)) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 193160d9d..29e10747c 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -3939,7 +3939,7 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o 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_swap_elements - own_callback_forwarded) + own_callback_forwarded own_optional_record_read own_param_default_kept own_literal_into_array) 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. @@ -3972,7 +3972,8 @@ endforeach() # 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_swap_elements own_callback_forwarded) + own_copy_within_returned_param own_swap_elements own_callback_forwarded + own_optional_record_read own_param_default_kept own_literal_into_array) 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() @@ -4023,6 +4024,21 @@ add_test(NAME test-own-di-null-narrowing set_tests_properties(test-own-di-null-narrowing PROPERTIES PASS_REGULAR_EXPRESSION "ts.Func" FAIL_REGULAR_EXPRESSION "error|Stack dump") +# ...and a for...of over an element of the array an outer for...of walks, whose local the debugger +# records too +add_test(NAME test-own-di-for-of-nested + COMMAND $ --emit=jit --di --opt_level=0 --no-default-lib -mm=own + "${PROJECT_SOURCE_DIR}/test/tester/own/own_for_of_nested_di.ts") +set_tests_properties(test-own-di-for-of-nested PROPERTIES PASS_REGULAR_EXPRESSION "done[.]" + FAIL_REGULAR_EXPRESSION "error|Stack dump") + +# A caught value given to a call own has no facts for, and read after it (spec 44) +add_test(NAME test-own-caught-value-call + COMMAND $ --emit=mlir-affine --no-default-lib -mm=own + "${PROJECT_SOURCE_DIR}/test/tester/own/own_caught_value_call.ts") +set_tests_properties(test-own-caught-value-call PROPERTIES PASS_REGULAR_EXPRESSION "ts.Func" + FAIL_REGULAR_EXPRESSION "error|Stack dump") + # A field read through an interface is a place read under own (spec 28); the same program under # every other model. foreach(field_chain_mm rc gc none) @@ -4651,7 +4667,8 @@ set(own_error_cases "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") + "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" + "own_err_optional_record_field_kept|:15:12: error: '[^']*' borrows the result of 'findIn' and cannot be stored, returned or captured") 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_caught_value_call.ts b/tslang/test/tester/own/own_caught_value_call.ts new file mode 100644 index 000000000..a5db2789a --- /dev/null +++ b/tslang/test/tester/own/own_caught_value_call.ts @@ -0,0 +1,27 @@ +// A caught value cast to its class, given to a function own has no facts for (declared here, as a +// default library's virtual `instanceOf` is to its callers), and read after the call: the call is +// given the value, which it borrows, and nothing else holds the exception, so it cannot free it. +// Before, the call was taken to free the catch variable's value. Compiled only: nothing defines +// `checkCode`. +class Failure { + constructor(public code: int) {} +} +function fail(c: int) { + if (c > 0) throw new Failure(c); +} +declare function checkCode(f: Failure, c: int): boolean; +function caught(c: int) { + let matched = false; + try { + fail(c); + } catch (e) { + const failure = e; + matched = checkCode(failure, c); + matched = matched && failure.code == c; + } + return matched; +} +function main() { + print(caught(0)); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_optional_record_field_kept.ts b/tslang/test/tester/own/own_err_optional_record_field_kept.ts new file mode 100644 index 000000000..533c9cf4c --- /dev/null +++ b/tslang/test/tester/own/own_err_optional_record_field_kept.ts @@ -0,0 +1,17 @@ +// A field read out of the record a borrowing `T | undefined` result holds borrows what the record +// borrows: pushing a class-typed field into another array is still a second reference. +class Tag { constructor(public v: string) {} } +type Fruit = { name: string, tag: Tag }; + +function findIn(this: Fruit[], test: (f: Fruit) => boolean): Fruit | undefined { + for (const v of this) if (test(v)) return v; + return undefined; +} + +function main() { + const inventory: Fruit[] = []; + const kept: Tag[] = []; + const r = inventory.findIn((f) => f.name == "f1"); + if (r) kept.push(r.tag); + print(kept.length); +} diff --git a/tslang/test/tester/own/own_for_of_nested_di.ts b/tslang/test/tester/own/own_for_of_nested_di.ts new file mode 100644 index 000000000..a5f48d01f --- /dev/null +++ b/tslang/test/tester/own/own_for_of_nested_di.ts @@ -0,0 +1,19 @@ +// A for...of over an element read out of the array an outer for...of walks: the inner loop's array +// is a local that borrows the element. Built with debug info, the local has a record for the +// debugger of its own, which keeps nothing; it was taken for a keep ("'.a' borrows an element and +// cannot be stored"), so DefaultLib's concat failed under own with --di only. +function total(other: string[][]) { + let n = 0; + for (const item of other) { + for (const v of item) n += v.length; + } + + return n; +} + +function main() { + const y = ["a" + 1, "bc"]; + const z = ["d" + 2]; + assert(total([y, z]) == 6, "total"); + print("done."); +} diff --git a/tslang/test/tester/own/own_literal_into_array.ts b/tslang/test/tester/own/own_literal_into_array.ts new file mode 100644 index 000000000..5901b749d --- /dev/null +++ b/tslang/test/tester/own/own_literal_into_array.ts @@ -0,0 +1,28 @@ +// An object literal that owns what it holds (a fresh string, a new object, an array literal) given +// as an element of an array literal, or pushed, moves into the array as the array's element: the +// array's release frees what the literal held, and nothing else releases it. Before, only an owning +// local, the result or a box could take such a literal ("is moved only into a local or returned as +// it is"). Each step churns, so a value freed too early reads garbage; every model runs it. +class C { constructor(public x: number) {} } +function label(i: number) { return "s" + i; } +function churn() { + const junk: C[] = []; + for (let i = 0; i < 64; i++) junk.push(new C(999)); +} +function main() { + let ok = 0; + for (let i = 0; i < 1000; i++) { + const patterns = [ + { name: label(i), order: [1, 2, 3], c: new C(i) }, + { name: "b", order: [3, 1, 2], c: new C(2) }, + ]; + churn(); + const more: { name: string, order: int[] }[] = []; + for (let j = 0; j < 3; j++) more.push({ name: label(j), order: [j] }); + churn(); + if (patterns[0].name == label(i) && patterns[0].order[1] == 2 && patterns[0].c.x == i && patterns[1].c.x == 2) ok++; + if (more.length == 3 && more[2].name == "s2" && more[2].order[0] == 2) ok++; + } + assert(ok == 2000, "ok"); + print("done."); +} diff --git a/tslang/test/tester/own/own_optional_record_read.ts b/tslang/test/tester/own/own_optional_record_read.ts new file mode 100644 index 000000000..103caab15 --- /dev/null +++ b/tslang/test/tester/own/own_optional_record_read.ts @@ -0,0 +1,58 @@ +// A record found in an array is returned as `T | undefined`, a borrow of the array: printing it +// reads its fields out of the optional's tuple value, and a field that is a number keeps nothing. +// A caught value cast to its class is the exception's until the catch ends, so a call on it - a +// virtual one own has no facts for - cannot free it. +type Fruit = { name: string, quantity: int }; + +function find(this: Fruit[], test: (f: Fruit) => boolean): Fruit | undefined { + for (const v of this) if (test(v)) return v; + return undefined; +} + +function made(n: int): Fruit[] { + const r: Fruit[] = []; + for (let i = 0; i < n; i++) { + const f = { name: "f" + i, quantity: i }; + r.push(f); + } + + return r; +} + +class Failure { + constructor(public code: int) {} + + isCode(c: int) { + return this.code == c; + } +} + +function fail(c: int) { + if (c > 0) throw new Failure(c); +} + +function caughtCast(c: int) { + let matched = false; + try { + fail(c); + } catch (e) { + const failure = e; + matched = failure.isCode(c) && failure instanceof Failure; + } + + return matched; +} + +function main() { + const inventory = made(4); + const found = inventory.find((f) => f.name == "f2"); + assert(found !== undefined && found.quantity == 2, "found"); + print(found); + print(inventory.find((f) => f.quantity == 3)); + print(inventory.find((f) => f.quantity == 9)); + + assert(caughtCast(5), "caught cast"); + assert(!caughtCast(0), "nothing thrown"); + + print("done."); +} diff --git a/tslang/test/tester/own/own_param_default_kept.ts b/tslang/test/tester/own/own_param_default_kept.ts new file mode 100644 index 000000000..53533ee25 --- /dev/null +++ b/tslang/test/tester/own/own_param_default_kept.ts @@ -0,0 +1,43 @@ +// A parameter with a default (`d: C[] = []`) is kept as any parameter is: its slot holds the +// argument given, moved in by the caller, or the default made here when none was given, and a store +// of it into a field moves it out on either path. A path that does not keep it releases what the +// slot holds, whichever it is. The argument left out is `undefined`, which holds no block. Before, +// the merge of the argument and the default was "takes a second reference". Each step churns, so a +// value freed too early reads garbage; the same program runs under every model. +class C { constructor(public x: number) {} } +function label(i: number) { return "s" + i; } +function churn() { + const junk: C[] = []; + for (let i = 0; i < 64; i++) junk.push(new C(999)); +} +class A { + d: C[] = []; + constructor(d: C[] = [new C(7)], keep = true) { + if (keep) this.d = d; + } +} +class B { + constructor(private items: C[] = [], public name = label(3)) {} + first() { return this.items.length > 0 ? this.items[0].x : -1; } +} +function main() { + let ok = 0; + for (let i = 0; i < 1000; i++) { + const a = new A(); + churn(); + if (a.d[0].x == 7) ok++; + const b = new A([new C(i)]); + churn(); + if (b.d[0].x == i) ok++; + const c = new A([new C(i)], false); + const e = new A(undefined, false); + churn(); + if (c.d.length == 0 && e.d.length == 0) ok++; + const f = new B(); + const g = new B([new C(5)], label(i)); + churn(); + if (f.first() == -1 && g.first() == 5 && f.name == "s3" && g.name == label(i)) ok++; + } + assert(ok == 1000 * 4, "ok"); + print("done."); +}