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
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ Both go on a function, method or constructor, and name its parameters. `"this"`
- `@borrows("this", "expr")`: the caller owns the result, but the result may not outlive these
arguments. The result is either the state of a generator over them, or what a call with such a
bounded result returns (`return this.value.matchAll(expr)`, a "forwarder").
- `@lends("l")` (added 2026-10-20): the result is a borrow of the one argument `l`
(`__own_result_borrows`). The caller owns nothing of it, and it may not outlive `l`. It names
exactly one parameter, and it cannot be combined with `@borrows` (a result is either the
caller's or lent). It can be combined with `@keeps`.

Under gc, rc and none they mean nothing. MLIRGen still records them, and the LLVM lowering drops
the attributes.
Expand All @@ -36,8 +40,9 @@ the attributes.
(`ts.own_declared_param_names`) for the errors. A name that is not a parameter is an error. The
generator wrapper copies the method's decorators, so a marked generator's maker carries them.
- **OwnershipSignaturePass:**
- A marked function's `@keeps` facts stand even when it is open (`meetFacts`). A family does not
erase them.
- A marked function's `@keeps` and `@lends` facts stand even when it is open (`meetFacts`). A
family does not erase them. A call this pass cannot resolve gets them from its callees'
signatures (`computeFacts`).
- A call gets what its callees' signatures say, whether or not it is resolved
(`declaredByCallees`, over `Callees::named`). The named callees are the direct callee, every
member of a virtual slot's family, or a dynamic import's address global.
Expand All @@ -49,7 +54,11 @@ the attributes.
- `findBoundedForwarders` gives a marked non-generator what its returned bounded calls borrow,
mapped back to its own parameters (directly, or a field read of one).
- `checkDeclared` holds each body to its signature, exactly. It also checks that the overrides in
a marked method's family say the same.
a marked method's family say the same. A function that says `@keeps` or `@lends` must say both
facts its body has: a body that returns a borrow needs `@lends`, one that keeps an argument
needs `@keeps`.
- The error for an open function that returns a borrow or keeps a parameter says that `@lends`
or `@keeps` would put it in its signature.
- **OwnershipInferencePass:** a marked function may return a bounded result (`boundedUses`).
- **Across modules:**
- `__decls` prints the markers with the printed parameter names (`p1`, and `this` for a
Expand Down Expand Up @@ -116,9 +125,13 @@ With these, `lib.ts` compiles under gc, rc, none and own, release and `--di`. De
made from the shape freed the borrowed argument a second time (exit 127). `import_final_class`
and `import_own_markers` now run under own too. `own_iterator_next.ts` covers the
interface-shaped call in one module, which already worked.
- **An own DefaultLib returns corrupted regex strings.** This is on main, and it is not this
branch's. `let buffer = "".clone().resize(len)` (`MatchResults.get`) borrows a temporary that is
freed after the statement, and own does not reject it. It needs its own fix.
- **An own DefaultLib returned corrupted regex strings.** Fixed 2026-10-20. It was not a borrowed
temporary. `resize` assigns its `this` parameter (`this.length = n`), and under own a string
parameter's slot did not own what was assigned to it: that string leaked, and `return this`
returned a copy of it, read up to its first NUL - one byte, which the caller filled with `len`
characters. Under own a string parameter the body assigns (including `p.length = n`) now starts
a local of its own, made from a copy, as rc already did for every assigned parameter
(`mlirGenFunctionOwnAssignedParams`). Covered by `own_string_param_assigned.ts`.
- **A generator local's previous value leaks** under rc and own (on main too): a `const` declared in
a loop inside a generator is stored into its state field again each iteration, and nothing gives
back what the last iteration stored. Releasing it there fixes rc, but under own it makes `next`
Expand All @@ -127,9 +140,10 @@ With these, `lib.ts` compiles under gc, rc, none and own, release and `--di`. De
- **A generator local's type from a function declared later** is `none` in the state object, and
the lowering fails ("failed to legalize operation 'ts.Cast'") under every model. `matchAll`
annotates `const more: boolean`.
- **`@borrows` for an ordinary function whose result points into an argument**
(`__own_result_borrows`, where the caller owns nothing) is not expressible yet. Such a function
is still rejected when it is open.
- **An ordinary function whose result points into an argument** (`__own_result_borrows`, where
the caller owns nothing) says so with `@lends`, added 2026-10-20. Covered by `own_lends.ts`, the
`own_err_lends_*` cases, and `import_own_markers` / `import_own_markers_lent_outlives` across
modules.
- **A ternary that picks an array literal or `null`** (`return i < 3 ? ["a"] : null`) fails MLIR
verification ("operand #0 does not dominate this use") under rc and own. This is unrelated.
- **Android** still builds gc, rc and none only.
9 changes: 6 additions & 3 deletions tslang/include/TypeScript/Defines.h
Original file line number Diff line number Diff line change
Expand Up @@ -256,13 +256,16 @@
// knows them - one in another module too, which reads them from the declaration it re-parses.
// `@keeps` names the parameters the function keeps: its callers move them in. `@borrows` names the
// arguments a generator's state object borrows: the caller owns the result, but it may not outlive
// them. -mm=own checks a body against what it says; the other memory models ignore both. Each is a
// FuncOp attribute of argument indices.
// them. `@lends` names the one argument the result is a borrow of (OWN_RESULT_BORROWS_ATTR_NAME):
// the caller owns nothing, and the result may not outlive it. -mm=own checks a body against what
// it says; the other memory models ignore all three. Each is a FuncOp attribute of argument indices.
#define KEEPS_DECORATOR "keeps"
#define BORROWS_DECORATOR "borrows"
#define LENDS_DECORATOR "lends"
#define OWN_DECLARED_KEEPS_ATTR_NAME "ts.own_declared_keeps"
#define OWN_DECLARED_BORROWS_ATTR_NAME "ts.own_declared_borrows"
// the names of the arguments of a function that says either, for -mm=own's errors about it
#define OWN_DECLARED_LENDS_ATTR_NAME "ts.own_declared_lends"
// the names of the arguments of a function that says any, for -mm=own's errors about it
#define OWN_DECLARED_PARAM_NAMES_ATTR_NAME "ts.own_declared_param_names"

// The process-heap allocator of a Windows module (TypeScript/ProcessHeap.h), which ProcessHeapPass
Expand Down
4 changes: 2 additions & 2 deletions tslang/include/TypeScript/MLIRLogic/MLIRDeclarationPrinter.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ namespace typescript
void printEnum(StringRef, NamespaceInfo::TypePtr, mlir::DictionaryAttr);
void printVariableDeclaration(StringRef, NamespaceInfo::TypePtr, mlir::Type, bool, StringRef = StringRef());
void print(StringRef, NamespaceInfo::TypePtr, mlir_ts::FunctionType, StringRef = StringRef(),
ArrayRef<int32_t> ownKeeps = {}, ArrayRef<int32_t> ownBorrows = {});
ArrayRef<int32_t> ownKeeps = {}, ArrayRef<int32_t> ownBorrows = {}, ArrayRef<int32_t> ownLends = {});
void print(ClassInfo::TypePtr);
void print(InterfaceInfo::TypePtr);
void printGenericClass(NamespaceInfo::TypePtr, StringRef);
Expand All @@ -40,7 +40,7 @@ namespace typescript
bool filterName(StringRef);
bool filterField(mlir::Attribute);
void printParams(ArrayRef<mlir::Type>, mlir::Type);
void printOwnMarkers(ArrayRef<int32_t>, ArrayRef<int32_t>, bool, unsigned);
void printOwnMarkers(ArrayRef<int32_t>, ArrayRef<int32_t>, ArrayRef<int32_t>, bool, unsigned);
void printFunction(StringRef, ArrayRef<mlir::Type>, mlir::Type);
void printMethod(bool, StringRef, ArrayRef<mlir::Type>, mlir::Type, mlir::Type);
void printAccessor(bool, StringRef, StringRef, mlir_ts::AccessLevel, ArrayRef<mlir::Type>, mlir::Type, mlir::Type, StringRef);
Expand Down
5 changes: 3 additions & 2 deletions tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h
Original file line number Diff line number Diff line change
Expand Up @@ -163,10 +163,11 @@ struct MethodInfo
int cppSlot = -1;
// a slot of a @cpp class's vtable that no method names (ClassInfo::getVirtualTable)
bool isCppSlotGap = false;
// `@keeps`, `@borrows` (OWN_DECLARED_KEEPS_ATTR_NAME): argument indices, `this` 0, for the
// declaration an importer re-parses
// `@keeps`, `@borrows`, `@lends` (OWN_DECLARED_KEEPS_ATTR_NAME): argument indices, `this` 0, for
// the declaration an importer re-parses; `@lends` names one, or none (-1)
std::vector<int32_t> ownKeeps;
std::vector<int32_t> ownBorrows;
int32_t ownLends = -1;
};

struct GenericMethodInfo
Expand Down
12 changes: 8 additions & 4 deletions tslang/lib/TypeScript/DeclarationPrinter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,8 @@ namespace typescript

// `@keeps("p1")`, `@borrows("this", "p1")`: by the names printParams gives the parameters, `this`
// the receiver of a method, so the importer reads the same argument indices back
void MLIRDeclarationPrinter::printOwnMarkers(ArrayRef<int32_t> keeps, ArrayRef<int32_t> borrows, bool hasThis, unsigned indent)
void MLIRDeclarationPrinter::printOwnMarkers(ArrayRef<int32_t> keeps, ArrayRef<int32_t> borrows, ArrayRef<int32_t> lends,
bool hasThis, unsigned indent)
{
auto printMarker = [&](StringRef name, ArrayRef<int32_t> indices) {
if (indices.empty())
Expand Down Expand Up @@ -261,6 +262,7 @@ namespace typescript

printMarker(KEEPS_DECORATOR, keeps);
printMarker(BORROWS_DECORATOR, borrows);
printMarker(LENDS_DECORATOR, lends);
}

void MLIRDeclarationPrinter::printFunction(StringRef name, ArrayRef<mlir::Type> params, mlir::Type returnType)
Expand Down Expand Up @@ -438,7 +440,7 @@ namespace typescript
}

void MLIRDeclarationPrinter::print(StringRef name, NamespaceInfo::TypePtr elementNamespace, mlir_ts::FunctionType funcType, StringRef dllName,
ArrayRef<int32_t> ownKeeps, ArrayRef<int32_t> ownBorrows)
ArrayRef<int32_t> ownKeeps, ArrayRef<int32_t> ownBorrows, ArrayRef<int32_t> ownLends)
{
printNamespaceBegin(elementNamespace);

Expand All @@ -450,7 +452,7 @@ namespace typescript
newline();
}

printOwnMarkers(ownKeeps, ownBorrows, /*hasThis=*/false, 0);
printOwnMarkers(ownKeeps, ownBorrows, ownLends, /*hasThis=*/false, 0);

printFunction(
name,
Expand Down Expand Up @@ -698,7 +700,9 @@ namespace typescript
newline();
}

printOwnMarkers(method.ownKeeps, method.ownBorrows, /*hasThis=*/!method.isStatic, 4);
printOwnMarkers(method.ownKeeps, method.ownBorrows,
method.ownLends >= 0 ? ArrayRef<int32_t>(method.ownLends) : ArrayRef<int32_t>(),
/*hasThis=*/!method.isStatic, 4);

// the slot of a @cpp class's virtual method: without it the importer takes the method
// for a non-virtual one
Expand Down
3 changes: 2 additions & 1 deletion tslang/lib/TypeScript/LowerToLLVM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2051,7 +2051,8 @@ struct FuncOpLowering : public TsLlvmPattern<mlir_ts::FuncOp>

// read by -mm=own only
if (name == OWN_DECLARED_NO_DROPS_ATTR_NAME || name == OWN_DECLARED_KEEPS_ATTR_NAME ||
name == OWN_DECLARED_BORROWS_ATTR_NAME || name == OWN_DECLARED_PARAM_NAMES_ATTR_NAME) {
name == OWN_DECLARED_BORROWS_ATTR_NAME || name == OWN_DECLARED_LENDS_ATTR_NAME ||
name == OWN_DECLARED_PARAM_NAMES_ATTR_NAME) {
continue;
}

Expand Down
46 changes: 38 additions & 8 deletions tslang/lib/TypeScript/MLIRGenFunctions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1287,7 +1287,8 @@ namespace mlirgen
auto globalName = isFullNamespaceName ? funcName.str() : getFullNamespaceName(funcName).str();
if (auto globalOp = theModule.lookupSymbol<mlir_ts::GlobalOp>(globalName))
{
for (auto *name : {OWN_DECLARED_KEEPS_ATTR_NAME, OWN_DECLARED_BORROWS_ATTR_NAME, OWN_DECLARED_PARAM_NAMES_ATTR_NAME})
for (auto *name : {OWN_DECLARED_KEEPS_ATTR_NAME, OWN_DECLARED_BORROWS_ATTR_NAME, OWN_DECLARED_LENDS_ATTR_NAME,
OWN_DECLARED_PARAM_NAMES_ATTR_NAME})
{
if (auto attr = declared->getAttr(name))
{
Expand Down Expand Up @@ -1479,8 +1480,10 @@ namespace mlirgen
// The names a function body assigns: `x = ...` and every compound assignment, the targets of a destructuring
// assignment, and the variable of `for (x of ...)` / `for (x in ...)`. Nested functions are walked as well, and
// a destructuring target counts every name in it: naming one too many costs a retain and a release, missing
// one leaves a parameter pointing at freed memory.
static void collectAssignedNames(ts::Node body, llvm::StringSet<> &names)
// one leaves a parameter pointing at freed memory. `lengthNames`, when given, collects the names whose
// `.length` is assigned (`this` for `this.length = n`): resizing a string makes a new one (spec 22.3), so for a
// string that is an assignment too.
static void collectAssignedNames(ts::Node body, llvm::StringSet<> &names, llvm::StringSet<> *lengthNames = nullptr)
{
auto addTargets = [&](ts::Node target) {
while (target == SyntaxKind::ParenthesizedExpression)
Expand Down Expand Up @@ -1511,6 +1514,25 @@ namespace mlirgen
if (ts::isAssignmentOperator(binaryExpression->operatorToken))
{
addTargets(binaryExpression->left);

auto left = binaryExpression->left;
if (lengthNames && left == SyntaxKind::PropertyAccessExpression)
{
auto propertyAccess = left.as<ts::PropertyAccessExpression>();
auto object = propertyAccess->expression;
if (propertyAccess->name == SyntaxKind::Identifier &&
MLIRHelper::getName(propertyAccess->name.as<ts::Identifier>()) == LENGTH_FIELD_NAME)
{
if (object == SyntaxKind::Identifier)
{
lengthNames->insert(MLIRHelper::getName(object.as<ts::Identifier>()));
}
else if (object == SyntaxKind::ThisKeyword)
{
lengthNames->insert(THIS_NAME);
}
}
}
}
}
else if (node == SyntaxKind::ForOfStatement)
Expand Down Expand Up @@ -1540,33 +1562,41 @@ namespace mlirgen
// copied into a local of the same name, which owns what it holds as any local does: a reference taken on the
// caller's value here, handed over by each assignment, given back at the function's exit.
//
// -mm=own refuses the assignment itself (OwnershipInferencePass), and a collector needs none of this.
// -mm=own: a string parameter the body assigns - `s = s + t`, or `this.length = n`, which resizes into a new
// string - starts a local of its own the same way, which own makes from a copy of the caller's string (spec
// 22.3) and which then owns each string assigned to it. Left as the parameter's slot, what was assigned to it
// had no owner: it leaked, and `return s` gave back a copy of it - read up to its first NUL, so the default
// library's `"".clone().resize(n)` returned a string of one byte that its caller wrote n into. Any other
// parameter own refuses to assign (OwnershipInferencePass), and a collector needs none of this.
mlir::LogicalResult MLIRGenImpl::mlirGenFunctionOwnAssignedParams(mlir::Location location,
FunctionLikeDeclarationBase functionLikeDeclarationBaseAST,
FunctionPrototypeDOM::TypePtr funcProto,
const GenContext &genContext)
{
if (!compileOptions.isRefCounted())
auto own = compileOptions.memoryModel == MemoryModelOwn;
if (!compileOptions.isRefCounted() && !own)
{
return mlir::success();
}

llvm::StringSet<> assignedNames;
llvm::StringSet<> lengthNames;
auto scanned = false;
for (const auto &param : funcProto->getParams())
{
if (param->getBindingPattern() || !mth.ownsHeapMemory(location, param->getType()))
if (param->getBindingPattern() || !mth.ownsHeapMemory(location, param->getType()) ||
(own && !mlir::isa<mlir_ts::StringType>(param->getType())))
{
continue;
}

if (!scanned)
{
collectAssignedNames(functionLikeDeclarationBaseAST->body, assignedNames);
collectAssignedNames(functionLikeDeclarationBaseAST->body, assignedNames, own ? &lengthNames : nullptr);
scanned = true;
}

if (!assignedNames.contains(param->getName()))
if (!assignedNames.contains(param->getName()) && !lengthNames.contains(param->getName()))
{
continue;
}
Expand Down
Loading
Loading