Skip to content

Commit c244279

Browse files
-mm=own: @keeps/@borrows parameter markers and own generator fixes (#622)
- @keeps("p") / @borrows("this","p") declare what a function keeps of, or borrows from, its arguments; they carry across modules through __decls and dynamic imports, and are checked against the body and family overrides. - A narrowed generator local is a view of its state field, not an SSA copy. - Under own, an iterator's next() result and a generator maker's state object are the caller's, even through the Iterator interface or another module. - Another module's generator state is released without its fields. - Yielded state fields are not released by the state. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent a2ead15 commit c244279

31 files changed

Lines changed: 1657 additions & 41 deletions
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# `@keeps` and `@borrows`: ownership facts in a function's signature
2+
3+
Date: 2026-10-19. Status: implemented (branch `own-param-markers`), with the own generator fixes
4+
`matchAll` needed.
5+
6+
## Why
7+
8+
After `@nodrops` and `@final` (PR #611), the default library still did not build under `-mm=own`.
9+
The remaining errors had one cause, "decision B": an exported function keeps or borrows a
10+
parameter. OwnershipSignaturePass computes these facts from the body (`__own_params`,
11+
`__own_result_bounded`). It drops them for any function a caller outside the module may reach (it
12+
is "open"), because a caller that does not know them borrows an argument the callee keeps, or
13+
releases a result the callee still points into. Either way that frees twice. The sites were
14+
`Response.constructor` (keeps `headers`), `___String.matchAll` and `String[Symbol.iterator]`
15+
(generators whose state borrows `this` and `expr`), and the `String.matchAll` wrapper.
16+
17+
The user chose markers: the facts become part of the function's declared signature, so every
18+
caller knows them, including one in another module, which re-parses the declaration.
19+
20+
## The markers
21+
22+
Both go on a function, method or constructor, and name its parameters. `"this"` names the receiver.
23+
24+
- `@keeps("headers", "body")`: the function keeps these arguments. Every caller moves them in.
25+
- `@borrows("this", "expr")`: the caller owns the result, but the result may not outlive these
26+
arguments. The result is either the state of a generator over them, or what a call with such a
27+
bounded result returns (`return this.value.matchAll(expr)`, a "forwarder").
28+
29+
Under gc, rc and none they mean nothing. MLIRGen still records them, and the LLVM lowering drops
30+
the attributes.
31+
32+
## How it works
33+
34+
- **MLIRGen** (`processOwnParamMarkers`) turns the names into argument indices (FuncOp attributes
35+
`ts.own_declared_keeps` and `ts.own_declared_borrows`). It also records the argument names
36+
(`ts.own_declared_param_names`) for the errors. A name that is not a parameter is an error. The
37+
generator wrapper copies the method's decorators, so a marked generator's maker carries them.
38+
- **OwnershipSignaturePass:**
39+
- A marked function's `@keeps` facts stand even when it is open (`meetFacts`). A family does not
40+
erase them.
41+
- A call gets what its callees' signatures say, whether or not it is resolved
42+
(`declaredByCallees`, over `Callees::named`). The named callees are the direct callee, every
43+
member of a virtual slot's family, or a dynamic import's address global.
44+
- A call whose callees say `@borrows` gets `__own_result_bounded` and `__own_fresh_result`: the
45+
caller owns the state.
46+
- `findBorrowingStates` accepts a marked maker even though it is public, and a method reached
47+
through its vtable. It also accepts `this` held by value (`.this`, read from the parameter's
48+
slot).
49+
- `findBoundedForwarders` gives a marked non-generator what its returned bounded calls borrow,
50+
mapped back to its own parameters (directly, or a field read of one).
51+
- `checkDeclared` holds each body to its signature, exactly. It also checks that the overrides in
52+
a marked method's family say the same.
53+
- **OwnershipInferencePass:** a marked function may return a bounded result (`boundedUses`).
54+
- **Across modules:**
55+
- `__decls` prints the markers with the printed parameter names (`p1`, and `this` for a
56+
receiver). `MethodInfo::ownKeeps` and `ownBorrows` carry them for methods;
57+
`addFunctionDeclarationToExport` gets them from the FuncOp.
58+
- A dynamic import's function is a global holding its address, so
59+
`mlirGenFunctionLikeDeclarationDynamicImport` copies the declaration's markers onto that global.
60+
- DefaultLib's `lib.d.ts` says the same as `lib.ts`.
61+
62+
## Found on the way and fixed here
63+
64+
- **A silent miscompile.** A public generator method over `this` with no marker compiled under own
65+
(release), and the state's release freed the caller's object. The caller then freed it a second
66+
time, and the program exited 127 with no output. `classifyStateBoxes` let a non-borrowing box
67+
take rc's reference for a parameter the function does not own. It now leaves that reference
68+
alone, so the existing "is a parameter, kept here" error fires (`own_err_generator_method_this`).
69+
`--di` already errored there, by accident: its `ts.DebugVariable` use broke the one-use check
70+
that paired the reference.
71+
- **`--di`.** `retainBefore` and `twinReadAfter` now ignore `ts.DebugVariable` uses (`soleUser`),
72+
so a marked generator over `this` builds with debug info too.
73+
74+
## Generator locals under own
75+
76+
`matchAll`'s body needed three fixes to own generators that markers could not give. Each is
77+
covered by `own_generator_locals.ts`, under gc, rc and own.
78+
79+
- **Narrowing.** A generator's locals are fields of its state object, so narrowing one
80+
(`if (result == null) break;`) made a second field, filled from the first: a second reference.
81+
`addSafeCastStatement` now declares a generator's narrowed local as a view of the local's own
82+
storage (`VariableDeclarationDOM::narrowedType`). Each read loads the local and casts it, so a
83+
read after a resume sees the current value. An SSA copy would not: it read stale values after a
84+
yield, and gc printed 3 where 9 is right.
85+
- **A spread in a generator.** `[...a]` is a `for...of` that MLIRGen writes, and its `.a` alias of
86+
the source array became a field holding a second reference. The loops of the spread, `forEach`,
87+
`every` and `some` hold no yield, so their locals stay in the frame (`allocateVarsInContextThis`
88+
and `allocateVarsOutsideOfOperation` off). `isBuiltArrayReturned` also accepts the built array
89+
moved into a field, `h.c = [...a]` or a generator's local.
90+
- **A yielded local.** `yield groups` read the field into the `{value, done}` record, a borrow of
91+
the state. OwnershipSignaturePass now finds the state fields that are read for nothing but
92+
yields, where each stored value is yielded later in its block (`findYieldedFields`). The yield's
93+
read is fresh (the value moves to the caller), an overwrite gives nothing back, and the state's
94+
release routine skips the field (`OWN_YIELDED_FIELDS_ATTR_NAME`). `matchAll` computes its
95+
empty-match skip from `result` before the yield, so `groups` is only yielded.
96+
- **The owned-return pass took a generator's `next` for a class method.** OwnedReturnConsumptionPass
97+
matches a virtual call's candidates by member name. A class's `next()` was grouped with every
98+
generator's `next`, whose record does not classify as owned, so `src.next()` lost
99+
`__owned_result` and own reported a second reference. Only class methods (a class first) are
100+
candidates now.
101+
102+
With these, `lib.ts` compiles under gc, rc, none and own, release and `--di`. DefaultLib's
103+
`build.bat` and `build.sh` build own as a fourth model.
104+
105+
## Not done (follow-ups)
106+
107+
- **Iterating another module's generator under own** works now. The user ruled that what an
108+
iterator's `next()` returns is the caller's, through the `Iterator` interface or into another
109+
module, and only that, not every unresolved call. OwnershipSignaturePass marks such calls fresh
110+
(`isIteratorNext`: a callee named `next` returning `{value, done}`), and so is a call that
111+
returns a generator's state object (`makesGeneratorState`), which a generator function always
112+
makes. `isFresh` reads that mark before it rejects a declared callee, and `walkBorrowed` does not
113+
follow a fresh result that is not bounded. Another module's generator state is released here
114+
without its fields (`findImportedStates`): only the box's cells, and their values unless the
115+
maker says `@borrows`. What its locals still hold when it is given up early leaks. A release
116+
made from the shape freed the borrowed argument a second time (exit 127). `import_final_class`
117+
and `import_own_markers` now run under own too. `own_iterator_next.ts` covers the
118+
interface-shaped call in one module, which already worked.
119+
- **An own DefaultLib returns corrupted regex strings.** This is on main, and it is not this
120+
branch's. `let buffer = "".clone().resize(len)` (`MatchResults.get`) borrows a temporary that is
121+
freed after the statement, and own does not reject it. It needs its own fix.
122+
- **A generator local's previous value leaks** under rc and own (on main too): a `const` declared in
123+
a loop inside a generator is stored into its state field again each iteration, and nothing gives
124+
back what the last iteration stored. Releasing it there fixes rc, but under own it makes `next`
125+
a call that may drop, and a caller's `for...of` over such a generator is then rejected (its
126+
loop variable borrows the record). The fix wants the caller-side `for...of` move as well.
127+
- **A generator local's type from a function declared later** is `none` in the state object, and
128+
the lowering fails ("failed to legalize operation 'ts.Cast'") under every model. `matchAll`
129+
annotates `const more: boolean`.
130+
- **`@borrows` for an ordinary function whose result points into an argument**
131+
(`__own_result_borrows`, where the caller owns nothing) is not expressible yet. Such a function
132+
is still rejected when it is open.
133+
- **A ternary that picks an array literal or `null`** (`return i < 3 ? ["a"] : null`) fails MLIR
134+
verification ("operand #0 does not dominate this use") under rc and own. This is unrelated.
135+
- **Android** still builds gc, rc and none only.

‎tslang/include/TypeScript/DOM.h‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ class VariableDeclarationDOM
3131
bool ignoreCapturing;
3232
bool _using;
3333
mlir::Value disposeGuard;
34+
// a generator's local narrowed by a test: a view of the local's own storage, read and cast
35+
// to this type at each read (addSafeCastStatement)
36+
mlir::Type narrowedType;
3437

3538
public:
3639
using TypePtr = std::shared_ptr<VariableDeclarationDOM>;
@@ -114,6 +117,15 @@ class VariableDeclarationDOM
114117
{
115118
return disposeGuard;
116119
}
120+
121+
mlir::Type getNarrowedType() const
122+
{
123+
return narrowedType;
124+
}
125+
void setNarrowedType(mlir::Type value)
126+
{
127+
narrowedType = value;
128+
}
117129
void setDisposeGuard(mlir::Value value)
118130
{
119131
disposeGuard = value;

‎tslang/include/TypeScript/Defines.h‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@
123123
// Its release routine skips them, and a borrowing `.captured` box frees its cells but not what
124124
// they hold.
125125
#define OWN_BORROWING_FIELDS_ATTR_NAME "ts.own_borrowing_fields"
126+
// -mm=own only, set by OwnershipSignaturePass on the module, in the same shape: the fields of a
127+
// generator's state object - its locals - read only to be yielded (`const groups = [...r]; yield
128+
// groups`). A yield moves what such a field holds to the caller, so the field never owns it: an
129+
// overwrite gives nothing back, and the state's release routine skips it.
130+
#define OWN_YIELDED_FIELDS_ATTR_NAME "ts.own_yielded_fields"
126131
#define RETURN_VARIABLE_NAME ".return"
127132
#define CAPTURED_NAME ".captured"
128133
#define LABEL_ATTR_NAME "label"
@@ -246,6 +251,19 @@
246251
// one re-parsed from a TypeScript library, whose functions may destroy what they are given.
247252
#define NO_DROPS_DECORATOR "nodrops"
248253
#define OWN_DECLARED_NO_DROPS_ATTR_NAME "ts.own_declared_no_drops"
254+
// `@keeps("p")` and `@borrows("this", "p")`: the facts of a function's body that its callers rely
255+
// on (OWN_PARAMS_ATTR_NAME, OWN_RESULT_BOUNDED_ATTR_NAME), said in its signature, so every caller
256+
// knows them - one in another module too, which reads them from the declaration it re-parses.
257+
// `@keeps` names the parameters the function keeps: its callers move them in. `@borrows` names the
258+
// arguments a generator's state object borrows: the caller owns the result, but it may not outlive
259+
// them. -mm=own checks a body against what it says; the other memory models ignore both. Each is a
260+
// FuncOp attribute of argument indices.
261+
#define KEEPS_DECORATOR "keeps"
262+
#define BORROWS_DECORATOR "borrows"
263+
#define OWN_DECLARED_KEEPS_ATTR_NAME "ts.own_declared_keeps"
264+
#define OWN_DECLARED_BORROWS_ATTR_NAME "ts.own_declared_borrows"
265+
// the names of the arguments of a function that says either, for -mm=own's errors about it
266+
#define OWN_DECLARED_PARAM_NAMES_ATTR_NAME "ts.own_declared_param_names"
249267

250268
// The process-heap allocator of a Windows module (TypeScript/ProcessHeap.h), which ProcessHeapPass
251269
// calls instead of the C runtime's. Defined by the async runtime library a program links,

‎tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -743,9 +743,16 @@ class OwnershipRoutineLogic
743743
auto loc = op->getLoc();
744744
auto llvmRecordType = tch.convertType(recordType);
745745
auto borrowing = borrowingFieldsOf(recordType);
746+
auto yielded = fieldsOf(OWN_YIELDED_FIELDS_ATTR_NAME, recordType);
746747

747748
for (auto [index, fieldType] : llvm::enumerate(getFieldTypes(recordType)))
748749
{
750+
// a generator's local that only yields: what it held went to the caller
751+
if (llvm::is_contained(yielded, static_cast<int32_t>(index)))
752+
{
753+
continue;
754+
}
755+
749756
// a field of a generator's state object that borrows from the generator's arguments:
750757
// nothing to give back, but for the `.captured` box itself, whose cells are the state's
751758
auto borrows = llvm::is_contained(borrowing, static_cast<int32_t>(index));
@@ -956,6 +963,12 @@ class OwnershipRoutineLogic
956963
// Under own, the fields of a generator's state object that borrow (OWN_BORROWING_FIELDS_ATTR_NAME),
957964
// the `.captured` box's first; empty for any other record or model.
958965
llvm::SmallVector<int32_t> borrowingFieldsOf(mlir::Type recordType)
966+
{
967+
return fieldsOf(OWN_BORROWING_FIELDS_ATTR_NAME, recordType);
968+
}
969+
970+
// Under own, the fields a module attribute of [[type, [field...]], ...] lists for this record.
971+
llvm::SmallVector<int32_t> fieldsOf(llvm::StringRef attrName, mlir::Type recordType)
959972
{
960973
llvm::SmallVector<int32_t> fields;
961974
if (compileOptions.memoryModel != MemoryModelOwn)
@@ -969,7 +982,7 @@ class OwnershipRoutineLogic
969982
}
970983

971984
auto parentModule = op->getParentOfType<ModuleOp>();
972-
auto entries = parentModule ? parentModule->getAttrOfType<ArrayAttr>(OWN_BORROWING_FIELDS_ATTR_NAME) : ArrayAttr();
985+
auto entries = parentModule ? parentModule->getAttrOfType<ArrayAttr>(attrName) : ArrayAttr();
973986
if (!entries)
974987
{
975988
return fields;

‎tslang/include/TypeScript/MLIRLogic/MLIRDeclarationPrinter.h‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ namespace typescript
2323
void printTypeDeclaration(StringRef, NamespaceInfo::TypePtr, mlir::Type);
2424
void printEnum(StringRef, NamespaceInfo::TypePtr, mlir::DictionaryAttr);
2525
void printVariableDeclaration(StringRef, NamespaceInfo::TypePtr, mlir::Type, bool, StringRef = StringRef());
26-
void print(StringRef, NamespaceInfo::TypePtr, mlir_ts::FunctionType, StringRef = StringRef());
26+
void print(StringRef, NamespaceInfo::TypePtr, mlir_ts::FunctionType, StringRef = StringRef(),
27+
ArrayRef<int32_t> ownKeeps = {}, ArrayRef<int32_t> ownBorrows = {});
2728
void print(ClassInfo::TypePtr);
2829
void print(InterfaceInfo::TypePtr);
2930
void printGenericClass(NamespaceInfo::TypePtr, StringRef);
@@ -39,6 +40,7 @@ namespace typescript
3940
bool filterName(StringRef);
4041
bool filterField(mlir::Attribute);
4142
void printParams(ArrayRef<mlir::Type>, mlir::Type);
43+
void printOwnMarkers(ArrayRef<int32_t>, ArrayRef<int32_t>, bool, unsigned);
4244
void printFunction(StringRef, ArrayRef<mlir::Type>, mlir::Type);
4345
void printMethod(bool, StringRef, ArrayRef<mlir::Type>, mlir::Type, mlir::Type);
4446
void printAccessor(bool, StringRef, StringRef, mlir_ts::AccessLevel, ArrayRef<mlir::Type>, mlir::Type, mlir::Type, StringRef);

‎tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,10 @@ struct MethodInfo
163163
int cppSlot = -1;
164164
// a slot of a @cpp class's vtable that no method names (ClassInfo::getVirtualTable)
165165
bool isCppSlotGap = false;
166+
// `@keeps`, `@borrows` (OWN_DECLARED_KEEPS_ATTR_NAME): argument indices, `this` 0, for the
167+
// declaration an importer re-parses
168+
std::vector<int32_t> ownKeeps;
169+
std::vector<int32_t> ownBorrows;
166170
};
167171

168172
struct GenericMethodInfo

‎tslang/lib/TypeScript/DeclarationPrinter.cpp‎

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,41 @@ namespace typescript
228228
os << ")";
229229
}
230230

231+
// `@keeps("p1")`, `@borrows("this", "p1")`: by the names printParams gives the parameters, `this`
232+
// the receiver of a method, so the importer reads the same argument indices back
233+
void MLIRDeclarationPrinter::printOwnMarkers(ArrayRef<int32_t> keeps, ArrayRef<int32_t> borrows, bool hasThis, unsigned indent)
234+
{
235+
auto printMarker = [&](StringRef name, ArrayRef<int32_t> indices) {
236+
if (indices.empty())
237+
{
238+
return;
239+
}
240+
241+
os.indent(indent);
242+
os << "@" << name << "(";
243+
for (auto [position, index] : enumerate(indices))
244+
{
245+
os << (position ? ", " : "") << "\"";
246+
if (index == 0 && hasThis)
247+
{
248+
os << "this";
249+
}
250+
else
251+
{
252+
os << "p" << index;
253+
}
254+
255+
os << "\"";
256+
}
257+
258+
os << ")";
259+
newline();
260+
};
261+
262+
printMarker(KEEPS_DECORATOR, keeps);
263+
printMarker(BORROWS_DECORATOR, borrows);
264+
}
265+
231266
void MLIRDeclarationPrinter::printFunction(StringRef name, ArrayRef<mlir::Type> params, mlir::Type returnType)
232267
{
233268
os << "function " << name;
@@ -402,7 +437,8 @@ namespace typescript
402437
printNamespaceEnd(elementNamespace);
403438
}
404439

405-
void MLIRDeclarationPrinter::print(StringRef name, NamespaceInfo::TypePtr elementNamespace, mlir_ts::FunctionType funcType, StringRef dllName)
440+
void MLIRDeclarationPrinter::print(StringRef name, NamespaceInfo::TypePtr elementNamespace, mlir_ts::FunctionType funcType, StringRef dllName,
441+
ArrayRef<int32_t> ownKeeps, ArrayRef<int32_t> ownBorrows)
406442
{
407443
printNamespaceBegin(elementNamespace);
408444

@@ -414,6 +450,8 @@ namespace typescript
414450
newline();
415451
}
416452

453+
printOwnMarkers(ownKeeps, ownBorrows, /*hasThis=*/false, 0);
454+
417455
printFunction(
418456
name,
419457
funcType.getParams(),
@@ -660,6 +698,8 @@ namespace typescript
660698
newline();
661699
}
662700

701+
printOwnMarkers(method.ownKeeps, method.ownBorrows, /*hasThis=*/!method.isStatic, 4);
702+
663703
// the slot of a @cpp class's virtual method: without it the importer takes the method
664704
// for a non-virtual one
665705
if (method.cppSlot >= 0)

‎tslang/lib/TypeScript/LowerToLLVM.cpp‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2050,7 +2050,8 @@ struct FuncOpLowering : public TsLlvmPattern<mlir_ts::FuncOp>
20502050
}
20512051

20522052
// read by -mm=own only
2053-
if (name == OWN_DECLARED_NO_DROPS_ATTR_NAME) {
2053+
if (name == OWN_DECLARED_NO_DROPS_ATTR_NAME || name == OWN_DECLARED_KEEPS_ATTR_NAME ||
2054+
name == OWN_DECLARED_BORROWS_ATTR_NAME || name == OWN_DECLARED_PARAM_NAMES_ATTR_NAME) {
20542055
continue;
20552056
}
20562057

‎tslang/lib/TypeScript/MLIRGenClasses.cpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3459,7 +3459,7 @@ genContext);
34593459
auto location = loc(funcLikeDeclaration);
34603460
auto dllNameAttr = funcOp->getAttrOfType<mlir::StringAttr>(DLL_NAME);
34613461
if (mlir::succeeded(mlirGenFunctionLikeDeclarationDynamicImport(
3462-
location, funcOp.getName(), funcOp.getFunctionType(), dllNameAttr ? dllNameAttr.getValue() : funcOp.getName(), genContext)))
3462+
location, funcOp, funcOp.getName(), funcOp.getFunctionType(), dllNameAttr ? dllNameAttr.getValue() : funcOp.getName(), genContext)))
34633463
{
34643464
// no need to generate method in code
34653465
funcLikeDeclaration->processed = true;

0 commit comments

Comments
 (0)