diff --git a/TSLANG_ISSUES.md b/TSLANG_ISSUES.md index 3dbd83fe5..a67bbe141 100644 --- a/TSLANG_ISSUES.md +++ b/TSLANG_ISSUES.md @@ -384,6 +384,57 @@ own errors are left (its `@keeps` and `Shared`, 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")` @@ -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). diff --git a/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md b/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md index 6157ad039..dd7172e8d 100644 --- a/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md +++ b/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md @@ -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` of an object literal (G4 of the ray tracer plan), 2026-10-21 The ray tracer's surfaces are module-level `new Shared({ ...function fields... })`. The diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index 645ccc767..9a2fcafb7 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -5522,7 +5522,15 @@ struct StoreOpLowering : public TsLlvmPattern 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(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 diff --git a/tslang/lib/TypeScript/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h index fb9923743..572f5ba31 100644 --- a/tslang/lib/TypeScript/OwnershipFacts.h +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -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" diff --git a/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp b/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp index c3fc74463..7e48140fe 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp @@ -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(user) && + user->getOperand(0) == borrower.getResult()) + { + uses.push_back({user, {kills.begin(), kills.end()}}); + continue; + } + auto readOp = mlir::dyn_cast(user); if (!readOp) { diff --git a/tslang/lib/TypeScript/OwnershipInferenceDiagnostics.cpp b/tslang/lib/TypeScript/OwnershipInferenceDiagnostics.cpp index 492885553..02ecf8f50 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceDiagnostics.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceDiagnostics.cpp @@ -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(OWN_OVERRIDABLE_VIA_ATTR_NAME); via && via.size() >= 2) + { + // `'Prop.tick' calls 'setNodeTransform', which calls 'GameWorld.setGameNodeTransform'` + auto ¬e = diag.attachNote(drop->getLoc()); + note << "'" << mlir::cast(via[1]).getValue() << "' calls '"; + for (auto index = 2u; index < via.size(); ++index) + { + note << mlir::cast(via[index]).getValue() << "', which calls '"; + } + + note << mlir::cast(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(); } diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index b8ade0d49..cea770bc2 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -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, diff --git a/tslang/lib/TypeScript/OwnershipSignatureGlobals.cpp b/tslang/lib/TypeScript/OwnershipSignatureGlobals.cpp index 17a9f31b8..12c9664e3 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureGlobals.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureGlobals.cpp @@ -141,7 +141,11 @@ namespace own_signature llvm::MapVector> candidates; auto mth = typeHelper(&getContext()); module.walk([&](mlir_ts::GlobalOp globalOp) { - if (!mlir::isa(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(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()) { diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp index a84a5c11e..0e40f2edd 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp @@ -343,6 +343,7 @@ namespace own_signature } } + markOverridableReached(); exportNoDrops(); ownGlobalInitializerCalls(module); @@ -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> callsIn; + for (auto &call : calls) + { + callsIn[call.op->getParentOfType()].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 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 funcs) -> mlir::ArrayAttr { + // each function reached, and the one that called it on the way + llvm::DenseMap reachedFrom; + llvm::SmallVector 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(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 path; + for (auto *at = holder; at; at = reachedFrom.lookup(at)) + { + path.push_back(mlir::cast(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 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::calleesFieldDrops(const Callees &callees) { if (!callees.known || callees.instanceOf) diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.h b/tslang/lib/TypeScript/OwnershipSignaturePass.h index 86d1827b6..7d67ff8d0 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.h +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.h @@ -636,6 +636,9 @@ class OwnershipSignaturePass : public mlir::PassWrapper 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 `..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 diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index c9dd59d1a..364fb8c77 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -1913,10 +1913,14 @@ endforeach() # TSLANG_ISSUES 57: a `let` that only holds parameters owns nothing # TSLANG_ISSUES 58: a `const` declared from a merge of owning locals borrows them; a merge made in a loop's body # TSLANG_ISSUES 59: a value moved into a field of an element or field read, then used, borrows that field +# TSLANG_ISSUES 60: a call that reaches an overridable method names it where a borrow ends +# TSLANG_ISSUES 61: an array global nothing assigns is immortal +# TSLANG_ISSUES 62: an array changed through a local that borrows its field 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_58 00tslang_issue_59 00tslang_issue_60 + 00tslang_issue_61 00tslang_issue_62) 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() @@ -5469,7 +5473,10 @@ set(own_error_cases "own_err_parameter_let_stored|:10:5: error: '[^']*' takes a second reference" "own_err_merged_locals_overwritten|:11:16: error: '[^']*' borrows '[^']*' but is used here after '[^']*' is released or overwritten" "own_err_merged_locals_moved|:12:5: error: '[^']*' is moved here while '[^']*' borrows it" - "own_err_element_field_moved_replaced|:11:12: error: '[^']*' borrows an element but is used here after it may be released or overwritten") + "own_err_element_field_moved_replaced|:11:12: error: '[^']*' borrows an element but is used here after it may be released or overwritten" + "own_err_overridable_reached|:34:13: note: 'Prop.tick' calls 'setNodeTransform', which calls 'World.setTransform'" + "own_err_immortal_array_element_overwritten|:15:12: error: '[^']*' borrows 'G\\[\\]' but is used here after it may be released or overwritten" + "own_err_borrowed_array_pushed_returned|:14:5: error: '[^']*' borrows a field and cannot be stored, returned or captured") 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_array_pushed_returned.ts b/tslang/test/tester/own/own_err_borrowed_array_pushed_returned.ts new file mode 100644 index 000000000..067e2bad1 --- /dev/null +++ b/tslang/test/tester/own/own_err_borrowed_array_pushed_returned.ts @@ -0,0 +1,17 @@ +// TSLANG_ISSUES 62: an array changed through a local that borrows the field holding it is still the +// field's: the local cannot be returned. +class V { + n = 1; +} + +class H { + arr: V[] = []; +} + +export function f(h: H): V[] { + const a = h.arr; + a.push(new V()); + return a; +} + +function main() { print(f(new H()).length); } diff --git a/tslang/test/tester/own/own_err_immortal_array_element_overwritten.ts b/tslang/test/tester/own/own_err_immortal_array_element_overwritten.ts new file mode 100644 index 000000000..ce4ddf5f8 --- /dev/null +++ b/tslang/test/tester/own/own_err_immortal_array_element_overwritten.ts @@ -0,0 +1,18 @@ +// TSLANG_ISSUES 61: an array global nothing assigns is immortal, but only its own block: an element +// overwritten frees the old element, so a borrow of it ends there. +class V { + n: int; + constructor(n: int) { + this.n = n; + } +} + +const G = [new V(1), new V(2)]; + +export function f(): int { + const v = G[0]; + G[0] = new V(5); + return v.n; +} + +function main() { print(f()); } diff --git a/tslang/test/tester/own/own_err_overridable_reached.ts b/tslang/test/tester/own/own_err_overridable_reached.ts new file mode 100644 index 000000000..9c5e9dea3 --- /dev/null +++ b/tslang/test/tester/own/own_err_overridable_reached.ts @@ -0,0 +1,41 @@ +// TSLANG_ISSUES 60: a call of a function defined here that may destroy anything only because a call it +// reaches is of a method another module may override: the error at a borrow it ends names that method +// and the way to it. +export class World { + count = 0; + setTransform(node: int) { + this.count += node; + } +} + +function setNodeTransform(world: World, node: int) { + world.setTransform(node); +} + +class Prop { + world: World; + node: int = 1; + constructor(world: World) { + this.world = world; + } + + tick() { + setNodeTransform(this.world, this.node); + } +} + +export class Game { + props: Prop[] = []; + name = "game"; + + tick(): int { + const name = this.name; + for (let i = 0; i < this.props.length; i++) { + this.props[i].tick(); + } + + return name.length; + } +} + +function main() { print(new Game().tick()); } diff --git a/tslang/test/tester/tests/00tslang_issue_60.ts b/tslang/test/tester/tests/00tslang_issue_60.ts new file mode 100644 index 000000000..e3e44eae5 --- /dev/null +++ b/tslang/test/tester/tests/00tslang_issue_60.ts @@ -0,0 +1,52 @@ +// TSLANG_ISSUES 60 (-mm=own), from native_ts_graphics' core/rtxpt_game.ts: `this.props[i].tick(t)`, where +// `Prop.tick` calls `setNodeTransform`, which calls a method of the exported `GameWorld`. Another module +// may override that method, so the call may destroy anything, and the error said so only for a direct call +// of it (own_err_overridable_reached). `@final` on the class says no module does: the borrows of `this.props[i]`, +// `this.world` and `this.name` then hold over the call. + +@final +export class World { + count = 0; + setTransform(node: int) { + this.count += node; + } +} + +function setNodeTransform(world: World, node: int) { + world.setTransform(node); +} + +class Prop { + node: int; + constructor(node: int) { + this.node = node; + } + + tick(world: World) { + setNodeTransform(world, this.node); + } +} + +export class Game { + world = new World(); + props: Prop[] = []; + name = "game"; + + tick(): int { + const name = this.name; + for (let i = 0; i < this.props.length; i++) { + this.props[i].tick(this.world); + } + + return name.length + this.world.count; + } +} + +function main() { + const game = new Game(); + game.props.push(new Prop(2)); + game.props.push(new Prop(3)); + assert(game.tick() == 9); + assert(game.tick() == 14); + print("done."); +} diff --git a/tslang/test/tester/tests/00tslang_issue_61.ts b/tslang/test/tester/tests/00tslang_issue_61.ts new file mode 100644 index 000000000..454990f37 --- /dev/null +++ b/tslang/test/tester/tests/00tslang_issue_61.ts @@ -0,0 +1,71 @@ +// TSLANG_ISSUES 61 (-mm=own), from native_ts_graphics' examples (nri/ray_tracing_reflection_nri.ts, ptlas.ts, +// nri/async_compute_bloom_nri.ts): module and namespace arrays nothing assigns, read into an array literal +// (`[CUBE_VERTICES, PLANE_VERTICES, CUBE_VERTICES]`), returned (`return GREEN;`) or held across calls +// (`const p = CAMERA_POSITION;`). An array global written once, by its initializer, is immortal, as a +// class's object global is (own spec 25): a read of it owns nothing and may go anywhere. + +class Mat { + r: number; + constructor(r: number) { + this.r = r; + } +} + +const CUBE = [1.0, 2.0, 3.0]; +const PLANE = [4.0, 5.0]; +const MATERIALS = [new Mat(0.5), new Mat(0.25)]; + +namespace Palette { + const GREEN = [0.0, 1.0, 0.0]; + const ORANGE = [1.0, 0.5, 0.0]; + + export function color(hot: boolean): number[] { + if (hot) { + return ORANGE; + } + + return GREEN; + } +} + +function touch(n: int): int { + return n + 1; +} + +function lists(): number { + const meshes = [CUBE, PLANE, CUBE]; + let total = 0.0; + for (const mesh of meshes) { + total += mesh[0]; + } + + return total; +} + +function held(): number { + const p = CUBE; + touch(1); + return p[2] + MATERIALS[1].r; +} + +class Model { + vertices: number[]; + materials: Mat[]; + constructor() { + this.vertices = PLANE; + this.materials = MATERIALS; + } +} + +function main() { + assert(lists() == 6.0); + assert(Palette.color(true)[1] == 0.5 && Palette.color(false)[1] == 1.0); + assert(held() == 3.25); + for (let i = 0; i < 3; i++) { + const m = new Model(); + assert(m.vertices[1] == 5.0 && m.materials[0].r == 0.5); + } + + assert(CUBE.length == 3 && MATERIALS.length == 2); + print("done."); +} diff --git a/tslang/test/tester/tests/00tslang_issue_62.ts b/tslang/test/tester/tests/00tslang_issue_62.ts new file mode 100644 index 000000000..6887e4d3e --- /dev/null +++ b/tslang/test/tester/tests/00tslang_issue_62.ts @@ -0,0 +1,43 @@ +// TSLANG_ISSUES 62 (-mm=own), from native_ts_graphics' examples/ray_tracing_extended.ts: `const model = +// this.refractionModel; ... model.push(0.0);` - an array changed in place through a local that borrows +// the field holding it. The local's slot holds the field's array, so the field sees each change. + +class V { + n: int; + constructor(n: int) { + this.n = n; + } +} + +class Holder { + model: number[] = []; + items: V[] = [new V(1)]; + + fill(): number { + const model = this.model; + if (model.length == 0) { + for (let i = 0; i < 4; i++) { + model.push(1.5); + } + } + + return model.length; + } + + churn(): int { + const items = this.items; + items.push(new V(2)); + items.unshift(new V(0)); + items.splice(1, 1); + const first = items.shift(); + const last = items.pop(); + return (first ? first.n : -1) * 10 + (last ? last.n : -1) + items.length * 100; + } +} + +function main() { + const h = new Holder(); + assert(h.fill() == 4 && h.fill() == 4 && h.model.length == 4); + assert(h.churn() == 2 && h.items.length == 0); + print("done."); +}