Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions docs/own-memory-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>` class holds values, strings or `Shared<T>` handles, as `Map` does: `new
Array<number>()` works, and `new Array<Node>()` is a compile error that points at `Shared<T>`.
`Array<string>` and `Array<Shared<T>>` 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<T>` 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<T>`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <RangeError>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<T>`) was `takes a second
reference`: MLIRGen makes a parameter with a default `HasValue(p) ? Value(p) : <default>`, 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.
102 changes: 101 additions & 1 deletion tslang/lib/TypeScript/OwnershipFacts.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<mlir_ts::OptionalUndefOp>(def))
// ...and so does `ts.Undef` of an optional: the argument left out for a parameter with a default
if (mlir::isa<mlir_ts::OptionalUndefOp>(def) ||
(mlir::isa<mlir_ts::UndefOp>(def) && mlir::isa<mlir_ts::OptionalType>(def->getResult(0).getType())))
{
return true;
}
Expand Down Expand Up @@ -1275,6 +1277,33 @@ inline bool isLoadedCell(mlir::Value value)
return loadOp && mlir::isa<mlir_ts::RefType>(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<mlir_ts::LoadOp>();
auto varOp = loadOp ? loadOp.getReference().getDefiningOp<mlir_ts::VariableOp>() : 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<mlir_ts::SaveCatchVarOp>(user))
{
saved = true;
}
else if (!mlir::isa<mlir_ts::LoadOp, mlir_ts::DebugVariableOp>(user))
{
return false;
}
}

return saved;
}

// A captured variable's cell, in the function that declared it.
inline bool isCellVariable(mlir::Value value)
{
Expand Down Expand Up @@ -1309,6 +1338,35 @@ inline bool isParameterSlot(mlir_ts::VariableOp varOp, int &index)
{
auto init = varOp.getInitializer();
auto argument = init ? mlir::dyn_cast<mlir::BlockArgument>(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<mlir::Value> 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<mlir::BlockArgument>(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<mlir_ts::FuncOp>(argument.getOwner()->getParentOp()) || isOwningVariable(varOp) ||
varOp.getCaptured().value_or(false))
Expand All @@ -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<mlir_ts::VariableOp, 1> parameterSlotsOf(mlir::BlockArgument argument)
{
llvm::SmallVector<mlir_ts::VariableOp, 1> slots;
auto add = [&](mlir::Operation *user) {
auto varOp = mlir::dyn_cast<mlir_ts::VariableOp>(user);
auto index = -1;
if (varOp && isParameterSlot(varOp, index) && index == static_cast<int>(argument.getArgNumber()) &&
!llvm::is_contained(slots, varOp))
{
slots.push_back(varOp);
}
};

for (auto *user : argument.getUsers())
{
add(user);
if (!mlir::isa<mlir_ts::ValueOp>(user))
{
continue;
}

for (auto &use : user->getResult(0).getUses())
{
auto branchOp = mlir::dyn_cast<mlir::BranchOpInterface>(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.
Expand Down
19 changes: 19 additions & 0 deletions tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<mlir_ts::DebugVariableOp>(user))
{
continue;
}

auto readOp = mlir::dyn_cast<mlir_ts::LoadOp>(user);
if (!readOp)
{
Expand Down Expand Up @@ -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<mlir_ts::ExtractPropertyOp>(user))
{
auto field = user->getResult(0);
if (mayPointInto(field) && seen.insert(field).second)
{
values.push_back({field, true, pending.kills});
}

continue;
}

llvm::SmallVector<mlir::Value> same;
llvm::SmallVector<mlir::Operation *, 2> renewedAt;
if (passesOn(use, same, &renewedAt))
Expand Down
13 changes: 2 additions & 11 deletions tslang/lib/TypeScript/OwnershipInferenceKeptParams.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,8 @@ namespace own_inference
return {};
}

for (auto *user : body.front().getArgument(index).getUsers())
{
auto varOp = mlir::dyn_cast<mlir_ts::VariableOp>(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)
Expand Down
8 changes: 8 additions & 0 deletions tslang/lib/TypeScript/OwnershipInferenceMoves.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<mlir_ts::ArrayPushOp>(user);
if (mlir::isa<mlir_ts::CreateArrayOp>(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<mlir_ts::VariableOp>(user))
Expand Down
7 changes: 6 additions & 1 deletion tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
18 changes: 2 additions & 16 deletions tslang/lib/TypeScript/OwnershipSignaturePass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<mlir_ts::VariableOp>(user);
auto index = -1;
if (!varOp || !isParameterSlot(varOp, index))
{
continue;
}

for (auto *slotUser : varOp.getResult().getUsers())
{
if (auto loadOp = mlir::dyn_cast<mlir_ts::LoadOp>(slotUser))
Expand Down Expand Up @@ -892,15 +885,8 @@ namespace own_signature
{
// the statements that keep the parameter, as decideParam sees a move
llvm::SmallVector<mlir::Operation *> takers;
for (auto *user : body.front().getArgument(index).getUsers())
for (auto varOp : parameterSlotsOf(body.front().getArgument(index)))
{
auto varOp = mlir::dyn_cast<mlir_ts::VariableOp>(user);
auto position = -1;
if (!varOp || !isParameterSlot(varOp, position))
{
continue;
}

for (auto *slotUser : varOp.getResult().getUsers())
{
if (auto loadOp = mlir::dyn_cast<mlir_ts::LoadOp>(slotUser))
Expand Down
Loading
Loading