diff --git a/TSLANG_ISSUES.md b/TSLANG_ISSUES.md index 953203fd7..a4a46cd80 100644 --- a/TSLANG_ISSUES.md +++ b/TSLANG_ISSUES.md @@ -527,6 +527,111 @@ type is the literal, so it holds that literal and nothing else, in read-only dat literal itself, as `"..."` written in place is. 69 exposed it: `loadTexture` is now found to keep its path. In the sweep, the 2 errors it gave (in `ray_tracing_extended_nri.ts` and `ray_tracing_invocation_reorder_nri.ts`) are gone. +71 (a value narrowed from `T | null`, kept by a constructor, then used: `const texture = +device.createStorageTexture(...); if (!texture) return null; const target = new Target(texture, ...); +target.readView = texture.createView(format);` in `examples/nri/raytracing_ao_nri.ts`, refused "is used here +after its value was moved") is fixed. TSLANG 53 reads the field the constructor keeps a value in for each use +after the call, but only for a value of the field's type, or a class value kept in a union field. A value of +the field's class or null, narrowed by the test before, is now one too: the field read back is widened to +the value's type again. The object popped before the use is still refused. Regression test +`tslang/test/tester/tests/00tslang_issue_71.ts` (every model; it holds 72 too). + +72 (a local, rather than a value, kept by a constructor or pushed, then read: `const t: number[] = []; for (let +i = 0; i < 16; i++) t.push(transform[i]); const node = new SceneNode(..., t, ...); ... this.movingX = t[12];` +in a loop in `examples/nri/extended_dynamic_state2_nri.ts`, and `let pipeline = ...; ...; +this.pipelines.push(pipeline); return pipeline.n;`) is fixed. TSLANG 65's reads of a local after a store (the +local assigned nowhere after it, its address taken nowhere) now serve the constructor and the push too. A +store in a loop is taken when the local is declared in that loop, so each time round starts it afresh, and an +array filled in place through the local is taken when the filling comes before the move. The local pushed to +after the move, declared outside the loop that moves it, or its new owner popped before the read, is still +refused. In the sweep, 71 and 72 take 20 errors, and `extended_dynamic_state2.ts` compiles. Regression test +`own/own_err_kept_local_array_popped.ts`. + +73 (a private field read off a parameter or `this` around a call: `constructor(private sample: NriSample)`, then +`this.sample.run(() => ...)` and a later `this.sample` read in the examples' `App` classes, refused "borrows a +field but is used here after it may be released or overwritten") is fixed. Any call could have overwritten the +field (a callback that writes it, say), so the read before it was not trusted after it. Only the class's own +methods can write a private field, and they are all in the module. So the signature pass now marks each private +field that nothing but its class's constructor writes, and a call no longer drops such a field read off a +parameter (`this` included) the function does not assign. The object is the caller's, so the field lives as long +as the call does. A private field a method writes, called back from the call, is still refused. Regression tests +`tslang/test/tester/tests/00tslang_issue_73.ts` (every model) and `own/own_err_private_field_written_callback.ts`. +`own/own_err_declared_drops.ts` lends a public field now: the private one it had is a field 73 keeps. + +74 (a value of `cond ? null : f(...)`, with `f` another module's, pushed where it is not null, then used: `const +texture = textureName == "" ? null : device.loadTexture(...); if (!texture) ... else { this.materialTextures.push(texture); +this.materialViews.push(texture.createView(texture.getFormat())); }` in `examples/nri/mouse_cursor_nri.ts`, refused +"is used here after its value was moved") is fixed. TSLANG 65's push step reads such a value back from the array +when the value is the function's own. A call's result alone counts, whoever defines the function. A merge of a +call's result and null (TSLANG 54) counted only when the call's result was known to be fresh, which, for another +module's function, the signature pass decides only after this step. A merge now counts when each way into it +brings null or a call's result, as the call alone does. Regression test: TSLANG 74's step in +`tslang/test/tester/own-facts-objects.cmake` (`tests/own_facts_tex_lib.ts`, `tests/own_facts_tex_main.ts`). + +75 (a call through a constant that holds a function: `const f = Math.fround;` in `examples/clustered_geometry.ts`, +then `centroidY = f(centroidY + c.positions[v])` with `c` a cluster of a parameter's mesh, refused "borrows a field +but is used here after it may be released or overwritten" at each later use of `c`) is fixed. The signature pass +took a call through a global's value as one through a callback it knows nothing of, so the call could destroy +anything. A global that no store writes, and that its initializer gives a function's symbol, now resolves to that +function: `f(...)` is a call of `Math.fround`, which destroys nothing. A global something assigns is still +unknown. In the sweep, `clustered_geometry.ts` goes from 44 errors to 16. Regression tests +`tslang/test/tester/tests/00tslang_issue_75.ts` (every model) and `own/own_err_const_alias_drops.ts`. + +76 (a parameter kept by a constructor that also gives `this` to a call: `constructor(private sample: NriSample) { +this.reset(); }`, then `sample.addExampleOption(...)` after `new App(sample)` in `examples/nri/simple_compute_nri.ts`, +refused "is used here after its value was moved") is fixed. TSLANG 53 reads the field a constructor keeps a +parameter in, for each use after the call. It took only a constructor that reads `this` for its fields and +nothing else, so that no call it makes can reach the field. A constructor that gives `this` to a call now keeps a +parameter too, when the field is private and no other function writes it: as in 73, no call can then overwrite +it. A field a method writes is still refused. In the sweep, the 4 examples with this shape lose their errors. +Regression tests `tslang/test/tester/tests/00tslang_issue_76.ts` (every model) and `own/own_err_kept_field_rebound.ts`. + +77 (an array kept by a constructor, then filled through the local: `const sceneSets: NriDescriptorSet[] = []; +this.queuedFrames.push(new QueuedFrame(sceneView, sceneSets)); for (...) sceneSets.push(sceneSet);` in +`examples/nri/msaa_nri.ts`, refused "is used here after its value was moved") is fixed. An array is a reference, +so after the move the local is the object's array, and each push fills it. TSLANG 72 took an array filled in +place before the move. One changed in place after it now changes the field, reached the way the other uses after +the move are: through the object, or the place it moved to last. The object popped before the push is still +refused. Regression tests `tslang/test/tester/tests/00tslang_issue_77.ts` (every model) and +`own/own_err_kept_array_filled_after_pop.ts`. + +78 (a value pushed on a field of a local object, then used: `const queuedFrame = new QueuedFrame(...); for (...) { +const faceAllocator = ...; queuedFrame.faceAllocators.push(faceAllocator); +queuedFrame.faceCommandBuffers.push(faceAllocator.createCommandBuffer()); }` in +`examples/nri/threaded_rendering_nri.ts`, refused "is used here after its value was moved") is fixed. TSLANG 65's +push step and the field store step read the value back through the object right after the move. They took an +object read out of a local or an element, but not a `const` holding the object itself, which is a value and +needs no read. That value now serves, as it is. The element popped before the use is still refused. +Regression tests `tslang/test/tester/tests/00tslang_issue_78.ts` (every model) and +`own/own_err_local_object_field_popped.ts`. + +79 (a local pushed only where it is not null, then used: `let pipeline: NriPipeline | null = null; if (loaded) +pipeline = device.createPipelineFromDesc(...); if (pipeline) { this.pipelineKeys.push(key); +this.pipelines.push(pipeline); } return pipeline;` in `examples/nri/msaa_nri.ts`, refused "is used here after its +value was moved") is fixed. TSLANG 67 did this for a value. For a local, the test and the uses are reads of +their own. Now, when nothing assigns the local from the test on, each read where the paths join takes the merge: +the element read back after the push, or the null the test found. The local is then not read after the join, so +its release goes on the edge that did not push it. The method returns a borrow of `this` on every way, so the +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. + +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 +cannot release it on the others yet") is fixed. Spec 30 placed a local's release on the edges that did not move +it; a value, like the `const` here, had no such rule. Now a value one taker moves gets its release on each edge +into what follows the move, from where the value is held and was not moved. The release after the move goes. A +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`. + 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")` @@ -535,9 +640,24 @@ For native_ts_graphics, from the same sweep (its code, not the compiler): - `examples/dynamic_primitive_clipping.ts:223`, `desc.setDepthClip(clip)` with `clip` the loop's `int`, fails under every model since #846 (a number is not a `boolean` argument, TS2345). alpha127 still takes it; the next release won't. `desc.setDepthClip(clip != 0)` fixes it. -- Under `-mm=own`, 3 `core/` modules do not compile yet (`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-70 in, 116 of the 276 sources fail (635 errors). The - largest group left is "borrows a field but is used here after it may be released or overwritten" (396, - 93 of them `this.sample.run` callbacks), then "used here after its value was moved" (52: mostly a value a pushed object or an - earlier call keeps, then returned or used). +- `core/nri_donut_textures.ts`: `MipBlit` held the texture it blits, and `recordBlits` read it back after the + blits array kept it, so the module and the 9 examples importing it failed under `-mm=own` (199 errors). A + `MipBlit` now holds the texture's index into `this.textures`. `MsvcRandom` (`core/random.ts`) and `GameWorld` + (`core/rtxpt_game.ts`) are now `@final`: a class that can be derived from may have its methods overridden, so + `-mm=own` cannot read their facts (about 48 errors). So are `AtomOrbitals`, `OrbitalsPass`, `Pose` and + `RtxptGame` now; a class that can be derived from turns each call of its methods into one that may + destroy anything. +- The `*_nri.ts` examples' `QueuedFrame` kept `this.frames.commandBuffer(i)`, a borrow of the frame ring's, + which a field cannot hold: it is read from `this.frames` where it is recorded (16 examples). The + `atom_orbitals` examples set two fields to one `Orbital` (`this.orbitalA = this.orbitalB`), a second owner: + each field gets its own. These are on ntg branch `own-sweep-fixes`. +- Left in `examples/clustered_geometry.ts` (ntg's call): `Cluster.clone` and `SceneObject.copy` share the + vertex, index and material arrays on purpose ("which nothing changes"). That gives each array two owners, + so either the copies copy them (`slice()`) or the arrays are `Shared`. The `add` closure keeps its mesh and + position and returns the object it pushed; a function, or the caller pushing, would say that. +- Under `-mm=own`, 2 `core/` modules do not compile yet (`ray_tracing_extended_scene.ts`, `rtxpt_game.ts`), and + nor do many examples, which #310 did not port: with every facts object built, 50-74 in and the changes above, + 48 of the 276 sources fail (194 errors). The largest groups left are "borrows a field but is used here after + it may be released or overwritten" (36), "borrows a field and cannot be stored, returned or captured" (31), + "used here after its value was moved" (27), and an element borrowed and stored (21) or used after it may be + released (20). diff --git a/tslang/include/TypeScript/Defines.h b/tslang/include/TypeScript/Defines.h index 83d9a0dd7..e31e9a952 100644 --- a/tslang/include/TypeScript/Defines.h +++ b/tslang/include/TypeScript/Defines.h @@ -160,6 +160,8 @@ #define OWN_BORROWING_FIELDS_ATTR_NAME "ts.own_borrowing_fields" // [class storage type, field position] of each field a back-reference is stored into (own spec 70) #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" // -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/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h index 4f41d699f..b32f3fc2e 100644 --- a/tslang/lib/TypeScript/OwnershipFacts.h +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -591,13 +591,15 @@ inline bool takenOutOfBorrowedRest(mlir::Operation *def) } inline bool isFresh(mlir::Value value); +inline bool isCall(mlir::Operation *op); // A value merged from branches (TSLANG 54): `const p = loaded ? device.create() : null`. It is the // function's own when every way into the merge brings a fresh value that nothing else uses, or null // or undefined, which holds no block - rc gave it the one reference the merge holds. Not across a // loop: a block that reaches one of its own predecessors carries a value from one iteration into the -// next, which is not decided here. -inline bool isFreshMerge(mlir::BlockArgument argument, unsigned depth = 0) +// next, which is not decided here. `anyCall`: a call's result counts as fresh too - for a step that runs +// before the calls' facts are known, as it takes an unmerged call's result (TSLANG 74). +inline bool isFreshMerge(mlir::BlockArgument argument, unsigned depth = 0, bool anyCall = false) { auto *block = argument.getOwner(); if (block->isEntryBlock() || block->hasNoPredecessors() || depth > 8) @@ -648,7 +650,7 @@ inline bool isFreshMerge(mlir::BlockArgument argument, unsigned depth = 0) if (auto nested = mlir::dyn_cast(incoming)) { - if (!nested.hasOneUse() || !isFreshMerge(nested, depth + 1)) + if (!nested.hasOneUse() || !isFreshMerge(nested, depth + 1, anyCall)) { return false; } @@ -693,7 +695,7 @@ inline bool isFreshMerge(mlir::BlockArgument argument, unsigned depth = 0) continue; } - if (!isFresh(value)) + if (!isFresh(value) && !(anyCall && def && isCall(def))) { return false; } @@ -2327,6 +2329,25 @@ inline mlir_ts::ClassStorageType fieldOwnerStorage(mlir_ts::PropertyRefOp proper return mlir::dyn_cast(type); } +// 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) +{ + auto propertyRefOp = ref.getDefiningOp(); + auto storage = propertyRefOp ? fieldOwnerStorage(propertyRefOp) : mlir_ts::ClassStorageType(); + auto module = propertyRefOp ? propertyRefOp->getParentOfType() : mlir::ModuleOp(); + auto entries = module ? module->getAttrOfType(OWN_STABLE_FIELDS_ATTR_NAME) : mlir::ArrayAttr(); + if (!storage || !entries) + { + return false; + } + + return llvm::any_of(entries.getAsRange(), [&](mlir::ArrayAttr entry) { + return mlir::cast(entry[0]).getValue() == storage && + mlir::cast(entry[1]).getInt() == propertyRefOp.getPosition(); + }); +} + // Is `ref` a field some back-reference is stored into (OWN_BACK_REFERENCE_FIELDS_ATTR_NAME, the // signature pass's)? What it holds may borrow the object it is in, so it is never taken out of it inline bool isBackReferenceField(mlir::Value ref) diff --git a/tslang/lib/TypeScript/OwnershipInferenceEdgeReleases.cpp b/tslang/lib/TypeScript/OwnershipInferenceEdgeReleases.cpp index 835d739be..07937e0a2 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceEdgeReleases.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceEdgeReleases.cpp @@ -483,15 +483,121 @@ namespace own_inference return true; } + bool OwnershipInferencePass::valueUnmovedEdges(mlir::Value value, mlir::Operation *definition, mlir::Operation *acquire, + mlir::Operation *taker, mlir::Operation *release) + { + auto &body = getFunction().getFunctionBody(); + if (!definition || definition->getParentRegion() != &body || acquire->getParentRegion() != &body || + release->getParentRegion() != &body || !dominance->properlyDominates(definition, release)) + { + return false; + } + + // what follows the move, while this value is the one the definition made: the blocks the move + // reaches, not through the definition again (a loop's next turn makes another) + auto *moveBlock = acquire->getBlock(); + llvm::SmallPtrSet after; + llvm::SmallVector work(moveBlock->getSuccessors().begin(), moveBlock->getSuccessors().end()); + while (!work.empty()) + { + auto *block = work.pop_back_val(); + if (block == definition->getBlock() || !after.insert(block).second) + { + continue; + } + + work.append(block->getSuccessors().begin(), block->getSuccessors().end()); + } + + if (!after.contains(release->getBlock()) || after.contains(moveBlock)) + { + return false; + } + + // nothing reads the value where the ways join: released on an edge into there, it is gone + // (a copyable value's read after the move is not one the moves refuse) + auto usedAfter = false; + forEachUse(value, [&](mlir::Operation *user, mlir::Value) { + auto *top = body.findAncestorOpInRegion(*user); + if (!top || user == release || user == taker || top == body.findAncestorOpInRegion(*taker) || + mlir::isa(user)) + { + return; + } + + usedAfter = usedAfter || after.contains(top->getBlock()) || + (top->getBlock() == moveBlock && acquire->isBeforeInBlock(top) && top != acquire); + }); + if (usedAfter) + { + return false; + } + + // the edges into it from where the value is held and not moved + auto handler = handlerBlocks(); + llvm::SmallVector placed; + for (auto &block : body) + { + if (after.contains(&block) || &block == moveBlock || !dominance->dominates(definition, block.getTerminator())) + { + continue; + } + + for (auto index : llvm::seq(0, block.getNumSuccessors())) + { + if (!after.contains(block.getSuccessor(index))) + { + continue; + } + + auto *terminator = block.getTerminator(); + if (!mlir::isa(terminator) || handler.contains(&block)) + { + return false; + } + + // another release of the value placed it already + if (llvm::any_of(edgeReleases, [&](EdgeRelease &edge) { + return edge.slot == value && edge.from == &block && edge.successor == index; + })) + { + continue; + } + + placed.push_back({value, &block, static_cast(index), terminator->getLoc()}); + } + } + + if (placed.empty()) + { + return false; + } + + edgeReleases.append(placed.begin(), placed.end()); + return true; + } + void OwnershipInferencePass::placeEdgeReleases() { + // a slot's release, or a value's (TSLANG 81) + auto releaseOf = [](mlir::OpBuilder &builder, EdgeRelease &edge) { + if (edge.slot.getDefiningOp()) + { + builder.create(edge.loc, edge.slot); + } + else + { + builder.create(edge.loc, edge.slot); + } + }; + for (auto &edge : edgeReleases) { auto *terminator = edge.from->getTerminator(); mlir::OpBuilder builder(terminator); if (terminator->getNumSuccessors() == 1) { - builder.create(edge.loc, edge.slot); + releaseOf(builder, edge); continue; } @@ -501,7 +607,7 @@ namespace own_inference auto *split = new mlir::Block(); split->insertAfter(edge.from); builder.setInsertionPointToEnd(split); - builder.create(edge.loc, edge.slot); + releaseOf(builder, edge); builder.create(edge.loc, join, forwarded); if (!forwarded.empty()) { diff --git a/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp b/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp index 8df5cb296..9e490262a 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp @@ -1039,9 +1039,12 @@ namespace own_inference // moved on some ways only: on the others it must be empty (TSLANG 49) - or moved by // another taker: `if (c) this.a.push(node); else this.b.push(node);` reaches the // release only past one move or the other (TSLANG 69) + // ...or, moved by this one taker, released on the edges that did not move it (TSLANG 81): + // `const c = new C(); if (c.read()) this.items.push(c);` if (!dominance->properlyDominates(acquire, release) && !emptyWhereNotMoved(value, definition, acquire, release) && - !(definition && takers.size() > 1 && !reachableAfter(definition, release, takers))) + !(definition && takers.size() > 1 && !reachableAfter(definition, release, takers)) && + !(takers.size() == 1 && valueUnmovedEdges(value, definition, acquire, taker, release))) { reportMovedOnSomePaths(taker, release, name); return false; diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.h b/tslang/lib/TypeScript/OwnershipInferencePass.h index 1292f29f8..fa52f4b02 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.h +++ b/tslang/lib/TypeScript/OwnershipInferencePass.h @@ -1239,6 +1239,10 @@ class OwnershipInferencePass llvm::DenseMap before; }; + // A value moved on some paths only (TSLANG 81): its release, which the move reaches, goes on the + // edges into what follows the move from the paths that did not move it. + bool valueUnmovedEdges(mlir::Value value, mlir::Operation *definition, mlir::Operation *acquire, mlir::Operation *taker, + mlir::Operation *release); bool placeOnUnmovedEdges(mlir_ts::VariableOp varOp, mlir::Operation *acquire, mlir::Operation *retain, mlir::Operation *taker, llvm::ArrayRef assignments, llvm::SetVector &toErase); diff --git a/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp b/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp index cb9f1c104..1c92464e9 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp @@ -691,6 +691,23 @@ namespace own_inference return false; } + // a private field nothing writes after construction, read straight off a parameter or `this` + // (TSLANG 73): no call overwrites it, and it is released only with its object, which the + // caller holds while this function runs + if (read && chain.places.size() == 1 && !chain.anyPlace) + { + auto loadOp = mlir::dyn_cast(read); + auto propertyRefOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); + auto objectLoad = propertyRefOp ? propertyRefOp.getObjectRef().getDefiningOp() : mlir_ts::LoadOp(); + auto objectSlot = objectLoad ? objectLoad.getReference().getDefiningOp() : mlir_ts::VariableOp(); + int index = -1; + if (propertyRefOp && isStableField(propertyRefOp.getResult()) && objectSlot && isParameterSlot(objectSlot, index) && + isUnassignedSlot(objectSlot.getResult())) + { + return false; + } + } + // a call given a closure may run it, and its box may hold anything auto givenClosure = llvm::any_of(drop->getOperands(), holdsCaptures); auto mayDrop = true; diff --git a/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp b/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp index eec7192e9..f28aadfaf 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp @@ -129,7 +129,16 @@ namespace own_signature return true; } - auto loadOp = field.getObjectRef().getDefiningOp(); + // ...or the object a value, not a read (`const frame = new Frame(); frame.allocators.push(a)`, + // TSLANG 78): the read right after uses that very value + // - an object of a class `new` made, not a closure's record of its captures + auto object = field.getObjectRef(); + if (object.getDefiningOp() && mlir::isa(object.getType())) + { + return true; + } + + auto loadOp = object.getDefiningOp(); return loadOp && (loadOp.getReference().getDefiningOp() || loadOp.getReference().getDefiningOp()); } @@ -145,32 +154,63 @@ namespace own_signature // `if (value) array.push(value);` and uses of `value` after it (TSLANG 67): the block the test's // false edge goes to, reached otherwise only through the push, gets an argument - the element // read back after the push (`read`) on those paths, null on the false edge - and the uses of - // `value` that block dominates read it. - void mergePushedOrNull(mlir::Operation *pushOp, mlir::Value value, mlir::Value read, mlir::DominanceInfo &dominance) + // `value` that block dominates read it. `slot`: the value is a read of this local, which nothing + // assigns after the push (usesAfterMove), and the test and the uses are reads of it of their own + // (TSLANG 79: `let pipeline = ...; if (pipeline) this.pipelines.push(pipeline); return pipeline;`). + void mergePushedOrNull(mlir::Operation *pushOp, mlir::Value value, mlir::Value read, mlir::DominanceInfo &dominance, + mlir_ts::VariableOp slot = {}) { auto *pushBlock = pushOp->getBlock(); - mlir::cf::CondBranchOp test; - for (auto *user : value.getUsers()) + // the value, or each read of the local + llvm::SmallVector values{value}; + if (slot) { - auto cast = mlir::dyn_cast(user); - if (!cast) + values.clear(); + for (auto *user : slot.getResult().getUsers()) { - continue; + if (auto loadOp = mlir::dyn_cast(user)) + { + values.push_back(loadOp.getResult()); + } } + } - for (auto *castUser : cast->getUsers()) + mlir::cf::CondBranchOp test; + for (auto tested : values) + { + for (auto *user : tested.getUsers()) { - for (auto *dialectUser : castUser->getUsers()) + auto cast = mlir::dyn_cast(user); + if (!cast) + { + continue; + } + + for (auto *castUser : cast->getUsers()) { - auto branch = mlir::dyn_cast(dialectUser); - if (branch && testsTruthOf(branch, value) && dominance.dominates(branch.getTrueDest(), pushBlock)) + for (auto *dialectUser : castUser->getUsers()) { - test = branch; + auto branch = mlir::dyn_cast(dialectUser); + if (branch && testsTruthOf(branch, tested) && dominance.dominates(branch.getTrueDest(), pushBlock)) + { + test = branch; + } } } } } + // ...and nothing assigns the local from the test on: the false edge brings the null it held + if (test && slot && + llvm::any_of(slot.getResult().getUsers(), [&](mlir::Operation *user) { + auto storeOp = mlir::dyn_cast(user); + return storeOp && storeOp.getReference() == slot.getResult() && + dominance.dominates(test->getBlock(), storeOp->getBlock()); + })) + { + return; + } + if (!test || test.getFalseDest() == test.getTrueDest()) { return; @@ -192,13 +232,16 @@ namespace own_signature } llvm::SmallVector uses; - for (auto &use : value.getUses()) + for (auto each : values) { - auto *user = use.getOwner(); - if (!mlir::isa(user) && user->getBlock() != test->getBlock() && - dominance.dominates(merge, user->getBlock())) + for (auto &use : each.getUses()) { - uses.push_back(&use); + auto *user = use.getOwner(); + if (!mlir::isa(user) && user->getBlock() != test->getBlock() && + dominance.dominates(merge, user->getBlock())) + { + uses.push_back(&use); + } } } @@ -225,6 +268,16 @@ namespace own_signature { use->set(argument); } + + // the reads of the local the merge now stands for, left unused: the local is not read after + // the paths join, so its release goes on the edge that did not push it (TSLANG 79) + for (auto each : values) + { + if (slot && each.use_empty()) + { + each.getDefiningOp()->erase(); + } + } } // `value` seen through the views that keep its block: an optional made of it or opened, a @@ -297,12 +350,14 @@ namespace own_signature } // A value the steps below may follow to where it moves: made here (not a read of a local or a - // place), or merged from branches that each bring a fresh value or null (TSLANG 54). + // place), or merged from branches that each bring a fresh value or null (TSLANG 54) - a call's + // result among them, as unmerged: `cond ? null : device.loadTexture(...)` of another module, whose + // facts are not read yet (TSLANG 74). bool ownValue(mlir::Value root, mlir::Operation *def) { if (auto argument = mlir::dyn_cast(root)) { - return own_facts::isFreshMerge(argument); + return own_facts::isFreshMerge(argument, 0, true); } return def && !mlir::isa(def); @@ -346,7 +401,10 @@ namespace own_signature return kept; } - // `this` only read, and only for its fields, which are only read or written + // `this` only read, and only for its fields, which are only read or written. Or `this` given + // to a call (`this.reset()`), which may reach any field it can write: then only a private + // field nothing but this constructor writes (TSLANG 76), which no call can + auto escapes = false; llvm::DenseMap stores; llvm::SmallVector candidates; for (auto *user : thisSlot.getUsers()) @@ -362,7 +420,8 @@ namespace own_signature auto propertyRefOp = mlir::dyn_cast(loadUser); if (!propertyRefOp) { - return kept; + escapes = true; + continue; } for (auto *refUser : propertyRefOp->getUsers()) @@ -391,11 +450,37 @@ namespace own_signature } } + // a private field of the constructor's class that no other function writes + auto writtenOnlyHere = [&](mlir_ts::PropertyRefOp field) { + auto storage = fieldOwnerStorage(field); + auto fields = storage ? storage.getFields() : decltype(storage.getFields())(); + auto position = field.getPosition(); + if (!storage || position < 0 || static_cast(position) >= fields.size() || + fields[position].accessLevel != mlir_ts::AccessLevel::Private) + { + return false; + } + + auto module = ctor->getParentOfType(); + auto written = false; + module.walk([&](mlir_ts::PropertyRefOp propertyRefOp) { + if (written || propertyRefOp.getPosition() != position || fieldOwnerStorage(propertyRefOp) != storage || + propertyRefOp->getParentOfType() == ctor) + { + return; + } + + written = !llvm::all_of(propertyRefOp->getUsers(), [](mlir::Operation *user) { return mlir::isa(user); }); + }); + return !written; + }; + for (auto storeOp : candidates) { auto propertyRefOp = storeOp.getReference().getDefiningOp(); auto loadOp = stripViews(storeOp.getValue()).getDefiningOp(); - if (storeOp->getBlock() != &entry || stores[propertyRefOp.getPosition()] != 1 || !loadOp) + if (storeOp->getBlock() != &entry || stores[propertyRefOp.getPosition()] != 1 || !loadOp || + (escapes && !writtenOnlyHere(propertyRefOp))) { continue; } @@ -720,7 +805,75 @@ namespace own_signature // comes before the store (TSLANG 65: `let pipeline = null; if (ok) pipeline = make(); this. // pipelines[i] = pipeline; return pipeline;`), every later read of the local the store // dominates. False when the value is neither. - auto usesAfterMove = [&](mlir::Operation *store, mlir::Value root, llvm::SmallVectorImpl &uses) { + // Can control go from `from` to `to`, `to` after `from` in its block or in a block after - + // not through `avoided` (a local's declaration, which starts it afresh)? + auto reaches = [&](mlir::Operation *from, mlir::Operation *to, mlir::Block *avoided = nullptr) { + if (from->getBlock() == to->getBlock() && from->isBeforeInBlock(to)) + { + return true; + } + + llvm::SmallPtrSet seen; + llvm::SmallVector work(from->getBlock()->getSuccessors().begin(), from->getBlock()->getSuccessors().end()); + while (!work.empty()) + { + auto *next = work.pop_back_val(); + if (next == to->getBlock()) + { + return true; + } + + if (next == avoided) + { + continue; + } + + if (seen.insert(next).second) + { + work.append(next->getSuccessors().begin(), next->getSuccessors().end()); + } + } + + return false; + }; + + // Does control come back to `op` on a way that does not declare `slot` again first? + auto runsAgainUndeclared = [&](mlir::Operation *op, mlir_ts::VariableOp slot) { + if (op->getParentRegion() != slot->getParentRegion() || !dominance.properlyDominates(slot.getOperation(), op)) + { + return true; + } + + // the declaration's block, between it and `op` (in the same block: before it) + auto *declared = slot->getBlock(); + llvm::SmallPtrSet seen; + llvm::SmallVector work(op->getBlock()->getSuccessors().begin(), op->getBlock()->getSuccessors().end()); + while (!work.empty()) + { + auto *next = work.pop_back_val(); + if (next == declared) + { + continue; + } + + if (next == op->getBlock()) + { + return true; + } + + if (seen.insert(next).second) + { + work.append(next->getSuccessors().begin(), next->getSuccessors().end()); + } + } + + return false; + }; + + // `inPlaceAfter`: an array changed in place through the local after the move is a use too, of + // the local's slot (TSLANG 77) - for a step that gives it the place the array moved to + auto usesAfterMove = [&](mlir::Operation *store, mlir::Value root, llvm::SmallVectorImpl &uses, + bool inPlaceAfter = false) { auto loadOp = root.getDefiningOp(); auto slot = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); if (!slot) @@ -743,9 +896,10 @@ namespace own_signature } // run once (not in a loop), so an assignment the store does not dominate runs before it - // or on a way that does not reach the reads after it + // or on a way that does not reach the reads after it - or once for each time the local + // is declared: `for (...) { const t = []; ...; new Node(t); t[0] }` (TSLANG 72) if (!own_facts::isOwningVariable(slot) || slot.getCaptured().value_or(false) || slot->getParentRegion() != body || - mayRunAgain(store)) + (mayRunAgain(store) && runsAgainUndeclared(store, slot))) { return false; } @@ -758,6 +912,32 @@ namespace own_signature return false; } + // an array filled in place through the local before the move: `t.push(x)` (TSLANG 72) + if (mlir::isa(user) && + !reaches(store, user, slot->getBlock())) + { + continue; + } + + // ...and after it, through the place it moved to: `const sets = []; this.frames.push(new + // Frame(sets)); for (...) sets.push(set)` (TSLANG 77) + if (inPlaceAfter && + mlir::isa(user) && + dominance.properlyDominates(store, user)) + { + for (auto &operand : user->getOpOperands()) + { + if (operand.get() == slot.getResult()) + { + uses.push_back(&operand); + } + } + + continue; + } + if (!mlir::isa(user)) { @@ -894,7 +1074,10 @@ namespace own_signature auto arrayType = mlir::dyn_cast(mlir::cast(field.getType()).getElementType()); auto root = stripViews(pushOp.getItems().front()); auto *def = root.getDefiningOp(); - if (!arrayType || !ownValue(root, def) || !own_facts::ownsHeap(root) || + // ...or a read of an owning local (TSLANG 72), its later reads the uses (usesAfterMove) + auto rootLoad = def ? mlir::dyn_cast(def) : mlir_ts::LoadOp(); + auto slotRead = rootLoad && rootLoad.getReference().getDefiningOp(); + if (!arrayType || (!ownValue(root, def) && !slotRead) || !own_facts::ownsHeap(root) || own_facts::isCopyableValueAt(pushOp, root.getType()) || MLIRTypeHelper::isSharedHandleType(root.getType())) { continue; @@ -909,12 +1092,23 @@ namespace own_signature } llvm::SmallVector uses; - for (auto &use : root.getUses()) + if (slotRead) { - auto *user = use.getOwner(); - if (!mlir::isa(user) && dominance.properlyDominates(pushOp.getOperation(), user)) + // no use the push dominates: there may be some where the paths meet (below) + if (!usesAfterMove(pushOp, root, uses)) { - uses.push_back(&use); + continue; + } + } + else + { + for (auto &use : root.getUses()) + { + auto *user = use.getOwner(); + if (!mlir::isa(user) && dominance.properlyDominates(pushOp.getOperation(), user)) + { + uses.push_back(&use); + } } } @@ -945,7 +1139,8 @@ namespace own_signature // ...and pushed only where it is not null (TSLANG 67): `const view = make(); if (view) // this.views.push(view); return view;`. Where the paths meet again it is the element // read back, or the null it was: a merge of the two, which each use there reads. - mergePushedOrNull(pushOp, root, read, dominance); + mergePushedOrNull(pushOp, root, read, dominance, + slotRead ? rootLoad.getReference().getDefiningOp() : mlir_ts::VariableOp()); } followFieldStores(); @@ -1010,9 +1205,18 @@ namespace own_signature auto *def = root.getDefiningOp(); auto fieldRefType = mlir::cast(kept.second); auto fieldType = fieldRefType.getElementType(); - auto narrowed = fieldType != root.getType() && mlir::isa(root.getType()) && - mlir::isa(fieldType); - if (!ownValue(root, def) || !own_facts::ownsHeap(root) || + // ...or a value narrowed to the field's class from that class or null: `const texture = + // load(); if (!texture) return; new Target(texture)` (TSLANG 71). Read back, the field + // is widened to the value's type again. + auto narrowed = fieldType != root.getType() && + ((mlir::isa(root.getType()) && + mlir::isa(fieldType)) || + (mlir::isa(fieldType) && + own_facts::holdsAsIs(root.getType(), fieldType, true))); + // ...or a read of an owning local (TSLANG 72), its later reads the uses (usesAfterMove) + auto rootLoad = def ? mlir::dyn_cast(def) : mlir_ts::LoadOp(); + auto slotRead = rootLoad && rootLoad.getReference().getDefiningOp(); + if ((!ownValue(root, def) && !slotRead) || !own_facts::ownsHeap(root) || own_facts::isCopyableValueAt(callOp, root.getType()) || MLIRTypeHelper::isSharedHandleType(root.getType()) || (fieldType != root.getType() && !narrowed)) { @@ -1040,15 +1244,44 @@ namespace own_signature }); }; - for (auto &use : root.getUses()) + if (slotRead) { - if (!mlir::isa(use.getOwner()) && dominance.properlyDominates(callOp, use.getOwner()) && - !afterRelease(use.getOwner())) + llvm::SmallVector later; + if (!usesAfterMove(callOp, root, later, true)) { - uses.push_back(&use); + continue; + } + + for (auto *use : later) + { + if (!afterRelease(use->getOwner())) + { + uses.push_back(use); + } + } + } + else + { + for (auto &use : root.getUses()) + { + if (!mlir::isa(use.getOwner()) && dominance.properlyDominates(callOp, use.getOwner()) && + !afterRelease(use.getOwner())) + { + uses.push_back(&use); + } } } + // an array changed in place after the move changes the field itself: its reference, of the + // local's type (TSLANG 77) + if (llvm::any_of(uses, [&](mlir::OpOperand *use) { + return mlir::isa(use->get().getType()) && (narrowed || use->get().getType() != fieldRefType); + })) + { + continue; + } + + llvm::DenseMap refs; for (auto *use : uses) { auto *user = use->getOwner(); @@ -1070,6 +1303,7 @@ namespace own_signature builder.setInsertionPointAfter(after); auto ref = builder.create(callOp->getLoc(), fieldRefType, place, builder.getI32IntegerAttr(kept.first)); + refs[place] = ref; fieldRead = builder.create(callOp->getLoc(), fieldType, ref); if (narrowed) { @@ -1077,7 +1311,7 @@ namespace own_signature } } - use->set(fieldRead); + use->set(mlir::isa(use->get().getType()) ? refs[place] : fieldRead); } } } diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp index 393da39c8..a71110e7a 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp @@ -144,6 +144,45 @@ namespace own_signature mlir::ArrayAttr::get(&getContext(), backReferenceFields.getArrayRef())); } + // the private fields nothing writes but their class's constructor (TSLANG 73): `constructor(private + // sample: NriSample)`. Only the class's own methods may write a private field, and they are all here + // (its constructor is), so once the object is made no call - a callback included - overwrites it: + // the field is released only with its object. + llvm::DenseMap, bool> writtenAfterConstruction; + module.walk([&](mlir_ts::PropertyRefOp propertyRefOp) { + auto storage = fieldOwnerStorage(propertyRefOp); + if (!storage) + { + return; + } + + auto &written = writtenAfterConstruction[{storage, propertyRefOp.getPosition()}]; + auto funcOp = propertyRefOp->getParentOfType(); + auto inConstructor = funcOp && funcOp.getSymName() == (storage.getName().getAttr().getValue() + ".constructor").str(); + auto onlyRead = llvm::all_of(propertyRefOp->getUsers(), [](mlir::Operation *user) { return mlir::isa(user); }); + written = written || (!onlyRead && !inConstructor); + }); + llvm::SetVector stableFields; + for (auto &[key, written] : writtenAfterConstruction) + { + auto storage = mlir::cast(key.first); + auto constructor = functions.lookup((storage.getName().getAttr().getValue() + ".constructor").str()); + auto fields = storage.getFields(); + if (written || !constructor || constructor.isExternal() || key.second < 0 || + static_cast(key.second) >= fields.size() || fields[key.second].accessLevel != mlir_ts::AccessLevel::Private) + { + continue; + } + + stableFields.insert(mlir::ArrayAttr::get( + &getContext(), {mlir::TypeAttr::get(storage), mlir::IntegerAttr::get(mlir::IntegerType::get(&getContext(), 32), key.second)})); + } + + if (!stableFields.empty()) + { + module->setAttr(OWN_STABLE_FIELDS_ATTR_NAME, mlir::ArrayAttr::get(&getContext(), stableFields.getArrayRef())); + } + if (auto functionsDropping = module->getAttrOfType(SHARED_LIB_OWN_DROP_FIELDS_ATTR_NAME)) { for (auto entry : functionsDropping) diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.h b/tslang/lib/TypeScript/OwnershipSignaturePass.h index 958d93547..929504fc5 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.h +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.h @@ -715,6 +715,8 @@ class OwnershipSignaturePass : public mlir::PassWrapper(def) || (def && isCall(def))) { // a callback parameter, field or local; or what a call returned (spec 56) @@ -467,6 +473,35 @@ namespace own_signature resolveFamily(index, identifier, callees); } + mlir::StringAttr OwnershipSignaturePass::globalFunction(mlir::Operation *def) + { + // `ts.Load(ts.AddressOf @g)` of a global no store writes, whose initializer gives it a + // function's symbol, seen through its casts + auto loadOp = mlir::dyn_cast_or_null(def); + auto addressOfOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp(); + if (!addressOfOp || writtenGlobals.contains(addressOfOp.getGlobalName())) + { + return {}; + } + + auto globalOp = globals.lookup(addressOfOp.getGlobalName()); + auto *block = globalOp ? globalOp.getInitializerBlock() : nullptr; + auto resultOp = block && !block->empty() ? mlir::dyn_cast(block->getTerminator()) : mlir_ts::GlobalResultOp(); + if (!resultOp || resultOp->getNumOperands() != 1) + { + return {}; + } + + auto value = resultOp->getOperand(0); + while (auto castOp = value.getDefiningOp()) + { + value = castOp.getIn(); + } + + auto symbolRefOp = value.getDefiningOp(); + return symbolRefOp ? symbolRefOp.getIdentifierAttr().getAttr() : mlir::StringAttr(); + } + void OwnershipSignaturePass::resolveFamily(int64_t index, mlir::StringAttr identifier, Callees &callees) { callees.known = true; diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 136340ef2..39a739234 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -1922,11 +1922,21 @@ endforeach() # TSLANG_ISSUES 66: a `let` borrowing a field, given fresh values on some paths, borrows them from a slot of their own # TSLANG_ISSUES 67-68: a value pushed only where it is not null; a union of string literals read as a string # TSLANG_ISSUES 69-70: a value moved on every path, by a different move on each; a constant literal global given to a keeping call +# TSLANG_ISSUES 71-72: a narrowed value kept by a constructor; a local kept by a constructor or pushed, then read +# TSLANG_ISSUES 73: a private field nothing writes after the constructor, borrowed across a call that runs callbacks +# TSLANG_ISSUES 75: a call through a constant global that holds a function, a call of that function +# TSLANG_ISSUES 76: a constructor that keeps a parameter in a private field only it writes, and gives `this` to a call +# 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 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_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) 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() @@ -5157,7 +5167,6 @@ set_tests_properties(test-own-err-names-the-variable set(own_error_cases "own_err_assign_after_move|:12:16: error: '[^']*' borrows '[^']*' but is used here after '[^']*' is released or overwritten" "own_err_borrow_escape|:9:5: error: '[^']*' borrows '[^']*' and cannot be stored, returned or captured" - "own_err_move_some_paths_field|:8:9: error: '[^']*' is moved here on some paths only" "own_err_borrow_and_move|:8:5: error: '[^']*' is moved here while '[^']*' borrows it" "own_err_borrow_bound_method|:20:17: error: '[^']*' borrows '[^']*' but is used here after '[^']*' is released or overwritten" "own_err_const_bound_method_reassigned|:20:11: error: '[^']*' takes a second reference" @@ -5166,7 +5175,7 @@ set(own_error_cases "own_err_borrow_derived_ref|:28:5: error: '[^']*' borrows '[^']*' but is used here after '[^']*' is released or overwritten" "own_err_const_alias_move|:12:5: error: '[^']*' borrows '[^']*' and cannot be assigned" "own_err_const_alias_field|:15:5: error: '[^']*' is moved here while '[^']*' borrows it" - "own_err_field_alias|:9:11: error: '[^']*' is used here after its value was moved" + "own_err_field_alias|:9:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_call_in_try_drops|:30:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_closure_escape_used|:9:11: error: '[^']*' is captured by a closure that escapes, and is still used here" "own_err_closure_escape_twice|:12:25: error: 'n' is captured by a closure that escapes, and is still used here" @@ -5190,12 +5199,11 @@ set(own_error_cases "own_err_generator_yields_borrow|borrows the generator's argument and cannot be stored, returned or captured" "own_err_generator_map_outlives|:7:11: error: '[^']*' borrows 'the generator's source' but is used here after 'the generator's source' is released or overwritten" "own_err_generator_place_overwritten|:15:16: error: '[^']*' borrows a field but is used here after it may be released or overwritten" - "own_err_two_owners|:8:13: error: '[^']*' is used here after its value was moved" "own_err_reassign_some_paths|:10:5: error: '[^']*' is moved here on some paths only" "own_err_push_let|:7:9: error: '[^']*' is used here after its value was moved" "own_err_push_loop|:6:33: error: '[^']*' is moved inside a loop but was made outside it" "own_err_field_loop|:10:9: error: '[^']*' is moved inside a loop but was made outside it" - "own_err_union_moved|:10:11: error: '[^']*' is used here after its value was moved" + "own_err_union_moved|:10:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_field_read_overwritten|:28:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_element_read_overwritten|:22:11: error: '[^']*' borrows an element but is used here after it may be released or overwritten" "own_err_element_read_popped|:26:11: error: '[^']*' borrows an element but is used here after it may be released or overwritten" @@ -5387,9 +5395,9 @@ set(own_error_cases "own_err_iface_field_view_overwritten|:26:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_iface_field_call|:27:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_iface_field_result|borrows the result of 'dirOf' but is used here after it may be released or overwritten" - "own_err_read_after_move_overwritten|:10:11: error: '[^']*' is used here after its value was moved" - "own_err_read_after_move_view|:12:11: error: '[^']*' is used here after its value was moved" - "own_err_read_after_move_call|:11:11: error: '[^']*' is used here after its value was moved" + "own_err_read_after_move_overwritten|:10:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_read_after_move_view|:12:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_read_after_move_call|:11:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_read_after_move_loop|:25:14: error: '[^']*' is used here after its value was moved" "own_err_read_after_move_after_loop|:14:11: error: '[^']*' is used here after its value was moved" "own_err_read_after_move_global|:42:15: error: '[^']*' borrows a field but is used here after it may be released or overwritten" @@ -5480,7 +5488,6 @@ set(own_error_cases "own_err_fill_in_loop|:23:11: error: '[^']*' borrows the result of 'Dev.getQ' but is used here after it may be released or overwritten" "own_err_fill_other_field|:20:11: error: '[^']*' borrows the result of 'Dev.getQ' but is used here after it may be released or overwritten" "own_err_fill_after_fill_call|:19:20: error: '[^']*' borrows the result of 'Dev.fill' but is used here after it may be released or overwritten" - "own_err_moved_when_tested_other|:13:13: error: '[^']*' is moved here on some paths only" "own_err_field_moved_call_between|:12:9: error: '[^']*' borrows a field and cannot be stored, returned or captured" "own_err_override_lends|:13:5: error: method 'read' returns a borrow .* but the method of 'Base' it overrides does not" "own_err_moved_field_overwritten|:15:25: error: '[^']*' borrows a field but is used here after it may be released or overwritten" @@ -5502,7 +5509,15 @@ set(own_error_cases "own_err_split_let_stored|:14:9: error: '[^']*' borrows a field and cannot be stored, returned or captured" "own_err_split_let_field_overwritten|:14:16: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_pushed_or_null_popped|:17:23: error: '[^']*' borrows an element but is used here after it may be released or overwritten" - "own_err_moved_on_two_of_three_paths|:16:13: error: '[^']*' is moved here on some paths only") + "own_err_moved_on_two_of_three_paths|:16:13: error: '[^']*' is moved here on some paths only" + "own_err_kept_local_array_popped|:16:22: error: '[^']*' borrows an element but is used here after it may be released or overwritten" + "own_err_private_field_written_callback|:33:24: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_const_alias_drops|:22:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_kept_field_rebound|:29:5: error: '[^']*' is used here after its value was moved" + "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") 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-facts-objects.cmake b/tslang/test/tester/own-facts-objects.cmake index aae48f9c8..767ad0829 100644 --- a/tslang/test/tester/own-facts-objects.cmake +++ b/tslang/test/tester/own-facts-objects.cmake @@ -19,7 +19,7 @@ set(ENV{TSLANG_LIB_PATH} "") file(REMOVE_RECURSE "${WORK_DIR}") file(MAKE_DIRECTORY "${WORK_DIR}") # the importer finds `./own_facts_lib` beside it, as a source to read declarations from -foreach(file own_facts_lib.ts own_facts_main.ts own_facts_main_drops.ts) +foreach(file own_facts_lib.ts own_facts_main.ts own_facts_main_drops.ts own_facts_tex_lib.ts own_facts_tex_main.ts) file(COPY "${SOURCE_DIR}/${file}" DESTINATION "${WORK_DIR}") endforeach() @@ -68,3 +68,24 @@ execute_process(COMMAND "${WORK_DIR}/own_facts.exe" if(NOT status EQUAL 0 OR NOT "${out}" MATCHES "^11[\r\n]+done[.]") message(FATAL_ERROR "own_facts.exe: exit ${status}\n${out}${err}") endif() + +# TSLANG 74: `cond ? null : device.load(...)` of the library's, pushed where not null, then used +tslang_run(tex_lib --emit=obj -mm=own own_facts_tex_lib.ts -o own_facts_tex_lib.obj) +if(NOT status EQUAL 0 OR output MATCHES "error:|Stack dump") + message(FATAL_ERROR "own_facts_tex_lib: exit ${status}\n${output}") +endif() + +tslang_run(tex_link --emit=exe -mm=own "--llvm-lib-path=${LLVM_LIB}" "--tslang-lib-path=${TSLANG_LIB}" + --own-facts=own_facts_tex_lib.obj --obj=own_facts_tex_lib.obj own_facts_tex_main.ts -o own_facts_tex.exe) +if(NOT status EQUAL 0 OR output MATCHES "error:|Stack dump") + message(FATAL_ERROR "own_facts_tex_main with --own-facts: exit ${status}\n${output}") +endif() + +execute_process(COMMAND "${WORK_DIR}/own_facts_tex.exe" + WORKING_DIRECTORY "${WORK_DIR}" + OUTPUT_VARIABLE out + ERROR_VARIABLE err + RESULT_VARIABLE status) +if(NOT status EQUAL 0 OR NOT "${out}" MATCHES "^false 2 12 16[\r\n]+done[.]") + message(FATAL_ERROR "own_facts_tex.exe: exit ${status}\n${out}${err}") +endif() diff --git a/tslang/test/tester/own/own_err_const_alias_drops.ts b/tslang/test/tester/own/own_err_const_alias_drops.ts new file mode 100644 index 000000000..82e5b44e5 --- /dev/null +++ b/tslang/test/tester/own/own_err_const_alias_drops.ts @@ -0,0 +1,27 @@ +// -mm=own rejects: `reset`, called through the constant `clear`, empties the mesh's clusters, so +// the cluster borrowed from them is used after it may be released (TSLANG 75 resolves the call to +// `reset`, whose facts say so). +class Cluster { + positions: number[] = [1, 2, 3]; +} + +class Mesh { + clusters: Cluster[] = [new Cluster()]; +} + +function reset(mesh: Mesh) { + mesh.clusters = []; +} + +const clear = reset; + +function sum(meshes: Mesh[]) { + const mesh = meshes[0]; + const c = mesh.clusters[0]; + clear(mesh); + print(c.positions.length); +} + +function main() { + sum([new Mesh()]); +} diff --git a/tslang/test/tester/own/own_err_declared_drops.ts b/tslang/test/tester/own/own_err_declared_drops.ts index 85eecdc8e..997001dec 100644 --- a/tslang/test/tester/own/own_err_declared_drops.ts +++ b/tslang/test/tester/own/own_err_declared_drops.ts @@ -1,5 +1,5 @@ -// -mm=own rejects: `strlen` is declared without `@nodrops`, so the method that calls it may -// destroy anything, and `this.value` cannot be lent to it (own_declared_no_drops). +// -mm=own rejects: `strlen` is declared without `@nodrops`, so the method that calls it may destroy anything, +// and `this.value` (public: not one TSLANG 73 keeps) cannot be lent to it (own_declared_no_drops). declare function strlen(s: string): index; namespace __S { @@ -9,7 +9,7 @@ namespace __S { } class W { - constructor(private value: string) {} + constructor(public value: string) {} size(): int { return this.value.size(); diff --git a/tslang/test/tester/own/own_err_kept_array_filled_after_pop.ts b/tslang/test/tester/own/own_err_kept_array_filled_after_pop.ts new file mode 100644 index 000000000..c3b0c06e0 --- /dev/null +++ b/tslang/test/tester/own/own_err_kept_array_filled_after_pop.ts @@ -0,0 +1,27 @@ +// -mm=own rejects: `sets`, moved into a frame pushed on `this.frames`, is that frame's array; the +// frame popped, the push through `sets` would fill a freed array (TSLANG 77). +@final +class DSet { + constructor(public id: int) {} +} + +@final +class Frame { + constructor(public view: int, public sets: DSet[]) {} +} + +@final +class App { + frames: Frame[] = []; + + build() { + const sets: DSet[] = []; + this.frames.push(new Frame(0, sets)); + this.frames.pop(); + sets.push(new DSet(1)); + } +} + +function main() { + new App().build(); +} diff --git a/tslang/test/tester/own/own_err_kept_field_rebound.ts b/tslang/test/tester/own/own_err_kept_field_rebound.ts new file mode 100644 index 000000000..8f2aa7984 --- /dev/null +++ b/tslang/test/tester/own/own_err_kept_field_rebound.ts @@ -0,0 +1,31 @@ +// -mm=own rejects: `App`'s constructor gives `this` to a call, and `rebind` writes the field it keeps +// `sample` in, so that field is not one the call cannot overwrite (TSLANG 76): `sample`, used after +// `new`, was moved. +@final +class Sample { + options: string[] = []; + + add(option: string) { + this.options.push(option); + } +} + +@final +class App { + constructor(private sample: Sample) { + this.reset(); + } + + reset() {} + + rebind(sample: Sample) { + this.sample = sample; + } +} + +function main() { + const sample = new Sample(); + const app = new App(sample); + sample.add("-slow"); + app.rebind(new Sample()); +} diff --git a/tslang/test/tester/own/own_err_kept_local_array_popped.ts b/tslang/test/tester/own/own_err_kept_local_array_popped.ts new file mode 100644 index 000000000..3dd9f0064 --- /dev/null +++ b/tslang/test/tester/own/own_err_kept_local_array_popped.ts @@ -0,0 +1,23 @@ +// TSLANG_ISSUES 72: a local kept by a constructor reads the object's field after, so the object popped +// before the read frees it. +class Node2 { + constructor(public kind: int, public t: number[]) {} +} + +class App { + nodes: Node2[] = []; + first = 0.0; + + build(o: int) { + const t: number[] = [1.0]; + const node = new Node2(o, t); + this.nodes.push(node); + this.nodes.pop(); + this.first = t[0]; + } +} + +function main() { + new App().build(1); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_local_object_field_popped.ts b/tslang/test/tester/own/own_err_local_object_field_popped.ts new file mode 100644 index 000000000..f5ce87ef5 --- /dev/null +++ b/tslang/test/tester/own/own_err_local_object_field_popped.ts @@ -0,0 +1,23 @@ +// -mm=own rejects: `a`, pushed on a field of the local object `frame`, is read back from it after the +// push (TSLANG 78); popped first, it would be read from a freed element. +@final +class Alloc { + constructor(public id: int) {} +} + +@final +class Frame { + allocs: Alloc[] = []; +} + +function build() { + const frame = new Frame(); + const a = new Alloc(1); + frame.allocs.push(a); + frame.allocs.pop(); + print(a.id); +} + +function main() { + build(); +} diff --git a/tslang/test/tester/own/own_err_move_some_paths_field.ts b/tslang/test/tester/own/own_err_move_some_paths_field.ts deleted file mode 100644 index 62e2495c7..000000000 --- a/tslang/test/tester/own/own_err_move_some_paths_field.ts +++ /dev/null @@ -1,11 +0,0 @@ -// -mm=own rejects: `a` moves into the field on one path only, and a field store cannot borrow. -class C { x: number = 5; } -class H { c: C = new C(); } -function main(argc: number) { - const h = new H(); - const a = new C(); - if (argc > 5) { - h.c = a; - } - print("done."); -} diff --git a/tslang/test/tester/own/own_err_moved_on_some_paths_used.ts b/tslang/test/tester/own/own_err_moved_on_some_paths_used.ts new file mode 100644 index 000000000..3213148f0 --- /dev/null +++ b/tslang/test/tester/own/own_err_moved_on_some_paths_used.ts @@ -0,0 +1,27 @@ +// -mm=own rejects: `c`, pushed only where `read` succeeds, is released on the other way (TSLANG 81), +// so a use of it after the ways join may find it moved. +@final +class Ctl { + node: int = 0; + + read(ok: boolean): boolean { + return ok; + } +} + +@final +class Model { + lights: Ctl[] = []; + + load(n: int) { + const c = new Ctl(); + if (c.read(n != 2)) { + this.lights.push(c); + } + print(c.node); + } +} + +function main() { + new Model().load(1); +} diff --git a/tslang/test/tester/own/own_err_moved_when_tested_other.ts b/tslang/test/tester/own/own_err_moved_when_tested_other.ts deleted file mode 100644 index ab70c9726..000000000 --- a/tslang/test/tester/own/own_err_moved_when_tested_other.ts +++ /dev/null @@ -1,18 +0,0 @@ -// TSLANG_ISSUES 49: a fresh local moved into a field on some ways only is fine when on the others it -// was tested empty - not here, where `n == "y"` may be false while `l` holds an object. -class Level { - name: string = "a"; -} - -class App { - level: Level | null = null; - read(n: string): Level | null { return n == "" ? null : new Level(); } - start(n: string) { - const l = this.read(n); - if (l && n == "y") { - this.level = l; - } - } -} - -function main() { const a = new App(); a.start("x"); print(a.level != null); } diff --git a/tslang/test/tester/own/own_err_private_field_written_callback.ts b/tslang/test/tester/own/own_err_private_field_written_callback.ts new file mode 100644 index 000000000..86338753e --- /dev/null +++ b/tslang/test/tester/own/own_err_private_field_written_callback.ts @@ -0,0 +1,43 @@ +// TSLANG_ISSUES 73: a private field another method writes may be overwritten by a callback the call +// runs, so a borrow of it across the call is refused. +const retained: ((n: int) => void)[] = []; + +class Sample { + frames = 0; + callback: ((n: int) => void) | null = null; + run(count: int): int { + for (let i = 0; i < count; i++) { + const callback = this.callback; + if (callback) { + callback(i); + } + this.frames++; + } + return this.frames; + } +} + +class App { + total = 0; + constructor(private sample: Sample) {} + reset() { + this.sample = new Sample(); + } + frame(n: int) { + this.total += n; + } + run(): int { + const frame: (n: int) => void = this.frame; + retained.push(frame); + this.sample.callback = frame; + const frames = this.sample.run(3); + this.sample.callback = null; + return frames + this.total; + } +} + +function main() { + const app = new App(new Sample()); + assert(app.run() == 3 + 0 + 1 + 2); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_pushed_let_popped.ts b/tslang/test/tester/own/own_err_pushed_let_popped.ts new file mode 100644 index 000000000..6546ba471 --- /dev/null +++ b/tslang/test/tester/own/own_err_pushed_let_popped.ts @@ -0,0 +1,28 @@ +// -mm=own rejects: `pipe`, a local pushed where it is not null, is the array's element after the +// push (TSLANG 79); popped before it is returned, it would be a freed element. +@final +class Pipe { + constructor(public id: int) {} +} + +@final +class Cache { + private pipes: Pipe[] = []; + + get(ok: boolean): Pipe | null { + let pipe: Pipe | null = null; + if (ok) { + pipe = new Pipe(1); + } + if (pipe) { + this.pipes.push(pipe); + this.pipes.pop(); + } + return pipe; + } +} + +function main() { + const c = new Cache(); + print(c.get(true) != null); +} diff --git a/tslang/test/tester/own/own_err_two_owners.ts b/tslang/test/tester/own/own_err_two_owners.ts deleted file mode 100644 index c844de441..000000000 --- a/tslang/test/tester/own/own_err_two_owners.ts +++ /dev/null @@ -1,15 +0,0 @@ -// -mm=own, phase 0 rejects: the array literal would have two owners, the field and `a`. With -// the count check loosened this compiles and double-frees; rc runs it correctly. -class N { v: number[] = [0]; } -function main() { - let sum = 0; - for (let i = 0; i < 100000; i++) { - const n = new N(); - let a = (n.v = [i, i + 1]); - const filler = [i, i, i, i]; - sum += a[1] - i + filler.length; - } - print(sum); - assert(sum == 500000); - print("done."); -} diff --git a/tslang/test/tester/tests/00tslang_issue_71.ts b/tslang/test/tester/tests/00tslang_issue_71.ts new file mode 100644 index 000000000..efaae6df2 --- /dev/null +++ b/tslang/test/tester/tests/00tslang_issue_71.ts @@ -0,0 +1,90 @@ +// TSLANG_ISSUES 71-72 (-mm=own), from native_ts_graphics: +// 71: examples/nri/raytracing_ao_nri.ts, `const texture = device.createStorageTexture(...); if (!texture) +// return null; const target = new Target(texture, ...); target.readView = texture.createView(format);` - +// a value narrowed from `T | null`, kept by a constructor, then used. +// 72: examples/nri/extended_dynamic_state2_nri.ts, `const t: number[] = []; for (...) t.push(...); const +// node = new SceneNode(..., t, ...); ... this.movingX = t[12];` in a loop - a local kept by a +// constructor, or pushed, then read; and `let p = make(); ...; this.ps.push(p); return p.k;`. + +class View { + constructor(public n: int) {} +} + +class Tex { + constructor(public n: int) {} + view(): View { + return new View(this.n); + } +} + +function load(n: int): Tex | null { + return n > 0 ? new Tex(n) : null; +} + +class Target { + view: View | null = null; + constructor(public tex: Tex, public state: int) {} +} + +class Node2 { + constructor(public kind: int, public t: number[]) {} +} + +class App { + targets: Target[] = []; + nodes: Node2[] = []; + others: Node2[] = []; + texs: Tex[] = []; + first = 0.0; + + target(n: int): Target | null { + const tex = load(n); + if (!tex) { + return null; + } + const target = new Target(tex, 1); + target.view = tex.view(); + this.targets.push(target); + return target; + } + + build(n: int) { + for (let o = 0; o < n; o++) { + const t: number[] = []; + for (let i = 0; i < 3; i++) { + t.push(o * 10 + i); + } + const node = new Node2(o % 2, t); + if (o % 2 == 0) { + this.nodes.push(node); + } else { + if (o == 1) { + this.first = t[0]; + } + this.others.push(node); + } + } + } + + keep(n: int, again: boolean): int { + let tex = load(n); + if (again) { + tex = load(n + 1); + } + if (!tex) { + return -1; + } + this.texs.push(tex); + return tex.n; + } +} + +function main() { + const app = new App(); + const t = app.target(3); + assert(t != null && t.view != null && t.view.n == 3 && app.target(0) == null); + app.build(4); + assert(app.first == 10.0 && app.nodes.length == 2 && app.others[1].t[2] == 32.0); + assert(app.keep(2, false) == 2 && app.keep(2, true) == 3 && app.keep(0, false) == -1 && app.texs.length == 2); + print("done."); +} diff --git a/tslang/test/tester/tests/00tslang_issue_73.ts b/tslang/test/tester/tests/00tslang_issue_73.ts new file mode 100644 index 000000000..24274189e --- /dev/null +++ b/tslang/test/tester/tests/00tslang_issue_73.ts @@ -0,0 +1,41 @@ +// TSLANG_ISSUES 73 (-mm=own), from native_ts_graphics' NRI examples: `constructor(private sample: NriSample)` +// and `this.sample.run(...)`, which runs callbacks into the app. A private field nothing writes after the +// constructor is not overwritten by them, so the borrow of it holds across the call. +const retained: ((n: int) => void)[] = []; + +class Sample { + frames = 0; + callback: ((n: int) => void) | null = null; + run(count: int): int { + for (let i = 0; i < count; i++) { + const callback = this.callback; + if (callback) { + callback(i); + } + this.frames++; + } + return this.frames; + } +} + +class App { + total = 0; + constructor(private sample: Sample) {} + frame(n: int) { + this.total += n; + } + run(): int { + const frame: (n: int) => void = this.frame; + retained.push(frame); + this.sample.callback = frame; + const frames = this.sample.run(3); + this.sample.callback = null; + return frames + this.total; + } +} + +function main() { + const app = new App(new Sample()); + assert(app.run() == 3 + 0 + 1 + 2); + print("done."); +} diff --git a/tslang/test/tester/tests/00tslang_issue_75.ts b/tslang/test/tester/tests/00tslang_issue_75.ts new file mode 100644 index 000000000..ee22265e5 --- /dev/null +++ b/tslang/test/tester/tests/00tslang_issue_75.ts @@ -0,0 +1,43 @@ +// TSLANG 75: a call through a global nothing assigns, given a function by its initializer, is a +// call of that function: `const f = half` (as `const f = Math.fround`) destroys nothing, so the cluster borrowed from a +// parameter's mesh outlives it. +namespace Clusters { + function half(x: number) { + return x / 2; + } + const f = half; + function twice(x: number) { + return x * 2; + } + const g = twice; + + export class Cluster { + positions: number[] = [1, 2, 3, 4]; + } + + export class Mesh { + clusters: Cluster[] = [new Cluster(), new Cluster()]; + } + + export function centroid(meshes: Mesh[]): number { + const mesh = meshes[0]; + let sum = 0.0; + for (const c of mesh.clusters) { + let y = 0.0; + for (let v = 0; v < c.positions.length; v++) { + y = f(y + c.positions[v]); + y = g(y); + } + sum = sum + y / c.positions.length; + } + + return sum; + } +} + +function main() { + const meshes = [new Clusters.Mesh()]; + const c = Clusters.centroid(meshes); + assert(c == 5, "centroid"); + print("done."); +} diff --git a/tslang/test/tester/tests/00tslang_issue_76.ts b/tslang/test/tester/tests/00tslang_issue_76.ts new file mode 100644 index 000000000..8ad15dea3 --- /dev/null +++ b/tslang/test/tester/tests/00tslang_issue_76.ts @@ -0,0 +1,37 @@ +// TSLANG 76: a constructor that keeps a parameter in a private field and gives `this` to a call +// (`this.reset()`): no other function writes the field, so the call cannot overwrite it, and the +// parameter, used after `new`, is read back from the field. +@final +class Sample { + options: string[] = []; + + add(option: string) { + this.options.push(option); + } +} + +@final +class App { + private constants: number[] = [0, 0]; + + constructor(private sample: Sample) { + this.reset(); + } + + reset() { + this.constants[0] = 4.0; + } + + count(): int { + return this.sample.options.length; + } +} + +function main() { + const sample = new Sample(); + const app = new App(sample); + sample.add("-slow"); + sample.add("-noUI"); + assert(app.count() == 2, "count"); + print("done."); +} diff --git a/tslang/test/tester/tests/00tslang_issue_77.ts b/tslang/test/tester/tests/00tslang_issue_77.ts new file mode 100644 index 000000000..526f0c9d5 --- /dev/null +++ b/tslang/test/tester/tests/00tslang_issue_77.ts @@ -0,0 +1,26 @@ +// TSLANG 77: an array moved into a constructor that keeps it, the object pushed, then filled through +// the local: each push fills the object's array (`sets` is that array, by reference). +@final +class DSet { + constructor(public id: int) {} +} +@final +class Frame { + constructor(public view: int, public sets: DSet[]) {} +} +@final +class App { + frames: Frame[] = []; + build(n: int): int { + for (let i = 0; i < 2; i++) { + const sets: DSet[] = []; + this.frames.push(new Frame(i, sets)); + for (let t = 0; t < n; t++) { + const set = new DSet(t); + sets.push(set); + } + } + return this.frames[1].sets.length; + } +} +function main() { const a = new App(); assert(a.build(3) == 3, "sets"); print("done."); } diff --git a/tslang/test/tester/tests/00tslang_issue_78.ts b/tslang/test/tester/tests/00tslang_issue_78.ts new file mode 100644 index 000000000..65deff4b2 --- /dev/null +++ b/tslang/test/tester/tests/00tslang_issue_78.ts @@ -0,0 +1,68 @@ +// TSLANG 78: a value pushed on, or stored into, a field of a local object (`const frame = new Frame()`, +// a value, not a read), then used, is read back from that field. +@final +class Buf { constructor(public id: int) {} } +@final +class Alloc { + constructor(public id: int) {} + make(): Buf { return new Buf(this.id); } +} +@final +class Frame { + allocs: Alloc[] = []; + bufs: Buf[] = []; + constructor(public n: int) {} +} +@final +class App { + frames: Frame[] = []; + build(): int { + const frame = new Frame(1); + for (let f = 0; f < 3; f++) { + const a = new Alloc(f); + frame.allocs.push(a); + frame.bufs.push(a.make()); + } + this.frames.push(frame); + return this.frames[0].bufs.length; + } +} +@final +class Mesh { + constructor(public n: int) {} +} + +@final +class Obj { + mesh: Mesh | null = null; +} + +function fieldStore() { + const objects: Obj[] = []; + const o = new Obj(); + const mesh = new Mesh(5); + o.mesh = mesh; + assert(mesh.n == 5, "mesh"); + objects.push(o); +} + +// an array stored into a field of a local object and read through a local declared from the store +// (was own_err_two_owners: the local now reads the field) +class N { + v: number[] = [0]; +} + +function aliasedStore(): number { + let sum = 0; + for (let i = 0; i < 1000; i++) { + const n = new N(); + let a = (n.v = [i, i + 1]); + const filler = [i, i, i, i]; + sum += a[1] - i + filler.length; + } + + return sum; +} + +function main() { assert(new App().build() == 3, "bufs"); fieldStore(); assert(aliasedStore() == 5000, "aliased"); + print("done."); } diff --git a/tslang/test/tester/tests/00tslang_issue_79.ts b/tslang/test/tester/tests/00tslang_issue_79.ts new file mode 100644 index 000000000..6a028deaf --- /dev/null +++ b/tslang/test/tester/tests/00tslang_issue_79.ts @@ -0,0 +1,37 @@ +// TSLANG 79: a local pushed only where it is not null (`let pipeline = ...; if (pipeline) +// this.pipelines.push(pipeline); return pipeline;`) reads the array's element after the push, and null +// where the test failed: the method returns a borrow of `this` on every way. +@final +class Pipe { + constructor(public id: int) {} +} +@final +class Cache { + private keys: int[] = []; + private pipes: Pipe[] = []; + + get(key: int, ok: boolean): Pipe | null { + for (let i = 0; i < this.keys.length; i++) { + if (this.keys[i] == key) { + return this.pipes[i]; + } + } + let pipe: Pipe | null = null; + if (ok) { + pipe = new Pipe(key); + } + if (pipe) { + this.keys.push(key); + this.pipes.push(pipe); + } + return pipe; + } +} +function main() { + const c = new Cache(); + const a = c.get(3, true); + const b = c.get(3, true); + const n = c.get(4, false); + assert((a ? a.id : -1) == 3 && (b ? b.id : -1) == 3 && n == null, "cache"); + print("done."); +} diff --git a/tslang/test/tester/tests/00tslang_issue_81.ts b/tslang/test/tester/tests/00tslang_issue_81.ts new file mode 100644 index 000000000..587355fe8 --- /dev/null +++ b/tslang/test/tester/tests/00tslang_issue_81.ts @@ -0,0 +1,79 @@ +// TSLANG 81: a value moved on some paths only (`const c = new Ctl(); if (c.read()) this.lights.push(c);`) +// is released on the edges that did not move it. +@final +class Ctl { + node: int = 0; + read(ok: boolean): boolean { return ok; } +} +@final +class Model { + lights: Ctl[] = []; + load(n: int) { + for (let i = 0; i < n; i++) { + const c = new Ctl(); + if (c.read(i != 2)) { + this.lights.push(c); + } + } + } + load2(n: int) { + const c = new Ctl(); + if (c.read(n != 2)) { + this.lights.push(c); + } + } +} +// moved into a field on one way only (was own_err_move_some_paths_field) +class C { + x: number = 5; +} + +class H { + c: C = new C(); +} + +function intoField(argc: number): number { + const h = new H(); + const a = new C(); + a.x = 7; + if (argc > 5) { + h.c = a; + } + + return h.c.x; +} + +// moved where a second test passes, not only where the value is set (was own_err_moved_when_tested_other) +class Level { + name: string = "a"; +} + +class App { + level: Level | null = null; + + read(n: string): Level | null { + return n == "" ? null : new Level(); + } + + start(n: string) { + const l = this.read(n); + if (l && n == "y") { + this.level = l; + } + } +} + +function main() { + const m = new Model(); + m.load(4); + m.load2(1); + m.load2(2); + assert(m.lights.length == 4, "lights"); + assert(intoField(1) == 5 && intoField(9) == 7, "field"); + const app = new App(); + app.start("x"); + assert(app.level == null, "x"); + app.start("y"); + assert(app.level != null, "y"); + print("done."); +} diff --git a/tslang/test/tester/tests/own_facts_tex_lib.ts b/tslang/test/tester/tests/own_facts_tex_lib.ts new file mode 100644 index 000000000..32ede2ab5 --- /dev/null +++ b/tslang/test/tester/tests/own_facts_tex_lib.ts @@ -0,0 +1,21 @@ +// TSLANG 74's library (own-facts-objects.cmake): a texture loader whose result is the caller's own, +// or null. +@final +export class Texture { + constructor(public size: number) {} + + format(): number { + return 4; + } + + view(format: number): number { + return this.size * format; + } +} + +@final +export class Device { + load(name: string): Texture | null { + return name.length > 1 ? new Texture(name.length) : null; + } +} diff --git a/tslang/test/tester/tests/own_facts_tex_main.ts b/tslang/test/tester/tests/own_facts_tex_main.ts new file mode 100644 index 000000000..7807f4b77 --- /dev/null +++ b/tslang/test/tester/tests/own_facts_tex_main.ts @@ -0,0 +1,32 @@ +// TSLANG 74 (own-facts-objects.cmake): a value of `cond ? null : f()`, f another module's, pushed where +// it is not null and then used, reads the array's element - the merge is the function's own, as `f()` +// alone is, though the signature pass marks the imported call's result fresh only after it looks. +import { Texture, Device } from "./own_facts_tex_lib"; + +class Materials { + textures: Texture[] = []; + views: number[] = []; + + add(device: Device, names: string[]): boolean { + let ok = true; + for (let m = 0; m < names.length; m++) { + const name = names[m]; + const texture = name == "" ? null : device.load(name); + if (!texture) { + ok = false; + } else { + this.textures.push(texture); + this.views.push(texture.view(texture.format())); + } + } + + return ok; + } +} + +function main() { + const materials = new Materials(); + const ok = materials.add(new Device(), ["abc", "", "x", "wxyz"]); + print(ok, materials.textures.length, materials.views[0], materials.views[1]); + print("done."); +}