diff --git a/tslang/docs/superpowers/specs/2026-10-19-own-param-markers-design.md b/tslang/docs/superpowers/specs/2026-10-19-own-param-markers-design.md new file mode 100644 index 000000000..1b9164f29 --- /dev/null +++ b/tslang/docs/superpowers/specs/2026-10-19-own-param-markers-design.md @@ -0,0 +1,135 @@ +# `@keeps` and `@borrows`: ownership facts in a function's signature + +Date: 2026-10-19. Status: implemented (branch `own-param-markers`), with the own generator fixes +`matchAll` needed. + +## Why + +After `@nodrops` and `@final` (PR #611), the default library still did not build under `-mm=own`. +The remaining errors had one cause, "decision B": an exported function keeps or borrows a +parameter. OwnershipSignaturePass computes these facts from the body (`__own_params`, +`__own_result_bounded`). It drops them for any function a caller outside the module may reach (it +is "open"), because a caller that does not know them borrows an argument the callee keeps, or +releases a result the callee still points into. Either way that frees twice. The sites were +`Response.constructor` (keeps `headers`), `___String.matchAll` and `String[Symbol.iterator]` +(generators whose state borrows `this` and `expr`), and the `String.matchAll` wrapper. + +The user chose markers: the facts become part of the function's declared signature, so every +caller knows them, including one in another module, which re-parses the declaration. + +## The markers + +Both go on a function, method or constructor, and name its parameters. `"this"` names the receiver. + +- `@keeps("headers", "body")`: the function keeps these arguments. Every caller moves them in. +- `@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"). + +Under gc, rc and none they mean nothing. MLIRGen still records them, and the LLVM lowering drops +the attributes. + +## How it works + +- **MLIRGen** (`processOwnParamMarkers`) turns the names into argument indices (FuncOp attributes + `ts.own_declared_keeps` and `ts.own_declared_borrows`). It also records the argument names + (`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 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. + - A call whose callees say `@borrows` gets `__own_result_bounded` and `__own_fresh_result`: the + caller owns the state. + - `findBorrowingStates` accepts a marked maker even though it is public, and a method reached + through its vtable. It also accepts `this` held by value (`.this`, read from the parameter's + slot). + - `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. +- **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 + receiver). `MethodInfo::ownKeeps` and `ownBorrows` carry them for methods; + `addFunctionDeclarationToExport` gets them from the FuncOp. + - A dynamic import's function is a global holding its address, so + `mlirGenFunctionLikeDeclarationDynamicImport` copies the declaration's markers onto that global. + - DefaultLib's `lib.d.ts` says the same as `lib.ts`. + +## Found on the way and fixed here + +- **A silent miscompile.** A public generator method over `this` with no marker compiled under own + (release), and the state's release freed the caller's object. The caller then freed it a second + time, and the program exited 127 with no output. `classifyStateBoxes` let a non-borrowing box + take rc's reference for a parameter the function does not own. It now leaves that reference + alone, so the existing "is a parameter, kept here" error fires (`own_err_generator_method_this`). + `--di` already errored there, by accident: its `ts.DebugVariable` use broke the one-use check + that paired the reference. +- **`--di`.** `retainBefore` and `twinReadAfter` now ignore `ts.DebugVariable` uses (`soleUser`), + so a marked generator over `this` builds with debug info too. + +## Generator locals under own + +`matchAll`'s body needed three fixes to own generators that markers could not give. Each is +covered by `own_generator_locals.ts`, under gc, rc and own. + +- **Narrowing.** A generator's locals are fields of its state object, so narrowing one + (`if (result == null) break;`) made a second field, filled from the first: a second reference. + `addSafeCastStatement` now declares a generator's narrowed local as a view of the local's own + storage (`VariableDeclarationDOM::narrowedType`). Each read loads the local and casts it, so a + read after a resume sees the current value. An SSA copy would not: it read stale values after a + yield, and gc printed 3 where 9 is right. +- **A spread in a generator.** `[...a]` is a `for...of` that MLIRGen writes, and its `.a` alias of + the source array became a field holding a second reference. The loops of the spread, `forEach`, + `every` and `some` hold no yield, so their locals stay in the frame (`allocateVarsInContextThis` + and `allocateVarsOutsideOfOperation` off). `isBuiltArrayReturned` also accepts the built array + moved into a field, `h.c = [...a]` or a generator's local. +- **A yielded local.** `yield groups` read the field into the `{value, done}` record, a borrow of + the state. OwnershipSignaturePass now finds the state fields that are read for nothing but + yields, where each stored value is yielded later in its block (`findYieldedFields`). The yield's + read is fresh (the value moves to the caller), an overwrite gives nothing back, and the state's + release routine skips the field (`OWN_YIELDED_FIELDS_ATTR_NAME`). `matchAll` computes its + empty-match skip from `result` before the yield, so `groups` is only yielded. +- **The owned-return pass took a generator's `next` for a class method.** OwnedReturnConsumptionPass + matches a virtual call's candidates by member name. A class's `next()` was grouped with every + generator's `next`, whose record does not classify as owned, so `src.next()` lost + `__owned_result` and own reported a second reference. Only class methods (a class first) are + candidates now. + +With these, `lib.ts` compiles under gc, rc, none and own, release and `--di`. DefaultLib's +`build.bat` and `build.sh` build own as a fourth model. + +## Not done (follow-ups) + +- **Iterating another module's generator under own** works now. The user ruled that what an + iterator's `next()` returns is the caller's, through the `Iterator` interface or into another + module, and only that, not every unresolved call. OwnershipSignaturePass marks such calls fresh + (`isIteratorNext`: a callee named `next` returning `{value, done}`), and so is a call that + returns a generator's state object (`makesGeneratorState`), which a generator function always + makes. `isFresh` reads that mark before it rejects a declared callee, and `walkBorrowed` does not + follow a fresh result that is not bounded. Another module's generator state is released here + without its fields (`findImportedStates`): only the box's cells, and their values unless the + maker says `@borrows`. What its locals still hold when it is given up early leaks. A release + 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. +- **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` + a call that may drop, and a caller's `for...of` over such a generator is then rejected (its + loop variable borrows the record). The fix wants the caller-side `for...of` move as well. +- **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. +- **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. diff --git a/tslang/include/TypeScript/DOM.h b/tslang/include/TypeScript/DOM.h index a254c0caa..e8cdb0f9f 100644 --- a/tslang/include/TypeScript/DOM.h +++ b/tslang/include/TypeScript/DOM.h @@ -31,6 +31,9 @@ class VariableDeclarationDOM bool ignoreCapturing; bool _using; mlir::Value disposeGuard; + // a generator's local narrowed by a test: a view of the local's own storage, read and cast + // to this type at each read (addSafeCastStatement) + mlir::Type narrowedType; public: using TypePtr = std::shared_ptr; @@ -114,6 +117,15 @@ class VariableDeclarationDOM { return disposeGuard; } + + mlir::Type getNarrowedType() const + { + return narrowedType; + } + void setNarrowedType(mlir::Type value) + { + narrowedType = value; + } void setDisposeGuard(mlir::Value value) { disposeGuard = value; diff --git a/tslang/include/TypeScript/Defines.h b/tslang/include/TypeScript/Defines.h index ba701120f..6c0aef4dc 100644 --- a/tslang/include/TypeScript/Defines.h +++ b/tslang/include/TypeScript/Defines.h @@ -123,6 +123,11 @@ // Its release routine skips them, and a borrowing `.captured` box frees its cells but not what // they hold. #define OWN_BORROWING_FIELDS_ATTR_NAME "ts.own_borrowing_fields" +// -mm=own only, set by OwnershipSignaturePass on the module, in the same shape: the fields of a +// generator's state object - its locals - read only to be yielded (`const groups = [...r]; yield +// groups`). A yield moves what such a field holds to the caller, so the field never owns it: an +// overwrite gives nothing back, and the state's release routine skips it. +#define OWN_YIELDED_FIELDS_ATTR_NAME "ts.own_yielded_fields" #define RETURN_VARIABLE_NAME ".return" #define CAPTURED_NAME ".captured" #define LABEL_ATTR_NAME "label" @@ -246,6 +251,19 @@ // one re-parsed from a TypeScript library, whose functions may destroy what they are given. #define NO_DROPS_DECORATOR "nodrops" #define OWN_DECLARED_NO_DROPS_ATTR_NAME "ts.own_declared_no_drops" +// `@keeps("p")` and `@borrows("this", "p")`: the facts of a function's body that its callers rely +// on (OWN_PARAMS_ATTR_NAME, OWN_RESULT_BOUNDED_ATTR_NAME), said in its signature, so every caller +// 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. +#define KEEPS_DECORATOR "keeps" +#define BORROWS_DECORATOR "borrows" +#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_PARAM_NAMES_ATTR_NAME "ts.own_declared_param_names" // The process-heap allocator of a Windows module (TypeScript/ProcessHeap.h), which ProcessHeapPass // calls instead of the C runtime's. Defined by the async runtime library a program links, diff --git a/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h b/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h index 4e18be368..77f51a713 100644 --- a/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h +++ b/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h @@ -743,9 +743,16 @@ class OwnershipRoutineLogic auto loc = op->getLoc(); auto llvmRecordType = tch.convertType(recordType); auto borrowing = borrowingFieldsOf(recordType); + auto yielded = fieldsOf(OWN_YIELDED_FIELDS_ATTR_NAME, recordType); for (auto [index, fieldType] : llvm::enumerate(getFieldTypes(recordType))) { + // a generator's local that only yields: what it held went to the caller + if (llvm::is_contained(yielded, static_cast(index))) + { + continue; + } + // a field of a generator's state object that borrows from the generator's arguments: // nothing to give back, but for the `.captured` box itself, whose cells are the state's auto borrows = llvm::is_contained(borrowing, static_cast(index)); @@ -956,6 +963,12 @@ class OwnershipRoutineLogic // Under own, the fields of a generator's state object that borrow (OWN_BORROWING_FIELDS_ATTR_NAME), // the `.captured` box's first; empty for any other record or model. llvm::SmallVector borrowingFieldsOf(mlir::Type recordType) + { + return fieldsOf(OWN_BORROWING_FIELDS_ATTR_NAME, recordType); + } + + // Under own, the fields a module attribute of [[type, [field...]], ...] lists for this record. + llvm::SmallVector fieldsOf(llvm::StringRef attrName, mlir::Type recordType) { llvm::SmallVector fields; if (compileOptions.memoryModel != MemoryModelOwn) @@ -969,7 +982,7 @@ class OwnershipRoutineLogic } auto parentModule = op->getParentOfType(); - auto entries = parentModule ? parentModule->getAttrOfType(OWN_BORROWING_FIELDS_ATTR_NAME) : ArrayAttr(); + auto entries = parentModule ? parentModule->getAttrOfType(attrName) : ArrayAttr(); if (!entries) { return fields; diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRDeclarationPrinter.h b/tslang/include/TypeScript/MLIRLogic/MLIRDeclarationPrinter.h index a307cf45c..825d469bc 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRDeclarationPrinter.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRDeclarationPrinter.h @@ -23,7 +23,8 @@ namespace typescript void printTypeDeclaration(StringRef, NamespaceInfo::TypePtr, mlir::Type); 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()); + void print(StringRef, NamespaceInfo::TypePtr, mlir_ts::FunctionType, StringRef = StringRef(), + ArrayRef ownKeeps = {}, ArrayRef ownBorrows = {}); void print(ClassInfo::TypePtr); void print(InterfaceInfo::TypePtr); void printGenericClass(NamespaceInfo::TypePtr, StringRef); @@ -39,6 +40,7 @@ namespace typescript bool filterName(StringRef); bool filterField(mlir::Attribute); void printParams(ArrayRef, mlir::Type); + void printOwnMarkers(ArrayRef, ArrayRef, bool, unsigned); void printFunction(StringRef, ArrayRef, mlir::Type); void printMethod(bool, StringRef, ArrayRef, mlir::Type, mlir::Type); void printAccessor(bool, StringRef, StringRef, mlir_ts::AccessLevel, ArrayRef, mlir::Type, mlir::Type, StringRef); diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h b/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h index af80fa68f..68a421850 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h @@ -163,6 +163,10 @@ 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 + std::vector ownKeeps; + std::vector ownBorrows; }; struct GenericMethodInfo diff --git a/tslang/lib/TypeScript/DeclarationPrinter.cpp b/tslang/lib/TypeScript/DeclarationPrinter.cpp index 8f5342c88..d6d7dd9a1 100644 --- a/tslang/lib/TypeScript/DeclarationPrinter.cpp +++ b/tslang/lib/TypeScript/DeclarationPrinter.cpp @@ -228,6 +228,41 @@ namespace typescript os << ")"; } + // `@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 keeps, ArrayRef borrows, bool hasThis, unsigned indent) + { + auto printMarker = [&](StringRef name, ArrayRef indices) { + if (indices.empty()) + { + return; + } + + os.indent(indent); + os << "@" << name << "("; + for (auto [position, index] : enumerate(indices)) + { + os << (position ? ", " : "") << "\""; + if (index == 0 && hasThis) + { + os << "this"; + } + else + { + os << "p" << index; + } + + os << "\""; + } + + os << ")"; + newline(); + }; + + printMarker(KEEPS_DECORATOR, keeps); + printMarker(BORROWS_DECORATOR, borrows); + } + void MLIRDeclarationPrinter::printFunction(StringRef name, ArrayRef params, mlir::Type returnType) { os << "function " << name; @@ -402,7 +437,8 @@ namespace typescript printNamespaceEnd(elementNamespace); } - void MLIRDeclarationPrinter::print(StringRef name, NamespaceInfo::TypePtr elementNamespace, mlir_ts::FunctionType funcType, StringRef dllName) + void MLIRDeclarationPrinter::print(StringRef name, NamespaceInfo::TypePtr elementNamespace, mlir_ts::FunctionType funcType, StringRef dllName, + ArrayRef ownKeeps, ArrayRef ownBorrows) { printNamespaceBegin(elementNamespace); @@ -414,6 +450,8 @@ namespace typescript newline(); } + printOwnMarkers(ownKeeps, ownBorrows, /*hasThis=*/false, 0); + printFunction( name, funcType.getParams(), @@ -660,6 +698,8 @@ namespace typescript newline(); } + printOwnMarkers(method.ownKeeps, method.ownBorrows, /*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 if (method.cppSlot >= 0) diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index 9a382771b..49145b3d6 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -2050,7 +2050,8 @@ struct FuncOpLowering : public TsLlvmPattern } // read by -mm=own only - if (name == OWN_DECLARED_NO_DROPS_ATTR_NAME) { + 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) { continue; } diff --git a/tslang/lib/TypeScript/MLIRGenClasses.cpp b/tslang/lib/TypeScript/MLIRGenClasses.cpp index 3c051cdae..8cdb4cd4f 100644 --- a/tslang/lib/TypeScript/MLIRGenClasses.cpp +++ b/tslang/lib/TypeScript/MLIRGenClasses.cpp @@ -3459,7 +3459,7 @@ genContext); auto location = loc(funcLikeDeclaration); auto dllNameAttr = funcOp->getAttrOfType(DLL_NAME); if (mlir::succeeded(mlirGenFunctionLikeDeclarationDynamicImport( - location, funcOp.getName(), funcOp.getFunctionType(), dllNameAttr ? dllNameAttr.getValue() : funcOp.getName(), genContext))) + location, funcOp, funcOp.getName(), funcOp.getFunctionType(), dllNameAttr ? dllNameAttr.getValue() : funcOp.getName(), genContext))) { // no need to generate method in code funcLikeDeclaration->processed = true; diff --git a/tslang/lib/TypeScript/MLIRGenFunctions.cpp b/tslang/lib/TypeScript/MLIRGenFunctions.cpp index 76a7e9154..3af7ae0dd 100644 --- a/tslang/lib/TypeScript/MLIRGenFunctions.cpp +++ b/tslang/lib/TypeScript/MLIRGenFunctions.cpp @@ -589,6 +589,12 @@ namespace mlirgen funcType.isVarArg(), /*isCVarArg=*/true); } + if (mlir::failed(processOwnParamMarkers(location, functionLikeDeclarationBaseAST, funcProto->getParams(), funcType, + attrs, funcProtoGenContext))) + { + return std::make_tuple(funcOp, funcProto, mlir::failure(), false); + } + if (funcType) { auto it = getCaptureVarsMap().find(funcProto->getName()); @@ -612,7 +618,7 @@ namespace mlirgen || functionLikeDeclarationBaseAST == SyntaxKind::ArrowFunction) { auto dllNameAttr = funcOp->getAttrOfType(DLL_NAME); - addFunctionDeclarationToExport(funcProto, currentNamespace, dllNameAttr ? dllNameAttr.getValue() : StringRef()); + addFunctionDeclarationToExport(funcProto, currentNamespace, dllNameAttr ? dllNameAttr.getValue() : StringRef(), funcOp); } } } @@ -1157,7 +1163,7 @@ namespace mlirgen { // TODO: we do not need to register funcOp as we need to reference global variables auto result = mlirGenFunctionLikeDeclarationDynamicImport( - location, funcProto->getNameWithoutNamespace(), funcOp.getFunctionType(), + location, funcOp, funcProto->getNameWithoutNamespace(), funcOp.getFunctionType(), dllFuncName, funcDeclGenContext, false); return {result, funcOp, funcProto->getName().str(), false}; } @@ -1260,8 +1266,8 @@ namespace mlirgen return {mlir::success(), funcOp, funcProto->getName().str(), false}; } - mlir::LogicalResult MLIRGenImpl::mlirGenFunctionLikeDeclarationDynamicImport(mlir::Location location, StringRef funcName, - mlir_ts::FunctionType functionType, StringRef dllFuncName, const GenContext &genContext, bool isFullNamespaceName) + mlir::LogicalResult MLIRGenImpl::mlirGenFunctionLikeDeclarationDynamicImport(mlir::Location location, mlir_ts::FuncOp declared, + StringRef funcName, mlir_ts::FunctionType functionType, StringRef dllFuncName, const GenContext &genContext, bool isFullNamespaceName) { registerVariable(location, funcName, isFullNamespaceName, VariableType::Var, [&](mlir::Location location, const GenContext &context) -> TypeValueInitType { @@ -1274,6 +1280,23 @@ namespace mlirgen }, genContext); + // what the declaration says (`@keeps`, `@borrows`) goes with the global its calls go + // through, which is all -mm=own sees of the function + if (declared && !genContext.dummyRun) + { + auto globalName = isFullNamespaceName ? funcName.str() : getFullNamespaceName(funcName).str(); + if (auto globalOp = theModule.lookupSymbol(globalName)) + { + for (auto *name : {OWN_DECLARED_KEEPS_ATTR_NAME, OWN_DECLARED_BORROWS_ATTR_NAME, OWN_DECLARED_PARAM_NAMES_ATTR_NAME}) + { + if (auto attr = declared->getAttr(name)) + { + globalOp->setAttr(name, attr); + } + } + } + } + return mlir::success(); } diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 202eb7178..10e30a333 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -3150,6 +3150,72 @@ class MLIRGenImpl } } + // `@keeps("p")`, `@borrows("this", "p")` (OWN_DECLARED_KEEPS_ATTR_NAME): the parameters they + // name, as indices of the function's arguments - after the captured values, when its type has + // an input more than it has parameters. + mlir::LogicalResult processOwnParamMarkers(mlir::Location location, FunctionLikeDeclarationBase functionLikeDeclarationBaseAST, + ArrayRef params, mlir_ts::FunctionType funcType, SmallVector &attrs, + const GenContext &genContext) + { + auto result = mlir::success(); + iterateDecorators(functionLikeDeclarationBaseAST, genContext, [&](StringRef name, SmallVector args) { + if (name != KEEPS_DECORATOR && name != BORROWS_DECORATOR) + { + return; + } + + auto attrName = name == KEEPS_DECORATOR ? OWN_DECLARED_KEEPS_ATTR_NAME : OWN_DECLARED_BORROWS_ATTR_NAME; + if (llvm::any_of(attrs, [&](mlir::NamedAttribute &attr) { return attr.getName() == attrName; })) + { + emitError(location) << "'@" << name << "' is given twice"; + result = mlir::failure(); + return; + } + + if (args.empty()) + { + emitError(location) << "'@" << name << "' names no parameter"; + result = mlir::failure(); + return; + } + + auto first = funcType && funcType.getInputs().size() == params.size() + 1 ? 1 : 0; + SmallVector indices; + for (auto arg : args) + { + auto found = llvm::find_if(params, [&](auto ¶m) { return param->getName() == arg; }); + if (found == params.end()) + { + emitError(location) << "'@" << name << "' names '" << arg << "', which is not a parameter of this function"; + result = mlir::failure(); + return; + } + + auto index = first + static_cast(found - params.begin()); + if (!llvm::is_contained(indices, index)) + { + indices.push_back(index); + } + } + + llvm::sort(indices); + attrs.push_back({builder.getStringAttr(attrName), builder.getDenseI32ArrayAttr(indices)}); + + if (llvm::none_of(attrs, [](mlir::NamedAttribute &attr) { return attr.getName() == OWN_DECLARED_PARAM_NAMES_ATTR_NAME; })) + { + SmallVector names(first, CAPTURED_NAME); + for (auto ¶m : params) + { + names.push_back(param->getName()); + } + + attrs.push_back({builder.getStringAttr(OWN_DECLARED_PARAM_NAMES_ATTR_NAME), builder.getStrArrayAttr(names)}); + } + }); + + return result; + } + bool processFunctionAttributes(mlir::Location location, StringRef fullName, FunctionLikeDeclarationBase functionLikeDeclarationBaseAST, SmallVector &attrs, const GenContext &genContext, bool suppressExport = false) @@ -3386,7 +3452,7 @@ class MLIRGenImpl return mlir::success(); } - mlir::LogicalResult mlirGenFunctionLikeDeclarationDynamicImport(mlir::Location location, StringRef funcName, + mlir::LogicalResult mlirGenFunctionLikeDeclarationDynamicImport(mlir::Location location, mlir_ts::FuncOp declared, StringRef funcName, mlir_ts::FunctionType functionType, StringRef dllFuncName, const GenContext &genContext, bool isFullNamespaceName = true); mlir::LogicalResult mlirGenFunctionEntry(mlir::Location location, FunctionPrototypeDOM::TypePtr funcProto, @@ -4042,6 +4108,32 @@ class MLIRGenImpl // we need to create dummy op to be able to use both values with cast and without cast auto wrappedValue = builder.create(location, castedValue.getType(), castedValue, exprValue); + // A generator's local is a field of its state object, and so would the narrowed copy be: a + // second reference to what the local holds, which -mm=own cannot have, and a value that + // goes stale when the local is assigned. It is a view of the local instead, read and cast + // at each use, which a resume after a yield reads again. (While the state type is still + // being found, the copy only has to give the name its type.) + if (genContext.allocateVarsInContextThis) + { + auto loadOp = exprValue.getDefiningOp(); + if (genContext.passResult || !loadOp) + { + GenContext discoveryContext(genContext); + discoveryContext.allocateVarsInContextThis = false; + return !!registerVariable( + location, parameterName, false, VariableType::Const, + [&](mlir::Location, const GenContext &) -> TypeValueInitType { + return {safeType, wrappedValue, TypeProvided::Yes}; + }, + discoveryContext, false, true) ? mlir::success() : mlir::failure(); + } + + auto view = std::make_shared(parameterName, safeType, location); + view->setReadWriteAccess(); + view->setNarrowedType(safeType); + return declare(location, view, loadOp.getReference(), genContext); + } + return !!registerVariable( location, parameterName, false, VariableType::Const, @@ -8490,9 +8582,14 @@ class MLIRGenImpl ValueOrLogicalResult mlirGen(CallExpression callExpression, const GenContext &genContext); mlir::LogicalResult mlirGenArrayForEach(mlir::Location location, ArrayRef operands, - const GenContext &genContext) + const GenContext &outerContext) { SymbolTableScopeT varScope(symbolTable); + // a loop with no yield in it: inside a generator its locals stay in the frame, not the state + // object, where `.a` - the source array, read through - would be a second reference to it + GenContext genContext(outerContext); + genContext.allocateVarsInContextThis = false; + genContext.allocateVarsOutsideOfOperation = false; auto arraySrc = operands[0]; auto funcSrc = operands[1]; @@ -8528,9 +8625,14 @@ class MLIRGenImpl } ValueOrLogicalResult mlirGenArrayEvery(mlir::Location location, ArrayRef operands, - const GenContext &genContext) + const GenContext &outerContext) { SymbolTableScopeT varScope(symbolTable); + // a loop with no yield in it: inside a generator its locals stay in the frame, not the state + // object, where `.a` - the source array, read through - would be a second reference to it + GenContext genContext(outerContext); + genContext.allocateVarsInContextThis = false; + genContext.allocateVarsOutsideOfOperation = false; auto varName = ".ev"; auto initVal = builder.create(location, getBooleanType(), builder.getBoolAttr(true)); @@ -8581,9 +8683,14 @@ class MLIRGenImpl } ValueOrLogicalResult mlirGenArraySome(mlir::Location location, ArrayRef operands, - const GenContext &genContext) + const GenContext &outerContext) { SymbolTableScopeT varScope(symbolTable); + // a loop with no yield in it: inside a generator its locals stay in the frame, not the state + // object, where `.a` - the source array, read through - would be a second reference to it + GenContext genContext(outerContext); + genContext.allocateVarsInContextThis = false; + genContext.allocateVarsOutsideOfOperation = false; auto varName = ".sm"; auto initVal = builder.create(location, getBooleanType(), builder.getBoolAttr(false)); @@ -9850,9 +9957,14 @@ class MLIRGenImpl const GenContext &genContext); ValueOrLogicalResult mlirGenAppendArrayByEachElement(mlir::Location location, mlir::Value arrayDest, mlir::Value arraySrc, - const GenContext &genContext) + const GenContext &outerContext) { SymbolTableScopeT varScope(symbolTable); + // a loop with no yield in it: inside a generator its locals stay in the frame, not the state + // object, where `.a` - the source array, read through - would be a second reference to it + GenContext genContext(outerContext); + genContext.allocateVarsInContextThis = false; + genContext.allocateVarsOutsideOfOperation = false; // register vals auto srcArrayVarDecl = std::make_shared(".src_array", arraySrc.getType(), location); @@ -12466,6 +12578,20 @@ class MLIRGenImpl } } + if (funcOp) + { + auto &methodInfo = methodIndex < 0 ? methodInfos.back() : methodInfos[methodIndex]; + if (auto keeps = funcOp->getAttrOfType(OWN_DECLARED_KEEPS_ATTR_NAME)) + { + methodInfo.ownKeeps.assign(keeps.asArrayRef().begin(), keeps.asArrayRef().end()); + } + + if (auto borrows = funcOp->getAttrOfType(OWN_DECLARED_BORROWS_ATTR_NAME)) + { + methodInfo.ownBorrows.assign(borrows.asArrayRef().begin(), borrows.asArrayRef().end()); + } + } + if (propertyName.size() > 0) { addAccessor(accessLevel); @@ -13818,7 +13944,8 @@ class MLIRGenImpl declExports << ss.str().str(); } - void addFunctionDeclarationToExport(FunctionPrototypeDOM::TypePtr funcProto, NamespaceInfo::TypePtr elementNamespace, StringRef dllName) + void addFunctionDeclarationToExport(FunctionPrototypeDOM::TypePtr funcProto, NamespaceInfo::TypePtr elementNamespace, StringRef dllName, + mlir_ts::FuncOp funcOp = {}) { // TODO: add distinct declaration @@ -13828,7 +13955,12 @@ class MLIRGenImpl SmallVector out; llvm::raw_svector_ostream ss(out); MLIRDeclarationPrinter dp(ss, classSpecializationNamer()); - dp.print(funcProto->getNameWithoutNamespace(), elementNamespace, funcProto->getFuncType(), dllName); + auto marker = [&](StringRef name) { + auto attr = funcOp ? funcOp->getAttrOfType(name) : mlir::DenseI32ArrayAttr(); + return attr ? attr.asArrayRef() : ArrayRef(); + }; + dp.print(funcProto->getNameWithoutNamespace(), elementNamespace, funcProto->getFuncType(), dllName, + marker(OWN_DECLARED_KEEPS_ATTR_NAME), marker(OWN_DECLARED_BORROWS_ATTR_NAME)); declExports << ss.str().str(); } diff --git a/tslang/lib/TypeScript/MLIRGenVariables.cpp b/tslang/lib/TypeScript/MLIRGenVariables.cpp index e8e5d5751..273f4b055 100644 --- a/tslang/lib/TypeScript/MLIRGenVariables.cpp +++ b/tslang/lib/TypeScript/MLIRGenVariables.cpp @@ -1163,6 +1163,18 @@ namespace mlirgen loadOp->setAttr(INVARIANT_ATTR_NAME, builder.getBoolAttr(true)); } + // a generator's narrowed local: what the local holds now, cast as the test narrowed it + if (auto narrowedType = value.second->getNarrowedType()) + { + auto castValue = castToNarrowedType(location, loadOp, narrowedType, genContext); + if (!castValue) + { + return mlir::Value(); + } + + return builder.create(location, narrowedType, castValue, loadOp); + } + return loadOp; } diff --git a/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp b/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp index c750f26b6..357c1def0 100644 --- a/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp +++ b/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp @@ -101,8 +101,12 @@ class OwnedReturnConsumptionPass } } + // a class's method, which takes its object first: what a virtual call can reach. An + // object literal's method, or a generator's `next`, shares the member name only. + auto inputs = funcOp.getFunctionType().getInputs(); + auto isClassMethod = !inputs.empty() && mlir::isa(inputs.front()); auto dot = name.rfind('.'); - if (dot != mlir::StringRef::npos && dot + 1 < name.size()) + if (isClassMethod && dot != mlir::StringRef::npos && dot + 1 < name.size()) { methodsByMemberName[name.substr(dot + 1)].push_back(name); } diff --git a/tslang/lib/TypeScript/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h index a7dddfb4f..dff39695a 100644 --- a/tslang/lib/TypeScript/OwnershipFacts.h +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -303,15 +303,17 @@ inline bool isFresh(mlir::Value value) return true; } - if (mlir::isa(def) && directCallee(def)) + // what the signature pass knows of a call: another own module's `..new`, a `@borrows` callee's + // state, an iterator's `next()` - a declared callee included + if (def->hasAttr(OWN_FRESH_RESULT_ATTR_NAME)) { - auto callee = mlir::SymbolTable::lookupNearestSymbolFrom(def, directCallee(def)); - return callee && !callee.isDeclaration(); + return true; } - if (def->hasAttr(OWN_FRESH_RESULT_ATTR_NAME)) + if (mlir::isa(def) && directCallee(def)) { - return true; + auto callee = mlir::SymbolTable::lookupNearestSymbolFrom(def, directCallee(def)); + return callee && !callee.isDeclaration(); } if (mlir::isa(op)) { - if (placeReadOf(loadOp.getResult())) + // a yield's read of a field that only yields (OWN_YIELDED_FIELDS_ATTR_NAME) takes + // the value out: fresh, not a borrow of the state + if (placeReadOf(loadOp.getResult()) && !loadOp->hasAttr(OWN_FRESH_RESULT_ATTR_NAME)) { placeReads.push_back(loadOp); } } + else if (auto releaseSlotOp = mlir::dyn_cast(op); releaseSlotOp && isYieldedField(releaseSlotOp.getSlot())) + { + // what the field held was yielded, the caller's now + toErase.insert(op); + return; + } else if (auto releaseSlotOp = mlir::dyn_cast(op)) { // an overwrite of a field or an element, or an assignment to a captured variable @@ -803,6 +811,46 @@ class OwnershipInferencePass return fields; } + // Is `ref` a field of a generator's state object that only yields (OWN_YIELDED_FIELDS_ATTR_NAME)? + bool isYieldedField(mlir::Value ref) + { + auto propertyRefOp = ref.getDefiningOp(); + if (!propertyRefOp) + { + return false; + } + + auto recordType = propertyRefOp->getOperand(0).getType(); + if (auto objectType = mlir::dyn_cast(recordType)) + { + recordType = objectType.getStorageType(); + } + + if (auto storageType = mlir::dyn_cast(recordType)) + { + recordType = mlir_ts::TupleType::get(&getContext(), storageType.getFields()); + } + + auto module = getFunction()->getParentOfType(); + auto entries = module ? module->getAttrOfType(OWN_YIELDED_FIELDS_ATTR_NAME) : mlir::ArrayAttr(); + if (!entries) + { + return false; + } + + for (auto entry : entries.getAsRange()) + { + if (mlir::cast(entry[0]).getValue() == recordType && + llvm::is_contained(mlir::cast(entry[1]).asArrayRef(), + static_cast(propertyRefOp.getPosition()))) + { + return true; + } + } + + return false; + } + // Is `ref` a field of a generator's state object that borrows (other than the `.captured` box)? bool isBorrowingField(mlir::Value ref) { @@ -1122,6 +1170,13 @@ class OwnershipInferencePass return; } + // returned by a function that says so (`@borrows`): its callers hold the result to + // the same bound, and the signature pass checked the arguments are its own + if (mlir::isa(user) && getFunction()->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME)) + { + return; + } + uses.push_back(user); if (isCall(user)) { @@ -1228,6 +1283,15 @@ class OwnershipInferencePass for (auto &fill : closure->fills) { + // a parameter this function does not own, in a box that does not borrow: the box + // cannot take rc's reference for it, the caller's still holds it; left to the + // second-reference error, which says the callers would have to give it up + auto param = borrowedParam(fill.second.getValue()); + if (!closure->borrowsValues && param >= 0 && !llvm::is_contained(ownedParams(getFunction()), param)) + { + continue; + } + if (auto *retain = retainBefore(fill.second.getValue(), fill.second)) { closure->retains.push_back(retain); @@ -1299,7 +1363,7 @@ class OwnershipInferencePass for (auto *user : loadOp.getReference().getUsers()) { auto earlier = mlir::dyn_cast(user); - auto *retain = earlier && earlier.getResult().hasOneUse() ? *earlier->getUsers().begin() : nullptr; + auto *retain = earlier ? soleUser(earlier.getResult()) : nullptr; if (retain && mlir::isa(retain) && !captureRetains.contains(retain) && twinReadAfter(retain, earlier.getResult()) == value && retain->getBlock() == store->getBlock() && retain->isBeforeInBlock(store)) @@ -2354,16 +2418,17 @@ class OwnershipInferencePass continue; } - // A result that borrows what it was given borrows this too, and is walked as it. A - // call known to make its result (`__own_fresh_result`: through a field that only - // ever holds one function) hands back a block of the caller's own - the `{value, - // done}` record of `it.next()` points nowhere into `it`, and what it does after - // its next call cannot be a use of the method read out of `it`. - if (isCall(user) && user->hasAttr(OWN_FRESH_RESULT_ATTR_NAME)) + // A call known to make its result (`__own_fresh_result`: an iterator's `next()`, a field + // that only ever holds one function, another own module's `..new`) hands back a block of + // the caller's own - the `{value, done}` record of `it.next()` points nowhere into `it`, + // and what it does after its next call cannot be a use of the method read out of `it` - + // unless it is a generator's state that borrows its arguments (`__own_result_bounded`). + if (isCall(user) && user->hasAttr(OWN_FRESH_RESULT_ATTR_NAME) && !user->hasAttr(OWN_RESULT_BOUNDED_ATTR_NAME)) { continue; } + // A result that borrows what it was given borrows this too, and is walked as it. for (auto result : user->getResults()) { if (mayPointInto(result) && seen.insert(result).second) @@ -3649,7 +3714,9 @@ class OwnershipInferencePass // `return [...a]`: the builder of isBuiltArrayRead, read once, when it is done, and returned. // The return's `ts.Retain` (`retain`) is the reference the caller gets: the fresh array moves // into the builder and then out of the function, and nothing else holds it. The builder's - // other reads only bind its `push` (`ts.ThisSymbolRef @__array_push`) or are not used. + // other reads only bind its `push` (`ts.ThisSymbolRef @__array_push`) or are not used. A store + // into a field is the same move, into a container: `h.c = [...a]`, and a generator's local + // (`const groups = [...result]`), which is a field of its state object. bool isBuiltArrayReturned(mlir::Operation *retain, mlir::Value value) { auto read = value.getDefiningOp(); @@ -3673,7 +3740,9 @@ class OwnershipInferencePass auto resultSlot = storeOp && storeOp.getValue() == read.getResult() ? storeOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); - if (!resultSlot || !isResultSlot(resultSlot) || returned) + auto intoField = storeOp && storeOp.getValue() == read.getResult() && + storeOp.getReference().getDefiningOp(); + if (!((resultSlot && isResultSlot(resultSlot)) || intoField) || returned) { return false; } @@ -4269,10 +4338,32 @@ class OwnershipInferencePass // rc reads a place twice for one copy - `.map(f)` boxes its array so - and retains the first // read, whose only use is that retain: the read of the same place that follows it in its block, // with nothing stored into the place between, is the same value. Null when there is none. + // The one op that uses `value`, but `--di`'s ts.DebugVariable, which only names it; or null. + static mlir::Operation *soleUser(mlir::Value value) + { + mlir::Operation *found = nullptr; + for (auto *user : value.getUsers()) + { + if (mlir::isa(user)) + { + continue; + } + + if (found) + { + return nullptr; + } + + found = user; + } + + return found; + } + static mlir::Value twinReadAfter(mlir::Operation *retain, mlir::Value value) { auto first = value.getDefiningOp(); - if (!first || !mlir::isa(retain) || !value.hasOneUse() || retain->getBlock() != first->getBlock()) + if (!first || !mlir::isa(retain) || soleUser(value) != retain || retain->getBlock() != first->getBlock()) { return {}; } diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp index 22df904aa..366c85480 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp @@ -190,9 +190,30 @@ class OwnershipSignaturePass : public mlir::PassWrappergetNumResults() == 1) + { + call.op->setAttr(OWN_RESULT_BOUNDED_ATTR_NAME, mlir::DenseI32ArrayAttr::get(&getContext(), bounded)); + // and the caller owns it (the generator's state), as `@borrows` says: fresh, though + // the function may be another module's + call.op->setAttr(OWN_FRESH_RESULT_ATTR_NAME, mlir::UnitAttr::get(&getContext())); + } + + // an iterator's `next()`, whoever implements it, and a generator's state, whoever makes + // it: see isIteratorNext + if (isIteratorNext(call.op) || makesGeneratorState(call.op)) + { + call.op->setAttr(OWN_FRESH_RESULT_ATTR_NAME, mlir::UnitAttr::get(&getContext())); + } + if (!call.callees.known) { continue; @@ -368,6 +389,9 @@ class OwnershipSignaturePass : public mlir::PassWrapper funcs; + // the symbols the call names, whether or not it is known: a direct callee, or every + // member of a virtual slot's family (for the facts a declaration says, declaredFacts) + llvm::SmallVector named; }; struct Call @@ -428,6 +452,623 @@ class OwnershipSignaturePass : public mlir::PassWrapper factsLocal; llvm::DenseMap factsEffective; + // ---- Facts a signature says (`@keeps`, `@borrows`) ---- + // + // A function that says them has them whoever calls it: a caller in another module reads them + // from the declaration it re-parses. Its body is checked against them instead (checkDeclared). + + static llvm::ArrayRef declared(mlir::Operation *funcOp, llvm::StringRef name) + { + if (auto attr = funcOp->getAttrOfType(name)) + { + return attr.asArrayRef(); + } + + return {}; + } + + static bool declaresFacts(mlir::Operation *funcOp) + { + return funcOp->hasAttr(OWN_DECLARED_KEEPS_ATTR_NAME) || funcOp->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME); + } + + // What every function a call names declares under `name`, when they all declare the same; + // nothing when one of them does not. + llvm::SmallVector declaredByCallees(const Callees &callees, llvm::StringRef name) + { + llvm::SmallVector agreed; + for (auto [index, symbol] : llvm::enumerate(callees.named)) + { + // a function, or the global holding the address of one a library exports (a dynamic + // import), which MLIRGen gave the declaration's markers + mlir::Operation *declaring = functions.lookup(symbol.getValue()); + if (!declaring) + { + declaring = globals.lookup(symbol.getValue()); + } + + if (!declaring || !declaring->hasAttr(name)) + { + return {}; + } + + auto indices = declared(declaring, name); + if (index == 0) + { + agreed.assign(indices.begin(), indices.end()); + } + else if (llvm::ArrayRef(agreed) != indices) + { + return {}; + } + } + + return agreed; + } + + // The parameters, for an error, by the names MLIRGen recorded beside the marker. + static std::string describeParams(mlir_ts::FuncOp funcOp, llvm::ArrayRef indices) + { + if (indices.empty()) + { + return "nothing"; + } + + auto names = funcOp->getAttrOfType(OWN_DECLARED_PARAM_NAMES_ATTR_NAME); + std::string text; + llvm::raw_string_ostream os(text); + for (auto [position, index] : llvm::enumerate(indices)) + { + os << (position ? ", " : "") << "'"; + if (names && static_cast(index) < names.size()) + { + os << mlir::cast(names[index]).getValue(); + } + else + { + os << "#" << index; + } + + os << "'"; + } + + return os.str(); + } + + // Functions this module defines that `@borrows`, and those whose state was found to borrow. + llvm::DenseMap> borrowsFound; + + // The argument of `funcOp` that `value` is, or a field of: its own value, its slot's, or a + // read out of either. -1 for anything else. + static int32_t parameterOf(mlir::Value value, mlir_ts::FuncOp funcOp) + { + auto root = rootOf(value); + if (auto loadOp = root.getDefiningOp()) + { + if (auto propertyRefOp = loadOp.getReference().getDefiningOp()) + { + return parameterOf(propertyRefOp->getOperand(0), funcOp); + } + + auto varOp = loadOp.getReference().getDefiningOp(); + int index = -1; + if (varOp && isParameterSlot(varOp, index) && varOp->getParentOfType() == funcOp) + { + return index; + } + + return -1; + } + + auto argument = mlir::dyn_cast(root); + if (argument && argument.getOwner()->isEntryBlock() && argument.getOwner()->getParentOp() == funcOp.getOperation()) + { + return static_cast(argument.getArgNumber()); + } + + return -1; + } + + // ---- The iterator protocol ---- + // + // `it.next()` returns a `{value, done}` record, and under own the caller owns what it holds, + // whoever implements it: a generator's `next` moves what it yields into the record (a yield of + // a borrow is an error in its body), and any other `next` is a function whose callers this pass + // cannot all see, which may not return a borrow. So the result is fresh even when the call goes + // through the `Iterator` interface, or into another module - the default library's `matchAll`. + + // A call of a function named `next` - read through an interface, out of a field, or by its + // symbol - whose one result is a `{value, done}` record. + static bool isIteratorNext(mlir::Operation *op) + { + if (op->getNumResults() != 1) + { + return false; + } + + auto tupleType = mlir::dyn_cast(op->getResult(0).getType()); + if (!tupleType || tupleType.getFields().size() != 2) + { + return false; + } + + auto fieldName = [&](int index) { + auto id = mlir::dyn_cast_or_null(tupleType.getFields()[index].id); + return id ? id.getValue() : llvm::StringRef(); + }; + if (fieldName(0) != "value" || fieldName(1) != "done") + { + return false; + } + + auto isNext = [](llvm::StringRef name) { return name == "next" || name.ends_with(".next"); }; + if (auto callee = directCallee(op)) + { + return isNext(callee.getValue()); + } + + auto function = calleeValue(op); + if (!function) + { + return false; + } + + if (auto getMethodOp = function.getDefiningOp()) + { + function = getMethodOp.getBoundFunc(); + } + + if (auto boundOp = function.getDefiningOp()) + { + function = boundOp.getFunc(); + } + + if (auto loadOp = function.getDefiningOp()) + { + if (auto refOp = loadOp.getReference().getDefiningOp()) + { + auto identifier = mlir::dyn_cast(refOp.getIdentifier()); + return identifier && isNext(identifier.getValue()); + } + + if (auto propertyRefOp = loadOp.getReference().getDefiningOp()) + { + auto storageType = storageTupleOfType(propertyRefOp->getOperand(0).getType()); + if (!storageType || propertyRefOp.getPosition() >= storageType.getFields().size()) + { + return false; + } + + auto id = mlir::dyn_cast_or_null(storageType.getFields()[propertyRefOp.getPosition()].id); + return id && isNext(id.getValue()); + } + + return false; + } + + if (auto refOp = function.getDefiningOp()) + { + return isNext(refOp.getIdentifier()); + } + + if (auto refOp = function.getDefiningOp()) + { + return isNext(refOp.getIdentifier()); + } + + if (auto refOp = function.getDefiningOp()) + { + return isNext(refOp.getIdentifier()); + } + + if (auto refOp = function.getDefiningOp()) + { + return isNext(refOp.getIdentifier()); + } + + return false; + } + + // A generator another module made: its state object's type is only its shape here, and what each + // field owns - a local, a yielded value now the caller's, what it borrows from its arguments - is + // known in that module alone. A release routine made here from the shape would give back what is + // not the state's. So this module's gives back none of it: only the `.captured` box's cells, and + // their values too unless the maker says it borrows them (`@borrows`). What the generator's own + // locals still hold when it is given up early leaks rather than being freed twice. + void findImportedStates(mlir::ModuleOp module) + { + llvm::MapVector states; + for (auto &call : calls) + { + if (!makesGeneratorState(call.op) || (call.callees.known && !call.callees.funcs.empty())) + { + continue; + } + + auto tupleType = storageTupleOfType(call.op->getResult(0).getType()); + auto borrows = !declaredByCallees(call.callees, OWN_DECLARED_BORROWS_ATTR_NAME).empty(); + states[tupleType] = states.lookup(tupleType) || borrows; + } + + if (states.empty()) + { + return; + } + + auto *context = &getContext(); + llvm::SmallVector entries; + if (auto existing = module->getAttrOfType(OWN_BORROWING_FIELDS_ATTR_NAME)) + { + entries.append(existing.begin(), existing.end()); + } + + for (auto &[type, borrows] : states) + { + auto tupleType = mlir::cast(type); + auto captured = fieldIndex(tupleType, CAPTURED_NAME); + // the box first, when it borrows; -1 names no field, so a box that owns is released + llvm::SmallVector fields{borrows && captured >= 0 ? captured : -1}; + for (auto [index, field] : llvm::enumerate(tupleType.getFields())) + { + if (static_cast(index) != captured && ownsHeap(module.getLoc(), field.type)) + { + fields.push_back(static_cast(index)); + } + } + + entries.push_back(mlir::ArrayAttr::get(context, {mlir::TypeAttr::get(type), mlir::DenseI32ArrayAttr::get(context, fields)})); + } + + module->setAttr(OWN_BORROWING_FIELDS_ATTR_NAME, mlir::ArrayAttr::get(context, entries)); + } + + // A call whose result is a generator's state object (an object whose fields are `.step` and + // `next`): a generator function made it, a `ts.New` block it hands over (spec 18.1), which the + // caller owns - in another module too, as the iteration that follows needs. + static bool makesGeneratorState(mlir::Operation *op) + { + if (op->getNumResults() != 1 || !mlir::isa(op->getResult(0).getType())) + { + return false; + } + + auto tupleType = storageTupleOfType(op->getResult(0).getType()); + if (!tupleType) + { + return false; + } + + auto hasStep = false; + auto hasNext = false; + for (auto field : tupleType.getFields()) + { + if (auto id = mlir::dyn_cast_or_null(field.id)) + { + hasStep = hasStep || id.getValue() == ".step"; + hasNext = hasNext || id.getValue() == "next"; + } + } + + return hasStep && hasNext; + } + + // storageTupleOf, without the pass's context: an object's storage, a reference's or a tuple + static mlir_ts::TupleType storageTupleOfType(mlir::Type type) + { + if (auto objectType = mlir::dyn_cast(type)) + { + if (auto storageType = mlir::dyn_cast(objectType.getStorageType())) + { + return mlir_ts::TupleType::get(type.getContext(), storageType.getFields()); + } + + // an object a declaration names by its shape (another module's generator) + return mlir::dyn_cast(objectType.getStorageType()); + } + + if (auto refType = mlir::dyn_cast(type)) + { + type = refType.getElementType(); + } + else if (auto valueRefType = mlir::dyn_cast(type)) + { + type = valueRefType.getElementType(); + } + + return mlir::dyn_cast(type); + } + + // ---- Fields a generator yields ---- + // + // A generator's locals are fields of its state object, and `yield groups` puts what one holds into + // the `{value, done}` record its `next` returns: a borrow of the state, which the caller cannot + // keep. When the field is read for nothing but yields, and each value stored into it is yielded + // before the generator can stop (later in the same block), the yield moves the value instead: + // the read is fresh (OWN_FRESH_RESULT_ATTR_NAME) and the field owns nothing + // (OWN_YIELDED_FIELDS_ATTR_NAME). + + // `ts.Store(value, ts.PropertyRef(record, i))`, where `record` is a local a `ts.Constant` + // initializes: a field of a record built to be returned, a yield's. + static bool isRecordFieldStore(mlir::Operation *user, mlir::Value value) + { + auto storeOp = mlir::dyn_cast(user); + auto propertyRefOp = storeOp && storeOp.getValue() == value + ? storeOp.getReference().getDefiningOp() + : mlir_ts::PropertyRefOp(); + auto recordOp = propertyRefOp ? propertyRefOp->getOperand(0).getDefiningOp() : mlir_ts::VariableOp(); + return recordOp && recordOp.getInitializer() && recordOp.getInitializer().getDefiningOp(); + } + + void findYieldedFields(mlir::ModuleOp module) + { + // (state type, field) -> its reads and its stores + struct FieldUses + { + llvm::SmallVector reads; + llvm::SmallVector stores; + bool other = false; + }; + + llvm::MapVector, FieldUses> fields; + module.walk([&](mlir_ts::PropertyRefOp propertyRefOp) { + auto tupleType = storageTupleOf(propertyRefOp->getOperand(0).getType()); + auto position = static_cast(propertyRefOp.getPosition()); + if (!tupleType || fieldIndex(tupleType, ".step") < 0 || position == fieldIndex(tupleType, CAPTURED_NAME) || + !mlir::isa(propertyRefOp.getType()) || + !ownsHeap(propertyRefOp.getLoc(), mlir::cast(propertyRefOp.getType()).getElementType())) + { + return; + } + + auto &uses = fields[{tupleType, position}]; + for (auto *user : propertyRefOp->getUsers()) + { + if (auto loadOp = mlir::dyn_cast(user)) + { + uses.reads.push_back(loadOp); + } + else if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getReference() == propertyRefOp.getResult()) + { + uses.stores.push_back(storeOp); + } + else if (!mlir::isa(user)) + { + uses.other = true; + } + } + }); + + // a field the signature pass already found borrowing is the caller's, not the generator's + llvm::DenseSet> borrowing; + if (auto entries = module->getAttrOfType(OWN_BORROWING_FIELDS_ATTR_NAME)) + { + for (auto entry : entries.getAsRange()) + { + for (auto index : mlir::cast(entry[1]).asArrayRef()) + { + borrowing.insert({mlir::cast(entry[0]).getValue(), index}); + } + } + } + + llvm::MapVector> yielded; + for (auto &[key, uses] : fields) + { + if (uses.other || uses.reads.empty() || uses.stores.empty() || borrowing.contains(key)) + { + continue; + } + + // read only to be put into a yield's record + auto onlyYielded = llvm::all_of(uses.reads, [](mlir_ts::LoadOp loadOp) { + auto users = llvm::make_filter_range(loadOp->getUsers(), [](mlir::Operation *user) { + return !mlir::isa(user); + }); + return llvm::hasSingleElement(users) && isRecordFieldStore(*users.begin(), loadOp.getResult()); + }); + + // each value stored is yielded later in its block: nothing between stops the generator + auto eachYielded = llvm::all_of(uses.stores, [&](mlir_ts::StoreOp storeOp) { + return llvm::any_of(uses.reads, [&](mlir_ts::LoadOp loadOp) { + return loadOp->getBlock() == storeOp->getBlock() && storeOp->isBeforeInBlock(loadOp); + }); + }); + + if (!onlyYielded || !eachYielded) + { + continue; + } + + yielded[key.first].push_back(key.second); + for (auto loadOp : uses.reads) + { + loadOp->setAttr(OWN_FRESH_RESULT_ATTR_NAME, mlir::UnitAttr::get(&getContext())); + } + } + + if (yielded.empty()) + { + return; + } + + auto *context = &getContext(); + llvm::SmallVector entries; + for (auto &[type, indices] : yielded) + { + entries.push_back(mlir::ArrayAttr::get(context, {mlir::TypeAttr::get(type), mlir::DenseI32ArrayAttr::get(context, indices)})); + } + + module->setAttr(OWN_YIELDED_FIELDS_ATTR_NAME, mlir::ArrayAttr::get(context, entries)); + } + + // A function that says `@borrows` and makes no state itself, but returns what a call that has + // a bounded result gives it (`return this.value.matchAll(expr)`): it borrows the parameters + // the call's bounded arguments are, or are read out of. + void findBoundedForwarders() + { + llvm::DenseMap calleesOf; + for (auto &call : calls) + { + calleesOf[call.op] = &call.callees; + } + + for (auto &entry : functions) + { + auto funcOp = entry.second; + if (funcOp.isDeclaration() || !funcOp->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME) || borrowsFound.count(funcOp)) + { + continue; + } + + llvm::SmallVector found; + auto forwards = true; + for (auto value : returnedValues(funcOp)) + { + if (holdsNoBlock(value)) + { + continue; + } + + auto *call = rootOf(value).getDefiningOp(); + auto known = call ? calleesOf.find(call) : calleesOf.end(); + if (known == calleesOf.end()) + { + forwards = false; + break; + } + + llvm::SmallVector bounded = declaredByCallees(*known->second, OWN_DECLARED_BORROWS_ATTR_NAME); + if (bounded.empty() && known->second->funcs.size() == 1) + { + auto made = declared(known->second->funcs.front(), OWN_RESULT_BOUNDED_ATTR_NAME); + bounded.assign(made.begin(), made.end()); + } + + if (bounded.empty()) + { + forwards = false; + break; + } + + auto args = callArgs(call); + for (auto index : bounded) + { + auto param = static_cast(index) < args.size() ? parameterOf(args[index], funcOp) : -1; + if (param < 0) + { + forwards = false; + break; + } + + if (!llvm::is_contained(found, param)) + { + found.push_back(param); + } + } + } + + if (forwards && !found.empty()) + { + llvm::sort(found); + borrowsFound[funcOp] = found; + } + } + } + + // A body that does other than its signature says, and an override that says other than the + // method it overrides: either way a caller that trusts the signature frees twice or leaks. + void checkDeclared() + { + for (auto &entry : functions) + { + auto funcOp = entry.second; + if (funcOp.isDeclaration()) + { + continue; + } + + if (funcOp->hasAttr(OWN_DECLARED_KEEPS_ATTR_NAME)) + { + auto local = factsLocal.lookup(funcOp); + auto says = declared(funcOp, OWN_DECLARED_KEEPS_ATTR_NAME); + if (local.borrows >= 0) + { + funcOp.emitError("'") << funcOp.getSymName() + << "' returns a borrow of an argument, which '@" KEEPS_DECORATOR + "' cannot say; -mm=own cannot export that yet"; + signalPassFailure(); + } + else if (llvm::ArrayRef(local.owned) != says) + { + funcOp.emitError("'@" KEEPS_DECORATOR "' of '") + << funcOp.getSymName() << "' names " << describeParams(funcOp, says) << ", but its body keeps " + << describeParams(funcOp, local.owned); + signalPassFailure(); + } + } + + if (funcOp->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME)) + { + auto says = declared(funcOp, OWN_DECLARED_BORROWS_ATTR_NAME); + auto found = borrowsFound.find(funcOp); + if (found == borrowsFound.end()) + { + funcOp.emitError("'@" BORROWS_DECORATOR "' of '") + << funcOp.getSymName() << "' names " << describeParams(funcOp, says) + << ", but it neither makes a generator whose state borrows its arguments nor returns one"; + signalPassFailure(); + } + else if (llvm::ArrayRef(found->second) != says) + { + funcOp.emitError("'@" BORROWS_DECORATOR "' of '") + << funcOp.getSymName() << "' names " << describeParams(funcOp, says) + << ", but the state it makes borrows " << describeParams(funcOp, found->second); + signalPassFailure(); + } + } + } + + // the members of a family a declaring method is in: each says the same + for (auto &[key, members] : families) + { + mlir_ts::FuncOp declaring; + for (auto member : members) + { + if (auto funcOp = functions.lookup(member.getValue()); funcOp && declaresFacts(funcOp)) + { + declaring = funcOp; + break; + } + } + + if (!declaring) + { + continue; + } + + for (auto member : members) + { + auto funcOp = functions.lookup(member.getValue()); + if (!funcOp || funcOp == declaring) + { + continue; + } + + for (auto *name : {OWN_DECLARED_KEEPS_ATTR_NAME, OWN_DECLARED_BORROWS_ATTR_NAME}) + { + if (funcOp->hasAttr(name) != declaring->hasAttr(name) || declared(funcOp, name) != declared(declaring, name)) + { + funcOp.emitError("'") << funcOp.getSymName() << "' overrides '" << declaring.getSymName() + << "', and has to say the same '@" KEEPS_DECORATOR "' and '@" BORROWS_DECORATOR + "'"; + signalPassFailure(); + break; + } + } + } + } + } + // Functions that destroy nothing their caller can reach. llvm::DenseSet noDrops; @@ -613,6 +1254,7 @@ class OwnershipSignaturePass : public mlir::PassWrapper(op)) @@ -633,6 +1275,26 @@ class OwnershipSignaturePass : public mlir::PassWrapper()) + { + function = getMethodOp.getBoundFunc(); + } + + if (auto boundOp = function.getDefiningOp()) + { + function = boundOp.getFunc(); + } + + auto loadOp = function.getDefiningOp(); + if (auto addressOfOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp(); + addressOfOp && !writtenGlobals.contains(addressOfOp.getGlobalName())) + { + callees.named.push_back(addressOfOp.getGlobalNameAttr().getAttr()); + } + if (addImportedByAddress(callee, callees)) { callees.known = true; @@ -685,10 +1347,12 @@ class OwnershipSignaturePass : public mlir::PassWrapper next; for (auto &call : calls) { - if (auto facts = callFacts(call.callees); !facts.empty()) + auto facts = callFacts(call.callees); + if (facts.empty()) + { + // a call this pass cannot resolve still has what its callees' signatures say + auto keeps = declaredByCallees(call.callees, OWN_DECLARED_KEEPS_ATTR_NAME); + facts.owned.assign(keeps.begin(), keeps.end()); + } + + if (!facts.empty()) { next[call.op] = facts; } @@ -1072,6 +1744,16 @@ class OwnershipSignaturePass : public mlir::PassWrapperhasAttr(OWN_DECLARED_KEEPS_ATTR_NAME)) + { + auto keeps = declared(entry.second, OWN_DECLARED_KEEPS_ATTR_NAME); + factsEffective[entry.second] = Facts{-1, llvm::SmallVector(keeps.begin(), keeps.end())}; + } + } + for (auto changed = true; changed;) { changed = false; @@ -1091,7 +1773,8 @@ class OwnershipSignaturePass : public mlir::PassWrapperhasAttr(OWN_DECLARED_KEEPS_ATTR_NAME) && factsEffective.erase(funcOp)) { changed = true; } @@ -1235,9 +1918,9 @@ class OwnershipSignaturePass : public mlir::PassWrapper(user) && + onlyCalled(user->getResult(0))))) + { + continue; + } + auto symbolRefOp = mlir::dyn_cast(user); if (!symbolRefOp) { @@ -1293,6 +1987,21 @@ class OwnershipSignaturePass : public mlir::PassWrapper(); + auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + int position = -1; + if (!varOp || !isParameterSlot(varOp, position) || varOp->getParentOfType() != maker) + { + return false; + } + + index = static_cast(position); + return true; + } + // `ts.Load(ts.PropertyRef(argument, i))` of one of `maker`'s parameters: `index` is its position. static bool readsParameterField(mlir::Value value, mlir_ts::FuncOp maker, int32_t &index) { @@ -1330,8 +2039,10 @@ class OwnershipSignaturePass : public mlir::PassWrappergetParentOfType(); - // an open maker has callers this pass cannot give the bound to - if (captured < 0 || !maker || !calledOnlyDirectly(module, maker)) + // an open maker has callers this pass cannot give the bound to, unless its signature + // gives it to them (`@borrows`) + if (captured < 0 || !maker || + !calledOnlyDirectly(module, maker, /*publicToo=*/maker->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME))) { return; } @@ -1386,6 +2097,23 @@ class OwnershipSignaturePass : public mlir::PassWrapperc; +} + +const other = new Word("xyz"); + +@borrows("w") +export function pick(w: Word, first: boolean) { + if (first) { + return lettersOf(w); + } + + return lettersOf(other); +} + +function main() { + const w = new Word("abc"); + for (const c of pick(w, true)) print(c); +} diff --git a/tslang/test/tester/own/own_err_borrows_not_generator.ts b/tslang/test/tester/own/own_err_borrows_not_generator.ts new file mode 100644 index 000000000..0179c880e --- /dev/null +++ b/tslang/test/tester/own/own_err_borrows_not_generator.ts @@ -0,0 +1,15 @@ +// -mm=own rejects: `@borrows` on a function that neither makes a generator over its argument nor +// returns one. +class Word { + constructor(public text: string) {} +} + +@borrows("w") +export function copyOf(w: Word) { + return new Word(w.text + ""); +} + +function main() { + const w = new Word("abc"); + print(copyOf(w).text); +} diff --git a/tslang/test/tester/own/own_err_generator_method_this.ts b/tslang/test/tester/own/own_err_generator_method_this.ts new file mode 100644 index 000000000..f4cd36b3f --- /dev/null +++ b/tslang/test/tester/own/own_err_generator_method_this.ts @@ -0,0 +1,15 @@ +// -mm=own rejects: a generator method of a class another module may call keeps `this` in its state, +// and its callers keep owning it. It compiled, and the state's release freed the caller's object +// (the program then freed it again); `@borrows("this")` says what it does. +class S { + constructor(public value: string) {} + + public *chars() { + for (const c of this.value) yield c; + } +} + +function main() { + const s = new S("abc"); + for (const c of s.chars()) print(c); +} diff --git a/tslang/test/tester/own/own_err_keeps_mismatch.ts b/tslang/test/tester/own/own_err_keeps_mismatch.ts new file mode 100644 index 000000000..02db3cb3f --- /dev/null +++ b/tslang/test/tester/own/own_err_keeps_mismatch.ts @@ -0,0 +1,20 @@ +// -mm=own rejects: the signature says the constructor keeps `a`, but its body keeps `b`. A caller +// in another module trusts the signature, so it would move `a` in and free `b` while it is kept. +class C { + constructor(public n: string) {} +} + +export class Pair { + b: C; + + @keeps("a") + constructor(a: C, b: C) { + this.b = b; + print(a.n); + } +} + +function main() { + const p = new Pair(new C("a"), new C("b")); + print(p.b.n); +} diff --git a/tslang/test/tester/own/own_err_marker_override.ts b/tslang/test/tester/own/own_err_marker_override.ts new file mode 100644 index 000000000..82ccf056b --- /dev/null +++ b/tslang/test/tester/own/own_err_marker_override.ts @@ -0,0 +1,25 @@ +// -mm=own rejects: an override says other than the method it overrides. A call through the base +// reaches either, and its caller can follow only one signature. +class C { + constructor(public n: string) {} +} + +class Base { + c: C | null = null; + + @keeps("c") + set(c: C) { + this.c = c; + } +} + +class Derived extends Base { + set(c: C) { + print(c.n); + } +} + +function main() { + const b: Base = new Derived(); + b.set(new C("a")); +} diff --git a/tslang/test/tester/own/own_err_marker_unknown_param.ts b/tslang/test/tester/own/own_err_marker_unknown_param.ts new file mode 100644 index 000000000..e05b9df30 --- /dev/null +++ b/tslang/test/tester/own/own_err_marker_unknown_param.ts @@ -0,0 +1,17 @@ +// A marker names a parameter the function does not have. +class C { + constructor(public n: string) {} +} + +export class Holder { + c: C; + + @keeps("d") + constructor(c: C) { + this.c = c; + } +} + +function main() { + print(new Holder(new C("a")).c.n); +} diff --git a/tslang/test/tester/own/own_generator_locals.ts b/tslang/test/tester/own/own_generator_locals.ts new file mode 100644 index 000000000..dcd4ff266 --- /dev/null +++ b/tslang/test/tester/own/own_generator_locals.ts @@ -0,0 +1,77 @@ +// -mm=own: what a generator does with its locals, which are fields of its state object. +// - A local narrowed by a test is a view of the local, read again at each use - after a yield too - +// not a second field holding a second reference. +// - A spread (`[...a]`) of a local builds its array in the frame; the array moves into its field. +// - A local read only to be yielded moves its value to the caller: the yield takes it out, and +// neither the state's release nor the next value stored there gives it back. +// churn() between yields reuses freed blocks, so a value freed too early reads garbage. +class Source { + n = 0; + + next(): string[] | null { + if (this.n >= 3) { + return null; + } + + const r: string[] = ["a" + this.n, "b" + this.n]; + this.n++; + return r; + } +} + +function churn() { + const junk: string[][] = []; + for (let i = 0; i < 32; i++) { + junk.push(["j" + i, "k" + i]); + } +} + +// narrowed before a yield, read after it +function* lengths(src: Source) { + while (true) { + const r = src.next(); + if (r == null) { + break; + } + + yield 1; + churn(); + yield r.length + r[0].length; + } +} + +// a spread of a narrowed local, yielded +function* copies(src: Source) { + while (true) { + const r = src.next(); + if (r == null) { + break; + } + + const groups = [...r]; + churn(); + yield groups; + } +} + +function main() { + let t = 0; + for (let i = 0; i < 2000; i++) { + for (const v of lengths(new Source())) { + churn(); + t += v; + } + + for (const g of copies(new Source())) { + churn(); + t += g.length + g[0].length + g[1].length; + } + + // given up after one value: the state still releases what it owns + const half = copies(new Source()); + t += half.next().value.length; + } + + assert(t == 2000 * (15 + 18 + 2), "t"); + print("done."); +} diff --git a/tslang/test/tester/own/own_iterator_next.ts b/tslang/test/tester/own/own_iterator_next.ts new file mode 100644 index 000000000..a434905c6 --- /dev/null +++ b/tslang/test/tester/own/own_iterator_next.ts @@ -0,0 +1,43 @@ +// -mm=own: what an iterator's next() returns is the caller's, whoever implements it - through an +// interface too, where this module cannot see the function. A generator's next moves what it +// yields into the record, and any other next is a function whose callers are not all known, which +// may not return a borrow. +class C { + constructor(public x: number) {} +} + +interface Source { + next(): { value: C; done: boolean }; +} + +class Counter implements Source { + n = 0; + + next() { + this.n++; + return { value: new C(this.n), done: this.n > 3 }; + } +} + +function churn() { + const junk: C[] = []; + for (let i = 0; i < 64; i++) { + junk.push(new C(999)); + } +} + +function main() { + let t: number = 0; + for (let i = 0; i < 1000; i++) { + const it: Source = new Counter(); + let r = it.next(); + while (!r.done) { + churn(); + t += r.value.x; + r = it.next(); + } + } + + assert(t == 6000, "t"); + print("done."); +} diff --git a/tslang/test/tester/own/own_param_markers.ts b/tslang/test/tester/own/own_param_markers.ts new file mode 100644 index 000000000..3dc5abda4 --- /dev/null +++ b/tslang/test/tester/own/own_param_markers.ts @@ -0,0 +1,51 @@ +// -mm=own: what a function's signature says - the parameters it keeps (@keeps), the arguments its +// result may not outlive (@borrows) - holds for an exported function too, whose callers this module +// does not all see. The other memory models ignore both. +class Tag { + constructor(public name: string) {} +} + +export class Holder { + tag: Tag; + + @keeps("tag") + constructor(tag: Tag) { + this.tag = tag; + } +} + +export class Word { + constructor(public text: string) {} + + // a generator method over `this`, which its state borrows + @borrows("this") + *letters() { + for (const c of this.text) yield c; + } +} + +// a generator whose state borrows its argument: the caller owns it, but it may not outlive `w` +@borrows("w") +export function *lettersOf(w: Word) { + for (const c of w.text) yield c; +} + +// returns what a call with a bounded result gives it, so it is bound to the same argument +@borrows("w") +export function forward(w: Word) { + return lettersOf(w); +} + +function main() { + const h = new Holder(new Tag("kept")); + assert(h.tag.name == "kept"); + + const w = new Word("abc"); + let s = ""; + for (const c of lettersOf(w)) s += c; + for (const c of forward(w)) s += c; + for (const c of w.letters()) s += c; + assert(s == "abcabcabc"); + + print("done."); +} diff --git a/tslang/test/tester/tests/export_own_markers.ts b/tslang/test/tester/tests/export_own_markers.ts new file mode 100644 index 000000000..140d5a7bb --- /dev/null +++ b/tslang/test/tester/tests/export_own_markers.ts @@ -0,0 +1,30 @@ +// The library side of @keeps and @borrows across a module boundary: the importer reads them from +// the declarations (__decls) and calls these knowing them. See import_own_markers.ts. +export class Tag { + constructor(public name: string) {} +} + +@final +export class Holder { + tag: Tag; + + @keeps("tag") + constructor(tag: Tag) { + this.tag = tag; + } +} + +@final +export class Word { + constructor(public text: string) {} + + @borrows("this") + *letters() { + for (const c of this.text) yield c; + } +} + +@borrows("w") +export function *lettersOf(w: Word) { + for (const c of w.text) yield c; +} diff --git a/tslang/test/tester/tests/import_own_markers.ts b/tslang/test/tester/tests/import_own_markers.ts new file mode 100644 index 000000000..d0f949a33 --- /dev/null +++ b/tslang/test/tester/tests/import_own_markers.ts @@ -0,0 +1,16 @@ +// @keeps and @borrows from another module (export_own_markers.ts): the constructor keeps the tag the +// importer moves in, and the generators borrow the word the importer still owns. +import './export_own_markers' + +function main() { + const h = new Holder(new Tag("kept")); + assert(h.tag.name == "kept"); + + const w = new Word("abc"); + let s = ""; + for (const c of lettersOf(w)) s += c; + for (const c of w.letters()) s += c; + assert(s == "abcabc"); + + print("done."); +} diff --git a/tslang/test/tester/tests/import_own_markers_keeps.ts b/tslang/test/tester/tests/import_own_markers_keeps.ts new file mode 100644 index 000000000..da6a97ebd --- /dev/null +++ b/tslang/test/tester/tests/import_own_markers_keeps.ts @@ -0,0 +1,9 @@ +// @keeps from another module under -mm=own (export_own_markers.ts), on its own: the importer moves +// the tag into the Holder's constructor, and only the Holder frees it. +import './export_own_markers' + +function main() { + const h = new Holder(new Tag("kept")); + assert(h.tag.name == "kept"); + print("done."); +} diff --git a/tslang/test/tester/tests/import_own_markers_moved.ts b/tslang/test/tester/tests/import_own_markers_moved.ts new file mode 100644 index 000000000..bfcd09be3 --- /dev/null +++ b/tslang/test/tester/tests/import_own_markers_moved.ts @@ -0,0 +1,10 @@ +// An error by design under -mm=own: @keeps crossed the module boundary, so the tag is moved into +// the Holder and cannot be used after. Without the marker the importer would borrow it, and both +// the Holder and the importer would free it. +import './export_own_markers' + +function main() { + const t = new Tag("kept"); + const h = new Holder(t); + print(t.name, h.tag.name); +} diff --git a/tslang/test/tester/tests/import_own_markers_outlives.ts b/tslang/test/tester/tests/import_own_markers_outlives.ts new file mode 100644 index 000000000..cbe09c3f2 --- /dev/null +++ b/tslang/test/tester/tests/import_own_markers_outlives.ts @@ -0,0 +1,9 @@ +// An error by design under -mm=own: @borrows crossed the module boundary, so the generator may not +// be kept where it could outlive the word it borrows. +import './export_own_markers' + +function main() { + const w = new Word("abc"); + const kept = [lettersOf(w)]; + print(kept.length); +}