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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 41 additions & 9 deletions TSLANG_ISSUES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -632,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`.

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
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_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
(`this.origin = origin`), which an exported constructor has to say: `@keeps("origin", "direction")`
Expand Down Expand Up @@ -662,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`:

Expand Down Expand Up @@ -700,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
Expand Down Expand Up @@ -728,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.
3 changes: 3 additions & 0 deletions tslang/include/TypeScript/Defines.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 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
// groups`). A yield moves what such a field holds to the caller, so the field never owns it: an
Expand Down
1 change: 1 addition & 0 deletions tslang/lib/TypeScript/MLIRGenImpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
24 changes: 24 additions & 0 deletions tslang/lib/TypeScript/MLIRGenModule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,7 @@ namespace mlirgen
if (mlir::succeeded(mlirCodeGenModule(module, includeFiles)))
{
devirtualizeClosedMethods();
markFinalClasses();
return theModule;
}
}
Expand All @@ -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 84): a @final class, or a declared @cpp one (a C++ object, which holds nothing of ours).
void MLIRGenImpl::markFinalClasses()
{
llvm::SmallVector<mlir::Attribute> 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) {
Expand Down
87 changes: 87 additions & 0 deletions tslang/lib/TypeScript/OwnershipFacts.h
Original file line number Diff line number Diff line change
Expand Up @@ -2329,6 +2329,93 @@ inline mlir_ts::ClassStorageType fieldOwnerStorage(mlir_ts::PropertyRefOp proper
return mlir::dyn_cast<mlir_ts::ClassStorageType>(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 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<mlir::Type> &seen)
{
if (holder == target)
{
return true;
}

if (auto unionType = mlir::dyn_cast<mlir_ts::UnionType>(target))
{
return llvm::any_of(unionType.getTypes(), [&](mlir::Type member) { return typeMayHold(module, holder, member, seen); });
}

if (auto optionalType = mlir::dyn_cast<mlir_ts::OptionalType>(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<mlir_ts::ClassType>(target))
{
auto finals = module ? module->getAttrOfType<mlir::ArrayAttr>(OWN_FINAL_CLASSES_ATTR_NAME) : mlir::ArrayAttr();
auto name = targetClass.getName().getValue();
if (!finals || !llvm::any_of(finals.getAsRange<mlir::StringAttr>(), [&](mlir::StringAttr each) { return each.getValue() == name; }))
{
return true;
}
}
else if (!mlir::isa<mlir_ts::ArrayType>(target))
{
return true;
}

if (mlir::isa<mlir_ts::NumberType, mlir_ts::BooleanType, mlir_ts::StringType, mlir_ts::CharType, mlir_ts::LiteralType,
mlir_ts::NullType, mlir_ts::UndefinedType, mlir_ts::EnumType, mlir_ts::OpaqueType, mlir::IntegerType,
mlir::FloatType, mlir::IndexType>(holder))
{
return false;
}

if (!seen.insert(holder).second)
{
return false; // its fields are looked at already
}

if (auto unionType = mlir::dyn_cast<mlir_ts::UnionType>(holder))
{
return llvm::any_of(unionType.getTypes(), [&](mlir::Type member) { return typeMayHold(module, member, target, seen); });
}

if (auto optionalType = mlir::dyn_cast<mlir_ts::OptionalType>(holder))
{
return typeMayHold(module, optionalType.getElementType(), target, seen);
}

if (auto arrayType = mlir::dyn_cast<mlir_ts::ArrayType>(holder))
{
return typeMayHold(module, arrayType.getElementType(), target, seen);
}

if (auto classType = mlir::dyn_cast<mlir_ts::ClassType>(holder))
{
auto finals = module ? module->getAttrOfType<mlir::ArrayAttr>(OWN_FINAL_CLASSES_ATTR_NAME) : mlir::ArrayAttr();
auto name = classType.getName().getValue();
auto isFinal = finals && llvm::any_of(finals.getAsRange<mlir::StringAttr>(), [&](mlir::StringAttr each) { return each.getValue() == name; });
auto storageType = mlir::dyn_cast<mlir_ts::ClassStorageType>(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<mlir::Type, 8> 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)
Expand Down
33 changes: 33 additions & 0 deletions tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 84)
auto borrowedValue = borrowed ? borrowed : (read && read->getNumResults() == 1 ? read->getResult(0) : mlir::Value());
auto module = drop->getParentOfType<mlir::ModuleOp>();
if (chain.anyPlace && !isCall(drop))
{
if (auto releaseSlotOp = mlir::dyn_cast<mlir_ts::ReleaseSlotOp>(drop); releaseSlotOp && borrowedValue)
{
auto slotType = mlir::cast<mlir_ts::RefType>(releaseSlotOp.getSlot().getType()).getElementType();
return typeMayHold(module, slotType, borrowedValue.getType());
}

return true;
}

Expand Down Expand Up @@ -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 84)
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<mlir_ts::RefType>(placeType))
{
placeType = refType.getElementType();
}

return typeMayHold(module, placeType, borrowedValue.getType());
});
}
}

if (mayDrop && !keepsClosure && !chain.anyPlace)
{
if (auto places = callDropFields(drop))
Expand Down
36 changes: 34 additions & 2 deletions tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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<mlir_ts::FuncOp>();
auto objectLoad = field.getObjectRef().getDefiningOp<mlir_ts::LoadOp>();
auto slot = objectLoad ? objectLoad.getReference().getDefiningOp<mlir_ts::VariableOp>() : mlir_ts::VariableOp();
if (!funcOp || !funcOp.getSymName().ends_with(".constructor") || !slot || slot->getNumOperands() != 1 ||
funcOp.isExternal())
{
return false;
}

auto argument = mlir::dyn_cast<mlir::BlockArgument>(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<mlir_ts::LoadOp>(user);
return loadOp && llvm::all_of(loadOp->getUsers(), [](mlir::Operation *loadUser) {
return mlir::isa<mlir_ts::PropertyRefOp>(loadUser);
});
});
}
} // namespace

void OwnershipSignaturePass::borrowParametersInLocals(mlir::ModuleOp module)
Expand Down Expand Up @@ -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())
Expand Down
5 changes: 4 additions & 1 deletion tslang/lib/TypeScript/OwnershipSignatureKeepers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,11 @@ namespace own_signature
auto *def = castRoot(resultOp->getOperand(0)).getDefiningOp();
if (auto createArrayOp = mlir::dyn_cast_or_null<mlir_ts::CreateArrayOp>(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<mlir_ts::ConstantOp>();
auto constantOp = castRoot(element).getDefiningOp<mlir_ts::ConstantOp>();
return constantOp && !mlir::isa<mlir_ts::ConstArrayType, mlir_ts::ConstTupleType>(constantOp.getType());
});
}

Expand Down
Loading
Loading