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
53 changes: 52 additions & 1 deletion TSLANG_ISSUES.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,57 @@ own errors are left (its `@keeps` and `Shared<T>`, below). One site of this shap
does not reproduce. Regression tests `tslang/test/tester/tests/00tslang_issue_59.ts` (every model) and
`own/own_err_element_field_moved_replaced.ts`.

60 (the error at a borrow a call ends did not say why, when the callee is a function defined here: `const
name = this.name; for (...) this.props[i].tick(this.gameTime); ... name` in `core/rtxpt_game.ts`, "borrows a
field but is used here after it may be released or overwritten", with only "it may be released or
overwritten here" at the call) is fixed. `Prop.tick` calls `setNodeTransform`, which calls
`world.setGameNodeTransform(...)`, a method of the exported `GameWorld`. Another module may extend
`GameWorld` and override it with a method that destroys anything, so `Prop.tick` may too, and the borrows
of `this.props[i]` and `this.name` end at it. 48 named the method only when the call itself was the
virtual one. The signature pass now finds, for a call whose callees may destroy anything, the overridable
call they reach, breadth first through calls that may destroy anything, and the error notes it: "'Prop.tick'
calls 'setNodeTransform', which calls 'GameWorld.setGameNodeTransform'; that method may be overridden in
another module by a method that does; '@final' on the method, or on its class, says it is not". Nothing
compiles that did not: it is the diagnostic. The note is given only when that method is the one
reason (no other overridable method, unknown or imported callee, or body that destroys anything on the
way), so `@final` on it is enough. In the sweep, 48 errors carry it, beside the 51 at a direct call of
such a method. They name native_ts_graphics' `@final`s to add: `MsvcRandom` (41, through the
examples' `randomFloat`) and `GameWorld` (7). Regression tests
`tslang/test/tester/tests/00tslang_issue_60.ts` (every model; the shape with `@final`, which compiles) and
`own/own_err_overridable_reached.ts` (the note).

61 (a module or namespace array nothing assigns, read where a borrow cannot go: `[CUBE_VERTICES,
PLANE_VERTICES, CUBE_VERTICES]` and `[[MAT_RED, MAT_GREEN, ...], [MAT_GREY]]` in
`examples/nri/ray_tracing_reflection_nri.ts`, `return GREEN;` in `examples/nri/collision_nri.ts`, `const p
= CAMERA_POSITION;` held across calls in `examples/nri/async_compute_bloom_nri.ts`, refused "the value read
from 'X' is kept where its end at an assignment of 'X' cannot be checked" and "borrows the global 'X' and
cannot be stored, returned or captured") is fixed. Even a plain `const A = [1.0, 2.0]` at the top level was
refused: only a global of a class type could be immortal (own spec 25). An array global is now one on the
same terms - not exported, written once by its initializer, given a fresh array, nothing, or another
immortal global's value - and a read of it owns nothing and may go anywhere. The block made immortal is the
array's header, which every array release tests first, so neither its data nor its elements are released
through it; an element overwritten, popped or spliced out still frees the old element, and a borrow of
`G[i]` stays bounded. A global pushed to, or written in any other way through its address, stays mortal.
The initializing store marks the header only when it is mortal: a constant array's header is in read-only
data, immortal already. Spec 25 has the amendment. In the sweep, 126 sources failing went to 119
(`ptlas.ts`, `ray_tracing_reflection.ts`, `collision.ts`, `nvrhi_direct.ts`, `async_compute_bloom.ts`,
`msaa.ts`, `texture_compression_comparison.ts` now compile, and `ray_tracing_reflection_nri.ts` went from
29 errors to 1), and errors from 907 to 811. Regression tests `tslang/test/tester/tests/00tslang_issue_61.ts`
(every model) and `own/own_err_immortal_array_element_overwritten.ts`.

62 (an array changed in place through a local that borrows the field holding it: `const model =
this.refractionModel; ... model.push(0.0);` in `examples/ray_tracing_extended.ts` and its NRI ports, refused
"borrows a field and cannot be stored, returned or captured" at the push) is fixed. `push`, `pop`,
`unshift`, `shift` and `splice` take the slot that holds the array's header, and the borrowing local's
slot was taken to keep the field's value. The header is the field's array, so the change is the field's
too: each such op through the local is now a use of the borrow, bounded as a read is. An element read
before a `pop` and used after, the field given another array before a use, and the local returned or
stored are still refused. A field that holds a null header (never given an array) gets one in the local's
slot only, which nothing frees: that is `undefined` in TypeScript terms, where `.push` throws, as the
array spec's ruling T4-D accepts for a parameter. In the sweep, 4 errors less (807); the two NRI ports
keep others. Regression tests `tslang/test/tester/tests/00tslang_issue_62.ts` (every model) and
`own/own_err_borrowed_array_pushed_returned.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 All @@ -394,7 +445,7 @@ For native_ts_graphics, from the same sweep (its code, not the compiler):
takes it; the next release won't. `desc.setDepthClip(clip != 0)` fixes it.
- Under `-mm=own`, 4 `core/` modules do not compile yet (`donut_first_person_camera.ts`,
`nri_donut_textures.ts`, `ray_tracing_extended_scene.ts`, `rtxpt_game.ts`), and nor do many examples,
which #310 did not port: with every facts object built and 50-59 in, 126 of the 276 sources fail. The
which #310 did not port: with every facts object built and 50-62 in, 119 of the 276 sources fail. The
largest group left is "borrows a field but is used here after it may be released or overwritten" (about
200 sites), then "used here after its value was moved" (37: mostly a value a pushed object or an
earlier call keeps, then returned or used).
Original file line number Diff line number Diff line change
Expand Up @@ -3057,6 +3057,18 @@ above, in a static block whose only store of `K.w` is the loop's) and `own_err_g
programs exit 139 on Windows and under `MALLOC_PERTURB_` on Linux. Newly rejected beside them:
only the unsafe shapes; the corpus keeps 526 files and the ray tracer has no error.

**Arrays (TSLANG 61), 2026-11-14.** A global of an array type is a candidate too, on the same terms
(not exported, written once by its initializer, given a fresh array, nothing, or another immortal
global's value): `const CUBE_VERTICES = [...]` read into `[CUBE_VERTICES, PLANE_VERTICES]`, or
`return GREEN;`. The block made immortal is the array's header, which every array release tests
first (`buildArrayBody`'s `emitIfLastReference`), so neither the data block nor the elements are
ever released through it. As with a class's fields, only that block: an element overwritten, popped
or spliced out frees the old element, and a borrow of `G[i]` stays bounded
(`own_err_immortal_array_element_overwritten`). A global given a push or any other write through its
address stays mortal (its `ts.AddressOf` is then not only loaded). The initializing store marks the
header with `emitMarkImmortalIfMortal`: a constant array's header is in read-only data, immortal
already. Test: `00tslang_issue_61`.

## 26. `Shared<I>` of an object literal (G4 of the ray tracer plan), 2026-10-21

The ray tracer's surfaces are module-level `new Shared<Surface>({ ...function fields... })`. The
Expand Down
10 changes: 9 additions & 1 deletion tslang/lib/TypeScript/LowerToLLVM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5522,7 +5522,15 @@ struct StoreOpLowering : public TsLlvmPattern<mlir_ts::StoreOp>
if (tsLlvmContext->compileOptions.memoryModel == MemoryModelOwn && storeOp->hasAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME))
{
OwnershipRoutineLogic orl(storeOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions);
orl.emitMarkImmortal(transformed.getValue());
// an array's header may be a constant one in read-only data, immortal already (TSLANG 61)
if (mlir::isa<mlir_ts::ArrayType>(storeOp.getValue().getType()))
{
orl.emitMarkImmortalIfMortal(transformed.getValue());
}
else
{
orl.emitMarkImmortal(transformed.getValue());
}
}

const bool isInvariantGroup = false; // TODO: Determine if this should be set based on some condition
Expand Down
4 changes: 4 additions & 0 deletions tslang/lib/TypeScript/OwnershipFacts.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ namespace mlir_ts = mlir::typescript;
// A virtual call that may destroy anything only because another module may override the method it
// names (TSLANG 48): the method's symbol, for the error that a borrow ends there
#define OWN_OVERRIDABLE_CALL_ATTR_NAME "__own_overridable_call"
// A call of functions defined here that may destroy anything because a call they reach, directly or
// through others, is such a virtual call (TSLANG 60): [the method's symbol, then the functions on the
// way to it, the callee first], for the same error
#define OWN_OVERRIDABLE_VIA_ATTR_NAME "__own_overridable_via"
// On a `ts.ReleaseSlot` of a field: a fill of the field, tested empty on the way (fillsEmptyField,
// TSLANG 47), which frees nothing. The signature pass decides it, with what calls in between may write.
#define OWN_FILL_ATTR_NAME "__own_fill"
Expand Down
12 changes: 12 additions & 0 deletions tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,18 @@ namespace own_inference
continue;
}

// `const model = this.model; model.push(0.0)` (TSLANG 62): the array changed in place,
// through the slot that holds its header - the borrowed array, which the field sees. A
// null header (an unset field) gets one in the local's slot only, which nothing frees:
// that is `undefined` in TypeScript terms, where `.push` throws (array spec, ruling T4-D)
if (mlir::isa<mlir_ts::ArrayPushOp, mlir_ts::ArrayPopOp, mlir_ts::ArrayUnshiftOp, mlir_ts::ArrayShiftOp,
mlir_ts::ArraySpliceOp>(user) &&
user->getOperand(0) == borrower.getResult())
{
uses.push_back({user, {kills.begin(), kills.end()}});
continue;
}

auto readOp = mlir::dyn_cast<mlir_ts::LoadOp>(user);
if (!readOp)
{
Expand Down
15 changes: 15 additions & 0 deletions tslang/lib/TypeScript/OwnershipInferenceDiagnostics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,21 @@ namespace own_inference
<< "'" << method.getValue() << "' may be overridden in another module by a method that does; '@" FINAL_DECORATOR
<< "' on the method, or on its class, says it is not";
}
// ...or a call of a function here that reaches such a call (TSLANG 60)
else if (auto via = drop->getAttrOfType<mlir::ArrayAttr>(OWN_OVERRIDABLE_VIA_ATTR_NAME); via && via.size() >= 2)
{
// `'Prop.tick' calls 'setNodeTransform', which calls 'GameWorld.setGameNodeTransform'`
auto &note = diag.attachNote(drop->getLoc());
note << "'" << mlir::cast<mlir::StringAttr>(via[1]).getValue() << "' calls '";
for (auto index = 2u; index < via.size(); ++index)
{
note << mlir::cast<mlir::StringAttr>(via[index]).getValue() << "', which calls '";
}

note << mlir::cast<mlir::FlatSymbolRefAttr>(via[0]).getValue()
<< "'; that method may be overridden in another module by a method that does; '@" << FINAL_DECORATOR
<< "' on the method, or on its class, says it is not";
}

signalPassFailure();
}
Expand Down
2 changes: 1 addition & 1 deletion tslang/lib/TypeScript/OwnershipInferencePass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -818,7 +818,7 @@ namespace own_inference
OWN_RESULT_RECORD_ATTR_NAME, OWN_RESULT_RECORD_UNBOXED_ATTR_NAME,
OWN_RECORD_UNBOXED_FIELDS_ATTR_NAME, OWN_RECORD_TEMPORARY_ATTR_NAME,
OWN_NO_DROPS_ATTR_NAME, OWN_DROP_FIELDS_ATTR_NAME, OWN_FACTS_LOST_ATTR_NAME,
OWN_OVERRIDABLE_CALL_ATTR_NAME, OWN_FILL_ATTR_NAME,
OWN_OVERRIDABLE_CALL_ATTR_NAME, OWN_OVERRIDABLE_VIA_ATTR_NAME, OWN_FILL_ATTR_NAME,
OWN_FRESH_RESULT_ATTR_NAME,
OWN_ASSIGNS_CAPTURES_ATTR_NAME,
OWN_RESULT_BOUNDED_ATTR_NAME, OWN_STATE_BORROW_ATTR_NAME,
Expand Down
6 changes: 5 additions & 1 deletion tslang/lib/TypeScript/OwnershipSignatureGlobals.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,11 @@ namespace own_signature
llvm::MapVector<mlir_ts::GlobalOp, std::pair<mlir::Value, mlir_ts::StoreOp>> candidates;
auto mth = typeHelper(&getContext());
module.walk([&](mlir_ts::GlobalOp globalOp) {
if (!mlir::isa<mlir_ts::ClassType>(globalOp.getType()) || !mth.ownsHeapMemory(globalOp.getLoc(), globalOp.getType()) ||
// a class's object, or an array (TSLANG 61: `const CUBE_VERTICES = [...]`, read into
// `[CUBE_VERTICES, PLANE_VERTICES]`): one block the global keeps, whose elements stay the
// array's, freed as any place's old value is when overwritten
if (!mlir::isa<mlir_ts::ClassType, mlir_ts::ArrayType>(globalOp.getType()) ||
!mth.ownsHeapMemory(globalOp.getLoc(), globalOp.getType()) ||
globalOp->hasAttr("export") || globalOp->hasAttr("import") || globalOp->hasAttr(DLL_EXPORT) ||
globalOp->hasAttr(DLL_IMPORT) || !globalOp.getInitializerRegion().hasOneBlock())
{
Expand Down
147 changes: 147 additions & 0 deletions tslang/lib/TypeScript/OwnershipSignaturePass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ namespace own_signature
}
}

markOverridableReached();
exportNoDrops();
ownGlobalInitializerCalls(module);

Expand Down Expand Up @@ -1819,6 +1820,152 @@ namespace own_signature
}
}

void OwnershipSignaturePass::markOverridableReached()
{
// `this.props[i].tick(t)`, where `Prop.tick` calls `setNodeTransform`, which calls
// `world.setGameNodeTransform(...)` of an exported class: the outer call may destroy anything
// only because of that virtual call, which the error at a borrow it ends names (TSLANG 60). The
// way to it is found breadth first, through calls that may destroy anything, so it is a short one.
llvm::DenseMap<mlir::Operation *, llvm::SmallVector<Call *>> callsIn;
for (auto &call : calls)
{
callsIn[call.op->getParentOfType<mlir_ts::FuncOp>()].push_back(&call);
}

auto mayDropAnything = [&](const Call &call) {
if (call.op->hasAttr(OWN_NO_DROPS_ATTR_NAME) || call.op->hasAttr(OWN_DROP_FIELDS_ATTR_NAME))
{
return false;
}

auto drops = calleesFieldDrops(call.callees);
return !drops || drops->any;
};

// a function's own body destroying anything (not through its calls), as the last round decided it
llvm::DenseMap<mlir::Operation *, bool> bodyAny;
auto bodyDropsAnything = [&](mlir_ts::FuncOp funcOp) {
auto [found, inserted] = bodyAny.try_emplace(funcOp, false);
if (inserted)
{
found->second = dropsInBody(funcOp, true) && fieldDropsInBody(funcOp, true).any;
}

return found->second;
};

// The one overridable method the functions reach, and the way to it - none when they reach
// another, or something else that may destroy anything (an unknown or imported callee, a body
// that does): `@final` on that method would not be enough then
auto explain = [&](llvm::ArrayRef<mlir_ts::FuncOp> funcs) -> mlir::ArrayAttr {
// each function reached, and the one that called it on the way
llvm::DenseMap<mlir::Operation *, mlir::Operation *> reachedFrom;
llvm::SmallVector<mlir_ts::FuncOp> work;
for (auto callee : funcs)
{
if (reachedFrom.try_emplace(callee, nullptr).second)
{
work.push_back(callee);
}
}

mlir::FlatSymbolRefAttr method;
mlir::Operation *holder = nullptr;
for (auto next = 0u; next < work.size(); ++next)
{
auto funcOp = work[next];
if (work.size() > 256 || bodyDropsAnything(funcOp))
{
return {};
}

for (auto *inner : callsIn[funcOp])
{
if (auto overridable = inner->op->getAttrOfType<mlir::FlatSymbolRefAttr>(OWN_OVERRIDABLE_CALL_ATTR_NAME))
{
if (method && method != overridable)
{
return {};
}

if (!method)
{
method = overridable;
holder = funcOp;
}

continue;
}

if (!mayDropAnything(*inner))
{
continue;
}

if (!inner->callees.known || inner->callees.imported || inner->callees.funcs.empty())
{
return {};
}

for (auto callee : inner->callees.funcs)
{
if (reachedFrom.try_emplace(callee, funcOp).second)
{
work.push_back(callee);
}
}
}
}

if (!method)
{
return {};
}

llvm::SmallVector<mlir::Attribute> path;
for (auto *at = holder; at; at = reachedFrom.lookup(at))
{
path.push_back(mlir::cast<mlir_ts::FuncOp>(at).getSymNameAttr());
}

std::reverse(path.begin(), path.end());
path.insert(path.begin(), method);
return mlir::ArrayAttr::get(&getContext(), path);
};

// a call of one function, the common case, explained once per function
llvm::DenseMap<mlir::Operation *, mlir::ArrayAttr> explained;
for (auto &call : calls)
{
if (!call.callees.known || call.callees.imported || call.callees.funcs.empty() ||
call.op->hasAttr(OWN_OVERRIDABLE_CALL_ATTR_NAME) || !mayDropAnything(call))
{
continue;
}

mlir::ArrayAttr via;
if (call.callees.funcs.size() == 1)
{
auto [found, inserted] = explained.try_emplace(call.callees.funcs.front(), mlir::ArrayAttr());
if (inserted)
{
found->second = explain(call.callees.funcs);
}

via = found->second;
}
else
{
via = explain(call.callees.funcs);
}

if (via)
{
call.op->setAttr(OWN_OVERRIDABLE_VIA_ATTR_NAME, via);
}
}
}

std::optional<OwnershipSignaturePass::FieldDrops> OwnershipSignaturePass::calleesFieldDrops(const Callees &callees)
{
if (!callees.known || callees.instanceOf)
Expand Down
3 changes: 3 additions & 0 deletions tslang/lib/TypeScript/OwnershipSignaturePass.h
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,9 @@ class OwnershipSignaturePass : public mlir::PassWrapper<OwnershipSignaturePass,

// The FieldDrops of a call's callees together, when every one has them.
std::optional<FieldDrops> calleesFieldDrops(const Callees &callees);
// OWN_OVERRIDABLE_VIA_ATTR_NAME on each call whose callees reach an overridable virtual call
// (TSLANG 60)
void markOverridableReached();

// A class's vtable is a global named `<class>..vtbl` that holds its `..instanceOf`. An
// interface's vtable for a class (`B.I..vtbl`) holds only the methods and is not one: a call
Expand Down
Loading
Loading