From 6f2ba362909f1912a2ecd17a26b2163714b21336 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 20:12:18 +0000 Subject: [PATCH 1/5] own: a constructor's parameter stored into a field is read from it past calls, when `this` never escapes (TSLANG 80) No call can reach an object the constructor only reads for its fields, so none writes them; the pairing of the parameter with its field now sees past calls there. No copy is made (a copy leaks the parameter the caller gave). Co-Authored-By: Claude Opus 5.5 --- TSLANG_ISSUES.md | 16 +++++---- .../OwnershipSignatureFieldLocals.cpp | 36 +++++++++++++++++-- tslang/test/tester/CMakeLists.txt | 7 ++-- .../own/own_err_escaped_this_field_read.ts | 32 +++++++++++++++++ tslang/test/tester/tests/00tslang_issue_80.ts | 28 +++++++++++++++ 5 files changed, 107 insertions(+), 12 deletions(-) create mode 100644 tslang/test/tester/own/own_err_escaped_this_field_read.ts create mode 100644 tslang/test/tester/tests/00tslang_issue_80.ts diff --git a/TSLANG_ISSUES.md b/TSLANG_ISSUES.md index df7693086..7bb9d5745 100644 --- a/TSLANG_ISSUES.md +++ b/TSLANG_ISSUES.md @@ -616,13 +616,15 @@ early `return this.pipelines[i]` is no longer refused either. `msaa_nri.ts` and `dynamic_multisample_rasterization_nri.ts` compile. Regression tests `tslang/test/tester/tests/00tslang_issue_79.ts` (every model) and `own/own_err_pushed_let_popped.ts`. -80 (a parameter the function owns, stored into a field, then read again: `constructor(world: GameWorld, public -name: string, ...)`, then `${name}` in a loop that calls, in `core/rtxpt_game.ts`, refused "is used here after -its value was moved") is open. The local/field pairing reads the field for later reads of the parameter, but stops -at any call, as in that loop. Copying the parameter at the store instead leaks it: the caller gave it up, and -nothing releases it then (`own/own_err_string_kept_param_known.ts`; a first fix that copied grew 3M calls to -2.7 GB, and was backed out). The fix is for the pairing to see past calls that cannot reach the object. Meanwhile -`this.name` reads the field. +80 (a parameter a constructor stores into a field, then reads in a loop that calls: `constructor(world: +GameWorld, public name: string, ...)`, then `${name}` in `core/rtxpt_game.ts`, refused "is used here after its +value was moved") is fixed. The pairing of a local with the field it was stored into reads the field for +the local's later reads, but it stopped at any call. A constructor that reads `this` only to reach its fields +never gives the object to anyone, so no call can write them, and the pairing now sees past calls there. Copying +the parameter at the store instead leaks it: the caller gave it up, and nothing releases the copy's original +(`own/own_err_string_kept_param_known.ts`). A first fix that copied grew 3M calls to 2.7 GB, and was backed out +before #856. A constructor that gives `this` away is still refused. Regression tests +`tslang/test/tester/tests/00tslang_issue_80.ts` (every model) and `own/own_err_escaped_this_field_read.ts`. 81 (a value moved on some paths only: `const controller = new LightController(); if (controller.read(...)) { ...; this.lights.push(controller); }` in `core/rtxpt_game.ts`, refused "is moved here on some paths only; -mm=own diff --git a/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp b/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp index f28aadfaf..d846d143a 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp @@ -506,6 +506,9 @@ namespace own_signature { mlir_ts::VariableOp local; mlir_ts::PropertyRefOp field; + // a field of a constructor's `this` that never escapes it: no call reaches the object, so + // none writes the field (TSLANG 80) + bool callsCannotWrite = false; // the state after `op`, given the state before it bool step(mlir::Operation *op, bool holds) @@ -571,9 +574,38 @@ namespace own_signature return false; } - return holds && !mayWriteField(op, field, slot, [](mlir::Operation *) { return true; }); + auto cannot = callsCannotWrite; + return holds && !mayWriteField(op, field, slot, [cannot](mlir::Operation *) { return !cannot; }); } }; + + // Is `field` a field of the `this` of a constructor that only ever reads `this` to reach its + // fields - never gives it to a call or stores it - so that no call can reach the object + // (TSLANG 80)? + static bool fieldOfUnescapedThis(mlir_ts::PropertyRefOp field) + { + auto funcOp = field->getParentOfType(); + auto objectLoad = field.getObjectRef().getDefiningOp(); + auto slot = objectLoad ? objectLoad.getReference().getDefiningOp() : mlir_ts::VariableOp(); + if (!funcOp || !funcOp.getSymName().ends_with(".constructor") || !slot || slot->getNumOperands() != 1 || + funcOp.isExternal()) + { + return false; + } + + auto argument = mlir::dyn_cast(slot->getOperand(0)); + if (!argument || argument.getArgNumber() != 0 || argument.getOwner() != &funcOp.getFunctionBody().front()) + { + return false; + } + + return llvm::all_of(slot.getResult().getUsers(), [](mlir::Operation *user) { + auto loadOp = mlir::dyn_cast(user); + return loadOp && llvm::all_of(loadOp->getUsers(), [](mlir::Operation *loadUser) { + return mlir::isa(loadUser); + }); + }); + } } // namespace void OwnershipSignaturePass::borrowParametersInLocals(mlir::ModuleOp module) @@ -1358,7 +1390,7 @@ namespace own_signature return; } - pairs.push_back({varOp, field}); + pairs.push_back({varOp, field, fieldOfUnescapedThis(field)}); }; if (auto init = varOp.getInitializer()) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 39a739234..160a706dc 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -1929,14 +1929,14 @@ endforeach() # TSLANG_ISSUES 77: an array kept by a constructor, then filled through the local # TSLANG_ISSUES 78: a value pushed on or stored into a field of a local object, then used # TSLANG_ISSUES 79: a local pushed only where it is not null, then used -# TSLANG_ISSUES 81: a value moved on some paths only +# TSLANG_ISSUES 80-81: a constructor parameter stored into a field, read past calls; a value moved on some paths only foreach(issues_mm gc rc none own) foreach(issues_test 00tslang_issue_44 00tslang_issue_45 00tslang_issue_46 00tslang_issue_47 00tslang_issue_48 00tslang_issue_49 00tslang_issue_50 00tslang_issue_52 00tslang_issue_53 00tslang_issue_54 00tslang_issue_55 00tslang_issue_56 00tslang_issue_57 00tslang_issue_58 00tslang_issue_59 00tslang_issue_60 00tslang_issue_61 00tslang_issue_62 00tslang_issue_63 00tslang_issue_66 00tslang_issue_67 00tslang_issue_69 00tslang_issue_71 00tslang_issue_73 00tslang_issue_75 00tslang_issue_76 00tslang_issue_77 00tslang_issue_78 00tslang_issue_79 - 00tslang_issue_81) + 00tslang_issue_80 00tslang_issue_81) tslang_add_test(NAME test-jit-${issues_test}-${issues_mm} COMMAND test-runner -jit -mm=${issues_mm} "${PROJECT_SOURCE_DIR}/test/tester/tests/${issues_test}.ts") tslang_add_test(NAME test-compile-${issues_test}-${issues_mm} COMMAND test-runner -mm=${issues_mm} "${PROJECT_SOURCE_DIR}/test/tester/tests/${issues_test}.ts") endforeach() @@ -5517,7 +5517,8 @@ set(own_error_cases "own_err_kept_array_filled_after_pop|:21:9: error: '[^']*' borrows an element but is used here after it may be released or overwritten" "own_err_local_object_field_popped|:18:11: error: '[^']*' borrows an element but is used here after it may be released or overwritten" "own_err_pushed_let_popped|:17:9: error: '[^']*' borrows an element but is used here after it may be released or overwritten" - "own_err_moved_on_some_paths_used|:21:15: error: '[^']*' is used here after its value was moved") + "own_err_moved_on_some_paths_used|:21:15: error: '[^']*' is used here after its value was moved" + "own_err_escaped_this_field_read|:24:28: error: '[^']*' is used here after its value was moved") foreach(own_error_case ${own_error_cases}) string(FIND "${own_error_case}" "|" own_error_sep) string(SUBSTRING "${own_error_case}" 0 ${own_error_sep} own_error_name) diff --git a/tslang/test/tester/own/own_err_escaped_this_field_read.ts b/tslang/test/tester/own/own_err_escaped_this_field_read.ts new file mode 100644 index 000000000..6cff021af --- /dev/null +++ b/tslang/test/tester/own/own_err_escaped_this_field_read.ts @@ -0,0 +1,32 @@ +// -mm=own rejects: the constructor gives `this` to `registry.add`, so the call in the loop may reach the +// object and overwrite `name`; the field cannot stand in for the parameter, which it took (TSLANG 80). +@final +class Registry { + models: Model[] = []; + + add(model: Model) { + this.models.push(model); + } + + rename() { + for (const model of this.models) { + model.name = "renamed"; + } + } +} + +@final +class Model { + constructor(public name: string, registry: Registry, n: int) { + registry.add(this); + for (let i = 0; i < n; i++) { + registry.rename(); + print(`model ${name}`); + } + } +} + +function main() { + const registry = new Registry(); + new Model("m", registry, 2); +} diff --git a/tslang/test/tester/tests/00tslang_issue_80.ts b/tslang/test/tester/tests/00tslang_issue_80.ts new file mode 100644 index 000000000..41fcd247d --- /dev/null +++ b/tslang/test/tester/tests/00tslang_issue_80.ts @@ -0,0 +1,28 @@ +// TSLANG 80: a parameter a constructor stores into a field, then reads in a loop that calls +// (`constructor(public name: string)`, then `${name}`): `this` never escapes the constructor, so no +// call reaches the object, and the read takes the field. +@final +class Ctl { + node: int = 0; + read(ok: boolean): boolean { return ok; } +} +@final +class Model { + lights: Ctl[] = []; + constructor(public name: string, n: int) { + for (let i = 0; i < n; i++) { + if (i == 1) { + print(`skip ${name}`); + continue; + } + const c = new Ctl(); + c.node = i; + this.lights.push(c); + } + } +} +function main() { + const m = new Model("m", 4); + assert(m.lights.length == 3 && m.name == "m", "model"); + print("done."); +} From 4609c097989d569f47191531e12498e2bec4330c Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 20:51:51 +0000 Subject: [PATCH 2/5] own: a borrow of a call's result survives a release whose type cannot hold it (TSLANG 82) MLIRGen lists the module's @final classes, and its declared @cpp ones, on the module; dropsChain follows the released type's fields for a chain whose places are not known, for a release and for a call whose drops are known stores. Co-Authored-By: Claude Opus 5.5 --- TSLANG_ISSUES.md | 14 +++ tslang/include/TypeScript/Defines.h | 3 + tslang/lib/TypeScript/MLIRGenImpl.h | 1 + tslang/lib/TypeScript/MLIRGenModule.cpp | 24 +++++ tslang/lib/TypeScript/OwnershipFacts.h | 87 +++++++++++++++++++ .../OwnershipInferencePlaceReads.cpp | 33 +++++++ tslang/test/tester/CMakeLists.txt | 6 +- .../own_err_borrowed_result_holder_stored.ts | 32 +++++++ tslang/test/tester/tests/00tslang_issue_82.ts | 41 +++++++++ 9 files changed, 239 insertions(+), 2 deletions(-) create mode 100644 tslang/test/tester/own/own_err_borrowed_result_holder_stored.ts create mode 100644 tslang/test/tester/tests/00tslang_issue_82.ts diff --git a/TSLANG_ISSUES.md b/TSLANG_ISSUES.md index 7bb9d5745..9eee7dd71 100644 --- a/TSLANG_ISSUES.md +++ b/TSLANG_ISSUES.md @@ -634,6 +634,20 @@ into what follows the move, from where the value is held and was not moved. The use after the paths join is still refused, a copyable value's read included (`own_string_copy_branch.ts`). Regression tests `tslang/test/tester/tests/00tslang_issue_81.ts` (every model) and `own/own_err_moved_on_some_paths_used.ts`. +82 (a borrow of a call's result across a store of an unrelated type: `const texture = this.swapChain.texture(index); +...; this.recordTriangle(commandBuffer); ...; commandBuffer.textureBarrier(texture, ...)` in +`examples/nri/color_write_enable_nri.ts`, where `recordTriangle` stores a pipeline into +`this.trianglePipelines[enabled]`, refused "borrows the result of 'NriSwapChain.texture' but is used here after +it may be released or overwritten") is fixed. A call's result borrows a place not known, so any release was +taken to free it. A release frees the released value and what it holds, which is what its type can hold. When +that type cannot hold the borrowed value's type, it cannot free it: the release, or a call whose drops are +known stores, is no longer taken to. Only an object whose class is known is followed: a @final class's, or a +declared @cpp one, which holds nothing of ours (MLIRGen lists them on the module, OWN_FINAL_CLASSES_ATTR_NAME). +Any other class may be a subclass's object, holding anything, and an interface, `any` or a function may hold +anything too. The borrowed value too must be of a @final class, or an array. In the sweep, +`color_write_enable_nri.ts` and `logic_op_dynamic_state_nri.ts` compile. Regression tests +`tslang/test/tester/tests/00tslang_issue_82.ts` (every model) and `own/own_err_borrowed_result_holder_stored.ts`. + For native_ts_graphics, from the same sweep (its code, not the compiler): - `core/ray_tracing_extended_scene.ts`: the exported classes' constructors keep their array parameters (`this.origin = origin`), which an exported constructor has to say: `@keeps("origin", "direction")` diff --git a/tslang/include/TypeScript/Defines.h b/tslang/include/TypeScript/Defines.h index e31e9a952..64f48b71c 100644 --- a/tslang/include/TypeScript/Defines.h +++ b/tslang/include/TypeScript/Defines.h @@ -162,6 +162,9 @@ #define OWN_BACK_REFERENCE_FIELDS_ATTR_NAME "ts.own_back_reference_fields" // the private fields of this module's classes nothing writes after the constructor (TSLANG 73) #define OWN_STABLE_FIELDS_ATTR_NAME "ts.own_stable_fields" +// the classes of the module, its own or imported, declared @final: their objects are of that class only, +// so what one holds is what its fields' types can hold (TSLANG 82) +#define OWN_FINAL_CLASSES_ATTR_NAME "ts.own_final_classes" // -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 diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 03eb775e0..fc65160e3 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -300,6 +300,7 @@ class MLIRGenImpl // calls through a vtable to methods no class overrides made direct, once the module is generated void devirtualizeClosedMethods(); + void markFinalClasses(); bool registerNamespace(llvm::StringRef namePtr, bool isFunctionNamespace = false); diff --git a/tslang/lib/TypeScript/MLIRGenModule.cpp b/tslang/lib/TypeScript/MLIRGenModule.cpp index fa5ce0f40..9d2a4769e 100644 --- a/tslang/lib/TypeScript/MLIRGenModule.cpp +++ b/tslang/lib/TypeScript/MLIRGenModule.cpp @@ -518,6 +518,7 @@ namespace mlirgen if (mlir::succeeded(mlirCodeGenModule(module, includeFiles))) { devirtualizeClosedMethods(); + markFinalClasses(); return theModule; } } @@ -530,6 +531,29 @@ namespace mlirgen // can inline it. Every class the module made is known by now, the imported sources' too; a // class code outside the module may extend - exported, declared or imported, a C++ binding, // or one of its subclasses so - keeps its methods virtual. + // The classes whose objects are of that class only, for -mm=own (OWN_FINAL_CLASSES_ATTR_NAME, + // TSLANG 82): a @final class, or a declared @cpp one (a C++ object, which holds nothing of ours). + void MLIRGenImpl::markFinalClasses() + { + llvm::SmallVector finals; + for (auto &classInfo : moduleClasses) + { + if (classInfo->isFinal || (classInfo->isCppVTable && classInfo->isDeclaration)) + { + auto name = builder.getStringAttr(classInfo->fullName); + if (!llvm::is_contained(finals, name)) + { + finals.push_back(name); + } + } + } + + if (!finals.empty()) + { + theModule->setAttr(OWN_FINAL_CLASSES_ATTR_NAME, builder.getArrayAttr(finals)); + } + } + void MLIRGenImpl::devirtualizeClosedMethods() { auto seenOutside = [](const ClassInfo::TypePtr &classInfo) { diff --git a/tslang/lib/TypeScript/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h index b32f3fc2e..300c5a7ca 100644 --- a/tslang/lib/TypeScript/OwnershipFacts.h +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -2329,6 +2329,93 @@ inline mlir_ts::ClassStorageType fieldOwnerStorage(mlir_ts::PropertyRefOp proper return mlir::dyn_cast(type); } +// Can a value of type `holder`, released, free a value of type `target`: is `target` the holder, or +// under it, by the types its fields, elements and members can hold (TSLANG 82)? A class not known to be +// of that class only (not @final, OWN_FINAL_CLASSES_ATTR_NAME) may be a subclass holding anything, and +// an interface, `any`, a function (its captures) or a type not listed here may too. +inline bool typeMayHold(mlir::ModuleOp module, mlir::Type holder, mlir::Type target, llvm::SmallPtrSetImpl &seen) +{ + if (holder == target) + { + return true; + } + + if (auto unionType = mlir::dyn_cast(target)) + { + return llvm::any_of(unionType.getTypes(), [&](mlir::Type member) { return typeMayHold(module, holder, member, seen); }); + } + + if (auto optionalType = mlir::dyn_cast(target)) + { + return typeMayHold(module, holder, optionalType.getElementType(), seen); + } + + // the target an object of its type only: an array, or a @final class's (not a base class's, whose + // object may be a subclass's, held in a field of that type) + if (auto targetClass = mlir::dyn_cast(target)) + { + auto finals = module ? module->getAttrOfType(OWN_FINAL_CLASSES_ATTR_NAME) : mlir::ArrayAttr(); + auto name = targetClass.getName().getValue(); + if (!finals || !llvm::any_of(finals.getAsRange(), [&](mlir::StringAttr each) { return each.getValue() == name; })) + { + return true; + } + } + else if (!mlir::isa(target)) + { + return true; + } + + if (mlir::isa(holder)) + { + return false; + } + + if (!seen.insert(holder).second) + { + return false; // its fields are looked at already + } + + if (auto unionType = mlir::dyn_cast(holder)) + { + return llvm::any_of(unionType.getTypes(), [&](mlir::Type member) { return typeMayHold(module, member, target, seen); }); + } + + if (auto optionalType = mlir::dyn_cast(holder)) + { + return typeMayHold(module, optionalType.getElementType(), target, seen); + } + + if (auto arrayType = mlir::dyn_cast(holder)) + { + return typeMayHold(module, arrayType.getElementType(), target, seen); + } + + if (auto classType = mlir::dyn_cast(holder)) + { + auto finals = module ? module->getAttrOfType(OWN_FINAL_CLASSES_ATTR_NAME) : mlir::ArrayAttr(); + auto name = classType.getName().getValue(); + auto isFinal = finals && llvm::any_of(finals.getAsRange(), [&](mlir::StringAttr each) { return each.getValue() == name; }); + auto storageType = mlir::dyn_cast(classType.getStorageType()); + if (!isFinal || !storageType) + { + return true; + } + + return llvm::any_of(storageType.getFields(), [&](mlir_ts::FieldInfo field) { return typeMayHold(module, field.type, target, seen); }); + } + + return true; +} + +inline bool typeMayHold(mlir::ModuleOp module, mlir::Type holder, mlir::Type target) +{ + llvm::SmallPtrSet seen; + return typeMayHold(module, holder, target, seen); +} + // Is `ref` a private field nothing writes after its object's constructor (OWN_STABLE_FIELDS_ATTR_NAME, the // signature pass's, TSLANG 73)? inline bool isStableField(mlir::Value ref) diff --git a/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp b/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp index 1c92464e9..54d761e0c 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp @@ -631,8 +631,19 @@ namespace own_inference return false; // a call that returns a borrow has not destroyed it by returning it } + // a chain whose places are not known (a borrowing call's result): what a release frees is what + // the released value's type can hold, so one that cannot hold the borrowed value's type does not + // free it (TSLANG 82) + auto borrowedValue = borrowed ? borrowed : (read && read->getNumResults() == 1 ? read->getResult(0) : mlir::Value()); + auto module = drop->getParentOfType(); if (chain.anyPlace && !isCall(drop)) { + if (auto releaseSlotOp = mlir::dyn_cast(drop); releaseSlotOp && borrowedValue) + { + auto slotType = mlir::cast(releaseSlotOp.getSlot().getType()).getElementType(); + return typeMayHold(module, slotType, borrowedValue.getType()); + } + return true; } @@ -734,6 +745,28 @@ namespace own_inference auto keepsClosure = givenClosure && llvm::any_of(drop->getOperands(), [&](mlir::Value operand) { return holdsCaptures(operand) && isKeptArgument(drop, operand); }); + // ...and a chain whose places are not known, by the types those places hold (TSLANG 82) + if (mayDrop && !keepsClosure && chain.anyPlace && borrowedValue) + { + if (auto places = callDropFields(drop)) + { + mayDrop = llvm::any_of(*places, [&](auto &place) { + if (place.first == OWN_CAPTURED_STRING_POSITION || place.first == OWN_CAPTURED_CELL_POSITION) + { + return true; + } + + auto placeType = place.second; + if (auto refType = mlir::dyn_cast(placeType)) + { + placeType = refType.getElementType(); + } + + return typeMayHold(module, placeType, borrowedValue.getType()); + }); + } + } + if (mayDrop && !keepsClosure && !chain.anyPlace) { if (auto places = callDropFields(drop)) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 160a706dc..fa6dd927e 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -1930,13 +1930,14 @@ endforeach() # TSLANG_ISSUES 78: a value pushed on or stored into a field of a local object, then used # TSLANG_ISSUES 79: a local pushed only where it is not null, then used # TSLANG_ISSUES 80-81: a constructor parameter stored into a field, read past calls; a value moved on some paths only +# TSLANG_ISSUES 82: a borrowed call result across a release of a type that cannot hold it foreach(issues_mm gc rc none own) foreach(issues_test 00tslang_issue_44 00tslang_issue_45 00tslang_issue_46 00tslang_issue_47 00tslang_issue_48 00tslang_issue_49 00tslang_issue_50 00tslang_issue_52 00tslang_issue_53 00tslang_issue_54 00tslang_issue_55 00tslang_issue_56 00tslang_issue_57 00tslang_issue_58 00tslang_issue_59 00tslang_issue_60 00tslang_issue_61 00tslang_issue_62 00tslang_issue_63 00tslang_issue_66 00tslang_issue_67 00tslang_issue_69 00tslang_issue_71 00tslang_issue_73 00tslang_issue_75 00tslang_issue_76 00tslang_issue_77 00tslang_issue_78 00tslang_issue_79 - 00tslang_issue_80 00tslang_issue_81) + 00tslang_issue_80 00tslang_issue_81 00tslang_issue_82) tslang_add_test(NAME test-jit-${issues_test}-${issues_mm} COMMAND test-runner -jit -mm=${issues_mm} "${PROJECT_SOURCE_DIR}/test/tester/tests/${issues_test}.ts") tslang_add_test(NAME test-compile-${issues_test}-${issues_mm} COMMAND test-runner -mm=${issues_mm} "${PROJECT_SOURCE_DIR}/test/tester/tests/${issues_test}.ts") endforeach() @@ -5518,7 +5519,8 @@ set(own_error_cases "own_err_local_object_field_popped|:18:11: error: '[^']*' borrows an element but is used here after it may be released or overwritten" "own_err_pushed_let_popped|:17:9: error: '[^']*' borrows an element but is used here after it may be released or overwritten" "own_err_moved_on_some_paths_used|:21:15: error: '[^']*' is used here after its value was moved" - "own_err_escaped_this_field_read|:24:28: error: '[^']*' is used here after its value was moved") + "own_err_escaped_this_field_read|:24:28: error: '[^']*' is used here after its value was moved" + "own_err_borrowed_result_holder_stored|:26:16: error: '[^']*' borrows a field but is used here after it may be released or overwritten") foreach(own_error_case ${own_error_cases}) string(FIND "${own_error_case}" "|" own_error_sep) string(SUBSTRING "${own_error_case}" 0 ${own_error_sep} own_error_name) diff --git a/tslang/test/tester/own/own_err_borrowed_result_holder_stored.ts b/tslang/test/tester/own/own_err_borrowed_result_holder_stored.ts new file mode 100644 index 000000000..2e283d065 --- /dev/null +++ b/tslang/test/tester/own/own_err_borrowed_result_holder_stored.ts @@ -0,0 +1,32 @@ +// -mm=own rejects: `texture` borrows the result of `Chain.texture`, a place not known, and the store +// into `this.chain` releases a Chain, whose type holds textures: it may free the one borrowed +// (TSLANG 82 refines by type only where the released type cannot hold the borrowed one). +@final +class Tex { + constructor(public id: int) {} +} + +@final +class Chain { + private textures: Tex[] = [new Tex(1), new Tex(2)]; + + @lends("this") + texture(i: int): Tex { + return this.textures[i]; + } +} + +@final +class App { + private chain = new Chain(); + + frame(): int { + const texture = this.chain.texture(1); + this.chain = new Chain(); + return texture.id; + } +} + +function main() { + print(new App().frame()); +} diff --git a/tslang/test/tester/tests/00tslang_issue_82.ts b/tslang/test/tester/tests/00tslang_issue_82.ts new file mode 100644 index 000000000..580bb43b2 --- /dev/null +++ b/tslang/test/tester/tests/00tslang_issue_82.ts @@ -0,0 +1,41 @@ +// TSLANG 82: a borrow of a call's result, whose place is not known (`this.chain.texture(1)`), across a +// store that releases a value whose type cannot hold it (a @final Pipe, holding no texture): the store +// does not free it. +@final +class Tex { + constructor(public id: int) {} +} +@final +class Chain { + private textures: Tex[] = [new Tex(1), new Tex(2)]; + @lends("this") + texture(i: int): Tex { return this.textures[i]; } +} +@final +class Pipe { + constructor(public id: int) {} +} +@final +class App { + private chain = new Chain(); + private pipes: (Pipe | null)[] = [null, null]; + pipeline(k: int): Pipe | null { + const made = this.pipes[k]; + if (made) { + return made; + } + const p = new Pipe(k); + this.pipes[k] = p; + return this.pipes[k]; + } + frame(): int { + const texture = this.chain.texture(1); + const p = this.pipeline(0); + return texture.id + (p ? p.id : 0); + } +} +function main() { + const a = new App(); + assert(a.frame() == 2, "frame"); + print("done."); +} From a5e1be3ffdb3d47788bb04bc530fe97c1315c25e Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 20:58:12 +0000 Subject: [PATCH 3/5] own: a global array of constant arrays is no keeper: a row bound to a local is borrowed, not freed (TSLANG 83) Its rows are blocks the literal makes, never made immortal, so a keeper's read let a local own one and free it on each call (a double free: a hang at exit or a crash). Co-Authored-By: Claude Opus 5.5 --- .../TypeScript/OwnershipSignatureKeepers.cpp | 5 +++- tslang/test/tester/CMakeLists.txt | 4 ++- tslang/test/tester/tests/00tslang_issue_83.ts | 29 +++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 tslang/test/tester/tests/00tslang_issue_83.ts diff --git a/tslang/lib/TypeScript/OwnershipSignatureKeepers.cpp b/tslang/lib/TypeScript/OwnershipSignatureKeepers.cpp index 762642c0b..bee067824 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureKeepers.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureKeepers.cpp @@ -42,8 +42,11 @@ namespace own_signature auto *def = castRoot(resultOp->getOperand(0)).getDefiningOp(); if (auto createArrayOp = mlir::dyn_cast_or_null(def)) { + // a constant that is no block of its own: not a constant array, which becomes one (a row of + // `[[1, 2], [3, 4]]`), and is not made immortal: a local given it would free it (TSLANG 83) return llvm::all_of(createArrayOp->getOperands(), [](mlir::Value element) { - return !!castRoot(element).getDefiningOp(); + auto constantOp = castRoot(element).getDefiningOp(); + return constantOp && !mlir::isa(constantOp.getType()); }); } diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index fa6dd927e..b5b128279 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -1931,13 +1931,15 @@ endforeach() # TSLANG_ISSUES 79: a local pushed only where it is not null, then used # TSLANG_ISSUES 80-81: a constructor parameter stored into a field, read past calls; a value moved on some paths only # TSLANG_ISSUES 82: a borrowed call result across a release of a type that cannot hold it +# TSLANG_ISSUES 83: a row of a global array of constant arrays bound to a local foreach(issues_mm gc rc none own) foreach(issues_test 00tslang_issue_44 00tslang_issue_45 00tslang_issue_46 00tslang_issue_47 00tslang_issue_48 00tslang_issue_49 00tslang_issue_50 00tslang_issue_52 00tslang_issue_53 00tslang_issue_54 00tslang_issue_55 00tslang_issue_56 00tslang_issue_57 00tslang_issue_58 00tslang_issue_59 00tslang_issue_60 00tslang_issue_61 00tslang_issue_62 00tslang_issue_63 00tslang_issue_66 00tslang_issue_67 00tslang_issue_69 00tslang_issue_71 00tslang_issue_73 00tslang_issue_75 00tslang_issue_76 00tslang_issue_77 00tslang_issue_78 00tslang_issue_79 - 00tslang_issue_80 00tslang_issue_81 00tslang_issue_82) + 00tslang_issue_80 00tslang_issue_81 00tslang_issue_82 + 00tslang_issue_83) tslang_add_test(NAME test-jit-${issues_test}-${issues_mm} COMMAND test-runner -jit -mm=${issues_mm} "${PROJECT_SOURCE_DIR}/test/tester/tests/${issues_test}.ts") tslang_add_test(NAME test-compile-${issues_test}-${issues_mm} COMMAND test-runner -mm=${issues_mm} "${PROJECT_SOURCE_DIR}/test/tester/tests/${issues_test}.ts") endforeach() diff --git a/tslang/test/tester/tests/00tslang_issue_83.ts b/tslang/test/tester/tests/00tslang_issue_83.ts new file mode 100644 index 000000000..c52a9a090 --- /dev/null +++ b/tslang/test/tester/tests/00tslang_issue_83.ts @@ -0,0 +1,29 @@ +// TSLANG 83: a row of a module-level array of arrays bound to a local (`const meta = POOL[idx]`): the +// rows are constant arrays the literal makes, not made immortal, so the global is no keeper and the +// local borrows the row. Taken for a keeper's immortal element, each call freed it: a double free from +// the second call (a hang at exit, or a crash). +const POOL = [[1000.0, 0.001], [500.0, 0.002]]; +const NAMES = ["a", "bb"]; + +function build(idx: int): number { + const meta = POOL[idx]; + return meta[0]; +} + +function name(idx: int): string { + const n = NAMES[idx]; + return n; +} + +function main() { + let total = 0.0; + let length = 0; + for (let i = 0; i < 1000; i++) { + total += build(i % 2); + length += name(i % 2).length; + } + + assert(total == 750000 && POOL[0][0] == 1000 && POOL[1][0] == 500, "pool"); + assert(length == 1500, "names"); + print("done."); +} From ade910a74d26a9f6225658f1d0934bc1709b8959 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 21:01:36 +0000 Subject: [PATCH 4/5] own: a closure's call of a function value it captured is a call of the functions stored into it (TSLANG 82); the type-reachability fix is TSLANG 84 When every store into the captured value's box field is a function's symbol, and the closures given the box write none of its fields, the call resolves to those functions. TSLANG 82 is the issue main's TSLANG_ISSUES numbers so; the release-type fix of the previous commit becomes 84. Co-Authored-By: Claude Opus 5.5 --- tslang/include/TypeScript/Defines.h | 2 +- tslang/lib/TypeScript/MLIRGenModule.cpp | 2 +- tslang/lib/TypeScript/OwnershipFacts.h | 2 +- .../OwnershipInferencePlaceReads.cpp | 4 +- .../lib/TypeScript/OwnershipSignaturePass.h | 4 + .../TypeScript/OwnershipSignatureResolve.cpp | 166 ++++++++++++++++++ tslang/test/tester/CMakeLists.txt | 8 +- .../own_err_borrowed_result_holder_stored.ts | 2 +- ...wn_err_closure_reassigned_captured_call.ts | 29 +++ tslang/test/tester/tests/00tslang_issue_82.ts | 54 ++---- tslang/test/tester/tests/00tslang_issue_84.ts | 41 +++++ 11 files changed, 270 insertions(+), 44 deletions(-) create mode 100644 tslang/test/tester/own/own_err_closure_reassigned_captured_call.ts create mode 100644 tslang/test/tester/tests/00tslang_issue_84.ts diff --git a/tslang/include/TypeScript/Defines.h b/tslang/include/TypeScript/Defines.h index 64f48b71c..bf1b494fd 100644 --- a/tslang/include/TypeScript/Defines.h +++ b/tslang/include/TypeScript/Defines.h @@ -163,7 +163,7 @@ // the private fields of this module's classes nothing writes after the constructor (TSLANG 73) #define OWN_STABLE_FIELDS_ATTR_NAME "ts.own_stable_fields" // the classes of the module, its own or imported, declared @final: their objects are of that class only, -// so what one holds is what its fields' types can hold (TSLANG 82) +// so what one holds is what its fields' types can hold (TSLANG 84) #define OWN_FINAL_CLASSES_ATTR_NAME "ts.own_final_classes" // -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 diff --git a/tslang/lib/TypeScript/MLIRGenModule.cpp b/tslang/lib/TypeScript/MLIRGenModule.cpp index 9d2a4769e..02068f182 100644 --- a/tslang/lib/TypeScript/MLIRGenModule.cpp +++ b/tslang/lib/TypeScript/MLIRGenModule.cpp @@ -532,7 +532,7 @@ namespace mlirgen // class code outside the module may extend - exported, declared or imported, a C++ binding, // or one of its subclasses so - keeps its methods virtual. // The classes whose objects are of that class only, for -mm=own (OWN_FINAL_CLASSES_ATTR_NAME, - // TSLANG 82): a @final class, or a declared @cpp one (a C++ object, which holds nothing of ours). + // TSLANG 84): a @final class, or a declared @cpp one (a C++ object, which holds nothing of ours). void MLIRGenImpl::markFinalClasses() { llvm::SmallVector finals; diff --git a/tslang/lib/TypeScript/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h index 300c5a7ca..eb0b4a0d5 100644 --- a/tslang/lib/TypeScript/OwnershipFacts.h +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -2330,7 +2330,7 @@ inline mlir_ts::ClassStorageType fieldOwnerStorage(mlir_ts::PropertyRefOp proper } // Can a value of type `holder`, released, free a value of type `target`: is `target` the holder, or -// under it, by the types its fields, elements and members can hold (TSLANG 82)? A class not known to be +// under it, by the types its fields, elements and members can hold (TSLANG 84)? A class not known to be // of that class only (not @final, OWN_FINAL_CLASSES_ATTR_NAME) may be a subclass holding anything, and // an interface, `any`, a function (its captures) or a type not listed here may too. inline bool typeMayHold(mlir::ModuleOp module, mlir::Type holder, mlir::Type target, llvm::SmallPtrSetImpl &seen) diff --git a/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp b/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp index 54d761e0c..93b656960 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp @@ -633,7 +633,7 @@ namespace own_inference // a chain whose places are not known (a borrowing call's result): what a release frees is what // the released value's type can hold, so one that cannot hold the borrowed value's type does not - // free it (TSLANG 82) + // free it (TSLANG 84) auto borrowedValue = borrowed ? borrowed : (read && read->getNumResults() == 1 ? read->getResult(0) : mlir::Value()); auto module = drop->getParentOfType(); if (chain.anyPlace && !isCall(drop)) @@ -745,7 +745,7 @@ namespace own_inference auto keepsClosure = givenClosure && llvm::any_of(drop->getOperands(), [&](mlir::Value operand) { return holdsCaptures(operand) && isKeptArgument(drop, operand); }); - // ...and a chain whose places are not known, by the types those places hold (TSLANG 82) + // ...and a chain whose places are not known, by the types those places hold (TSLANG 84) if (mayDrop && !keepsClosure && chain.anyPlace && borrowedValue) { if (auto places = callDropFields(drop)) diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.h b/tslang/lib/TypeScript/OwnershipSignaturePass.h index 929504fc5..0ae951c1d 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.h +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.h @@ -717,6 +717,10 @@ class OwnershipSignaturePass : public mlir::PassWrapper capturedFunctions(mlir::Operation *def); // A method of a closed interface (interfaceFamilies), `ts.InterfaceSymbolRef(value, index)`, or a // function-typed field of one (interfaceFieldFamilies), read and called: the family of the slot diff --git a/tslang/lib/TypeScript/OwnershipSignatureResolve.cpp b/tslang/lib/TypeScript/OwnershipSignatureResolve.cpp index 3438e8fcd..0ac174eaf 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureResolve.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureResolve.cpp @@ -423,6 +423,19 @@ namespace own_signature resolveInterfaceSlot(interfaceRefOp, interfaceFamilies, callees); return; } + else if (auto symbols = capturedFunctions(def); !symbols.empty()) + { + // a function value a closure captured: `const f = (y) => ...; const g = (y) => f(y)` + // (TSLANG 82), a call of the functions stored into its box's field + callees.known = true; + for (auto symbol : symbols) + { + callees.named.push_back(symbol); + callees.known = (addDefined(symbol, callees) || addImported(symbol, callees)) && callees.known; + } + + return; + } else if (auto symbol = globalFunction(def)) { // a global nothing assigns, given a function by its initializer: `const f = Math.fround` @@ -473,6 +486,159 @@ namespace own_signature resolveFamily(index, identifier, callees); } + llvm::SmallVector OwnershipSignaturePass::capturedFunctions(mlir::Operation *def) + { + llvm::SmallVector symbols; + // `ts.Load(ts.PropertyRef(%box, n))`, the box the closure's first argument + auto loadOp = mlir::dyn_cast_or_null(def); + auto field = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); + auto box = field ? mlir::dyn_cast(field.getObjectRef()) : mlir::BlockArgument(); + auto closure = box ? mlir::dyn_cast_or_null(box.getOwner()->getParentOp()) : mlir_ts::FuncOp(); + if (!closure || box.getArgNumber() != 0 || !box.getOwner()->isEntryBlock() || !mlir::isa(box.getType())) + { + return symbols; + } + + auto position = field.getPosition(); + auto name = closure.getSymName(); + auto module = closure->getParentOfType(); + auto failed = false; + llvm::SetVector boxes; + llvm::SetVector closures; + // the boxes the closure is made with, or called with + module.walk([&](mlir::Operation *op) { + if (auto boundOp = mlir::dyn_cast(op)) + { + auto symbolRefOp = boundOp.getFunc().getDefiningOp(); + if (symbolRefOp && symbolRefOp.getIdentifier() == name) + { + boxes.insert(boundOp.getThisVal()); + } + } + else if (auto callOp = mlir::dyn_cast(op); + callOp && callOp.getCallee() == name && callOp->getNumOperands() > 0) + { + boxes.insert(callOp->getOperand(0)); + } + else if (auto refOp = mlir::dyn_cast(op); refOp && refOp.getIdentifier() == name) + { + // the closure's symbol used other than to bind it to a box: not known + failed = failed || llvm::any_of(refOp->getUsers(), [](mlir::Operation *user) { + return !mlir::isa(user); + }); + } + }); + + if (failed || boxes.empty()) + { + return symbols; + } + + // each box: a local only bound, called with and read for its fields, whose field `position` is + // given a function's symbol and nothing else; the closures that get it write none of its fields + for (auto boxValue : boxes) + { + auto varOp = boxValue.getDefiningOp(); + if (!varOp) + { + return {}; + } + + for (auto *user : varOp->getUsers()) + { + if (auto boundOp = mlir::dyn_cast(user)) + { + auto symbolRefOp = boundOp.getFunc().getDefiningOp(); + if (!symbolRefOp) + { + return {}; + } + + closures.insert(symbolRefOp.getIdentifier()); + continue; + } + + if (auto callOp = mlir::dyn_cast(user)) + { + if (llvm::count(callOp->getOperands(), varOp.getResult()) != 1 || callOp->getOperand(0) != varOp.getResult()) + { + return {}; + } + + closures.insert(callOp.getCallee()); + continue; + } + + auto propertyRefOp = mlir::dyn_cast(user); + if (!propertyRefOp) + { + return {}; + } + + for (auto *refUser : propertyRefOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(refUser); + if (mlir::isa(refUser) || + (storeOp && propertyRefOp.getPosition() != position && storeOp.getReference() == propertyRefOp.getResult())) + { + continue; + } + + if (!storeOp || storeOp.getReference() != propertyRefOp.getResult()) + { + return {}; + } + + auto stored = storeOp.getValue(); + while (auto castOp = stored.getDefiningOp()) + { + stored = castOp.getIn(); + } + + auto symbolRefOp = stored.getDefiningOp(); + if (!symbolRefOp) + { + return {}; + } + + if (!llvm::is_contained(symbols, symbolRefOp.getIdentifierAttr().getAttr())) + { + symbols.push_back(symbolRefOp.getIdentifierAttr().getAttr()); + } + } + } + } + + // the closures that get the box write none of its fields + for (auto closureName : closures) + { + auto funcOp = functions.lookup(closureName); + if (!funcOp || funcOp.isExternal()) + { + return {}; + } + + auto &entry = funcOp.getFunctionBody().front(); + if (entry.getNumArguments() == 0) + { + return {}; + } + + auto written = false; + funcOp.walk([&](mlir_ts::PropertyRefOp propertyRefOp) { + written = written || (propertyRefOp.getObjectRef() == entry.getArgument(0) && + llvm::any_of(propertyRefOp->getUsers(), + [](mlir::Operation *user) { return !mlir::isa(user); })); + }); + if (written) + { + return {}; + } + } + + return symbols; + } + mlir::StringAttr OwnershipSignaturePass::globalFunction(mlir::Operation *def) { // `ts.Load(ts.AddressOf @g)` of a global no store writes, whose initializer gives it a diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index b5b128279..9ebb8571d 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -1930,8 +1930,9 @@ endforeach() # TSLANG_ISSUES 78: a value pushed on or stored into a field of a local object, then used # TSLANG_ISSUES 79: a local pushed only where it is not null, then used # TSLANG_ISSUES 80-81: a constructor parameter stored into a field, read past calls; a value moved on some paths only -# TSLANG_ISSUES 82: a borrowed call result across a release of a type that cannot hold it +# TSLANG_ISSUES 82: a closure calling a function value it captured # TSLANG_ISSUES 83: a row of a global array of constant arrays bound to a local +# TSLANG_ISSUES 84: a borrowed call result across a release of a type that cannot hold it foreach(issues_mm gc rc none own) foreach(issues_test 00tslang_issue_44 00tslang_issue_45 00tslang_issue_46 00tslang_issue_47 00tslang_issue_48 00tslang_issue_49 00tslang_issue_50 00tslang_issue_52 00tslang_issue_53 00tslang_issue_54 00tslang_issue_55 00tslang_issue_56 00tslang_issue_57 @@ -1939,7 +1940,7 @@ foreach(issues_mm gc rc none own) 00tslang_issue_61 00tslang_issue_62 00tslang_issue_63 00tslang_issue_66 00tslang_issue_67 00tslang_issue_69 00tslang_issue_71 00tslang_issue_73 00tslang_issue_75 00tslang_issue_76 00tslang_issue_77 00tslang_issue_78 00tslang_issue_79 00tslang_issue_80 00tslang_issue_81 00tslang_issue_82 - 00tslang_issue_83) + 00tslang_issue_83 00tslang_issue_84) tslang_add_test(NAME test-jit-${issues_test}-${issues_mm} COMMAND test-runner -jit -mm=${issues_mm} "${PROJECT_SOURCE_DIR}/test/tester/tests/${issues_test}.ts") tslang_add_test(NAME test-compile-${issues_test}-${issues_mm} COMMAND test-runner -mm=${issues_mm} "${PROJECT_SOURCE_DIR}/test/tester/tests/${issues_test}.ts") endforeach() @@ -5522,7 +5523,8 @@ set(own_error_cases "own_err_pushed_let_popped|:17:9: error: '[^']*' borrows an element but is used here after it may be released or overwritten" "own_err_moved_on_some_paths_used|:21:15: error: '[^']*' is used here after its value was moved" "own_err_escaped_this_field_read|:24:28: error: '[^']*' is used here after its value was moved" - "own_err_borrowed_result_holder_stored|:26:16: error: '[^']*' borrows a field but is used here after it may be released or overwritten") + "own_err_borrowed_result_holder_stored|:26:16: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_closure_reassigned_captured_call|:23:15: error: '[^']*' borrows a field but is used here after it may be released or overwritten") foreach(own_error_case ${own_error_cases}) string(FIND "${own_error_case}" "|" own_error_sep) string(SUBSTRING "${own_error_case}" 0 ${own_error_sep} own_error_name) diff --git a/tslang/test/tester/own/own_err_borrowed_result_holder_stored.ts b/tslang/test/tester/own/own_err_borrowed_result_holder_stored.ts index 2e283d065..9f2d3c9df 100644 --- a/tslang/test/tester/own/own_err_borrowed_result_holder_stored.ts +++ b/tslang/test/tester/own/own_err_borrowed_result_holder_stored.ts @@ -1,6 +1,6 @@ // -mm=own rejects: `texture` borrows the result of `Chain.texture`, a place not known, and the store // into `this.chain` releases a Chain, whose type holds textures: it may free the one borrowed -// (TSLANG 82 refines by type only where the released type cannot hold the borrowed one). +// (TSLANG 84 refines by type only where the released type cannot hold the borrowed one). @final class Tex { constructor(public id: int) {} diff --git a/tslang/test/tester/own/own_err_closure_reassigned_captured_call.ts b/tslang/test/tester/own/own_err_closure_reassigned_captured_call.ts new file mode 100644 index 000000000..a942b6fd4 --- /dev/null +++ b/tslang/test/tester/own/own_err_closure_reassigned_captured_call.ts @@ -0,0 +1,29 @@ +// -mm=own rejects: `rcp` calls `f`, a function value it captured, and `swap` gives `f` a function that +// empties `this.values`: the call is not known (TSLANG 82 resolves a captured function value only when +// every store into it is a function's symbol, and no closure writes it), so `a` may be freed. +class P { + values: number[] = [2.0]; + + calc(x: number): number { + let f = (y: number): number => y + 1; + const swap = () => { + f = (y: number): number => { + this.values = []; + return y; + }; + }; + const rcp = (y: number): number => f(y * 2); + swap(); + return rcp(x); + } + + run() { + const a = this.values; + this.calc(1.0); + print(a[0]); + } +} + +function main() { + new P().run(); +} diff --git a/tslang/test/tester/tests/00tslang_issue_82.ts b/tslang/test/tester/tests/00tslang_issue_82.ts index 580bb43b2..4fa88a2cf 100644 --- a/tslang/test/tester/tests/00tslang_issue_82.ts +++ b/tslang/test/tester/tests/00tslang_issue_82.ts @@ -1,41 +1,25 @@ -// TSLANG 82: a borrow of a call's result, whose place is not known (`this.chain.texture(1)`), across a -// store that releases a value whose type cannot hold it (a @final Pipe, holding no texture): the store -// does not free it. -@final -class Tex { - constructor(public id: int) {} -} -@final -class Chain { - private textures: Tex[] = [new Tex(1), new Tex(2)]; - @lends("this") - texture(i: int): Tex { return this.textures[i]; } -} -@final -class Pipe { - constructor(public id: int) {} -} -@final -class App { - private chain = new Chain(); - private pipes: (Pipe | null)[] = [null, null]; - pipeline(k: int): Pipe | null { - const made = this.pipes[k]; - if (made) { - return made; - } - const p = new Pipe(k); - this.pipes[k] = p; - return this.pipes[k]; +// TSLANG 82: a closure that calls a function value it captured (`const f = (y) => ...; const rcp = (y) => +// f(y)`): every store into the captured `f` is a function's symbol, and no closure writes it, so the +// call is a call of that function, which destroys nothing; the method's callers keep their borrows. +class Holder { n: number = 1.0; } +class P { + holder: Holder | null = new Holder(); + values: number[] = [2.0]; + calc(h: Holder | null, x: number): number { + const f = (y: number): number => y * 2; + const rcp = (y: number): number => { + const r = f(y); + return f(r + r); + }; + return h ? rcp(x * h.n) : 0.0; } - frame(): int { - const texture = this.chain.texture(1); - const p = this.pipeline(0); - return texture.id + (p ? p.id : 0); + run(): number { + const a = this.values; + const r = this.calc(this.holder, 1.0); + return a[0] + r; } } function main() { - const a = new App(); - assert(a.frame() == 2, "frame"); + assert(new P().run() == 10, "run"); print("done."); } diff --git a/tslang/test/tester/tests/00tslang_issue_84.ts b/tslang/test/tester/tests/00tslang_issue_84.ts new file mode 100644 index 000000000..98962118a --- /dev/null +++ b/tslang/test/tester/tests/00tslang_issue_84.ts @@ -0,0 +1,41 @@ +// TSLANG 84: a borrow of a call's result, whose place is not known (`this.chain.texture(1)`), across a +// store that releases a value whose type cannot hold it (a @final Pipe, holding no texture): the store +// does not free it. +@final +class Tex { + constructor(public id: int) {} +} +@final +class Chain { + private textures: Tex[] = [new Tex(1), new Tex(2)]; + @lends("this") + texture(i: int): Tex { return this.textures[i]; } +} +@final +class Pipe { + constructor(public id: int) {} +} +@final +class App { + private chain = new Chain(); + private pipes: (Pipe | null)[] = [null, null]; + pipeline(k: int): Pipe | null { + const made = this.pipes[k]; + if (made) { + return made; + } + const p = new Pipe(k); + this.pipes[k] = p; + return this.pipes[k]; + } + frame(): int { + const texture = this.chain.texture(1); + const p = this.pipeline(0); + return texture.id + (p ? p.id : 0); + } +} +function main() { + const a = new App(); + assert(a.frame() == 2, "frame"); + print("done."); +} From 1480fdfc6a18a45cba7054f412ead8bbdecb9da9 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 21:02:22 +0000 Subject: [PATCH 5/5] TSLANG_ISSUES: 82 and 83 fixed; the release-type fix is 84 Co-Authored-By: Claude Opus 5.5 --- TSLANG_ISSUES.md | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/TSLANG_ISSUES.md b/TSLANG_ISSUES.md index 9eee7dd71..9b4684cdc 100644 --- a/TSLANG_ISSUES.md +++ b/TSLANG_ISSUES.md @@ -634,7 +634,7 @@ into what follows the move, from where the value is held and was not moved. The use after the paths join is still refused, a copyable value's read included (`own_string_copy_branch.ts`). Regression tests `tslang/test/tester/tests/00tslang_issue_81.ts` (every model) and `own/own_err_moved_on_some_paths_used.ts`. -82 (a borrow of a call's result across a store of an unrelated type: `const texture = this.swapChain.texture(index); +84 (a borrow of a call's result across a store of an unrelated type: `const texture = this.swapChain.texture(index); ...; this.recordTriangle(commandBuffer); ...; commandBuffer.textureBarrier(texture, ...)` in `examples/nri/color_write_enable_nri.ts`, where `recordTriangle` stores a pipeline into `this.trianglePipelines[enabled]`, refused "borrows the result of 'NriSwapChain.texture' but is used here after @@ -646,7 +646,7 @@ declared @cpp one, which holds nothing of ours (MLIRGen lists them on the module Any other class may be a subclass's object, holding anything, and an interface, `any` or a function may hold anything too. The borrowed value too must be of a @final class, or an array. In the sweep, `color_write_enable_nri.ts` and `logic_op_dynamic_state_nri.ts` compile. Regression tests -`tslang/test/tester/tests/00tslang_issue_82.ts` (every model) and `own/own_err_borrowed_result_holder_stored.ts`. +`tslang/test/tester/tests/00tslang_issue_84.ts` (every model) and `own/own_err_borrowed_result_holder_stored.ts`. For native_ts_graphics, from the same sweep (its code, not the compiler): - `core/ray_tracing_extended_scene.ts`: the exported classes' constructors keep their array parameters @@ -678,7 +678,7 @@ For native_ts_graphics, from the same sweep (its code, not the compiler): "used here after its value was moved" (27), and an element borrowed and stored (21) or used after it may be released (20). -82 (open, found 2026-11-14 on head 5aa1cdb0; native_ts_graphics `examples/raytracing_mini_engine.ts` +82 (fixed; found 2026-11-14 on head 5aa1cdb0; native_ts_graphics `examples/raytracing_mini_engine.ts` `computeAO`): a closure that calls another closure it captured makes its method count as freeing anything, so every caller's borrows end at the call. Run with `-mm=own --emit=obj`: @@ -716,7 +716,15 @@ FieldDrops resolves a call made inside a lambda through a captured value. Workar native_ts_graphics: the helper is a module-level function (`refinedReciprocal`) instead of a closure capturing `f`. -83 (open, found 2026-11-14 on head 5aa1cdb0; native_ts_graphics `hdr_display`, `hdr_display_nri`): +Fixed: the signature pass took a call through a field of the closure's capture box (its first argument) as a +callback it knows nothing of. A captured function value now resolves to the functions stored into that +field, when every store into it, in each function that makes or calls the closure with a box, is a +function's symbol, and the closures given the box write none of its fields. `const f = ...` captured by +`rcp` is then a call of `f`'s function. A captured `let` another closure assigns stays unknown. Regression +tests `tslang/test/tester/tests/00tslang_issue_82.ts` (every model) and +`own/own_err_closure_reassigned_captured_call.ts`. + +83 (fixed; found 2026-11-14 on head 5aa1cdb0; native_ts_graphics `hdr_display`, `hdr_display_nri`): under `-mm=own`, a row of a module-level array of arrays bound to a local (`const meta = POOL[idx];`) is freed at the local's end, so the second call frees it again: the program hangs (here, and in hdr_display at exit in mimalloc's `mi_process_done` -> `_mi_page_free_collect`, a cyclic free list) or @@ -744,5 +752,13 @@ Workaround in native_ts_graphics: `.slice()` (a copy) at the three such sites (` `hdr_display_nri.ts`, `raytracing_mini_engine.ts`; native_ts_graphics #322). 61 (immortal array globals, after 5aa1cdb0) may cover this; not rechecked on a head with it. +Fixed (it still hung after 61 and #856): `POOL` passed for a keeper, an array global whose elements are +immortal, because its literal's elements are constants. Its rows are constant arrays, though, each a block +the literal makes, and nothing makes them immortal. A keeper's element read is no borrow, so `meta` owned the +row and freed it on each call. A global whose literal has constant arrays (or tuples) for elements is no +keeper now, so a read of its row is a borrow, and the local releases nothing. Scalar and string constants +still make one. Regression test `tslang/test/tester/tests/00tslang_issue_83.ts` (every model; built with the +old compiler, its exe hangs at exit). + native_ts_graphics: with #321 and #322 (2026-11-14, on 5aa1cdb0 with workarounds) every source compiles under `-mm=own` and its ctest passes 525/525, as under gc and rc.