diff --git a/README.md b/README.md index 7b881a956..fd469227f 100644 --- a/README.md +++ b/README.md @@ -759,7 +759,8 @@ what to do about it. ### Performance The ray tracer benchmark in [tslang/benchmarks/raytrace](tslang/benchmarks/raytrace/README.md) -(`raytrace.ts` at 512x512, median of 7 runs, 2026-10-21) against Rust and C++ ports of the same +(`raytrace.ts` at 512x512, median of 7 runs; the `-mm=own` row measured 2026-10-23, the other rows +2026-10-21) against Rust and C++ ports of the same code, each written twice: with plain value vectors and colors, and with every object reference-counted on the heap the way tslang allocates them: @@ -768,7 +769,7 @@ reference-counted on the heap the way tslang allocates them: | `-mm=gc` | 1298 ms | 6 MB | 26x slower | | `-mm=rc` | 599 ms | 4 MB | 12x slower | | `-mm=none` | 703 ms | 845 MB | 14x slower | -| `-mm=own` | does not compile yet | | | +| `-mm=own` (2026-10-23) | 628 ms | 4.5 MB | 12x slower | | C++, values (clang / MSVC) | 51 / 69 ms | | | | Rust, values (`Copy` vectors and colors) | 55 ms | 4 MB | 1.1x slower | | Rust, every object in an `Rc` | 492 ms | 4 MB | 9.7x slower | @@ -779,6 +780,10 @@ Rust that allocates the same way is only 1.2x faster than `-mm=rc`, and C++ with (atomic counts) is 1.4x slower than it. `-mm=none` is slower than `-mm=rc` because it never frees and keeps touching fresh pages. Stack promotion makes no measurable difference here yet: the hot allocations are returned from functions or captured by closures. +`-mm=own` builds the benchmark's copy with two source edits (the shared surfaces are `Shared` +handles, and one method returns a fresh vector instead of its own field). It runs as fast as +`-mm=rc` in as little memory, counting only those two handles (its row measured 2026-10-23, when +`-mm=rc` took 638 ms and peaked at 4.5 MB). ### `-mm=own` diff --git a/tslang/benchmarks/raytrace/README.md b/tslang/benchmarks/raytrace/README.md index 8fe964426..2d5699881 100644 --- a/tslang/benchmarks/raytrace/README.md +++ b/tslang/benchmarks/raytrace/README.md @@ -1,6 +1,9 @@ # Ray tracer: memory models vs Rust and C++ `rtbench.ts` is `test/tester/tests/raytrace.ts` at 512x512 with a pixel checksum (see its header). +`rtbench_own.ts` is the same program for `-mm=own`, with two source edits: the surfaces, which every +thing shares, are `Shared` handles, and `Plane.normal` returns a fresh vector instead of +its own field. `rust/` and `cpp/` each hold two ports of the same algorithm: - `rust/src/bin/idiomatic.rs`, `cpp/idiomatic.cpp`: `Vector`/`Color` are plain values, things are @@ -19,11 +22,11 @@ pwsh tslang/benchmarks/raytrace/bench.ps1 ``` - tslang: `--emit=exe --opt --opt_level=3 --no-default-lib --mm=` (`--no-default-lib` - because the file declares its own `sqrt`/`floor`/`pow`/`fabs`) + because the file declares its own `sqrt`/`floor`/`pow`/`fabs`); `-mm=own` builds `rtbench_own.ts` - Rust: `opt-level=3`, `lto="fat"`, `codegen-units=1`, `panic="abort"` - C++: `clang++ -std=c++20 -O3`, and `cl /std:c++20 /O2 /EHsc /MT` -## Results 2026-10-21 +## Results 2026-10-21 (own rows 2026-10-23) main 49b5f6bb, Release, Windows 11 x64; Rust 1.99.0, clang 21.1.4, MSVC from Visual Studio 18; median of 7 runs after a warm-up. Every build prints the same checksum. @@ -36,7 +39,8 @@ median of 7 runs after a warm-up. Every build prints the same checksum. | tslang rc, `--no-stack-promotion` | 600 ms | | | | tslang none | 703 ms | 845 MB | 14x | | tslang none, `--no-stack-promotion` | 727 ms | | | -| tslang own | does not compile | | | +| tslang own (2026-10-23) | 628 ms | 4.5 MB | 12x | +| tslang own, `--no-stack-promotion` (2026-10-23) | 627 ms | | | | Rust idiomatic | 55 ms | 4 MB | 1.1x | | Rust Rc-boxed | 492 ms | 4 MB | 9.7x | | C++ idiomatic (clang) | 51 ms | | 1x | @@ -52,5 +56,11 @@ median of 7 runs after a warm-up. Every build prints the same checksum. under rc keeps every hot-path allocation on the heap: `Vector.*`/`Color.*` results are "returned", the `Ray`/`Intersection` literals are "converted" (to the interface) or "recursive", and the `addLight`/`getPoint` closures are "counted-capture". -- `-mm=own` reports about 44 errors: the program shares the `Surfaces` globals, keeps owned values - in object literals, and aliases `Color.black` from two statics. +- The own rows were measured on 2026-10-23 (branch own-raytrace, the same machine, two runs of + `bench.ps1`, median of 7 each; both gave 627.5 ms), the rest of the table on 2026-10-21. In the + same session rc took 638-640 ms and peaked at 4.5 MB as well, so own runs as fast as rc and frees + as promptly, with no counts: only the two `Shared` handles count. Peak working set is the + process's `PeakWorkingSet64`, polled while it runs. +- `-mm=own` needs `rtbench_own.ts`'s two edits: the surfaces are shared by every thing, and + `Plane.normal` returned a borrow of its own field where `Sphere.normal` returns a fresh vector, so + a caller through the `Thing` interface could not know whether it owns the result. diff --git a/tslang/benchmarks/raytrace/bench.ps1 b/tslang/benchmarks/raytrace/bench.ps1 index 931408bcb..88c4da472 100644 --- a/tslang/benchmarks/raytrace/bench.ps1 +++ b/tslang/benchmarks/raytrace/bench.ps1 @@ -1,6 +1,6 @@ -# Builds rtbench.ts under every memory model (with and without stack promotion) and the two Rust -# ports, checks that they all print the same checksum, then times each one: a warm-up run and -# $Runs timed runs, reporting min/median/max wall time. +# Builds rtbench.ts under every memory model (with and without stack promotion; -mm=own builds its +# copy rtbench_own.ts) and the Rust and C++ ports, checks that they all print the same checksum, +# then times each one: a warm-up run and $Runs timed runs, reporting min/median/max wall time. # # pwsh tslang/benchmarks/raytrace/bench.ps1 [-Tslang ] [-Runs 7] param( @@ -19,7 +19,8 @@ foreach ($mm in "gc", "rc", "none", "own") { $exe = Join-Path $out ("rtb_$mm" + $(if ($sp) { "_nsp" } else { "" }) + ".exe") $flags = @("--emit=exe", "--opt", "--opt_level=3", "--no-default-lib", "--mm=$mm") if ($sp) { $flags += "--no-stack-promotion" } - & $Tslang @flags "$PSScriptRoot\rtbench.ts" -o $exe 2>&1 | Out-Null + $src = if ($mm -eq "own") { "$PSScriptRoot\rtbench_own.ts" } else { "$PSScriptRoot\rtbench.ts" } + & $Tslang @flags $src -o $exe 2>&1 | Out-Null if ($LASTEXITCODE -ne 0) { "{0,-22} does not compile (exit {1})" -f $name, $LASTEXITCODE; continue } $bins += @{ n = $name; p = $exe } } diff --git a/tslang/benchmarks/raytrace/rtbench_own.ts b/tslang/benchmarks/raytrace/rtbench_own.ts new file mode 100644 index 000000000..e22da49d7 --- /dev/null +++ b/tslang/benchmarks/raytrace/rtbench_own.ts @@ -0,0 +1,318 @@ +// @strict-null false +// rtbench.ts for -mm=own, with its two source edits: the surfaces, which every thing shares, are +// `Shared` handles, and `Plane.normal` returns a fresh vector, as `Sphere.normal` does, +// instead of its own field. Otherwise the same program and the same checksum (38837369). +// print() shows the sum with 6 significant digits, so the low digits are printed separately. +declare function sqrt(v: number): number; +declare function floor(v: number): number; +declare function pow(v: number, s: number): number; +declare function fabs(v: number): number; + +class Math { + static sqrt(v: number): number { + return sqrt(v); + } + + static floor(v: number): number { + return floor(v); + } + + static pow(v: number, s: number): number { + return pow(v, s); + } + + static abs(v: number): number { + return fabs(v); + } + + static sign(v: number): number { + if (v > 0) return 1; + if (v < 0) return -1; + return 0; + } +} + +class Vector { + constructor(public x: number, + public y: number, + public z: number) { + } + static times(k: number, v: Vector) { return new Vector(k * v.x, k * v.y, k * v.z); } + static minus(v1: Vector, v2: Vector) { return new Vector(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z); } + static plus(v1: Vector, v2: Vector) { return new Vector(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z); } + static dot(v1: Vector, v2: Vector) { return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z; } + static mag(v: Vector) { return Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z); } + static norm(v: Vector) { + let mag = Vector.mag(v); + let div = (mag === 0) ? Infinity : 1.0 / mag; + return Vector.times(div, v); + } + static cross(v1: Vector, v2: Vector) { + return new Vector(v1.y * v2.z - v1.z * v2.y, + v1.z * v2.x - v1.x * v2.z, + v1.x * v2.y - v1.y * v2.x); + } +} + +class Color { + constructor(public r: number, + public g: number, + public b: number) { + } + static scale(k: number, v: Color) { return new Color(k * v.r, k * v.g, k * v.b); } + static plus(v1: Color, v2: Color) { return new Color(v1.r + v2.r, v1.g + v2.g, v1.b + v2.b); } + static times(v1: Color, v2: Color) { return new Color(v1.r * v2.r, v1.g * v2.g, v1.b * v2.b); } + static white = new Color(1.0, 1.0, 1.0); + static grey = new Color(0.5, 0.5, 0.5); + static black = new Color(0.0, 0.0, 0.0); + static background = Color.black; + static defaultColor = Color.black; + static toDrawingColor(c: Color) { + let legalize = (d: number) => d > 1 ? 1 : d; + return { + r: Math.floor(legalize(c.r) * 255), + g: Math.floor(legalize(c.g) * 255), + b: Math.floor(legalize(c.b) * 255) + } + } +} + +class Camera { + public forward: Vector; + public right: Vector; + public up: Vector; + + constructor(public pos: Vector, lookAt: Vector) { + let down = new Vector(0.0, -1.0, 0.0); + this.forward = Vector.norm(Vector.minus(lookAt, this.pos)); + this.right = Vector.times(1.5, Vector.norm(Vector.cross(this.forward, down))); + this.up = Vector.times(1.5, Vector.norm(Vector.cross(this.forward, this.right))); + } +} + +interface Ray { + start: Vector; + dir: Vector; +} + +interface Intersection { + thing: Thing; + ray: Ray; + dist: number; +} + +interface Surface { + diffuse: (pos: Vector) => Color; + specular: (pos: Vector) => Color; + reflect: (pos: Vector) => number; + roughness: number; +} + +interface Thing { + intersect(ray: Ray): Intersection; + normal(pos: Vector): Vector; + surface: Shared; +} + +interface Light { + pos: Vector; + color: Color; +} + +interface Scene { + things: Thing[]; + lights: Light[]; + camera: Camera; +} + +class Sphere implements Thing { + public radius2: number; + + constructor(public center: Vector, radius: number, public surface: Shared) { + this.radius2 = radius * radius; + } + normal(pos: Vector): Vector { return Vector.norm(Vector.minus(pos, this.center)); } + intersect(ray: Ray): Intersection { + let eo = Vector.minus(this.center, ray.start); + let v = Vector.dot(eo, ray.dir); + let dist = 0.0; + if (v >= 0) { + let disc = this.radius2 - (Vector.dot(eo, eo) - v * v); + if (disc >= 0) { + dist = v - Math.sqrt(disc); + } + } + if (dist === 0) { + return null; + } else { + return { thing: this, ray: ray, dist: dist }; + } + } +} + +class Plane implements Thing { + constructor(public norm: Vector, public offset: number, public surface: Shared) { + } + public normal(pos: Vector): Vector { return new Vector(this.norm.x, this.norm.y, this.norm.z); } + public intersect(ray: Ray): Intersection { + let denom = Vector.dot(this.norm, ray.dir); + if (denom > 0) { + return null; + } else { + let dist = (Vector.dot(this.norm, ray.start) + this.offset) / (-denom); + return { thing: this, ray: ray, dist: dist }; + } + } +} + +module Surfaces { + export let shiny = new Shared({ + diffuse: function (pos: Vector) { return Color.white; }, + specular: function (pos: Vector) { return Color.grey; }, + reflect: function (pos: Vector) { return 0.7; }, + roughness: 250.0 + }); + export let checkerboard = new Shared({ + diffuse: function (pos: Vector) { + if ((Math.floor(pos.z) + Math.floor(pos.x)) % 2 !== 0) { + return Color.white; + } else { + return Color.black; + } + }, + specular: function (pos: Vector) { return Color.white; }, + reflect: function (pos: Vector) { + if ((Math.floor(pos.z) + Math.floor(pos.x)) % 2 !== 0) { + return 0.1; + } else { + return 0.7; + } + }, + roughness: 150.0 + }); +} + + +class RayTracer { + private maxDepth = 5; + + private intersections(ray: Ray, scene: Scene) { + let closest = +Infinity; + let closestInter: Intersection = undefined; + for (let i in scene.things) { + let inter = scene.things[i].intersect(ray); + if (inter != null && inter.dist < closest) { + closestInter = inter; + closest = inter.dist; + } + } + + return closestInter; + } + + private testRay(ray: Ray, scene: Scene) { + let isect = this.intersections(ray, scene); + if (isect != null) { + return isect.dist; + } else { + return undefined; + } + } + + private traceRay(ray: Ray, scene: Scene, depth: number): Color { + let isect = this.intersections(ray, scene); + if (isect === undefined) { + return Color.background; + } else { + return this.shade(isect, scene, depth); + } + } + + private shade(isect: Intersection, scene: Scene, depth: number) { + let d = isect.ray.dir; + let pos = Vector.plus(Vector.times(isect.dist, d), isect.ray.start); + let normal = isect.thing.normal(pos); + let reflectDir = Vector.minus(d, Vector.times(2, Vector.times(Vector.dot(normal, d), normal))); + let naturalColor = Color.plus(Color.background, + this.getNaturalColor(isect.thing, pos, normal, reflectDir, scene)); + let reflectedColor = (depth >= this.maxDepth) ? Color.grey : this.getReflectionColor(isect.thing, pos, normal, reflectDir, scene, depth); + return Color.plus(naturalColor, reflectedColor); + } + + private getReflectionColor(thing: Thing, pos: Vector, normal: Vector, rd: Vector, scene: Scene, depth: number) { + return Color.scale(thing.surface.value.reflect(pos), this.traceRay({ start: pos, dir: rd }, scene, depth + 1)); + } + + private getNaturalColor(thing: Thing, pos: Vector, norm: Vector, rd: Vector, scene: Scene) { + const addLight = (col: Color, light: Light) => { + let ldis = Vector.minus(light.pos, pos); + let livec = Vector.norm(ldis); + let neatIsect = this.testRay({ start: pos, dir: livec }, scene); + let isInShadow = (neatIsect === undefined) ? false : (neatIsect <= Vector.mag(ldis)); + if (isInShadow) { + return col; + } else { + let illum = Vector.dot(livec, norm); + let lcolor = (illum > 0) ? Color.scale(illum, light.color) + : Color.defaultColor; + let specular = Vector.dot(livec, Vector.norm(rd)); + let scolor = (specular > 0) ? Color.scale(Math.pow(specular, thing.surface.value.roughness), light.color) + : Color.defaultColor; + return Color.plus(col, Color.plus(Color.times(thing.surface.value.diffuse(pos), lcolor), + Color.times(thing.surface.value.specular(pos), scolor))); + } + } + + return scene.lights.reduce(addLight, Color.defaultColor); + } + + render(scene: Scene, screenWidth: number, screenHeight: number) { + let sum: number = 0; + const getPoint = (x: number, y: number, camera: Camera) => { + const recenterX = (x: number) => (x - (screenWidth / 2.0)) / 2.0 / screenWidth; + const recenterY = (y: number) => - (y - (screenHeight / 2.0)) / 2.0 / screenHeight; + return Vector.norm(Vector.plus(camera.forward, Vector.plus(Vector.times(recenterX(x), camera.right), Vector.times(recenterY(y), camera.up)))); + } + + for (let y = 0; y < screenHeight; y++) { + for (let x = 0; x < screenWidth; x++) { + let color = this.traceRay({ start: scene.camera.pos, dir: getPoint(x, y, scene.camera) }, scene, 0); + let c = Color.toDrawingColor(color); + sum += c.r + c.g + c.b; + //ctx.fillStyle = "rgb(" + String(c.r) + ", " + String(c.g) + ", " + String(c.b) + ")"; + //ctx.fillRect(x, y, x + 1, y + 1); + } + } + return sum; + } +} + + +function defaultScene(): Scene { + return { + things: [new Plane(new Vector(0.0, 1.0, 0.0), 0.0, Surfaces.checkerboard), + new Sphere(new Vector(0.0, 1.0, -0.25), 1.0, Surfaces.shiny), + new Sphere(new Vector(-6, 3.0, -5.0), 1.0, Surfaces.shiny), + // new Sphere(new Vector(-6, 8.0, 15.0), 5.0, Surfaces.shiny), + new Sphere(new Vector(-1.0, 0.5, 1.5), 0.5, Surfaces.shiny)], + + lights: [{ pos: new Vector(-2.0, 2.5, 0.0), color: new Color(0.49, 0.07, 0.07) }, + { pos: new Vector(1.5, 2.5, 1.5), color: new Color(0.07, 0.07, 0.49) }, + { pos: new Vector(1.5, 2.5, -1.5), color: new Color(0.07, 0.49, 0.071) }, + { pos: new Vector(0.0, 3.5, 0.0), color: new Color(0.21, 0.21, 0.35) }], + camera: new Camera(new Vector(3.0, 2.0, 4.0), new Vector(-1.0, 0.5, 0.0)) + }; +} + +function exec() { + let rayTracer = new RayTracer(); + let sum = rayTracer.render(defaultScene(), 512, 512); + print("checksum:", sum, "low digits:", sum - 38837000); +} + + +function main() { + print("start..."); + exec(); + print("done."); +} diff --git a/tslang/docs/superpowers/plans/2026-10-21-own-raytrace.md b/tslang/docs/superpowers/plans/2026-10-21-own-raytrace.md new file mode 100644 index 000000000..8207d12a0 --- /dev/null +++ b/tslang/docs/superpowers/plans/2026-10-21-own-raytrace.md @@ -0,0 +1,414 @@ +# `-mm=own` compiles the ray tracer - Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** the ray tracer, with two small source edits, compiles and runs correctly under `-mm=own`, by closing nine compiler gaps as general rules. + +**Architecture:** every change is in the own analysis: cross-function facts in +`OwnershipSignaturePass.cpp`, per-function moves and borrows in `OwnershipInferencePass.cpp`, +shared predicates in `OwnershipFacts.h`, and release routines in +`include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h`. Each task is one PR off `main`, small +and squash-merged, in the order below. + +**Tech Stack:** C++17, MLIR/LLVM 22, tslang `ts` dialect, CMake/CTest, MSVC (Windows) and GCC (WSL). + +**Spec:** `tslang/docs/superpowers/specs/2026-10-21-own-raytrace-design.md` (approved 2026-10-21). +Background: `tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md` (§2 semantics, +§15 signature pass, §23 `Shared`, §24 assignment after a move). + +## Global Constraints + +- No unsound acceptance: when a rule cannot prove the block outlives every use, keep the error. +- No run-time counting under own outside `Shared`: every positive own test is also in the + `own_test` list, whose `test-own-no-counting-*` check fails on `__tslang_inc_ref|__tslang_dec_ref`. +- gc, rc and none compile and print exactly what they did; every positive test runs under gc, rc + and own (and none unless it allocates in a loop). +- Diagnostics keep their existing texts; new rejections reuse an existing text where one fits. +- Build: `cmake --build __build/tslang/windows-msbuild-2026-release --config Release --parallel 8` + (drop to `--parallel 4` if a `.td` changes). Tests: `cd __build/tslang/windows-msbuild-2026-release && ctest -j 16 -C Release` + (`-C Release` is required; without it nothing runs). Before a PR the full suite has no new + failures against `main`. +- Any change to what is released or freed also runs the own tests on Linux (the WSL build): + the Windows heap hides double frees. +- Teeth for every new acceptance rule: a temporary `getenv("OWN_TEETH")` switch that skips the + rule's safety condition must make the new positive test crash or print wrong under AOT + (`test-compile-own-*`), then the switch is removed. Commit before checking out anything. +- Test files go in `tslang/test/tester/own/`; positives join the `own_test` foreach in + `tslang/test/tester/CMakeLists.txt` (~line 3460) and get a gc/rc(/none) foreach of their own; + negatives join `own_error_cases` (~line 3624) as `"name|regex"` (no `;` in the regex). +- Each task appends a results section to `2026-09-24-own-memory-model-design.md` (§25, §26, ...). +- Commits are GPG-signed; on `gpg: signing failed: Timeout` run `gpg-connect-agent "scd killscd" /bye` + and retry. Never `--no-gpg-sign`. + +## Review Focus + +- An immortal global's field overwritten (`K.w.child = new C()`) must still free the old child, + and a borrow of `K.w.child` must still end there - Task 1, `own_err_immortal_field_overwritten`. +- A read after a move where the new owner is overwritten on one path only must stay an error - + Task 5, `own_err_read_after_move_overwritten`. +- A borrowing record stored into a field of a longer-lived object must stay an error - Task 7, + `own_err_borrowing_record_stored`. +- A literal argument given to a callee that keeps it must move (the caller must not free it) - + Task 3, `own_literal_moves` (`keepRay` case). +- An interface with an exported implementer must stay open (no facts) - Task 6, + `own_err_interface_exported_impl`. + +## Shared helpers + +Each repro below is minimal and is the test. `V` in the tests is: + +```ts +class V { + constructor(public x: number) {} + static plus(a: V, b: V) { return new V(a.x + b.x); } +} +``` + +Debugging recipe (not committed): `tslang --emit=mlir-affine --opt --opt_level=3 --no-default-lib -mm=own --mlir-print-ir-after-all --mlir-disable-threading file.ts 2> ir.txt` +shows the IR each pass gets; `--stack-promotion-report` is unrelated and stays off. + +--- + +### Task 1: Immortal globals (G1) + +**Files:** +- Modify: `tslang/lib/TypeScript/OwnershipFacts.h` (attr name + predicate) +- Modify: `tslang/lib/TypeScript/OwnershipSignaturePass.cpp` (`moveIntoGlobals`, ~line 118; `writtenGlobals`, ~line 160) +- Modify: `tslang/lib/TypeScript/OwnershipInferencePass.cpp` (`isImmortalLiteral`, ~line 3840; `reportShapeOfSecondReference`, ~line 4690) +- Modify: the own global-initializer lowering in `tslang/lib/TypeScript/LowerToLLVM.cpp` (`GlobalOpLowering`) +- Test: `tslang/test/tester/own/own_immortal_global.ts`, `own_err_global_returned.ts` (rewritten), `own_err_immortal_borrowed_init.ts`, `own_err_immortal_field_overwritten.ts` + +**Interfaces:** +- Produces: `#define OWN_IMMORTAL_GLOBAL_ATTR_NAME "__own_immortal"` (unit attr on `ts.Global`), and + `bool own_facts::isImmortalGlobalRead(mlir::Value value)` - true for a `ts.Load` of a + `ts.AddressOf` whose global carries the attr (looking through views as `rootOf` does). + Tasks 7 and 8 treat such a value as owning nothing. + +- [ ] **Step 1: Write the positive test** `own_immortal_global.ts`: + +```ts +// V as in the plan's shared helpers +class K { + static w = new V(1); + static d = K.w; // an immortal global from another + static f() { return K.w; } // returned +} +class H { v: V; constructor() { this.v = K.w; } } // stored into a field +function g(n: number) { return n > 1 ? K.w : new V(n); } // immortal or fresh +function main() { + const h = new H(); + print(K.f().x, K.d.x, h.v.x, g(2).x, g(0).x); // 1 1 1 1 0 + let s = 0; + for (let i = 0; i < 100000; i++) { s += g(i % 3).x; } // fresh ones are freed + print(s); // 66666 + assert(s == 66666); + print("done."); +} +``` + +- [ ] **Step 2: Write the negatives.** + - Rewrite `own_err_global_returned.ts`: keep its body and add `function reset() { V.def = new V(2); }` called from `main` after the print. A reassigned global stays mortal. Regex unchanged: `borrows the global 'V.def' and cannot be stored, returned or captured`. + - `own_err_immortal_borrowed_init.ts`: `class K { static list = [new V(1)]; static first = K.list[0]; }` and `function main() { print(K.first.x); }`. Regex: `the initializer of global 'K.first' takes a second reference`. + - `own_err_immortal_field_overwritten.ts`: `class N { constructor(public c: V) {} }`, `class K { static n = new N(new V(1)); }`, `function main() { const c = K.n.c; K.n.c = new V(2); print(c.x); }`. Regex: `borrows .* but is used here after .* released or overwritten`. +- [ ] **Step 3: Register** `own_immortal_global` in the `own_test` list and in a `foreach(mm rc gc none)` pair; add the three negatives to `own_error_cases`. +- [ ] **Step 4: Build and run** `ctest -C Release -R "immortal|global_returned"`. Expected: `own_immortal_global` FAILS under own with `borrows the global 'K.w'`; gc/rc/none PASS; the negatives PASS. +- [ ] **Step 5: Implement.** + - In `OwnershipSignaturePass`, after `writtenGlobals` is filled: a global gets `__own_immortal` when it is not in `writtenGlobals`, not exported, and its initializer region's stored value is fresh, holds no block, or is a load of another `__own_immortal` global (fixpoint over globals in module order). Run this before `moveIntoGlobals`, which treats a load of an immortal global like `isConstantData`. + - In inference, `isImmortalLiteral(value)` also returns `isImmortalGlobalRead(value)`; `reportShapeOfSecondReference`'s global branch is then unreachable for immortal globals. + - Lowering, own only: when a `ts.Global` with `__own_immortal` stores a heap block, write `HEAP_BLOCK_IMMORTAL` into its header (the same header word `LLVMCodeHelperBase` writes for literals, ~line 290), so every release skips it at run time. +- [ ] **Step 6: Run** `ctest -C Release -R "immortal|global_returned|own"`. Expected: all PASS, `test-own-no-counting-own_immortal_global` PASS. +- [ ] **Step 7: Teeth:** with `OWN_TEETH` set, skip the header store; `test-compile-own-own_immortal_global` must fail (the first release frees `K.w`). Remove the switch. +- [ ] **Step 8: Full suite, WSL own tests, spec §25, commit, PR** ("-mm=own: a global never reassigned is immortal"). + +### Task 2: `Shared` of an object literal (G4) + +**Files:** +- Modify: `tslang/lib/TypeScript/OwnershipSignaturePass.cpp` (`moveIntoGlobals`: `touchesHandle`, `isFresh`) +- Modify: `tslang/lib/TypeScript/OwnershipInferencePass.cpp` (the handle construction path of §23.2) +- Test: `tslang/test/tester/own/own_shared_literal.ts` + +**Interfaces:** +- Consumes: nothing new. Produces: none. + +- [ ] **Step 1: Write the test** `own_shared_literal.ts`: + +```ts +interface S { f: (x: number) => number; roughness: number; } +let g = new Shared({ f: function (x: number) { return x * 2; }, roughness: 2 }); +function main() { + let l = new Shared({ f: function (x: number) { return x + 1; }, roughness: 3 }); + const m = l; + print(g.value.f(3), g.value.roughness, l.value.f(3), m.value.roughness); // 6 2 4 3 + print("done."); +} +``` + +- [ ] **Step 2: Register** in `own_test` and a `foreach(mm rc gc none)`. Run `ctest -C Release -R own_shared_literal`. Expected: own FAILS (`the initializer of global 'g' takes a second reference`, and the local's `borrows a field`); gc/rc/none PASS. +- [ ] **Step 3: Implement:** a boxed object literal (the `ts.NewInterface`/box of a folded literal, which §18.2 treats as a view) given to the `Shared` constructor moves in exactly as `new C()` does - in `moveIntoGlobals` (the retain is the handle's own) and in the inference's handle construction. +- [ ] **Step 4: Run** `ctest -C Release -R "own_shared|shared"`. Expected: PASS, including the existing `Shared` tests. +- [ ] **Step 5: Teeth** (skip the move: the literal is freed with the frame while the handle points at it), full suite, WSL, spec §26, commit, PR. + +### Task 3: Owning literals moved anywhere (G3) + +**Files:** +- Modify: `tslang/lib/TypeScript/OwnershipInferencePass.cpp` (`recordRetainOf`, `checkValueMoves` ~line 3996, the literal branch of `reportShapeOfSecondReference` ~line 4792) +- Modify: `tslang/lib/TypeScript/OwnershipFacts.h` if the "owning record" predicate moves there +- Test: `own_literal_moves.ts`; `own_err_record_literal_given.ts` becomes the positive `own_record_literal_given.ts`; new negative `own_err_literal_moved_twice.ts` + +**Interfaces:** +- Produces: `bool own_facts::isOwningRecord(mlir::Value value)` - an object literal (or its interface view) whose fields each are fresh, a moved local, or hold no block. Task 7 builds the borrowing variant beside it. + +- [ ] **Step 1: Write the positive test** `own_literal_moves.ts`: + +```ts +// V as in the plan's shared helpers +interface Ray { start: V; dir: V; } +interface L { pos: V; } +interface Sc { ls: V[]; c: V; } +class Keeper { rays: Ray[] = []; } +function len(r: Ray) { return r.start.x + r.dir.x; } +function keepRay(k: Keeper, r: Ray) { k.rays.push(r); } +function mk(): Sc { return { ls: [new V(1), new V(2)], c: new V(3) }; } +function main() { + const p = new V(1); + const d = new V(2); + print(len({ start: p, dir: d })); // 3 (p, d move in) + const q = new V(1); + print(len({ start: V.plus(q, q), dir: q })); // 3 (fresh + moved) + const ls: L[] = [{ pos: new V(1) }, { pos: new V(2) }]; + const r: Ray = { start: new V(4), dir: new V(5) }; + const s = mk(); + print(ls[1].pos.x, r.dir.x, s.c.x, s.ls[1].x); // 2 5 3 2 + const k = new Keeper(); + for (let i = 0; i < 1000; i++) keepRay(k, { start: new V(i), dir: new V(1) }); + print(k.rays.length, k.rays[999].start.x); // 1000 999 + print("done."); +} +``` + +- [ ] **Step 2: Negatives.** Move `own_err_record_literal_given.ts` to `own_record_literal_given.ts` (positive, prints `1`) and drop its `own_error_cases` line. Add `own_err_literal_moved_twice.ts`: `const p = new V(1); const k = new Keeper(); keepRay(k, { start: p, dir: new V(2) }); keepRay(k, { start: p, dir: new V(3) });`. Regex: `is used here after its value was moved`. +- [ ] **Step 3: Register; run** `ctest -C Release -R "literal_moves|record_literal|literal_moved_twice"`. Expected: positives FAIL under own with `an object literal that holds a value it owns`; gc/rc/none PASS. +- [ ] **Step 4: Implement:** an owning record is a value like a fresh object - it may move into a call argument (the caller releases it after a borrowing callee; a keeping callee takes it), an array literal element, a nested literal field, a local through an interface view, or the result. A moved local inside it is moved at the literal (use after = existing use-after-move error). A field read (`this.pos`) inside it is left to Task 7: until then such a literal is accepted only as a call-argument temporary. +- [ ] **Step 5: Run** the same filter plus `-R own`. Expected: PASS. Teeth (skip releasing the argument record: rc/own leak shows nothing - use the double-free direction instead: release `p` after the call too; AOT must crash). Full suite, WSL, spec §27, commit, PR. + +### Task 4: A field read into a `let` (G6) + +**Files:** +- Modify: `tslang/lib/TypeScript/OwnershipInferencePass.cpp` (`decideSlot` ~line 1887 and the "takes a second reference" path through `reportSecondReference` ~line 4655) +- Test: `own_field_chain_let.ts` + +**Interfaces:** none new. + +- [ ] **Step 1: Write the test** `own_field_chain_let.ts`: + +```ts +// V as in the plan's shared helpers +interface R { dir: V; } +interface Is { ray: R; dist: number; } +function f(r: R) { let d = r.dir; let p = V.plus(d, d); return V.plus(p, d); } +function sh(i: Is) { let d = i.ray.dir; let pos = V.plus(d, i.ray.dir); return V.plus(d, pos); } +function main() { + const r: R = { dir: new V(1) }; + const i: Is = { ray: { dir: new V(2) }, dist: 3 }; + print(f(r).x, sh(i).x); // 3 6 + print("done."); +} +``` + +- [ ] **Step 2: Register; run.** Expected under own: FAIL with `takes a second reference` (after Task 3, the literals themselves pass). +- [ ] **Step 3: Implement:** a `let` initialized from a field read of a parameter (through any chain of field reads and interface views) borrows, bounded by that parameter, and a call ends it only when the call's facts say it may drop (as `own_err_param_read_call` already pins). +- [ ] **Step 4: Run** `ctest -C Release -R "field_chain_let|field_let|param_read_call"`. Expected: PASS (the existing negatives still fail as they should). Teeth, full suite, spec §28, commit, PR. + +### Task 5: Read after a move while the new owner holds it (G7) + +**Files:** +- Modify: `tslang/lib/TypeScript/OwnershipInferencePass.cpp` (`checkSlotMove` ~line 4400, `reportUseAfterMove` ~line 4983) +- Test: `own_read_after_move.ts`; `own_err_field_alias.ts` and `own_err_union_moved.ts` rewritten; new `own_err_read_after_move_overwritten.ts` + +**Interfaces:** +- Produces: in the inference, a use after a move is a **read** when it is a field/element load, a + call argument the callee borrows (signature facts), a comparison, or a method call through it + whose facts do not keep it. Any other use (a store, a `let` binding, a keeping call, a return) is + still a second owner. + +- [ ] **Step 1: Write the positive test** `own_read_after_move.ts`: + +```ts +// V as in the plan's shared helpers +class N { v: number[] = []; } +class C { x: number = 5; } +class H { c: C | null = null; } +function main() { + const a: number[] = [1]; + const n = new N(); + n.v = a; + print(a.length); // 1 + const h = new H(); + const c = new C(); + h.c = c; + print(c.x); // 5 + const vs = [new V(1), new V(3), new V(2)]; + let best: V = undefined; + let m = 0; + for (let i in vs) { + let v = V.plus(vs[i], vs[i]); + if (v.x > m) { best = v; m = v.x; } // read after the move into best + } + print(best.x); // 6 + print("done."); +} +``` + +- [ ] **Step 2: Negatives.** `own_err_field_alias.ts`: insert `n.v = [2];` between `n.v = a;` and `print(a.length);`. `own_err_union_moved.ts`: insert `h.c = null;` before `print(c.x);`. Regex for both becomes `is used here after its value was moved`. New `own_err_read_after_move_overwritten.ts` (overwritten on one path only): + +```ts +class C { x: number = 5; } +class H { c: C | null = null; } +function main() { + const h = new H(); + const c = new C(); + h.c = c; + if (c.x > 1) { h.c = null; } + print(c.x); +} +``` + +Regex: `is used here after its value was moved`. Unchanged and still failing: `own_err_param_kept_used`, `own_err_push_let`, `own_err_two_owners`, `own_err_literal_two_records`. +- [ ] **Step 3: Register; run.** Expected: positive FAILS under own (`is used here after its value was moved`). +- [ ] **Step 4: Implement:** in `checkSlotMove`/the move check, a read (definition above) after a move into a destination that is a local slot, a field of a local or of `this`, or an element store in this function is a borrow through that destination, sound when no path from the move to the read overwrites, releases or moves the destination (reuse the kill walk `readUsedAfter` does for borrows). A move into a call (kept parameter) or `push` is not such a destination. +- [ ] **Step 5: Run** `ctest -C Release -R "read_after_move|field_alias|union_moved|param_kept_used|push_let|two_owners|literal_two_records"`. Expected: PASS. Teeth (drop the kill walk: the overwritten negative compiles and its AOT run reads freed memory). Full suite, WSL, spec §29, commit, PR. + +### Task 6: Facts through a module-private interface (G9) + +**Files:** +- Modify: `tslang/lib/TypeScript/OwnershipSignaturePass.cpp` (`collectClassVTables` ~line 1127, `families`, `computeFacts` ~line 1741, `meetFacts` ~line 1790) +- Test: `own_interface_facts.ts`, `own_err_interface_exported_impl.ts` + +**Interfaces:** +- Produces: a call through an interface vtable slot (`...vtbl`) is resolved to the + family of every implementation of that slot when the interface is closed; it then carries the + meet of their facts (`__own_no_drops`, `__own_params`, `__own_result_borrows`) like a private + virtual call. Task 7 relies on `__own_result_borrows` arriving on such calls. + +- [ ] **Step 1: Write the test** `own_interface_facts.ts`: + +```ts +// V as in the plan's shared helpers +interface Th { area(v: V): number; } +class A implements Th { area(v: V) { return v.x; } } +class B implements Th { area(v: V) { return v.x * 2; } } +class Sc { ts: Th[] = [new A(), new B()]; } +function tot(sc: Sc, r: V) { + let best = 0; + for (let i in sc.ts) { let a = sc.ts[i].area(r); if (a > best) best = a; } + return best + r.x; +} +function main() { print(tot(new Sc(), new V(3))); print("done."); } // 9 +``` + +- [ ] **Step 2: Negative** `own_err_interface_exported_impl.ts`: the same program with `export class B`. Regex: `borrows .* but is used here after it may be released or overwritten`. +- [ ] **Step 3: Register; run.** Expected: positive FAILS under own with `borrows an element but is used here after it may be released or overwritten`. +- [ ] **Step 4: Implement:** an interface is closed when it is not exported, and every class vtable `...vtbl` and every object literal implementing it is in this module with no exported implementer. Group interface vtable entries into families by `(interface, position)`; a call through such a slot gets the family meet; if any member is open, the call stays open. +- [ ] **Step 5: Run** `ctest -C Release -R "interface"`. Expected: PASS, existing interface tests unchanged. Teeth (give the call `__own_no_drops` when one implementation drops: an implementation that releases its argument's field must crash the AOT run). Full suite, WSL, spec §30, commit, PR. + +### Task 7: Borrowing records (G2) + +**Files:** +- Modify: `tslang/lib/TypeScript/OwnershipSignaturePass.cpp` (result facts; `__own_result_borrows` becomes a set) +- Modify: `tslang/lib/TypeScript/OwnershipFacts.h` (`resultBorrows`, `borrowedParam` ~line 752-808) +- Modify: `tslang/lib/TypeScript/OwnershipInferencePass.cpp` (records with borrowed fields; bounded uses) +- Modify: `tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h` (release of a record skips its borrowed fields, chosen at the creation site) +- Test: `own_borrowing_record.ts`, `own_err_borrowing_record_stored.ts`, `own_err_borrowing_record_outlives.ts` + +**Interfaces:** +- Consumes: Task 3 `isOwningRecord`; Task 6 facts on interface calls; Task 1 `isImmortalGlobalRead`. +- Produces: `__own_result_borrows` as an `ArrayAttr` of argument indices (receiver = 0 for + methods); `llvm::SmallVector own_facts::resultBorrowsAll(mlir::Operation *call)`, with the + existing single-index `resultBorrows` kept as a wrapper returning the first or -1 until no caller + needs it. `@lends` accepts several names. + +- [ ] **Step 1: Write the positive test** `own_borrowing_record.ts`: + +```ts +// V as in the plan's shared helpers +interface Ray { start: V; dir: V; } +interface Hit { thing: Th; ray: Ray; dist: number; } +interface Th { hit(ray: Ray): Hit; k: number; } +class A implements Th { k = 1; hit(ray: Ray): Hit { return { thing: this, ray: ray, dist: ray.dir.x }; } } +class B implements Th { k = 2; hit(ray: Ray): Hit { return ray.dir.x > 5 ? null : { thing: this, ray: ray, dist: ray.dir.x * 2 }; } } +class Sc { ts: Th[] = [new A(), new B()]; } +function closest(ray: Ray, sc: Sc) { + let best = +Infinity; + let bestHit: Hit = undefined; + for (let i in sc.ts) { + let h = sc.ts[i].hit(ray); + if (h != null && h.dist < best) { bestHit = h; best = h.dist; } + } + return bestHit; +} +function main() { + const sc = new Sc(); + let s = 0; + for (let i = 0; i < 1000; i++) { + const h = closest({ start: new V(0), dir: new V(i % 10) }, sc); + s += h.thing.k + h.dist; + } + print(s); // 5500: A always wins (dist d against B's 2d), k 1 + assert(s == 5500); + print("done."); +} +``` + +(Before implementing, confirm 5500 under rc and gc; if it differs, fix the expected value from the +rc run, not the code.) + +- [ ] **Step 2: Negatives.** `own_err_borrowing_record_stored.ts`: `class Keep { h: Hit; }` and `keep.h = sc.ts[0].hit(ray)` where `keep` is a global. Regex: `borrows the result of .* and cannot be stored, returned or captured`. `own_err_borrowing_record_outlives.ts`: `let h = a.hit(ray); a = new A(); print(h.thing.k);` with `a` a `let`. Regex: `borrows the result of .* but is used here after it may be released or overwritten`. + Add a two-name case to `own_lends.ts`: an exported `@lends("a", "b")` function returning `{ a: a, b: b }`, used while both live. +- [ ] **Step 3: Register; run.** Expected: positive FAILS under own (`an object literal that holds a value it owns` at the `return`). +- [ ] **Step 4: Implement:** a returned literal whose fields are `this`, a parameter, or field reads of them (and possibly fresh values) makes the function's result borrow that set; the record's release frees only its fresh fields (per creation site: the box carries its own routine). At the caller a borrowing result is a borrow of each argument in the set, merged and returned as plain borrows are (`closest` returns a borrow of `ray` and `sc`). +- [ ] **Step 5: Run** `ctest -C Release -R "borrowing_record|borrowed_result|lends|own"`. Expected: PASS. Teeth (release the borrowed fields too: AOT double free). Full suite, WSL, spec §31, commit, PR. + +### Task 8: `reduce`, and a parameter returned on some paths (G5) + +**Files:** +- Modify: `tslang/lib/TypeScript/OwnershipSignaturePass.cpp` (a parameter returned on some paths and replaced by a fresh value on others is kept: `__own_params`) +- Modify: `tslang/lib/TypeScript/OwnershipInferencePass.cpp` (`otherReturn`, the mixed-return report ~line 4696) +- Modify: the `___array_reduce` generator in MLIRGen if its accumulator is not already a moved local (find it with `grep -rn "___array_reduce" tslang/lib`) +- Test: `own_reduce.ts`; `own_err_mixed_return.ts` rewritten + +**Interfaces:** +- Consumes: Task 1 (immortal start value `Color.defaultColor`). + +- [ ] **Step 1: Write the test** `own_reduce.ts`: + +```ts +// V as in the plan's shared helpers +class K { static zero = new V(0); } +function pick(acc: V, v: V) { return v.x > 2 ? acc : V.plus(acc, v); } // returns acc on one path +function main() { + const a = [new V(1), new V(2), new V(3)]; + const r = a.reduce((acc: V, v: V) => V.plus(acc, v), new V(0)); + const q = a.reduce(pick, K.zero); + print(r.x, q.x); // 6 3 + print("done."); +} +``` + +- [ ] **Step 2: Negative.** `own_err_mixed_return.ts`: change `main` to `const c = new C("a"); print(pick(c, true).n, c.n);` (the caller still uses `c`, so it cannot give it up). Regex: `is used here after its value was moved`. +- [ ] **Step 3: Register; run.** Expected: positive FAILS under own (`cannot make the callers of '___array_reduce<...>' give it up`). +- [ ] **Step 4: Implement:** when a function returns a parameter on some paths and another value on others, and every caller can move the argument in, the parameter is kept (`__own_params`): the returning path moves it out, the others release it. `___array_reduce` moves the initial value into its accumulator, gives the accumulator to the callback (kept), and takes the result as the new accumulator. +- [ ] **Step 5: Run** `ctest -C Release -R "reduce|mixed_return|param_kept"`. Expected: PASS. Teeth, full suite, WSL, spec §32, commit, PR. + +### Task 9: The ray tracer under own (G8 and the deliverable) + +**Files:** +- Create: `tslang/test/tester/own/own_raytrace.ts` (the own copy at 64x64) +- Create: `tslang/benchmarks/raytrace/rtbench_own.ts` (after PR #634 merges) +- Modify: `tslang/benchmarks/raytrace/bench.ps1`, `tslang/benchmarks/raytrace/README.md`, `README.md` (Performance) + +- [ ] **Step 1: Write the test** `own_raytrace.ts`: `tslang/benchmarks/raytrace/rtbench.ts` (from PR #634) with the two source edits of the spec (`Shared`; `Plane.normal` returns `new Vector(this.norm.x, this.norm.y, this.norm.z)`), rendering 64x64, and `exec` ending in `print("checksum:", sum); assert(sum == 606318);`. +- [ ] **Step 2: Register** in `own_test` and `foreach(mm rc gc)`; run. Expected: gc/rc PASS. Under own, list every remaining error. +- [ ] **Step 3: For each remaining error:** reduce it to a minimal repro in `own/`, decide whether an earlier task's rule should have covered it (fix that rule) or it is a new gap (add the rule here with its own test and negative). No source edits beyond the two in the spec. +- [ ] **Step 4: Run** `ctest -C Release -R own_raytrace`. Expected: PASS under own, gc and rc; `test-own-no-counting-own_raytrace` PASS. +- [ ] **Step 5: Benchmark:** `rtbench_own.ts` = the test at 512x512 with `low digits: 369`; `bench.ps1` builds it with `--mm=own` (with and without stack promotion) and replaces the `tslang own does not compile` rows; record peak working set as the README does. Update both READMEs' tables and drop "does not compile". +- [ ] **Step 6: Full suite, WSL, spec §33 (results: errors left 0, time, memory), commit, PR.** 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 8e03e90eb..a3aa8f239 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 @@ -1626,7 +1626,7 @@ place, so all four places that report it get the same names. Each message has a | a global's value, stored, returned or boxed | `'x' borrows the global 'g' and cannot be stored, returned or captured` | `own_err_global_returned` | | a global's value read into a local | `'x' borrows the global 'g', but -mm=own cannot prove the global keeps its value while 'x' is used: a call or an assignment may overwrite it` | `own_err_global_let_call` | | a captured variable's value, through its cell | `'x' borrows a captured variable and cannot be stored, returned or captured` | `own_err_captured_returned` | -| an object literal holding a value it owns, given anywhere but an owning local or the result | `an object literal that holds a value it owns is moved only into a local or returned as it is; -mm=own cannot move it here yet` | `own_err_record_literal_given` | +| an object literal holding a value it does not own outright: a literal nested by value, or one holding a borrowed or `let` field when it is not seen as an interface (§32; since §27, before which any literal given anywhere but an owning local or the result was one) | `an object literal that holds a value it owns is moved only into a local or returned as it is; -mm=own cannot move it here yet` | the interface-typed shapes are borrowing records (§32): `own_err_borrowing_record_*`, `own_literal_moves`, `own_borrowing_record_arg`; `own_err_record_literal_let` (§27) is gone | The mixed-return message is the largest bucket (17 files). Most are two synthesized helpers: - `___unbox` (`` of an `any`) returns the box's own string when it holds one, and a @@ -2789,3 +2789,1540 @@ used `let b = a; … a = new C()` to make `b` a borrow, but that is a move now. after `let b = a`, which keeps `b` a borrow. `own_move_reassign` runs the shapes under own, rc and gc: own holds 4.4 MB, as rc's 4.5 MB. On a quarantine heap it runs clean; with the assignment's release kept, it is a double free. Corpus: 467 of 628 compile under own, unchanged. + +## 25. Immortal globals (G1 of the ray tracer plan), 2026-10-21 + +A read of a global was a borrow that could not be stored, returned or captured (§21.2): the global +owns its value, and a call or an assignment may give it another. The ray tracer's `Color.white`, +`Color.black` and `Color.background = Color.black` are never given another value, and are returned +and stored everywhere. Such a global is now **immortal**, like a string literal: its block's header +is `HEAP_BLOCK_IMMORTAL`, so every release skips it, and a read of it owns nothing and may go +anywhere - returned, stored into a field or another global, given to a call that keeps it, merged +with a fresh value (`n > 1 ? K.w : new V(n)` is an owned result on both paths; the caller's release +of `K.w` does nothing). + +- **Which globals.** OwnershipSignaturePass (`findImmortalGlobals`) marks a `ts.Global` + `__own_immortal` when it is of a class type that owns a heap block, is not exported (no `export`, + `import`, `dllexport` or `dllimport`), and is written once, by its initializer. The initializer is + either the global's own region (`const c = new V(1)`: every `ts.AddressOf` of it only loaded), or, + for a class static, its region yields only the default and the one `ts.Store` through a + `ts.AddressOf` of it is in a static constructor a `ts.GlobalConstructor` runs. Every symbol use of + the global is checked (`SymbolTable::getSymbolUses`), not only `ts.AddressOf`: a static is also + reached through its class's vtable, and `k.w = x` with `k: typeof K` writes it through a + `ts.VirtualSymbolRef` that `writtenGlobals` never sees. When any vtable reference is written + through, no static is immortal. A module-level `export let`, an exported class's static, and a + global assigned anywhere else stay mortal. Top-level script code is itself a function a + `ts.GlobalConstructor` runs, so its one straight-line store into a global (`let c: V;` then + `c = new V(1)` at the top level, or `K.w = new V(1)`) is accepted like a static constructor's. + The store must run once (fix-up of 2026-10-23, below): one in a loop op, or in a block on a cycle + of its function's CFG, leaves the global mortal. +- **Which values.** The initializer gives it an object made right there (`ts.New`, an owning + literal; none it gives back with a `ts.Release`), nothing, or another immortal global's value + (found to a fixpoint). A call's result never qualifies, whatever its callee: the pass decides + before it knows which callees return a borrow, and a global's region is no function the + inference checks. So `static w = id(new V(1))`, `const c = id(new V(1))` and + `const c = firstOf(list)` stay mortal and keep today's errors (`own_err_immortal_region_call`, + `own_err_immortal_region_borrow`), as does a borrowed initializer (`static first = K.list[0]`, + `own_err_immortal_borrowed_init`). Marking a block someone else owns immortal would turn its + owner's free into a silent leak; marking a temporary the region frees would write the header + into a freed block, and the global would hold it. Since main's #638 (merged into this branch + after T8) a global's region calls get their callees' facts (`ownGlobalInitializerCalls`), and a + region that holds a call's borrowing result is rejected there first ("the initializer of global + 'c' holds the result of 'id', which borrows its argument"); `own_err_immortal_region_call` now + pins that message. They stay mortal all the same: `findImmortalGlobals` runs first and still + refuses them. `own_err_immortal_region_borrow` (since the fix-up of 2026-10-23) calls through a + function value instead, `const c = pick()(list)`, whose callee #638 cannot know, so it again shows + this refusal: `f`'s `return c` is "borrows the global 'c' and cannot be stored, returned or + captured". + Main's two other global checks step aside for an immortal global's read: a global may be given + one (#636, `let e = c`), and one may be lent to a call that may drop (#639; a static's one store + in its static constructor is in `writtenGlobals`, so `lend(b, K.w)` was rejected after the merge + - `own_immortal_global` now runs it). +- **Only the global's own block.** `K.n.c = x` still destroys the old `c`, and a borrow of `K.n.c` + stays bounded (`own_err_immortal_field_overwritten`). +- **Inference.** `own_facts::isImmortalGlobalRead` is a `ts.Load` of a `ts.AddressOf` the pass + marked, seen through views. `isImmortalLiteral` includes it, and its retains (a `ts.Retain`, or a + `ts.RetainSlot` where the slot still holds it) are erased, as a constant's are. `moveIntoGlobals` + erases a global region's retain of one. +- **Lowering.** The initializing `ts.Store` carries `__own_immortal`; under own its lowering writes + `HEAP_BLOCK_IMMORTAL` into the block's header when the value is not null. A region that gives + the global its value is always made a `__cctor` (it would otherwise become a constant + initializer, with no store), and the store the `__cctor` makes is marked from the global's + attribute. A static's region, which yields only the default, is left alone: a `__cctor` of it + would run after the static constructor and put the default back. So every block the global ever + holds is one no release frees, whatever reads of it are taken to own; the value test makes it a + block that was the global's to keep. + +Not fixed here, and older than this section: a mortal global whose region stores a call's result +that borrows a temporary the region frees (`export const c = id(new V(1))`, or `const c = ...` +read only in place) holds a freed block, and a read of it in place is accepted. The region is no +function, and the call's facts are not pinned there. + +`own_immortal_global` runs the static shapes (from another global, returned, stored into a field, +merged with a fresh value, in a loop of 100000) and `own_immortal_global_region` the module-level +ones (`const c = new V(1)`, `let e = c`), under own, rc, gc and none; own emits no counting. +`own_err_global_returned` and `own_err_param_kept_not_owned` now also give the global another +value, which keeps them errors: their programs as they were are accepted. (Since the merge of main, +`own_err_param_kept_not_owned`'s `keep` pushes instead of storing into a field: a keeper that +drops is rejected at the call by #639 before phase 4's message.) The brief's text for +`own_err_immortal_borrowed_init` (`the initializer of global 'K.first' takes a second reference`) +is moveIntoGlobals', which only a region initializer reaches; a static's borrowed initializer is +reported by inference in its static constructor, as before (`borrows 'K.list[]' and cannot be +stored, returned or captured`). Teeth: with the header store skipped, the AOT and JIT runs of both +fail with `0xC0000374` (heap corruption: the first release frees `K.w`, and later reads and +releases use the freed block). With the loop at 30 million (20 million fresh `V`s freed), own +peaks at 4.3 MB, as rc does. + +**Fix-up, 2026-10-23: the store must run once.** "Written once" counted store ops, not how often +they run. A static's one store in a loop of its static block (`static { for (1e6) K.w = new +V(i + 1); }`), or a global's in a loop of top-level script code, marked every block it was given +immortal, and the `ts.ReleaseSlot` before each store skipped the old one: own peaked at 589 MB for +a million `V`s of 64 numbers (static block), 602 MB (`let c: V;` written in a top-level loop) and +600 MB (`K.w` written in a top-level loop), rc at 4.4-4.5 MB. A leak, not a use-after-free: no block +was freed early. `findImmortalGlobals` now refuses a store that may run again (`mayRunAgain`: an +enclosing loop op, kept for safety, or a block on a cycle of its region's CFG, which is what the +affine lowering leaves of every loop). Such a global is mortal: each store releases the value it +replaces, and the three programs peak at 4.4 MB under own. A store in an `if` still runs once and +is still accepted. What only compiled because the global was immortal is now an error, as for any +mortal global: `let e = c` after the top-level loop ("borrows the global 'c' and cannot be stored, +returned or captured", #636), and `let e = K.w` after the static block loop ("the initializer of +global 'e' reads a value another owner keeps"). + +`own_global_loop_store` writes `K.w` in a loop of its static block, `S.w` in a `while` of +top-level code and `T.w` once in an `if` of its static block (returned from `t()`, which only an +immortal global allows), 10000 rounds, under own, rc, gc and none, AOT and JIT; own emits no +counting. `test-own-global-loop-store-mortal` checks its `--emit=mlir-affine`: `T.w`'s +`ts.AddressOf` is marked `__own_immortal`, `K.w`'s and `S.w`'s are not. The module-level form +`let c: V;` is not in the test: a `let` with no initializer warns. Negatives +`own_err_global_loop_store_static` (`return K.w` after a static block loop) and +`own_err_global_loop_store_script` (`let e = K.w` after a top-level loop), "borrows the global +'K.w' and cannot be stored, returned or captured". Teeth (a temporary `OWN_TEETH` switch skipping +`mayRunAgain`, removed): the mortal check fails (`K.w`/`S.w` marked) and both negatives compile. + +**The other write guards (final review I4, 2026-10-23).** A global wrongly marked immortal and +later written frees what its reads were taken to own nothing of - a use-after-free, not a leak - +so each guard of "written once, by its initializer" has a program of its own, checked the same +way (`test-own-global-mortal-*`, `--emit=mlir-affine`: `M`'s `ts.AddressOf` is not marked +`__own_immortal`, `I`'s is): `own_global_mortal_export` (`export let M` and an exported class's +static `MK.m`), `own_global_mortal_typeof` (`k.w = I` with `k: typeof M`: the vtable-write guard, +the only one that reads a `ts.VirtualSymbolRef`'s users; a write through `typeof K` is that route, +not another), `own_global_mortal_static_twice` (two stores in one static block) and +`own_global_mortal_static_method` (`this.w = new V(3)` in a static method, the static's only +store: a function no `ts.GlobalConstructor` runs). Each program has one guard between `M` and +immortality. Teeth (one temporary `OWN_TEETH` switch per guard, removed): skipping the `export` +test, `vtableWritten`, the second-store test (`initStore`) or the static-constructor test each +makes its own check fail (`M`, `MK.m` / `M.w` marked) and leaves the other three passing. The +typeof write of a fresh value (`k.w = new V(3)`) is not used: it is an error today (`'this value' +takes a second reference`), and `k.w = M.w` reaches checkGlobalRead's keep backstop. + +**A `const` read of a mortal global ends at its assignment (fix-up review C1, 2026-10-23).** Making +those globals mortal exposed an older hole, on main too: `const c = G` names the read itself (a +`ts.Load` of the global's `ts.AddressOf`, `__owned_result_named`, with no retain), which no check +bounded - `let c = G` is a borrowing local, rejected, but the `const` was neither a place read nor +a local. So `const c = K.w; K.w = new V(i + 1); print(c.x)` in a static block's or top-level code's +loop compiled and, now that the store frees the old block, read freed memory (exit 139 on Windows +and Linux); before the fix-up it leaked instead. The same in any function (`const c = G; G = new +V(1); print(c.x)`) read freed memory on main. OwnershipInferencePass now checks each read of a +mortal global (`isGlobalRead`: not immortal, owns a heap block, not a handle; `checkGlobalRead`): +its uses, and the uses of what it produces (walkBorrowed, made again at the read), must not come +after a `ts.Store` or `ts.ReleaseSlot` of that global in the same function ("'this value' borrows +'G' but is used here after 'G' is released or overwritten": the name is the read's, as in the +other own diagnostics, and `ownerName` finds a variable's name only with `--di`). Where the walk +meets a keep, the read is followed into what holds it or rejected (both below). Not here: a call that assigns the global (`const c = G; reset(); print(c.x)`, still accepted; #639 +covers such a call only when the read is its argument), and the same with a local +(`g(n, n = mk(2))`, still accepted, and wrong at run time under own before and after). A `const` +of an owning local (`const c = n; n = mk(5); print(c.x)`) is a move, which is sound. Tests: +`own_err_global_const_store_static`, `own_err_global_const_store_script` and +`own_err_global_const_store` (runnable programs; gc prints `1 64 ... 5`). Tooth (a temporary +`OWN_TEETH` switch skipping the check, removed): the three compile, and the built programs exit +139 under `MALLOC_PERTURB_` on Linux. Newly rejected: only programs of this shape; the corpus +keeps its 526 files. + +**An object literal that holds such a read is bounded by the global too (fix-up re-review N1, +2026-10-23).** An object literal built in a local (`const o = { v: c }`, `{ v: K.w }`, +`{ p: { v: K.w } }`, `const { v } = { v: G }`) takes the read with no retain: a `ts.Store` into a +`ts.PropertyRef` of the local's value tuple, or the local's initializer. walkBorrowed took that +store for a keep, and checkGlobalRead ended there, so `o.v.x` after `K.w = new V(i + 1)` read the +freed block (exit 139 on Windows and Linux; BASE leaked instead, and the module-global form +`const o = { v: G }; G = new V(1)` read freed memory on main). The literal holds the borrow, not +a second owner, so checkGlobalRead now follows it (`recordLocalHolding`): every use of the local - +its fields' reads, a copy (`const o = b`), a spread (`{ ...b }`, through `ts.DeconstructTuple`), +a destructuring - and what those lead to are uses of the read, made again where the local is +declared. A value made from the borrow and stored into another such local makes that local a +holder too. Tests: +`own_err_global_record_static` (the static block's loop) and `own_err_global_record` (a module +global in a function), runnable, gc printing `1 64 ... 5` and `0 64 ... 5`. Tooth (a temporary +`OWN_TEETH` switch ending the follow at once, removed): both compile, and the built programs exit +139 on Windows and under `MALLOC_PERTURB_` on Linux. The ray tracer is unchanged: the globals its +records read (`Color.*`) are immortal and its `Surfaces.*` are handles, which this check skips. + +**Every keep is followed or rejected (fix-up re-review N2, 2026-10-23).** A `let` that takes the +value back out of such a literal (`const o = { v: K.w }; let l = o.v`, `let l: V = o.v`, +`let l = o["v"]`, `let l = o.v`, an interface-typed `let`, `const p = o.v; let l = p`, +`let { v } = o`, `let { v } = { v: K.w }`) was still a keep that ended the walk in silence, and the +`let` itself passes as a borrowing `let` of `o`'s field, bounded by `o`, not by the global: `l.x` +after `K.w = new V(i + 1)` read the freed block (exit 139 on Windows and Linux; the module-global +form on main too). So checkGlobalRead no longer lets a keep go. Each keep the walk meets is +either: +- followed as a holder (`borrowHolderOf`): a local - a `let`, a `const`, an object literal built in + place, a destructured local - declared from the read, assigned it, or given it in a field of its + value tuple at any depth. Its uses under the field path that holds the borrow, and what they lead + to, are uses of the read, made again only where the local is given a new value (its declaration + when it has an initializer, and a store into that path or a prefix of it; the paragraphs + below); its own retain and releases + are its bookkeeping, which the owner checks decide. A sibling field never given the borrow + (`{ v: K.w, z: Z.z }` with only `o.z` read later) is not followed. A tuple taken apart + (`ts.DeconstructTuple`) is followed field by field, and a value made from the borrow that + declares or is stored into another local makes it a holder too. rc's temporary release of a + call's result that borrows an argument goes with that result, as in checkPlaceRead; +- or rejected: "the value read from 'G' is kept where its end at an assignment of 'G' cannot be + checked; -mm=own cannot prove it outlives the global's value" - a field of an object, an array, + a global, a callee that keeps it, a capture, a return. A string read is copied where it is kept + (§22), so its keeps are not this check's. + +Tests: `own_err_global_let_holder_static` (a static block's loop), `own_err_global_let_destructure` +(`let { v } = { v: K.w }` in a top-level loop) and `own_err_global_let_holder` (a module global), +runnable, gc printing `1 64 ... 5` and `0 64 ... 5`. Tooth (a temporary `OWN_TEETH` switch that +lets a keep go and follows no plain local, removed): the three compile, and the built programs +exit 139 on Windows and under `MALLOC_PERTURB_` on Linux. Newly rejected beside them: the unsafe +shapes, and one correct one - a spread of a holder (`const o = { v: K.w, z: Z.z }; const q = +{ ...o }`, then `q.z` read after the store): `ts.DeconstructTuple` is followed field by field, so +every field of the copy holds the borrow and the copy loses the field path. That is a sound +over-rejection, kept. The corpus keeps 526 files, the ray tracer has no error, and the sibling +field of round 2's over-rejection is accepted again. + +**A holder is renewed where it is stored, never at the read (fix-up re-review N3, 2026-10-23).** +The follow above made a holder's uses again at the read as well as at the local's declaration +(the kills the walk reached it with). For a holder declared before the loop that is wrong: `const +o = { v: Z.z }; for (...) { const c = G; print(o.v.x); o.v = c; G = new V(i + 1); churn(); }` +reads the read's block in the next round - the path back from the store goes through +`const c = G`, which renews `c`, not `o.v` - and own accepted it (exit 139 on Windows and Linux; +the module-global form on main too, the static-block and top-level forms since the loop-store +fix-up). So a holder's uses are now made again only where the holder is given a new value +(`holderRenewals`): the local's declaration when it has an initializer (N4, below), and every +store into the field path that holds the borrow or into a prefix of it (the whole local, `o.p` for +`o.p.v`); a store into a sibling field or below the path is not one. Those depend on the holder +alone, not on the way the walk reached it, so a holder is followed once. A value loaded out of the holder is made again only by a path +back through that load, which is itself a use bounded by the renewals; and a store that is a +renewal is also a use of the value it stores, bounded by that value's own renewals, so a store of +a stale borrow into the path is rejected at the store. Reachability goes around the back edge: a +store in one round reaches a use in the next. Accepted by this: the field stored before each +round's use (`o.v = c; print(o.v.x); G = ...`), a second read re-stored after the store +(`G = ...; const d = G; o.v = d; print(o.v.x)`), a fresh value stored into the path or a prefix +of it after the store (`o.v = Z.z`, `o.p = { v: Z.z }`), and a sibling read. Rejected: the use +before the store in the next round (`for`, `while`, `do`, `for-of`, `continue`, a labelled +`continue`, nested loops, a holder in an enclosing block or an outer loop, nested paths, two +globals in one holder), a store on one arm only, the holder used after the loop, and a stale value +stored back (`const t = o.v; G = ...; o.v = t`). Tests: `own_err_global_outer_holder` (a module +global, the shape above) and `own_err_global_outer_holder_static` (a static block's loop, the +store in one round and the use in the later ones), runnable, gc printing `42 64 0 64 1 64 2 64 4` +and `1 64 1 64 1 64 5`. Tooth (a temporary `OWN_TEETH` switch making the read and the +declaration the renewals again, removed): both compile, and the built programs exit 139 on +Windows and under `MALLOC_PERTURB_` on Linux. Newly rejected beside them: the unsafe shapes, and +two correct ones, both sound over-rejections - a holder stored on one arm of an inner loop before +every use in practice (`for (j ...) { if (j == 0) { o.v = c; } print(o.v.x); }`), which the CFG +cannot order, and a holder copied into another holder, renewed and copied again +(`h.v = c; outer.h = h; G = ...; h.v = Z.z; outer.h = h; print(outer.h.v.x)`); the corpus keeps +526 files and the ray tracer has no error. + +**A declaration without an initializer renews nothing (fix-up re-review N4, 2026-10-23).** `let +l!: V` (or `let l: V`, `let o: { v: V }`) declared inside the loop body stores nothing: tslang +does not reset the slot each round, so it still holds the last round's value. The renewal rule +above counted every declaration, so `for (...) { let l!: V; if (i > 1) { print(l.x); } const c = +K.w; if (i == 1) { l = c; } K.w = new V(i + 1); churn(); }` was accepted and read the freed block +in round 2 (exit 139; the module-global form on main too, the static-block and top-level forms +since the loop-store fix-up). Now a declaration is a renewal only when it has an initializer +(`holderRenewals`, and walkBorrowed's `passesOn` says the same for the non-owning local it hands a +value to - a narrowing for every borrow check, which can only reject more: a place read into such +a `let` in a loop was accepted the same way, `const c = p.v; if (i == 1) { l = c; } p.v = new +V(i + 1)`, and is rejected now). `let l: V | undefined` has an initializer and is a renewal. Still +accepted: the same `let` assigned before each round's use, a `const o = { v: Z.z }` declared in the +loop (its initializer re-stores every round), and `for (x of arr)` into an outer `let x!: V` +(each round's element is stored first). Tests: `own_err_global_uninit_let_static` (the shape +above, in a static block whose only store of `K.w` is the loop's) and `own_err_global_uninit_let` +(a module global in a function), runnable, gc printing `1 64 1 64 4`. Tooth (a temporary +`OWN_TEETH` switch counting every declaration again, removed): both compile, and the built +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. + +## 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 +literal is a boxed object (`ts.New` of its record) seen as the interface through `ts.NewInterface`, +which §18.2 makes a view of the box. MLIRGen gives the payload to `ts.SharedNew` with rc's retain +right before it (`mlirGenRetainCaptured`): a `new C()` arrives carrying its reference and has no +such retain, but the interface view is retained. + +- **In a function** the inference already took that retain as the move into the handle (§23.2): + `let l = new Shared({...})` compiled before this section, and needed no change. +- **In a global's initializer** `moveIntoGlobals` judged the retain and found its value going into + `ts.SharedNew` rather than the global's `ts.GlobalResult`: `the initializer of global 'g' takes a + second reference`. `movesIntoResult` now takes `new Shared(x)`'s payload operand as a move: the + handle's block owns `x` from then on, whatever becomes of the handle, which rc counts. The other + conditions stay: the value is fresh (the box, seen through its views), retained once, and given + back by nothing in the region. So `let b = new Shared(a.value)`, a second owner of `a`'s + payload, keeps the error (`own_err_shared_literal_borrowed`). +- **Not here: calling a function field through the handle.** `g.value.f(3)` and `l.value.f(3)` are + still `'this value' borrows a field but is used here after it may be released or overwritten`. A + borrow read through a handle ends at any call that may drop (§23.3), and a call through an + interface slot has no facts: the signature pass does not name what it reaches. That is not the + literal's: `new Shared(new K())` with a class `K`, and `p.s.f(3)` on a parameter `p` with no + handle at all, fail the same way, and a direct method call through a handle (`l.value.m(3)` on a + `Shared`) compiles. It is G9 (facts through a module-private interface). + +`own_shared_literal` makes a global and a local `Shared` of a literal with function fields, +copies the local's handle, reuses freed memory, and reads `roughness` through all three handles, +under own, rc, gc and none, AOT and JIT; `--verify-ownership` is quiet on it under own and rc. Teeth: +with the global's move replaced by a release of the literal (freed while the handle points at it), +the AOT run fails its assertion that the global's literal is alive: its block was freed and reused. + +## 27. Owning literals moved anywhere (G3 of the ray tracer plan), 2026-10-22 + +An object literal was moved only into an owning local or the result (§18.1, §20.2): `len({ start: +p, dir: d })`, `[{ pos: new V(1) }, { pos: new V(2) }]`, `const r: Ray = {...}` and `return { ls: +[...], c: new V(3) }` through an interface type were `an object literal that holds a value it owns +is moved only into a local or returned as it is`, 13 of the ray tracer's 44 errors. Such a literal is +now a value like a fresh object. + +- **The box.** A literal given as an object or an interface is built in a local a `ts.Constant` + starts, read once, and the read copied into a block of its own: a `ts.New` of a `value_ref` + filled by that one store, seen as the object (`ts.Cast`) and the interface (`ts.NewInterface`), + which are views of it (§18.2). `recordRetainOf` now takes that copy as the record's owner, beside + the result slot and an owning local, when the read goes nowhere else. rc's `ts.Retain` of the + read is then where the fresh values stored into the fields move in, as for a record built to be + returned: each value's own check erases its release, which the retain dominates, and reports a use + of it after the literal (`const p` moved into two literals, `own_err_literal_moved_twice`: `is used + here after its value was moved`; one moved in a loop it was made outside of: `is moved inside a + loop`). +- **Where the box goes** is the box's own check, as for any `ts.New`: a call argument is released + after a callee that borrows it, and moves into one that keeps it (`__own_params`: `keepRay` pushes + it, and the caller's release goes); an array literal element moves into the `ts.CreateArray`; a + `const r: Ray = {...}` is the box, released at the end of its scope; a literal returned is the + result. A literal in an interface-typed field of another literal (`{ inner: { start: ... } }` with + `inner: Ray`) is a fresh box stored into the outer record, which moves in like any fresh value. +- **`own_facts::isOwningRecord`** names such a record - its read, its box, or a view of the box - + when every field that owns a block holds what the constant put there, nothing, or a fresh value: a + new object, a call's result that borrows nothing, a literal, a `const` local folded into the value + it holds. `holdsConstantsOrMoves` reads its fields through it. A field that holds no block (`null` + in a `V | null`) is now accepted too; it moves nothing. + +Not here, and still `an object literal that holds a value it owns ...`: +- **A `let` read into a field** (`own_err_record_literal_let`). Its slot keeps its release at the end + of its scope, which nothing cancels: accepting it would free the block twice. A sound narrowing of + the plan's "a moved local": a `const` local is folded into its value and moves in. +- **A borrowed field**: a parameter (`{ start: a }`) or a field read (`{ start: this.pos }`, also + `borrows a field and cannot be stored`). The box's release would free the borrowed block. That is + G2's borrowing record, whose release skips borrowed fields; the ray tracer's `{ start: pos, dir: + rd }` is one. +- **A literal nested by value** (`const o = { inner: { item: new C() } }` with no interface type), + §20.6's catch-all: the inner record is copied into the outer one's field, not boxed. + +A literal stored into a field of a nullable interface type (`k.r = {...}` with `r: Ray | null`) is +`takes a second reference`: in the IR the pass sees, the box is retained once for the interface and +again for the union, and released nowhere in the function (the program compiles and runs under rc). +Into an `r: Ray` field it compiles. + +**A method that keeps its argument or returns a borrow, reached through an interface.** A call +through an interface (`s.take(x)` with `s: Sink`, a `ts.InterfaceSymbolRef` and an indirect call) +is not resolved, so it gets none of the method's facts. The caller released what the method kept, +and released a result that borrows (an argument, `this.v`, `this.items[0]`) as its own: a double +free under own either way, older than this section (`s.take(new V(1))`, `g.get(a)`), which a +literal argument now reached too. The interface's vtable for a class (`KS.Sink..vtbl`) holds the +class's `..instanceOf`, so the signature pass took it for a class vtable and its entries for calls +it resolves. Now `collectInterfaceReached` tells it apart by its name (`...vtbl`, +where the class's own `..instanceOf` is `..instanceOf`), and a method it holds that keeps a +parameter or returns a borrow, by its body or by `@keeps` / `@lends`, loses its facts. Its body is +then the error the path that finds it gives - `'' is a parameter, kept here, but -mm=own +cannot make the callers of 'KS.take' give it up`, `... is a parameter, returned here, but -mm=own +cannot tell the callers of 'G.get' that the result borrows it`, `borrows a field (an element) and +cannot be stored, returned or captured` - noted `it is reached through an interface, whose calls are +not given its facts`, which wins over an exported method's `'@lends' or '@keeps' would say it` +(`own_err_interface_keeps_literal`, `own_err_interface_keeps_param`, +`own_err_interface_borrow_param`, `own_err_interface_borrow_element`, +`own_err_interface_borrow_field`). A direct call of such a method (`ks.take(x)`) is refused too +whenever the class implements the interface: its interface vtable is made for the `implements` +clause, cast or not. G9 (facts through a module-private interface) relaxes this for closed +interfaces by giving the interface call the meet of these facts. + +`own_literal_moves` runs the shapes (two moved `const`s; a fresh call result beside a moved one; an +array of literals; a `const r: Ray`; a returned interface record holding an array; 1000 literals +given to a callee that keeps them) under own, rc, gc and none, AOT and JIT, and on Linux; +`own_record_literal_given` (was `own_err_record_literal_given`) returns `{ v: new Vec(n) }` as a +`Holder`. Own emits no counting. Teeth: with the moved-in values' releases kept, both AOT and JIT +runs of both fail with `0xC0000374` (heap corruption: the box's release frees the field, then the +value's own release frees it again). A loop of 2 million iterations of every shape (about 10 +blocks each) peaks at 4.8 MB under own, as under rc (gc 6.7 MB): the box's release frees its fields. + +## 28. A field read through an interface (G6 of the ray tracer plan), 2026-10-22 + +`let d = r.dir` with `r: R` an interface was `takes a second reference; -mm=own cannot prove a move +or a borrow here yet`, while the same read of a class field borrows (§2.3, phase 3). The field of an +interface is a `ts.InterfaceSymbolRef` - the object's field at the offset the interface's vtable +holds - and `isPlace` named only `ts.PropertyRef`, `ts.ElementRef` and `s.value`. So the read was no +borrow at all: a `let` from it fell through to the second-reference error, and a `const` from it, or +a read used in place, was not checked against anything. That was a use after free older than this +section: `const c = h.c; h.c = new C(9); print(c.x)` with `h: HI` printed a freed block's value. The +overwrite was invisible too: `hi.c = new C(9)` through an interface view of `h` did not end a borrow +of the class field `h.c`, and the signature pass gave `function set(hi: HI) { hi.c = new C(9); }` +`__own_no_drops`, so its callers kept their borrows of `hi.c` across the call. + +- **An interface field is a place** (`own_facts::isInterfaceField`: an `InterfaceSymbolRef` whose + result is a reference; a method is a bound function, not a place). Its read borrows, as a class + field's does: tenure-checked against the ends of what it was read through and against the drops + (`checkPlaceRead`), through any chain of field reads and interface views (`i.ray.dir`: the chain + `dir`, `ray`, parameter `i`). A call ends it only when the call's facts say it may drop - `V.plus` + does not - and any call does for a root the function does not own, as `own_err_param_read_call` + pins for a class field. `borrowedParam`, `reachesSharedValue` and the signature pass's + `reachesOutside` walk through it, so `function dirOf(r: R) { return r.dir; }` returns a borrow of + `r`, bounded by the caller's `r`, and an overwrite through an interface parameter is a drop. A + parameter stored through an interface field (`function keep(r: R, v: V) { r.dir = v; }`) is kept + (`__own_params`), as one stored into a class field is: the caller moves it in. Before, that was + `is a parameter, kept here, but -mm=own cannot make the callers of 'keep' give it up`. +- **Which field it is** is not known where it is read: the interface's index is not the class's + position, and a class may declare the field narrower than the interface. So the chain records it + as `Chain::InterfaceField`, and `dropsChain` matches it with any field overwrite of any position + and type, and an overwrite through an interface with every field on a chain. Elements and `s.value` + keep their own matching. + +`own_field_chain_let` runs `let d = r.dir` and `let d = i.ray.dir` (a nested interface literal, as +§27 accepts it) with `V.plus` between the reads, a class implementing the interface passed as it, +and `dirOf`'s borrowed result, 100,000 times, then `keep` moving a `const` into a field through an +interface and reading it 1000 times, under own, rc, gc and none, AOT and JIT, and on Linux; +own emits no counting. Negatives: `own_err_iface_field_overwritten` (overwritten through the +parameter), `own_err_iface_field_view_overwritten` (a class field's borrow, overwritten through an +interface view), `own_err_iface_field_call` (a callee given the interface overwrites it: no +`__own_no_drops`), `own_err_iface_field_result` (`dirOf`'s result read after the field is +overwritten), all `borrows ... but is used here after it may be released or overwritten`. Teeth: +with the borrowing `let`'s releases kept (only its retain erased), the AOT run of +`own_field_chain_let` fails with `0xC0000374` (heap corruption: the `let`'s release frees `r.dir`), +and so does the JIT run; with the interface clause of `dropsChain` skipped, +`own_err_iface_field_view_overwritten` compiles and prints a freed block's value. With the call's +`__own_result_borrows` dropped for a callee that reads through an interface field (the caller then +owns `dirOf`'s result and releases `r.dir` while the record holds it), and with the call's +`__own_params` dropped for such a callee (the caller still releases `nv` after `keep` stored it in +`k.dir`), the AOT run fails with `0xC0000374` both times. A loop of 2 million iterations (about 6 +blocks each) peaks at 4.3 MB under own, as under rc. + +Only fields: `borrowedParam`, `reachesSharedValue` and `reachesOutside` walk through an +`InterfaceSymbolRef` only when it is a field (`isInterfaceField`). A method read through an +interface (`function getM(m: M) { return m.get; }`) is what it was before this section. + +Corpus (`test/tester/tests/*.ts` under `-mm=own --no-default-lib`, `--emit=obj`): 519 of 693 +compile on Linux before this section and 519 after, the same files; none lost, none gained. On +Windows 522 after (`00catch_value`, `00cppvtable_hidden_return` and `00throw_in_block_debug_info` +compile there and not on Linux, before and after). One file that fails either way fails earlier, +with a more precise error: `00array_assignment5` was `takes a second reference` and is now `borrows +'Test.lineTokens.tokens' and cannot be stored, returned or captured`. + +Not here: the ray tracer's `shade` still fails at `let d = isect.ray.dir`, now as `borrows a field +but is used here after it may be released or overwritten`, because `isect.thing.normal(pos)` is a +call through an interface, which has no facts (§27) and so may drop anything the function does not +own. Checking reads through an interface also reports the reads the ray tracer used in place across +such calls (`scene.camera.pos` beside the `getPoint` closure, `scene.lights` given to `reduce` with +a closure, the rest of `shade`): 24 errors before, 33 now, none of them a new kind. + +They are G9's (facts through a module-private interface), G5's (`reduce`) and G2's (borrowing +records). + +Seen on the way, and rc's own, older than this section: under rc, `const c = h.c; h.c = new C(9); +churn(); print(c.x)` with a class `H` prints 999 (gc prints 0): the `const` takes no reference, so +the overwrite frees what it reads. A `let` is retained. + +## 29. A read after a move, through the new owner (G7 of the ray tracer plan), 2026-10-22 + +`h.c = c; print(c.x)`, `n.v = a; print(a.length)` and `best = v; s += v.x` were `is used here after +its value was moved`: a move (§2.2) allowed no use of the old name after it. Now a use after a move +that only reads the value is a borrow of the place the value moved into, as a read of `h.c` itself +would be (§2.3), and is accepted while that place still holds it (USER decision). + +- **What a read is** (`isReadAfterMove`): a field, element or length read through the value, a + method reached through it, a comparison or arithmetic, `print`, `assert`, a test, or an argument + the callee borrows (`!isKeptArgument`). What the read produces - a field's reference, a bound + method - is walked as a borrow's uses are (`walkBorrowed`, starting not borrowed); a call result + that borrows its argument is the value again and may not be kept. A method reached through it + whose callee keeps its `this` is not a read. A store through a field read (`h.c = c; c.x = 7`) + is a use of the borrow, not a keep: the block stays the place's. Anything else after the move - + a store of the value, an owning `let`, a keeping call, a return, rc's retain for a receiver - is + still a second owner and keeps the error. +- **Where it moved** (`destinationChain`): an owning local (a declaration, or an assignment), or a + field or element of a local or a parameter (`this`), described as the `Chain` `checkPlaceRead` + reads (`chainOf` from the store's place). A move into a call that keeps the argument, a `push`, a + return, a global's field, a captured variable or a handle's payload is not such a destination. +- **The kill walk** is `checkPlaceRead`'s: the ends of the chain's roots (`rootEnds`) and the drops + that may destroy what the place holds (`dropsChain`, with the moved value standing in for the + read's result): an overwrite of the place, matched by position and type, through an interface + view too (§28); a call whose facts may drop (no `__own_no_drops`) given something that reaches the + place's root, or any such call for a root the function does not own; a call given a closure; and + an assignment to a global, for a root the function does not own (below). The read fails when + some end or drop E has a path move -> E (not through the value's definition, the local's + declaration or its assignments) and a path E -> the read or anything it produced (not through + those either, nor through the move again). Together the two are exactly one path from + the move through E to the read on which the value is neither made again nor moved again; a path + that moves the same value again without making it again is the `moved inside a loop` error + already. A read of the old name again after E is no new value - the move leaves the slot as it + was - so, unlike `checkPlaceRead`, the read itself is no kill. In a loop that makes the value in + each iteration (`let v = ...; best = v; s += v.x`), the next iteration's `best = v` overwrite is + reached from the move only through `let v`, so the read in the same iteration stands. +- `movesOf` (a fresh SSA value, a folded `const`) asks it of each read after a taker; + `checkSlotMove` (an owning local) of every use of a slot read the move reaches, and of each late + use `readUsedAfter` finds. + +`own_read_after_move` runs the straight-line moves into a field (`n.v = a`, `h.c = c`), into an +element (`arr[0] = ce`) and into a field of `this` (`this.f = c` in a method), each read after +same-size allocations; a move into an owning local in a loop that makes the value each iteration; +100,000 iterations of a move into a field and into a local each read after it; and a declaration +that takes a value over (`let q = p; print(p.x); p = mkv(8)`: `q` keeps the first value, §24). +Then it allocates same-size blocks, reads the moved values again and asserts them, under own, rc, +gc and none, AOT and JIT, and on Linux; own emits no counting. Its values come from factories +(`mkc(n)`), so stack promotion leaves them heap blocks. Negatives, all `is used here after its +value was moved`: `own_err_field_alias` (`n.v = [2]` after `n.v = a`), `own_err_union_moved` +(`h.c = null`), `own_err_read_after_move_overwritten` (overwritten on one path), +`own_err_read_after_move_view` (overwritten through an interface view `hv.c = null`), +`own_err_read_after_move_call` (`clear(h)`, which may drop), +`own_err_read_after_move_loop` (the read at the top of a loop reached around the back edge after +the iteration's `best = new V(i)` freed the moved value), `own_err_read_after_move_after_loop` +(a read after a loop that overwrote the destination). `own_err_param_kept_used`, `own_err_push_let`, +`own_err_two_owners` and `own_err_literal_two_records` still fail as before. + +`own_err_borrow_bound_method`, `own_err_borrow_catch_local` and `own_err_borrow_finally_local` +(phase 2) made `let b = a` a borrow by reading `a` after it. That read now goes through `b`, which +takes the value over, and `a = new C(2)` frees nothing `b` holds: the programs are correct (they +print what rc prints, on Windows and Linux). They now give `a` to a second `let`, so that each +borrows it, as before. + +**An assignment to a global is a drop** for a chain with a root this function does not own (found +in review). `G = mkH()` is `ts.ReleaseSlot(ts.AddressOf @G)` before its store, and the drops took +only a `ts.ReleaseSlot` of a place or a cell. So `function run(h: H) { const c = mk(); h.c = c; G = +mkH(); churn(); print(c.x); }`, called as `run(G)`, compiled and printed 999 (a freed block that +the allocations reused; rc prints 5), and so did the same through `this` in a method of the +global's class. A parameter, `this`, or a value read out of a field or a global may be the global's +old value, or under it, and the assignment frees it. The plain place read had it too, older than +this section: `h.c = mk(); const x = h.c; G = mkH(); print(x.x)`. Now such a `ts.ReleaseSlot` is a +drop, and `dropsChain` takes it for every chain that is not owned (`!chain.owned()`); a local or a +value this function made cannot be the global's. An immortal global's (§25) one store is its +initializer, whose old value is empty, so its `ts.ReleaseSlot` drops nothing. A callee that assigns +a global was already no `__own_no_drops` (`dropsInBody`), so a call of it ends such chains. +Negatives: `own_err_read_after_move_global` (the parameter, `this`, and the assignment in a callee: +`is used here after its value was moved`) and `own_err_field_let_global_overwritten` (the place +read: `borrows a field but is used here after it may be released or overwritten`). Teeth: with the +global's `ts.ReleaseSlot` dropping nothing, both compile but the callee's case; the first prints +garbage (`9.88131e-324`) for the parameter and 999 for `this`, the second `9.88131e-324`, instead +of 5. Still open, every model (rc too): a parameter itself read after the global it came from is +assigned (`function run(h: C) { G = mk(); print(h.x); }` called as `run(G)`): no place is read, +so there is no chain to end. Under own main's #639 (merged into this branch after T8) closes that +one at the caller: a value read out of a global something assigns, lent to a call that may drop, +is an error ("a value read out of the global 'G' is lent to a call that may release or overwrite +it"). That error comes first for `run(G)`, `GK.run()` and `runCallee(G)` too, so since the merge +both negatives give their callees fresh values (`run(mkH())`, `new K().run()`) and pin this +section's callee-side rule alone; the teeth above were measured with the global passed, before +the merge. The original shapes are #639 negatives of their own (fix-up of 2026-10-23), so a +narrower #639 would still meet them: `own_err_global_argument_transitive` (`run(G)`, `run` calls +`reset`, which assigns `G`), `own_err_global_method_transitive` (`G.m()`, `m` calls `reset`), +`own_err_global_this_field_move` (`GK.run()` moves into `this.f`, then assigns `GK`), +`own_err_global_argument_field_let` (`run(G)`, `const x = h.c`, then `G` assigned) and +`own_err_global_kept_field` (`keep(h, g)` stores `c` into `h.c`, `g` assigned by a `reset` +elsewhere), each "a value read out of the global '...' is lent to a call that may release or +overwrite it". + +Teeth: with the kill walk skipped (`readsThroughOwner` accepting every read), all seven negatives +compile, and AOT runs with same-size allocations before the read print a freed block's value: +`c.x` prints 0 (overwritten on one path, `clear`, set to null) and 999 (through the view) instead +of 5; `a.length, a[0]` prints `64 6.4649e-312` instead of `1 1`; the loop's sum is 1999 instead of +3; the read after the loop prints 999 instead of 1. The positive cannot show it: by design nothing +in it overwrites a destination before a read. With the rule off, the positive fails to +compile with `is used here after its value was moved`, at every read after a move. A loop of 2 +million iterations peaks at 4.5 MB under own, as under rc. + +Not here: an assignment that moves on some paths only (`if (v.x > m) { best = v; }`) is `is moved +here on some paths only; -mm=own cannot release it on the others yet` - the local's one release at +the join is on a path that moved it and one that did not. The use-after-move error came first and +hid it. It is G10 (Task 5b, static drop elaboration), not a follow-on of G7; the ray tracer's +`intersections` has this shape, and the `moved here on some paths only` of design G8 is it too. +Matching an overwrite by position and type also matches another class's field of the same type at +the same position (`dropsChain`, as for any place read): `own_read_after_move`'s loop holder keeps +its field elsewhere for that reason. + +Corpus (`test/tester/tests/*.ts` under `-mm=own --no-default-lib`, `--emit=obj`, Windows): 522 of +693 before this section, 527 after; none lost. Gained: `00class_recursive`, +`00generator_manual_next2`, `00globals_entry_point`, `00owned_locals`, `00union_value_equality`, +each accepted by this rule. Each prints under own what it prints under rc (Windows AOT) and runs +clean under own on Linux. + +The ray tracer (`rtbench_own.ts`) has 33 errors, as at §28, but none of them is `is used here +after its value was moved` any more. Two are `is moved here on some paths only` (G10), which the +use-after-move errors used to hide. The rest are G9's, G5's and G2's (§28). + +## 30. A move on some paths only (G10 of the ray tracer plan), 2026-10-22 + +`let v = ...; if (v.x > m) { best = v; }` was `is moved here on some paths only; -mm=own cannot +release it on the others yet`: where the paths join, `v`'s one `ts.ReleaseSlot` is on a path that +moved the value and on one that did not, and no run-time record of the move is kept (§2.2). Now the +release is placed statically (static drop elaboration, controller ruling; a run-time drop flag was +ruled out): on each edge into the join that holds the value and did not move it, and nowhere on +the paths that moved it. The releases after the join go. Each path frees the value once - through +the new owner where it moved, on the edge where it did not. + +- **The state of the slot** (`placeOnUnmovedEdges`): a forward walk of the function's CFG gives, + before each op of the body, what the local's slot may hold - nothing (before its `ts.Variable`, + after a release), its value (after the `ts.Variable` or an assignment), or a moved value (after + the move's acquire). A point that some paths reach holding the value and others having moved it + is *joined*. Every `ts.ReleaseSlot` that may hold a moved value goes; one that some path also + reaches with nothing in the slot keeps the error. An assignment's own release before its store + is one of them: an assignment over a value that may have moved frees the old value only where it + did not move (`if (k % 3 == 0) { kept = r; } r = new V(k);` in a loop). +- **Where the release goes**: on every edge from a block that ends holding the value, and only the + value, into a joined block - at the end of the block when it has no other successor (a `then` or + `else` branch, a loop's latch), else in a new block on the edge. A joined block that some path + reaches with nothing in the slot, an edge whose branch is not `cf.br`/`cf.cond_br` (an + `Invoke`), and an edge out of an exception handler (from a landing pad to its + `EndCatch`/`EndCleanup`) keep the error. +- **The safety condition**: the release moves earlier, from the end of the local's scope to where + the paths join, so nothing may use the value at a joined point: no read of the slot, and nothing + read out of it - followed forward through every op's results (a field's reference, a view, a + bound method, a block argument), through a store into a local (every read of that local), and + out of a region (a closure, an async body) through the op that holds it. A store of such a value + into a place or into a captured local is not followed and keeps the error. A call's result is + followed only when its facts say it borrows that argument (`__own_result_borrows`); anything + else a call returns is its own block. The move hands the value to its new owner, whose reads are + its own, and rc's retain for it is erased. A value the slot is given (its initializer, an + assignment's) is the slot's only once stored, so a use of it before that - the constructor's, or + one around a loop before the value is made again - is no use of the slot's value. +- **...and past the joined points** (found in review): what was read out of the value before an + edge released it - a bound method `v.get`, an interface view `const i: I = v` - may not be used + anywhere reachable from the start of a block the edge released it into, before the read it came + from (or the op that made the value) runs again: not after a new value is stored into the local + (`if (c) { b = v; } v = mkv(2); m()`), where the state is no longer joined, and not after the + scope ends. A read of the slot itself after a new value is stored reads the new one, and stays + accepted. The error then carries a note at the use: `used here, after the paths join, so it + cannot be released before they do`. +- A `string` read out of the value counts as a pointer into it, though own copies strings in some + places: too strict, but sound. +- **One receiver**: the rule applies when the move is the local's one receiver (`decideSlot`). + With two, `if (c) { a = v; } else { b = v; }`, each move's other paths include the other's, and + the error stays. +- **Reads after the join**: a read of `v` after the paths join (`if (c) { b = v; } print(v.x);`) + goes through `b` on the path that moved it (§29), but on the other `v` must free the value after + the read, where only a run-time record could tell the paths apart: the error stays. So does a + `let` declared outside a loop, moved on some iterations and read in the next without a new value + (`moved inside a loop`). +- **Unwind paths** are paths of the CFG (`ts.Invoke`'s unwind edge to a landing pad), and get the + same treatment: a release on the normal edge into a joined `try` body comes before the call that + may throw, and a cleanup's `__unwind_only` release of a joined slot goes with the others. +- The releases are recorded during the analysis (which runs twice, dry and real, and tries a move + quietly before a borrow) and put in only after it, before the erasures (`placeEdgeReleases`). + +`own_conditional_move` runs: the brief's loop that keeps the largest value (`best = v` on some +iterations, `v` declared in the loop); a straight-line move taken and one not taken; 100,000 +iterations of a move into a local on even iterations, read through the new owner inside the branch +(§29) and freed by the local on odd ones; an assignment in a loop over a value that may have moved; +a value moved into a field on every path and given a new value on some, which that path frees +before the paths join (the old `own_err_reassign_some_paths`); a move into a call that keeps its +argument on one arm of `?:` (the ray tracer's `shade`), and on the right of `&&`, whose skipping +edge carries the `&&`'s value, so the release goes in a block of its own that passes the value on; +and a move in a `try` whose call throws on +half of 20,000 calls, with a `finally`. It prints the same under own, rc, gc and none, AOT and JIT, +and on Linux; own emits no counting. Negatives: `own_err_conditional_move_read` (`b = undefined` +after the join, then `v.x`: `is used here after its value was moved`), +`own_err_conditional_move_outer` (`moved inside a loop but was made outside it`), +`own_err_conditional_move_read_after_join` (`v.x` after the join), +`own_err_conditional_move_field_borrow` (a field of `v` read after the join), +`own_err_conditional_move_catch` (the move inside a `catch`), `own_err_conditional_move_bound_method` +(`const m = v.get` called after the join), `own_err_conditional_move_view` (`const i: I = v` read +after the join) and `own_err_conditional_move_old_view` (both, used after `v` is given a new value +past the join), the last six `is moved here on some paths only`. + +The brief's positive read `t` after the join (`if (k % 2 == 0) { b = t; } s += t.x;`). That is the +shape the rule leaves out, so the read moved into the branch (`b = t; s += t.x;`) and before the +`if`, and `s` is 199999 instead of 149999. + +`own_err_reassign_some_paths` (a value moved, then given a new value on some paths) and +`own_err_borrow_derived_ref` (phase 2: `let b = a`, then `a = new C(2)` in a loop) became correct +programs: the path that gave `a` a new value frees it before the paths join, and the move into `b` +needs no borrow. They print what rc prints. The first now reads `a` after the join, and the second +gives `a` to a second `let`, so that each borrows it; both fail as before. + +Teeth: with the release placed on the edges that moved the value as well, `own_conditional_move` +(AOT, `test-compile-own-`) exits 0xC0000374 (heap corruption), and so do the brief's loop, the `?:` +move into a keeping call and the `try` with a throwing call, each alone; the straight-line +`if (v.x > 0) { best = v; } print(best.x)` prints 0 instead of 1. With the safety condition off, +`own_err_conditional_move_read_after_join` compiles, and a copy with same-size allocations before +the read prints 0 for `v.x` on the path that did not move it, instead of 1; no other check covers +it, since §29 accepts the read on the path that moved. With what is read out of `v` not followed +(only the slot's reads), `own_err_conditional_move_bound_method` and `_view` compile - no older +check sees them - and their AOT runs crash (0xC0000005), where rc prints `1 1`. With the walk +checking only joined points (as first written), both forms of `own_err_conditional_move_old_view` +compile and read the freed old value: the bound method printed `3 4` on one run and crashed +(0xC0000005) on the other, instead of `3 3`; the view printed `3 1002` and `3 1004`. A field of +`v` read after the join, a `const` alias of `v` and a call result that borrows `v` are each also +rejected by an older check (the move ends a place borrow of `v`; two receivers; `borrows the +result of`). Still open, every model (rc prints garbage too): a bound method or a view of a +local used after the local is given a new value, with no move at all (`const m = v.get; v = +mkv(2); m()`). + +Corpus (`test/tester/tests/*.ts` under `-mm=own --no-default-lib`, `--emit=obj`, Windows): 527 of +693 before this section, 528 after; none lost. Gained: `00nullish_values`; it prints under own what +it prints under rc (Windows AOT) and runs clean under own on Linux. + +The ray tracer (`rtbench_own.ts`) has 31 errors, from 33: both `is moved here on some paths only` +are gone - `intersections` (`closestInter = inter` on some iterations) and `shade` (`pos` and +`reflectDir` moved into `getReflectionColor`, whose facts keep them, on one arm of `?:`). The rest +are G9's, G5's and G2's (§28): 21 `borrows a field`, 5 object-literal moves, 2 `borrows an +element`, 1 mixed return, 1 kept parameter (the `reduce`), 1 `borrows a field and cannot be +stored`. + +## 31. Facts through a closed interface (G9 of the ray tracer plan), 2026-10-22 + +A call through an interface (`ts.InterfaceSymbolRef`, then an indirect call) was not resolved: it +had no facts, so it might destroy anything, and since §27 every method held in an interface vtable +lost its keep and borrow facts, called directly or not. `sc.ts[i].area(r)` ended the borrow of +`sc.ts` it was made through (`borrows an element but is used here after it may be released or +overwritten`), and `s.take(x)` / `g.get(a)` / `i.first()` / `h.hold()` were the §27 errors in the +method's body. Now a call through an interface only this module can see is resolved like a call +of a private virtual family. + +- **Closed.** An interface is open when another module can see it - its name is a word of the + module's exported declarations (`__decls`), or `!ts.iface<@I>` is in the type of an exported or + declared function or of a global that is not private or is exported or imported (a class's + fields included) - or when a value of it may be made other than by `ts.NewInterface` over a + vtable the signature pass reads: the global, a heap copy of it, or the one an object's class + vtable holds for it. A cast into the interface from another interface opens both (the other's + members are reached through it); one from `any` or an opaque pointer opens every interface boxed + into one; a `ts.InterfaceRef` used as a value, a vtable read other than to make a value, an + exported class whose vtable holds one of its vtables, and a vtable named for it that no value is + made with all open it. An untraceable `ts.NewInterface` opens every interface. +- **Families.** Each slot of a closed interface (its vtable position) has the functions its + vtables hold there - a class's method, an object literal's method, never a declaration, never one + that says `@keeps` or `@lends`. A call through the slot gets `__own_no_drops` when every member + destroys nothing, and the kept parameters and borrowed result its members agree on: `meetFacts` + demotes a family that disagrees, as it does a class family, so the borrowing or keeping member's + body is the error it was (`own_err_interface_family_disagrees`). That is a sound narrowing of the + plan's "a parameter kept by any member is kept by the call": a member that does not keep a moved-in + argument has no release for it, which would leak. A `public` method is open (§15) and gives its + family no facts; an object literal's method is open too (its symbol is a value in the literal). +- **Members keep their facts.** §27's stripping (`interfaceReached`) applies only to the members of + open families and to vtables no value is made with, so a direct call of a method of a closed + interface keeps its facts (`ks.take(x)`, `f.first()`). +- **A result that borrows `this`** through the interface is bounded by the interface value: + `boundThis` sees `ts.GetThis` of an interface-bound method as the object the interface value is a + view of (`own_err_interface_result_outlives`: the local holding it is given a new value before the + read). +- **Function-typed fields** (`f: (x: number) => number`, the ray tracer's `Surface`): the slot holds + the offset of an object literal's field, and the call goes to whatever function the field holds. + When the interface is closed and every function ever stored into those fields is one this module + defines - the literal's own (`{ f: function ... }`, in its `ts.Constant`, a closure over locals + included: its captures are a field of the object), or one stored later through the object or + through the interface - the call gets their facts. A store of anything else, the field reached + other than to be read or stored, an object of the literal's type made other than by the literal, + a function-typed store through an open interface, or a class implementer (its slot is a thunk's + cell) keeps the call open. In the language as it is, a function stored into such a field is + always the literal's: `s.f = fn` through the interface is `saving to constant object`, and a + function without `this` (a parameter, a named function) is held through a thunk, which is open + (`own_err_interface_fn_field_unknown`). + +`own_interface_facts` runs the plan's loop over `sc.ts` (prints 9), a field borrowed across a call +through two class members and an object-literal member, the closed forms of the five §27 negatives +(a kept literal, a kept `const`, a kept value, through the interface and directly; a borrowed +argument, element and field, through the interface and directly), each read after churn, under own, +rc, gc and none, AOT and JIT, and on Linux; own emits no counting. `own_interface_fn_field` calls a +function field through a global and a local `Shared` of a literal, through a parameter with no +handle, and through a handle given as a parameter, each after churn, under the four models (counted, +so not in `own_test`), and `--verify-ownership` is quiet under own and rc. The five §27 negatives +now export their interface (the open form, same texts). New negatives: `own_err_interface_exported_impl` +(the plan's program with `export class B`), `own_err_interface_family_disagrees`, +`own_err_interface_member_drops`, `own_err_interface_from_interface`, +`own_err_interface_result_outlives`, `own_err_interface_fn_field_drops`, +`own_err_interface_fn_field_unknown`, `own_err_interface_fn_field_exported`. + +Teeth: with the call's `__own_params` dropped for a call through a closed interface, and with its +`__own_result_borrows` dropped, the AOT and JIT runs of `own_interface_facts` fail with `0xC0000374` +(heap corruption: the caller releases what `KS.take` kept, and what `G.get` lent). With +`__own_no_drops` given to a call through a family one of whose members overwrites a field of its +argument, `own_err_interface_member_drops` (with same-size allocations before the read) compiles and +prints 77777 instead of 5 (gc prints 5); the same for a function field whose function overwrites +`this.v` (`own_err_interface_fn_field_drops`): 77777 instead of 5. The store-of-unknown-origin +condition has no program that reaches it (above), so it has no tooth. + +Measured: 2 million iterations of a kept argument, a borrowed result and a function-field call +through closed interfaces peak at 4.4 MB under own, as under rc (gc 6.4 MB). + +Corpus (`test/tester/tests/*.ts` under `-mm=own --no-default-lib`, `--emit=obj`, Windows): 528 of +693 before this section and 528 after, the same files. + +The ray tracer (`rtbench_own.ts`) has 31 errors, as at §30: none of them is cleared, for two reasons +outside this rule. `Sphere.normal` and `Sphere.intersect` call `Math.sqrt`, whose `sqrt` is declared +without `@nodrops`, so they may destroy anything and the `Thing` families get no `__own_no_drops`; +`checkerboard`'s functions call `Math.floor` the same way. And `Surface` is exported: +`export let shiny` in `module Surfaces` puts the globals and `interface Surface` into `__decls`, so +another module may store its own function into a surface. Measured on copies: with the four C +declarations `@nodrops`, 22 errors before this section and 18 after (cleared: both errors at +`scene.things[i].intersect(ray)` in `intersections`, `isect.thing.normal(pos)` and the +`Vector.minus(d, ...)` after it in `shade`); with the `export`s dropped too, 22 and 14 (also +`thing.surface.value.reflect(pos)`, `diffuse(pos)`, `specular(pos)` and the `roughness` read); with +only the `export`s dropped, 31 and 30 (`specular(pos)`). Neither is one of the plan's two source +edits. + +Not here: +- **Kept parameters do not take the union** across members (above): a family whose members + disagree is demoted, as a class family is. +- **Class families group by (position, name)** across every vtable that holds an `.instanceOf`, + interface vtables included (unchanged): two closed interfaces with a same-named method at the same + position demote each other when they disagree. Too strict, but sound. +- A class implementing a function-typed field (`f = (x) => ...`) is held through a thunk's cell and + stays open. +- **A `public` method** is open (§15: a function that is not private may be called from another + module), so its effective facts are empty, and a family it is in can agree only on none: a + `public get(v) { return v; }` beside a private one returning the same borrow demotes both (the + ray tracer's `Plane.normal` and `Plane.intersect` are `public`). For type exposure this section + reads only exported and declared functions; `findOpen` does not, yet. +- **`export` inside a namespace** exports the declaration from the module (`__decls` lists `namespace + NS { interface Th ... }`), so an interface declared `export` in a namespace for use outside it is + open; one used only inside its namespace is closed. + +**After review** (2026-10-22). The two "not here" items above on `public` methods and namespace +exports are now handled as follows, and `own_interface_facts`' churn now keeps 2000 blocks alive, so +a block freed too early is reused before it is read. + +- **A function value reinterpreted** carried an interface value into a closed family unseen (found + in review, a new acceptance that crashed under own): `f2 as any as (o: Op, a: V) => number` with + `f2` taking an `Op2` let an `Op` value reach `Op2`'s calls, and so did `(() => Op)` cast through + `any` to `(() => Op2)`. Now a cast that makes a function out of `any` or `unknown`, or sees one + function as a function of another signature, opens every interface either signature mentions (a + class's fields included). A function boxed into `any` puts its signature's interfaces among the + boxed ones, which any unboxing opens. A bound function's `this` and an opaque `this` are not part + of the comparison: a closure seen as a plain function of the same parameters is the same. + Negatives `own_err_interface_fn_any_param`, `_result`, `_drops`. +- **C declarations of numbers** (controller ruling): a body-less `declare function` with no `export` + or import marker (another TypeScript module's declarations carry `export`), not variadic, whose + parameters and result are numbers, booleans, characters, integers or enums of them, destroys + nothing: it is given no block of this module. `sqrt`, `floor`, `pow` and `fabs` in the ray tracer. + The risk, as for `@nodrops`, is a C function calling back into the module: one registered through + another declaration (`signal(15, onSig as Opaque)`, then `raise(15)`; a window procedure and + `SendMessage`), or an exported function reached by name. The first is answered by type below (the + rule is off in a module that casts a function-holding value to a type that holds none, or has a + call that is not resolved to functions of this module with an argument whose type holds a + function); the second remains. + `own_declared_scalar_no_drops` holds a field's + borrow across `sqrt`, across a function that calls `sqrt` and `floor`, and across a call through a + closed interface whose member does; `own_err_declared_drops` (`strlen(s: string)`) is unchanged. +- **A `public` method of a class no other module can name** (controller ruling) is not callable from + another module: the class is not exported (its vtable is private), not a word of `__decls`, and not + in the type of an exported or declared function or of an exported or imported global (a derived + class's storage holds its base, so a nameable class makes its bases nameable). `findOpen` and class + families use the same test (`callableFromOutside`). `own_err_interface_public_exported` (an exported + class) and `own_err_interface_public_reachable` (a class an exported function returns) keep the + error; `own_interface_facts` calls `public` methods that return a borrow through a closed interface. + `own_err_generator_method_this` exports its class to keep what it tests. +- **An executable's exports** (USER decision): a module built into a program - `--emit=exe`, or the + module the JIT enters (`isExecutable || isJit`; a module the JIT cache compiles beside it has + `isJit` off) - is imported by no other module, so its `__decls`, exported functions and globals, and + exported classes open no interface and make no class nameable. What it imports still does: a + declared function, an imported global, a global with no initializer (an import of a `.ts` module). + An object file (test-runner's AOT build is `--emit=obj --entry-point`), a DLL and a `-shared` + library stay as they were; the pass takes the kind as an option, a library when not told. + `own_interface_exe_exports` (an exported interface and an exported global of it) runs under the + JIT and as `tslang --emit=exe`, and is the error on `G.get` as `--emit=obj` and `--emit=dll` + (`own-exe-exports.cmake`). The rule covers interfaces and classes, not functions: + `callableFromOutside` ignores the kind, so a program's `export function` is still taken as one + another module calls - never a closed function, a callback-family member or a family member with + facts of its own. A sound over-rejection (final review M3): the function is treated as a + library's exported function is. +- **An iterator `next()` through a closed interface** (ruling) gets the family's facts like any other + call; it is no longer assumed fresh. + +Teeth: with function reinterpretations not opening anything, `own_err_interface_fn_any_param` and +`_result` compile and their AOT runs fail with `0xC0000374` (with and without `--opt`), and `_drops` +prints 77777 instead of 5 (gc 5). With every declaration without `export` taken as destroying +nothing, `own_err_declared_drops` compiles, and so does a program that lends a field and passes the +field's object to a declared `free(p: V)`; its AOT run fails with `0xC0000374`. With a `public` +method's class never nameable, `own_err_interface_public_reachable` compiles; with every module taken +for an executable, `own-exe-exports.cmake` fails (the object file and the DLL compile), and so do +`own_err_interface_exported_impl` and `own_err_interface_keeps_param`. The last two rules protect +callers in another module, which no single-module run can show. + +Corpus (`--emit=obj`, not an executable): 528 of 693 before this section and after, the same files. + +The ray tracer built as a program, `tslang --emit=exe --opt --opt_level=3 --no-default-lib -mm=own +rtbench_own.ts`: 31 errors before this section, 15 after. Cleared: `Camera`'s constructor (three, +`Vector.norm` reaching `sqrt`), `Sphere.normal`, `this` in `Sphere.intersect`, both errors at +`scene.things[i].intersect(ray)`, `isect.thing.normal(pos)` and the `Vector.minus(d, ...)` after it +in `shade`, `thing.surface.value.reflect(pos)`, `.diffuse(pos)`, `.specular(pos)` and the +`roughness` read, the three `camera.forward/right/up` reads in `getPoint`, and one of the two at +`traceRay({ start: scene.camera.pos, ... })`. New in their place: `scene.things[i].intersect(ray)` +is `moved inside a loop but was made outside it` - the call now has the family's facts, and +`Sphere.intersect` and `Plane.intersect` keep `this` and `ray` in the record they return; that is +G2's borrowing record. As an object file (`--emit=obj`) it has 19: the `Surface` calls stay open. + +Not here, also: +- A member that says `@keeps` or `@lends` opens its family (above): a sound narrowing of the plan, + which carried declared facts through closed interfaces too (controller ruling: accepted). Such an + implementer through an interface stays the error it was. +- An abstract class's interface vtable is always open: too strict, but sound. +- A class value reinterpreted through `any` (`new C() as any as C2`, `C2`'s field an `Op2` where + `C`'s is an `Op`) is not followed; the program stops at run time under every model, before any + call through the field. +- A function value retyped inside a `ts.CreateTuple` or `ts.CreateArray` (a `HasF2` element put + into a `HasF` field) is not seen as a cast, by the member closure or by the C-callback test; no + program that compiles today reaches it (every such program is stopped by another ownership + error first). + +**After re-review** (2026-10-22): + +- **An interface made out of another one hands over its members' arguments.** A `HasF2` + (`f: (o: Op2, h: H) => number`) seen as a `HasF` (`f: (o: Op, h: H) => number`) - by + assignment, through `any`, as a parameter - lets a caller hand an `Op` to a function that calls + through `Op2`; so does a `Runner2` (`go(o: Op2, ...)`) seen as a `Runner`. Now a cast that makes an + interface value out of another interface or out of `any` opens, with the two interfaces, every + interface their members mention - the types of their vtables' slots and of what is read out of + their values, function signatures and class fields included - and theirs, recursively. Null seen + as an interface makes nothing (it has no vtable), so `return null` from a method typed `Intersection` + opens nothing. Negatives `own_err_interface_member_types`, `own_err_interface_member_methods`. +- **An override of a method of a class another module can name, or defines,** is called from that + module through the base's vtable without its facts, whatever the override's own class is: a + `public keep` of an unexported `Der extends Base`, with `Base` exported and handed out by + `export function mk(): Base`, kept its argument while an importer released it; and an importer's + `Der extends Base` of an imported `Base` (abstract or with a body) was called by the library the + same way. Now a method of a class whose base chain (the first field of a derived class's storage) + holds a nameable class, or one with no own vtable here, is callable from outside, private or not - + the private form was accepted before this section too. Negatives + `own_err_override_nameable_base` (library side), `own_err_override_imported_base` and + `own_err_override_imported_class` (importer side, the latter a private override of a concrete + base); the library-and-importer pairs run under rc, gc and none (`test-runner -shared`). +- **C callbacks.** A C function the module declares may call a function of the module back during + the call, and that function may destroy what a caller borrows (`signal(15, onSig as Opaque)`, + then `const c = gh.c; raise(15); ... c.x` with `onSig` overwriting `gh.c`). Foreign code can only + learn of a function of this module through the module's foreign surface, or through a function + turned into data; so the C-scalar rule holds only in a module whose **entire foreign surface is + scalar** and that **turns no function into data by the means the pass sees**: a `ts.Cast`, an + `inline_asm`, a `call_intrinsic` (controller ruling, fix round 5; a closed-world test, in place + of the three earlier rounds' list of the routes a function value takes out, which each review + extended). A type is scalar when it is a number, a boolean, a character, an integer or + float kind, or an enum or a literal of one. Not scalar: an opaque pointer, a reference, a string, + a class, an interface, an object, a tuple, an array, a union with such a member, a function type, + `any`, `unknown`, a rest parameter. The foreign surface, every item of which must be scalar: + - every body-less function - each parameter and the result (void is fine): a C declaration, + another module's function (declared here with `import`, or a DLL's), a `declare class`'s + method, a `@cpp` class's `@slot` declaration, and the compiler's own members of a `declare + class` (`..instanceOf`, `..new`), which take the class; + - every global declared with no initializer or imported: a `declare var`, a `declare class`'s + vtable and type-name string (so a `declare class` always turns the rule off: a sound narrowing + of the ruling's "any member"); + - every `@cpp` class's fields and its bases' (a base is a `@cpp` class of its own), with no method + of its own with a body: C++ calls one through the C++ vtable; + - every function found in a library at run time (`SearchForAddressOfSymbol`): the function type + its address is cast to. + + What turns a function into data, or makes an address of foreign memory: + - a `ts.Cast` from a type that holds a function to a type that holds none (`any`, `unknown`, an + opaque pointer, an integer, a reference, anything else but a boolean, which cannot carry one); + a class or object input counts its fields for every output but a class (its base) or an + interface (a view of it, read through its vtable only) - `>k` with `K { f: + (s: s32) => void }`, then a load of the field, is counted (final patch; before it only + `any`, `unknown` and opaque outputs counted the fields, review6r5 g1-g4, f7); a vtable's + address is the compiler's own and does not count; + - a `ts.Cast` to an opaque pointer or a reference from a type that is not itself an address of + memory this module made (`0x1234`, then `Deref(>p) = onSig`): a number, + `any`, a union; + - an `inline_asm(...)` or `call_intrinsic(...)` (`ts.InlineAsm`, `ts.CallIntrinsicOp`) + given an operand whose type holds a function, fields counted (final patch; review6r5 h1, h2: + `inline_asm("mov $1, $0", "=r,r", onSig)`, an identity intrinsic on `onSig`). + + A type holds a function when it is one, or a union's or optional's member, a tuple's field, an + array's element or a reference's pointee does, recursively; a class or object does when a field + does; an interface does when a slot of a vtable its values are made with, past the header, holds + a method or a function-typed field, or a field's type holds one (recursively: an interface field + whose interface has methods, a class field with a function field, an array of them) - the header + `.instanceOf`, the compiler's own, does not count - and an interface no vtable of this module + makes values with (one a declared function returns) holds one. The test runs after the class + storages and the interface families are known. No ordinary call is examined: a function value + given to one can only reach foreign code through a declared symbol, which the surface test + already judged by its type; the two builtins that are calls of no declared symbol, `inline_asm` + and `call_intrinsic`, are examined by their operands (above). Not here: a function reached by + name - an exported function of a library + module, which another module may hand to C - stays accepted, as it was with `@nodrops` (an + executable's exports are visible to no module, above). Negatives + `own_err_declared_callback` (`signal(15, onSig as Opaque)`; `onSig` overwrites `gh.c`, which a + caller of `raise(15)` borrows), `_global` (`let cb = onSig`), `_method` (`k.m as Opaque`), + `_tuple` (`reg([onSig, 1])`, compiled only), `_indirect` (`let sg = signal; sg(15, onSig)`), + `_wrapper` (`reg(signal)`, `reg` calling its parameter with `onSig`), `_class_method` + (`w.reg(onSig)` on a `declare class`, compiled only), `_interface_field` (a data-only `Box` + whose field is a `CB` with a method, given to a declared function through a function value, + compiled only), `_stored_global` (`declare var hook: (s: s32) => void; hook = onSig`, compiled + only), `_stored_reference` (`Deref(getSlot()) = onSig` through a declared + `getSlot(): Reference<(s: s32) => void>`, compiled only), `_cpp_class` (`addListener(new + MyL())`, `MyL` a `@cpp` class whose `on` has a body, compiled only), `_field_reference` + (`>k`, the field loaded as an integer into `signal(s32, u64)`) and `_intrinsic` + (`call_intrinsic("llvm.launder.invariant.group.p0", onSig)`, then ``). Cost: a module with any + non-scalar declaration - a C function taking a string, a struct or a pointer, a `declare class`, + a `@cpp` class with methods, an imported function - loses the rule for its scalar declarations + too (explicit `@nodrops` still works); a data-only interface handed to a declared function + (review6r4 b9) is such a declaration. The ray tracer declares four scalar functions and nothing + else. + +Teeth: with the member closure skipped, `own_err_interface_member_types` and `_methods` compile and +print `5 77777` (gc `5 5`). With the C rule kept on despite an escaping function, +`own_err_declared_callback` prints `77777 9` (gc `5 9`). With a base chain never making a method +callable from outside, every override negative compiles, and the pairs under `-mm=own` fail their +assert (`kept value still there`): the library side AOT, the importer side AOT and under the JIT +(gc passes all of them). + +The ray tracer built as a program stays at 15 errors (the same ones); as an object file, 19. +Corpus: 528 before and after, the same files. + +**Fix round 3** (2026-10-22). The member types of an interface whose values are all made from class +objects (`const r2: Runner2 = rr`, the vtable read through the object's class) were not read: the +class vtables joined the interface's vtables only after the member closure. They join before it now +(negatives `own_err_interface_member_class`, `own_err_interface_member_any`). The C-callback test is +by type (the bullet above). Teeth: with the class vtables left out of the closure, both new member +negatives compile and print `5 77777` (gc `5 5`); with the type test off, `_global` and `_method` +print `77777 9` (gc `5 9`). The ray tracer built as a program stays at 15 errors; corpus 528, the +same files. + +**Fix round 4** (2026-10-22). The C-callback test looked only at direct calls of declarations, so a +declared function called through a value (`let sg = signal; sg(15, onSig)`, a global holding it, a +wrapper's parameter, a C-returned pointer cast to a function), a `declare class` or `@cpp` method +and a foreign interface's member left the rule on (own printed `77777 9`, gc `5 9`). A call is now +judged by what it resolves to (the bullet above), interfaces count by what their vtables hold, and +a class or object cast to `any`/`unknown`/opaque takes its fields along. "An interface holds a +function" was first taken as every interface (controller ruling, option (a)): that turned the rule +off in the ray tracer through `reduce`'s helper, `___array_reduce`, whose +`f(r, v)` calls its function parameter with a `Light` - `{ pos; color }`, no method, no function +field - and the ray tracer went to 31 errors, the 16 cleared by the C-scalar rule back. Option (b), +by the vtable's slots past the header, recursively through field types, keeps it at the same 15 and +rejects every probe of the review (n7b, n7d, n7e, n7g, n8b, n8c, n9, n1b, n11, n2b, and the earlier +k1-k10, c3, n6, n10, n13, n16, n17, n18). Negatives `_indirect`, `_wrapper`, `_class_method`, +`_interface_field`. Teeth: with the call test back to direct calls of declarations, `_indirect` and +`_wrapper` compile and print `77777 9` (gc `5 9`), and `_class_method` and `_interface_field` +compile. The ray tracer built as a program stays at 15 errors (the same ones); as an object file, +19. Corpus 528, the same files. + +**Fix round 5** (2026-10-22). Round 4's test still listed the routes a function takes out of the +module - a cast, a call into foreign code - and the re-review found two it did not list: a +function stored into memory C reads (`declare var hook: fn; hook = onSig`; `Deref(getSlot()) = +onSig` through a declared `Reference`; an `Opaque` a C function returned seen as a class with +a function field, then a store; a `declare class` with a function field) and a `@cpp` object handed +to C++, whose C++ vtable slots are this module's methods (`addListener(new MyL())`, also as +`Opaque`, also through a C++ method). The test is inverted (the bullet above): instead of naming +what takes a function out, the rule holds only where nothing can - every declared, imported, +`@cpp` or run-time-found symbol is scalar, and no cast turns a function into data or makes an +address out of a number. The enumeration it replaces is gone: no call is resolved for it, no +argument's type is walked. Also from the re-review: a cast to a boolean (`if (s)` on an interface +with methods, `if (cb)` on an optional callback, `cb?.(1)`) no longer turns the rule off; the +earlier text's "no route of the value matters" was false, and the `.instanceOf` slot and a field +holding only defined functions are no longer part of any call test. Negatives `_stored_global`, +`_stored_reference`, `_cpp_class`. Teeth: with the surface test skipped, `own_err_declared_callback` +(`signal(15, onSig as Opaque)`) compiles and prints `77777 9` (gc `5 9`), and the three new +negatives, review6r4 s8 and the `@cpp`-through-an-integer probe (`addListener((new MyL() as +Opaque))`, rejected only by the `@cpp` clause) compile. Every probe of rounds 1-4 is still +rejected (review6r1 c3, review6r2 k1-k10, review6r3 n1b n2b n6 n7b n7d n7e n7g n8b n8c n9 n10 n11 +n13 n16 n17 n18, review6r4 a1-a8, b2-b8, e1-e3, s1 s3 s5 s6 s8 s9, c1 c2 c3); d1, d3, d4, d5, d6, d6c +keep the rule; so does a program declaring only scalar functions. Newly rejected by the +whitelist, as its cost: review6r3 n0 (an unused `declare function signal(sig: s32, handler: +Opaque)`), review6r4 b1, b1b, b5 and b9 (a class or a data-only interface given to a declared +function). The ray tracer built as a program stays at 15 errors (the same ones). Corpus 528, the +same files. + +**Final patch** (2026-10-22, user decision: keep the implicit C-scalar rule with these two fixes, +then stop). Re-review 5 found two ways a function became data that the cast test did not see: a +class or array with a function field cast to an integer or a reference (`>k`, +`>>>ks`, `ks`; the field then loaded as an integer into +the all-scalar `signal(s32, u64)`; own printed `77777 9`, gc `5 9`) - the fields of a class or +object input were counted only into `any`, `unknown` and opaque outputs, a narrowing left from +round 3 - and `inline_asm(...)` / `call_intrinsic(...)`, calls of no declared symbol that +take any operand (`inline_asm("mov $1, $0", "=r,r", onSig)`; an identity intrinsic on +`onSig`). Now a cast counts the input's fields for every output but a class or an interface, and +an `inline_asm` or `call_intrinsic` with an operand whose type holds a function (fields counted) +turns the rule off. Negatives `own_err_declared_callback_field_reference` (review6r5 g2) and +`_intrinsic` (h2), both runnable. Teeth, one switch per fix: with the fields counted only into +`any`/`unknown`/opaque again, `_field_reference` compiles and prints `77777 9` under own (gc `5 +9`) while `_intrinsic` stays rejected; with the `inline_asm`/`call_intrinsic` clause off, +`_intrinsic` prints `77777 9` while `_field_reference` stays rejected. g1-g5, f7, f9, f10, h1, h2 +rejected; d1-d9 and the scalar-only control keep the rule. The ray tracer built as a program stays +at 15 errors (the same ones); as an object file, 19. Corpus 528, the same files. + +## 32. Borrowing records (G2 of the ray tracer plan), 2026-10-22 + +`{ thing: this, ray: ray, dist: d }` returned as an `Intersection` was `an object literal that holds +a value it owns is moved only into a local or returned as it is` (§27): the box's release would free +`this` and `ray`, which are the caller's. So were the ray tracer's call-argument temporaries `{ +start: pos, dir: rd }`, `{ start: pos, dir: livec }` and `{ start: scene.camera.pos, dir: +getPoint(...) }`, and `const r: Ray = { start: this.pos, dir: new V(1) }`. Such a literal is now a +**borrowing record** (USER decision): Rust's `struct Hit<'a> { thing: &'a Th, ray: &'a Ray, dist }`. +Its box is owned like any fresh block - by the local that holds it, the temporary, or, returned, the +caller - and its release frees the box and the fields it owns and leaves the borrowed ones to their +owners. What it borrows bounds every use of it. (The plan calls the result "a borrow" of its sources; +a caller that owned nothing would leave every box and its fresh fields to nobody, and the plan's +own interplay - a record moved on some paths only, read through its new owner, released by a +routine that frees only its fresh fields - is an owner's. The bound is a borrow's: the same chain +and the same errors as a call's result that borrows its arguments.) + +- **Which literals.** The signature pass classifies each object literal box seen only as interfaces + (`classifyRecordBoxes`, in every round of the facts fixpoint, since a field's call may turn out to + return a borrow): each field that holds a block is *owned* - a fresh value, which moves in as in + §27 - or *borrowed* (`recordFieldSource`): `this` or a parameter, seen through views; a read out + of a place, or a call's result that borrows an argument; an owning local's value (`let d`); a + captured variable's value (a closure body's read of a cell); an immortal global or literal. A + field is never both, and never a string (strings are copied, §22). A box with a borrowed field is + marked, and so is every interface made of it (`__own_record_borrowed_fields`, the fields' tuple + positions); a store into a borrowed field keeps nothing (`keeps`, so `this` and `ray` are not + kept parameters of `hit` any more). +- **The release travels with the value** (the design's builder check). The interface's tag names a + descriptor of its own (variant `r...`) whose release routine (`tsrelrec__`) frees + the box and the fields it owns and skips the marked ones; a non-escaping closure's box + (`__own_borrows_captures`) is the precedent. Every release of the record - a temporary's, a + local's at the end of its scope, §30's edge release, an overwrite of a local, the caller's - reads + the tag, so records made at + different sites (`Sphere.intersect`, `Plane.intersect`) may meet in one local. A box that is seen + as anything but interfaces is not classified (its static object type's routine would free every + field), and stack promotion refuses a marked box (its `ts.ReleaseContents` would give back every + field); today's escape analysis refuses every box seen as an interface anyway ("converted"). +- **The facts.** `__own_result_borrows` is a set (`DenseI32ArrayAttr` of argument indices, the + receiver 0 for a method; `own_facts::resultBorrowsAll`). Alone it is a plain borrow and names one + argument, as before (`resultBorrows` keeps that index; a declared `@lends` of several is a plain + borrow of each). With `__own_result_record` the result is a borrowing record: the caller owns the + box, and it may hold a borrow of each argument named. A function's result is a record when every + heap value it returns is one - a box made there, or a record call's result - through views, + merges and the owning locals that hold it (`recordParamsOf`), and every field it borrows is a + parameter's, `this`'s, or a read out of one: those parameters are the set. A record on one path + and anything else on another (a plain borrow, a fresh object) gives no facts. `closest` returns + `bestHit`, which holds `sc.ts[i].hit(ray)`'s records: its result borrows `ray` and `sc`. + `@lends` may name several parameters: `@lends("a", "b") pairOf(a, b): Pair { return { a: a, b: b } + }` (`own_lends`). A declaration's `@lends`, seen from another module without its body, is a plain + borrow: a record's box then leaks at that caller, and nothing is freed twice. +- **Families** meet by agreement, as before (`meetFacts`): members whose sets or record-ness differ + are demoted. The union of the sets would be sound for records (the caller would bound the result by + more arguments), but not across a record and a plain borrow, whose callers release different + things; agreement is what the ray tracer needs (`Sphere.intersect` and `Plane.intersect` both lend + `this` and `ray`). +- **Where the record is made** (`classifyRecords`, `checkBorrowingRecords`): every use of it and of + what is read out of it, through its views, merges and the locals that hold it (`walkRecord`), is a + use of each borrowed field's source. A read out of a place has them as its own uses + (`walkBorrowed`, through the field's store, now a borrow), so a drop of the place before a use is + `borrows a field but is used here after ...`. An owning local's value is bounded by the local's + ends (`borrows 'd' but is used here after 'd' is released or overwritten`), a captured variable's + by whatever may assign it (an assignment through a cell, a call that may drop), a parameter the + function keeps by its moves. `this` and other parameters are the caller's for the whole call. + rc's retain of the record read goes (`holdsConstantsOrMoves`); its fresh fields move in against it + as an owning record's do. The record stored anywhere but a local (a field, an element, a global, a + capture), given to a callee that keeps it, or written through (`r.start = ...`: the overwrite would + release a borrowed value, and the release would leave the one written) is `... and cannot be + stored, returned or captured`. Returned, every field it borrows must be a parameter's that the + function's facts lend; a local's value, or a function whose facts are lost (an exported one + without `@lends`), is the same error. +- **Where a call returns one** (`checkRecordResults`): the result is fresh (`isFresh`, whatever the + call goes through), so its moves and releases are the ordinary ones - `let h = ...`, §30's move + into `bestHit` on some paths with its release on the other edges, §29's reads through the new + owner - and each release is the per-site routine. It may hold a borrow of each named argument, + somewhere under it, so the chain `checkPlaceRead` builds for a borrowing call, now over all of + them (`chainOf`), bounds every use: `borrows the result of 'A.hit' but is used here after it may + be released or overwritten`. Kept, it is `borrows the result of ... and cannot be stored`; + returned, its arguments must be parameters this function lends. rc's retain of a fresh argument + right before the call is no end there: the box's own check decides it. A call's result that + records a value is followed by `derivedFrom` and by §30's taint. A method reached through an + interface is named `.` in these errors (`own_err_interface_result_outlives` + reads `the result of 'First.first'`). +- **Iterators** (§31): no `next()` or field function whose result is a record is marked + `__own_fresh_result`; a record's bound applies. + +`own_borrowing_record` is the brief's program: `A.hit` and `B.hit` (`null` on some paths) return +records through the closed interface `Th`, and `closest` keeps the best in a local and returns it, +1000 times with churn between, under own, rc, gc and none, AOT and JIT, and on Linux; own emits no +counting. `own_borrowing_record_arg` covers the addendum: call-argument temporaries with two +parameters, a parameter and a fresh value, a field read and a fresh value, an owning local's value +and a captured parameter (the ray tracer's `addLight`), a `let` read into a field, `const` and `let` +locals holding a record, and a method returning one; the borrowed values are read again after the +records are gone. The brief's program as written does not compile in any model (`k = 1` is an +`s32`, which does not match the interface's `number`, and `cond ? null : {...}` cannot be cast to +`Hit`), so the test declares `k: number` and returns `null` from an `if`. + +Negatives: `own_err_borrowing_record_stored` (`keep.h = sc.ts[0].hit(ray)`, a global's field), +`_outlives` (`a = new A()` before `h.thing.k`), `_kept_arg` (a temporary given to a callee that keeps +it), `_let_outlives`, `_cell`, `_kept_param`, `_place_overwritten`, `_returned_local`, `_open`, +`_field_written`, `_borrowed_again`. `own_err_record_literal_let` (§27: a `let` read into a field) is +a correct program now - the record borrows the `let`'s value, and the `let` keeps its release - and +is gone; its shape is in `own_borrowing_record_arg`. `own_err_lends_two_params` now reads `'@lends' +of 'pick' names 'a', 'b', but its body returns a borrow of 'a'`. A method's `@lends` reaches its +class's declaration as a 64-bit mask, so since the final review (M4, 2026-10-23) an index past 63 is +an error rather than a borrow an importer silently never learns of: `own_err_lends_index_64` (a +method of 64 parameters lending its last, argument 64), "'@lends' of 'K.pick' names its argument +64, but a method's '@lends' can name only its arguments 0 to 63". The compiler before it accepted +the program; the 63rd parameter (argument 63) is still accepted, under own, rc and gc. + +Teeth (temporary `OWN_TEETH` switches, removed): with the per-site routine dropped (every record's +interface given its type's descriptor), the AOT runs of `own_borrowing_record` and +`own_borrowing_record_arg` fail with `0xC0000005` and `0xC0000374`; with only the boxes of records +that are not returned (the call-argument temporaries) given their type's routine, +`own_borrowing_record_arg` fails with `0xC0000374` and `own_borrowing_record` passes (its only +temporary owns both fields). The rules each have a negative that reads a freed block with the rule +off: the caller's bound skipped, `_outlives` prints 77777 +and `_stored` 6.95159e-310 (gc 1 and 1); a local's, a captured variable's and a kept parameter's +ends skipped, `_let_outlives`, `_cell` and `_kept_param` print 77779 (gc 3); the return check and +the local's ends skipped, `_returned_local` prints 77777 (gc 1), and `_open` compiles; the kept +check skipped, `_kept_arg` prints 77777 (gc 3), and with the place read's hook skipped too, +`_field_written` exits `0xC0000374` (gc `8 3`); the hook alone skipped, `_place_overwritten` +prints 77778 (gc 4). `_borrowed_again` is the hole review of this rule found: `const h2 = id(h)` +was bounded by `h`'s ends, not by what `h` borrows; before the fix it compiled and printed 77777 +(gc 1). With stack promotion's refusal skipped nothing changes: its escape analysis refuses every +box seen as an interface already ("converted"). + +Measured: 2 million iterations of the brief's loop and a call-argument temporary (about 7 blocks +each) peak at 4.2 MB under own, as under rc (gc 6.2 MB), AOT with `--opt`. + +Corpus (`test/tester/tests/*.ts` under `-mm=own --no-default-lib`, `--emit=obj`, Windows): 528 of +693 before this section and 528 after, the same files. + +The ray tracer built as a program (`--emit=exe`): 15 errors before this section, 10 after. +Cleared: the records `Sphere.intersect` and `Plane.intersect` return (148, 163); `moved inside a +loop` at `scene.things[i].intersect(ray)` (203: `ray` and the receiver are borrowed now, not kept); +`getReflectionColor`'s `{ start: pos, dir: rd }` (243) and `addLight`'s `{ start: pos, dir: livec }` +(250); `scene.camera.pos` stored into `render`'s literal (279:43). That literal (279:29) is now +`borrows a field but is used here after it may be released or overwritten`: the record holds +`scene.camera.pos` across `traceRay`, which may drop because `getNaturalColor` calls `reduce` with +a closure. New for the same reason: `traceRay`'s `this.shade(isect, ...)`, where `isect` borrows +the result of `intersections` (227). The rest are G5's (`reduce`: 237-239 and 266 across +`getNaturalColor`, the generated `___array_reduce`, `addLight` returning its parameter on one path) +and the `getPoint` read (279:75), present before this section (G8). With `reduce` replaced by a +loop, 2 errors remain: `addLight`'s mixed return and the `getPoint` read. + +Not here: +- A record whose box is seen as an object rather than an interface (`const o = { a: this.p }` with + no interface type) is not classified: its static type's routine would free the borrowed field. + §27's error stands. +- A field written through a borrowing record is an error even when it is a field the record owns. +- A record's borrowed field may not hold a string. +- Plain borrows are still of one argument (`c ? a : b` is no borrow), and do not flow through an + owning local assigned in a loop; only records do. + +## 33. `reduce` takes its accumulator (G5 of the ray tracer plan), 2026-10-22 + +`a.reduce((acc: V, v: V) => V.plus(acc, v), new V(0))` was `'init' is a parameter, kept here, but +-mm=own cannot make the callers of '___array_reduce<...>' give it up` (the generated helper's `let r = +init`, then `r = f(r, v)`), and `borrows an element but is used here after it may be released`: the +call through `f` went to an unknown function, which may drop anything. A callback that returns its +accumulator on some paths (the ray tracer's `addLight`, `return col` when in shadow) was `returns a +borrow of its argument on some paths and another value on others`. Now the initial value moves in, +each callback is given the accumulator to keep, and its result is the next one, like Rust's +`fold(init, |acc, x| ...)`, whose closure takes `acc` by value. + +- **A parameter returned on some paths only is kept** (`mixedReturnParam`, OwnershipSignaturePass). + When every value a function returns that holds a block is either one parameter itself (its slot's + read, through views and merges) or one the function owns, and both occur, that parameter is kept + (`__own_params`): the callers move the argument in, the path that returns it moves it out, and + every other path releases it. Not when the parameter is returned on every path (a borrowed result, + as before), two parameters are returned, a part of one is (`acc.next`), a borrowing record is, or + the value is a string (copied, spec 22) or a handle. A caller that still uses the argument after + the call is `is used here after its value was moved` (`own_err_mixed_return`, rewritten: + `print(pick(c, true).n, c.n)`). A function whose facts are lost (an exported one, one used as a + value) keeps today's error in its body. +- **Its releases are placed statically** (OwnershipInferencePass, `releaseKeptParamsAtExits`). Such + a parameter is named in `__own_params_released`: a kept parameter its body never takes into a + field, an element, a global, a keeping call or an assigned owning local (one it does take there + keeps the old rule, the move must reach every return: `own_err_param_kept_some_paths`). MLIRGen + makes no owner of a parameter, so the inference puts a release of its slot before each + `ts.ReturnInternal` and each `ts.ThrowCall` that leaves the function (not one whose thrown value + is read out of the parameter), as MLIRGen puts an owning local's. The move's own check + (`checkSlotMove` with §30's placement) then erases every release the moved value reaches and + places one on each edge into the join from a path that did not move it; nothing may use the value + where the paths have joined (§30's safety condition, followed through what was read out of it). + `pick`'s `V.plus(acc, v)` arm releases `acc` before the arms join; an early return, a `break` out + of a loop, a return from inside a loop or from a `try` with a `finally`, a `throw`, each release + it once. No run-time flag, no pointer comparison. A parameter kept this way and moved by two + receivers (`if (n > 5) return a; if (n < 0) return a;`) is still `used here after its value was + moved`. +- **A parameter read into an owning `let` that is given another value is kept** (`keeps`): `let r = + init; ... r = f(r, v)` cannot borrow `init`, since the assignment releases what `r` holds, so + `init` moves into `r` (the move dominates every return, as for any kept parameter, and rc's + `ts.RetainSlot` goes with it). Not a string: the local copies it. `___array_reduce`'s source is + unchanged; this rule makes its accumulator a moved local. +- **Callbacks** (`collectCallbackFamilies`). A function-typed parameter of a closed function (private, + every use of its symbol a direct call) that is only ever called, and to which every caller passes a + function this module defines - its symbol, through casts between function types, or a closure over + a capture box built of it - reaches exactly those functions: the parameter's family. Its members + are closed when every other use of their symbols is a direct call (`callbackPassing` in `findOpen`), + and a call through the parameter is resolved to them (`resolveCallback`): it gets the facts they + agree on, and `__own_no_drops` when none of them drops. A closure's body takes its capture box + first, so its parameter k is the call's argument k - 1 (`shiftFacts`); a fact about the box itself + agrees with nothing. `__own_params = [0]` on `___array_reduce`'s `f(r, v)` moves `r` in; the + assignment's release of the old `r` goes with the move (#528), and `r` owns the result. +- **Agreement by promotion** (`meetCallbacks`). `own_reduce` passes `(acc, v) => V.plus(acc, v)`, + which only borrows `acc`, and `pick`, which keeps it, to the one `___array_reduce`. A kept + parameter of some member that every other member only borrows - a plain slot, not a string or a + handle, not lent by the result - is given to all of them to keep; the others release it on every + path (`__own_params_released`). Any other disagreement demotes every member, as a class family's + are, and their bodies fail with the note `a function passed in its place disagrees` + (`own_err_callback_disagrees`: `pick` beside `(acc, v) => acc`). A member a callback family demoted + is never promoted again, so the meet settles. Class and interface families still agree or demote + (addendum): promotion is only for callbacks; a closed interface whose implementations both return + their accumulator on some paths agrees and keeps it, one beside a plain borrow demotes both. A + promoted member called directly moves its argument in too (`own_reduce`'s `add`; + `own_err_callback_promoted_used` reads the argument after). +- **The immortal start value.** `a.reduce(pick, K.zero)` moves the immortal `K.zero` (§25) in: its + retains are erased, and the releases `pick` and `main` make of it are skipped by its header. + `own_reduce` reduces `[new V(5), new V(7)]` with `pick` from `K.zero`, so `pick` returns `K.zero` + itself and `main` releases it at its end; after churn `K.zero.x` reads 0, and is asserted. No path + writes it: `V.plus` makes a new object. +- `___array_reduce` has no foreign surface, and the C-scalar rule (§31) does not read it: nothing + changed there. + +`own_reduce` runs the brief's program (`r.x, q.x` is `6 3`), a reduce that returns the immortal +start value, a named callback `add` that only borrows its accumulator beside `pick` (promoted), and +20,000 iterations of an early return, a return from inside a loop, a return from a `try` with a +`finally`, a `throw` that the caller catches, and a direct call of `add`, each read after churn, +under own, rc, gc and none, AOT and JIT, and on Linux; own emits no counting, and +`--verify-ownership` is quiet on it under own and rc. Negatives: `own_err_mixed_return` +(rewritten), `own_err_mixed_return_join_use` (a field read out of the kept parameter before the join +and used after it: `is moved here on some paths only`), `own_err_callback_escapes` (`pick` also +called through a `let`: `cannot make the callers of 'pick' give it up`), `own_err_callback_disagrees`, +`own_err_callback_promoted_used`. `own_err_borrowed_result_escapes` passed `firstOf` straight to +`apply`; the call through `g` now reaches only `firstOf` and gets its borrowed result, so the program +is correct (it prints what gc prints). It now passes `firstOf` through a `let`, and fails as before. + +Teeth (temporary `OWN_TEETH` switches, removed): with the exit releases kept on the paths that +return the parameter (the returning path releases it too), the AOT run of `own_reduce` exits +`0xC0000374` (heap corruption), and so do the brief's two reduces alone and the early-return, loop +and `try` returns alone; with the call through the callback given no kept parameter +(`___array_reduce` releases the accumulator it handed to a callback that keeps it), the AOT run of +`own_reduce` and of the brief's reduces alone exit `0xC0000374`, and the direct returns alone pass. + +Measured: 2 million iterations of both reduces and an early return (about 9 blocks each) peak at +4.9 MB under own, as under rc (gc 6.5 MB), AOT. + +Corpus (`test/tester/tests/*.ts` under `-mm=own --no-default-lib`, `--emit=obj`, Windows): 528 of +693 before this section and 528 after, the same files. + +The ray tracer built as a program (`--emit=exe`): 10 errors before this section, 1 after. Cleared: +`addLight`'s mixed return (253) and the two in the generated `___array_reduce` (its kept `init`, and +the element its call through `f` borrows); and, since the call through `f` now has the closure's +facts and destroys nothing, so does `getNaturalColor`, and the reads held across it or across what +calls it: `scene.lights` given to `reduce` (266), `shade`'s three (237-239), `traceRay`'s +`this.shade(isect, ...)` (227) and `scene.camera.pos` in `render`'s literal across `traceRay` +(279:29). Left: the `scene.camera` argument of `getPoint` in `render` (279:75, G8), now reported +against `Color.toDrawingColor(color)` (280:25), which builds and calls its `legalize` closure and so +may drop. It was there before §32. It does not reproduce in three smaller programs (a closure over +`camera` called in a loop beside a call that may drop, inside a record argument, in a method), and +a method in place of the closure fails the same way, so it is not the closure's call. + +On a scratch copy whose only other change is `legalize` made a module-level function (so +`toDrawingColor` destroys nothing; not one of the plan's source edits), the program compiles under +own as an executable and runs: checksum 38837369 as under rc, exit 0, peak 4.9 MB as rc's 4.9 MB on +Windows, and 4.3 MB (rc 4.4 MB) on Linux, where glibc reports no bad free. + +**After review** (2026-10-22). The `let` rule above accepted a read of freed memory (found in review): +`const inner = p.v; let r = p; r = new N(new V(2)); ... inner.x` moved `p` into `r`, and the +assignment freed `p` and `p.v` while `inner` still read them (own printed 779000, gc 501500; BASE +rejected the program, since `p` was not kept). The same shape with a field as the new owner, `const +inner = p.v; h.n = p; h.n = new N(...); inner.x`, was wrong before this section too (BASE and HEAD +both printed 779000): a borrow read out of a kept parameter had no ends at all - the parameter is the +caller's for the whole call, unless the function keeps it. Now a borrow read out of a parameter the +function keeps, before the parameter moves, is bounded by the new owner from the move on, as a read +through that owner would be (§29's destination chain, `keptParamOutlives` in `checkPlaceRead`): the +owner's ends and the drops of its place after the move end it. A move into anything that is not such +a place (a call that keeps it, a return, a push) ends it at the move, and so do the parameter slot's +releases (the exit releases above). A borrow read before the move and used while the new owner +still holds it is accepted (`h.n = p; churn(); inner.x`, and `let r = p; inner.x + r.v.x` before `r` +is given another value). Negatives `own_err_kept_param_let_overwritten` and +`own_err_kept_param_field_overwritten` (each built through a `@noinline` factory, with churn: an +argument made at the call site can be stack-promoted, and its release then frees nothing): `borrows +a field but is used here after it may be released or overwritten`. Tooth: with the bound skipped, both +compile, print 779000 instead of 501500 (gc), and fail their assertion. `own_reduce`, the ray tracer +(1 error, the same) and the corpus (528, the same files) are unchanged. It narrows one shape that +was correct: a borrow read out of `p` and used in a `finally` after `return p` (the return ends it at +the move, though the caller then holds the value) is now rejected; no suite test or corpus file has +it. + +**After re-review** (2026-10-22). A call that keeps one argument and is given the same value again +through another operand let the callee free what it still borrowed: `f(p, q) { const inner = q.v; +let r = p; r = new N(...); inner.x }` called as `f(n, n)` printed 631370 (gc 501500), a variant +crashed, and the same with `n` an owning `let` printed 639140 - BASE rejected them, since `p` was not +kept, and the `let` rule above made them reachable. The same through a field keep (`f(h, n, n)` with +`h.n = p; h.n = new N(...)`, then `q.v`) was wrong before this section too. The caller's +use-after-move checks skipped the taker itself as a use: `movesOf` for a fresh or folded value, +`checkSlotMove`'s reads for an owning slot. Now (`callGivenAgain`) a call that keeps an argument is a +use after the move when another of its operands - the value again, a view of it, something read out +of it (`f(h, n, n.v)`), another read of the same slot, its receiver - may point into the moved block: +`is used here after its value was moved`. `___array_reduce`'s `f(r, v)` passes the accumulator once. +Negatives `own_err_kept_and_borrowed_arg` (a folded `const`), `_slot` (an owning `let`) and `_field` +(the field keep), each through a `@noinline` factory with churn and an assert. Tooth: with the check +skipped, the first two print 779000 instead of 501500 and fail their assertion, and the third crashes +(0xC0000005). `own_reduce`, the ray tracer (1 error, the same) and the corpus (528, the same files) +are unchanged. + +**After the second re-review** (2026-10-22). One value at two kept positions of one call (`f(n, n)` +with both `p` and `q` kept: two `let`s given other values, a field and a `let`, two fields, two +pushes) is two owners of one block, each of which frees it; `callGivenAgain` passed over every kept +operand rooted at the moved value. Now it passes over one - the move itself - and a second is a use +after it (`is used here after its value was moved`). The `let` forms printed wrong or crashed only +since this section's `let` rule (BASE rejected them); two fields and two pushes were wrong at BASE +too. Negatives `own_err_kept_twice` (two `let`s), `_field` (a field and a `let`), `_push` (two +pushes), each through a `@noinline` factory with churn and an assert. Tooth: with every kept position +passed over again, they exit `0xC0000374`, `0xC0000005` and `0xC0000374`. Distinct objects (`f(n, +m)`, `f(h, n, m)`) stay accepted. Also, for an owning slot the other reads counted are only those +that reach the call without crossing an assignment of the slot (or its declaration): `const arr = +[n]; n = mk(5); f(n, arr)`, which reads the old value into `arr`, is accepted again, as before the +first re-review. That is not "holds the moved value" by itself; it is, once the move's own read is +held to the same condition (the third re-review, below). + +**After the third re-review** (2026-10-22). The read filter above dropped every read of the old value +when the slot was assigned inside the call's own argument list, the move's read among them: +`let n = mk(i); f(n, n, (n = mk(5), 0))` with `p` kept and `q` borrowed was accepted and crashed +(0xC0000005; the previous round rejected it). Underneath, the assignment alone is wrong, with no +second operand: `f(n, (n = mk(7), 0))` with `p` kept in a `let` (BASE rejected it, since `p` was not +kept) or in a field (wrong at BASE too) frees the old `n` at the assignment's release and then hands +it to `f`. Now `checkSlotMove` rejects a move whose own read does not reach the taker without +crossing an assignment of the slot (`!reachableAfter(load, taker, kills)`; the taker itself may be +the slot's assignment, `n = n`, which is the destination, not an assignment between), reporting the +assignment between them as `is used here after its value was moved` (the call is "value moved here"). That +test asks whether every path from the read to the call is assigned; it misses an assignment on one +arm (the fourth re-review, below), and the read filter is not made exact by it: a read made after +the assignment on the assigning arm holds a new value and still passes (an over-rejection, harmless). +`f(n, (n = mk(i), 0), n)` (the old `n` kept, the new one borrowed) stays rejected, +by this check. Negatives `own_err_kept_arg_assigned_in_call` (`f(n, (n = mk(7), 0))`, `p` kept in a +`let`) and `own_err_kept_and_borrowed_arg_assigned_in_call` (`f(n, n, (n = mk(5), 0))`, `q` +borrowed), each through a `@noinline` factory with churn and an assert; gc and none print 501500. +Tooth: with the check skipped, both, and the field-keep form, compile and exit `0xC0000005` with no +output. `const arr = [n]; n = mk(5); f(n, arr)`, distinct objects at two kept positions, `own_reduce`, +the ray tracer (1 error, the same) and the corpus (528, the same files) are unchanged. + +**After the fourth re-review** (2026-10-22). The check above was an all-paths test: with the +assignment on one arm of a conditional in the argument list - `f(n, i % 2 == 0 ? (n = mk(7), 0) : +0)`, the same through `&&` or `||`, or with an `await` on the other arm - the other arm is a path +from the read to the call that crosses no assignment, so the move was accepted, and on the assigning +arm the old `n` was freed before `f` kept it (exit 139; BASE rejected the `let` forms, and the +field-keep form was wrong at BASE too). Now the test is existential: the move is rejected when some +assignment of the slot other than the taker lies on a path from the move's read to the taker +(`reachableAfter(load, a, varOp) && reachableAfter(a, taker, {load, varOp})`), reported at that +assignment with the same text; the all-paths test stays as a fallback (reported at the taker), and a +read and taker `reachableAfter` cannot order (one statement kept as a region, or outside the body; +unreachable today, since the moved-in-loop check takes those first and no op of a body holds a +region by this pass) now refuse instead of passing. Shapes that stay accepted, and why the predicate +leaves them alone: an assignment before the read, with the read between it and the call (`n = mk(i); +f(n, 0)` in a loop over a slot declared outside it, `f((n = mk(i), n), 0)`, `const arr = [n]; n = +mk(5); f(n, arr)`); an assignment after the call, conditional or not (the way back to the taker +crosses the read, or the declaration); `n = n`, `n = (n = mk(5), n)`, `n = acc(n, i)`. Negatives +`own_err_kept_arg_assigned_on_arm` (`i >= 0 ? (n = mk(7), 0) : 0`) and `own_err_kept_arg_assigned_in_and` +(`(i % 2 == 0 && (n = mk(7), true)) ? 0 : 0`), each through a `@noinline` factory with churn and an +assert; gc and none print 501500. Tooth: with the check skipped, both, and the field-keep form with +the conditional assignment, compile and exit `0xC0000005` with no output. `own_reduce`, the ray +tracer (1 error, the same) and the corpus (528, the same files) are unchanged. + +Not here: +- A slot assigned inside the argument list of a call that keeps it (`f(n, (n = mk(7), 0))`, + with or without a second operand, unconditionally or on one arm) crashes under rc as well (exit + 139): rc releases the slot's old value before the store, ahead of the callee that keeps it. That is + rc's codegen, out of scope here; own rejects these shapes since the third and fourth re-reviews. +- Unwinding out of the function - a call that throws outside a `try` - before a kept parameter is + moved leaks it, as it leaks an owning local; so does a `throw` of any value computed from it, even + a number (`throw acc.x`): the release before a leaving throw is skipped when the thrown value is + computed from the parameter at all, where only one that can hold its block needs it. +- After `r = f(r, v)` moves `r` into the call, `r`'s slot still holds the moved pointer until the + call's result is stored into it. If `f` throws, the slot dangles. That is benign only because + owning locals are not released on unwind; a change that releases them there must not release a + slot whose value a call took. +- A callback passed through a `let`, a field or a global, a member whose symbol is used anywhere + else (`a.reduce(V.plus, ...)`: a static method is a vtable entry), a parameter that is passed on + rather than called, an exported or `@keeps`/`@lends` function: no family, as before. +- The default library's `reduce` (design G5: a follow-up). + +## 34. A direct call's result, and the ray tracer (G8 of the ray tracer plan), 2026-10-23 + +After §25-§33 and the merge of main, the ray tracer's own copy +(`benchmarks/raytrace/rtbench_own.ts`: `rtbench.ts` with the plan's two source edits, +`Shared` surfaces and a fresh vector from `Plane.normal`) had one error left, built as a +program: `'this value' borrows a field but is used here after it may be released or overwritten` at +`render`'s `getPoint(x, y, scene.camera)`, with `Color.toDrawingColor(color)` on the next line as +the place it may be released. G8's other items (`Camera`'s constructor, `Sphere.normal`, `shade`'s +ternary, `let d = isect.ray.dir`) were cleared by §28-§33. + +The cause was not the closure. `walkBorrowed` follows a borrow through every value made from it, and +took the result of any call it is given to as one of them, since a result may point into an +argument. So the vector `getPoint` makes was walked as part of the borrow `scene.camera`; its uses +(through the record given to `traceRay`) were uses of that borrow; and `toDrawingColor` - which +builds and calls its `legalize` closure, so it has no `__own_no_drops` - may destroy anything +reachable from a parameter, `scene.camera` among it. The minimal program is `const p = gp(x, +sc.cam); const c = drop(p); use(p)` with any function `gp` that returns a new object and any `drop` +that may destroy. It compiles when nothing after the result may destroy: with `const legalize` in +place of the ray tracer's `let legalize`, `toDrawingColor` has `__own_no_drops`. + +- **A direct call of a function this module defines ends the walk at the call** (`walkBorrowed`, + OwnershipInferencePass), as a call marked `__own_fresh_result` already did (an iterator's + `next()`, another own module's `..new`). The test is `isFresh` of its one result - a + `ts.SymbolCallInternal` or `ts.Invoke` naming a `ts.Func` with a body, with no + `__own_result_borrows` - and the call has neither `__own_result_bounded` (a generator's state that + borrows its arguments) nor `__own_result_record` (a borrowing record, §32): those are still + walked. The call itself is still a use of the borrow for as long as it runs. Sound because such a + body hands back nothing that points into what it was given but what its facts name: returning a + borrow of a parameter, of a part of one, or a closure, bound method, array or record over one is + either a fact its callers see or an error in the body (`borrows a field and cannot be stored, + returned or captured`, or, when its callers cannot all know the fact, §31's body errors). A string + is no exception: one read out of a parameter and returned is not copied but is a borrow fact like + any other (`function nm(c: Cam) { return c.name; }` gets `__own_result_borrows`, so `nm(sc.cam)` + is still walked, and its result read after `drop(sc)` is "borrows the result of 'nm' but is used + here after it may be released or overwritten"). Since the fix-up of 2026-10-23 this test and the + `__own_fresh_result` one are one helper, `callResultIsOwn`. +- **Not widened:** a call through a value, through an interface, through a callback family or to a + declared function is still walked (isFresh is false), and so is a method call: a class method is + called through `ts.ThisVirtualSymbolRef`, so `sc.cam.copy()` and `this.point(x, sc.cam)` keep the + error. That is a sound over-rejection: the signature pass resolves those calls, but the inference + does not learn that a resolved call's result is owned (a follow-up). The ray tracer's `getPoint` + is a closure, whose call is a direct call of its lifted function. + +`own_fresh_call_result` reads `scene.cam` into a closure's call, a function's call and a record +given to a method (`{ start: scene.cam.f, dir: getPoint(x, scene.cam) }`), then calls a `toDrawing` +that may destroy, churns, and uses all three results, in a loop; it prints 124 under own, rc, gc and +none, AOT and JIT, with no counting under own. Before this section it had three errors (54:26, +54:32, 51:61). Negatives `own_err_call_result_bounded` (a generator's state made from `s.cam`, +iterated after `replace(s)`) and `own_err_call_result_record` (a borrowing record `{ v: c.f, n: 1 }` +made from `s.cam`, read after `replace(s)`), each through a `@noinline` factory with churn and an +assert; gc, rc and none print 5 and 6. Tooth (a temporary `OWN_TEETH` switch, removed): with every +call's result ending the walk, `own_err_call_result_bounded` compiles and exits 139 (gc 5); +`own_err_call_result_record` stays an error through the record's own bound (`borrows the result of +'hit'`). The positive cannot fail with the rule widened: its results really are fresh. + +Probes (own as an executable with `--opt`, before the rule / after / with the tooth): a lifted +closure returning `sc.cam.f` or a captured borrowing `let`, an exported function returning `c.f`, +one returning a mortal global's field, a callback family with a member returning its argument, a +method returning `this` or `this.f`, a virtual override returning `c.f`, `id(c).f`, a `V | null` of +`c.f`, an `any` holding `c.f`, an element of an array parameter, a function that makes an unrelated +object and returns `c.f`, a `Shared` parameter's `s.value`, an async function returning `c.f` +or, after an `await`, `new V(c.f.x)`: rejected before and after. An async function that reads +`c.f.x` after an `await`, awaited after the drop: accepted before and after, printing what gc +prints (async is outside this rule either way). A generator over `c.f.x`: rejected before and +after; with the tooth it compiles and crashes. Newly accepted, and printing what gc prints: a +function returning `new V(c.f.x)` or `[c.f.x]`, the same called inside a `try` (a `ts.Invoke`), and +a closure called with `sc.cam` in a loop across a call that may destroy. + +**The ray tracer** (`test/tester/own/own_raytrace.ts`: `rtbench_own.ts` at 64x64, ending +`print("checksum:", sum); assert(sum == 606318)`) compiles under own with no errors and prints +checksum 606318, as under gc, rc and none. Built as a program only: it exports `Surfaces.shiny` and +`Surfaces.checkerboard`, which an object file or a DLL opens to other modules (§31, USER decision), +so test-runner's AOT build (an object file it links itself) cannot build it under own. Registered as +`test-jit-own-own_raytrace` (the JIT's entry module counts as a program) and +`test-compile-own-own_raytrace` (`own-exe-run.cmake`: `tslang --emit=exe`, run, match the checksum), +and under gc, none and rc through test-runner, JIT and AOT. Not in `own_test`: its `Shared` +handles count (`__tslang_inc_ref`/`__tslang_dec_ref` appear only in the handle's retain and release +routines), as for `own_shared_literal` (§26); for the same reason, and because an +`--emit=mlir-affine` build is not a program, no `--verify-ownership` run. + +Measured (`bench.ps1`, 512x512, Windows 11 x64, the machine otherwise idle; two runs of the script, +median of 7 runs each after a warm-up): own 627.5 ms both times (`--no-stack-promotion` 626.4 and +626.8 ms), rc 640.0 and 637.9 ms in the same runs (gc 1295-1299, none 662-666, Rust idiomatic 58, +Rust `Rc` 515, C++ idiomatic clang 54 ms). Peak working set (`PeakWorkingSet64` polled while the +process runs, three runs each): own 4.5 MB, own without stack promotion 4.5 MB, rc 4.5-4.9 MB, gc +6.5 MB, none 840-846 MB. Linux (WSL, ninja Release): own 0.13 s and 4.5 MB maximum resident set, rc +0.15 s and 4.3 MB, checksum ...369, exit 0, three runs each. Errors left in the own copy: 0. + +Corpus (`test/tester/tests/*.ts` under `-mm=own --no-default-lib`, `--emit=obj`, Windows): 525 of +702 before this section, 526 after: `00owned_any_boxing` gained (`(boxed[0]).length`: the +generated `___unbox`'s result, a copy, was walked as a borrow of the array's element, which +the array's release at the return ends; built under own it prints what gc prints). None lost. + +Not here: +- A method call's result (`o.m(borrow)`, through `ts.ThisVirtualSymbolRef`) is still walked as a + borrow of its arguments, as is an indirect call's. +- Known limit of the teeth (fix-up, 2026-10-23): with the `isFresh` test alone removed, + `own_err_field_function_reassigned` (a conservative negative) compiles and prints gc's output; + with the borrowing-record exclusion alone removed, `own_err_call_result_record` stays an error + through the record's own bound (`borrows the result of 'hit'`). No probe (review9's 95, T9's, + the merge review's, every `own_err_*`) was accepted unsafely with either removed, so no negative + shows a use-after-free that only those two tests prevent. +- The benchmark keeps its two source edits; the unedited `rtbench.ts` does not compile under own (a + non-goal of the plan). diff --git a/tslang/docs/superpowers/specs/2026-10-21-own-raytrace-design.md b/tslang/docs/superpowers/specs/2026-10-21-own-raytrace-design.md new file mode 100644 index 000000000..a179be49b --- /dev/null +++ b/tslang/docs/superpowers/specs/2026-10-21-own-raytrace-design.md @@ -0,0 +1,267 @@ +# `-mm=own` compiles the ray tracer + +Date: 2026-10-21. Status: design, approved decisions marked **USER**. + +## Why + +The ray tracer benchmark (`tslang/benchmarks/raytrace`, PR #634) runs under gc, rc and none, and +is the program the README quotes for performance. Under `-mm=own` it does not compile: 44 errors. +`own` is meant to accept ordinary object-oriented TypeScript that has a single owner for each +value, and this program almost does. Its few genuinely shared objects are the two surfaces. + +**USER:** small deliberate source edits like `Shared` for genuinely shared objects are fine; every +other error is a compiler gap and gets fixed in the compiler. `own` gets its own copy of the +benchmark with the edit; gc, rc and none keep running the unedited source. + +## Goals + +- `rtbench_own.ts` (the benchmark with the two source edits below, nothing else) compiles + under `-mm=own --opt --opt_level=3 --no-default-lib`, prints checksum 38837369, and frees what it + allocates (peak working set comparable to rc's 4 MB). +- Each gap below is fixed as a general rule, with minimal positive tests that run under gc, rc and + own, teeth (a deliberately broken variant that frees too early must fail), and negative tests for + the cases that must stay errors. +- The benchmark table gains an own row. + +## Non-goals + +- Making the unedited `rtbench.ts` compile under own. The surfaces are shared; that is `Shared`. +- Stack promotion of the ray tracer's temporaries (a separate item: caller-provided result slot). +- Performance work beyond what falls out. +- Annotations. Everything is inferred, as `__own_result_borrows` is today. + +## Anti-goals + +- **No unsound acceptance.** A rule that accepts a program and then frees a block someone still + reads, or frees it twice, is worse than the error it removes. When unsure, keep the error. +- No runtime counting under own outside `Shared`. +- No change in what gc, rc or none compile or print, except where a fix in shared MLIRGen code is + also a fix there (say so in the PR). + +## The source edits (the only two) + +`rtbench_own.ts` differs from `rtbench.ts` by two edits. + +**1. `Plane.normal` returns a fresh copy (USER).** `Plane.normal` returned `this.norm`, a borrow of +its own field, while `Sphere.normal` returns a fresh vector. Through the `Thing` interface a caller +cannot know whether it owns the result, and a run-time flag is ruled out. So: + +```ts +public normal(pos: Vector): Vector { return new Vector(this.norm.x, this.norm.y, this.norm.z); } +``` + +The compiler does not insert such copies itself: for class objects a copy changes identity. + +**2. The surfaces are `Shared`:** + +```ts +interface Thing { ...; surface: Shared; } +class Sphere { constructor(public center: Vector, radius: number, public surface: Shared) ... } +class Plane { constructor(public norm: Vector, public offset: number, public surface: Shared) ... } +module Surfaces { + export let shiny = new Shared({ ... }); + export let checkerboard = new Shared({ ... }); +} +// and every thing.surface.X becomes thing.surface.value.X +``` + +With both edits it compiles and prints the right checksum under rc today. + +## The gaps + +Each has a minimal repro (kept as the test). "Today" is main 49b5f6bb. Error texts are the +current ones. + +### G1. Immortal globals (USER decision) + +A global that is never reassigned after its initializer is **immortal**: like a string literal, a +release skips it, so a read of it may be returned, stored into a field, element or another global, +captured, or given to a call that keeps it, the way Rust hands out a `'static`. + +```ts +class K { static w = new V(1); static d = K.w; static f() { return K.w; } } +function g(n: number) { return n > 1 ? K.w : new V(n); } // mixed: immortal or fresh +``` + +Today: `'this value' borrows the global 'K.w' and cannot be stored, returned or captured`. + +- **Never reassigned** (decided here, correct me): a class static or module-level `let`/`const` + whose only store is its initializer, in this module. An exported global stays mortal unless the + module is the program's entry and nothing imports it - other modules may assign it. (In the ray + tracer, `Color.*` statics are not exported; `Surfaces.*` are `Shared` anyway.) +- **Which values** (builder check, my call): only a global whose initializer is a fresh value + (`new C()`, an owning literal) or another immortal global; no call's result, fresh or not (as + built: own-model spec §25, which supersedes this list). A borrowed + initializer (`static x = arr[0]`, a call that returns a borrow) stays mortal and keeps today's + error: marking a block someone else owns immortal would turn its later free into a silent leak. +- **Mechanism:** the existing `HEAP_BLOCK_IMMORTAL` header (OwnershipRoutineLogic.h): the global's + initializer marks the block it stores immortal; inference treats a load of such a global as it + treats `isImmortalLiteral` - owns nothing, may go anywhere, its release erased or skipped. +- **A mixed return** (`n > 1 ? K.w : new V(n)`) is then an owned result on every path: the caller + "owns" the immortal one too, and its release is a no-op at run time. Today's error + `returns a borrow of its argument on some paths and another value on others` must not fire for an + immortal path. +- **Only the global's own value is immortal.** Its fields are not: `K.w.child = x` still frees + the old child, so a borrow of `K.w.child` stays bounded as today. +- Under gc/rc/none nothing changes. + +### G2. Borrowing records (USER decision) + +A function that returns an object literal built from borrowed values returns a **borrow** of +those sources (inferred, like a Rust struct with a lifetime): + +```ts +interface I { t: T; d: number; } +class T { k = 1; hit(): I { return { t: this, d: 2 }; } } // result borrows `this` +``` + +Today: `an object literal that holds a value it owns is moved only into a local or returned as it +is` and `argument 'this value' is given to 'T.hit', which keeps it`. + +- The result is `__own_result_borrows` of the receiver/parameters it holds (directly, or a field + read of one). Several sources (ray tracer: `{ thing: this, ray: ray, dist }` borrows `this` and + `ray`) means several - today's fact names one argument; it becomes a set. +- **Mixed fields:** a record may hold owned fields (`new V()`) and borrowed ones. Its release frees + the owned fields and leaves the borrowed ones (a per-record routine skipping borrowed fields, as + `ts.own_borrowing_fields` does for generator states in phase 7b). +- **The release travels with the value** (builder check, my call): which fields a record's release + skips is decided where the record is made, not from the static type where it is released. Then + records from different creation sites (`Sphere.intersect` and `Plane.intersect`) may meet in one + local, as `closestInter` does. +- **Flow:** borrow-ness flows through returns, merges and locals as for plain borrows today: + `intersections` keeps the best of several such records in a local and returns it - its result + borrows its own `ray` and `scene`. +- **Stays an error:** storing a borrowing record where it can outlive a source (field, global, + element of a longer-lived array, closure that escapes). +- Across modules: an open (exported) function has to say it with `@lends`, which today names one + parameter; it may name several. (The ray tracer is one module, so this can come last.) + +### G3. Owning literals moved anywhere + +An object literal that owns its fields (fresh values, or locals moved in) may move into a call +argument, an array literal element, a nested literal, or be returned through an interface type: + +```ts +len({ start: p, dir: d }); // p and d move in; the callee borrows the record +const ls: L[] = [{ pos: new V(1) }, { pos: new V(2) }]; +return { ls: [new V(1), new V(2)], c: new V(3) }; +``` + +Today: `an object literal that holds a value it owns is moved only into a local or returned as it +is; -mm=own cannot move it here yet` (13 of the 44). + +- This includes a literal converted to an interface type even when it goes into a local: + `const r: Ray = { start: new V(1), dir: new V(2) }` fails today with the same error. +- An argument record is a temporary: the caller owns it and releases it after the call, as it does + a fresh `new C()` argument. Whether the callee borrows or keeps it comes from the callee's facts, + as for any argument. +- When a field is a local moved in (`{ start: p }`), `p` is moved (use after = error, G7 applies). + When it is a borrow (`{ start: this.pos }`), the record is a borrowing record (G2) bounded by the + source, and as a call-argument temporary that is always fine. + +### G4. `Shared` of an object literal + +```ts +let g = new Shared({ f: function (x: number) { return x; }, roughness: 2 }); // global or local +``` + +Today: global `the initializer of global 'g' takes a second reference`; local `borrows a field +but is used here after it may be released or overwritten`. `new Shared(new C())` already works: +the literal must move into the handle the same way. + +### G5. `reduce` takes its accumulator + +```ts +const r = a.reduce((acc: V, v: V) => V.plus(acc, v), new V(0)); +``` + +Today: `is a parameter, kept here, but -mm=own cannot make the callers of '___array_reduce<...>' give +it up` and `borrows an element but is used here after it may be released`. The initial value moves +in, each callback result replaces the accumulator (the previous one released unless the callback +returned it - `addLight` returns `col` on one path), elements are borrowed by the callback, and the +final accumulator is returned owned. Under `--no-default-lib` this is the generated +`___array_reduce`. + +- **A callback that returns its accumulator on some paths** (builder check, my call): the + callback's accumulator parameter is inferred as owned (kept), like a Rust `fn(acc: Color) -> + Color` by value. Returning it moves it out; a path that returns something else releases it. All + static: no run-time pointer comparison (anti-goal: no run-time bookkeeping). The same rule serves + any function that returns a parameter on some paths and a fresh value on others, when every + caller can give the argument up. +- **The default library's `reduce`** is a follow-up, not this series: the benchmark builds with + `--no-default-lib`, and stating "the callback's result replaces the accumulator" in `lib.d.ts` + needs a marker that does not exist yet. + +### G6. A field read into a `let` + +```ts +function f(r: R) { let d = r.dir; let p = V.plus(d, d); return V.plus(p, d); } +``` + +Today: `takes a second reference; -mm=own cannot prove a move or a borrow here yet`. `d` is a borrow +of a parameter's field, bounded by the parameter; calls that cannot drop it (signature facts) must +not end it. + +### G7. Read after a move while the new owner holds it (USER decision) + +```ts +if (v.x > m) { best = v; m = v.x; } +``` + +Today: `is used here after its value was moved`. The read is a borrow through the new owner: sound +while that owner is not overwritten, released or moved on between the move and the read (and the +value is not mutated through a path that would free it). Otherwise it stays the error. + +### G9. Facts through a module-private interface (builder check) + +`OwnershipSignaturePass` treats a call through an interface as open: it does not name what it +reaches, so it gets no facts. The ray tracer's hot calls all go through interfaces +(`scene.things[i].intersect(ray)`, `isect.thing.normal(pos)`, `surface.value.diffuse(pos)`), so +G2's borrowing result on `Sphere.intersect` would never reach `intersections`. + +- **Closed world for interfaces** (my call): when an interface is not exported and every class or + object literal that implements it is in this module (and none is exported as that interface), a + call through it gets the meet of its implementations' facts, as a private virtual family already + does. +- If the implementations disagree in a way the meet cannot express, the call stays open and the + existing errors stand. +- Its own PR, before G2. + +### G8. Not yet reproduced in isolation + +The ray tracer also has `borrows a field but is used here after it may be released or overwritten` +at `Camera`'s constructor, `Sphere.normal` and `render`'s `getPoint` (`this.pos`, `this.center`, +`camera.forward`), one `moved here on some paths only` on `shade`'s ternary, and one `takes a +second reference` at `let d = isect.ray.dir`. Minimal versions of each compile: the `Camera` +constructor with chained calls over `this.pos`, `normal()` through the `Thing` interface, the +`getPoint` closure reading `camera.forward`, and the `depth >= maxDepth ? … : this.call()` ternary +(probes h1-h4). `let d = i.ray.dir` through two interface fields reproduces, and is G6. The rest +are therefore expected to be follow-ons of G1-G7 inside the full program. Each is +re-checked after G1-G7 land; any that remains gets its own repro and fix, in this same series. + +## Order of work + +Each item is one PR, small and squash-merged, with its tests (gc, rc, own), teeth, negatives, a +corpus run (`own` corpus count may only go up) and the full suite on Windows; Linux (WSL) for any +change to what is released, since the Windows heap hides double frees. + +1. G1 immortal globals. +2. G4 `Shared` of a literal. +3. G3 owning literals moved anywhere. +4. G6 field read into a let. +5. G7 read after a move. +6. G9 facts through a module-private interface. +7. G2 borrowing records (the largest; needs G3 and G9). +8. G5 `reduce`. +9. G8 leftovers; then `tests/raytrace_own.ts` (the own copy at a test size) under own in the suite, + and `benchmarks/raytrace/rtbench_own.ts` + an own row in both READMEs (after #634 merges). + +## Left to the builder + +- Exact attribute names and which pass decides each fact (signature pass vs inference), following + the existing split: cross-function facts in `OwnershipSignaturePass`, per-function in + `OwnershipInferencePass`, routines in `OwnershipRoutineLogic.h`. +- Test names and CMake anchors (use distinct anchors per PR - parallel PRs conflicted before). +- Whether a step splits into two PRs. +- The results of each step go in `2026-09-24-own-memory-model-design.md` as a new section, as + earlier phases did. diff --git a/tslang/include/TypeScript/Defines.h b/tslang/include/TypeScript/Defines.h index 2db4725d0..539bdeae0 100644 --- a/tslang/include/TypeScript/Defines.h +++ b/tslang/include/TypeScript/Defines.h @@ -134,6 +134,13 @@ // groups`). A yield moves what such a field holds to the caller, so the field never owns it: an // overwrite gives nothing back, and the state's release routine skips it. #define OWN_YIELDED_FIELDS_ATTR_NAME "ts.own_yielded_fields" +// -mm=own only, set by OwnershipSignaturePass for the inference and the lowering: on the `ts.New` +// that boxes an object literal, and on every `ts.NewInterface` made of that box, the fields of a +// borrowing record (own spec 32) - those holding a value the record does not own (`this`, a +// parameter, a field read, a local's value). The interface's tag then names a release routine of +// its own, which frees the box and the fields it owns and leaves these to their owners: the routine +// travels with the value, so records made at different sites may meet in one local. +#define OWN_RECORD_BORROWED_FIELDS_ATTR_NAME "__own_record_borrowed_fields" #define RETURN_VARIABLE_NAME ".return" #define CAPTURED_NAME ".captured" #define LABEL_ATTR_NAME "label" @@ -262,8 +269,9 @@ // knows them - one in another module too, which reads them from the declaration it re-parses. // `@keeps` names the parameters the function keeps: its callers move them in. `@borrows` names the // arguments a generator's state object borrows: the caller owns the result, but it may not outlive -// them. `@lends` names the one argument the result is a borrow of (OWN_RESULT_BORROWS_ATTR_NAME): -// the caller owns nothing, and the result may not outlive it. -mm=own checks a body against what +// them. `@lends` names the arguments the result is a borrow of (OWN_RESULT_BORROWS_ATTR_NAME): one +// for a plain borrow, whose caller owns nothing, or several for a borrowing record (own spec 32), +// whose caller owns the box; the result may not outlive any of them. -mm=own checks a body against what // it says; the other memory models ignore all three. Each is a FuncOp attribute of argument indices. #define KEEPS_DECORATOR "keeps" #define BORROWS_DECORATOR "borrows" diff --git a/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h b/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h index 77f51a713..2a89bae6a 100644 --- a/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h +++ b/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h @@ -158,6 +158,56 @@ class OwnershipRoutineLogic return name; } + // -mm=own: the release routine of one borrowing record's box (own spec 32), an object whose + // `borrowed` fields hold values the record does not own. Like the object's own routine, but it + // leaves those fields to their owners. Made for the interface tag of each box marked so where it + // is made (OWN_RECORD_BORROWED_FIELDS_ATTR_NAME), so the routine travels with the value. + std::string getOrCreateRecordReleaseRoutine(mlir::Type type, ArrayRef borrowed) + { + auto objectType = dyn_cast(type); + if (!objectType || borrowed.empty() || !ownsHeapMemory(type)) + { + return getOrCreateReleaseRoutine(type); + } + + std::stringstream nameStream; + nameStream << "tsrelrec_" << (size_t)hash_value(type); + for (auto index : borrowed) + { + nameStream << "_" << index; + } + + auto name = nameStream.str(); + auto parentModule = op->getParentOfType(); + if (parentModule.lookupSymbol(name)) + { + return name; + } + + TypeHelper th(rewriter); + auto loc = op->getLoc(); + + OpBuilder::InsertionGuard insertGuard(rewriter); + rewriter.setInsertionPointToStart(parentModule.getBody()); + + auto funcOp = rewriter.create( + loc, name, th.getFunctionType(th.getVoidType(), {th.getPtrType()}), LLVM::Linkage::Internal); + + auto *entryBlock = funcOp.addEntryBlock(rewriter); + rewriter.setInsertionPointToStart(entryBlock); + + auto instanceValue = rewriter.create(loc, th.getPtrType(), entryBlock->getArgument(0)); + emitIfLastReference(instanceValue, [&]() { + releaseFields(objectType.getStorageType(), instanceValue, /*nullReleased=*/false, borrowed); + emitFreeBlock(instanceValue); + }); + + rewriter.create(loc, ValueRange{}); + + dropDebugLocations(funcOp); + return name; + } + // Symbol name of the retain routine for `type`, generating it if needed. Empty when the // type owns no heap memory, in which case the descriptor's retain slot stays null. // @@ -410,6 +460,25 @@ class OwnershipRoutineLogic }); } + // Marks the block `payloadPtr` addresses HEAP_BLOCK_IMMORTAL, when there is one: what an + // immortal global is given (own spec 25). Every release then skips it, as it skips a string + // literal's. + void emitMarkImmortal(mlir::Value payloadPtr) + { + emitIfNonNull(payloadPtr, [&]() { + TypeHelper th(rewriter); + TypeConverterHelper tch(typeConverter); + LLVMCodeHelperBase ch(op, rewriter, typeConverter, compileOptions); + auto loc = op->getLoc(); + + auto llvmIndexType = tch.convertType(th.getIndexType()); + auto blockPtr = ch.getBlockPtrFromPayloadPtr(loc, payloadPtr, llvmIndexType); + rewriter.create( + loc, rewriter.create(loc, llvmIndexType, rewriter.getIntegerAttr(llvmIndexType, HEAP_BLOCK_IMMORTAL)), + blockPtr); + }); + } + private: // Field types of a record-shaped type, empty for anything else. llvm::SmallVector getFieldTypes(mlir::Type type) @@ -734,8 +803,10 @@ class OwnershipRoutineLogic } // Releases each field a record-shaped value owns. `basePtr` addresses the record itself. With - // `nullReleased`, each field released is then zeroed. - void releaseFields(mlir::Type recordType, mlir::Value basePtr, bool nullReleased = false) + // `nullReleased`, each field released is then zeroed. `left` names fields a borrowing record's box + // does not own (own spec 32): their values are someone else's. + void releaseFields(mlir::Type recordType, mlir::Value basePtr, bool nullReleased = false, + ArrayRef left = {}) { TypeHelper th(rewriter); TypeConverterHelper tch(typeConverter); @@ -747,6 +818,11 @@ class OwnershipRoutineLogic for (auto [index, fieldType] : llvm::enumerate(getFieldTypes(recordType))) { + if (llvm::is_contained(left, static_cast(index))) + { + continue; + } + // a generator's local that only yields: what it held went to the caller if (llvm::is_contained(yielded, static_cast(index))) { diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h b/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h index d272f1588..c13644b33 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h @@ -164,10 +164,12 @@ struct MethodInfo // a slot of a @cpp class's vtable that no method names (ClassInfo::getVirtualTable) bool isCppSlotGap = false; // `@keeps`, `@borrows`, `@lends` (OWN_DECLARED_KEEPS_ATTR_NAME): argument indices, `this` 0, for - // the declaration an importer re-parses; `@lends` names one, or none (-1) + // the declaration an importer re-parses; `@lends` names one, or several for a borrowing record + // (own spec 32), as a mask of argument indices (a vector would push the vtable's SmallVector + // past its inline-size limit) std::vector ownKeeps; std::vector ownBorrows; - int32_t ownLends = -1; + uint64_t ownLendsMask = 0; }; struct GenericMethodInfo diff --git a/tslang/include/TypeScript/Passes.h b/tslang/include/TypeScript/Passes.h index c41180cb5..890ba6ce3 100644 --- a/tslang/include/TypeScript/Passes.h +++ b/tslang/include/TypeScript/Passes.h @@ -37,8 +37,10 @@ std::unique_ptr createOwnershipInferencePass(); /// The callee facts -mm=own infers across the module - a parameter the callee keeps, a result that /// borrows an argument, a callee that destroys nothing its caller can reach - pinned on each call it -/// resolves. Runs just before OwnershipInferencePass, under own only. -std::unique_ptr createOwnershipSignaturePass(); +/// resolves. Runs just before OwnershipInferencePass, under own only. `executable`: the module is +/// built into a program (an executable, or the JIT's entry module), whose exports no other module +/// sees; false when unknown. +std::unique_ptr createOwnershipSignaturePass(bool executable = false); /// Lets a call take over the reference its callee returned instead of retaining a second one. /// Runs on the whole module, after MLIRGen, because deciding which callees retain their result diff --git a/tslang/lib/TypeScript/DeclarationPrinter.cpp b/tslang/lib/TypeScript/DeclarationPrinter.cpp index eacdda372..57bf0d2da 100644 --- a/tslang/lib/TypeScript/DeclarationPrinter.cpp +++ b/tslang/lib/TypeScript/DeclarationPrinter.cpp @@ -700,9 +700,17 @@ namespace typescript newline(); } - printOwnMarkers(method.ownKeeps, method.ownBorrows, - method.ownLends >= 0 ? ArrayRef(method.ownLends) : ArrayRef(), - /*hasThis=*/!method.isStatic, 4); + // every index `@lends` names: MLIRGen refuses one past 63, which the mask cannot hold + SmallVector lends; + for (int32_t index = 0; index < 64; ++index) + { + if (method.ownLendsMask & (uint64_t(1) << index)) + { + lends.push_back(index); + } + } + + printOwnMarkers(method.ownKeeps, method.ownBorrows, lends, /*hasThis=*/!method.isStatic, 4); // the slot of a @cpp class's virtual method: without it the importer takes the method // for a non-virtual one diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index 0b8ef48da..2854a8ca3 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -40,6 +40,9 @@ #include "TypeScript/LowerToLLVM/LLVMDebugInfoFixer.h" #include "TypeScript/LowerToLLVM/ArrayLayout.h" +// OWN_IMMORTAL_GLOBAL_ATTR_NAME, own_facts::regionYieldsDefault +#include "OwnershipFacts.h" + #include "scanner_enums.h" #define DISABLE_SWITCH_STATE_PASS 1 @@ -4802,6 +4805,14 @@ struct StoreOpLowering : public TsLlvmPattern // isInvariant = invariantAttr.getValue(); // } + // an immortal global's initializer (own spec 25): the block it is given is marked, so every + // release of a read of the global skips it + if (tsLlvmContext->compileOptions.memoryModel == MemoryModelOwn && storeOp->hasAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME)) + { + OwnershipRoutineLogic orl(storeOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); + orl.emitMarkImmortal(transformed.getValue()); + } + const bool isInvariantGroup = false; // TODO: Determine if this should be set based on some condition rewriter.replaceOpWithNewOp(storeOp, transformed.getValue(), transformed.getReference(), alignment, isVolatile, isNonTemporal, isInvariantGroup, ordering, syncscope); #ifdef DBG_INFO_ADD_VALUE_OP @@ -5003,6 +5014,15 @@ struct GlobalOpLowering : public TsLlvmPattern createAsGlobalConstructor = true; } + // an immortal global (own spec 25) whose region gives it its value gets it from a store, + // which marks the block. A class static's region yields only the default: the store in the + // static constructor gives it its value, and a constructor here would run after that one. + if (globalOp->hasAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME) && globalOp.getInitializerBlock() && + !own_facts::regionYieldsDefault(globalOp)) + { + createAsGlobalConstructor = true; + } + auto linkage = globalOp.getLinkage(); LLVM::GlobalOp llvmGlobalOp; if (createAsGlobalConstructor) @@ -5024,7 +5044,20 @@ struct GlobalOpLowering : public TsLlvmPattern // ("DISubprogram attached to more than one function") since a DISubprogram must // identify exactly one function. Same "no debug info for this synthetic function" // precedent as setDISubProgramTypesToFormOp's bodiless-declaration handling above. - lch.createFunctionFromRegion(mlir::UnknownLoc::get(rewriter.getContext()), name, globalOp.getInitializerRegion(), globalOp.getSymName()); + auto initFuncOp = lch.createFunctionFromRegion(mlir::UnknownLoc::get(rewriter.getContext()), name, + globalOp.getInitializerRegion(), globalOp.getSymName()); + + // an immortal global (own spec 25): its one store, here, marks the block it is given + if (globalOp->hasAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME)) + { + initFuncOp.walk([&](mlir_ts::StoreOp storeOp) { + auto addressOfOp = storeOp.getReference().getDefiningOp(); + if (addressOfOp && addressOfOp.getGlobalName() == globalOp.getSymName()) + { + storeOp->setAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME, rewriter.getUnitAttr()); + } + }); + } { OpBuilder::InsertionGuard insertGuard(rewriter); @@ -6653,15 +6686,31 @@ struct NewInterfaceOpLowering : public TsLlvmPattern // generated first: the descriptor's initializer takes their addresses OwnershipRoutineLogic orl(newInterfaceOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); - auto releaseRoutineName = orl.getOrCreateReleaseRoutine(thisType); + // -mm=own: a borrowing record's box (own spec 32) has a routine, and so a descriptor, of its + // own, which leaves the fields it borrows to their owners + llvm::ArrayRef borrowed; + if (auto attr = newInterfaceOp->getAttrOfType(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME); + attr && tsLlvmContext->compileOptions.memoryModel == MemoryModelOwn) + { + borrowed = attr.asArrayRef(); + } + + auto releaseRoutineName = borrowed.empty() ? orl.getOrCreateReleaseRoutine(thisType) + : orl.getOrCreateRecordReleaseRoutine(thisType, borrowed); // under own nothing retains: see TypeDescriptorOpLowering auto retainRoutineName = tsLlvmContext->compileOptions.memoryModel == MemoryModelOwn ? std::string() : orl.getOrCreateRetainRoutine(thisType); + std::string variant; + for (auto index : borrowed) + { + variant += "r" + std::to_string(index); + } + LLVMCodeHelper ch(newInterfaceOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); return ch.getOrCreateTypeDescriptorName(thisType, name, TypeOfOpHelper::typeKindFromName(name), - releaseRoutineName, retainRoutineName); + releaseRoutineName, retainRoutineName, variant); } LogicalResult matchAndRewrite(mlir_ts::NewInterfaceOp newInterfaceOp, Adaptor transformed, diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 423af4d27..6f197a9d1 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -3261,14 +3261,7 @@ class MLIRGenImpl return; } - // a result is a borrow of one argument - if (name == LENDS_DECORATOR && args.size() > 1) - { - emitError(location) << "'@" << name << "' names one parameter, the one the result is a borrow of"; - result = mlir::failure(); - return; - } - + // `@lends` may name several: a borrowing record holds a borrow of each (own spec 32) auto first = funcType && funcType.getInputs().size() == params.size() + 1 ? 1 : 0; SmallVector indices; for (auto arg : args) @@ -12690,7 +12683,23 @@ class MLIRGenImpl if (auto lends = funcOp->getAttrOfType(OWN_DECLARED_LENDS_ATTR_NAME)) { - methodInfo.ownLends = lends.asArrayRef().empty() ? -1 : lends.asArrayRef().front(); + methodInfo.ownLendsMask = 0; + for (auto index : lends.asArrayRef()) + { + // the mask the class's declaration is printed from (DeclarationPrinter) has + // 64 bits: an index past it would be a borrow an importer never learns of + if (index >= 64) + { + emitError(location) << "'@lends' of '" << funcOp.getSymName() << "' names its argument " << index + << ", but a method's '@lends' can name only its arguments 0 to 63"; + return false; + } + + if (index >= 0) + { + methodInfo.ownLendsMask |= uint64_t(1) << index; + } + } } } diff --git a/tslang/lib/TypeScript/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h index dff39695a..9cfb98266 100644 --- a/tslang/lib/TypeScript/OwnershipFacts.h +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -29,8 +29,20 @@ namespace mlir_ts = mlir::typescript; // // The parameters the callee keeps: the caller moves each such argument into the call. #define OWN_PARAMS_ATTR_NAME "__own_params" -// The argument the result is a borrow of: the caller owns nothing it gets back. +// On a function: the parameters it keeps (OWN_PARAMS_ATTR_NAME) that its body never takes into a +// field, an element, a global, a keeping call or an assigned owning local - one it returns on some +// paths only, one a callback family gives it to keep (spec 33). The inference releases each on the +// paths that do not move it, statically. +#define OWN_PARAMS_RELEASED_ATTR_NAME "__own_params_released" +// The arguments the result is a borrow of, as argument indices (the receiver is 0 for a method): the +// result may not outlive any of them. Alone, the caller owns nothing it gets back, and it names one +// argument. With OWN_RESULT_RECORD_ATTR_NAME it may name several (spec 32). #define OWN_RESULT_BORROWS_ATTR_NAME "__own_result_borrows" +// Beside OWN_RESULT_BORROWS_ATTR_NAME: the result is a borrowing record (spec 32) - an object literal +// box the callee made, holding borrows of those arguments. The caller owns the box and releases it; +// the box's release (its interface's tag, OWN_RECORD_BORROWED_FIELDS_ATTR_NAME) frees only the +// fields the record owns. +#define OWN_RESULT_RECORD_ATTR_NAME "__own_result_record" // The callee destroys nothing its caller can reach: no overwrite of a place it did not make, no // assignment of a global, no call that may. #define OWN_NO_DROPS_ATTR_NAME "__own_no_drops" @@ -51,6 +63,14 @@ namespace mlir_ts = mlir::typescript; // result is the caller's to own, but no use of it may come after any of these arguments is given // back, and it may not be kept anywhere that outlives them. #define OWN_RESULT_BOUNDED_ATTR_NAME "__own_result_bounded" +// An immortal global (spec 25): a class-typed global of this module, not exported, whose only +// write is its initializer, which gives it an object made there (not a call's result) or another +// immortal global's. Like a +// string literal, its block is marked HEAP_BLOCK_IMMORTAL, so every release skips it, and a read of +// it owns nothing and may go anywhere. Pinned by OwnershipSignaturePass on the `ts.Global`, on +// every `ts.AddressOf` of it (isImmortalGlobalRead), and on the `ts.Store` that initializes it, +// whose lowering writes the header word. +#define OWN_IMMORTAL_GLOBAL_ATTR_NAME "__own_immortal" // A call inside a try body is a `ts.Invoke` (`ts.InvokeHybrid` through a hybrid function): a // terminator whose normal and unwind edges are its successors. It names its callee as a symbol, or @@ -131,15 +151,37 @@ inline llvm::ArrayRef ownedParams(mlir::Operation *op) return {}; } -// The argument a call's result borrows, or -1. +// Every argument a call's (or a function's) result borrows: one for a plain borrow, any number for a +// borrowing record (spec 32). Empty for none. +inline llvm::SmallVector resultBorrowsAll(mlir::Operation *op) +{ + llvm::SmallVector indices; + if (auto attr = op->getAttrOfType(OWN_RESULT_BORROWS_ATTR_NAME)) + { + indices.append(attr.asArrayRef().begin(), attr.asArrayRef().end()); + } + + return indices; +} + +// A result that is a borrowing record (OWN_RESULT_RECORD_ATTR_NAME): a box the caller owns, bounded by +// the arguments it borrows. +inline bool isRecordResult(mlir::Operation *op) +{ + return op && op->hasAttr(OWN_RESULT_RECORD_ATTR_NAME) && op->hasAttr(OWN_RESULT_BORROWS_ATTR_NAME); +} + +// The argument a call's result is a plain borrow of, or -1: none, or a borrowing record, which the +// caller owns (isRecordResult). Kept beside resultBorrowsAll for the plain borrow, which names one. inline int resultBorrows(mlir::Operation *op) { - if (auto attr = op->getAttrOfType(OWN_RESULT_BORROWS_ATTR_NAME)) + if (isRecordResult(op)) { - return static_cast(attr.getInt()); + return -1; } - return -1; + auto indices = resultBorrowsAll(op); + return indices.empty() ? -1 : indices.front(); } // Is `narrow` a member of the union or optional `wide` as it is, so that a cast between them keeps @@ -227,6 +269,34 @@ inline mlir::Value rootOf(mlir::Value value) return value; } +// The `ts.AddressOf` of the global a value is read out of, seen through its views, or null. +inline mlir_ts::AddressOfOp globalReadOf(mlir::Value value) +{ + auto loadOp = rootOf(value).getDefiningOp(); + return loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp(); +} + +// A read of an immortal global (OWN_IMMORTAL_GLOBAL_ATTR_NAME), seen through its views: the +// global's own block, which no release frees. Its fields are not immortal: a read out of one is a +// place read like any other. +inline bool isImmortalGlobalRead(mlir::Value value) +{ + auto addressOfOp = globalReadOf(value); + return addressOfOp && addressOfOp->hasAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME); +} + +// Does a global's initializer region yield only the default (`ts.Default`, `ts.Null`, +// `ts.Undef`)? A class static's does: the one store in its static constructor gives it its value +// (spec 25). OwnershipSignaturePass marks such a global immortal only with that store, and the +// lowering then makes no `__cctor` of the region, which would put the default back after it. +inline bool regionYieldsDefault(mlir_ts::GlobalOp globalOp) +{ + auto *block = globalOp.getInitializerBlock(); + auto *terminator = block ? block->getTerminator() : nullptr; + auto *valueOp = terminator && terminator->getNumOperands() == 1 ? terminator->getOperand(0).getDefiningOp() : nullptr; + return mlir::isa_and_nonnull(valueOp); +} + // Every user of `root` and of the views of it, with the value it uses. A view itself is not a // use. inline void forEachUse(mlir::Value root, llvm::function_ref each) @@ -290,6 +360,13 @@ inline bool isFresh(mlir::Value value) return false; } + // a borrowing record (spec 32): the box is the caller's, whatever the call goes through; what it + // borrows bounds it (OwnershipInferencePass's checkRecordResult) + if (isRecordResult(def)) + { + return true; + } + // a view is the block it shows: the object literal's interface cast is marked as arriving with // a reference, but `let raw = {...}; raw` is `raw`'s block if (isView(def)) @@ -332,11 +409,20 @@ inline bool isCall(mlir::Operation *op) mlir_ts::InvokeHybridOp>(op); } -// A field, an element, or `s.value` - the payload of a handle (spec 23.1). +// A field of an object seen through an interface (`i.f`): the object's own field, reached by the +// offset the interface's vtable holds. A method of the interface is a bound function, not a place. +inline bool isInterfaceField(mlir::Value ref) +{ + return ref.getDefiningOp() && mlir::isa(ref.getType()); +} + +// A field, an element, or `s.value` - the payload of a handle (spec 23.1) - and a field reached +// through an interface (spec 28). inline bool isPlace(mlir::Value ref) { return mlir::isa_and_nonnull( - ref.getDefiningOp()); + ref.getDefiningOp()) || + isInterfaceField(ref); } // A handle (spec 23.2): counted in every model that tracks ownership, and left alone by own. @@ -429,6 +515,10 @@ inline bool reachesSharedValue(mlir::Value ref) { work.push_back(elementRefOp.getArray()); } + else if (auto interfaceRefOp = value.getDefiningOp(); interfaceRefOp && isInterfaceField(value)) + { + work.push_back(interfaceRefOp.getInterfaceVal()); + } else if (auto loadOp = value.getDefiningOp(); loadOp && isPlace(loadOp.getReference())) { work.push_back(loadOp.getReference()); @@ -506,6 +596,13 @@ inline bool holdsNoBlock(mlir::Value value) return false; } +// An argument that holds no block (holdsNoBlock) - but not an opaque pointer, by which a call through +// an interface or a bound method is given its receiver (`ts.GetThis`), whose type owns nothing. +inline bool argumentHoldsNoBlock(mlir::Value arg) +{ + return !mlir::isa(arg.getType()) && holdsNoBlock(arg); +} + // A value -mm=own copies rather than give a second owner (spec 22.1): a string, a string literal, // a union of strings and null that needs no tag (one pointer), or an optional of one of those // (`{ptr, i1}`). @@ -617,6 +714,127 @@ inline bool isConstantData(mlir::Value value) return false; } +// A non-owning local a `ts.Constant` initializes, used only through its fields and its reads: the +// local MLIRGen builds an object literal in. +inline bool isConstantRecordLocal(mlir_ts::VariableOp varOp) +{ + if (isOwningVariable(varOp) || varOp.getCaptured().value_or(false) || !varOp.getInitializer() || + !varOp.getInitializer().getDefiningOp()) + { + return false; + } + + return llvm::all_of(varOp->getUsers(), [](mlir::Operation *user) { + return mlir::isa(user); + }); +} + +// The block an object literal is boxed into when it is given as an object or an interface: a +// `ts.New` of a `value_ref`, filled by one store of the record's read (spec 27). Its views (the +// object, the interface) are it. Null for anything else. +inline mlir_ts::NewOp recordBoxOf(mlir::Value value) +{ + auto newOp = rootOf(value).getDefiningOp(); + if (!newOp || !mlir::isa(newOp.getType())) + { + return {}; + } + + mlir_ts::StoreOp fill; + for (auto &use : newOp->getUses()) + { + auto storeOp = mlir::dyn_cast(use.getOwner()); + if (!storeOp || storeOp.getReference() != use.get()) + { + continue; + } + + if (fill) + { + return {}; + } + + fill = storeOp; + } + + auto read = fill ? fill.getValue().getDefiningOp() : mlir_ts::LoadOp(); + auto varOp = read ? read.getReference().getDefiningOp() : mlir_ts::VariableOp(); + return varOp && isConstantRecordLocal(varOp) ? newOp : mlir_ts::NewOp(); +} + +// The local an object literal is built in, from its read, or from the box it is copied into (or a +// view of that box). Null for anything else. +inline mlir_ts::VariableOp recordLocalOf(mlir::Value value) +{ + if (auto boxOp = recordBoxOf(value)) + { + for (auto *user : boxOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(user); + if (storeOp && storeOp.getReference() == boxOp.getResult()) + { + value = storeOp.getValue(); + } + } + } + + auto read = value.getDefiningOp(); + auto varOp = read ? read.getReference().getDefiningOp() : mlir_ts::VariableOp(); + return varOp && isConstantRecordLocal(varOp) ? varOp : mlir_ts::VariableOp(); +} + +// An object literal that owns what it holds (spec 27): its read, the box it is copied into, or a view +// of that box (the interface it is given as), where every field that owns a block holds what the +// constant put there (null, a number, an immortal literal), nothing at all, or a fresh value stored +// into it - a new object, a call's result, a literal, a `const` local folded into the value it holds. +// Each such value moves into the record: its own check, against the record's retain, decides that +// nothing uses it after (OwnershipInferencePass's recordRetainOf). A read out of a place (`this.pos`) +// or a parameter is a borrow, and such a record is not this one. Neither is a `let` read into a field: +// its slot keeps its own release, which nothing here cancels. +inline bool isOwningRecord(mlir::Value value) +{ + auto varOp = recordLocalOf(value); + if (!varOp) + { + return false; + } + + MLIRTypeHelper mth(value.getContext(), CompileOptions{}); + for (auto *user : varOp->getUsers()) + { + auto propertyRefOp = mlir::dyn_cast(user); + if (!propertyRefOp) + { + continue; + } + + // a method's field is a `!ts.bound_ref`, which holds a function, not a block + auto field = propertyRefOp.getResult(); + auto refType = mlir::dyn_cast(field.getType()); + if (refType && !mth.ownsHeapMemory(field.getLoc(), refType.getElementType())) + { + continue; + } + + for (auto *fieldUser : field.getUsers()) + { + if (mlir::isa(fieldUser)) + { + continue; + } + + auto storeOp = mlir::dyn_cast(fieldUser); + if (!storeOp || storeOp.getReference() != field || + !(isFresh(rootOf(storeOp.getValue())) || holdsNoBlock(storeOp.getValue()))) + { + return false; + } + } + } + + return true; +} + // A captured variable's cell, as a closure body reaches it: read out of a field of its capture box // (or of any other block - a RefType loaded from somewhere is a cell). Its variable lives in // whatever function declared it, so an assignment through it may destroy a value that function, and @@ -681,7 +899,9 @@ inline bool isParameterSlot(mlir_ts::VariableOp varOp, int &index) return true; } -// The object a method call is made on: `ts.GetThis` of a method bound to it. Null otherwise. +// The object a method call is made on: `ts.GetThis` of a method bound to it - by its class, or by an +// interface value whose method it is (`i.m`: the object the interface value is a view of, spec 31). +// Null otherwise. inline mlir::Value boundThis(mlir::Value value) { auto getThisOp = value.getDefiningOp(); @@ -696,6 +916,11 @@ inline mlir::Value boundThis(mlir::Value value) return refOp.getThisVal(); } + if (auto refOp = mlir::dyn_cast_or_null(bound); refOp && !isInterfaceField(refOp.getResult())) + { + return refOp.getInterfaceVal(); + } + return {}; } @@ -783,6 +1008,11 @@ inline int borrowedParam(mlir::Value value, int depth = 0) return borrowedParam(elementRefOp.getArray(), depth + 1); } + if (auto interfaceRefOp = mlir::dyn_cast_or_null(def); interfaceRefOp && isInterfaceField(value)) + { + return borrowedParam(interfaceRefOp.getInterfaceVal(), depth + 1); + } + if (auto loadOp = mlir::dyn_cast_or_null(def)) { auto ref = loadOp.getReference(); @@ -802,15 +1032,160 @@ inline int borrowedParam(mlir::Value value, int depth = 0) return isParameterSlot(varOp, index) ? index : -1; } - if (def && isCall(def) && resultBorrows(def) >= 0 && - static_cast(resultBorrows(def)) < callArgs(def).size()) + // a plain borrow of its arguments (a declared `@lends` may name several): all of one parameter + if (def && isCall(def) && resultBorrows(def) >= 0) { - return borrowedParam(callArgs(def)[resultBorrows(def)], depth + 1); + auto args = callArgs(def); + auto found = -1; + for (auto index : resultBorrowsAll(def)) + { + auto param = static_cast(index) < args.size() ? borrowedParam(args[index], depth + 1) : -1; + if (param < 0 || (found >= 0 && param != found)) + { + return -1; + } + + found = param; + } + + return found; } return -1; } +// ---- Borrowing records (spec 32) ---- +// +// An object literal boxed as an interface whose fields hold values it does not own - `this`, a +// parameter, a field read, an owning `let`'s value, a captured variable's - beside fresh values it +// owns. OwnershipSignaturePass classifies each box where it is made and marks it (and every interface +// made of it) with the fields it borrows (OWN_RECORD_BORROWED_FIELDS_ATTR_NAME): the box's release, +// through the interface's tag, frees the fields it owns and leaves the others to their owners. +// OwnershipInferencePass then bounds every use of the record by what each borrowed field is read from. + +// Where a field of a record gets a value that holds a block, for the record's release and its bound. +enum class RecordFieldSource +{ + NoBlock, // a number, null: nothing to own + Fresh, // made here (moves into the record, which owns it) + Immortal, // an immortal global or string literal: nobody frees it + Param, // a parameter or `this`, seen through views: the caller's for the whole call + Place, // a read out of a field or an element, or a call's result that borrows an argument + Slot, // the value of an owning local, which keeps it + Cell, // a captured variable's value, read through its cell + Unknown +}; + +inline RecordFieldSource recordFieldSource(mlir::Value value) +{ + if (holdsNoBlock(value)) + { + return RecordFieldSource::NoBlock; + } + + if (isImmortalGlobalRead(value)) + { + return RecordFieldSource::Immortal; + } + + auto root = rootOf(value); + if (auto castOp = root.getDefiningOp(); + castOp && mlir::isa(castOp.getType()) && castOp.getIn().getDefiningOp()) + { + return RecordFieldSource::Immortal; + } + + if (isFresh(root)) + { + return RecordFieldSource::Fresh; + } + + if (auto loadOp = root.getDefiningOp()) + { + if (isPlace(loadOp.getReference())) + { + return RecordFieldSource::Place; + } + + if (isLoadedCell(loadOp.getReference())) + { + return RecordFieldSource::Cell; + } + } + + if (auto *def = root.getDefiningOp(); def && isCall(def) && resultBorrows(def) >= 0) + { + return RecordFieldSource::Place; + } + + if (borrowedParam(root) >= 0) + { + return RecordFieldSource::Param; + } + + if (slotLoadOf(value)) + { + return RecordFieldSource::Slot; + } + + return RecordFieldSource::Unknown; +} + +// The box of the object literal built in `varOp`: its one read, copied into a block `ts.New` made +// (recordBoxOf). Null for anything else. +inline mlir_ts::NewOp recordBoxOfLocal(mlir_ts::VariableOp varOp) +{ + if (!varOp || !isConstantRecordLocal(varOp)) + { + return {}; + } + + for (auto *user : varOp->getUsers()) + { + auto loadOp = mlir::dyn_cast(user); + if (!loadOp) + { + continue; + } + + for (auto *readUser : loadOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(readUser); + if (storeOp && storeOp.getValue() == loadOp.getResult()) + { + if (auto boxOp = recordBoxOf(storeOp.getReference()); boxOp && boxOp.getResult() == storeOp.getReference()) + { + return boxOp; + } + } + } + } + + return {}; +} + +// The fields a borrowing record's box (or an interface made of it) leaves to their owners; empty for +// any other value. +inline llvm::ArrayRef recordBorrowedFields(mlir::Operation *op) +{ + if (auto attr = op ? op->getAttrOfType(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME) : nullptr) + { + return attr.asArrayRef(); + } + + return {}; +} + +// A store into a field of a borrowing record that the record borrows: it takes nothing. +inline bool isBorrowedRecordFieldStore(mlir::Operation *op) +{ + auto storeOp = mlir::dyn_cast(op); + auto propertyRefOp = storeOp ? storeOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); + auto varOp = propertyRefOp ? propertyRefOp.getObjectRef().getDefiningOp() : mlir_ts::VariableOp(); + auto boxOp = varOp ? recordBoxOfLocal(varOp) : mlir_ts::NewOp(); + return boxOp && llvm::is_contained(recordBorrowedFields(boxOp), static_cast(propertyRefOp.getPosition())); +} + // The tuple an object, a reference or a tuple value of this type stores its fields in; null for // any other type. A method sees its object as `!ts.object>`, whose // fields are the tuple's. diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index aa893f2f2..1dacfa314 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -1,6 +1,7 @@ #include "mlir/Pass/Pass.h" #include "mlir/IR/Dominance.h" #include "mlir/Dialect/LLVMIR/LLVMAttrs.h" +#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" #include "mlir/Interfaces/ControlFlowInterfaces.h" #include "mlir/Interfaces/FunctionInterfaces.h" @@ -19,6 +20,11 @@ #include "llvm/ADT/SmallPtrSet.h" #include "llvm/ADT/SmallVector.h" #include "llvm/ADT/STLFunctionalExtras.h" +#include "llvm/ADT/StringSet.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include #define DEBUG_TYPE "own" @@ -48,7 +54,9 @@ using namespace own_facts; // after its taker, a taker that control comes back to without the value being made again is a // move on every loop iteration (2.6), and each release the move reaches must be dominated by it // and is erased. A release the move cannot reach stays - the owner on the paths that did not -// move. Anything else is an error at its location. +// move. A local's release that paths which moved it and paths which did not both reach is placed +// instead on the edges where the latter join the former (spec 30). Anything else is an error at +// its location. // // It runs on every function: a `ts.Func`, and the `func.func` the async lowering outlines an // `await`'s continuation or a `for await` body into, whose arguments are the values it captures. @@ -117,9 +125,12 @@ class OwnershipInferencePass // reads out of fields and elements, and results that borrow an argument, and what may // destroy what they read (spec 2.3, 2.4) llvm::SmallVector placeReads; + // reads of what a mortal global holds (isGlobalRead) + llvm::SmallVector globalReads; // calls whose callee keeps an argument: each such argument moves into the call llvm::SmallVector keepingCalls; drops.clear(); + edgeReleases.clear(); derivedCache.clear(); closures.clear(); closureOf.clear(); @@ -130,11 +141,31 @@ class OwnershipInferencePass captureRetains.clear(); returnedParams.clear(); returnsBorrowOf = resultBorrows(f); + returnsRecordOf = isRecordResult(f) ? resultBorrowsAll(f) : llvm::SmallVector(); + records.clear(); + recordStoreUses.clear(); // first: whether a closure borrows what it captures changes what its box's stores are classifyClosures(); classifyStateBoxes(); classifyBorrowingFields(); + // ...and whether an object literal borrows what its fields hold (spec 32) + classifyRecords(); + // a kept parameter the body may not move on every path is released at its exits (spec 33) + releaseKeptParamsAtExits(); + auto exitReleasesGone = llvm::make_scope_exit([&]() { + // the dry run changes nothing: what it put in goes again + if (dryRun) + { + for (auto *release : exitReleases) + { + toErase.remove(release); + release->erase(); + } + } + + exitReleases.clear(); + }); // A view of a block is that block: the candidate is always the root. f.walk([&](mlir::Operation *op) { @@ -196,6 +227,10 @@ class OwnershipInferencePass { placeReads.push_back(loadOp); } + else if (isGlobalRead(loadOp)) + { + globalReads.push_back(loadOp); + } } else if (auto releaseSlotOp = mlir::dyn_cast(op); releaseSlotOp && isYieldedField(releaseSlotOp.getSlot())) { @@ -205,9 +240,10 @@ class OwnershipInferencePass } else if (auto releaseSlotOp = mlir::dyn_cast(op)) { - // an overwrite of a field or an element, or an assignment to a captured variable + // an overwrite of a field or an element, or an assignment to a captured variable or + // a global (spec 29: the global may hold what `this` or a parameter is) auto slot = releaseSlotOp.getSlot(); - if (isPlace(slot) || isCellVariable(slot) || isLoadedCell(slot)) + if (isPlace(slot) || isCellVariable(slot) || isLoadedCell(slot) || slot.getDefiningOp()) { drops.push_back(op); } @@ -262,6 +298,11 @@ class OwnershipInferencePass checkPlaceRead(read, toErase); } + for (auto read : globalReads) + { + checkGlobalRead(read); + } + // What is proven loses its retains; any other retain is a second reference nobody // proved safe - of a loaded value, a parameter, an unknown producer, or of a fresh value // whose own check was reported above. @@ -352,6 +393,14 @@ class OwnershipInferencePass continue; } + // an immortal global's value (spec 25): owned by no one, so returned, stored or kept + // as often as it is read, and its releases skip it + if (value && isImmortalGlobalRead(value) && (mlir::isa(op) || slotHoldsInitializerAt(op))) + { + toErase.insert(op); + continue; + } + // an array a spread builds, read into the owning local it is made for, or returned if (value && mlir::isa(op) && isBuiltArrayRead(op, value)) { @@ -418,6 +467,8 @@ class OwnershipInferencePass decideCells(); checkBorrowedReads(); checkBoundedResults(); + checkBorrowingRecords(); + checkRecordResults(); toErase.insert(fieldBorrowReleases.begin(), fieldBorrowReleases.end()); if (dryRun) @@ -427,6 +478,7 @@ class OwnershipInferencePass // a handle's retains and releases are rc's, and stay (spec 23.2) toErase.remove_if([](mlir::Operation *op) { return touchesHandle(op); }); + placeEdgeReleases(); for (auto *op : toErase) { op->erase(); @@ -435,8 +487,10 @@ class OwnershipInferencePass // the facts were for this pass only returnedParams.clear(); f.walk([](mlir::Operation *op) { - for (auto *name : {OWN_PARAMS_ATTR_NAME, OWN_RESULT_BORROWS_ATTR_NAME, OWN_NO_DROPS_ATTR_NAME, - OWN_FACTS_LOST_ATTR_NAME, OWN_FRESH_RESULT_ATTR_NAME, OWN_ASSIGNS_CAPTURES_ATTR_NAME, + for (auto *name : {OWN_PARAMS_ATTR_NAME, OWN_PARAMS_RELEASED_ATTR_NAME, OWN_RESULT_BORROWS_ATTR_NAME, + OWN_RESULT_RECORD_ATTR_NAME, + OWN_NO_DROPS_ATTR_NAME, OWN_FACTS_LOST_ATTR_NAME, OWN_FRESH_RESULT_ATTR_NAME, + OWN_ASSIGNS_CAPTURES_ATTR_NAME, OWN_RESULT_BOUNDED_ATTR_NAME, OWN_CAPTURE_BORROW_ATTR_NAME}) { @@ -615,6 +669,10 @@ class OwnershipInferencePass int returnsBorrowOf = -1; llvm::DenseSet returnedParams; + // The parameters this function's result is a borrowing record of (OWN_RESULT_RECORD_ATTR_NAME, + // spec 32); empty for none. + llvm::SmallVector returnsRecordOf; + // ---- Closures (spec 2.5, phase 5) ---- // // A closure is `ts.CreateBoundFunction(box, @f) {__owns_capture}`; its box holds a cell for each @@ -1889,9 +1947,10 @@ class OwnershipInferencePass auto owner = varName(slot.getDefiningOp()); if (receivers.size() == 1 || takeValuesOfTheirOwn(slot, receivers)) { + // a move on some paths only is placed when it is the local's one receiver (spec 30) for (auto &receiver : receivers) { - decideSlotReceiver(slot, receiver, owner, toErase); + decideSlotReceiver(slot, receiver, owner, toErase, receivers.size() == 1); } return; @@ -1901,9 +1960,9 @@ class OwnershipInferencePass } void decideSlotReceiver(mlir::Value slot, const SlotReceiver &receiver, llvm::StringRef owner, - llvm::SetVector &toErase) + llvm::SetVector &toErase, bool sole) { - if (quietly([&] { return checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase); })) + if (quietly([&] { return checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase, sole); })) { // rc's retain goes with the move; a call that keeps the argument is the move itself if (!isCall(receiver.retain)) @@ -1920,7 +1979,7 @@ class OwnershipInferencePass return; } - checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase); // reports + checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase, sole); // reports } // Receivers of one owning local that each read a value of its own (#528): `h.c = a; a = new C(); @@ -1951,6 +2010,114 @@ class OwnershipInferencePass return true; } + // ---- A kept parameter released where it is not moved (spec 33) ---- + // + // `return v.x > 2 ? acc : V.plus(acc, v)`: `acc` is kept (OwnershipSignaturePass), so the caller + // moved it in; the path that returns it moves it out, and the others must release it. So must a + // callback that only borrows the accumulator its family gives it to keep. MLIRGen made no owner + // of a parameter, so its releases are put in here, before each way out of the body - a return, + // a throw that leaves the function - as MLIRGen puts an owning local's. The move's own check then + // erases those it reaches, and places the release on the edges into the join that did not move + // it (spec 30), statically: no run-time flag. Unwinding out of the function (a call that throws + // outside a `try`, a `finally` passed on the way out) gives nothing back: it leaks, as an owning + // local does there. + + // the releases put in for this run + llvm::SmallVector exitReleases; + + // The slot of parameter `index` (isParameterSlot), or null. + mlir_ts::VariableOp parameterSlot(int32_t index) + { + auto &body = getFunction().getFunctionBody(); + if (body.empty() || static_cast(index) >= body.front().getNumArguments()) + { + return {}; + } + + for (auto *user : body.front().getArgument(index).getUsers()) + { + auto varOp = mlir::dyn_cast(user); + auto position = -1; + if (varOp && isParameterSlot(varOp, position) && position == index) + { + return varOp; + } + } + + return {}; + } + + // Is `slot` a parameter this function keeps and releases where it does not move it? + bool isReleasedParam(mlir::Value slot) + { + auto f = getFunction(); + auto attr = f->getAttrOfType(OWN_PARAMS_RELEASED_ATTR_NAME); + auto varOp = slot.getDefiningOp(); + auto index = -1; + return attr && varOp && isParameterSlot(varOp, index) && llvm::is_contained(attr.asArrayRef(), index) && + llvm::is_contained(ownedParams(f), index); + } + + // Does `value` come from a read of `slot`: computed from one, through any op? + static bool readsSlot(mlir::Value value, mlir::Value slot, int depth = 0) + { + auto *def = value.getDefiningOp(); + if (!def) + { + return false; + } + + // too deep to tell: as if it did + if (depth > 16) + { + return true; + } + + if (auto loadOp = mlir::dyn_cast(def); loadOp && loadOp.getReference() == slot) + { + return true; + } + + return llvm::any_of(def->getOperands(), [&](mlir::Value operand) { return readsSlot(operand, slot, depth + 1); }); + } + + void releaseKeptParamsAtExits() + { + exitReleases.clear(); + auto f = getFunction(); + auto attr = f->getAttrOfType(OWN_PARAMS_RELEASED_ATTR_NAME); + if (!attr) + { + return; + } + + for (auto index : attr.asArrayRef()) + { + auto varOp = parameterSlot(index); + if (!varOp || !llvm::is_contained(ownedParams(f), index)) + { + continue; + } + + for (auto &block : f.getFunctionBody()) + { + auto *exit = block.empty() ? nullptr : &block.back(); + // a thrown value made of the parameter takes it with it: no release there (a leak, + // if it does not) + auto leaves = mlir::isa_and_nonnull(exit) || + (mlir::isa_and_nonnull(exit) && + !readsSlot(exit->getOperand(0), varOp.getResult())); + if (!leaves) + { + continue; + } + + mlir::OpBuilder builder(exit); + exitReleases.push_back(builder.create(exit->getLoc(), varOp.getResult())); + } + } + } + // A parameter this function keeps (`__own_params`): its caller moved the argument in, and this // function has no release for it, so exactly one taker must move it on, on every way out of // the function, and nothing may read it after that. @@ -1967,8 +2134,22 @@ class OwnershipInferencePass return; } - if (!checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase)) + // one the body releases on the paths that do not move it (spec 33): the move's own check + // erases the releases at the exits it reaches, and places one on each edge into where the + // paths join that did not move it (spec 30) + auto released = isReleasedParam(slot); + if (!checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase, released)) + { + return; + } + + if (released) { + if (!isCall(receiver.retain)) + { + toErase.insert(receiver.retain); + } + return; } @@ -1984,7 +2165,9 @@ class OwnershipInferencePass } }); - if (everyPath && mlir::isa(receiver.retain)) + // rc's retain goes with the move - a `ts.RetainSlot` too, for an owning local the parameter + // moves into (`let r = init`, spec 33); a call that keeps it is the move itself + if (everyPath && !isCall(receiver.retain)) { toErase.insert(receiver.retain); } @@ -2235,7 +2418,8 @@ class OwnershipInferencePass // that owns nothing and is assigned it (`for (x of arr)` into an outer `let x`). Answers // whether the use is one of those, even where nothing comes out of it. Where the value lands // somewhere that is replaced - a block argument, a local - `renewedAt` gets what replaces it: - // the block's entry, the local's declaration and assignments. + // the block's entry, the local's declaration (when it has an initializer: `let l!: V` stores + // nothing, and in a loop the slot keeps the last round's value) and assignments. static bool passesOn(mlir::OpOperand &use, llvm::SmallVectorImpl &values, llvm::SmallVectorImpl *renewedAt = nullptr) { @@ -2267,7 +2451,7 @@ class OwnershipInferencePass if (auto varOp = storeOp.getReference().getDefiningOp(); varOp && !isOwningVariable(varOp) && !varOp.getCaptured().value_or(false)) { - if (renewedAt) + if (renewedAt && varOp.getInitializer()) { renewedAt->push_back(varOp); } @@ -2318,7 +2502,7 @@ class OwnershipInferencePass mlir::Operation *walkBorrowed( mlir::Value start, llvm::ArrayRef kills, llvm::SmallVectorImpl &uses, llvm::function_ref)> isBorrower = nullptr, - Keeping keeping = Keeping{nullptr, false}, bool countedHandle = false) + Keeping keeping = Keeping{nullptr, false}, bool countedHandle = false, bool startBorrowed = true) { struct Pending { @@ -2328,7 +2512,9 @@ class OwnershipInferencePass }; llvm::SmallVector values; - values.push_back({start, true, {kills.begin(), kills.end()}}); + // `startBorrowed` false: `start` is what a read produced (a field's reference), whose uses + // are uses of the borrow but not the borrowed value itself + values.push_back({start, startBorrowed, {kills.begin(), kills.end()}}); llvm::SmallPtrSet seen{start}; while (!values.empty()) { @@ -2361,6 +2547,16 @@ class OwnershipInferencePass } } + // stored into a field a borrowing record borrows (spec 32): whatever uses the record + // reads it + if (auto found = recordStoreUses.find(user); found != recordStoreUses.end()) + { + for (auto &recordUse : found->second->walk.uses) + { + uses.push_back({recordUse.op, pending.kills, recordUse.callee}); + } + } + if (pending.borrowed && keeping.returns && mlir::isa(user)) { continue; @@ -2418,12 +2614,8 @@ class OwnershipInferencePass continue; } - // A call known to make its result (`__own_fresh_result`: an iterator's `next()`, a field - // that only ever holds one function, another own module's `..new`) hands back a block of - // the caller's own - the `{value, done}` record of `it.next()` points nowhere into `it`, - // and what it does after its next call cannot be a use of the method read out of `it` - - // unless it is a generator's state that borrows its arguments (`__own_result_bounded`). - if (isCall(user) && user->hasAttr(OWN_FRESH_RESULT_ATTR_NAME) && !user->hasAttr(OWN_RESULT_BOUNDED_ATTR_NAME)) + // a call whose result is the caller's own (callResultIsOwn) ends the walk at the call + if (callResultIsOwn(user)) { continue; } @@ -2442,6 +2634,30 @@ class OwnershipInferencePass return nullptr; } + // Whether a call hands back a block of the caller's own, which points into nothing it was given: + // walkBorrowed then ends a borrow's walk at the call (the call itself is still a use for as long + // as it runs). Never a generator's state that borrows its arguments (`__own_result_bounded`). + // - A call known to make its result (`__own_fresh_result`: an iterator's `next()`, a field that + // only ever holds one function, another own module's `..new`) - the `{value, done}` record of + // `it.next()` points nowhere into `it`, and what it does after its next call cannot be a use of + // the method read out of `it`. + // - A direct call of a function this module defines (isFresh): its result points into nothing it + // was given but what its facts name - a borrow of an argument (`__own_result_borrows`, never + // fresh), a borrowing record (isFresh, but bounded by what it borrows), a generator's state + // that borrows its arguments - and a body whose callers cannot all know such a fact is an + // error. `getPoint(x, scene.camera)` makes a new vector: a call after it that may destroy + // `scene.camera` does not end that vector (spec 34). + static bool callResultIsOwn(mlir::Operation *call) + { + if (!isCall(call) || call->hasAttr(OWN_RESULT_BOUNDED_ATTR_NAME)) + { + return false; + } + + return call->hasAttr(OWN_FRESH_RESULT_ATTR_NAME) || + (call->getNumResults() == 1 && !isRecordResult(call) && isFresh(call->getResult(0))); + } + // Is this what an indirect call goes through: its callee value, or, for a method read out of // a field of an object (`o.m()`, `m` a field holding a function), the object it is bound to, // which is the container of what was read, not what was borrowed. A method of a class bound @@ -2468,6 +2684,46 @@ class OwnershipInferencePass mlir::isa(methodField.getReference().getType()); } + // ---- Borrowing records (spec 32) ---- + // + // An object literal boxed as an interface that holds values it does not own - `this`, a parameter, + // a field read, an owning `let`'s value, a captured variable's - beside fresh values, which move + // in. The signature pass marked its box (OWN_RECORD_BORROWED_FIELDS_ATTR_NAME): its release frees + // the box and the fields it owns, and leaves the others to their owners. So the box has one owner + // like any fresh block - the box's own check - and what it borrows bounds every use of it: a read + // out of a place is checked with the record's uses as its own (walkBorrowed), a local's value + // against the local's ends, a captured variable's against whatever may assign it. Returned, the + // record is the caller's to own and to bound (checkRecordResults), which the function's facts + // must say. Stored anywhere else that may outlive what it borrows, it is an error. + struct RecordWalk + { + llvm::SmallVector uses; + // the first use that would keep the record itself: a store into a place, a callee that keeps + // it, a capture + mlir::Operation *kept = nullptr; + llvm::SmallVector returns; + }; + + struct BorrowedField + { + mlir_ts::StoreOp store; + RecordFieldSource source; + }; + + struct BorrowingRecord + { + mlir_ts::NewOp box; + mlir_ts::VariableOp local; + llvm::SmallVector interfaces; + llvm::SmallVector borrowed; + RecordWalk walk; + }; + + llvm::SmallVector> records; + // a store of a place read into a borrowing record's field -> the record's uses, which are the + // read's uses too (walkBorrowed) + llvm::DenseMap recordStoreUses; + // Can a value of this type point into a heap block - a reference into one, a block of its own, // or a method bound to one? A number or a boolean read out of a borrowed block cannot. bool mayPointInto(mlir::Value value) @@ -2478,146 +2734,634 @@ class OwnershipInferencePass ownsHeap(value); } - // ---- Reads out of containers (spec 2.3, phase 3) ---- - // - // `h.c` and `arr[i]` read a value the container owns. Such a read is a borrow: it may not be - // kept, and none of its uses - nor any use of what it produces - may run after something that - // may destroy it (spec 2.2's dropping mutations): an overwrite of a place on the way from the - // read up to its root, an array op that removes elements, an end of the root itself, or a call - // that may reach the root. A path back to a use through the read itself is a new borrow. - - // A read of a heap value out of a field or an element, or a call whose result borrows an - // argument (spec 2.4, phase 4), seen through its views. Null for anything else. - mlir::Operation *placeReadOf(mlir::Value value) + // The borrowing record built in `varOp` (classifyRecords), or null. + BorrowingRecord *borrowingRecordOf(mlir_ts::VariableOp varOp) { - auto root = rootOf(value); - auto *def = root.getDefiningOp(); - // a handle read out of a place that rc gives an owner of its own is a counted copy, not a - // borrow (spec 23.3); any other is a borrow of the place, as a field read is - if (!def || !ownsHeap(root) || (isHandle(root) && isCountedHandleRead(root))) + for (auto &record : records) { - return nullptr; - } - - if (auto loadOp = mlir::dyn_cast(def); loadOp && isPlace(loadOp.getReference())) - { - // a field of a generator's borrowing box: only its maker fills it, and nothing a call - // can reach overwrites it; it is the caller's, as a parameter is (checkBorrowedReads) - if (isBorrowingBoxField(loadOp.getReference())) + if (record->local == varOp) { - return nullptr; + return record.get(); } - - return def; } - return borrowingCall(root); + return nullptr; } - // How a handle read out of a place is used (spec 23.3), seen through casts, merges and locals - // that own nothing. Every use is classified, and nothing falls through (Ruling 9): - // - `counted`: a receiver that takes a count rc made for that very use - // (isCountedHandleReceiver) - a store, a push or an unshift, `new Shared`, an array - // literal's element, a return, an `any` box, or an owning local's initializer; - // - passed on: a cast or a view that is still a handle, a merge, a local that owns nothing - - // whose own uses are classified in turn; - // - `borrowed`: everything else. A use that reads through the place (`f(p, p.child)`, a - // `.value` read, `Shared.count`), and every use that is neither of the above: a tuple's - // element, an object literal's field, a capture, a `yield`, a receiver rc made no retain - // for, any op not named here. The read is then a borrow, and tenure-checked - // (checkPlaceRead). - // rc's retains count for nothing on their own: each is the count of the receiver right after - // it. `cur = cur.value.next` is counted only. - struct HandleReadUses + // The boxes the signature pass marked as borrowing records, where every field still holds what it + // was marked for: a borrowed field a value this pass bounds (recordFieldSource), any other one a + // fresh value or nothing. A store into a borrowed field takes nothing, and whatever uses the record + // reads what it stored (walkBorrowed, through recordStoreUses). A box that is not what it was + // marked for is left to the record's retain, which is then an error. + void classifyRecords() { - bool counted; - bool borrowed; - }; + getFunction().walk([&](mlir_ts::NewOp newOp) { + auto fields = recordBorrowedFields(newOp); + auto varOp = fields.empty() ? mlir_ts::VariableOp() : recordLocalOf(newOp.getResult()); + if (!varOp) + { + return; + } - static HandleReadUses handleReadUses(mlir::Value value) - { - HandleReadUses result{false, false}; - llvm::SmallVector values{value}; - llvm::SmallPtrSet seen{value}; - while (!values.empty()) - { - auto current = values.pop_back_val(); - for (auto &use : current.getUses()) + auto record = std::make_shared(); + record->box = newOp; + record->local = varOp; + for (auto *user : varOp->getUsers()) { - auto *user = use.getOwner(); - if (mlir::isa(user)) + auto propertyRefOp = mlir::dyn_cast(user); + if (!propertyRefOp) { continue; } - // a return and an `any` box take the value, though passesOn would hand them on - if (isHandOff(user, current)) + auto borrows = llvm::is_contained(fields, static_cast(propertyRefOp.getPosition())); + for (auto *fieldUser : propertyRefOp->getUsers()) { - (isCountedHandleReceiver(user, current) ? result.counted : result.borrowed) = true; - continue; + auto storeOp = mlir::dyn_cast(fieldUser); + if (!storeOp || storeOp.getReference() != propertyRefOp.getResult()) + { + continue; + } + + auto source = recordFieldSource(storeOp.getValue()); + if (source == RecordFieldSource::NoBlock) + { + continue; + } + + if (source == RecordFieldSource::Unknown || borrows != (source != RecordFieldSource::Fresh)) + { + return; + } + + if (borrows) + { + record->borrowed.push_back({storeOp, source}); + } } + } - llvm::SmallVector same; - if (passesOn(use, same)) + for (auto *user : newOp->getUsers()) + { + if (auto castOp = mlir::dyn_cast(user)) { - for (auto next : same) + for (auto *castUser : castOp->getUsers()) { - // what is no longer a handle is no receiver: a test (`if (p.child)`) only - // reads it - if (!isHandle(next)) + if (auto newInterfaceOp = mlir::dyn_cast(castUser)) { - result.borrowed = true; - } - else if (seen.insert(next).second) - { - values.push_back(next); + record->interfaces.push_back(newInterfaceOp.getResult()); } } - - continue; } - - (isCountedHandleReceiver(user, current) ? result.counted : result.borrowed) = true; } - } - return result; - } + // a path back to a use through the record's making is another record + llvm::SmallVector kills{newOp.getOperation(), varOp.getOperation()}; + walkRecord(record->interfaces, kills, record->walk); + for (auto &field : record->borrowed) + { + setAttrTracked(field.store, OWN_CAPTURE_BORROW_ATTR_NAME); + recordStoreUses[field.store.getOperation()] = record.get(); + } - // A handle read out of a place that rc gives a count of its own is a counted copy, not a borrow - // (spec 23.3) - but only when every use of it is one rc counts (Ruling 7), and a use that is - // not known to be one is not (Ruling 9). A read with one use that reads through the place as - // well (`f(p, q, q.child = p.child)`), or that keeps it with no count (`[(q.child = p.child), - // 1]`), is a borrow, and that use is tenure-checked; the counted uses are copies - // (checkPlaceRead). - static bool isCountedHandleRead(mlir::Value value) - { - auto uses = handleReadUses(value); - return uses.counted && !uses.borrowed; + records.push_back(std::move(record)); + }); } - // A use that gives a handle an owner rc counts, with rc's retain for that very use: rc's - // retain itself, a store into a field, an element, a global, an owning local or the result - // slot (a return) that rc retained for (countedStoreRetain); a push or an unshift, `new - // Shared`, an array literal's element, an `any` box, a `ts.ReturnVal`, each right after rc's - // retain of what it takes (retainedRightBefore); or the initializer of an owning local, which - // takes a count of its own (`ts.RetainSlot`). Any of them without its retain is not one: it - // keeps the read without a count (`{ s: (q.child = p.child) }`'s field). - static bool isCountedHandleReceiver(mlir::Operation *user, mlir::Value value) + // Every use of a borrowing record (spec 32), and of what is read out of it - each may read what + // the record borrows - through its views, merges, and the locals that hold it, made again at + // `kills`. Its own retains and releases are the box's, which its own check decides. The record + // itself is `kept` when it is stored anywhere but a local, given to a callee that keeps it, or + // captured; returned, it is in `returns`. A call given it hands back its own block, unless that + // is a borrowing record that holds it, whose uses are then this one's as well. + void walkRecord(llvm::ArrayRef starts, llvm::ArrayRef kills, RecordWalk &walk) { - if (mlir::isa(user)) + struct Pending { - return true; - } + mlir::Value value; + bool record; + }; - if (auto varOp = mlir::dyn_cast(user)) + llvm::SmallVector work; + llvm::SmallPtrSet seen; + auto push = [&](mlir::Value value, bool record) { + if ((record || mayPointInto(value)) && seen.insert(value).second) + { + work.push_back({value, record}); + } + }; + + auto followSlot = [&](mlir_ts::VariableOp varOp, bool record) { + for (auto *slotUser : varOp->getUsers()) + { + // its releases and its other values are the slot's own + if (auto loadOp = mlir::dyn_cast(slotUser)) + { + push(loadOp.getResult(), record); + } + } + }; + + auto keep = [&](mlir::Operation *user) { walk.kept = walk.kept ? walk.kept : user; }; + + for (auto start : starts) { - return isOwningVariable(varOp) && llvm::any_of(varOp->getUsers(), [](mlir::Operation *varUser) { - return mlir::isa(varUser); - }); + push(start, true); } - if (auto storeOp = mlir::dyn_cast(user)) + while (!work.empty()) + { + auto pending = work.pop_back_val(); + auto value = pending.value; + for (auto &use : value.getUses()) + { + auto *user = use.getOwner(); + if (mlir::isa(user) || + (pending.record && mlir::isa(user))) + { + continue; + } + + // the same value: a view, a cast, a merge + if ((isView(user) && use.getOperandNumber() == 0) || mlir::isa(user)) + { + push(user->getResult(0), pending.record); + continue; + } + + if (auto branchOp = mlir::dyn_cast(user)) + { + if (auto argument = branchOp.getSuccessorBlockArgument(use.getOperandNumber())) + { + push(*argument, pending.record); + } + + continue; + } + + // a local that holds it: its reads + auto varOp = mlir::dyn_cast(user); + auto storeOp = mlir::dyn_cast(user); + if (varOp || (storeOp && storeOp.getValue() == value)) + { + auto slotOp = varOp ? varOp : storeOp.getReference().getDefiningOp(); + if (slotOp && !slotOp.getCaptured().value_or(false)) + { + followSlot(slotOp, pending.record); + continue; + } + + // a place, a global, a captured variable: what is read out of the record is checked + // where it is read (checkPlaceRead) + walk.uses.push_back({user, {kills.begin(), kills.end()}}); + if (pending.record) + { + keep(user); + } + + continue; + } + + walk.uses.push_back({user, {kills.begin(), kills.end()}, isCall(user) && isCalleeOperand(use)}); + if (mlir::isa(user)) + { + if (pending.record) + { + walk.returns.push_back(user); + } + + continue; + } + + if (isCall(user)) + { + if (pending.record && isKeptArgument(user, value)) + { + keep(user); + continue; + } + + // a result that borrows it - a plain borrow (`id(r)`), or a borrowing record that + // holds it - points where it points + auto args = callArgs(user); + auto given = [&](llvm::ArrayRef indices) { + return llvm::any_of(indices, [&](int index) { + return static_cast(index) < args.size() && args[index] == value; + }); + }; + + if (user->getNumResults() == 1 && given(resultBorrowsAll(user))) + { + push(user->getResult(0), pending.record || isRecordResult(user)); + } + + // ...and a generator's state that borrows it (OWN_RESULT_BOUNDED_ATTR_NAME) + if (auto bounded = user->getAttrOfType(OWN_RESULT_BOUNDED_ATTR_NAME); + bounded && user->getNumResults() == 1 && + given(llvm::SmallVector(bounded.asArrayRef().begin(), bounded.asArrayRef().end()))) + { + push(user->getResult(0), false); + } + + continue; + } + + if (pending.record && !isBorrow(user, value)) + { + keep(user); + continue; + } + + // a field written through the record: its old value may be one the record borrows, + // which the overwrite would release, and the field may be one its release leaves + if (pending.record && mlir::isa(user)) + { + for (auto *fieldUser : user->getResult(0).getUsers()) + { + auto fieldStore = mlir::dyn_cast(fieldUser); + if (mlir::isa(fieldUser) || + (fieldStore && fieldStore.getReference() == user->getResult(0))) + { + keep(fieldUser); + } + } + } + + // what it produces - a field's reference, a value read out of it, a bound method - may + // still point into what the record borrows + for (auto result : user->getResults()) + { + push(result, false); + } + } + } + } + + // The name the record goes by: the local that holds it, or a folded `const`'s. + llvm::StringRef recordName(mlir::Value value) + { + llvm::StringRef name = ownerName(value); + forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { + if (auto varOp = mlir::dyn_cast(user); varOp && varOp.getInitializer() == used) + { + name = varName(varOp); + } + }); + + return name; + } + + // What a borrowed field holds, for an error: `'ray'`, `'this.pos'`, `'livec'`, ... + std::string describeRecordSource(BorrowedField field) + { + auto value = field.store.getValue(); + auto quoted = [](llvm::StringRef name) { return "'" + name.str() + "'"; }; + switch (field.source) + { + case RecordFieldSource::Param: + return quoted(paramName(borrowedParam(value))); + case RecordFieldSource::Place: + if (auto loadOp = rootOf(value).getDefiningOp(); loadOp && isPlace(loadOp.getReference())) + { + return describePlace(loadOp.getReference()); + } + + return describeCall(rootOf(value).getDefiningOp()); + case RecordFieldSource::Slot: + return quoted(varName(slotLoadOf(value).getReference().getDefiningOp())); + case RecordFieldSource::Cell: + return "a captured variable"; + default: + return "a value it does not own"; + } + } + + // A record this function makes (spec 32): kept, it is an error; returned, each field it borrows + // must be a parameter's that this function's facts lend (`__own_result_record`), so that its + // callers bound it; and no use of it may come after what a field borrows may be released or + // overwritten - an owning local's ends, and whatever may assign a captured variable. A parameter + // and `this` are the caller's for the whole call; a read out of a place has the record's uses as + // its own (checkPlaceRead). + void checkBorrowingRecords() + { + for (auto &record : records) + { + auto name = recordName(record->interfaces.empty() ? record->box.getResult() : record->interfaces.front()); + if (record->walk.kept) + { + reportPlaceBorrowEscapes(record->walk.kept, name, + record->borrowed.empty() ? "a value it does not own" + : describeRecordSource(record->borrowed.front())); + continue; + } + + auto ok = true; + for (auto *returned : record->walk.returns) + { + for (auto &field : record->borrowed) + { + if (!ok || field.source == RecordFieldSource::Immortal) + { + continue; + } + + auto param = field.source == RecordFieldSource::Param || field.source == RecordFieldSource::Place + ? borrowedParam(field.store.getValue()) + : -1; + if (param < 0 || !llvm::is_contained(returnsRecordOf, param)) + { + reportPlaceBorrowEscapes(returned, name, describeRecordSource(field)); + ok = false; + } + } + } + + for (auto &field : record->borrowed) + { + if (!ok) + { + break; + } + + auto value = field.store.getValue(); + Chain chain; + if (field.source == RecordFieldSource::Slot) + { + chain.roots.push_back({slotLoadOf(value).getReference(), Chain::Slot}); + } + else if (auto load = field.source == RecordFieldSource::Param ? keptParamLoadOf(value) : mlir_ts::LoadOp()) + { + // a parameter this function keeps is its own to move away, as a local's value is + chain.roots.push_back({load.getReference(), Chain::Slot}); + } + else if (field.source == RecordFieldSource::Cell) + { + chain.roots.push_back({rootOf(value), Chain::NotOwned}); + } + else + { + continue; + } + + auto ends = rootEnds(chain); + if (field.source == RecordFieldSource::Cell) + { + for (auto *drop : drops) + { + if (dropsChain(drop, chain, nullptr, value)) + { + ends.push_back(drop); + } + } + } + + for (auto *end : ends) + { + for (auto &use : record->walk.uses) + { + if (ok && (use.op != end || !use.callee) && reachableAfter(end, use.op, use.kills)) + { + if (field.source == RecordFieldSource::Slot) + { + reportBorrowOutlives(use.op, end, name, varName(chain.roots.front().first.getDefiningOp())); + } + else + { + reportPlaceBorrowOutlives(use.op, end, name, describeRecordSource(field)); + } + + ok = false; + } + } + } + } + } + } + + // A call whose result is a borrowing record (OWN_RESULT_RECORD_ATTR_NAME, spec 32): the box is + // this function's to own - it is fresh, and its own check decides its moves and releases - but + // it may hold a borrow of each argument the facts name, as a call's result that borrows them + // does (checkPlaceRead): somewhere under each. So no use of it, through the locals it moves + // into, may come after one of those arguments may be released or overwritten, and it may not be + // kept. Returned, the arguments must be parameters this function's own facts lend. Its releases + // are not uses: they free only what the record owns. + void checkRecordResults() + { + getFunction().walk([&](mlir::Operation *call) { + if (!isCall(call) || !isRecordResult(call) || call->getNumResults() != 1) + { + return; + } + + auto result = call->getResult(0); + auto place = describeCall(call); + auto name = recordName(result); + RecordWalk walk; + llvm::SmallVector kills{call}; + walkRecord({result}, kills, walk); + if (walk.kept) + { + reportPlaceBorrowEscapes(walk.kept, name, place); + return; + } + + auto args = callArgs(call); + for (auto *returned : walk.returns) + { + for (auto index : resultBorrowsAll(call)) + { + if (static_cast(index) >= args.size() || argumentHoldsNoBlock(args[index]) || + isImmortalLiteral(args[index])) + { + continue; + } + + auto param = borrowedParam(args[index]); + if (param < 0 || !llvm::is_contained(returnsRecordOf, param)) + { + reportPlaceBorrowEscapes(returned, name, place); + return; + } + } + } + + auto chain = chainOf(call); + // rc's retain of a fresh argument right before the call is no end: the box's own check + // decides it, and the use that takes it is an end of its own + llvm::SmallVector ends; + for (auto *end : rootEnds(chain)) + { + auto ownedRetain = mlir::isa(end) && llvm::any_of(chain.roots, [&](auto &root) { + return root.second == Chain::Owned && rootOf(end->getOperand(0)) == root.first; + }); + if (!ownedRetain) + { + ends.push_back(end); + } + } + + for (auto *drop : drops) + { + if (drop != call && dropsChain(drop, chain, call)) + { + ends.push_back(drop); + } + } + + for (auto *end : ends) + { + for (auto &use : walk.uses) + { + if ((use.op != end || !use.callee) && reachableAfter(end, use.op, use.kills)) + { + reportPlaceBorrowOutlives(use.op, end, name, place); + return; + } + } + } + }); + } + + // ---- Reads out of containers (spec 2.3, phase 3) ---- + // + // `h.c` and `arr[i]` read a value the container owns. Such a read is a borrow: it may not be + // kept, and none of its uses - nor any use of what it produces - may run after something that + // may destroy it (spec 2.2's dropping mutations): an overwrite of a place on the way from the + // read up to its root, an array op that removes elements, an end of the root itself, or a call + // that may reach the root. A path back to a use through the read itself is a new borrow. + + // A read of a heap value out of a field or an element, or a call whose result borrows an + // argument (spec 2.4, phase 4), seen through its views. Null for anything else. + mlir::Operation *placeReadOf(mlir::Value value) + { + auto root = rootOf(value); + auto *def = root.getDefiningOp(); + // a handle read out of a place that rc gives an owner of its own is a counted copy, not a + // borrow (spec 23.3); any other is a borrow of the place, as a field read is + if (!def || !ownsHeap(root) || (isHandle(root) && isCountedHandleRead(root))) + { + return nullptr; + } + + if (auto loadOp = mlir::dyn_cast(def); loadOp && isPlace(loadOp.getReference())) + { + // a field of a generator's borrowing box: only its maker fills it, and nothing a call + // can reach overwrites it; it is the caller's, as a parameter is (checkBorrowedReads) + if (isBorrowingBoxField(loadOp.getReference())) + { + return nullptr; + } + + return def; + } + + return borrowingCall(root); + } + + // How a handle read out of a place is used (spec 23.3), seen through casts, merges and locals + // that own nothing. Every use is classified, and nothing falls through (Ruling 9): + // - `counted`: a receiver that takes a count rc made for that very use + // (isCountedHandleReceiver) - a store, a push or an unshift, `new Shared`, an array + // literal's element, a return, an `any` box, or an owning local's initializer; + // - passed on: a cast or a view that is still a handle, a merge, a local that owns nothing - + // whose own uses are classified in turn; + // - `borrowed`: everything else. A use that reads through the place (`f(p, p.child)`, a + // `.value` read, `Shared.count`), and every use that is neither of the above: a tuple's + // element, an object literal's field, a capture, a `yield`, a receiver rc made no retain + // for, any op not named here. The read is then a borrow, and tenure-checked + // (checkPlaceRead). + // rc's retains count for nothing on their own: each is the count of the receiver right after + // it. `cur = cur.value.next` is counted only. + struct HandleReadUses + { + bool counted; + bool borrowed; + }; + + static HandleReadUses handleReadUses(mlir::Value value) + { + HandleReadUses result{false, false}; + llvm::SmallVector values{value}; + llvm::SmallPtrSet seen{value}; + while (!values.empty()) + { + auto current = values.pop_back_val(); + for (auto &use : current.getUses()) + { + auto *user = use.getOwner(); + if (mlir::isa(user)) + { + continue; + } + + // a return and an `any` box take the value, though passesOn would hand them on + if (isHandOff(user, current)) + { + (isCountedHandleReceiver(user, current) ? result.counted : result.borrowed) = true; + continue; + } + + llvm::SmallVector same; + if (passesOn(use, same)) + { + for (auto next : same) + { + // what is no longer a handle is no receiver: a test (`if (p.child)`) only + // reads it + if (!isHandle(next)) + { + result.borrowed = true; + } + else if (seen.insert(next).second) + { + values.push_back(next); + } + } + + continue; + } + + (isCountedHandleReceiver(user, current) ? result.counted : result.borrowed) = true; + } + } + + return result; + } + + // A handle read out of a place that rc gives a count of its own is a counted copy, not a borrow + // (spec 23.3) - but only when every use of it is one rc counts (Ruling 7), and a use that is + // not known to be one is not (Ruling 9). A read with one use that reads through the place as + // well (`f(p, q, q.child = p.child)`), or that keeps it with no count (`[(q.child = p.child), + // 1]`), is a borrow, and that use is tenure-checked; the counted uses are copies + // (checkPlaceRead). + static bool isCountedHandleRead(mlir::Value value) + { + auto uses = handleReadUses(value); + return uses.counted && !uses.borrowed; + } + + // A use that gives a handle an owner rc counts, with rc's retain for that very use: rc's + // retain itself, a store into a field, an element, a global, an owning local or the result + // slot (a return) that rc retained for (countedStoreRetain); a push or an unshift, `new + // Shared`, an array literal's element, an `any` box, a `ts.ReturnVal`, each right after rc's + // retain of what it takes (retainedRightBefore); or the initializer of an owning local, which + // takes a count of its own (`ts.RetainSlot`). Any of them without its retain is not one: it + // keeps the read without a count (`{ s: (q.child = p.child) }`'s field). + static bool isCountedHandleReceiver(mlir::Operation *user, mlir::Value value) + { + if (mlir::isa(user)) + { + return true; + } + + if (auto varOp = mlir::dyn_cast(user)) + { + return isOwningVariable(varOp) && llvm::any_of(varOp->getUsers(), [](mlir::Operation *varUser) { + return mlir::isa(varUser); + }); + } + + if (auto storeOp = mlir::dyn_cast(user)) { return storeOp.getValue() == value && countedStoreRetain(storeOp); } @@ -2763,12 +3507,19 @@ class OwnershipInferencePass return retainOp && retainOp.getReference() == storeOp.getValue() ? retainOp.getOperation() : nullptr; } - // The call a value is the result of, when that result borrows an argument. + // The call a value is the result of, when that result is a plain borrow of an argument. A borrowing + // record (spec 32) is not one: the caller owns the box (checkRecordResults). static mlir::Operation *borrowingCall(mlir::Value value) { auto *def = value.getDefiningOp(); - auto index = def && isCall(def) ? resultBorrows(def) : -1; - return index >= 0 && static_cast(index) < callArgs(def).size() ? def : nullptr; + if (!def || !isCall(def) || resultBorrows(def) < 0) + { + return nullptr; + } + + auto args = callArgs(def); + auto indices = resultBorrowsAll(def); + return llvm::all_of(indices, [&](int index) { return static_cast(index) < args.size(); }) ? def : nullptr; } // The places from a read up to its roots, and the roots: the values the function reached the @@ -2784,9 +3535,11 @@ class OwnershipInferencePass }; // a field's position and reference type; an element is position -1, a handle's payload - // (`s.value`) is SharedValue + // (`s.value`) is SharedValue, a field reached through an interface is InterfaceField: its + // position in the object is not known here (spec 28) llvm::SmallVector> places; static constexpr int64_t SharedValue = -2; + static constexpr int64_t InterfaceField = -3; llvm::SmallVector> roots; // The roots that are handles - what a `s.value` was read through - each also NotOwned // among `roots` (spec 23.3). Known from the op, not the root's type: a view may hide it. @@ -2801,19 +3554,34 @@ class OwnershipInferencePass } }; - Chain chainOf(mlir::Operation *read) + // With `place`, the chain of that place itself: where a store into it moves a value (spec 29). + Chain chainOf(mlir::Operation *read, mlir::Value place = {}) { Chain chain; llvm::SmallVector refs; llvm::SmallVector first; - if (auto loadOp = mlir::dyn_cast(read)) + if (place) + { + refs.push_back(place); + } + else if (auto loadOp = mlir::dyn_cast(read)) { refs.push_back(loadOp.getReference()); } else { + // a call's result that borrows its arguments - a plain borrow, or a borrowing record's box + // (spec 32), which may hold a borrow of each - is somewhere under each of them chain.anyPlace = true; - first.push_back(rootOf(callArgs(read)[resultBorrows(read)])); + auto args = callArgs(read); + for (auto index : resultBorrowsAll(read)) + { + if (static_cast(index) < args.size() && !argumentHoldsNoBlock(args[index]) && + !isImmortalLiteral(args[index])) + { + first.push_back(rootOf(args[index])); + } + } } llvm::SmallPtrSet seen; @@ -2841,6 +3609,11 @@ class OwnershipInferencePass base = sharedValueRefOp.getShared(); handleBases.insert(rootOf(base)); } + else if (auto interfaceRefOp = ref.getDefiningOp()) + { + chain.places.push_back({Chain::InterfaceField, interfaceRefOp.getType()}); + base = interfaceRefOp.getInterfaceVal(); + } else { auto elementRefOp = ref.getDefiningOp(); @@ -2905,7 +3678,15 @@ class OwnershipInferencePass if (auto *call = borrowingCall(base)) { chain.anyPlace = true; - bases.push_back(rootOf(callArgs(call)[resultBorrows(call)])); + auto args = callArgs(call); + for (auto index : resultBorrowsAll(call)) + { + if (static_cast(index) < args.size()) + { + bases.push_back(rootOf(args[index])); + } + } + continue; } @@ -3079,6 +3860,15 @@ class OwnershipInferencePass auto *user = use.getOwner(); if (isCall(user)) { + // ...but a borrowing record given this value holds it (spec 32) + auto args = callArgs(user); + if (isRecordResult(user) && llvm::any_of(resultBorrowsAll(user), [&](int index) { + return static_cast(index) < args.size() && args[index] == value; + })) + { + work.push_back(user->getResult(0)); + } + continue; } @@ -3104,9 +3894,11 @@ class OwnershipInferencePass // anything (`__own_no_drops`, phase 4) - any such call, for a root this function does not // own, since the owner may be a global the callee assigns; else one given something derived // from the root that the read did not produce. - bool dropsChain(mlir::Operation *drop, const Chain &chain, mlir::Operation *read) + // `borrowed`, when given, is the value the borrow holds in place of the read's result (a value + // moved into the place, spec 29), and `read` may then be null. + bool dropsChain(mlir::Operation *drop, const Chain &chain, mlir::Operation *read, mlir::Value borrowed = {}) { - if (drop == read) + if (read && drop == read) { return false; // a call that returns a borrow has not destroyed it by returning it } @@ -3126,6 +3918,15 @@ class OwnershipInferencePass return !chain.owned(); } + // An assignment to a global destroys its old value and all that value owns. A root this + // function does not own - `this`, a parameter, a global's or a field's value - may be + // that very block, or under it (spec 29). A root this function made cannot. An immortal + // global's one store is its initializer, whose old value is the empty one (spec 25). + if (auto addressOfOp = slot.getDefiningOp()) + { + return !addressOfOp->hasAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME) && !chain.owned(); + } + // `x.value = ...` through any handle, of any type, may replace the payload another // handle reads (spec 23.3): a handle converts to one of a base class's type if (slot.getDefiningOp()) @@ -3133,13 +3934,28 @@ class OwnershipInferencePass return llvm::any_of(chain.places, [&](auto &place) { return place.first == Chain::SharedValue; }); } + auto isField = [](int64_t position) { return position >= 0 || position == Chain::InterfaceField; }; int64_t position = -1; if (auto propertyRefOp = slot.getDefiningOp()) { position = propertyRefOp.getPosition(); } + else if (isInterfaceField(slot)) + { + position = Chain::InterfaceField; + } + // A field reached through an interface may be any field of the object under it, of any + // type (a class may declare the field narrower than the interface): an overwrite through + // an interface drops every field on the chain, and any field's overwrite drops a field + // read through an interface (spec 28). return llvm::any_of(chain.places, [&](auto &place) { + if (isField(position) && isField(place.first) && + (position == Chain::InterfaceField || place.first == Chain::InterfaceField)) + { + return true; + } + return place.first == position && place.second == slot.getType(); }); } @@ -3157,7 +3973,7 @@ class OwnershipInferencePass return true; } - auto &fromRead = derivedFrom(read->getResult(0)); + auto &fromRead = derivedFrom(borrowed ? borrowed : read->getResult(0)); auto sources = rootSources(chain); return llvm::any_of(drop->getOperands(), [&](mlir::Value operand) { return !fromRead.contains(operand) && llvm::any_of(sources, [&](mlir::Value source) { @@ -3265,6 +4081,11 @@ class OwnershipInferencePass } } + if (keptParamOutlives(chain, outlives)) + { + return false; + } + for (auto *drop : drops) { if (countedStoreReleases.contains(drop)) @@ -3282,6 +4103,374 @@ class OwnershipInferencePass return true; } + // A local that holds a borrow through `user`, and the field path under it that holds it (empty: + // the local's value itself): a store of it into the local, or into a field of the local's value + // tuple at any depth (an object literal built in place), or the local declared from it. + struct BorrowHolder + { + mlir_ts::VariableOp local; + llvm::SmallVector path; + }; + + static std::optional borrowHolderOf(mlir::Operation *user) + { + BorrowHolder holder; + mlir::Value ref; + if (auto storeOp = mlir::dyn_cast(user)) + { + ref = storeOp.getReference(); + while (auto propertyRefOp = ref.getDefiningOp()) + { + holder.path.insert(holder.path.begin(), propertyRefOp.getPosition()); + ref = propertyRefOp.getObjectRef(); + } + } + else if (auto varOp = mlir::dyn_cast(user)) + { + ref = varOp.getResult(); + } + + auto varOp = ref ? ref.getDefiningOp() : mlir_ts::VariableOp(); + if (!varOp || varOp.getCaptured().value_or(false) || isCellVariable(varOp.getResult())) + { + return std::nullopt; + } + + holder.local = varOp; + return holder; + } + + // Where the holder is given a new value: the local's declaration when it has an initializer + // (`let l!: V` stores nothing, and in a loop the slot keeps the last round's value: fix-up + // re-review N4), and every store into the field path that holds the borrow or into a prefix of + // it (the whole local, `o.p` for `o.p.v`). A store into a sibling field, or below the path, + // gives the path no new value. + static llvm::SmallVector holderRenewals(BorrowHolder holder) + { + llvm::SmallVector renewals; + if (holder.local.getInitializer()) + { + renewals.push_back(holder.local); + } + + llvm::SmallVector> refs{{holder.local.getResult(), 0}}; + while (!refs.empty()) + { + auto [ref, depth] = refs.pop_back_val(); + for (auto *refUser : ref.getUsers()) + { + if (auto propertyRefOp = mlir::dyn_cast(refUser)) + { + if (depth < holder.path.size() && propertyRefOp.getPosition() == holder.path[depth]) + { + refs.push_back({propertyRefOp.getResult(), depth + 1}); + } + } + else if (auto storeOp = mlir::dyn_cast(refUser); storeOp && storeOp.getReference() == ref) + { + renewals.push_back(storeOp); + } + } + } + + return renewals; + } + + // Whether one field path is on the way to the other: a read of either may reach the other's value. + static bool pathsMeet(llvm::ArrayRef a, llvm::ArrayRef b) + { + auto common = std::min(a.size(), b.size()); + return a.take_front(common) == b.take_front(common); + } + + // A read of what a mortal global holds (spec 25: an immortal one's one store is its + // initializer): a heap value it owns, not a handle (spec 23.3 counts those). + bool isGlobalRead(mlir_ts::LoadOp loadOp) + { + auto addressOfOp = loadOp.getReference().getDefiningOp(); + auto result = loadOp.getResult(); + return addressOfOp && !addressOfOp->hasAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME) && ownsHeap(result) && !isHandle(result); + } + + // Such a read is a borrow of the value the global owns - `const c = K.w` names the read itself, + // with no retain - and an assignment of the global releases it. So no use of the read, nor of + // what it produces (walkBorrowed), may run after one in this function. A path back to a use + // through the read itself is a new read. A call that assigns the global is #639's at its + // arguments. (C1 of the fix-up review: `const c = K.w; K.w = new V(i + 1); print(c.x)` in a loop + // read the block the assignment freed.) + // + // Where the walk meets a keep, the read is either followed into what holds it or rejected - + // never let go unchecked (fix-up re-review N2: a kept read is not always another check's). A + // local holds it (borrowHolderOf): an object literal built in place (`const o = { v: c }`, + // `{ p: { v: K.w } }`), a `let` or a destructured local declared or assigned from it + // (`let l = o.v`, `let { v } = o`). Every use of that local under the path that holds the borrow + // - its reads, a copy, a spread (`ts.DeconstructTuple`), what those lead to - is a use of the + // read, made again only where the local is given a new value (holderRenewals): never at the + // read, which renews `c` and not `o.v` - a holder declared before a loop keeps the old borrow + // across the back edge until it is stored again (fix-up re-review N3). The local's own retain + // and releases are its bookkeeping, which the owner checks decide. A sibling field never given + // the borrow is not followed. A value made from the borrow and stored into another local makes + // that one a holder too. Any other keep - a field of an object, an array, a global, a callee + // that keeps it, a capture, a return - cannot be bounded here and is an error. A string read is + // copied where it is kept (spec 22), so its keeps are not this check's. + void checkGlobalRead(mlir_ts::LoadOp read) + { + // each access of a global is a `ts.AddressOf` of its own + auto global = read.getReference().getDefiningOp().getGlobalName(); + llvm::SmallVector uses; + llvm::StringSet<> followed; + mlir::Operation *keptAt = nullptr; + // rc's temporary release of a call's result that borrows an argument goes with that result + // (walkBorrowed's `keeping`); the owner checks decide it + llvm::SmallVector bookkeeping; + Keeping keeping{&bookkeeping, false}; + std::function)> intoLocal; + + // the uses of what `holder` holds, made again at its renewals: those depend on the holder + // alone, not on the way the walk reached it, so a holder is followed once. A value loaded + // out of it is made again only by a path back through that load (the load itself is a use, + // bounded by the renewals): a store into the path after the load gives the loaded value + // nothing new. + auto followHolder = [&](BorrowHolder holder) { + std::string key; + llvm::raw_string_ostream keyOut(key); + keyOut << holder.local.getOperation(); + for (auto position : holder.path) + { + keyOut << "." << position; + } + + if (!followed.insert(keyOut.str()).second) + { + return true; + } + + auto localKills = holderRenewals(holder); + llvm::SmallVector>> refs{{holder.local.getResult(), {}}}; + while (!refs.empty()) + { + auto [ref, path] = refs.pop_back_val(); + for (auto *refUser : ref.getUsers()) + { + if (auto propertyRefOp = mlir::dyn_cast(refUser)) + { + auto fieldPath = path; + fieldPath.push_back(propertyRefOp.getPosition()); + if (pathsMeet(fieldPath, holder.path)) + { + refs.push_back({propertyRefOp.getResult(), fieldPath}); + } + + continue; + } + + if (ref == holder.local.getResult() && + mlir::isa(refUser)) + { + continue; + } + + uses.push_back({refUser, {localKills.begin(), localKills.end()}}); + if (auto loadOp = mlir::dyn_cast(refUser)) + { + if (auto *kept = walkBorrowed(loadOp.getResult(), loadOp.getOperation(), uses, intoLocal, keeping)) + { + keptAt = keptAt ? keptAt : kept; + return false; + } + } + } + } + + return true; + }; + + intoLocal = [&](mlir::Operation *user, llvm::ArrayRef kills) { + // a tuple taken apart (`{ ...b }`, a destructuring): each field is the borrow again + if (mlir::isa(user)) + { + for (auto result : user->getResults()) + { + if (!mayPointInto(result)) + { + continue; + } + + if (auto *kept = walkBorrowed(result, kills, uses, intoLocal, keeping)) + { + keptAt = keptAt ? keptAt : kept; + return false; + } + } + + return true; + } + + auto holder = borrowHolderOf(user); + return holder && followHolder(*holder); + }; + + auto *kept = walkBorrowed(read.getResult(), read.getOperation(), uses, intoLocal, keeping); + + // a value made from the borrow (a field of a tuple read out of a holder, `{ ...b }`) stored + // into another local, or declaring one: that local holds the borrow too + for (size_t index = 0; !kept && !keptAt && index < uses.size(); index++) + { + auto *op = uses[index].op; + auto storeOp = mlir::dyn_cast(op); + auto varOp = mlir::dyn_cast(op); + auto value = storeOp ? storeOp.getValue() : varOp ? varOp.getInitializer() : mlir::Value(); + if (!value || !mayPointInto(value)) + { + continue; + } + + if (auto holder = borrowHolderOf(op)) + { + followHolder(*holder); + } + } + + if (kept || keptAt) + { + if (mlir::isa(read.getType())) + { + return; + } + + if (reporting()) + { + auto *at = keptAt ? keptAt : kept; + at->emitError("the value read from '") + << global << "' is kept where its end at an assignment of '" << global + << "' cannot be checked; -mm=own cannot prove it outlives the global's value"; + signalPassFailure(); + } + + return; + } + + llvm::SmallVector ends; + getFunction().walk([&](mlir::Operation *op) { + mlir::Value slot; + if (auto releaseSlotOp = mlir::dyn_cast(op)) + { + slot = releaseSlotOp.getSlot(); + } + else if (auto storeOp = mlir::dyn_cast(op)) + { + slot = storeOp.getReference(); + } + + auto other = slot ? slot.getDefiningOp() : mlir_ts::AddressOfOp(); + if (other && other.getGlobalName() == global) + { + ends.push_back(op); + } + }); + + for (auto *end : ends) + { + for (auto &use : uses) + { + if (use.op != end && reachableAfter(end, use.op, use.kills)) + { + reportBorrowOutlives(use.op, end, ownerName(read.getResult()), global); + return; + } + } + } + } + + // A borrow read out of a parameter this function keeps, taken before the parameter moves into a + // new owner (spec 33, review fix): from the move on it is bounded by that owner, as a read through + // the owner would be (spec 29) - by the owner's ends and the drops of its place after the move. + // `const inner = p.v; let r = p; r = new N(...); inner.x` reads what the assignment freed. A move + // into anything that is not such a place (a call that keeps it, a return, a push) ends the borrow + // there. So do the releases of the parameter's slot (spec 33's exit releases). True when some use + // of the borrow comes after one of these (`outlives` reported it). + bool keptParamOutlives(const Chain &chain, llvm::function_ref outlives) + { + for (auto &root : chain.roots) + { + auto load = keptParamLoadOf(root.first); + if (!load) + { + continue; + } + + // the moves of the parameter: what takes a read of its slot + llvm::SmallVector> takers; + for (auto *user : load.getReference().getUsers()) + { + if (mlir::isa(user)) + { + if (outlives(user)) + { + return true; + } + + continue; + } + + auto readOp = mlir::dyn_cast(user); + if (!readOp) + { + continue; + } + + forEachUse(readOp.getResult(), [&](mlir::Operation *use, mlir::Value used) { + auto varOp = mlir::dyn_cast(use); + if (mlir::isa(use)) + { + auto *taker = takerOf(use, used); + takers.push_back({taker ? taker : use, used}); + } + else if ((varOp && isOwningVariable(varOp)) || (isCall(use) && isKeptArgument(use, used))) + { + takers.push_back({use, used}); + } + }); + } + + for (auto [taker, moved] : takers) + { + Chain owner; + if (!destinationChain(taker, owner)) + { + if (outlives(taker)) + { + return true; + } + + continue; + } + + auto afterMove = [&](mlir::Operation *end) { + return end != taker && reachableAfter(taker, end, llvm::ArrayRef{}) && outlives(end); + }; + + for (auto *end : rootEnds(owner)) + { + if (afterMove(end)) + { + return true; + } + } + + for (auto *drop : drops) + { + if (dropsChain(drop, owner, nullptr, moved) && afterMove(drop)) + { + return true; + } + } + } + } + + return false; + } + // A parameter, or a view of one (`u`, the payload `ts.Unbox` reads out of an `any`), that // this function returns as a borrow (`__own_result_borrows`). rc retained it for the caller, // who now takes no reference: the retain goes, and nothing may keep it. The caller's own @@ -3335,6 +4524,16 @@ class OwnershipInferencePass { callee = refOp.getIdentifierAttr().getAttr(); } + else if (auto refOp = mlir::dyn_cast_or_null(calleeOp)) + { + // a method reached through an interface (spec 31): `.` + auto method = mlir::dyn_cast(refOp.getIdentifier()); + auto interfaceType = mlir::dyn_cast(refOp.getInterfaceVal().getType()); + if (method && interfaceType) + { + return (interfaceType.getName().getValue() + "." + method.getValue()).str(); + } + } } return callee ? callee.getValue().str() : std::string(); @@ -3378,6 +4577,17 @@ class OwnershipInferencePass base = sharedValueRefOp.getShared(); step = ".value"; } + else if (auto interfaceRefOp = ref.getDefiningOp()) + { + auto field = mlir::dyn_cast(interfaceRefOp.getIdentifier()); + if (!field) + { + return {}; + } + + base = interfaceRefOp.getInterfaceVal(); + step = "." + field.getValue().str(); + } else { return {}; @@ -3548,65 +4758,40 @@ class OwnershipInferencePass { auto loadOp = value.getDefiningOp(); auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); - if (!varOp || !isConstantRecordLocal(varOp)) + // a borrowing record (spec 32) takes nothing for what it borrows, and its fresh values move in + // as an owning record's do + if (!varOp || !isConstantRecordLocal(varOp) || !(isOwningRecord(value) || borrowingRecordOf(varOp))) { return false; } - for (auto *user : varOp->getUsers()) - { + // a fresh value moved in, its move decided by its own check against this retain + auto movesIn = llvm::any_of(varOp->getUsers(), [&](mlir::Operation *user) { auto propertyRefOp = mlir::dyn_cast(user); if (!propertyRefOp) { - continue; + return false; } // a method's field is a `!ts.bound_ref`, which holds a function, not a block - auto field = propertyRefOp.getResult(); - auto refType = mlir::dyn_cast(field.getType()); - for (auto *fieldUser : field.getUsers()) + auto refType = mlir::dyn_cast(propertyRefOp.getType()); + if (refType && !ownsHeap(propertyRefOp.getLoc(), refType.getElementType())) { - if (mlir::isa(fieldUser)) - { - continue; - } - - if (refType && !ownsHeap(field.getLoc(), refType.getElementType())) - { - continue; - } - - // a fresh value moved in, its move decided by its own check against this retain - auto storeOp = mlir::dyn_cast(fieldUser); - if (!storeOp || storeOp.getReference() != field || !isFresh(rootOf(storeOp.getValue())) || - recordRetainOf(varOp) == nullptr) - { - return false; - } + return false; } - } - - return true; - } - - // A non-owning local a `ts.Constant` initializes, used only through its fields and its reads. - static bool isConstantRecordLocal(mlir_ts::VariableOp varOp) - { - if (isOwningVariable(varOp) || varOp.getCaptured().value_or(false) || !varOp.getInitializer() || - !varOp.getInitializer().getDefiningOp()) - { - return false; - } - return llvm::all_of(varOp->getUsers(), [](mlir::Operation *user) { - return mlir::isa(user); + return llvm::any_of(propertyRefOp->getUsers(), + [](mlir::Operation *fieldUser) { return mlir::isa(fieldUser); }); }); + + return !movesIn || recordRetainOf(varOp) != nullptr; } // rc's one `ts.Retain` of a record local's one read, where the read is only returned: stored - // into a local that owns nothing and is read only by returns (the result slot). It is the - // reference rc gives each fresh value stored into the record, so it is where they move in. - // Null for any other record. + // into a local that owns nothing and is read only by returns (the result slot), or copied into + // the box it is given in as an object or an interface (spec 27). It is the reference rc gives + // each fresh value stored into the record, so it is where they move in. Null for any other + // record. static mlir::Operation *recordRetainOf(mlir_ts::VariableOp varOp) { if (!isConstantRecordLocal(varOp)) @@ -3634,6 +4819,8 @@ class OwnershipInferencePass } mlir::Operation *retain = nullptr; + auto boxed = false; + auto other = false; for (auto *user : read->getUsers()) { if (mlir::isa(user)) @@ -3647,6 +4834,23 @@ class OwnershipInferencePass continue; } + // copied into the block it is given in as an object or an interface (spec 27): the box + // owns what the record held, and its own check decides where the box goes + auto storeOp = mlir::dyn_cast(user); + auto box = storeOp && storeOp.getValue() == read.getResult() ? recordBoxOf(storeOp.getReference()) + : mlir_ts::NewOp(); + if (box && box.getResult() == storeOp.getReference()) + { + if (boxed) + { + return nullptr; + } + + boxed = true; + continue; + } + + other = true; if (mlir::isa(user)) { continue; @@ -3669,7 +4873,6 @@ class OwnershipInferencePass } // stored into the result slot, or into an owning local (`a = { item: new C() }`) - auto storeOp = mlir::dyn_cast(user); auto slot = storeOp && storeOp.getValue() == read.getResult() ? storeOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); @@ -3679,6 +4882,12 @@ class OwnershipInferencePass } } + // a boxed record goes nowhere else: the box is its one owner + if (boxed && other) + { + return nullptr; + } + return retain; } @@ -3836,9 +5045,15 @@ class OwnershipInferencePass return retain; } - // A string constant cast to `string` is the global itself, which a release skips. + // A string constant cast to `string` is the global itself, which a release skips; so is a read + // of an immortal global (spec 25). static bool isImmortalLiteral(mlir::Value value) { + if (isImmortalGlobalRead(value)) + { + return true; + } + auto castOp = rootOf(value).getDefiningOp(); return castOp && mlir::isa(castOp.getType()) && castOp.getIn().getDefiningOp(); @@ -4089,6 +5304,7 @@ class OwnershipInferencePass llvm::SmallVector takers; llvm::SmallVector releases; llvm::SmallVector reads; + llvm::DenseMap readOf; forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { if (mlir::isa(user)) { @@ -4102,6 +5318,7 @@ class OwnershipInferencePass else if (isBorrow(user, used)) { reads.push_back(user); + readOf.try_emplace(user, used); } else { @@ -4120,10 +5337,30 @@ class OwnershipInferencePass return false; } + llvm::SmallVector made; + if (definition) + { + made.push_back(definition); + } + + // the call that keeps it, given it again through another operand + if (auto *again = callGivenAgain(taker, value, {value})) + { + reportUseAfterMove(again, taker, name); + return false; + } + for (auto *use : llvm::concat(takers, reads)) { if (use != taker && reachableAfter(taker, use, definition)) { + // a read through the new owner, while it holds the value (spec 29) + if (auto found = readOf.find(use); + found != readOf.end() && readsThroughOwner(taker, found->second, use, made)) + { + continue; + } + reportUseAfterMove(use, taker, name); return false; } @@ -4162,6 +5399,50 @@ class OwnershipInferencePass return true; } + // A call that keeps an argument and is given the same value again through another operand - the + // value itself, a view of it, something read out of it, another read of its slot (`f(n, n)`, + // `f(h, n, n.v)` with only the first `n` kept): the callee takes the value while it borrows it + // through the other operand, and may free it under that borrow, so the other operand is a use + // after the move (spec 33, review). `sources` are what the moved value is: the value, or every + // read of the owning slot it came from. The call itself, or null. + mlir::Operation *callGivenAgain(mlir::Operation *taker, mlir::Value moved, llvm::ArrayRef sources) + { + if (!isCall(taker) || ownedParams(taker).empty()) + { + return nullptr; + } + + auto args = callArgs(taker); + auto kept = ownedParams(taker); + auto movedRoot = rootOf(moved); + // one kept position is the move itself; the value at a second one (`f(n, n)` with both + // kept) is a second owner, a use after the first move like any other operand + auto skipped = false; + for (auto &operand : taker->getOpOperands()) + { + // the moved argument itself, where the callee keeps it + auto number = operand.getOperandNumber(); + if (!skipped && !args.empty() && number >= args.getBeginOperandIndex() && + number < args.getBeginOperandIndex() + args.size() && + llvm::is_contained(kept, static_cast(number - args.getBeginOperandIndex())) && + rootOf(operand.get()) == movedRoot) + { + skipped = true; + continue; + } + + for (auto source : sources) + { + if (derivedFrom(source).contains(operand.get())) + { + return taker; + } + } + } + + return nullptr; + } + // Does control come back round to `taker` without making the value again? Only then is it a // move on every iteration of a loop. bool takerLoops(mlir::Operation *taker, mlir::Operation *definition) @@ -4207,14 +5488,26 @@ class OwnershipInferencePass // path through it is a new value, not this one. Straight-line order within a block, then the // CFG from the block's successors; a path back into `from`'s own block reaches every op in it. // - // Ops nested in a region that is not the function body (an `async.execute` body, a lambda) - // are compared by their ancestors in the body. That answers "reachable" whenever the two - // share an ancestor, which can only turn a program into an error. + // Ops nested in a region that is not the function body are compared by their ancestors in the + // body. That answers "reachable" whenever the two share an ancestor, which can only turn a + // program into an error. (By this pass async bodies and lambdas are functions of their own; + // after the affine lowering no op of a body holds a region, so the case is kept for safety.) bool reachableAfter(mlir::Operation *from, mlir::Operation *to, mlir::Operation *kill) { return reachableAfter(from, to, kill ? llvm::ArrayRef(kill) : llvm::ArrayRef()); } + // Whether reachableAfter can order `from` and `to` at all: both inside the function body and not + // one op of it (nested in the same statement). A check whose safe answer is "not reachable" must + // refuse what it cannot order, where reachableAfter would answer "reachable". + bool orderedInBody(mlir::Operation *from, mlir::Operation *to) + { + auto &body = getFunction().getFunctionBody(); + from = body.findAncestorOpInRegion(*from); + to = body.findAncestorOpInRegion(*to); + return from && to && from != to; + } + // The same, where a path through any of `kills` is a new value. A use that is itself a kill // runs after the value is made again. bool reachableAfter(mlir::Operation *from, mlir::Operation *to, llvm::ArrayRef kills) @@ -4389,7 +5682,8 @@ class OwnershipInferencePass // A move out of an owning local: `let b = a`, `h.c = a`, `return a`. rc read the slot and // retained what it read for the receiver; the move erases that retain and every release of - // the slot the move reaches, which must be dominated by it. Any other use of the slot the + // the slot the move reaches, which must be dominated by it - or, with `placeReleases`, is + // placed on the paths that did not move it (spec 30). Any other use of the slot the // move reaches - a read, an assignment's own read of it - is a use after the move. Reports and // returns false when it is not a move. // @@ -4398,7 +5692,7 @@ class OwnershipInferencePass // with the move - what the slot held is the receiver's - and the releases only a path through // an assignment reaches stay, to destroy the new value. bool checkSlotMove(mlir::Operation *retain, mlir::Value value, mlir_ts::LoadOp load, - llvm::SetVector &toErase) + llvm::SetVector &toErase, bool placeReleases = false) { auto slot = load.getReference(); auto varOp = slot.getDefiningOp(); @@ -4415,81 +5709,654 @@ class OwnershipInferencePass llvm::SmallVector kills(assignments.begin(), assignments.end()); if (varOp) { - kills.push_back(varOp); + kills.push_back(varOp); + } + + // the move's own read, met again around a loop the slot was declared outside of + if (reachableAfter(taker, load, kills)) + { + reportMovedInLoop(taker, name); + return false; + } + + // the slot given another value on some path between the move's read and its taker - inside + // the call's own argument list, `f(n, (n = mk(7), 0))`, on one arm of it, `f(n, c ? (n = + // mk(7), 0) : 0)`: the assignment's release frees what the read holds before the callee takes + // it, so the assignment is a use of the slot after the move. The taker may be an assignment + // of the slot itself (`n = n`): it is the destination, not between. A read and a taker + // reachableAfter cannot order (one statement kept as a region, or outside the body) refuse. + { + llvm::SmallVector readAgain{load.getOperation()}; + if (varOp) + { + readAgain.push_back(varOp); + } + + for (auto *assignment : assignments) + { + if (assignment != taker && reachableAfter(load, assignment, varOp) && + reachableAfter(assignment, taker, readAgain)) + { + reportUseAfterMove(assignment, taker, name); + return false; + } + } + + llvm::SmallVector killsBetween(kills); + llvm::erase(killsBetween, taker); + if (!orderedInBody(load, taker) || !reachableAfter(load, taker, killsBetween)) + { + reportUseAfterMove(taker, taker, name); + return false; + } + } + + // the call that keeps it, given the slot's value again through another operand: a read of + // the slot that reaches the call without crossing an assignment of it (not one made before + // an assignment gave the slot its current value, `const arr = [n]; n = mk(5); f(n, arr)`); + // since the move's own read reaches the call so too, such a read holds the moved value + llvm::SmallVector slotReads; + for (auto *user : slot.getUsers()) + { + if (auto readOp = mlir::dyn_cast(user); readOp && reachableAfter(readOp, taker, kills)) + { + slotReads.push_back(readOp.getResult()); + } + } + + if (auto *again = callGivenAgain(taker, value, slotReads)) + { + reportUseAfterMove(again, taker, name); + return false; + } + + // Every read of the slot is a use of `a`, and so is every use of what a read returned: a + // `const b = a` is folded into its load, so `b.x` after the move reads the moved value + // through a load that came before it. A read through the new owner, while it holds the + // value, is not such a use (spec 29). + auto throughOwner = [&](mlir::Operation *user, mlir::Value used) { + return readsThroughOwner(taker, used, user, kills); + }; + + for (auto *use : slot.getUsers()) + { + if (mlir::isa(use)) + { + continue; + } + + if (use != load.getOperation() && reachableAfter(taker, use, kills)) + { + auto readOp = mlir::dyn_cast(use); + if (!readOp || !onlyReadThroughOwner(readOp.getResult(), throughOwner)) + { + reportUseAfterMove(use, taker, name); + return false; + } + + continue; + } + + if (auto readOp = mlir::dyn_cast(use)) + { + if (auto *late = readUsedAfter(readOp, retain, taker, varOp, throughOwner)) + { + reportUseAfterMove(late, taker, name); + return false; + } + } + } + + auto *acquire = mlir::isa(retain) ? taker : retain; + llvm::SmallVector moved; + for (auto *use : slot.getUsers()) + { + if (!mlir::isa(use) || !reachableAfter(acquire, use, kills)) + { + continue; + } + + // a release that one path reaches holding the moved value and another holding a new + // one an assignment after the move stored: there is no single answer for it + auto someMoved = !dominance->properlyDominates(acquire, use); + for (auto *assignment : assignments) + { + llvm::SmallVector movedAgain{load.getOperation()}; + if (varOp) + { + movedAgain.push_back(varOp); + } + + someMoved = someMoved || + (reachableAfter(acquire, assignment, varOp) && reachableAfter(assignment, use, movedAgain)); + } + + if (someMoved) + { + // ...but the paths that did not move it can release it before they join the + // others (spec 30) + if (placeReleases && placeOnUnmovedEdges(varOp, acquire, retain, taker, assignments, toErase)) + { + return true; + } + + reportMovedOnSomePaths(taker, use, name, placeReleases ? joinedUse : nullptr); + return false; + } + + moved.push_back(use); + } + + toErase.insert(moved.begin(), moved.end()); + return true; + } + + // ---- A move on some paths only (spec 30) ---- + // + // `if (c) { best = v; }`: after the paths join, `v` holds its value on one and a moved one on + // the other, and its one release there has no single answer. No run-time record of the move is + // kept (spec 2.2); instead, where nothing uses the value after the join, the release is placed + // on each edge into the join that did not move it - freeing the value earlier, where it is + // already dead - and the releases after the join go. Each path frees the value once: through + // its new owner where it moved, through the edge where it did not. + + // What the slot of an owning local holds at a point: nothing yet or any more (declared later, + // released), its value, or a value moved out of it. A point some paths reach holding the value + // and others having moved it is where the paths join. + enum SlotState : unsigned + { + SlotDead = 1, + SlotLive = 2, + SlotMoved = 4 + }; + + static bool isJoined(unsigned state) + { + return (state & SlotLive) && (state & SlotMoved); + } + + // A value of this type can point into a block: anything but a number, a boolean, a literal or + // an empty value. + static bool carriesPointer(mlir::Type type) + { + return !mlir::isa(type); + } + + // A release on an edge into the join that did not move the local's value (spec 30), put in after + // the analysis. + struct EdgeRelease + { + mlir::Value slot; + mlir::Block *from; + unsigned successor; + mlir::Location loc; + }; + + llvm::SmallVector edgeReleases; + + // The use that kept placeOnUnmovedEdges from placing the releases, for the error's note. + mlir::Operation *joinedUse = nullptr; + + // Can control reach `to` from the start of `start`, without passing `kills` on the way? + bool reachableFrom(mlir::Block *start, mlir::Operation *to, llvm::ArrayRef kills) + { + auto &body = getFunction().getFunctionBody(); + to = body.findAncestorOpInRegion(*to); + if (!to) + { + return true; + } + + llvm::SmallVector killAt; + for (auto *kill : kills) + { + if (auto *top = body.findAncestorOpInRegion(*kill)) + { + killAt.push_back(top); + } + } + + llvm::SmallPtrSet seen; + llvm::SmallVector work{start}; + while (!work.empty()) + { + auto *block = work.pop_back_val(); + if (!seen.insert(block).second) + { + continue; + } + + auto killed = [&](mlir::Operation *before) { + return llvm::any_of(killAt, [&](mlir::Operation *kill) { + return kill->getBlock() == block && (!before || kill->isBeforeInBlock(before) || kill == before); + }); + }; + + if (to->getBlock() == block && !killed(to)) + { + return true; + } + + if (!killed(nullptr)) + { + work.append(block->getSuccessors().begin(), block->getSuccessors().end()); + } + } + + return false; + } + + // The blocks of an exception handler: from a landing pad to the end of its catch. A release there + // would be a call inside an exception funclet. + llvm::SmallPtrSet handlerBlocks() + { + llvm::SmallPtrSet handler; + llvm::SmallVector work; + for (auto &block : getFunction().getFunctionBody()) + { + if (!block.empty() && mlir::isa(block.front())) + { + work.push_back(&block); + } + } + + while (!work.empty()) + { + auto *block = work.pop_back_val(); + if (!handler.insert(block).second) + { + continue; + } + + auto ends = llvm::any_of(*block, [](mlir::Operation &op) { + return mlir::isa(op); + }); + if (!ends) + { + work.append(block->getSuccessors().begin(), block->getSuccessors().end()); + } + } + + return handler; + } + + // The move at `acquire` out of the owning local `varOp` reaches a release that other paths reach + // holding the value. Places a release of the slot on every edge into the join that holds the value + // and has not moved it, and erases every release a moved value reaches - when nothing uses the + // value, through the slot or through anything read out of it, where the paths have joined. False, + // with nothing placed or erased, when it cannot. + bool placeOnUnmovedEdges(mlir_ts::VariableOp varOp, mlir::Operation *acquire, mlir::Operation *retain, + mlir::Operation *taker, llvm::ArrayRef assignments, + llvm::SetVector &toErase) + { + joinedUse = nullptr; + auto &body = getFunction().getFunctionBody(); + if (!varOp || varOp.getCaptured().value_or(false) || varOp->getParentRegion() != &body || + acquire->getParentRegion() != &body || + llvm::any_of(assignments, [&](mlir::Operation *op) { return op->getParentRegion() != &body; })) + { + return false; + } + + auto slot = varOp.getResult(); + llvm::SmallVector releases; + for (auto *user : slot.getUsers()) + { + if (mlir::isa(user)) + { + if (user->getParentRegion() != &body) + { + return false; + } + + releases.push_back(user); + } + } + + // the slot's state through the function, forward from its entry + auto step = [&](mlir::Operation *op, unsigned state) -> unsigned { + if (op == varOp.getOperation() || llvm::is_contained(assignments, op)) + { + return SlotLive; + } + + if (op == acquire) + { + return SlotMoved; + } + + if (llvm::is_contained(releases, op)) + { + return SlotDead; + } + + return state; + }; + + llvm::DenseMap entry; + auto exitOf = [&](mlir::Block *block) { + auto state = entry.lookup(block); + for (auto &op : *block) + { + state = step(&op, state); + } + + return state; + }; + + entry[&body.front()] = SlotDead; + llvm::SmallVector work{&body.front()}; + while (!work.empty()) + { + auto *block = work.pop_back_val(); + auto state = exitOf(block); + for (auto *successor : block->getSuccessors()) + { + auto &into = entry[successor]; + if ((into | state) != into) + { + into |= state; + work.push_back(successor); + } + } } - // the move's own read, met again around a loop the slot was declared outside of - if (reachableAfter(taker, load, kills)) + // ...and before each op of the body + llvm::DenseMap before; + for (auto &block : body) { - reportMovedInLoop(taker, name); - return false; + auto state = entry.lookup(&block); + for (auto &op : block) + { + before[&op] = state; + state = step(&op, state); + } } - // Every read of the slot is a use of `a`, and so is every use of what a read returned: a - // `const b = a` is folded into its load, so `b.x` after the move reads the moved value - // through a load that came before it. - for (auto *use : slot.getUsers()) + auto joinedAt = [&](mlir::Operation *user) { + auto *top = body.findAncestorOpInRegion(*user); + return !top || isJoined(before.lookup(top)); + }; + + // every release a moved value reaches goes; one some path reaches with nothing in the slot + // is not this rule's + llvm::SmallVector erased; + for (auto *release : releases) { - if (mlir::isa(use)) + auto state = before.lookup(release); + if (state & SlotMoved) { - continue; + if (state & SlotDead) + { + return false; + } + + erased.push_back(release); } + } - if (use != load.getOperation() && reachableAfter(taker, use, kills)) + // the edges into the join that hold the value and did not move it + auto handler = handlerBlocks(); + llvm::SmallVector placed; + llvm::SetVector releasedInto; + for (auto &block : body) + { + if (!entry.count(&block)) { - reportUseAfterMove(use, taker, name); - return false; + continue; // unreachable } - if (auto readOp = mlir::dyn_cast(use)) + auto state = exitOf(&block); + for (auto index : llvm::seq(0, block.getNumSuccessors())) { - if (auto *late = readUsedAfter(readOp, retain, taker, varOp)) + auto into = entry.lookup(block.getSuccessor(index)); + if (!isJoined(into)) + { + continue; + } + + // a join some path reaches with nothing in the slot is not this rule's + if (into & SlotDead) + { + return false; + } + + // an edge that moved it, or from where the paths have joined already + if (state != SlotLive) + { + continue; + } + + auto *terminator = block.getTerminator(); + if (!mlir::isa(terminator) || handler.contains(&block) || + !dominance->dominates(varOp.getOperation(), terminator)) { - reportUseAfterMove(late, taker, name); return false; } + + placed.push_back({slot, &block, static_cast(index), terminator->getLoc()}); + releasedInto.insert(block.getSuccessor(index)); } } - auto *acquire = mlir::isa(retain) ? taker : retain; - llvm::SmallVector moved; - for (auto *use : slot.getUsers()) + if (placed.empty()) { - if (!mlir::isa(use) || !reachableAfter(acquire, use, kills)) + return false; + } + + // Nothing uses the value where the paths have joined - a read there, on the path that did + // not move it, needs what the edge released - neither through the slot nor through anything + // read out of it: a field, a view, a borrowing local, a closure. The move itself hands the + // value to its new owner, whose reads are its own. A value the slot is given - its + // initializer, an assignment's - is the slot's only once it is stored: a use before that (a + // constructor's, or one around a loop before it is made again) is not a use of it. + // + // Past the joined points too: what was read out of the value before an edge released it + // may not be used after that edge - after a new value is stored into the slot, or after its + // scope ends - unless it was read out again (spec 30). A read of the slot after a new value + // is stored reads that one. + struct Origin + { + mlir::Operation *stored; + mlir::Operation *made; + }; + + llvm::SmallVector origins{{nullptr, nullptr}}; + llvm::SmallVector> values; + llvm::DenseSet> seen; + auto taint = [&](mlir::Value value, unsigned origin) { + if (carriesPointer(value.getType()) && seen.insert({value, origin}).second) { - continue; + values.push_back({value, origin}); } + }; + + auto given = [&](mlir::Value value, mlir::Operation *stored) { + origins.push_back({stored, value.getDefiningOp()}); + taint(value, origins.size() - 1); + }; + + taint(slot, 0); + if (varOp.getInitializer()) + { + given(varOp.getInitializer(), varOp); + } + + for (auto *assignment : assignments) + { + given(mlir::cast(assignment).getValue(), assignment); + } - if (!dominance->properlyDominates(acquire, use)) + // reached from an edge that released the value, before the read it came from (or the op + // that made it) runs again + auto usedAfterRelease = [&](mlir::Operation *made, mlir::Operation *user) { + llvm::SmallVector madeAgain; + if (made) { - reportMovedOnSomePaths(taker, use, name); - return false; + madeAgain.push_back(made); } - // a release that one path reaches holding the moved value and another holding a new - // one an assignment after the move stored: there is no single answer for it - for (auto *assignment : assignments) + return llvm::any_of(releasedInto, [&](mlir::Block *join) { return reachableFrom(join, user, madeAgain); }); + }; + + auto heldAt = [&](unsigned origin, mlir::Operation *user) { + auto &from = origins[origin]; + if (!from.stored) { - llvm::SmallVector movedAgain{load.getOperation()}; - if (varOp) + return true; + } + + llvm::SmallVector madeAgain; + if (from.made) + { + madeAgain.push_back(from.made); + } + + return reachableAfter(from.stored, user, madeAgain); + }; + + while (!values.empty()) + { + auto [value, origin] = values.pop_back_val(); + for (auto &use : value.getUses()) + { + auto *user = use.getOwner(); + if (mlir::isa(user) || user == retain) { - movedAgain.push_back(varOp); + continue; + } + + // the slot's own releases and assignments are decided above + if (value == slot && (mlir::isa(user) || llvm::is_contained(assignments, user))) + { + continue; } - if (reachableAfter(acquire, assignment, varOp) && reachableAfter(assignment, use, movedAgain)) + if (joinedAt(user) && heldAt(origin, user)) { - reportMovedOnSomePaths(taker, use, name); + joinedUse = user; return false; } - } - moved.push_back(use); + if (value != slot && usedAfterRelease(origins[origin].made, user)) + { + joinedUse = user; + return false; + } + + if (user == taker) + { + continue; + } + + // a store of it into a local makes every read of that local one; one into a place + // is not followed, and the release is not placed + if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getValue() == value) + { + auto into = storeOp.getReference().getDefiningOp(); + if (!into || into.getCaptured().value_or(false)) + { + return false; + } + + taint(into.getResult(), origin); + continue; + } + + if (auto branch = mlir::dyn_cast(user)) + { + for (auto index : llvm::seq(0, user->getNumSuccessors())) + { + auto operands = branch.getSuccessorOperands(index); + for (auto [position, operand] : llvm::enumerate(operands.getForwardedOperands())) + { + if (operand == value) + { + taint(user->getSuccessor(index)->getArgument(operands.getProducedOperandCount() + position), + origin); + } + } + } + } + + // a call's result is its own block, unless its facts say it borrows this argument - a + // plain borrow, or a borrowing record that holds it (spec 32) + auto borrows = true; + if (isCall(user)) + { + auto args = callArgs(user); + borrows = llvm::any_of(resultBorrowsAll(user), [&](int index) { + return static_cast(index) < args.size() && args[index] == value; + }); + } + + // a read of the slot is what its uses came from + auto from = origin; + if (value == slot && user->getNumResults() > 0) + { + origins.push_back({nullptr, user}); + from = origins.size() - 1; + } + + for (auto result : user->getResults()) + { + if (borrows) + { + taint(result, from); + } + } + + // inside a region - a closure, an async body - it is the op that holds the region + // that hands it on + if (user->getParentRegion() != &body) + { + for (auto result : body.findAncestorOpInRegion(*user)->getResults()) + { + taint(result, origin); + } + } + } } - toErase.insert(moved.begin(), moved.end()); + toErase.insert(erased.begin(), erased.end()); + edgeReleases.append(placed.begin(), placed.end()); return true; } + // Puts in the releases placeOnUnmovedEdges decided: at the end of a block that goes only to the + // join, else in a block of its own on the edge. + void placeEdgeReleases() + { + for (auto &edge : edgeReleases) + { + auto *terminator = edge.from->getTerminator(); + mlir::OpBuilder builder(terminator); + if (terminator->getNumSuccessors() == 1) + { + builder.create(edge.loc, edge.slot); + continue; + } + + auto *join = terminator->getSuccessor(edge.successor); + auto operands = mlir::cast(terminator).getSuccessorOperands(edge.successor); + llvm::SmallVector forwarded(operands.getForwardedOperands()); + auto *split = new mlir::Block(); + split->insertAfter(edge.from); + builder.setInsertionPointToEnd(split); + builder.create(edge.loc, edge.slot); + builder.create(edge.loc, join, forwarded); + if (!forwarded.empty()) + { + operands.erase(0, forwarded.size()); + } + + terminator->setSuccessor(split, edge.successor); + } + + edgeReleases.clear(); + } + // The stores of the assignments to an owning local: rc releases what the slot holds right // before it stores the new value (an assignment, and a `var` declaration met again). static llvm::SmallVector slotAssignments(mlir::Value slot) @@ -4513,8 +6380,10 @@ class OwnershipInferencePass // reach, other than the move's own retain and taker. None if there is none. A path back // through `read` itself reaches a use of a value read again: the read, met after the move, // is the slot's own use to check (around a loop, of a value an assignment stored). + // `isRead` accepts a use that reads the value through its new owner (spec 29). mlir::Operation *readUsedAfter(mlir_ts::LoadOp read, mlir::Operation *retain, mlir::Operation *taker, - mlir::Operation *kill) + mlir::Operation *kill, + llvm::function_ref isRead = nullptr) { llvm::SmallVector values{read.getResult()}; while (!values.empty()) @@ -4539,7 +6408,7 @@ class OwnershipInferencePass kills.push_back(kill); } - if (reachableAfter(taker, user, kills)) + if (reachableAfter(taker, user, kills) && !(isRead && isRead(user, current))) { return user; } @@ -4549,6 +6418,192 @@ class OwnershipInferencePass return nullptr; } + // ---- Reads after a move, through the new owner (spec 29) ---- + // + // `h.c = c; print(c.x)`, `best = v; s += v.x`: after the move the value is the new owner's, and + // a read of it through the old name is a borrow of the place it moved into, as a read of + // `h.c` would be. It is bounded the way checkPlaceRead bounds such a read - by the ends of the + // place's roots and by the drops that may destroy what the place holds (an overwrite, through an + // interface view too; a call whose facts may drop; a call given a closure) - on a path from the + // move to the read on which the value is not made again or moved again. + + // Every use of `value`, through its views, is a read `isRead` accepts. A retain - rc's reference + // for an owner - is not. + static bool onlyReadThroughOwner(mlir::Value value, llvm::function_ref isRead) + { + auto all = true; + forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { + all = all && (mlir::isa(user) || + (!mlir::isa(user) && isRead(user, used))); + }); + + return all; + } + + // A use that only reads the value: a field, element or length read through it, a method reached + // through it, a comparison or arithmetic, a print, an assert, a test, or an argument the callee + // borrows. Anything else - a store, an owning `let`, a keeping call, a return - would be a second + // owner. + static bool isReadAfterMove(mlir::Operation *user, mlir::Value value) + { + if (mlir::isa(user)) + { + return isBorrow(user, value); + } + + return isCall(user) && !isKeptArgument(user, value); + } + + // The place a taker moves its value into, as a chain checkPlaceRead would read it: an owning + // local (a declaration, or an assignment), or a field or element - of a local or a parameter + // (`this`). False for anything else: a call that keeps it, a push, a return, a global, a + // captured variable, a handle's payload. + bool destinationChain(mlir::Operation *taker, Chain &chain) + { + auto ownSlot = [](mlir_ts::VariableOp varOp) { + return varOp && isOwningVariable(varOp) && !varOp.getCaptured().value_or(false); + }; + + if (auto varOp = mlir::dyn_cast(taker)) + { + if (!ownSlot(varOp)) + { + return false; + } + + chain.roots.push_back({varOp.getResult(), Chain::Slot}); + return true; + } + + auto storeOp = mlir::dyn_cast(taker); + if (!storeOp) + { + return false; + } + + auto ref = storeOp.getReference(); + if (auto varOp = ref.getDefiningOp()) + { + if (!ownSlot(varOp)) + { + return false; + } + + chain.roots.push_back({ref, Chain::Slot}); + return true; + } + + if (!isPlace(ref)) + { + return false; + } + + chain = chainOf(nullptr, ref); + if (!chain.handles.empty() || chain.roots.empty()) + { + return false; + } + + // a global's field, or a captured variable's: not a place of this function + return llvm::none_of(chain.roots, [](auto &root) { + auto loadOp = root.first.template getDefiningOp(); + return loadOp && (loadOp.getReference().template getDefiningOp() || + isLoadedCell(loadOp.getReference())); + }); + } + + // Is `use`, a use of `value` after `taker` moved it, a read through the place it moved into + // (spec 29)? `made` is where the value is made again (its definition, the local's declaration + // and assignments). Sound when no path from the move through an end or a drop of the place to + // the read - or to anything the read produced - makes the value again or moves it again: such a + // path is the only way the place can stop holding the value before the read. + bool readsThroughOwner(mlir::Operation *taker, mlir::Value value, mlir::Operation *use, + llvm::ArrayRef made) + { + if (!isReadAfterMove(use, value)) + { + return false; + } + + Chain chain; + if (!destinationChain(taker, chain)) + { + return false; + } + + // A read again of the old name after a drop still finds the moved value - the slot is not + // emptied by a move - so neither the read nor the slot's read is where it is made again. + llvm::SmallVector kills(made.begin(), made.end()); + kills.push_back(taker); + llvm::SmallVector uses; + uses.push_back({use, {kills.begin(), kills.end()}}); + for (auto result : use->getResults()) + { + if (!mayPointInto(result)) + { + continue; + } + + // what the read produced - a field's reference, a bound method - still points into the + // block; a call's result that borrows its argument is the value again and may not be kept + if (walkBorrowed(result, kills, uses, nullptr, Keeping{nullptr, false}, false, !!borrowingCall(result))) + { + return false; + } + } + + // a method reached through it whose callee keeps its `this`: a second owner + for (auto &borrowUse : uses) + { + if (!isCall(borrowUse.op)) + { + continue; + } + + auto args = callArgs(borrowUse.op); + for (auto index : ownedParams(borrowUse.op)) + { + if (static_cast(index) < args.size() && + mlir::isa_and_nonnull(args[index].getDefiningOp())) + { + return false; + } + } + } + + auto outlives = [&](mlir::Operation *end) { + if (end == taker || !reachableAfter(taker, end, made)) + { + return false; + } + + return llvm::any_of(uses, [&](BorrowUse &borrowUse) { + return (borrowUse.op != end || !borrowUse.callee) && reachableAfter(end, borrowUse.op, borrowUse.kills); + }); + }; + + for (auto *end : rootEnds(chain)) + { + if (outlives(end)) + { + return false; + } + } + + for (auto *drop : drops) + { + if (dropsChain(drop, chain, nullptr, value) && outlives(drop)) + { + return false; + } + } + + return true; + } + // Where rc takes the receiver's reference: its `ts.Retain` of the value, when that comes // first in the taker's block, else the taker itself (a declaration, whose `ts.RetainSlot` // follows it). A release between the two - a `return`'s scope exit runs after its retain @@ -5154,7 +7209,8 @@ class OwnershipInferencePass signalPassFailure(); } - void reportMovedOnSomePaths(mlir::Operation *move, mlir::Operation *release, llvm::StringRef name) + void reportMovedOnSomePaths(mlir::Operation *move, mlir::Operation *release, llvm::StringRef name, + mlir::Operation *usedAt = nullptr) { if (!reporting()) { @@ -5165,6 +7221,12 @@ class OwnershipInferencePass << "' is moved here on some paths only; -mm=own cannot release it " "on the others yet"; diag.attachNote(release->getLoc()) << "released here on every path"; + if (usedAt) + { + diag.attachNote(usedAt->getLoc()) + << "used here, after the paths join, so it cannot be released before they do"; + } + signalPassFailure(); } }; diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp index b99ede9e5..59496e0c8 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp @@ -13,6 +13,7 @@ #include "llvm/ADT/DenseMap.h" #include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/MapVector.h" #include "llvm/ADT/SetVector.h" #include "llvm/ADT/SmallVector.h" #include "llvm/ADT/StringMap.h" @@ -50,14 +51,22 @@ class OwnershipSignaturePass : public mlir::PassWrapper({...})`). Its retains are judged on their own. static bool movesIntoResult(mlir::Value value) { return llvm::all_of(value.getUses(), [](mlir::OpOperand &use) { @@ -67,6 +76,13 @@ class OwnershipSignaturePass : public mlir::PassWrapper(user)) + { + return sharedNewOp.getValue() == use.get(); + } + if ((isView(user) && use.getOperandNumber() == 0) || mlir::isa(user)) { return movesIntoResult(user->getResult(0)); @@ -115,6 +131,239 @@ class OwnershipSignaturePass : public mlir::PassWrapper(at); at = at->getParentOp()) + { + if (mlir::isa(at)) + { + return true; + } + + auto *block = at->getBlock(); + if (!block) + { + return true; // not in a function: nothing is known of how often it runs + } + + llvm::SmallPtrSet seen; + llvm::SmallVector work(block->getSuccessors().begin(), block->getSuccessors().end()); + while (!work.empty()) + { + auto *next = work.pop_back_val(); + if (next == block) + { + return true; + } + + if (seen.insert(next).second) + { + work.append(next->getSuccessors().begin(), next->getSuccessors().end()); + } + } + } + + return false; + } + + // The immortal globals (OWN_IMMORTAL_GLOBAL_ATTR_NAME, spec 25). A global of a class type, + // not exported, is immortal when only its initializer writes it, once, and gives it a fresh + // object, nothing, or another immortal global's value. Its initializer is either its own region + // (`const c = new C()`) or, for a class static, the one store into it in the class's static + // constructor, which a `ts.GlobalConstructor` runs (the region then yields only the default). + // Top-level script code is such a constructor too, so a module-level `let c: C;` it stores + // into once is one as well. "Once" is how often the store runs, not how many stores there are: + // a store in a loop (`static { for (...) K.w = new V(i); }`) would mark every block it is given + // immortal, and each old one would leak, so a store that may run again leaves the global mortal, + // and each store releases the old value, as for any other global. + // + // The store - the region's, which the lowering makes - marks the block HEAP_BLOCK_IMMORTAL, so + // every block the global ever holds is one no release frees. The value test makes sure the block + // is the global's to keep: an object made right there and given back by nobody. A call's result + // is not one, whatever its callee: these facts are found before the callees' (computeFacts), and + // a global's region is no function the inference checks, so a result that borrows its argument - + // a temporary the region frees, an element of another global's array - would be marked, and + // stored, as the global's own. Such a global stays mortal, and a read of it keeps today's errors. + void findImmortalGlobals(mlir::ModuleOp module) + { + // the static constructors, which run once before the program + llvm::StringSet<> constructors; + module.walk([&](mlir_ts::GlobalConstructorOp constructorOp) { constructors.insert(constructorOp.getGlobalName()); }); + + // a static's address taken out of a class's vtable (`k.w = x`, `k: typeof K`) and written + // through: it may be any class's static the vtable slot holds, so no static is immortal + auto vtableWritten = false; + module.walk([&](mlir::Operation *op) { + if (mlir::isa(op) && + mlir::isa(op->getResult(0).getType()) && + !llvm::all_of(op->getUsers(), [](mlir::Operation *user) { return mlir::isa(user); })) + { + vtableWritten = true; + } + }); + + auto uses = mlir::SymbolTable::getSymbolUses(&module.getBodyRegion()); + if (!uses) + { + return; // an op this pass cannot read symbols through: nothing is known never written + } + + llvm::StringMap> usesOf; + for (auto &use : *uses) + { + usesOf[use.getSymbolRef().getRootReference().getValue()].push_back(use.getUser()); + } + + // each candidate with the value its initializer gives it, and the store that does, if any + llvm::MapVector> candidates; + MLIRTypeHelper mth(&getContext(), CompileOptions{}); + module.walk([&](mlir_ts::GlobalOp globalOp) { + 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()) + { + return; + } + + auto resultOp = mlir::dyn_cast(globalOp.getInitializerBlock()->getTerminator()); + if (!resultOp || resultOp->getNumOperands() != 1) + { + return; + } + + mlir_ts::StoreOp initStore; + for (auto *user : usesOf.lookup(globalOp.getSymName())) + { + // its vtable's entry, which only a `ts.VirtualSymbolRef` reads (vtableWritten) + if (mlir::isa(user) && user->getParentOfType()) + { + if (vtableWritten) + { + return; + } + + continue; + } + + if (mlir::isa(user)) + { + continue; // only loaded, or vtableWritten + } + + auto addressOfOp = mlir::dyn_cast(user); + if (!addressOfOp) + { + return; + } + + auto released = false; + auto stored = false; + for (auto *addressUser : addressOfOp->getUsers()) + { + if (mlir::isa(addressUser)) + { + continue; + } + + // the old value, the default, given up before the store + if (mlir::isa(addressUser)) + { + released = true; + continue; + } + + // the one store, in a static constructor, run once + auto storeOp = mlir::dyn_cast(addressUser); + auto funcOp = storeOp ? storeOp->getParentOfType() : mlir_ts::FuncOp(); + if (initStore || !storeOp || storeOp.getReference() != addressOfOp.getResult() || + storeOp.getValue() == addressOfOp.getResult() || !funcOp || !constructors.contains(funcOp.getSymName()) || + mayRunAgain(storeOp)) + { + return; + } + + initStore = storeOp; + stored = true; + } + + if (released && !stored) + { + return; + } + } + + // a static's region yields only its default, which the store replaces + if (initStore && !regionYieldsDefault(globalOp)) + { + return; + } + + candidates[globalOp] = {initStore ? initStore.getValue() : resultOp->getOperand(0), initStore}; + }); + + // given a fresh object, nothing, or another immortal global's value: in module order, until + // no more are found + llvm::StringSet<> immortal; + auto madeHere = [&](mlir::Value value) { + auto root = rootOf(value); + if (!root.getDefiningOp() || isCall(root.getDefiningOp()) || !isFresh(root)) + { + return false; + } + + // and not given back before the global takes it + auto released = false; + forEachUse(root, [&](mlir::Operation *user, mlir::Value) { released = released || mlir::isa(user); }); + return !released; + }; + // isImmortalGlobalRead, of the globals found so far: none is marked yet + auto readsImmortal = [&](mlir::Value value) { + auto addressOfOp = globalReadOf(value); + return addressOfOp && immortal.contains(addressOfOp.getGlobalName()); + }; + + for (auto found = true; found;) + { + found = false; + for (auto &[globalOp, init] : candidates) + { + auto value = init.first; + if (!immortal.contains(globalOp.getSymName()) && (madeHere(value) || holdsNoBlock(value) || readsImmortal(value))) + { + immortal.insert(globalOp.getSymName()); + found = true; + } + } + } + + auto unit = mlir::UnitAttr::get(&getContext()); + for (auto &[globalOp, init] : candidates) + { + if (!immortal.contains(globalOp.getSymName())) + { + continue; + } + + globalOp->setAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME, unit); + if (init.second) + { + init.second->setAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME, unit); + } + + for (auto *user : usesOf.lookup(globalOp.getSymName())) + { + if (mlir::isa(user)) + { + user->setAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME, unit); + } + } + } + } + void moveIntoGlobals(mlir::ModuleOp module) { module.walk([&](mlir_ts::GlobalOp globalOp) { @@ -130,6 +379,13 @@ class OwnershipSignaturePass : public mlir::PassWrappersetAttr(OWN_FRESH_RESULT_ATTR_NAME, mlir::UnitAttr::get(&getContext())); } @@ -237,13 +503,14 @@ class OwnershipSignaturePass : public mlir::PassWrappergetNumResults() == 1) + if (call.callees.fieldFunction && call.op->getNumResults() == 1 && !isRecordResult(call.op)) { call.op->setAttr(OWN_FRESH_RESULT_ATTR_NAME, mlir::UnitAttr::get(&getContext())); } - // a generator whose state object borrows from its arguments - if (call.callees.funcs.size() == 1 && !call.callees.imported) + // a generator whose state object borrows from its arguments (not through a callback + // family, whose member's parameters may be shifted) + if (call.callees.funcs.size() == 1 && !call.callees.imported && !call.callees.viaCallback) { if (auto bounded = call.callees.funcs.front()->getAttrOfType(OWN_RESULT_BOUNDED_ATTR_NAME)) { @@ -389,6 +656,13 @@ class OwnershipSignaturePass : public mlir::PassWrapper shifts; // the name the imported function is exported under llvm::StringRef importedName; llvm::SmallVector funcs; @@ -427,1210 +701,3936 @@ class OwnershipSignaturePass : public mlir::PassWrapper open; - // The parameter a function's result borrows and the parameters it keeps (spec 2.4); absent is - // rc's convention. - struct Facts + // Methods an interface's vtable holds (`...vtbl`, or `...vtbl` + // for an object literal) whose calls through the interface are not resolved: the slot's family + // is open (interfaceFamilies), or the vtable is one this pass cannot tie to an interface. Such + // a call (`ts.InterfaceSymbolRef`, then an indirect call) is given none of the method's facts: a + // caller that hands it an argument it keeps still releases it, and a caller of one whose result + // borrows an argument or `this` releases the borrow as its own (a double free either way). So a + // method reached this way that keeps a parameter or returns a borrow - by its body or by `@keeps` + // / `@lends` - loses its facts, and the body's error says why (`is a parameter, kept here, but + // -mm=own cannot make the callers of ... give it up`, `is a parameter, returned here, but -mm=own + // cannot tell the callers of ... that the result borrows it`). A method reached only through + // closed interfaces keeps its facts: their calls get them (spec 31). + llvm::DenseSet interfaceReached; + + // ---- Interfaces closed in this module (G9, spec 31) ---- + // + // A call through an interface reaches whatever function the slot of the interface value's vtable + // holds. When every such vtable is one this module defines and every interface value is made + // from one of them, the slot holds one of a known set of functions - the slot's family - and the + // call is resolved to it like a virtual call of a private class family: it gets the facts its + // members agree on (meetFacts demotes a family that disagrees), and `__own_no_drops` when every + // member destroys nothing. Anything this cannot see opens the interface, and then the call is + // what it was: no facts, and its members reached that way lose theirs (interfaceReached). + // + // Open, for an interface I: + // - another module can see it: its name in an exported declaration (`__decls`), or `!ts.iface<@I>` + // in the type of a function or global another module can reach - an exported or declared + // function, a global that is not private, or is exported or imported; + // - an I value made other than by `ts.NewInterface` over a vtable this pass reads (the global, a + // heap copy of it, or the one an object's class vtable holds for I): a cast from another + // interface (`J` to `I`, which opens `J` too: its members are reached through `I`), from + // `any` or an opaque pointer (which opens every interface put into one), from a tuple or array + // that does not hold I already (castMakesInterface), a `ts.InterfaceRef` used as a value; + // - a vtable of I written after its initializer, or read other than to make an interface value; + // - a class vtable that holds a vtable of I and is not private (an exported class: another + // module may extend it); + // - and, per slot, a member that is not a function this module defines, a slot that holds no + // function (a field's offset, a thunk's cell), or a member that says `@keeps` or `@lends` (its + // facts are its signature's, which a family cannot change). A member that is not private (a + // `public` method) is open (findOpen): it has no facts of its own to give the family. + struct InterfaceFamily { - int borrows = -1; - llvm::SmallVector owned; - - bool empty() const - { - return borrows < 0 && owned.empty(); - } - - bool operator==(const Facts &other) const - { - return borrows == other.borrows && owned == other.owned; - } + // the slot's name, which every vtable of the interface agrees on + mlir::StringAttr field; + llvm::SmallVector members; + bool closed = true; + }; - bool operator!=(const Facts &other) const - { - return !(*this == other); - } + // (interface, vtable position) -> the functions every vtable of the interface holds there + std::map, InterfaceFamily> interfaceFamilies; + + // The words of the module's exported declarations (none in an executable), the classes a class + // vtable is the own vtable of, and the classes another module can name: exported, or in the + // type of an exported or declared function or of an exported or imported global (findOpen). + llvm::StringSet<> exportWords; + llvm::StringSet<> ownClasses; + llvm::StringSet<> nameableClasses; + + // A field of function type (`f: (x: number) => number`), called through the interface: the slot + // holds the offset of a field of each implementing object literal, and the call goes to whatever + // function that field holds. When every vtable's slot is such an offset and every function ever + // stored into those fields is one this module defines - the literal's own (`{ f: function ... }`, + // in its `ts.Constant`), or one stored into the field later, through the object or through the + // interface - the call is resolved to them (spec 31). A store of anything else (a parameter, a + // call's result, a closure), a field reached other than to be read or stored, or an object of + // the literal's type made other than by the literal opens the slot. A class implementing the + // field holds a thunk's cell, which this does not follow: open. + struct InterfaceFieldFamily + { + mlir::StringAttr field; + // (object literal's storage tuple, field index) of each vtable + llvm::SmallVector, 2> targets; + llvm::SmallVector members; + bool closed = true; }; - // Each function's facts, from its body (local) and as its callers see them (effective: closed, - // and agreed by every family it is in). - llvm::DenseMap factsLocal; - llvm::DenseMap factsEffective; + std::map, InterfaceFieldFamily> interfaceFieldFamilies; - // ---- Facts a signature says (`@keeps`, `@borrows`, `@lends`) ---- - // - // A function that says them has them whoever calls it: a caller in another module reads them - // from the declaration it re-parses. Its body is checked against them instead (checkDeclared). + // interface name -> the tuple types of the vtables its values are made with, when every one of + // them is read here (holdsFunction: what a value of the interface carries) + llvm::StringMap> interfaceVTableTypes; - static llvm::ArrayRef declared(mlir::Operation *funcOp, llvm::StringRef name) + // `ts.PropertyRef(ts.Cast(null), i)`, seen through casts: the offset of field `i` of an object of + // that type, as an object literal's interface vtable holds it. Null for anything else. + std::pair slotField(mlir::Value value) { - if (auto attr = funcOp->getAttrOfType(name)) + while (auto castOp = value ? value.getDefiningOp() : mlir_ts::CastOp()) { - return attr.asArrayRef(); + value = castOp.getIn(); } - return {}; - } + auto propertyRefOp = value ? value.getDefiningOp() : mlir_ts::PropertyRefOp(); + auto nullObject = propertyRefOp ? propertyRefOp.getObjectRef().getDefiningOp() : mlir_ts::CastOp(); + if (!nullObject || !nullObject.getIn().getDefiningOp() || + !mlir::isa(nullObject.getType())) + { + return {}; + } - static bool declaresFacts(mlir::Operation *funcOp) - { - return funcOp->hasAttr(OWN_DECLARED_KEEPS_ATTR_NAME) || funcOp->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME) || - funcOp->hasAttr(OWN_DECLARED_LENDS_ATTR_NAME); - } + auto tupleType = storageTupleOf(nullObject.getType()); + if (!tupleType) + { + return {}; + } - // The facts `@keeps` and `@lends` say (Facts): what callers move in, and what the result is a - // borrow of. - static bool declaresCallerFacts(mlir::Operation *funcOp) - { - return funcOp->hasAttr(OWN_DECLARED_KEEPS_ATTR_NAME) || funcOp->hasAttr(OWN_DECLARED_LENDS_ATTR_NAME); + return {tupleType, static_cast(propertyRefOp.getPosition())}; } - static int declaredLends(mlir::Operation *funcOp) + // What every object-literal field an interface's field slot may be holds (InterfaceFieldFamily). + void collectFieldFunctions(mlir::ModuleOp module) { - auto lends = declared(funcOp, OWN_DECLARED_LENDS_ATTR_NAME); - return lends.empty() ? -1 : lends.front(); - } + llvm::DenseMap, llvm::SetVector> stored; + llvm::DenseSet> unknownFields; + llvm::DenseSet unknownTypes; + // a function of unknown origin stored through an interface this pass cannot follow + auto unknownAnywhere = false; - // What every function a call names declares under `name`, when they all declare the same; - // nothing when one of them does not. - llvm::SmallVector declaredByCallees(const Callees &callees, llvm::StringRef name) - { - llvm::SmallVector agreed; - for (auto [index, symbol] : llvm::enumerate(callees.named)) - { - // a function, or the global holding the address of one a library exports (a dynamic - // import), which MLIRGen gave the declaration's markers - mlir::Operation *declaring = functions.lookup(symbol.getValue()); - if (!declaring) + auto store = [&](std::pair key, mlir::Value value) { + auto symbol = slotFunction(value); + auto funcOp = symbol ? functions.lookup(symbol.getValue()) : mlir_ts::FuncOp(); + if (funcOp && !funcOp.isDeclaration()) { - declaring = globals.lookup(symbol.getValue()); + stored[key].insert(symbol); } - - if (!declaring || !declaring->hasAttr(name)) + else { - return {}; + unknownFields.insert(key); } + }; - auto indices = declared(declaring, name); - if (index == 0) - { - agreed.assign(indices.begin(), indices.end()); - } - else if (llvm::ArrayRef(agreed) != indices) - { - return {}; - } - } + auto isFunctionType = [](mlir::Type type) { + return mlir::isa(type); + }; - return agreed; - } + module.walk([&](mlir::Operation *op) { + // the literal's own values + if (auto constantOp = mlir::dyn_cast(op)) + { + auto constTupleType = mlir::dyn_cast(constantOp.getType()); + auto values = mlir::dyn_cast(constantOp.getValue()); + if (!constTupleType || !values) + { + return; + } - // The parameters, for an error, by the names MLIRGen recorded beside the marker. - static std::string describeParams(mlir_ts::FuncOp funcOp, llvm::ArrayRef indices) - { - if (indices.empty()) - { - return "nothing"; - } + mlir::Type tupleType = mlir_ts::TupleType::get(&getContext(), constTupleType.getFields()); + for (auto [index, value] : llvm::enumerate(values)) + { + std::pair key{tupleType, static_cast(index)}; + if (auto symbol = mlir::dyn_cast(value)) + { + auto funcOp = functions.lookup(symbol.getValue()); + if (funcOp && !funcOp.isDeclaration()) + { + stored[key].insert(symbol.getAttr()); + } + else + { + unknownFields.insert(key); + } + } + else if (!mlir::isa(value) && index < constTupleType.getFields().size() && + isFunctionType(constTupleType.getFields()[index].type)) + { + unknownFields.insert(key); + } + } - auto names = funcOp->getAttrOfType(OWN_DECLARED_PARAM_NAMES_ATTR_NAME); - std::string text; - llvm::raw_string_ostream os(text); - for (auto [position, index] : llvm::enumerate(indices)) - { - os << (position ? ", " : "") << "'"; - if (names && static_cast(index) < names.size()) - { - os << mlir::cast(names[index]).getValue(); + return; } - else + + // a store into the field through the object + if (auto propertyRefOp = mlir::dyn_cast(op)) { - os << "#" << index; - } + auto tupleType = storageTupleOf(propertyRefOp.getObjectRef().getType()); + if (!tupleType) + { + return; + } - os << "'"; - } + std::pair key{tupleType, static_cast(propertyRefOp.getPosition())}; + for (auto &use : propertyRefOp->getUses()) + { + auto *user = use.getOwner(); + if (mlir::isa(user)) + { + continue; + } - return os.str(); - } + auto storeOp = mlir::dyn_cast(user); + if (storeOp && storeOp.getReference() == propertyRefOp.getResult() && storeOp.getValue() != propertyRefOp.getResult()) + { + store(key, storeOp.getValue()); + continue; + } - // Functions this module defines that `@borrows`, and those whose state was found to borrow. - llvm::DenseMap> borrowsFound; + // an interface vtable's offset of the field (slotField) reads nothing + if (user->getParentOfType() && mlir::isa(user)) + { + continue; + } - // The argument of `funcOp` that `value` is, or a field of: its own value, its slot's, or a - // read out of either. -1 for anything else. - static int32_t parameterOf(mlir::Value value, mlir_ts::FuncOp funcOp) - { - auto root = rootOf(value); - if (auto loadOp = root.getDefiningOp()) - { - if (auto propertyRefOp = loadOp.getReference().getDefiningOp()) + unknownFields.insert(key); + } + + return; + } + + // a store into the field through an interface: into each literal its vtables reach + if (auto interfaceRefOp = mlir::dyn_cast(op)) { - return parameterOf(propertyRefOp->getOperand(0), funcOp); + auto refType = mlir::dyn_cast(interfaceRefOp.getType()); + if (!refType || !isFunctionType(refType.getElementType())) + { + return; + } + + auto interfaceType = mlir::dyn_cast(interfaceRefOp.getInterfaceVal().getType()); + auto found = interfaceType ? interfaceFieldFamilies.find({interfaceType.getName().getValue().str(), + static_cast(interfaceRefOp.getIndex()) + + INTERFACE_VTABLE_HEADER_SLOTS}) + : interfaceFieldFamilies.end(); + for (auto &use : interfaceRefOp->getUses()) + { + auto *user = use.getOwner(); + if (mlir::isa(user)) + { + continue; + } + + auto storeOp = mlir::dyn_cast(user); + if (storeOp && storeOp.getReference() == interfaceRefOp.getResult() && found != interfaceFieldFamilies.end() && + found->second.closed) + { + for (auto target : found->second.targets) + { + store(target, storeOp.getValue()); + } + + continue; + } + + unknownAnywhere = true; + } + + return; } - auto varOp = loadOp.getReference().getDefiningOp(); - int index = -1; - if (varOp && isParameterSlot(varOp, index) && varOp->getParentOfType() == funcOp) + // an object of the literal's type made other than by the literal: its fields may hold + // anything. A cast keeps the type's own value (the literal's box seen as the object, an + // object, a reference to its storage) or makes the null object a vtable's offsets are + // taken from; from anything else it reinterprets. + if (auto castOp = mlir::dyn_cast(op)) { - return index; + auto tupleType = storageTupleOf(castOp.getType()); + if (tupleType && !mlir::isa(castOp.getIn().getType()) && + storageTupleOf(castOp.getIn().getType()) != tupleType) + { + unknownTypes.insert(tupleType); + } + + return; } - return -1; - } + if (mlir::isa(op) || isCall(op) || isView(op) || mlir::isa(op)) + { + return; + } - auto argument = mlir::dyn_cast(root); - if (argument && argument.getOwner()->isEntryBlock() && argument.getOwner()->getParentOp() == funcOp.getOperation()) + for (auto result : op->getResults()) + { + if (mlir::isa(result.getType())) + { + if (auto tupleType = storageTupleOf(result.getType())) + { + unknownTypes.insert(tupleType); + } + } + } + }); + + for (auto &[key, family] : interfaceFieldFamilies) { - return static_cast(argument.getArgNumber()); - } + if (!family.closed) + { + continue; + } - return -1; - } + for (auto target : family.targets) + { + if (unknownAnywhere || unknownTypes.contains(target.first) || unknownFields.contains(target)) + { + family.closed = false; + break; + } - // ---- The iterator protocol ---- - // - // `it.next()` returns a `{value, done}` record, and under own the caller owns what it holds, - // whoever implements it: a generator's `next` moves what it yields into the record (a yield of - // a borrow is an error in its body), and any other `next` is a function whose callers this pass - // cannot all see, which may not return a borrow. So the result is fresh even when the call goes - // through the `Iterator` interface, or into another module - the default library's `matchAll`. + for (auto member : stored.lookup(target)) + { + if (!llvm::is_contained(family.members, member)) + { + family.members.push_back(member); + } + } + } - // A call of a function named `next` - read through an interface, out of a field, or by its - // symbol - whose one result is a `{value, done}` record. - static bool isIteratorNext(mlir::Operation *op) + family.closed = family.closed && !family.members.empty(); + } + } + + // The interfaces mentioned by a type as values: the interface itself, a union or optional, a + // tuple's fields, an array's elements, what a reference points to. Not a class's or an object's + // fields (a reference to a block made elsewhere), nor a function's parameters and result. + static void valueInterfaces(mlir::Type type, llvm::StringSet<> &found, int depth = 0) { - if (op->getNumResults() != 1) + if (!type || depth > 16) { - return false; + return; } - auto tupleType = mlir::dyn_cast(op->getResult(0).getType()); - if (!tupleType || tupleType.getFields().size() != 2) + if (auto interfaceType = mlir::dyn_cast(type)) { - return false; + found.insert(interfaceType.getName().getValue()); + return; } - auto fieldName = [&](int index) { - auto id = mlir::dyn_cast_or_null(tupleType.getFields()[index].id); - return id ? id.getValue() : llvm::StringRef(); + auto fields = [&](llvm::ArrayRef infos) { + for (auto &info : infos) + { + valueInterfaces(info.type, found, depth + 1); + } }; - if (fieldName(0) != "value" || fieldName(1) != "done") + + if (auto tupleType = mlir::dyn_cast(type)) { - return false; + fields(tupleType.getFields()); } - - auto isNext = [](llvm::StringRef name) { return name == "next" || name.ends_with(".next"); }; - if (auto callee = directCallee(op)) + else if (auto constTupleType = mlir::dyn_cast(type)) { - return isNext(callee.getValue()); + fields(constTupleType.getFields()); } - - auto function = calleeValue(op); - if (!function) + else if (auto unionType = mlir::dyn_cast(type)) { - return false; + for (auto member : unionType.getTypes()) + { + valueInterfaces(member, found, depth + 1); + } } - - if (auto getMethodOp = function.getDefiningOp()) + else if (auto optionalType = mlir::dyn_cast(type)) { - function = getMethodOp.getBoundFunc(); + valueInterfaces(optionalType.getElementType(), found, depth + 1); } - - if (auto boundOp = function.getDefiningOp()) + else if (auto arrayType = mlir::dyn_cast(type)) { - function = boundOp.getFunc(); + valueInterfaces(arrayType.getElementType(), found, depth + 1); } - - if (auto loadOp = function.getDefiningOp()) + else if (auto constArrayType = mlir::dyn_cast(type)) { - if (auto refOp = loadOp.getReference().getDefiningOp()) - { - auto identifier = mlir::dyn_cast(refOp.getIdentifier()); - return identifier && isNext(identifier.getValue()); - } - - if (auto propertyRefOp = loadOp.getReference().getDefiningOp()) - { - auto storageType = storageTupleOfType(propertyRefOp->getOperand(0).getType()); - if (!storageType || propertyRefOp.getPosition() >= storageType.getFields().size()) - { - return false; - } - - auto id = mlir::dyn_cast_or_null(storageType.getFields()[propertyRefOp.getPosition()].id); - return id && isNext(id.getValue()); - } - - return false; + valueInterfaces(constArrayType.getElementType(), found, depth + 1); } - - if (auto refOp = function.getDefiningOp()) + else if (auto refType = mlir::dyn_cast(type)) { - return isNext(refOp.getIdentifier()); + valueInterfaces(refType.getElementType(), found, depth + 1); } - - if (auto refOp = function.getDefiningOp()) + else if (auto valueRefType = mlir::dyn_cast(type)) { - return isNext(refOp.getIdentifier()); + valueInterfaces(valueRefType.getElementType(), found, depth + 1); + } + else if (auto boundRefType = mlir::dyn_cast(type)) + { + valueInterfaces(boundRefType.getElementType(), found, depth + 1); + } + else if (auto sharedType = mlir::dyn_cast(type)) + { + valueInterfaces(sharedType.getElementType(), found, depth + 1); } + } - if (auto refOp = function.getDefiningOp()) + // Every interface a type mentions anywhere, a class's fields and a function's parameters + // included: what another module handed a value of this type could reach. With `classes`, the + // classes it mentions too (a base class is its derived class's first field). + static void anyInterfaces(mlir::Type type, llvm::StringSet<> &found, llvm::DenseSet &seen, + llvm::StringSet<> *classes = nullptr) + { + if (!type || !seen.insert(type).second) { - return isNext(refOp.getIdentifier()); + return; } - if (auto refOp = function.getDefiningOp()) + if (auto interfaceType = mlir::dyn_cast(type)) { - return isNext(refOp.getIdentifier()); + found.insert(interfaceType.getName().getValue()); + return; } - return false; - } + auto fields = [&](llvm::ArrayRef infos) { + for (auto &info : infos) + { + anyInterfaces(info.type, found, seen, classes); + } + }; - // A generator another module made: its state object's type is only its shape here, and what each - // field owns - a local, a yielded value now the caller's, what it borrows from its arguments - is - // known in that module alone. A release routine made here from the shape would give back what is - // not the state's. So this module's gives back none of it: only the `.captured` box's cells, and - // their values too unless the maker says it borrows them (`@borrows`). What the generator's own - // locals still hold when it is given up early leaks rather than being freed twice. - void findImportedStates(mlir::ModuleOp module) - { - llvm::MapVector states; - for (auto &call : calls) + // the types whose fields are not parameters of the type + if (auto classStorageType = mlir::dyn_cast(type)) { - if (!makesGeneratorState(call.op) || (call.callees.known && !call.callees.funcs.empty())) + if (classes) { - continue; + classes->insert(classStorageType.getName().getValue()); } - auto tupleType = storageTupleOfType(call.op->getResult(0).getType()); - auto borrows = !declaredByCallees(call.callees, OWN_DECLARED_BORROWS_ATTR_NAME).empty(); - states[tupleType] = states.lookup(tupleType) || borrows; + fields(classStorageType.getFields()); + return; } - if (states.empty()) + if (auto objectStorageType = mlir::dyn_cast(type)) { + fields(objectStorageType.getFields()); return; } - auto *context = &getContext(); - llvm::SmallVector entries; - if (auto existing = module->getAttrOfType(OWN_BORROWING_FIELDS_ATTR_NAME)) + if (auto tupleType = mlir::dyn_cast(type)) { - entries.append(existing.begin(), existing.end()); + fields(tupleType.getFields()); + return; } - for (auto &[type, borrows] : states) + if (auto constTupleType = mlir::dyn_cast(type)) { - auto tupleType = mlir::cast(type); - auto captured = fieldIndex(tupleType, CAPTURED_NAME); - // the box first, when it borrows; -1 names no field, so a box that owns is released - llvm::SmallVector fields{borrows && captured >= 0 ? captured : -1}; - for (auto [index, field] : llvm::enumerate(tupleType.getFields())) - { - if (static_cast(index) != captured && ownsHeap(module.getLoc(), field.type)) - { - fields.push_back(static_cast(index)); - } - } - - entries.push_back(mlir::ArrayAttr::get(context, {mlir::TypeAttr::get(type), mlir::DenseI32ArrayAttr::get(context, fields)})); + fields(constTupleType.getFields()); + return; } - module->setAttr(OWN_BORROWING_FIELDS_ATTR_NAME, mlir::ArrayAttr::get(context, entries)); + type.walkImmediateSubElements([](mlir::Attribute) {}, [&](mlir::Type sub) { anyInterfaces(sub, found, seen, classes); }); } - // A call whose result is a generator's state object (an object whose fields are `.step` and - // `next`): a generator function made it, a `ts.New` block it hands over (spec 18.1), which the - // caller owns - in another module too, as the iteration that follows needs. - static bool makesGeneratorState(mlir::Operation *op) + // The function values a value of this type holds: the type itself, a union's or an optional's + // member, a tuple's field, an array's element. Not what a reference points to (a vtable's + // address seen as an opaque pointer is the compiler's own, not a value). + static void functionValues(mlir::Type type, llvm::SmallVectorImpl &found, int depth = 0) { - if (op->getNumResults() != 1 || !mlir::isa(op->getResult(0).getType())) + if (!type || depth > 16) { - return false; + return; } - auto tupleType = storageTupleOfType(op->getResult(0).getType()); - if (!tupleType) + if (mlir::isa(type)) { - return false; + found.push_back(type); + return; } - auto hasStep = false; - auto hasNext = false; - for (auto field : tupleType.getFields()) - { - if (auto id = mlir::dyn_cast_or_null(field.id)) + auto fields = [&](llvm::ArrayRef infos) { + for (auto &info : infos) { - hasStep = hasStep || id.getValue() == ".step"; - hasNext = hasNext || id.getValue() == "next"; + functionValues(info.type, found, depth + 1); } - } - - return hasStep && hasNext; - } + }; - // storageTupleOf, without the pass's context: an object's storage, a reference's or a tuple - static mlir_ts::TupleType storageTupleOfType(mlir::Type type) - { - if (auto objectType = mlir::dyn_cast(type)) + if (auto tupleType = mlir::dyn_cast(type)) { - if (auto storageType = mlir::dyn_cast(objectType.getStorageType())) + fields(tupleType.getFields()); + } + else if (auto constTupleType = mlir::dyn_cast(type)) + { + fields(constTupleType.getFields()); + } + else if (auto unionType = mlir::dyn_cast(type)) + { + for (auto member : unionType.getTypes()) { - return mlir_ts::TupleType::get(type.getContext(), storageType.getFields()); + functionValues(member, found, depth + 1); } - - // an object a declaration names by its shape (another module's generator) - return mlir::dyn_cast(objectType.getStorageType()); } - - if (auto refType = mlir::dyn_cast(type)) + else if (auto optionalType = mlir::dyn_cast(type)) { - type = refType.getElementType(); + functionValues(optionalType.getElementType(), found, depth + 1); } - else if (auto valueRefType = mlir::dyn_cast(type)) + else if (auto arrayType = mlir::dyn_cast(type)) { - type = valueRefType.getElementType(); + functionValues(arrayType.getElementType(), found, depth + 1); + } + else if (auto constArrayType = mlir::dyn_cast(type)) + { + functionValues(constArrayType.getElementType(), found, depth + 1); + } + else if (auto sharedType = mlir::dyn_cast(type)) + { + functionValues(sharedType.getElementType(), found, depth + 1); } + } - return mlir::dyn_cast(type); - } - - // ---- Fields a generator yields ---- - // - // A generator's locals are fields of its state object, and `yield groups` puts what one holds into - // the `{value, done}` record its `next` returns: a borrow of the state, which the caller cannot - // keep. When the field is read for nothing but yields, and each value stored into it is yielded - // before the generator can stop (later in the same block), the yield moves the value instead: - // the read is fresh (OWN_FRESH_RESULT_ATTR_NAME) and the field owns nothing - // (OWN_YIELDED_FIELDS_ATTR_NAME). - - // `ts.Store(value, ts.PropertyRef(record, i))`, where `record` is a local a `ts.Constant` - // initializes: a field of a record built to be returned, a yield's. - static bool isRecordFieldStore(mlir::Operation *user, mlir::Value value) + // A function type's parameters and results, whatever kind of function it is. + static std::pair, llvm::SmallVector> signatureOf(mlir::Type type) { - auto storeOp = mlir::dyn_cast(user); - auto propertyRefOp = storeOp && storeOp.getValue() == value - ? storeOp.getReference().getDefiningOp() - : mlir_ts::PropertyRefOp(); - auto recordOp = propertyRefOp ? propertyRefOp->getOperand(0).getDefiningOp() : mlir_ts::VariableOp(); - return recordOp && recordOp.getInitializer() && recordOp.getInitializer().getDefiningOp(); + llvm::SmallVector inputs; + llvm::SmallVector results; + if (auto functionType = mlir::dyn_cast(type)) + { + inputs.append(functionType.getInputs().begin(), functionType.getInputs().end()); + results.append(functionType.getResults().begin(), functionType.getResults().end()); + } + else if (auto boundType = mlir::dyn_cast(type)) + { + inputs.append(boundType.getInputs().begin(), boundType.getInputs().end()); + results.append(boundType.getResults().begin(), boundType.getResults().end()); + } + else if (auto hybridType = mlir::dyn_cast(type)) + { + inputs.append(hybridType.getInputs().begin(), hybridType.getInputs().end()); + results.append(hybridType.getResults().begin(), hybridType.getResults().end()); + } + else + { + // a kind this does not read: never the same signature as another + inputs.push_back(type); + } + + return {inputs, results}; } - void findYieldedFields(mlir::ModuleOp module) + // Does a cast reinterpret a function value - from `any` or `unknown`, or as a function of another + // signature? Then an interface value its parameters or + // results hold crosses unseen (a `(o: Op2) => ...` called as `(o: Op) => ...`): `interfaces` + // gets every interface either side's signatures mention, and `boxed` the input's when it is put + // into `any`. + static bool castReinterpretsFunction(mlir::Type in, mlir::Type out, llvm::StringSet<> &interfaces, bool &boxing) { - // (state type, field) -> its reads and its stores - struct FieldUses + // `!ts.opaque` is the compiler's own (a vtable slot read as the `.instanceOf` it holds), not + // a value a program can put a function into + auto isOpaque = [](mlir::Type type) { return mlir::isa(type); }; + llvm::SmallVector inFunctions; + llvm::SmallVector outFunctions; + functionValues(in, inFunctions); + functionValues(out, outFunctions); + boxing = false; + if ((inFunctions.empty() && outFunctions.empty()) || in == out) { - llvm::SmallVector reads; - llvm::SmallVector stores; - bool other = false; - }; + return false; + } - llvm::MapVector, FieldUses> fields; - module.walk([&](mlir_ts::PropertyRefOp propertyRefOp) { - auto tupleType = storageTupleOf(propertyRefOp->getOperand(0).getType()); - auto position = static_cast(propertyRefOp.getPosition()); - if (!tupleType || fieldIndex(tupleType, ".step") < 0 || position == fieldIndex(tupleType, CAPTURED_NAME) || - !mlir::isa(propertyRefOp.getType()) || - !ownsHeap(propertyRefOp.getLoc(), mlir::cast(propertyRefOp.getType()).getElementType())) + auto mention = [&](llvm::ArrayRef types) { + for (auto type : types) { - return; + llvm::DenseSet seen; + anyInterfaces(type, interfaces, seen); } + }; - auto &uses = fields[{tupleType, position}]; - for (auto *user : propertyRefOp->getUsers()) - { - if (auto loadOp = mlir::dyn_cast(user)) - { - uses.reads.push_back(loadOp); - } - else if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getReference() == propertyRefOp.getResult()) + if (isOpaque(out)) + { + boxing = true; + mention(inFunctions); + return false; + } + + // the same signatures, but for `this` seen as an opaque pointer on one side, or bound on one + // side only (a closure's box, a bound function's object: the caller supplies neither) + auto sameSignature = [](mlir::Type a, mlir::Type b) { + auto [aInputs, aResults] = signatureOf(a); + auto [bInputs, bResults] = signatureOf(b); + auto dropBoundThis = [](mlir::Type type, llvm::SmallVector &inputs) { + if (!inputs.empty() && (mlir::isa(type) || mlir::isa(inputs.front()))) { - uses.stores.push_back(storeOp); + inputs.erase(inputs.begin()); } - else if (!mlir::isa(user)) + }; + + if (aInputs.size() == bInputs.size() + 1) + { + dropBoundThis(a, aInputs); + } + else if (bInputs.size() == aInputs.size() + 1) + { + dropBoundThis(b, bInputs); + } + + if (aResults != bResults || aInputs.size() != bInputs.size()) + { + return false; + } + + for (auto [index, pair] : llvm::enumerate(llvm::zip(aInputs, bInputs))) + { + auto [aInput, bInput] = pair; + if (aInput != bInput && + !(index == 0 && (mlir::isa(aInput) || mlir::isa(bInput)))) { - uses.other = true; + return false; } } - }); - // a field the signature pass already found borrowing is the caller's, not the generator's - llvm::DenseSet> borrowing; - if (auto entries = module->getAttrOfType(OWN_BORROWING_FIELDS_ATTR_NAME)) + return true; + }; + + // a function out of `any`, or one function seen as another; a function seen as something + // that is no function (a vtable slot's reference to it) is the compiler's own + auto reinterprets = isOpaque(in) && !outFunctions.empty(); + if (!inFunctions.empty() && !outFunctions.empty()) { - for (auto entry : entries.getAsRange()) + reinterprets = reinterprets || inFunctions.size() != outFunctions.size(); + for (auto [inFunction, outFunction] : llvm::zip(inFunctions, outFunctions)) { - for (auto index : mlir::cast(entry[1]).asArrayRef()) - { - borrowing.insert({mlir::cast(entry[0]).getValue(), index}); - } + reinterprets = reinterprets || !sameSignature(inFunction, outFunction); } } - llvm::MapVector> yielded; - for (auto &[key, uses] : fields) + if (reinterprets) { - if (uses.other || uses.reads.empty() || uses.stores.empty() || borrowing.contains(key)) + mention(inFunctions); + mention(outFunctions); + } + + return reinterprets; + } + + // The interfaces an interface member's type mentions: a method's or a function field's + // parameters and result, a field's type, anything they hold (a class's fields included). + static void memberTypeInterfaces(mlir::Type type, llvm::StringSet<> &found) + { + llvm::DenseSet seen; + anyInterfaces(type, found, seen); + } + + static bool isNullish(mlir::Type type) + { + return mlir::isa(type); + } + + // May a cast from `in` to `out` make an interface value out of something that was not that + // interface's value already? A union or optional narrowed to its interface member (whose other + // members are null or undefined) or widened from it, null given a nullable interface type, a + // class or object seen as another (the same block, its fields as they are) and a function seen as + // another are not; a cast from another interface, `any`, an opaque pointer, or a tuple or an + // array whose element is not the same interface is. + static bool castMakesInterface(mlir::Type in, mlir::Type out, int depth = 0) + { + // null seen as an interface value has no vtable: a call through it reaches nothing + if (in == out || depth > 16 || isNullish(in)) + { + return false; + } + + llvm::StringSet<> made; + valueInterfaces(out, made); + if (made.empty()) + { + return false; + } + + if (mlir::isa(out)) + { + return !holdsAsIs(in, out, /*onlyNullable=*/true); + } + + if (mlir::isa(out)) + { + if (isNullish(in) || holdsAsIs(out, in)) { - continue; + return false; } - // read only to be put into a yield's record - auto onlyYielded = llvm::all_of(uses.reads, [](mlir_ts::LoadOp loadOp) { - auto users = llvm::make_filter_range(loadOp->getUsers(), [](mlir::Operation *user) { - return !mlir::isa(user); - }); - return llvm::hasSingleElement(users) && isRecordFieldStore(*users.begin(), loadOp.getResult()); - }); + llvm::SmallVector members; + if (auto unionType = mlir::dyn_cast(in)) + { + members.append(unionType.getTypes().begin(), unionType.getTypes().end()); + } + else if (auto optionalType = mlir::dyn_cast(in)) + { + members.push_back(optionalType.getElementType()); + } + else + { + return true; + } - // each value stored is yielded later in its block: nothing between stops the generator - auto eachYielded = llvm::all_of(uses.stores, [&](mlir_ts::StoreOp storeOp) { - return llvm::any_of(uses.reads, [&](mlir_ts::LoadOp loadOp) { - return loadOp->getBlock() == storeOp->getBlock() && storeOp->isBeforeInBlock(loadOp); - }); - }); + return !llvm::all_of(members, [&](mlir::Type member) { return isNullish(member) || holdsAsIs(out, member); }); + } - if (!onlyYielded || !eachYielded) + auto tupleFields = [](mlir::Type type) -> std::optional> { + if (auto tupleType = mlir::dyn_cast(type)) { - continue; + return tupleType.getFields(); } - yielded[key.first].push_back(key.second); - for (auto loadOp : uses.reads) + if (auto constTupleType = mlir::dyn_cast(type)) { - loadOp->setAttr(OWN_FRESH_RESULT_ATTR_NAME, mlir::UnitAttr::get(&getContext())); + return constTupleType.getFields(); + } + + return std::nullopt; + }; + + if (auto outFields = tupleFields(out)) + { + auto inFields = tupleFields(in); + if (!inFields || inFields->size() != outFields->size()) + { + return true; + } + + for (auto [inField, outField] : llvm::zip(*inFields, *outFields)) + { + if (castMakesInterface(inField.type, outField.type, depth + 1)) + { + return true; + } } + + return false; } - if (yielded.empty()) + auto elementOf = [](mlir::Type type) -> mlir::Type { + if (auto arrayType = mlir::dyn_cast(type)) + { + return arrayType.getElementType(); + } + + if (auto constArrayType = mlir::dyn_cast(type)) + { + return constArrayType.getElementType(); + } + + return {}; + }; + + if (auto outElement = elementOf(out)) { - return; + auto inElement = elementOf(in); + return !inElement || castMakesInterface(inElement, outElement, depth + 1); } - auto *context = &getContext(); - llvm::SmallVector entries; - for (auto &[type, indices] : yielded) + // a reference seen as another (`!ts.bound_ref` as `!ts.ref`): what it points to + auto pointee = [](mlir::Type type) -> mlir::Type { + if (auto refType = mlir::dyn_cast(type)) + { + return refType.getElementType(); + } + + if (auto valueRefType = mlir::dyn_cast(type)) + { + return valueRefType.getElementType(); + } + + if (auto boundRefType = mlir::dyn_cast(type)) + { + return boundRefType.getElementType(); + } + + return {}; + }; + + if (auto outPointee = pointee(out)) { - entries.push_back(mlir::ArrayAttr::get(context, {mlir::TypeAttr::get(type), mlir::DenseI32ArrayAttr::get(context, indices)})); + auto inPointee = pointee(in); + return !inPointee || castMakesInterface(inPointee, outPointee, depth + 1); } - module->setAttr(OWN_YIELDED_FIELDS_ATTR_NAME, mlir::ArrayAttr::get(context, entries)); + // anything else reinterpreted: what is read out of it was never checked + return true; } - // A function that says `@borrows` and makes no state itself, but returns what a call that has - // a bounded result gives it (`return this.value.matchAll(expr)`): it borrows the parameters - // the call's bounded arguments are, or are read out of. - void findBoundedForwarders() + // The function a vtable slot holds: `ts.SymbolRef`, seen through casts. Null for anything else. + static mlir::StringAttr slotFunction(mlir::Value value) { - llvm::DenseMap calleesOf; - for (auto &call : calls) + while (auto castOp = value ? value.getDefiningOp() : mlir_ts::CastOp()) { - calleesOf[call.op] = &call.callees; + value = castOp.getIn(); } - for (auto &entry : functions) + auto symbolRefOp = value ? value.getDefiningOp() : mlir_ts::SymbolRefOp(); + return symbolRefOp ? symbolRefOp.getIdentifierAttr().getAttr() : mlir::StringAttr(); + } + + static mlir_ts::AddressOfOp addressOfThroughCasts(mlir::Value value) + { + while (auto castOp = value ? value.getDefiningOp() : mlir_ts::CastOp()) { - auto funcOp = entry.second; - if (funcOp.isDeclaration() || !funcOp->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME) || borrowsFound.count(funcOp)) + value = castOp.getIn(); + } + + return value ? value.getDefiningOp() : mlir_ts::AddressOfOp(); + } + + // The words of the module's exported declarations (`__decls*` globals): what another module + // can name. + static llvm::StringSet<> exportedWords(mlir::ModuleOp module) + { + llvm::StringSet<> words; + module.walk([&](mlir_ts::GlobalOp globalOp) { + if (!globalOp.getSymName().starts_with("__decls")) { - continue; + return; } - llvm::SmallVector found; - auto forwards = true; - for (auto value : returnedValues(funcOp)) - { - if (holdsNoBlock(value)) + globalOp.getInitializerRegion().walk([&](mlir_ts::ConstantOp constantOp) { + auto text = mlir::dyn_cast(constantOp.getValue()); + if (!text) { - continue; + return; } - auto *call = rootOf(value).getDefiningOp(); - auto known = call ? calleesOf.find(call) : calleesOf.end(); - if (known == calleesOf.end()) + llvm::StringRef rest = text.getValue(); + while (!rest.empty()) { - forwards = false; - break; + auto isWordChar = [](char c) { return llvm::isAlnum(c) || c == '_' || c == '$'; }; + rest = rest.drop_while([&](char c) { return !isWordChar(c); }); + auto word = rest.take_while(isWordChar); + if (!word.empty()) + { + words.insert(word); + } + + rest = rest.drop_front(word.size()); } + }); + }); - llvm::SmallVector bounded = declaredByCallees(*known->second, OWN_DECLARED_BORROWS_ATTR_NAME); - if (bounded.empty() && known->second->funcs.size() == 1) + return words; + } + + void collectInterfaceFamilies(mlir::ModuleOp module) + { + llvm::StringSet<> open; + auto openAll = [&](const llvm::StringSet<> &names) { + for (auto &entry : names) + { + open.insert(entry.getKey()); + } + }; + + // the vtables: an interface's (`ts.InsertProperty` of each slot, slot 0 `.instanceOf` of a + // class that is not the vtable's owner, or `.instanceOf.none` of an object literal), and the + // slots of a class's own vtable that hold the address of one of its interface vtables + struct InterfaceVTable + { + mlir_ts::TupleType type; + llvm::DenseMap slots; + // a field's slot: the object literal's field it is the offset of + llvm::DenseMap> fields; + bool written = false; + }; + + llvm::StringMap vtables; + // `..vtbl` globals that are neither (no `.instanceOf` this reads in slot 0) + llvm::SmallVector unread; + // interface name -> the interface vtables class vtables hold for it, read through an + // object's class at run time (`ts.VTableOffsetRef` of the object's vtable) + llvm::StringMap> classHeld; + module.walk([&](mlir_ts::GlobalOp globalOp) { + auto name = globalOp.getSymName(); + if (!name.ends_with("..vtbl")) + { + return; + } + + auto owner = name.drop_back(llvm::StringRef("..vtbl").size()); + llvm::DenseMap values; + globalOp.getInitializerRegion().walk([&](mlir_ts::InsertPropertyOp insertOp) { + if (insertOp.getPosition().size() == 1) { - auto made = declared(known->second->funcs.front(), OWN_RESULT_BOUNDED_ATTR_NAME); - bounded.assign(made.begin(), made.end()); + values[insertOp.getPosition()[0]] = insertOp.getValue(); } + }); - if (bounded.empty()) + auto header = slotFunction(values.lookup(0)); + auto tupleType = mlir::dyn_cast(globalOp.getType()); + if (!header || !tupleType) + { + unread.push_back(name); + return; + } + + if (header.getValue() == (owner + "..instanceOf").str()) + { + // an exported class (not in an executable, whose exports no module sees) + auto exposed = !executable && (!globalOp.isPrivate() || globalOp->hasAttr("export") || globalOp->hasAttr(DLL_EXPORT)); + ownClasses.insert(owner); + if (exposed) { - forwards = false; - break; + nameableClasses.insert(owner); } - auto args = callArgs(call); - for (auto index : bounded) + for (auto &[position, value] : values) { - auto param = static_cast(index) < args.size() ? parameterOf(args[index], funcOp) : -1; - if (param < 0) + auto addressOfOp = addressOfThroughCasts(value); + auto id = position < static_cast(tupleType.size()) + ? mlir::dyn_cast_or_null(tupleType.getFieldInfo(position).id) + : mlir::StringAttr(); + if (!addressOfOp || !id) { - forwards = false; - break; + continue; } - if (!llvm::is_contained(found, param)) + classHeld[id.getValue()].insert(addressOfOp.getGlobalName()); + if (exposed) { - found.push_back(param); + open.insert(id.getValue()); // another module may extend the class } } + + return; } - if (forwards && !found.empty()) + if (!header.getValue().ends_with("..instanceOf") && header.getValue() != ".instanceOf.none") { - llvm::sort(found); - borrowsFound[funcOp] = found; + unread.push_back(name); + return; } - } - } - // A body that does other than its signature says, and an override that says other than the - // method it overrides: either way a caller that trusts the signature frees twice or leaks. - void checkDeclared() - { - for (auto &entry : functions) - { - auto funcOp = entry.second; - if (funcOp.isDeclaration()) + auto &vtable = vtables[name]; + vtable.type = tupleType; + for (auto &[position, value] : values) { - continue; + if (position > 0) + { + vtable.slots[position] = slotFunction(value); + if (auto field = slotField(value); field.first) + { + vtable.fields[position] = field; + } + } } + }); - if (funcOp->hasAttr(OWN_DECLARED_LENDS_ATTR_NAME) && funcOp->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME)) + // interface name -> the vtables its values are made with + llvm::StringMap> madeWith; + auto vtableOf = [&](mlir::Value value) -> llvm::StringRef { + // the global itself + if (auto addressOfOp = addressOfThroughCasts(value)) { - funcOp.emitError("'") << funcOp.getSymName() - << "' cannot say both '@" LENDS_DECORATOR "' and '@" BORROWS_DECORATOR - "': its caller owns a result that borrows, and nothing of one it is lent"; - signalPassFailure(); - continue; + return vtables.count(addressOfOp.getGlobalName()) ? addressOfOp.getGlobalName() : llvm::StringRef(); } - if (declaresCallerFacts(funcOp)) + // a copy of it on the heap, its absent members' slots patched + while (auto castOp = value.getDefiningOp()) { - auto local = factsLocal.lookup(funcOp); - auto keeps = declared(funcOp, OWN_DECLARED_KEEPS_ATTR_NAME); - auto lends = declaredLends(funcOp); - if (local.borrows != lends) + value = castOp.getIn(); + } + + if (auto newOp = value.getDefiningOp()) + { + llvm::StringRef copied; + for (auto *user : newOp->getUsers()) { - if (lends < 0) + auto storeOp = mlir::dyn_cast(user); + auto loadOp = storeOp && storeOp.getReference() == newOp.getResult() + ? storeOp.getValue().getDefiningOp() + : mlir_ts::LoadOp(); + auto addressOfOp = loadOp ? addressOfThroughCasts(loadOp.getReference()) : mlir_ts::AddressOfOp(); + if (addressOfOp && vtables.count(addressOfOp.getGlobalName())) { - funcOp.emitError("'") << funcOp.getSymName() << "' returns a borrow of " - << describeParams(funcOp, local.borrows) - << ", which it has to say with '@" LENDS_DECORATOR "'"; + if (!copied.empty()) + { + return {}; + } + + copied = addressOfOp.getGlobalName(); } - else if (local.borrows < 0) + } + + return copied; + } + + return {}; + }; + + // an object's class vtable read at run time: `ts.VTableOffsetRef(ts.Load(ts.PropertyRef(object, 0)), k)` + auto readFromClass = [](mlir::Value value) { + auto offsetRefOp = value.getDefiningOp(); + auto loadOp = offsetRefOp ? offsetRefOp.getVtable().getDefiningOp() : mlir_ts::LoadOp(); + auto propertyRefOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); + return propertyRefOp && propertyRefOp.getPosition() == 0 && + mlir::isa(propertyRefOp.getObjectRef().getType()); + }; + + // what reads a vtable's address: making an interface value, a class vtable's slot, the copy + std::function onlyMakesValues = [&](mlir::Value address) { + return llvm::all_of(address.getUses(), [&](mlir::OpOperand &use) { + auto *user = use.getOwner(); + if (auto newInterfaceOp = mlir::dyn_cast(user)) + { + return newInterfaceOp.getInterfaceVTable() == use.get(); + } + + if (mlir::isa(user)) + { + return onlyMakesValues(user->getResult(0)); + } + + if (mlir::isa(user)) + { + return use.getOperandNumber() == 0 && user->getParentOfType() != nullptr; + } + + // the copy: read whole, and stored into a block `ts.New` made + if (auto loadOp = mlir::dyn_cast(user)) + { + return llvm::all_of(loadOp->getUsers(), [&](mlir::Operation *loadUser) { + auto storeOp = mlir::dyn_cast(loadUser); + return storeOp && storeOp.getValue() == loadOp.getResult() && + storeOp.getReference().getDefiningOp(); + }); + } + + return false; + }); + }; + + // An interface value made out of another one reaches the other's members: both are open. One + // made out of `any` or an opaque pointer may be any interface value put into one: every + // interface so boxed is open with it, and every interface at all when it is untraceable. + auto allOpen = false; + llvm::StringSet<> boxed; + llvm::StringSet<> unboxedInto; + auto unboxedFunction = false; + // the interfaces a cast makes a value of out of another interface's or an opaque one, and + // the ones it came from: closed over their members' types below + llvm::StringSet<> castOpened; + // interface -> the interfaces its members' types mention: its vtables' slots and what is + // read out of its values (`ts.InterfaceSymbolRef`), function signatures and class fields + // included + llvm::StringMap> memberInterfaces; + auto isOpaque = [](mlir::Type type) { return mlir::isa(type); }; + module.walk([&](mlir::Operation *op) { + if (auto newInterfaceOp = mlir::dyn_cast(op)) + { + auto name = mlir::cast(newInterfaceOp.getType()).getName().getValue(); + auto vtable = newInterfaceOp.getInterfaceVTable(); + if (auto found = vtableOf(vtable); !found.empty()) + { + madeWith[name].insert(found); + } + else if (readFromClass(vtable)) + { + // any class's vtable for the interface (classHeld, all in its families below) + if (!classHeld.count(name)) { - funcOp.emitError("'@" LENDS_DECORATOR "' of '") - << funcOp.getSymName() << "' names " << describeParams(funcOp, lends) - << ", but its body does not return a borrow of it"; + open.insert(name); } - else + } + else if (auto extractOp = vtable.getDefiningOp()) + { + castOpened.insert(name); + valueInterfaces(extractOp.getInterfaceVal().getType(), castOpened); + } + else + { + allOpen = true; + } + + return; + } + + if (auto castOp = mlir::dyn_cast(op)) + { + auto in = castOp.getIn().getType(); + auto out = castOp.getType(); + if (isOpaque(out)) + { + valueInterfaces(in, boxed); + } + + if (castMakesInterface(in, out)) + { + valueInterfaces(out, castOpened); + valueInterfaces(in, castOpened); + if (isOpaque(in)) { - funcOp.emitError("'@" LENDS_DECORATOR "' of '") - << funcOp.getSymName() << "' names " << describeParams(funcOp, lends) - << ", but its body returns a borrow of " << describeParams(funcOp, local.borrows); + valueInterfaces(out, unboxedInto); } - - signalPassFailure(); } - else if (llvm::ArrayRef(local.owned) != keeps) + + // a function value reinterpreted carries the interface values of its parameters and + // results across unseen (review6 x1c, x1r) + llvm::StringSet<> signatures; + auto boxing = false; + if (castReinterpretsFunction(in, out, signatures, boxing)) { - if (funcOp->hasAttr(OWN_DECLARED_KEEPS_ATTR_NAME)) + openAll(signatures); + if (isOpaque(in)) { - funcOp.emitError("'@" KEEPS_DECORATOR "' of '") - << funcOp.getSymName() << "' names " << describeParams(funcOp, keeps) << ", but its body keeps " - << describeParams(funcOp, local.owned); + unboxedFunction = true; } - else + } + else if (boxing) + { + for (auto &entry : signatures) { - funcOp.emitError("'") << funcOp.getSymName() << "' keeps " << describeParams(funcOp, local.owned) - << ", which it has to say with '@" KEEPS_DECORATOR "'"; + boxed.insert(entry.getKey()); } + } - signalPassFailure(); + return; + } + + if (auto interfaceRefOp = mlir::dyn_cast(op)) + { + if (!interfaceRefOp->use_empty()) + { + open.insert(interfaceRefOp.getIdentifier()); } + + return; } - if (funcOp->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME)) + if (auto symbolRefOp = mlir::dyn_cast(op)) { - auto says = declared(funcOp, OWN_DECLARED_BORROWS_ATTR_NAME); - auto found = borrowsFound.find(funcOp); - if (found == borrowsFound.end()) + if (auto interfaceType = mlir::dyn_cast(symbolRefOp.getInterfaceVal().getType())) { - funcOp.emitError("'@" BORROWS_DECORATOR "' of '") - << funcOp.getSymName() << "' names " << describeParams(funcOp, says) - << ", but it neither makes a generator whose state borrows its arguments nor returns one"; - signalPassFailure(); + memberTypeInterfaces(symbolRefOp.getType(), memberInterfaces[interfaceType.getName().getValue()]); } - else if (llvm::ArrayRef(found->second) != says) + + return; + } + + if (auto addressOfOp = mlir::dyn_cast(op)) + { + if (auto found = vtables.find(addressOfOp.getGlobalName()); found != vtables.end() && !onlyMakesValues(addressOfOp.getResult())) { - funcOp.emitError("'@" BORROWS_DECORATOR "' of '") - << funcOp.getSymName() << "' names " << describeParams(funcOp, says) - << ", but the state it makes borrows " << describeParams(funcOp, found->second); - signalPassFailure(); + found->second.written = true; } } + }); + + // seen from another module: a word of its exported declarations, or in a reachable type. An + // executable's exports are seen by no other module (USER decision, spec 31): only what it + // imports - a declared function, an imported global - comes from outside. + if (!executable) + { + exportWords = exportedWords(module); } - // the members of a family a declaring method is in: each says the same - for (auto &[key, members] : families) + auto &words = exportWords; + llvm::StringSet<> exposed; + llvm::DenseSet seen; + for (auto &entry : functions) { - mlir_ts::FuncOp declaring; - for (auto member : members) + // a `public` method of a class no other module can name is not one: only a declaration + // re-parsed from `__decls` names a function of another module + auto exported = !executable && (entry.second->hasAttr("export") || entry.second->hasAttr(DLL_EXPORT)); + if (entry.second.isDeclaration() || exported) { - if (auto funcOp = functions.lookup(member.getValue()); funcOp && declaresFacts(funcOp)) - { - declaring = funcOp; - break; - } + anyInterfaces(entry.second.getFunctionType(), exposed, seen, &nameableClasses); } + } - if (!declaring) + for (auto &entry : globals) + { + auto globalOp = entry.second; + // another module's global, declared here with no initializer (an import of a .ts module + // carries `export`) + auto imported = globalOp->hasAttr("import") || globalOp->hasAttr(DLL_IMPORT) || + globalOp.getInitializerRegion().empty() || globalOp.getInitializerRegion().front().empty(); + auto exported = !globalOp.isPrivate() || globalOp->hasAttr("export") || globalOp->hasAttr(DLL_EXPORT); + if (imported || (exported && !executable)) { - continue; + anyInterfaces(globalOp.getType(), exposed, seen, &nameableClasses); } + } - for (auto member : members) + openAll(exposed); + if (!unboxedInto.empty() || unboxedFunction) + { + openAll(boxed); + for (auto &entry : boxed) { - auto funcOp = functions.lookup(member.getValue()); - if (!funcOp || funcOp == declaring) - { - continue; - } + castOpened.insert(entry.getKey()); + } + } - for (auto *name : {OWN_DECLARED_KEEPS_ATTR_NAME, OWN_DECLARED_BORROWS_ATTR_NAME, OWN_DECLARED_LENDS_ATTR_NAME}) + // An interface value made out of another one (or out of `any`) is called with the other's + // members: `HasF2 { f: (o: Op2, ...) }` seen as `HasF { f: (o: Op, ...) }` hands an `Op` to a + // function that calls through `Op2` (review6r1 v5, v11f). So every interface either side's + // members mention is open too, and theirs, recursively (controller ruling, spec 31). + // + // A class's vtables for its interfaces are its members' families whether or not this module + // makes an interface value of the class (a method called directly then keeps its facts), and + // their slots' types are the interface's members' types: an interface whose values are all + // made from class objects is read through them (review6r2 d1-d3). + for (auto &[name, held] : classHeld) + { + madeWith[name].insert(held.begin(), held.end()); + } + + for (auto &[name, made] : madeWith) + { + for (auto vtableName : made) + { + if (auto found = vtables.find(vtableName); found != vtables.end() && found->second.type) { - if (funcOp->hasAttr(name) != declaring->hasAttr(name) || declared(funcOp, name) != declared(declaring, name)) + for (auto [position, field] : llvm::enumerate(found->second.type.getFields())) { - funcOp.emitError("'") << funcOp.getSymName() << "' overrides '" << declaring.getSymName() - << "', and has to say the same '@" KEEPS_DECORATOR "', '@" BORROWS_DECORATOR - "' and '@" LENDS_DECORATOR "'"; - signalPassFailure(); - break; + if (position > 0) + { + memberTypeInterfaces(field.type, memberInterfaces[name]); + } } } } } - } - // Functions that destroy nothing their caller can reach. - llvm::DenseSet noDrops; + llvm::SmallVector work; + for (auto &entry : castOpened) + { + work.push_back(entry.getKey().str()); + } - // 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 - // through an interface does not name what it reaches. - void collectClassVTables(mlir::ModuleOp module) - { - llvm::SmallVector>>> vtables; - module.walk([&](mlir_ts::GlobalOp globalOp) { - auto entries = classVTableEntries(globalOp); - if (!entries.empty()) + llvm::StringSet<> expanded; + while (!work.empty()) + { + auto name = work.pop_back_val(); + if (!expanded.insert(name).second) { - auto name = globalOp.getSymName(); - classNames.insert(name.drop_back(llvm::StringRef("..vtbl").size())); - vtables.push_back({name, std::move(entries)}); + continue; } - }); - for (auto &[name, entries] : vtables) - { - for (auto [position, symbol] : entries) + open.insert(name); + for (auto &member : memberInterfaces[name]) { - families[{position, methodName(symbol.getValue())}].push_back(symbol); + work.push_back(member.getKey().str()); } } - } - mlir_ts::TupleType storageTupleOf(mlir::Type type) - { - return own_facts::storageTupleOf(&getContext(), type); - } + auto isOpen = [&](llvm::StringRef name) { + // a namespaced interface is declared by its last name + auto last = name.rsplit('.').second; + return allOpen || open.contains(name) || words.contains(name) || (!last.empty() && words.contains(last)); + }; - // The functions a class vtable holds, by position; empty for any other global. - static llvm::SmallVector> classVTableEntries(mlir_ts::GlobalOp globalOp) - { - llvm::SmallVector> entries; - if (!globalOp.getSymName().ends_with("..vtbl")) + // A vtable this pass could not read, or one no value of any interface is made with, that is + // named for an interface (`...vtbl`, `...vtbl`): the interface may have an + // implementation outside its families. Belt and braces - every way to make an interface + // value is traced above - but a missed member would get its family's facts. + llvm::DenseSet made; + for (auto &entry : madeWith) { - return entries; + made.insert(entry.second.begin(), entry.second.end()); } - auto isClass = false; - globalOp.getInitializerRegion().walk([&](mlir_ts::InsertPropertyOp insertOp) { - auto symbolRefOp = insertOp.getValue().getDefiningOp(); - if (!symbolRefOp || insertOp.getPosition().size() != 1) + llvm::SmallVector loose(unread.begin(), unread.end()); + for (auto &entry : vtables) + { + if (!made.contains(entry.getKey())) { - return; + loose.push_back(entry.getKey()); } - - auto symbol = symbolRefOp.getIdentifierAttr().getAttr(); - isClass = isClass || symbol.getValue().ends_with("..instanceOf"); - entries.push_back({insertOp.getPosition()[0], symbol}); - }); - - if (!isClass) - { - entries.clear(); } - return entries; - } - - static bool isClassVTableEntry(mlir::Operation *op) - { - auto globalOp = op->getParentOfType(); - return globalOp && !classVTableEntries(globalOp).empty(); - } - - // `B.get` -> `get`: the symbol with the longest class name that prefixes it taken off. An - // override keeps its base's method name, which is what puts both in one family. - std::string methodName(llvm::StringRef symbol) - { - size_t best = 0; - for (auto &entry : classNames) + for (auto &entry : madeWith) { - auto name = entry.getKey(); - if (name.size() > best && symbol.size() > name.size() && symbol.starts_with(name) && - symbol[name.size()] == '.') + auto name = entry.getKey().str(); + if (llvm::any_of(loose, [&](llvm::StringRef vtableName) { + return vtableName.ends_with("." + name + "..vtbl") || vtableName.starts_with(name + "."); + })) { - best = name.size() + 1; + open.insert(entry.getKey()); } } - return symbol.drop_front(best).str(); - } - - llvm::SmallVector familyOf(int64_t index, mlir::StringAttr identifier) - { - llvm::SmallVector members; + // the families + llvm::DenseSet tied; + for (auto &[name, made] : madeWith) + { + // a class vtable's slot holding a vtable this pass did not read (another module's) + auto allRead = llvm::all_of(made, [&](llvm::StringRef vtableName) { return vtables.count(vtableName); }); + auto closed = !isOpen(name) && allRead; + for (auto vtableName : made) + { + if (!vtables.count(vtableName)) + { + continue; + } + + if (allRead) + { + interfaceVTableTypes[name].push_back(vtables[vtableName].type); + } + + tied.insert(vtableName); + auto &vtable = vtables[vtableName]; + for (auto [position, field] : llvm::enumerate(vtable.type.getFields())) + { + if (position == 0) + { + continue; + } + + auto &family = interfaceFamilies[{name.str(), static_cast(position)}]; + auto id = mlir::dyn_cast_or_null(field.id); + auto member = vtable.slots.lookup(position); + auto funcOp = member ? functions.lookup(member.getValue()) : mlir_ts::FuncOp(); + if (!family.field) + { + family.field = id; + } + + // a member that is not private (a `public` method) is open (findOpen): it has no + // facts to give, and gives the family none + family.closed = family.closed && closed && !vtable.written && id && family.field == id && funcOp && + !funcOp.isDeclaration() && !declaresCallerFacts(funcOp); + if (member && !llvm::is_contained(family.members, member)) + { + family.members.push_back(member); + } + + // the same slot as a field: an object literal's field of function type + auto &fieldFamily = interfaceFieldFamilies[{name.str(), static_cast(position)}]; + auto target = vtable.fields.lookup(position); + auto refType = mlir::dyn_cast(field.type); + if (!fieldFamily.field) + { + fieldFamily.field = id; + } + + fieldFamily.closed = fieldFamily.closed && closed && !vtable.written && id && fieldFamily.field == id && + target.first && refType && mlir::isa(refType.getElementType()); + if (target.first && !llvm::is_contained(fieldFamily.targets, target)) + { + fieldFamily.targets.push_back(target); + } + } + } + } + + collectFieldFunctions(module); + + // what an open family holds, and a vtable no interface value is made with, is reached + // through calls that are not resolved + auto reached = [&](mlir::StringAttr member) { + if (auto funcOp = member ? functions.lookup(member.getValue()) : mlir_ts::FuncOp()) + { + interfaceReached.insert(funcOp); + } + }; + + for (auto &[key, family] : interfaceFamilies) + { + if (!family.closed) + { + llvm::for_each(family.members, reached); + } + } + + for (auto &entry : vtables) + { + if (!tied.contains(entry.getKey())) + { + for (auto &slot : entry.second.slots) + { + reached(slot.second); + } + } + } + } + static constexpr const char *INTERFACE_REACHED_WHY = + "it is reached through an interface, whose calls are not given its facts"; + + // The parameters a function's result borrows and the parameters it keeps (spec 2.4); absent is + // rc's convention. A plain borrow names one parameter; a borrowing record (`record`, spec 32) + // any number, and its caller owns the box. + struct Facts + { + llvm::SmallVector borrows; + llvm::SmallVector owned; + bool record = false; + + bool empty() const + { + return borrows.empty() && owned.empty(); + } + + bool operator==(const Facts &other) const + { + return borrows == other.borrows && owned == other.owned && record == other.record; + } + + bool operator!=(const Facts &other) const + { + return !(*this == other); + } + }; + + // Each function's facts, from its body (local) and as its callers see them (effective: closed, + // and agreed by every family it is in). + llvm::DenseMap factsLocal; + llvm::DenseMap factsEffective; + // The parameters each body takes into something - a field, an element, a global, a keeping call, + // an owning local it assigns (localOwnedParams). A kept parameter that is not one of them is + // released on the paths that do not move it (OWN_PARAMS_RELEASED_ATTR_NAME, spec 33). + llvm::DenseMap> takenParams; + + // ---- Facts a signature says (`@keeps`, `@borrows`, `@lends`) ---- + // + // A function that says them has them whoever calls it: a caller in another module reads them + // from the declaration it re-parses. Its body is checked against them instead (checkDeclared). + + static llvm::ArrayRef declared(mlir::Operation *funcOp, llvm::StringRef name) + { + if (auto attr = funcOp->getAttrOfType(name)) + { + return attr.asArrayRef(); + } + + return {}; + } + + static bool declaresFacts(mlir::Operation *funcOp) + { + return funcOp->hasAttr(OWN_DECLARED_KEEPS_ATTR_NAME) || funcOp->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME) || + funcOp->hasAttr(OWN_DECLARED_LENDS_ATTR_NAME); + } + + // The facts `@keeps` and `@lends` say (Facts): what callers move in, and what the result is a + // borrow of. + static bool declaresCallerFacts(mlir::Operation *funcOp) + { + return funcOp->hasAttr(OWN_DECLARED_KEEPS_ATTR_NAME) || funcOp->hasAttr(OWN_DECLARED_LENDS_ATTR_NAME); + } + + // The parameters `@lends` names: one for a plain borrow, any number for a borrowing record + // (spec 32). + static llvm::SmallVector declaredLends(mlir::Operation *funcOp) + { + auto lends = declared(funcOp, OWN_DECLARED_LENDS_ATTR_NAME); + llvm::SmallVector sorted(lends.begin(), lends.end()); + llvm::sort(sorted); + sorted.erase(std::unique(sorted.begin(), sorted.end()), sorted.end()); + return sorted; + } + + // What every function a call names declares under `name`, when they all declare the same; + // nothing when one of them does not. + llvm::SmallVector declaredByCallees(const Callees &callees, llvm::StringRef name) + { + llvm::SmallVector agreed; + for (auto [index, symbol] : llvm::enumerate(callees.named)) + { + // a function, or the global holding the address of one a library exports (a dynamic + // import), which MLIRGen gave the declaration's markers + mlir::Operation *declaring = functions.lookup(symbol.getValue()); + if (!declaring) + { + declaring = globals.lookup(symbol.getValue()); + } + + if (!declaring || !declaring->hasAttr(name)) + { + return {}; + } + + auto indices = declared(declaring, name); + if (index == 0) + { + agreed.assign(indices.begin(), indices.end()); + } + else if (llvm::ArrayRef(agreed) != indices) + { + return {}; + } + } + + return agreed; + } + + // The parameters, for an error, by the names MLIRGen recorded beside the marker. + static std::string describeParams(mlir_ts::FuncOp funcOp, llvm::ArrayRef indices) + { + if (indices.empty()) + { + return "nothing"; + } + + auto names = funcOp->getAttrOfType(OWN_DECLARED_PARAM_NAMES_ATTR_NAME); + std::string text; + llvm::raw_string_ostream os(text); + for (auto [position, index] : llvm::enumerate(indices)) + { + os << (position ? ", " : "") << "'"; + if (names && static_cast(index) < names.size()) + { + os << mlir::cast(names[index]).getValue(); + } + else + { + os << "#" << index; + } + + os << "'"; + } + + return os.str(); + } + + // Functions this module defines that `@borrows`, and those whose state was found to borrow. + llvm::DenseMap> borrowsFound; + + // The argument of `funcOp` that `value` is, or a field of: its own value, its slot's, or a + // read out of either. -1 for anything else. + static int32_t parameterOf(mlir::Value value, mlir_ts::FuncOp funcOp) + { + auto root = rootOf(value); + if (auto loadOp = root.getDefiningOp()) + { + if (auto propertyRefOp = loadOp.getReference().getDefiningOp()) + { + return parameterOf(propertyRefOp->getOperand(0), funcOp); + } + + auto varOp = loadOp.getReference().getDefiningOp(); + int index = -1; + if (varOp && isParameterSlot(varOp, index) && varOp->getParentOfType() == funcOp) + { + return index; + } + + return -1; + } + + auto argument = mlir::dyn_cast(root); + if (argument && argument.getOwner()->isEntryBlock() && argument.getOwner()->getParentOp() == funcOp.getOperation()) + { + return static_cast(argument.getArgNumber()); + } + + return -1; + } + + // ---- The iterator protocol ---- + // + // `it.next()` returns a `{value, done}` record, and under own the caller owns what it holds, + // whoever implements it: a generator's `next` moves what it yields into the record (a yield of + // a borrow is an error in its body), and any other `next` is a function whose callers this pass + // cannot all see, which may not return a borrow. So the result is fresh even when the call goes + // through the `Iterator` interface, or into another module - the default library's `matchAll`. + + // A call of a function named `next` - read through an interface, out of a field, or by its + // symbol - whose one result is a `{value, done}` record. + static bool isIteratorNext(mlir::Operation *op) + { + if (op->getNumResults() != 1) + { + return false; + } + + auto tupleType = mlir::dyn_cast(op->getResult(0).getType()); + if (!tupleType || tupleType.getFields().size() != 2) + { + return false; + } + + auto fieldName = [&](int index) { + auto id = mlir::dyn_cast_or_null(tupleType.getFields()[index].id); + return id ? id.getValue() : llvm::StringRef(); + }; + if (fieldName(0) != "value" || fieldName(1) != "done") + { + return false; + } + + auto isNext = [](llvm::StringRef name) { return name == "next" || name.ends_with(".next"); }; + if (auto callee = directCallee(op)) + { + return isNext(callee.getValue()); + } + + auto function = calleeValue(op); + if (!function) + { + return false; + } + + if (auto getMethodOp = function.getDefiningOp()) + { + function = getMethodOp.getBoundFunc(); + } + + if (auto boundOp = function.getDefiningOp()) + { + function = boundOp.getFunc(); + } + + if (auto loadOp = function.getDefiningOp()) + { + if (auto refOp = loadOp.getReference().getDefiningOp()) + { + auto identifier = mlir::dyn_cast(refOp.getIdentifier()); + return identifier && isNext(identifier.getValue()); + } + + if (auto propertyRefOp = loadOp.getReference().getDefiningOp()) + { + auto storageType = storageTupleOfType(propertyRefOp->getOperand(0).getType()); + if (!storageType || propertyRefOp.getPosition() >= storageType.getFields().size()) + { + return false; + } + + auto id = mlir::dyn_cast_or_null(storageType.getFields()[propertyRefOp.getPosition()].id); + return id && isNext(id.getValue()); + } + + return false; + } + + if (auto refOp = function.getDefiningOp()) + { + return isNext(refOp.getIdentifier()); + } + + if (auto refOp = function.getDefiningOp()) + { + return isNext(refOp.getIdentifier()); + } + + if (auto refOp = function.getDefiningOp()) + { + return isNext(refOp.getIdentifier()); + } + + if (auto refOp = function.getDefiningOp()) + { + return isNext(refOp.getIdentifier()); + } + + return false; + } + + // A generator another module made: its state object's type is only its shape here, and what each + // field owns - a local, a yielded value now the caller's, what it borrows from its arguments - is + // known in that module alone. A release routine made here from the shape would give back what is + // not the state's. So this module's gives back none of it: only the `.captured` box's cells, and + // their values too unless the maker says it borrows them (`@borrows`). What the generator's own + // locals still hold when it is given up early leaks rather than being freed twice. + void findImportedStates(mlir::ModuleOp module) + { + llvm::MapVector states; + for (auto &call : calls) + { + if (!makesGeneratorState(call.op) || (call.callees.known && !call.callees.funcs.empty())) + { + continue; + } + + auto tupleType = storageTupleOfType(call.op->getResult(0).getType()); + auto borrows = !declaredByCallees(call.callees, OWN_DECLARED_BORROWS_ATTR_NAME).empty(); + states[tupleType] = states.lookup(tupleType) || borrows; + } + + if (states.empty()) + { + return; + } + + auto *context = &getContext(); + llvm::SmallVector entries; + if (auto existing = module->getAttrOfType(OWN_BORROWING_FIELDS_ATTR_NAME)) + { + entries.append(existing.begin(), existing.end()); + } + + for (auto &[type, borrows] : states) + { + auto tupleType = mlir::cast(type); + auto captured = fieldIndex(tupleType, CAPTURED_NAME); + // the box first, when it borrows; -1 names no field, so a box that owns is released + llvm::SmallVector fields{borrows && captured >= 0 ? captured : -1}; + for (auto [index, field] : llvm::enumerate(tupleType.getFields())) + { + if (static_cast(index) != captured && ownsHeap(module.getLoc(), field.type)) + { + fields.push_back(static_cast(index)); + } + } + + entries.push_back(mlir::ArrayAttr::get(context, {mlir::TypeAttr::get(type), mlir::DenseI32ArrayAttr::get(context, fields)})); + } + + module->setAttr(OWN_BORROWING_FIELDS_ATTR_NAME, mlir::ArrayAttr::get(context, entries)); + } + + // A call whose result is a generator's state object (an object whose fields are `.step` and + // `next`): a generator function made it, a `ts.New` block it hands over (spec 18.1), which the + // caller owns - in another module too, as the iteration that follows needs. + static bool makesGeneratorState(mlir::Operation *op) + { + if (op->getNumResults() != 1 || !mlir::isa(op->getResult(0).getType())) + { + return false; + } + + auto tupleType = storageTupleOfType(op->getResult(0).getType()); + if (!tupleType) + { + return false; + } + + auto hasStep = false; + auto hasNext = false; + for (auto field : tupleType.getFields()) + { + if (auto id = mlir::dyn_cast_or_null(field.id)) + { + hasStep = hasStep || id.getValue() == ".step"; + hasNext = hasNext || id.getValue() == "next"; + } + } + + return hasStep && hasNext; + } + + // storageTupleOf, without the pass's context: an object's storage, a reference's or a tuple + static mlir_ts::TupleType storageTupleOfType(mlir::Type type) + { + if (auto objectType = mlir::dyn_cast(type)) + { + if (auto storageType = mlir::dyn_cast(objectType.getStorageType())) + { + return mlir_ts::TupleType::get(type.getContext(), storageType.getFields()); + } + + // an object a declaration names by its shape (another module's generator) + return mlir::dyn_cast(objectType.getStorageType()); + } + + if (auto refType = mlir::dyn_cast(type)) + { + type = refType.getElementType(); + } + else if (auto valueRefType = mlir::dyn_cast(type)) + { + type = valueRefType.getElementType(); + } + + return mlir::dyn_cast(type); + } + + // ---- Fields a generator yields ---- + // + // A generator's locals are fields of its state object, and `yield groups` puts what one holds into + // the `{value, done}` record its `next` returns: a borrow of the state, which the caller cannot + // keep. When the field is read for nothing but yields, and each value stored into it is yielded + // before the generator can stop (later in the same block), the yield moves the value instead: + // the read is fresh (OWN_FRESH_RESULT_ATTR_NAME) and the field owns nothing + // (OWN_YIELDED_FIELDS_ATTR_NAME). + + // `ts.Store(value, ts.PropertyRef(record, i))`, where `record` is a local a `ts.Constant` + // initializes: a field of a record built to be returned, a yield's. + static bool isRecordFieldStore(mlir::Operation *user, mlir::Value value) + { + auto storeOp = mlir::dyn_cast(user); + auto propertyRefOp = storeOp && storeOp.getValue() == value + ? storeOp.getReference().getDefiningOp() + : mlir_ts::PropertyRefOp(); + auto recordOp = propertyRefOp ? propertyRefOp->getOperand(0).getDefiningOp() : mlir_ts::VariableOp(); + return recordOp && recordOp.getInitializer() && recordOp.getInitializer().getDefiningOp(); + } + + void findYieldedFields(mlir::ModuleOp module) + { + // (state type, field) -> its reads and its stores + struct FieldUses + { + llvm::SmallVector reads; + llvm::SmallVector stores; + bool other = false; + }; + + llvm::MapVector, FieldUses> fields; + module.walk([&](mlir_ts::PropertyRefOp propertyRefOp) { + auto tupleType = storageTupleOf(propertyRefOp->getOperand(0).getType()); + auto position = static_cast(propertyRefOp.getPosition()); + if (!tupleType || fieldIndex(tupleType, ".step") < 0 || position == fieldIndex(tupleType, CAPTURED_NAME) || + !mlir::isa(propertyRefOp.getType()) || + !ownsHeap(propertyRefOp.getLoc(), mlir::cast(propertyRefOp.getType()).getElementType())) + { + return; + } + + auto &uses = fields[{tupleType, position}]; + for (auto *user : propertyRefOp->getUsers()) + { + if (auto loadOp = mlir::dyn_cast(user)) + { + uses.reads.push_back(loadOp); + } + else if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getReference() == propertyRefOp.getResult()) + { + uses.stores.push_back(storeOp); + } + else if (!mlir::isa(user)) + { + uses.other = true; + } + } + }); + + // a field the signature pass already found borrowing is the caller's, not the generator's + llvm::DenseSet> borrowing; + if (auto entries = module->getAttrOfType(OWN_BORROWING_FIELDS_ATTR_NAME)) + { + for (auto entry : entries.getAsRange()) + { + for (auto index : mlir::cast(entry[1]).asArrayRef()) + { + borrowing.insert({mlir::cast(entry[0]).getValue(), index}); + } + } + } + + llvm::MapVector> yielded; + for (auto &[key, uses] : fields) + { + if (uses.other || uses.reads.empty() || uses.stores.empty() || borrowing.contains(key)) + { + continue; + } + + // read only to be put into a yield's record + auto onlyYielded = llvm::all_of(uses.reads, [](mlir_ts::LoadOp loadOp) { + auto users = llvm::make_filter_range(loadOp->getUsers(), [](mlir::Operation *user) { + return !mlir::isa(user); + }); + return llvm::hasSingleElement(users) && isRecordFieldStore(*users.begin(), loadOp.getResult()); + }); + + // each value stored is yielded later in its block: nothing between stops the generator + auto eachYielded = llvm::all_of(uses.stores, [&](mlir_ts::StoreOp storeOp) { + return llvm::any_of(uses.reads, [&](mlir_ts::LoadOp loadOp) { + return loadOp->getBlock() == storeOp->getBlock() && storeOp->isBeforeInBlock(loadOp); + }); + }); + + if (!onlyYielded || !eachYielded) + { + continue; + } + + yielded[key.first].push_back(key.second); + for (auto loadOp : uses.reads) + { + loadOp->setAttr(OWN_FRESH_RESULT_ATTR_NAME, mlir::UnitAttr::get(&getContext())); + } + } + + if (yielded.empty()) + { + return; + } + + auto *context = &getContext(); + llvm::SmallVector entries; + for (auto &[type, indices] : yielded) + { + entries.push_back(mlir::ArrayAttr::get(context, {mlir::TypeAttr::get(type), mlir::DenseI32ArrayAttr::get(context, indices)})); + } + + module->setAttr(OWN_YIELDED_FIELDS_ATTR_NAME, mlir::ArrayAttr::get(context, entries)); + } + + // A function that says `@borrows` and makes no state itself, but returns what a call that has + // a bounded result gives it (`return this.value.matchAll(expr)`): it borrows the parameters + // the call's bounded arguments are, or are read out of. + void findBoundedForwarders() + { + llvm::DenseMap calleesOf; + for (auto &call : calls) + { + calleesOf[call.op] = &call.callees; + } + + for (auto &entry : functions) + { + auto funcOp = entry.second; + if (funcOp.isDeclaration() || !funcOp->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME) || borrowsFound.count(funcOp)) + { + continue; + } + + llvm::SmallVector found; + auto forwards = true; + for (auto value : returnedValues(funcOp)) + { + if (holdsNoBlock(value)) + { + continue; + } + + auto *call = rootOf(value).getDefiningOp(); + auto known = call ? calleesOf.find(call) : calleesOf.end(); + if (known == calleesOf.end()) + { + forwards = false; + break; + } + + llvm::SmallVector bounded = declaredByCallees(*known->second, OWN_DECLARED_BORROWS_ATTR_NAME); + if (bounded.empty() && known->second->funcs.size() == 1 && !known->second->viaCallback) + { + auto made = declared(known->second->funcs.front(), OWN_RESULT_BOUNDED_ATTR_NAME); + bounded.assign(made.begin(), made.end()); + } + + if (bounded.empty()) + { + forwards = false; + break; + } + + auto args = callArgs(call); + for (auto index : bounded) + { + auto param = static_cast(index) < args.size() ? parameterOf(args[index], funcOp) : -1; + if (param < 0) + { + forwards = false; + break; + } + + if (!llvm::is_contained(found, param)) + { + found.push_back(param); + } + } + } + + if (forwards && !found.empty()) + { + llvm::sort(found); + borrowsFound[funcOp] = found; + } + } + } + + // A body that does other than its signature says, and an override that says other than the + // method it overrides: either way a caller that trusts the signature frees twice or leaks. + void checkDeclared() + { + for (auto &entry : functions) + { + auto funcOp = entry.second; + if (funcOp.isDeclaration()) + { + continue; + } + + if (funcOp->hasAttr(OWN_DECLARED_LENDS_ATTR_NAME) && funcOp->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME)) + { + funcOp.emitError("'") << funcOp.getSymName() + << "' cannot say both '@" LENDS_DECORATOR "' and '@" BORROWS_DECORATOR + "': its caller owns a result that borrows, and nothing of one it is lent"; + signalPassFailure(); + continue; + } + + if (declaresCallerFacts(funcOp)) + { + auto local = factsLocal.lookup(funcOp); + auto keeps = declared(funcOp, OWN_DECLARED_KEEPS_ATTR_NAME); + auto lends = declaredLends(funcOp); + if (local.borrows != lends) + { + if (lends.empty()) + { + funcOp.emitError("'") << funcOp.getSymName() << "' returns a borrow of " + << describeParams(funcOp, local.borrows) + << ", which it has to say with '@" LENDS_DECORATOR "'"; + } + else if (local.borrows.empty()) + { + funcOp.emitError("'@" LENDS_DECORATOR "' of '") + << funcOp.getSymName() << "' names " << describeParams(funcOp, lends) + << ", but its body does not return a borrow of it"; + } + else + { + funcOp.emitError("'@" LENDS_DECORATOR "' of '") + << funcOp.getSymName() << "' names " << describeParams(funcOp, lends) + << ", but its body returns a borrow of " << describeParams(funcOp, local.borrows); + } + + signalPassFailure(); + } + else if (llvm::ArrayRef(local.owned) != keeps) + { + if (funcOp->hasAttr(OWN_DECLARED_KEEPS_ATTR_NAME)) + { + funcOp.emitError("'@" KEEPS_DECORATOR "' of '") + << funcOp.getSymName() << "' names " << describeParams(funcOp, keeps) << ", but its body keeps " + << describeParams(funcOp, local.owned); + } + else + { + funcOp.emitError("'") << funcOp.getSymName() << "' keeps " << describeParams(funcOp, local.owned) + << ", which it has to say with '@" KEEPS_DECORATOR "'"; + } + + signalPassFailure(); + } + } + + if (funcOp->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME)) + { + auto says = declared(funcOp, OWN_DECLARED_BORROWS_ATTR_NAME); + auto found = borrowsFound.find(funcOp); + if (found == borrowsFound.end()) + { + funcOp.emitError("'@" BORROWS_DECORATOR "' of '") + << funcOp.getSymName() << "' names " << describeParams(funcOp, says) + << ", but it neither makes a generator whose state borrows its arguments nor returns one"; + signalPassFailure(); + } + else if (llvm::ArrayRef(found->second) != says) + { + funcOp.emitError("'@" BORROWS_DECORATOR "' of '") + << funcOp.getSymName() << "' names " << describeParams(funcOp, says) + << ", but the state it makes borrows " << describeParams(funcOp, found->second); + signalPassFailure(); + } + } + } + + // the members of a family a declaring method is in: each says the same + for (auto &[key, members] : families) + { + mlir_ts::FuncOp declaring; + for (auto member : members) + { + if (auto funcOp = functions.lookup(member.getValue()); funcOp && declaresFacts(funcOp)) + { + declaring = funcOp; + break; + } + } + + if (!declaring) + { + continue; + } + + for (auto member : members) + { + auto funcOp = functions.lookup(member.getValue()); + if (!funcOp || funcOp == declaring) + { + continue; + } + + for (auto *name : {OWN_DECLARED_KEEPS_ATTR_NAME, OWN_DECLARED_BORROWS_ATTR_NAME, OWN_DECLARED_LENDS_ATTR_NAME}) + { + if (funcOp->hasAttr(name) != declaring->hasAttr(name) || declared(funcOp, name) != declared(declaring, name)) + { + funcOp.emitError("'") << funcOp.getSymName() << "' overrides '" << declaring.getSymName() + << "', and has to say the same '@" KEEPS_DECORATOR "', '@" BORROWS_DECORATOR + "' and '@" LENDS_DECORATOR "'"; + signalPassFailure(); + break; + } + } + } + } + } + + // Functions that destroy nothing their caller can reach. + llvm::DenseSet noDrops; + + // 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 + // through an interface does not name what it reaches. + void collectClassVTables(mlir::ModuleOp module) + { + llvm::SmallVector>>> vtables; + module.walk([&](mlir_ts::GlobalOp globalOp) { + auto entries = classVTableEntries(globalOp); + if (!entries.empty()) + { + auto name = globalOp.getSymName(); + classNames.insert(name.drop_back(llvm::StringRef("..vtbl").size())); + vtables.push_back({name, std::move(entries)}); + } + }); + + for (auto &[name, entries] : vtables) + { + for (auto [position, symbol] : entries) + { + families[{position, methodName(symbol.getValue())}].push_back(symbol); + } + } + } + + // Does a method reached through an open interface keep a parameter or return a borrow + // (interfaceReached)? Then its callers through the interface cannot be told, and it has no facts + // at all. + bool factsThroughInterface(mlir::Operation *funcOp, const Facts &facts) + { + return !facts.empty() && interfaceReached.contains(funcOp); + } + + mlir_ts::TupleType storageTupleOf(mlir::Type type) + { + return own_facts::storageTupleOf(&getContext(), type); + } + + // The functions a class vtable holds, by position; empty for any other global. + static llvm::SmallVector> classVTableEntries(mlir_ts::GlobalOp globalOp) + { + llvm::SmallVector> entries; + if (!globalOp.getSymName().ends_with("..vtbl")) + { + return entries; + } + + auto isClass = false; + globalOp.getInitializerRegion().walk([&](mlir_ts::InsertPropertyOp insertOp) { + auto symbolRefOp = insertOp.getValue().getDefiningOp(); + if (!symbolRefOp || insertOp.getPosition().size() != 1) + { + return; + } + + auto symbol = symbolRefOp.getIdentifierAttr().getAttr(); + isClass = isClass || symbol.getValue().ends_with("..instanceOf"); + entries.push_back({insertOp.getPosition()[0], symbol}); + }); + + if (!isClass) + { + entries.clear(); + } + + return entries; + } + + static bool isClassVTableEntry(mlir::Operation *op) + { + auto globalOp = op->getParentOfType(); + return globalOp && !classVTableEntries(globalOp).empty(); + } + + // `B.get` -> `get`: the symbol with the longest class name that prefixes it taken off. An + // override keeps its base's method name, which is what puts both in one family. + std::string methodName(llvm::StringRef symbol) + { + size_t best = 0; + for (auto &entry : classNames) + { + auto name = entry.getKey(); + if (name.size() > best && symbol.size() > name.size() && symbol.starts_with(name) && + symbol[name.size()] == '.') + { + best = name.size() + 1; + } + } + + return symbol.drop_front(best).str(); + } + + llvm::SmallVector familyOf(int64_t index, mlir::StringAttr identifier) + { + llvm::SmallVector members; if (auto found = families.find({index, methodName(identifier.getValue())}); found != families.end()) { - members = found->second; + members = found->second; + } + + if (!llvm::is_contained(members, identifier)) + { + members.push_back(identifier); + } + + return members; + } + + // Adds `symbol`'s function to `callees`; false when it is not defined in this module. + bool addDefined(mlir::StringAttr symbol, Callees &callees) + { + auto funcOp = functions.lookup(symbol.getValue()); + if (!funcOp || funcOp.isDeclaration()) + { + return false; + } + + if (!llvm::is_contained(callees.funcs, funcOp)) + { + callees.funcs.push_back(funcOp); + } + + return true; + } + + // A function declared here and defined in a library that exported its `__own_no_drops`: one + // linked with this module, through its import library. Or a C function declared + // `@nodrops` (OWN_DECLARED_NO_DROPS_ATTR_NAME), or one whose parameters and result hold no + // heap block (scalarDeclaration). + bool addImported(mlir::StringAttr symbol, Callees &callees) + { + auto funcOp = functions.lookup(symbol.getValue()); + if (!funcOp || !funcOp.isDeclaration() || + !(importedNoDrops.contains(symbol.getValue()) || funcOp->hasAttr(OWN_DECLARED_NO_DROPS_ATTR_NAME) || + (scalarDeclaration(funcOp) && !functionsEscape))) + { + return false; + } + + callees.imported = true; + callees.importedName = symbol.getValue(); + return true; + } + + // A number, a boolean, a character, or an enum or a literal of one: a value that holds no block + // and no function of this module, so given to foreign code it hands it neither. Not an opaque + // pointer, a reference, a string, a class, an interface, an object, a tuple, an array, a union + // with such a member, a function, `any` or `unknown`. + static bool scalarType(mlir::Type type) + { + if (auto literalType = mlir::dyn_cast(type)) + { + return scalarType(literalType.getElementType()); + } + + if (auto enumType = mlir::dyn_cast(type)) + { + return scalarType(enumType.getElementType()); + } + + return mlir::isa(type); + } + + // Every parameter and result scalar (a void result too), and no rest parameter. + static bool scalarSignature(mlir_ts::FunctionType functionType) + { + return !functionType.isVarArg() && llvm::all_of(functionType.getInputs(), scalarType) && + llvm::all_of(functionType.getResults(), + [](mlir::Type type) { return mlir::isa(type) || scalarType(type); }); + } + + // A body-less `declare function` of C (no `export` or import marker: not another TypeScript + // module's, whose declarations carry `export`) whose parameters and result are scalar + // (scalarType): given no block of this module, it can destroy none (controller ruling, spec 31: + // `sqrt`, `floor`, `pow`, `fabs`). The risk is a C function that calls back into the module + // (`signal(15, f as Opaque)` then `raise(15)`, review6r1 c3), which it can only do with a + // function it was given - so the rule is off in a module whose foreign surface could hand it + // one (functionsEscape) - or an exported function reached by name, which a `@nodrops` + // declaration takes on as well. + static bool scalarDeclaration(mlir_ts::FuncOp funcOp) + { + return funcOp.isDeclaration() && !funcOp->hasAttr("export") && !funcOp->hasAttr("import") && + !funcOp->hasAttr(DLL_EXPORT) && !funcOp->hasAttr(DLL_IMPORT) && scalarSignature(funcOp.getFunctionType()); + } + + // `ts.Load(ts.AddressOf @f)`, where the global `f` holds the address of a function exported + // by a library loaded at run time, looked up by name: that function, if the library exported + // its `__own_no_drops`. + bool addImportedByAddress(mlir::Value callee, Callees &callees) + { + auto loadOp = callee.getDefiningOp(); + auto addressOfOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp(); + if (!addressOfOp) + { + return false; + } + + auto globalOp = globals.lookup(addressOfOp.getGlobalName()); + if (!globalOp || writtenGlobals.contains(addressOfOp.getGlobalName())) + { + return false; + } + + // the global is never anything but that address: `Cast(SearchForAddressOfSymbol(name))` + auto ®ion = globalOp.getInitializerRegion(); + if (!region.hasOneBlock()) + { + return false; + } + + auto resultOp = mlir::dyn_cast(region.front().getTerminator()); + auto castOp = resultOp && resultOp->getNumOperands() == 1 + ? resultOp->getOperand(0).getDefiningOp() + : mlir_ts::CastOp(); + auto searchOp = castOp ? castOp.getIn().getDefiningOp() + : mlir_ts::SearchForAddressOfSymbolOp(); + auto nameOp = searchOp ? searchOp->getOperand(0).getDefiningOp() : mlir_ts::ConstantOp(); + auto name = nameOp ? mlir::dyn_cast(nameOp.getValue()) : mlir::StringAttr(); + if (!name || !importedNoDrops.contains(name.getValue())) + { + return false; + } + + callees.imported = true; + callees.importedName = name.getValue(); + return true; + } + + Callees resolve(mlir::Operation *op) + { + Callees callees; + if (auto callee = directCallee(op)) + { + callees.named.push_back(callee.getAttr()); + callees.known = addDefined(callee.getAttr(), callees) || addImported(callee.getAttr(), callees); + } + else if (mlir::isa(op)) + { + resolveValue(calleeValue(op), callees); + } + + if (!callees.known) + { + callees.funcs.clear(); + } + + return callees; + } + + // The function value a call goes through: a method of a class, virtual or not, a symbol, or + // the `.instanceOf` slot. Anything else - a closure, a function read from a field, an + // interface's method - is unknown. + void resolveValue(mlir::Value callee, Callees &callees) + { + // `ts.Load(ts.AddressOf @g)`: a dynamic import's global, which may say what the function + // keeps; a method of a class imported so is bound to its object first + auto function = callee; + if (auto getMethodOp = function.getDefiningOp()) + { + function = getMethodOp.getBoundFunc(); + } + + if (auto boundOp = function.getDefiningOp()) + { + function = boundOp.getFunc(); + } + + auto loadOp = function.getDefiningOp(); + if (auto addressOfOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp(); + addressOfOp && !writtenGlobals.contains(addressOfOp.getGlobalName())) + { + callees.named.push_back(addressOfOp.getGlobalNameAttr().getAttr()); + } + + if (addImportedByAddress(callee, callees)) + { + callees.known = true; + return; + } + + if (auto getMethodOp = callee.getDefiningOp()) + { + callee = getMethodOp.getBoundFunc(); + } + + auto *def = callee.getDefiningOp(); + mlir::StringAttr identifier; + int64_t index = -1; + if (auto refOp = mlir::dyn_cast_or_null(def)) + { + identifier = refOp.getIdentifierAttr().getAttr(); + index = refOp.getIndex(); + } + else if (auto refOp = mlir::dyn_cast_or_null(def)) + { + identifier = refOp.getIdentifierAttr().getAttr(); + index = refOp.getIndex(); + } + else if (auto refOp = mlir::dyn_cast_or_null(def)) + { + identifier = refOp.getIdentifierAttr().getAttr(); + } + else if (auto refOp = mlir::dyn_cast_or_null(def)) + { + identifier = refOp.getIdentifierAttr().getAttr(); + } + else if (isInstanceOfSlot(def)) + { + callees.known = true; + callees.instanceOf = true; + return; + } + else if (auto symbol = fieldFunctions.of(def)) + { + callees.known = addDefined(symbol, callees); + callees.fieldFunction = callees.known; + return; + } + else if (auto interfaceRefOp = mlir::dyn_cast_or_null(def)) + { + resolveInterfaceSlot(interfaceRefOp, interfaceFamilies, callees); + return; + } + else if (auto loadOp = mlir::dyn_cast_or_null(def)) + { + resolveCallback(loadOp, callees); + return; + } + else if (auto boundOp = mlir::dyn_cast_or_null(def)) + { + // a function-typed field of an interface, called: `ts.CreateBoundFunction( + // ts.ExtractInterfaceThis(i), ts.Load(ts.InterfaceSymbolRef(i, f)))` + auto loadOp = boundOp.getFunc().getDefiningOp(); + auto refOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::InterfaceSymbolRefOp(); + auto thisOp = boundOp.getThisVal().getDefiningOp(); + if (refOp && isInterfaceField(refOp.getResult()) && thisOp && thisOp.getInterfaceVal() == refOp.getInterfaceVal()) + { + resolveInterfaceSlot(refOp, interfaceFieldFamilies, callees); + } + + return; + } + + if (!identifier) + { + return; + } + + if (index < 0) + { + callees.named.push_back(identifier); + callees.known = addDefined(identifier, callees) || addImported(identifier, callees); + return; + } + + callees.named = familyOf(index, identifier); + resolveFamily(index, identifier, callees); + } + + // A method of a closed interface (interfaceFamilies), `ts.InterfaceSymbolRef(value, index)`, or a + // function-typed field of one (interfaceFieldFamilies), read and called: the family of the slot + // after the vtable's header - every function the vtables hold there, or every function the + // implementing literals' fields are ever given. Open, the call is unknown, as before. + template + void resolveInterfaceSlot(mlir_ts::InterfaceSymbolRefOp refOp, const std::map, Family> &families, + Callees &callees) + { + auto interfaceType = mlir::dyn_cast(refOp.getInterfaceVal().getType()); + auto identifier = mlir::dyn_cast(refOp.getIdentifier()); + if (!interfaceType || !identifier) + { + return; + } + + auto position = static_cast(refOp.getIndex()) + INTERFACE_VTABLE_HEADER_SLOTS; + auto found = families.find({interfaceType.getName().getValue().str(), position}); + if (found == families.end() || !found->second.closed || found->second.field != identifier || + found->second.members.empty()) + { + return; + } + + callees.named.assign(found->second.members.begin(), found->second.members.end()); + callees.viaInterface = true; + callees.known = true; + for (auto member : found->second.members) + { + callees.known = addDefined(member, callees) && callees.known; + } + } + + // Can a module other than this one call the function? A `public` method (not private, not + // exported itself) of a class no other module can name - not exported, not a word of the + // exported declarations, not in the type of an exported or declared function or of an exported + // or imported global (nameableClasses) - no: another module calls a function of this one through + // the declaration `__decls` gives it (controller ruling, spec 31). But a method of a class that + // extends a class another module can name, or one another module defines, yes, private or not: + // that module calls it through the base's vtable. + bool callableFromOutside(mlir_ts::FuncOp funcOp) + { + if (!funcOp.isPrivate() && (funcOp.isDeclaration() || funcOp->hasAttr("export") || funcOp->hasAttr(DLL_EXPORT))) + { + return true; + } + + // its class: the longest own class name that prefixes its symbol + auto symbol = funcOp.getSymName(); + llvm::StringRef owner; + for (auto &entry : ownClasses) + { + auto name = entry.getKey(); + if (name.size() > owner.size() && symbol.size() > name.size() && symbol.starts_with(name) && symbol[name.size()] == '.') + { + owner = name; + } + } + + if (owner.empty()) + { + return !funcOp.isPrivate(); + } + + auto nameable = [&](llvm::StringRef name) { + auto last = name.rsplit('.').second; + return nameableClasses.contains(name) || exportWords.contains(name) || (!last.empty() && exportWords.contains(last)); + }; + + if (!funcOp.isPrivate() && nameable(owner)) + { + return true; + } + + // An override of a method of a nameable class, or of one another module defines (no own + // vtable here), fills a slot of that class's vtable: another module calls it through the + // base (review6r1 p5, p6, p7; controller ruling: any base of that kind on the chain). The + // base is the first field of a derived class's storage. + auto storage = classStorages.lookup(owner); + if (!storage) + { + // its storage is not known here: no telling what it extends + return !funcOp.isPrivate(); + } + + for (auto depth = 0; depth < 64; ++depth) + { + auto fields = storage.getFields(); + auto base = fields.empty() ? mlir_ts::ClassStorageType() : mlir::dyn_cast(fields.front().type); + if (!base) + { + return false; + } + + auto name = base.getName().getValue(); + if (nameable(name) || !ownClasses.contains(name)) + { + return true; + } + + storage = base; + } + + return true; + } + + // Can foreign code learn of a function of this module, to call it back during a call of a + // scalar declaration (scalarDeclaration)? It can only through the module's foreign surface, or + // through a function turned into data (controller ruling, spec 31, fix round 5: a closed-world + // test, in place of listing the routes a function value takes out). So the rule is on only when: + // - every foreign symbol is scalar (scalarType): every body-less function (C's, another + // module's - `import`, a DLL's - a `declare class`'s, a `@cpp` class's slot; a rest parameter + // is not scalar), every global declared with no initializer or imported (a `declare var`, a + // `declare class`'s vtable and type name), every `@cpp` class's fields and its bases', with + // no method of its own (C++ calls one through the C++ vtable), every function found in a + // library at run time (`SearchForAddressOfSymbol`, seen through its cast); an opaque pointer + // or a reference anywhere in such a type is a value of foreign memory, not scalar; + // - no cast makes an opaque pointer or a reference out of a number (`0x1234`, then a + // store through it reaches foreign memory); + // - no cast makes a value of a type that holds no function (`any`, `unknown`, an opaque + // pointer, an integer, a reference; a boolean cannot carry one and does not count) out of one + // that holds one (`onSig as Opaque`, `k.m as u64`, `>k`; a class or object + // takes its fields along unless seen as a class or an interface); + // - no `inline_asm` or `call_intrinsic` is given a value whose type holds a function. + bool functionsEscape = false; + + // The types a holdsFunction walk has been through, with or without their fields. + using SeenTypes = llvm::DenseSet>; + + // Does a value of this type hold a function: the type itself, a union's or an optional's + // member, a tuple's field, an array's element, what a reference points to, an interface whose + // vtable holds one past its header - a method, a function-typed field, or a field whose type + // holds one, recursively (an interface with methods, a class with a function field, an array of + // them) - or whose vtable this module does not make values with (a foreign one: `declare + // function makeW(): W`), and with `fields` a class's or an object's fields. The header slot + // (`.instanceOf`, the compiler's own, which destroys nothing) does not count. + bool holdsFunction(mlir::Type type, bool fields, SeenTypes &seen) + { + if (!type || !seen.insert({type, fields}).second) + { + return false; + } + + if (mlir::isa(type)) + { + return true; + } + + if (auto interfaceType = mlir::dyn_cast(type)) + { + auto found = interfaceVTableTypes.find(interfaceType.getName().getValue()); + if (found == interfaceVTableTypes.end()) + { + return true; + } + + return llvm::any_of(found->second, [&](mlir_ts::TupleType vtableType) { + return llvm::any_of(llvm::drop_begin(vtableType.getFields(), 1), + [&](const mlir_ts::FieldInfo &info) { return holdsFunction(info.type, /*fields=*/true, seen); }); + }); + } + + auto any = [&](llvm::ArrayRef infos) { + return llvm::any_of(infos, [&](const mlir_ts::FieldInfo &info) { return holdsFunction(info.type, fields, seen); }); + }; + + if (auto tupleType = mlir::dyn_cast(type)) + { + return any(tupleType.getFields()); + } + + if (auto constTupleType = mlir::dyn_cast(type)) + { + return any(constTupleType.getFields()); + } + + if (auto classStorageType = mlir::dyn_cast(type)) + { + return fields && any(classStorageType.getFields()); + } + + if (auto objectStorageType = mlir::dyn_cast(type)) + { + return fields && any(objectStorageType.getFields()); + } + + if (mlir::isa(type) && !fields) + { + return false; + } + + auto found = false; + type.walkImmediateSubElements([](mlir::Attribute) {}, [&](mlir::Type sub) { found = found || holdsFunction(sub, fields, seen); }); + return found; + } + + // The `@cpp` class storage a method's `this` is of, if any: the storage of its first + // parameter's class, when the method's symbol is `Class.name`. + mlir_ts::ClassStorageType cppClassOfMethod(mlir_ts::FuncOp funcOp) + { + auto inputs = funcOp.getFunctionType().getInputs(); + auto classType = inputs.empty() ? mlir_ts::ClassType() : mlir::dyn_cast(inputs.front()); + auto storage = classType ? mlir::dyn_cast(classType.getStorageType()) : mlir_ts::ClassStorageType(); + if (!storage || !storage.isCpp()) + { + return {}; + } + + auto name = storage.getName().getValue(); + auto symbol = funcOp.getSymName(); + return symbol.size() > name.size() && symbol.starts_with(name) && symbol[name.size()] == '.' ? storage : mlir_ts::ClassStorageType(); + } + + // Does a value of this type carry an address of memory this module made - so a cast of it to + // an opaque pointer or a reference points into this module, not into foreign memory? + static bool addressType(mlir::Type type) + { + return mlir::isa(type); + } + + void findEscapingFunctions(mlir::ModuleOp module) + { + // every body-less function: C's, another module's, a `declare class`'s (its methods, its + // `..instanceOf`, its `..new`), a `@cpp` class's slot + for (auto &entry : functions) + { + auto funcOp = entry.second; + if (funcOp.isDeclaration() && !scalarSignature(funcOp.getFunctionType())) + { + functionsEscape = true; + return; + } + + // a method of a `@cpp` class: C++ calls it through the class's C++ vtable + if (cppClassOfMethod(funcOp)) + { + functionsEscape = true; + return; + } + } + + // every global declared with no initializer or imported: a `declare var`, a `declare + // class`'s vtable (functions) and type name (a string) + for (auto &entry : globals) + { + auto globalOp = entry.second; + auto foreign = globalOp->hasAttr("import") || globalOp->hasAttr(DLL_IMPORT) || globalOp.getInitializerRegion().empty() || + globalOp.getInitializerRegion().front().empty(); + if (foreign && !scalarType(globalOp.getType())) + { + functionsEscape = true; + return; + } + } + + // every `@cpp` class's fields; a base (the first field, a storage) is a `@cpp` class of its + // own, judged on its own + for (auto &entry : classStorages) + { + auto storage = entry.second; + if (!storage.isCpp()) + { + continue; + } + + for (auto [index, field] : llvm::enumerate(storage.getFields())) + { + auto base = index == 0 ? mlir::dyn_cast(field.type) : mlir_ts::ClassStorageType(); + if (base ? !base.isCpp() : !scalarType(field.type)) + { + functionsEscape = true; + return; + } + } + } + + module.walk([&](mlir::Operation *op) { + if (functionsEscape) + { + return; + } + + // a function found in a library at run time: scalar through every cast of its address + if (mlir::isa(op)) + { + functionsEscape = !llvm::all_of(op->getResult(0).getUsers(), [](mlir::Operation *user) { + auto castOp = mlir::dyn_cast(user); + auto functionType = castOp ? mlir::dyn_cast(castOp.getType()) : mlir_ts::FunctionType(); + return functionType && scalarSignature(functionType); + }); + return; + } + + // `inline_asm(...)` and `call_intrinsic(...)`: a function given to either is data + // of no type (review6r5 h1, h2: `inline_asm("mov $1, $0", "=r,r", onSig)`) + if (mlir::isa(op)) + { + functionsEscape = llvm::any_of(op->getOperands(), [&](mlir::Value operand) { + SeenTypes seen; + return holdsFunction(operand.getType(), /*fields=*/true, seen); + }); + return; + } + + auto castOp = mlir::dyn_cast(op); + if (!castOp) + { + return; + } + + // a vtable's address (a class's, an interface's, a thunk's cell) seen as an opaque + // pointer or an integer: the compiler's own, inside a vtable or storing an object's + // vtable, never a value a program gives C + auto vtableOp = op->getParentOfType(); + auto addressOfOp = addressOfThroughCasts(castOp.getIn()); + auto isVTable = [](llvm::StringRef name) { return name.ends_with("..vtbl") || name.contains("..vtbl."); }; + if ((vtableOp && isVTable(vtableOp.getSymName())) || (addressOfOp && isVTable(addressOfOp.getGlobalName()))) + { + return; + } + + // an opaque pointer or a reference made out of a number: an address of foreign memory + if (mlir::isa(castOp.getType()) && !addressType(castOp.getIn().getType())) + { + functionsEscape = true; + return; + } + + // a function seen as something that holds none; a boolean cannot carry one. A class or + // object takes its fields along, seen as anything (`any`, an opaque pointer, an integer, + // a reference: `>k`, then a load of the field, review6r5 g2) but a class + // (its base) or an interface (a view of it, read through its vtable only) + if (mlir::isa(castOp.getType())) + { + return; + } + + auto fields = !mlir::isa(castOp.getType()); + SeenTypes seenIn; + SeenTypes seenOut; + functionsEscape = holdsFunction(castOp.getIn().getType(), fields, seenIn) && + !holdsFunction(castOp.getType(), /*fields=*/true, seenOut); + }); + } + + // class name -> its storage, from every class type a function's signature or a global mentions + llvm::StringMap classStorages; + + void collectClassStorages(mlir::ModuleOp module) + { + llvm::DenseSet seen; + std::function visit = [&](mlir::Type type) { + if (!type || !seen.insert(type).second) + { + return; + } + + if (auto storage = mlir::dyn_cast(type)) + { + classStorages[storage.getName().getValue()] = storage; + for (auto &field : storage.getFields()) + { + visit(field.type); + } + + return; + } + + type.walkImmediateSubElements([](mlir::Attribute) {}, visit); + }; + + for (auto &entry : functions) + { + visit(entry.second.getFunctionType()); + } + + module.walk([&](mlir::Operation *op) { + for (auto result : op->getResults()) + { + if (mlir::isa(result.getType())) + { + visit(result.getType()); + } + } + }); + } + + // A class another module can see may be extended there, and its override is a candidate this + // module never sees: every member has to be private and defined here. + void resolveFamily(int64_t index, mlir::StringAttr identifier, Callees &callees) + { + callees.known = true; + for (auto member : familyOf(index, identifier)) + { + callees.known = addDefined(member, callees) && callees.known; + } + + callees.known = callees.known && llvm::all_of(callees.funcs, [&](mlir_ts::FuncOp funcOp) { return !callableFromOutside(funcOp); }); + } + + // `ts.Cast(ts.VTableOffsetRef(vtable, 0))`, where `vtable` is the one an object's first word + // points to or an interface value's own: slot 0 of either is `.instanceOf` - a class's + // generated one, a string compare, or `.instanceOf.none` for an object literal behind an + // interface. Only mlirGenInstanceOfThroughVTable builds a call through it. Neither destroys + // anything. + static bool isInstanceOfSlot(mlir::Operation *def) + { + auto castOp = mlir::dyn_cast_or_null(def); + auto slot = castOp ? castOp.getIn().getDefiningOp() : mlir_ts::VTableOffsetRefOp(); + if (!slot || slot.getIndex() != INTERFACE_VTABLE_INSTANCEOF_SLOT) + { + return false; + } + + // a class vtable, read out of an object's first word, or an interface's own vtable, + // whose slot 0 is its implementer's `.instanceOf` (INTERFACE_VTABLE_HEADER_SLOTS) + auto *vtable = slot.getVtable().getDefiningOp(); + if (auto classVTable = mlir::dyn_cast_or_null(vtable)) + { + return classVTable.getIndex() == 0; + } + + return mlir::isa_and_nonnull(vtable); + } + + // Is every use of this function value the callee of a call - through `ts.GetMethod`, with + // `ts.GetThis` reading the object beside it? + static bool onlyCalled(mlir::Value value) + { + for (auto &use : value.getUses()) + { + auto *user = use.getOwner(); + if (mlir::isa(user) && + calleeValue(user) == use.get() && use.getOperandNumber() == 0) + { + continue; + } + + if (mlir::isa(user)) + { + continue; + } + + if (mlir::isa(user) && onlyCalled(user->getResult(0))) + { + continue; + } + + return false; + } + + return true; + } + + // The closed world. A function is open when a caller this pass cannot see may reach it: it is + // not private (another module may call it), or its symbol is used other than by a direct call, + // a class vtable's entry, or a method reference used only as a callee. A virtual reference + // that escapes opens its whole family. + void findOpen(mlir::ModuleOp module) + { + for (auto &entry : functions) + { + if (callableFromOutside(entry.second)) + { + open.try_emplace(entry.second, "it can be called from another module; '@" LENDS_DECORATOR "' or '@" KEEPS_DECORATOR + "' would say it in its signature"); + } } - if (!llvm::is_contained(members, identifier)) + // the module is a symbol table, which getSymbolUses does not look inside: walk its body + auto uses = mlir::SymbolTable::getSymbolUses(&module.getBodyRegion()); + if (!uses) { - members.push_back(identifier); + // an op this pass cannot read symbols through: nothing is closed + for (auto &entry : functions) + { + open.try_emplace(entry.second, "its callers cannot all be found"); + } + + return; } - return members; + for (auto &use : *uses) + { + auto symbol = use.getSymbolRef().getRootReference(); + auto funcOp = functions.lookup(symbol.getValue()); + if (!funcOp) + { + continue; + } + + auto *user = use.getUser(); + if (mlir::isa(user)) + { + continue; + } + + if (mlir::isa(user) && isClassVTableEntry(user)) + { + continue; + } + + // passed only to a parameter whose family is closed: its calls are resolved (spec 33) + if (callbackPassing.contains(user)) + { + continue; + } + + int64_t index = -1; + if (auto refOp = mlir::dyn_cast(user)) + { + index = refOp.getIndex(); + } + else if (auto refOp = mlir::dyn_cast(user)) + { + index = refOp.getIndex(); + } + + auto inFunction = !!user->getParentOfType(); + if (inFunction && mlir::isa(user) && + onlyCalled(user->getResult(0))) + { + // a virtual call this pass cannot resolve reaches its family without the facts + if (index >= 0) + { + Callees callees; + resolveFamily(index, symbol, callees); + if (!callees.known) + { + openFamily(index, symbol, "an override may be defined in another module"); + } + } + + continue; + } + + open.try_emplace(funcOp, "it is used other than by a call"); + if (index >= 0) + { + openFamily(index, symbol, "it is used other than by a call"); + } + } } - // Adds `symbol`'s function to `callees`; false when it is not defined in this module. - bool addDefined(mlir::StringAttr symbol, Callees &callees) + void openFamily(int64_t index, mlir::StringAttr symbol, const char *why) { - auto funcOp = functions.lookup(symbol.getValue()); - if (!funcOp || funcOp.isDeclaration()) + for (auto member : familyOf(index, symbol)) { - return false; + if (auto memberOp = functions.lookup(member.getValue())) + { + open.try_emplace(memberOp, why); + } } + } - if (!llvm::is_contained(callees.funcs, funcOp)) - { - callees.funcs.push_back(funcOp); - } + // ---- Callbacks (spec 33) ---- + // + // `___array_reduce(arr, f, init)` calls its parameter `f`: an indirect call, which reaches + // whatever function its callers pass. When the function is closed - private, and every use of its + // symbol a direct call - the parameter is only ever called, and every caller passes there a + // function this module defines, by its symbol or as a closure over a capture box made right + // there, a call through the parameter reaches exactly those functions: the parameter's family. + // Its members are closed too, when their symbols are used only to be called directly or passed + // so; then the call gets the facts the members agree on (callFacts), and `__own_no_drops` when + // none of them destroys anything. A closure's body takes its capture box first: its parameter k + // is the call's argument k - 1 (CallbackMember::shift). + // + // A kept parameter is agreed on the other way round from a class family's (meetCallbacks): a + // member that only borrows a parameter another member keeps is given it to keep too, and releases + // it on every path (OWN_PARAMS_RELEASED_ATTR_NAME) - `reduce` with a callback that returns its + // accumulator on some paths and one that never does, in one `___array_reduce`. Anything + // else the members disagree on demotes them all, as a class family's members are. - return true; + struct CallbackMember + { + mlir::StringAttr symbol; + int32_t shift = 0; + }; + + struct CallbackFamily + { + llvm::SmallVector members; + // the ops that name a member's symbol to pass it here + llvm::SmallVector passing; + bool closed = true; + }; + + // (function, parameter index) -> the functions its callers pass there + std::map, CallbackFamily> callbackFamilies; + // the members whose families disagreed (meetFacts), for the error in their bodies + llvm::DenseSet demotedCallbacks; + // uses of a member's symbol that only pass it to a closed family: no escape (findOpen) + llvm::DenseSet callbackPassing; + + static bool isFunctionValueType(mlir::Type type) + { + return mlir::isa(type); } - // A function declared here and defined in a library that exported its `__own_no_drops`: one - // linked with this module, through its import library. Or a C function declared - // `@nodrops` (OWN_DECLARED_NO_DROPS_ATTR_NAME). - bool addImported(mlir::StringAttr symbol, Callees &callees) + // Is every use of this parameter - through its slot, never assigned or captured - the function + // a call goes through? + static bool onlyCalledParam(mlir::BlockArgument argument) { - auto funcOp = functions.lookup(symbol.getValue()); - if (!funcOp || !funcOp.isDeclaration() || - !(importedNoDrops.contains(symbol.getValue()) || funcOp->hasAttr(OWN_DECLARED_NO_DROPS_ATTR_NAME))) + for (auto *user : argument.getUsers()) { - return false; + auto varOp = mlir::dyn_cast(user); + auto index = -1; + if (!varOp || !isParameterSlot(varOp, index)) + { + return false; + } + + for (auto *slotUser : varOp->getUsers()) + { + if (mlir::isa(slotUser)) + { + continue; + } + + auto loadOp = mlir::dyn_cast(slotUser); + if (!loadOp) + { + return false; + } + + for (auto &use : loadOp->getUses()) + { + auto *call = use.getOwner(); + if (!isCall(call) || calleeValue(call) != use.get() || + llvm::is_contained(callArgs(call), use.get())) + { + return false; + } + } + } } - callees.imported = true; - callees.importedName = symbol.getValue(); return true; } - // `ts.Load(ts.AddressOf @f)`, where the global `f` holds the address of a function exported - // by a library loaded at run time, looked up by name: that function, if the library exported - // its `__own_no_drops`. - bool addImportedByAddress(mlir::Value callee, Callees &callees) + // The function a caller passes as `value`: its symbol, seen as a function value through casts + // between function types, or a closure over a capture box built of it. Null for anything else. + static mlir_ts::SymbolRefOp passedFunction(mlir::Value value, int32_t &shift) { - auto loadOp = callee.getDefiningOp(); - auto addressOfOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp(); - if (!addressOfOp) + while (auto castOp = value.getDefiningOp()) { - return false; + if (!isFunctionValueType(castOp.getIn().getType()) || !isFunctionValueType(castOp.getType())) + { + return {}; + } + + value = castOp.getIn(); } - auto globalOp = globals.lookup(addressOfOp.getGlobalName()); - if (!globalOp || writtenGlobals.contains(addressOfOp.getGlobalName())) + if (auto refOp = value.getDefiningOp()) { - return false; + shift = 0; + return refOp; } - // the global is never anything but that address: `Cast(SearchForAddressOfSymbol(name))` - auto ®ion = globalOp.getInitializerRegion(); - if (!region.hasOneBlock()) + auto boundOp = value.getDefiningOp(); + if (!boundOp || closureOfBox(boundOp.getThisVal()) != boundOp) + { + return {}; + } + + shift = 1; + return boundOp.getFunc().getDefiningOp(); + } + + // Does the function value `value` go only where `passes` allows - as an argument of a call - and + // through casts between function types, a closure built of it, and that closure's own retains + // and releases? + static bool passedOnly(mlir::Value value, llvm::function_ref passes, int depth = 0) + { + if (depth > 8) { return false; } - auto resultOp = mlir::dyn_cast(region.front().getTerminator()); - auto castOp = resultOp && resultOp->getNumOperands() == 1 - ? resultOp->getOperand(0).getDefiningOp() - : mlir_ts::CastOp(); - auto searchOp = castOp ? castOp.getIn().getDefiningOp() - : mlir_ts::SearchForAddressOfSymbolOp(); - auto nameOp = searchOp ? searchOp->getOperand(0).getDefiningOp() : mlir_ts::ConstantOp(); - auto name = nameOp ? mlir::dyn_cast(nameOp.getValue()) : mlir::StringAttr(); - if (!name || !importedNoDrops.contains(name.getValue())) + for (auto &use : value.getUses()) { + auto *user = use.getOwner(); + if (mlir::isa(user) || passes(use)) + { + continue; + } + + auto castOp = mlir::dyn_cast(user); + auto boundOp = mlir::dyn_cast(user); + if (castOp && isFunctionValueType(castOp.getType()) && passedOnly(castOp.getResult(), passes, depth + 1)) + { + continue; + } + + if (boundOp && boundOp.getFunc() == use.get() && closureOfBox(boundOp.getThisVal()) == boundOp && + passedOnly(boundOp.getResult(), passes, depth + 1)) + { + continue; + } + return false; } - callees.imported = true; - callees.importedName = name.getValue(); return true; } - Callees resolve(mlir::Operation *op) + void collectCallbackFamilies(mlir::ModuleOp module) { - Callees callees; - if (auto callee = directCallee(op)) + auto uses = mlir::SymbolTable::getSymbolUses(&module.getBodyRegion()); + if (!uses) { - callees.named.push_back(callee.getAttr()); - callees.known = addDefined(callee.getAttr(), callees) || addImported(callee.getAttr(), callees); + return; } - else if (mlir::isa(op)) + + llvm::DenseMap> usersOf; + for (auto &use : *uses) { - resolveValue(calleeValue(op), callees); + if (auto funcOp = functions.lookup(use.getSymbolRef().getRootReference().getValue())) + { + usersOf[funcOp].push_back(use.getUser()); + } } - if (!callees.known) + auto callsOnly = [&](mlir_ts::FuncOp funcOp) { + auto &users = usersOf[funcOp]; + return !users.empty() && llvm::all_of(users, [&](mlir::Operation *user) { + auto callee = directCallee(user); + return callee && callee.getValue() == funcOp.getSymName(); + }); + }; + + for (auto &entry : functions) { - callees.funcs.clear(); + auto funcOp = entry.second; + if (funcOp.isDeclaration() || callableFromOutside(funcOp) || declaresFacts(funcOp) || !callsOnly(funcOp)) + { + continue; + } + + for (auto argument : funcOp.getBody().front().getArguments()) + { + if (!isFunctionValueType(argument.getType()) || !onlyCalledParam(argument)) + { + continue; + } + + auto index = static_cast(argument.getArgNumber()); + CallbackFamily family; + for (auto *user : usersOf[funcOp]) + { + auto args = callArgs(user); + auto shift = 0; + auto refOp = static_cast(index) < args.size() ? passedFunction(args[index], shift) + : mlir_ts::SymbolRefOp(); + auto member = refOp ? functions.lookup(refOp.getIdentifier()) : mlir_ts::FuncOp(); + if (!member || member.isDeclaration() || callableFromOutside(member) || declaresFacts(member)) + { + family.closed = false; + break; + } + + auto symbol = member.getSymNameAttr(); + auto same = llvm::find_if(family.members, [&](const CallbackMember &known) { return known.symbol == symbol; }); + if (same != family.members.end() && same->shift != shift) + { + family.closed = false; + break; + } + + if (same == family.members.end()) + { + family.members.push_back({symbol, shift}); + } + + family.passing.push_back(refOp); + } + + callbackFamilies[{funcOp, index}] = std::move(family); + } } - return callees; - } + // A member's symbol used anywhere else - a call through another value, a field, a vtable, a + // global - reaches callers this cannot see; so does one passed to a family that is not closed. + // Each family that has such a member is open, until nothing changes. + auto passesToClosed = [&](mlir::OpOperand &use) { + auto *call = use.getOwner(); + auto callee = directCallee(call); + auto calleeOp = callee ? functions.lookup(callee.getValue()) : mlir_ts::FuncOp(); + if (!calleeOp) + { + return false; + } - // The function value a call goes through: a method of a class, virtual or not, a symbol, or - // the `.instanceOf` slot. Anything else - a closure, a function read from a field, an - // interface's method - is unknown. - void resolveValue(mlir::Value callee, Callees &callees) - { - // `ts.Load(ts.AddressOf @g)`: a dynamic import's global, which may say what the function - // keeps; a method of a class imported so is bound to its object first - auto function = callee; - if (auto getMethodOp = function.getDefiningOp()) + // the argument this use is, by its operand's position + auto args = callArgs(call); + if (args.empty() || use.getOperandNumber() < args.getBeginOperandIndex() || + use.getOperandNumber() >= args.getBeginOperandIndex() + args.size()) + { + return false; + } + + auto index = static_cast(use.getOperandNumber() - args.getBeginOperandIndex()); + auto found = callbackFamilies.find({calleeOp, index}); + return found != callbackFamilies.end() && found->second.closed; + }; + + for (auto changed = true; changed;) { - function = getMethodOp.getBoundFunc(); - } + changed = false; + for (auto &[key, family] : callbackFamilies) + { + if (!family.closed) + { + continue; + } + + for (auto &member : family.members) + { + auto memberOp = functions.lookup(member.symbol.getValue()); + auto escapes = llvm::any_of(usersOf[memberOp], [&](mlir::Operation *user) { + if (auto callee = directCallee(user); callee && callee.getValue() == member.symbol.getValue()) + { + return false; + } - if (auto boundOp = function.getDefiningOp()) - { - function = boundOp.getFunc(); + auto refOp = mlir::dyn_cast(user); + return !refOp || !user->getParentOfType() || + !passedOnly(refOp.getResult(), passesToClosed); + }); + + if (escapes) + { + family.closed = false; + changed = true; + break; + } + } + } } - auto loadOp = function.getDefiningOp(); - if (auto addressOfOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp(); - addressOfOp && !writtenGlobals.contains(addressOfOp.getGlobalName())) + for (auto &[key, family] : callbackFamilies) { - callees.named.push_back(addressOfOp.getGlobalNameAttr().getAttr()); + if (family.closed) + { + callbackPassing.insert(family.passing.begin(), family.passing.end()); + } } + } - if (addImportedByAddress(callee, callees)) + // A call through a parameter whose family is closed: its members. + void resolveCallback(mlir_ts::LoadOp loadOp, Callees &callees) + { + auto varOp = loadOp.getReference().getDefiningOp(); + auto funcOp = loadOp->getParentOfType(); + auto index = -1; + if (!varOp || !funcOp || !isParameterSlot(varOp, index)) { - callees.known = true; return; } - if (auto getMethodOp = callee.getDefiningOp()) + auto found = callbackFamilies.find({funcOp.getOperation(), index}); + if (found == callbackFamilies.end() || !found->second.closed || found->second.members.empty()) { - callee = getMethodOp.getBoundFunc(); + return; } - auto *def = callee.getDefiningOp(); - mlir::StringAttr identifier; - int64_t index = -1; - if (auto refOp = mlir::dyn_cast_or_null(def)) - { - identifier = refOp.getIdentifierAttr().getAttr(); - index = refOp.getIndex(); - } - else if (auto refOp = mlir::dyn_cast_or_null(def)) - { - identifier = refOp.getIdentifierAttr().getAttr(); - index = refOp.getIndex(); - } - else if (auto refOp = mlir::dyn_cast_or_null(def)) - { - identifier = refOp.getIdentifierAttr().getAttr(); - } - else if (auto refOp = mlir::dyn_cast_or_null(def)) - { - identifier = refOp.getIdentifierAttr().getAttr(); - } - else if (isInstanceOfSlot(def)) + callees.viaCallback = true; + callees.known = true; + for (auto &member : found->second.members) { - callees.known = true; - callees.instanceOf = true; - return; + callees.named.push_back(member.symbol); + callees.known = addDefined(member.symbol, callees) && callees.known; + callees.shifts.push_back(member.shift); } - else if (auto symbol = fieldFunctions.of(def)) + + callees.known = callees.known && callees.shifts.size() == callees.funcs.size(); + } + + // A member's facts as a call through its family sees them: each parameter k as argument + // k - shift. False when a fact is about the capture box itself, which the call does not pass. + static bool shiftFacts(const Facts &facts, int32_t shift, Facts &shifted) + { + shifted = Facts(); + shifted.record = facts.record; + for (auto [from, to] : {std::make_pair(&facts.owned, &shifted.owned), std::make_pair(&facts.borrows, &shifted.borrows)}) { - callees.known = addDefined(symbol, callees); - callees.fieldFunction = callees.known; - return; + for (auto index : *from) + { + if (index < shift) + { + return false; + } + + to->push_back(index - shift); + } } - if (!identifier) + return true; + } + + // Can the body of `funcOp` be given parameter `index` to keep and release it where it does not + // move it: a plain slot (isParameterSlot), a block, not a handle, not a string? + bool canReleaseParam(mlir_ts::FuncOp funcOp, int32_t index) + { + if (funcOp.getBody().empty() || static_cast(index) >= funcOp.getNumArguments()) { - return; + return false; } - if (index < 0) + auto argument = funcOp.getBody().front().getArgument(index); + if (isHandle(argument) || isCopyableString(argument.getType()) || holdsNoBlock(argument)) { - callees.named.push_back(identifier); - callees.known = addDefined(identifier, callees) || addImported(identifier, callees); - return; + return false; } - callees.named = familyOf(index, identifier); - resolveFamily(index, identifier, callees); + return llvm::any_of(argument.getUsers(), [&](mlir::Operation *user) { + auto varOp = mlir::dyn_cast(user); + auto position = -1; + return varOp && isParameterSlot(varOp, position) && position == index; + }); } - // A class another module can see may be extended there, and its override is a candidate this - // module never sees: every member has to be private and defined here. - void resolveFamily(int64_t index, mlir::StringAttr identifier, Callees &callees) + // The members of each closed callback family agree (the section above), by promotion of a kept + // parameter or by demotion. True when any member's facts changed. + bool meetCallbacks(llvm::DenseSet &demoted) { - callees.known = true; - for (auto member : familyOf(index, identifier)) + auto changed = false; + for (auto &[key, family] : callbackFamilies) { - callees.known = addDefined(member, callees) && callees.known; + if (!family.closed) + { + continue; + } + + llvm::SmallVector members; + llvm::SmallVector seen; + auto shiftable = true; + for (auto &member : family.members) + { + auto funcOp = functions.lookup(member.symbol.getValue()); + Facts shifted; + shiftable = shiftable && funcOp && shiftFacts(factsEffective.lookup(funcOp), member.shift, shifted); + members.push_back(funcOp); + seen.push_back(shifted); + } + + if (shiftable && llvm::all_equal(seen)) + { + continue; + } + + // a parameter some member keeps, and each other one only borrows: kept by all + llvm::SmallVector kept; + for (auto &facts : seen) + { + kept.append(facts.owned.begin(), facts.owned.end()); + } + + llvm::sort(kept); + kept.erase(std::unique(kept.begin(), kept.end()), kept.end()); + auto promotable = shiftable && llvm::all_of(seen, [&](const Facts &facts) { + return facts.borrows == seen.front().borrows && facts.record == seen.front().record && + llvm::none_of(kept, [&](int32_t index) { return llvm::is_contained(facts.borrows, index); }); + }); + + for (auto [position, member] : llvm::enumerate(family.members)) + { + auto funcOp = members[position]; + // a member another caller reaches without the facts is given none (findOpen) + for (auto index : kept) + { + promotable = promotable && !demoted.contains(funcOp) && !open.contains(funcOp) && + !interfaceReached.contains(funcOp) && + (llvm::is_contained(seen[position].owned, index) || + canReleaseParam(funcOp, index + member.shift)); + } + } + + if (promotable) + { + for (auto [position, member] : llvm::enumerate(family.members)) + { + auto funcOp = members[position]; + auto facts = factsEffective.lookup(funcOp); + for (auto index : kept) + { + auto param = index + member.shift; + if (!llvm::is_contained(facts.owned, param)) + { + facts.owned.insert(llvm::upper_bound(facts.owned, param), param); + } + } + + if (facts != factsEffective.lookup(funcOp)) + { + factsEffective[funcOp] = facts; + changed = true; + } + } + + continue; + } + + for (auto funcOp : members) + { + if (funcOp && !declaresCallerFacts(funcOp)) + { + demoted.insert(funcOp); + if (factsEffective.erase(funcOp)) + { + changed = true; + } + } + } } - callees.known = callees.known && llvm::all_of(callees.funcs, [](mlir_ts::FuncOp funcOp) { return funcOp.isPrivate(); }); + return changed; } - // `ts.Cast(ts.VTableOffsetRef(vtable, 0))`, where `vtable` is the one an object's first word - // points to or an interface value's own: slot 0 of either is `.instanceOf` - a class's - // generated one, a string compare, or `.instanceOf.none` for an object literal behind an - // interface. Only mlirGenInstanceOfThroughVTable builds a call through it. Neither destroys - // anything. - static bool isInstanceOfSlot(mlir::Operation *def) + // ---- Results that borrow an argument ---- + + // The values a function returns: what is stored into the local its returns read (MLIRGen's + // result slot), or what a return hands back directly. + static llvm::SmallVector returnedValues(mlir_ts::FuncOp funcOp) { - auto castOp = mlir::dyn_cast_or_null(def); - auto slot = castOp ? castOp.getIn().getDefiningOp() : mlir_ts::VTableOffsetRefOp(); - if (!slot || slot.getIndex() != INTERFACE_VTABLE_INSTANCEOF_SLOT) + llvm::SmallVector values; + funcOp.walk([&](mlir_ts::ReturnInternalOp returnOp) { + for (auto operand : returnOp.getRetOperands()) + { + auto loadOp = operand.getDefiningOp(); + auto slot = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + if (!slot || slot.getInitializer() || isOwningVariable(slot)) + { + values.push_back(operand); + continue; + } + + for (auto *user : slot.getResult().getUsers()) + { + auto storeOp = mlir::dyn_cast(user); + if (storeOp && storeOp.getReference() == slot.getResult()) + { + values.push_back(storeOp.getValue()); + } + } + } + }); + + return values; + } + + struct ResultBorrows + { + llvm::SmallVector borrows; + bool record = false; + }; + + // What the function's result borrows (spec 2.4, 32): the one parameter every heap value it returns + // is a borrow of, or, when every such value is a borrowing record, the parameters the records + // borrow. A result that holds no block (`null`, a number) agrees with any; a record on one path + // and anything else on another is nothing a caller can be told. + ResultBorrows localResultBorrows(mlir_ts::FuncOp funcOp) + { + auto found = -1; + auto record = false; + llvm::SmallVector params; + for (auto value : returnedValues(funcOp)) { - return false; + if (holdsNoBlock(value)) + { + continue; + } + + // a handle returned is a counted copy, never a borrow (spec 23.2) + if (isHandle(value)) + { + return {}; + } + + switch (recordParamsOf(value, params)) + { + case RecordFlow::Bad: + return {}; + case RecordFlow::Record: + record = true; + continue; + case RecordFlow::None: + break; + } + + auto index = borrowedParam(value); + if (index < 0 || (found >= 0 && index != found)) + { + return {}; + } + + found = index; } - // a class vtable, read out of an object's first word, or an interface's own vtable, - // whose slot 0 is its implementer's `.instanceOf` (INTERFACE_VTABLE_HEADER_SLOTS) - auto *vtable = slot.getVtable().getDefiningOp(); - if (auto classVTable = mlir::dyn_cast_or_null(vtable)) + if (record && found >= 0) { - return classVTable.getIndex() == 0; + return {}; } - return mlir::isa_and_nonnull(vtable); + if (record) + { + llvm::sort(params); + params.erase(std::unique(params.begin(), params.end()), params.end()); + return {params, true}; + } + + if (found >= 0) + { + return {{found}, false}; + } + + return {}; } - // Is every use of this function value the callee of a call - through `ts.GetMethod`, with - // `ts.GetThis` reading the object beside it? - static bool onlyCalled(mlir::Value value) + // The parameter a function returns as it is on some paths, where every other value it returns + // that holds a block is one it owns (spec 33): `return v.x > 2 ? acc : V.plus(acc, v)`. Such a + // parameter is kept: the callers move the argument in, the path that returns it moves it out, + // and every other path releases it. -1 for none - a parameter returned on every path is a + // borrowed result; two parameters returned, or a part of one (`acc.next`), or a borrowing + // record, have no such answer; a string is copied instead (spec 22). + int32_t mixedReturnParam(mlir_ts::FuncOp funcOp) { - for (auto &use : value.getUses()) + auto param = -1; + auto other = false; + auto returned = returnedValues(funcOp); + llvm::SmallVector work(returned.begin(), returned.end()); + llvm::DenseSet seen; + while (!work.empty()) { - auto *user = use.getOwner(); - if (mlir::isa(user) && - calleeValue(user) == use.get() && use.getOperandNumber() == 0) + auto value = work.pop_back_val(); + if (!seen.insert(value).second || holdsNoBlock(value)) { continue; } - if (mlir::isa(user)) + if (isHandle(value) || isCopyableString(value.getType())) + { + return -1; + } + + auto root = rootOf(value); + if (auto argument = mlir::dyn_cast(root); argument && !argument.getOwner()->isEntryBlock()) { + llvm::SmallVector merged; + if (!mergedInto(argument, merged)) + { + return -1; + } + + work.append(merged.begin(), merged.end()); continue; } - if (mlir::isa(user) && onlyCalled(user->getResult(0))) + int32_t index = -1; + if (readsParameterSlot(root, funcOp, index)) { + if ((param >= 0 && param != index) || isHandle(root) || isCopyableString(root.getType())) + { + return -1; + } + + param = index; continue; } - return false; - } - - return true; + llvm::SmallVector recordParams; + if (borrowedParam(root) >= 0 || recordParamsOf(root, recordParams) != RecordFlow::None) + { + return -1; + } + + other = true; + } + + return other ? param : -1; + } + + // ---- Borrowing records (spec 32) ---- + // + // `{ thing: this, ray: ray, dist: d }` given as an interface: an object literal's box holding values + // it does not own beside values it does. Rust's `struct Hit<'a> { thing: &'a Th, .. }`: the box is + // its owner's - the caller's, when it is returned - and it may not outlive what it borrows. Each box + // is classified where it is made: the fields it borrows (OWN_RECORD_BORROWED_FIELDS_ATTR_NAME, on the + // box and on every interface made of it, whose tag then names a release routine that leaves them), + // and, when every one of them is a parameter, `this`, or a read out of one, those parameters - what + // a function returning it lends (OWN_RESULT_RECORD_ATTR_NAME). The inference bounds every use. + + struct RecordBox + { + llvm::SmallVector params; + // every borrowed field is a parameter's, `this`'s, a read out of one, or immortal + bool returnable = true; + }; + + llvm::DenseMap recordBoxes; + + void classifyRecordBoxes() + { + recordBoxes.clear(); + getOperation().walk([&](mlir_ts::NewOp newOp) { + newOp->removeAttr(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME); + for (auto *user : newOp->getUsers()) + { + for (auto *viewUser : user->getUsers()) + { + if (mlir::isa(viewUser)) + { + viewUser->removeAttr(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME); + } + } + } + + classifyRecordBox(newOp); + }); } - // The closed world. A function is open when a caller this pass cannot see may reach it: it is - // not private (another module may call it), or its symbol is used other than by a direct call, - // a class vtable's entry, or a method reference used only as a callee. A virtual reference - // that escapes opens its whole family. - void findOpen(mlir::ModuleOp module) + void classifyRecordBox(mlir_ts::NewOp newOp) { - for (auto &entry : functions) + auto boxOp = recordBoxOf(newOp.getResult()); + auto varOp = boxOp == newOp ? recordLocalOf(newOp.getResult()) : mlir_ts::VariableOp(); + if (!varOp) + { + return; + } + + // the record read once, and only into its box (rc's retain of it beside) + mlir_ts::LoadOp read; + for (auto *user : varOp->getUsers()) { - if (!entry.second.isPrivate()) + if (auto loadOp = mlir::dyn_cast(user)) { - open.try_emplace(entry.second, "it can be called from another module; '@" LENDS_DECORATOR "' or '@" KEEPS_DECORATOR - "' would say it in its signature"); + if (read) + { + return; + } + + read = loadOp; } } - // the module is a symbol table, which getSymbolUses does not look inside: walk its body - auto uses = mlir::SymbolTable::getSymbolUses(&module.getBodyRegion()); - if (!uses) + for (auto *user : read->getUsers()) { - // an op this pass cannot read symbols through: nothing is closed - for (auto &entry : functions) + auto storeOp = mlir::dyn_cast(user); + if (!mlir::isa(user) && !(storeOp && storeOp.getReference() == newOp.getResult())) { - open.try_emplace(entry.second, "its callers cannot all be found"); + return; } - - return; } - for (auto &use : *uses) + // the box seen only as interfaces: every release of it reads their tag + llvm::SmallVector interfaces; + for (auto &use : newOp->getUses()) { - auto symbol = use.getSymbolRef().getRootReference(); - auto funcOp = functions.lookup(symbol.getValue()); - if (!funcOp) + auto *user = use.getOwner(); + if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getReference() == newOp.getResult()) { continue; } - auto *user = use.getUser(); - if (mlir::isa(user)) + auto castOp = mlir::dyn_cast(user); + if (!castOp || !isView(castOp)) { - continue; + return; } - if (mlir::isa(user) && isClassVTableEntry(user)) + for (auto *castUser : castOp->getUsers()) { - continue; + auto newInterfaceOp = mlir::dyn_cast(castUser); + if (!newInterfaceOp || newInterfaceOp.getThisVal() != castOp.getResult()) + { + return; + } + + interfaces.push_back(castUser); } + } - int64_t index = -1; - if (auto refOp = mlir::dyn_cast(user)) + if (interfaces.empty()) + { + return; + } + + // each field that holds a block: owned (made here, or nothing) or borrowed, never both + RecordBox info; + llvm::SmallVector borrowed; + MLIRTypeHelper mth(&getContext(), CompileOptions{}); + for (auto *user : varOp->getUsers()) + { + auto propertyRefOp = mlir::dyn_cast(user); + if (!propertyRefOp) { - index = refOp.getIndex(); + continue; } - else if (auto refOp = mlir::dyn_cast(user)) + + auto field = propertyRefOp.getResult(); + auto refType = mlir::dyn_cast(field.getType()); + if (!refType || !mth.ownsHeapMemory(field.getLoc(), refType.getElementType())) { - index = refOp.getIndex(); + continue; } - auto inFunction = !!user->getParentOfType(); - if (inFunction && mlir::isa(user) && - onlyCalled(user->getResult(0))) + auto owns = false; + auto borrows = false; + for (auto *fieldUser : field.getUsers()) { - // a virtual call this pass cannot resolve reaches its family without the facts - if (index >= 0) + if (mlir::isa(fieldUser)) { - Callees callees; - resolveFamily(index, symbol, callees); - if (!callees.known) + continue; + } + + auto storeOp = mlir::dyn_cast(fieldUser); + if (!storeOp || storeOp.getReference() != field) + { + return; + } + + auto value = storeOp.getValue(); + switch (recordFieldSource(value)) + { + case RecordFieldSource::NoBlock: + continue; + case RecordFieldSource::Fresh: + owns = true; + continue; + case RecordFieldSource::Unknown: + return; + case RecordFieldSource::Immortal: + borrows = true; + continue; + case RecordFieldSource::Param: + case RecordFieldSource::Place: + if (auto param = borrowedParam(value); param >= 0) { - openFamily(index, symbol, "an override may be defined in another module"); + info.params.push_back(param); + } + else + { + info.returnable = false; } + + borrows = true; + continue; + case RecordFieldSource::Slot: + case RecordFieldSource::Cell: + info.returnable = false; + borrows = true; + continue; } + } - continue; + // a string is copied, not borrowed (spec 22) + if ((owns && borrows) || (borrows && isCopyableString(refType.getElementType()))) + { + return; } - open.try_emplace(funcOp, "it is used other than by a call"); - if (index >= 0) + if (borrows) { - openFamily(index, symbol, "it is used other than by a call"); + borrowed.push_back(propertyRefOp.getPosition()); } } - } - void openFamily(int64_t index, mlir::StringAttr symbol, const char *why) - { - for (auto member : familyOf(index, symbol)) + if (borrowed.empty()) { - if (auto memberOp = functions.lookup(member.getValue())) - { - open.try_emplace(memberOp, why); - } + return; + } + + llvm::sort(borrowed); + borrowed.erase(std::unique(borrowed.begin(), borrowed.end()), borrowed.end()); + llvm::sort(info.params); + info.params.erase(std::unique(info.params.begin(), info.params.end()), info.params.end()); + + auto attr = mlir::DenseI32ArrayAttr::get(&getContext(), borrowed); + newOp->setAttr(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME, attr); + for (auto *newInterfaceOp : interfaces) + { + newInterfaceOp->setAttr(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME, attr); } + + recordBoxes[newOp.getOperation()] = std::move(info); } - // ---- Results that borrow an argument ---- + enum class RecordFlow + { + None, // no borrowing record + Record, // borrowing records only (or values that hold no block) + Bad // a record that borrows what is not a parameter, or one beside anything else + }; - // The values a function returns: what is stored into the local its returns read (MLIRGen's - // result slot), or what a return hands back directly. - static llvm::SmallVector returnedValues(mlir_ts::FuncOp funcOp) + // Whether `value` - through views, merges and the owning locals that hold it - is a borrowing + // record, and the parameters it borrows: a box made here, or a call's result that is one, whose + // arguments are this function's parameters or reads out of them. + RecordFlow recordParamsOf(mlir::Value value, llvm::SmallVectorImpl ¶ms) { - llvm::SmallVector values; - funcOp.walk([&](mlir_ts::ReturnInternalOp returnOp) { - for (auto operand : returnOp.getRetOperands()) + llvm::SmallVector work{value}; + llvm::SmallPtrSet seen; + auto record = false; + auto other = false; + while (!work.empty()) + { + // what holds no block, by its own type (a box's `value_ref` owns nothing by type, but + // the object its view shows does) + auto next = work.pop_back_val(); + auto current = rootOf(next); + if (!seen.insert(current).second || holdsNoBlock(next)) { - auto loadOp = operand.getDefiningOp(); - auto slot = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); - if (!slot || slot.getInitializer() || isOwningVariable(slot)) + continue; + } + + auto *def = current.getDefiningOp(); + if (auto found = def ? recordBoxes.find(def) : recordBoxes.end(); found != recordBoxes.end()) + { + if (!found->second.returnable) { - values.push_back(operand); - continue; + return RecordFlow::Bad; } - for (auto *user : slot.getResult().getUsers()) + params.append(found->second.params.begin(), found->second.params.end()); + record = true; + continue; + } + + if (def && isCall(def) && isRecordResult(def)) + { + auto args = callArgs(def); + for (auto index : resultBorrowsAll(def)) { - auto storeOp = mlir::dyn_cast(user); - if (storeOp && storeOp.getReference() == slot.getResult()) + if (static_cast(index) >= args.size()) { - values.push_back(storeOp.getValue()); + return RecordFlow::Bad; } - } - } - }); - return values; - } + auto arg = args[index]; + if (argumentHoldsNoBlock(arg) || isImmortalGlobalRead(arg)) + { + continue; + } - // The parameter every heap value the function returns borrows, or -1 (spec 2.4). A result - // that holds no block (`null`, a number) agrees with any. - static int localResultBorrows(mlir_ts::FuncOp funcOp) - { - auto found = -1; - for (auto value : returnedValues(funcOp)) - { - if (holdsNoBlock(value)) - { + auto param = borrowedParam(arg); + if (param < 0) + { + return RecordFlow::Bad; + } + + params.push_back(param); + } + + record = true; continue; } - // a handle returned is a counted copy, never a borrow (spec 23.2) - if (isHandle(value)) + if (auto argument = mlir::dyn_cast(current)) { - return -1; + llvm::SmallVector merged; + if (mergedInto(argument, merged)) + { + work.append(merged.begin(), merged.end()); + continue; + } + + other = true; + continue; } - auto index = borrowedParam(value); - if (index < 0 || (found >= 0 && index != found)) + // what an owning local holds: its initializer and every value stored into it + auto loadOp = mlir::dyn_cast_or_null(def); + auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + if (varOp && isOwningVariable(varOp) && !varOp.getCaptured().value_or(false)) { - return -1; + if (varOp.getInitializer()) + { + work.push_back(varOp.getInitializer()); + } + + for (auto *user : varOp->getUsers()) + { + if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getReference() == varOp.getResult()) + { + work.push_back(storeOp.getValue()); + } + } + + continue; } - found = index; + other = true; } - return found; + if (!record) + { + return RecordFlow::None; + } + + return other ? RecordFlow::Bad : RecordFlow::Record; } // ---- Parameters the callee keeps ---- @@ -1649,6 +4649,12 @@ class OwnershipSignaturePass : public mlir::PassWrapper(user)) { + // a field a borrowing record borrows (spec 32): the record keeps nothing + if (isBorrowedRecordFieldStore(user)) + { + return false; + } + auto ref = storeOp.getReference(); return storeOp.getValue() == used && (isPlace(ref) || ref.getDefiningOp()); } @@ -1682,6 +4688,16 @@ class OwnershipSignaturePass : public mlir::PassWrapper(user)) + { + return varOp.getInitializer() == used && isOwningVariable(varOp) && !varOp.getCaptured().value_or(false) && + isAssigned(varOp) && !isCopyableString(used.getType()); + } + return false; } @@ -1746,11 +4762,28 @@ class OwnershipSignaturePass : public mlir::PassWrapper pinned; for (auto round = 0; round < 16; ++round) { + // which object literals borrow depends on what the calls stored into them return, and a + // function's facts on which literals borrow (spec 32) + classifyRecordBoxes(); + for (auto &entry : functions) { if (!entry.second.isDeclaration()) { - factsLocal[entry.second] = {localResultBorrows(entry.second), localOwnedParams(entry.second)}; + auto result = localResultBorrows(entry.second); + Facts facts; + facts.borrows = std::move(result.borrows); + facts.record = result.record; + facts.owned = localOwnedParams(entry.second); + // what the body takes into something on every path; a parameter it returns on + // some paths only is kept too, and released on the others (spec 33) + takenParams[entry.second] = facts.owned; + if (auto mixed = mixedReturnParam(entry.second); mixed >= 0 && !llvm::is_contained(facts.owned, mixed)) + { + facts.owned.insert(llvm::upper_bound(facts.owned, mixed), mixed); + } + + factsLocal[entry.second] = std::move(facts); } } @@ -1763,11 +4796,16 @@ class OwnershipSignaturePass : public mlir::PassWrapper(keeps.begin(), keeps.end())}; + Facts said; + said.borrows = declaredLends(entry.second); + said.owned.assign(keeps.begin(), keeps.end()); + // a body here that returns a borrowing record of what `@lends` names: callers in this + // module own the box (spec 32); checkDeclared holds the body to the parameters + auto local = factsLocal.lookup(entry.second); + said.record = !said.borrows.empty() && local.record && local.borrows == said.borrows; + if (factsThroughInterface(entry.second, said)) + { + continue; + } + + factsEffective[entry.second] = said; } } + // the members of a family that disagree lose their facts: a call through the family is + // given only what all of them say. A closed interface's slot is such a family too. + auto demote = [&](llvm::ArrayRef members) { + llvm::SmallVector seen; + for (auto member : members) + { + auto funcOp = functions.lookup(member.getValue()); + seen.push_back(funcOp ? factsEffective.lookup(funcOp) : Facts()); + } + + auto changed = false; + if (llvm::all_equal(seen)) + { + return changed; + } + + for (auto member : members) + { + if (auto funcOp = functions.lookup(member.getValue()); + funcOp && !declaresCallerFacts(funcOp) && factsEffective.erase(funcOp)) + { + changed = true; + } + } + + return changed; + }; + + // a member a callback family demoted is never promoted again: each step only adds or only + // takes away, so this settles + demotedCallbacks.clear(); for (auto changed = true; changed;) { changed = false; for (auto &[key, members] : families) { - llvm::SmallVector seen; - for (auto member : members) - { - auto funcOp = functions.lookup(member.getValue()); - seen.push_back(funcOp ? factsEffective.lookup(funcOp) : Facts()); - } + changed = demote(members) || changed; + } + + changed = meetCallbacks(demotedCallbacks) || changed; - if (llvm::all_equal(seen)) + for (auto &[key, family] : interfaceFamilies) + { + if (family.closed) { - continue; + changed = demote(family.members) || changed; } + } - for (auto member : members) + for (auto &[key, family] : interfaceFieldFamilies) + { + if (family.closed) { - if (auto funcOp = functions.lookup(member.getValue()); - funcOp && !declaresCallerFacts(funcOp) && factsEffective.erase(funcOp)) - { - changed = true; - } + changed = demote(family.members) || changed; } } } @@ -1848,6 +4929,24 @@ class OwnershipSignaturePass : public mlir::PassWrapper agreed; + for (auto [funcOp, shift] : llvm::zip(callees.funcs, callees.shifts)) + { + Facts shifted; + if (!shiftFacts(factsEffective.lookup(funcOp), shift, shifted) || (agreed && *agreed != shifted)) + { + return {}; + } + + agreed = shifted; + } + + return agreed.value_or(Facts()); + } + auto facts = factsEffective.lookup(callees.funcs.front()); auto agree = llvm::all_of(callees.funcs, [&](mlir_ts::FuncOp callee) { return factsEffective.lookup(callee) == facts; @@ -1859,11 +4958,15 @@ class OwnershipSignaturePass : public mlir::PassWrappergetContext(); op->removeAttr(OWN_RESULT_BORROWS_ATTR_NAME); + op->removeAttr(OWN_RESULT_RECORD_ATTR_NAME); op->removeAttr(OWN_PARAMS_ATTR_NAME); - if (facts.borrows >= 0) + if (!facts.borrows.empty()) { - op->setAttr(OWN_RESULT_BORROWS_ATTR_NAME, - mlir::IntegerAttr::get(mlir::IntegerType::get(context, 32), facts.borrows)); + op->setAttr(OWN_RESULT_BORROWS_ATTR_NAME, mlir::DenseI32ArrayAttr::get(context, facts.borrows)); + if (facts.record) + { + op->setAttr(OWN_RESULT_RECORD_ATTR_NAME, mlir::UnitAttr::get(context)); + } } if (!facts.owned.empty()) @@ -1886,9 +4989,31 @@ class OwnershipSignaturePass : public mlir::PassWrapperremoveAttr(OWN_FACTS_LOST_ATTR_NAME); + funcOp->removeAttr(OWN_PARAMS_RELEASED_ATTR_NAME); + // a signature's `@keeps` is checked against the body as it is (checkDeclared) + llvm::SmallVector released; + for (auto index : effective.owned) + { + if (!declaresCallerFacts(funcOp) && !llvm::is_contained(takenParams.lookup(funcOp), index)) + { + released.push_back(index); + } + } + + if (!released.empty()) + { + funcOp->setAttr(OWN_PARAMS_RELEASED_ATTR_NAME, mlir::DenseI32ArrayAttr::get(&getContext(), released)); + } + if (effective.empty() && !local.empty()) { - auto *why = open.lookup(funcOp); + // through an interface first: a marker would not help there (interfaceReached) + auto *why = factsThroughInterface(funcOp, local) ? INTERFACE_REACHED_WHY : open.lookup(funcOp); + if (!why && demotedCallbacks.contains(funcOp)) + { + why = "a function passed in its place disagrees"; + } + funcOp->setAttr(OWN_FACTS_LOST_ATTR_NAME, mlir::StringAttr::get(&getContext(), why ? why : "an override in its class family disagrees")); } @@ -2326,8 +5451,10 @@ class OwnershipSignaturePass : public mlir::PassWrapper(); auto *place = loadOp ? loadOp.getReference().getDefiningOp() : nullptr; + // an immortal global's value (spec 25) is owned by no one: any number of globals hold it if (!place || !mlir::isa(place) || - isHandle(value) || isConstantData(value) || !ownsHeap(value.getLoc(), value.getType())) + isHandle(value) || isConstantData(value) || isImmortalGlobalRead(value) || + !ownsHeap(value.getLoc(), value.getType())) { return; } @@ -2423,7 +5550,10 @@ class OwnershipSignaturePass : public mlir::PassWrappergetLoc(), argument.getType())) + // an immortal global (spec 25) is written by its initializer only, and that runs once + // (a static's is a store in its static constructor, outside any loop, which + // writtenGlobals counts), and no release frees its block + if (isHandle(argument) || isImmortalGlobalRead(argument) || !ownsHeap(call.op->getLoc(), argument.getType())) { continue; } @@ -2469,7 +5599,7 @@ class OwnershipSignaturePass : public mlir::PassWrapper= 0) + if (!facts.borrows.empty()) { op->emitError("the initializer of global '") << globalOp.getSymName() << "' holds the result of '" << calleeName @@ -2689,6 +5819,12 @@ class OwnershipSignaturePass : public mlir::PassWrapper(); interfaceRefOp && isInterfaceField(value)) + { + work.push_back(interfaceRefOp.getInterfaceVal()); + continue; + } + if (auto argument = mlir::dyn_cast(value)) { llvm::SmallVector merged; @@ -2777,7 +5913,7 @@ class OwnershipSignaturePass : public mlir::PassWrapper mlir_ts::createOwnershipSignaturePass() +std::unique_ptr mlir_ts::createOwnershipSignaturePass(bool executable) { - return std::make_unique(); + return std::make_unique(executable); } diff --git a/tslang/lib/TypeScript/StackPromotionPass.cpp b/tslang/lib/TypeScript/StackPromotionPass.cpp index 91ec43dcd..0ff38592c 100644 --- a/tslang/lib/TypeScript/StackPromotionPass.cpp +++ b/tslang/lib/TypeScript/StackPromotionPass.cpp @@ -376,6 +376,13 @@ class StackPromotionPass : public mlir::PassWrapperhasAttr(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME)) + { + return "borrowing-record"; + } + // A promoted object gives back what it owns when its function returns, later than the heap // object it replaces would have been destroyed. Only a Shared handle's count can see // that, so under the models that count, a module using Shared keeps every object that diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 11240c4b2..b4c72b716 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -3505,7 +3505,13 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o own_string_copy_branch own_array_param_push own_declared_no_drops own_return_paths own_string_concat_element own_final_fresh_result own_final_field_lend own_null_narrowing_di - own_param_markers own_generator_locals own_string_param_assigned own_lends) + own_param_markers own_generator_locals own_string_param_assigned own_lends + own_immortal_global own_immortal_global_region own_global_loop_store + own_literal_moves own_record_literal_given + own_field_chain_let own_read_after_move own_conditional_move + own_interface_facts own_declared_scalar_no_drops + own_borrowing_record own_borrowing_record_arg + own_reduce own_fresh_call_result) tslang_add_test(NAME test-jit-own-${own_test} COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/own/${own_test}.ts") tslang_add_test(NAME test-compile-own-${own_test} COMMAND test-runner -mm=own "${PROJECT_SOURCE_DIR}/test/tester/own/${own_test}.ts") # The promise: no counting at all in what own emits. @@ -3517,6 +3523,45 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o FAIL_REGULAR_EXPRESSION "__tslang_inc_ref|__tslang_dec_ref|error:|Stack dump") endforeach() +# A global never reassigned is immortal under own (spec 25); the same program under every other model. +foreach(immortal_mm rc gc none) + foreach(immortal_test own_immortal_global own_immortal_global_region own_global_loop_store) + tslang_add_test(NAME test-jit-${immortal_mm}-${immortal_test} COMMAND test-runner -jit -mm=${immortal_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/${immortal_test}.ts") + tslang_add_test(NAME test-compile-${immortal_mm}-${immortal_test} COMMAND test-runner -mm=${immortal_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/${immortal_test}.ts") + endforeach() +endforeach() + +# A global whose one store runs in a loop is mortal (spec 25): its stores are not marked, so each +# releases the value before it, which an immortal header would leak. The `if` arm's store runs once. +add_test(NAME test-own-global-loop-store-mortal + COMMAND $ --emit=mlir-affine --no-default-lib -mm=own + "${PROJECT_SOURCE_DIR}/test/tester/own/own_global_loop_store.ts") +set_tests_properties(test-own-global-loop-store-mortal + PROPERTIES PASS_REGULAR_EXPRESSION "global_name = @T[.]w}> {__own_immortal}" + FAIL_REGULAR_EXPRESSION "global_name = @[KS][.]w}> {__own_immortal}|error|Stack dump") + +# The other write guards of an immortal global (spec 25), one program each: an exported global, a +# static written through its class's vtable (`k: typeof M`), a second store in a static block, and a +# store in a static method leave `M` (`M.w`, `MK.m`) mortal; `I` (`I.w`) is immortal. +foreach(mortal_test export typeof static_twice static_method) + string(REPLACE "_" "-" mortal_name "${mortal_test}") + add_test(NAME test-own-global-mortal-${mortal_name} + COMMAND $ --emit=mlir-affine --no-default-lib -mm=own + "${PROJECT_SOURCE_DIR}/test/tester/own/own_global_mortal_${mortal_test}.ts") + set_tests_properties(test-own-global-mortal-${mortal_name} + PROPERTIES PASS_REGULAR_EXPRESSION "global_name = @I([.]w)?}> {__own_immortal}" + FAIL_REGULAR_EXPRESSION "global_name = @M[^}]*}> {__own_immortal}|error|Stack dump") +endforeach() + +# An object literal that owns its fields moves anywhere a fresh object does under own (spec 27); +# the same programs under every other model. +foreach(owning_record_mm rc gc none) + foreach(owning_record_test own_literal_moves own_record_literal_given) + tslang_add_test(NAME test-jit-${owning_record_mm}-${owning_record_test} COMMAND test-runner -jit -mm=${owning_record_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/${owning_record_test}.ts") + tslang_add_test(NAME test-compile-${owning_record_mm}-${owning_record_test} COMMAND test-runner -mm=${owning_record_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/${owning_record_test}.ts") + endforeach() +endforeach() + # ...and built with debug info, whose records of narrowed variables are ops of their own add_test(NAME test-own-di-null-narrowing COMMAND $ --emit=mlir-affine --di --opt_level=0 --no-default-lib -mm=own @@ -3524,6 +3569,91 @@ add_test(NAME test-own-di-null-narrowing set_tests_properties(test-own-di-null-narrowing PROPERTIES PASS_REGULAR_EXPRESSION "ts.Func" FAIL_REGULAR_EXPRESSION "error|Stack dump") +# A field read through an interface is a place read under own (spec 28); the same program under +# every other model. +foreach(field_chain_mm rc gc none) + tslang_add_test(NAME test-jit-${field_chain_mm}-own_field_chain_let COMMAND test-runner -jit -mm=${field_chain_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_field_chain_let.ts") + tslang_add_test(NAME test-compile-${field_chain_mm}-own_field_chain_let COMMAND test-runner -mm=${field_chain_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_field_chain_let.ts") +endforeach() + +# A read after a move, through the new owner, under own (spec 29); the same program under every +# other model. +foreach(read_after_move_mm rc gc none) + tslang_add_test(NAME test-jit-${read_after_move_mm}-own_read_after_move COMMAND test-runner -jit -mm=${read_after_move_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_read_after_move.ts") + tslang_add_test(NAME test-compile-${read_after_move_mm}-own_read_after_move COMMAND test-runner -mm=${read_after_move_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_read_after_move.ts") +endforeach() + +# A value moved out of a local on some paths only, released on the others (spec 30); the same +# program under every other model. +foreach(conditional_move_mm rc gc none) + tslang_add_test(NAME test-jit-${conditional_move_mm}-own_conditional_move COMMAND test-runner -jit -mm=${conditional_move_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_conditional_move.ts") + tslang_add_test(NAME test-compile-${conditional_move_mm}-own_conditional_move COMMAND test-runner -mm=${conditional_move_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_conditional_move.ts") +endforeach() + +# A call through an interface closed in its module gets its implementations' facts (spec 31); the +# same program under every other model. +foreach(interface_facts_mm rc gc none) + foreach(interface_facts_test own_interface_facts own_declared_scalar_no_drops own_interface_exe_exports) + tslang_add_test(NAME test-jit-${interface_facts_mm}-${interface_facts_test} COMMAND test-runner -jit -mm=${interface_facts_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/${interface_facts_test}.ts") + tslang_add_test(NAME test-compile-${interface_facts_mm}-${interface_facts_test} COMMAND test-runner -mm=${interface_facts_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/${interface_facts_test}.ts") + endforeach() +endforeach() + +# An object literal that borrows some of what it holds - a borrowing record, returned or a temporary - +# under own (spec 32); the same programs under every other model. +foreach(borrowing_record_mm rc gc none) + foreach(borrowing_record_test own_borrowing_record own_borrowing_record_arg) + tslang_add_test(NAME test-jit-${borrowing_record_mm}-${borrowing_record_test} COMMAND test-runner -jit -mm=${borrowing_record_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/${borrowing_record_test}.ts") + tslang_add_test(NAME test-compile-${borrowing_record_mm}-${borrowing_record_test} COMMAND test-runner -mm=${borrowing_record_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/${borrowing_record_test}.ts") + endforeach() +endforeach() + +# `reduce` takes its accumulator, and a parameter returned on some paths only is kept (spec 33); +# the same program under every other model. +foreach(reduce_mm rc gc none) + tslang_add_test(NAME test-jit-${reduce_mm}-own_reduce COMMAND test-runner -jit -mm=${reduce_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_reduce.ts") + tslang_add_test(NAME test-compile-${reduce_mm}-own_reduce COMMAND test-runner -mm=${reduce_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_reduce.ts") +endforeach() + +# A direct call's result is the caller's own, not a use of a borrow given to it (spec 34); the same +# program under every other model. +foreach(fresh_call_mm rc gc none) + tslang_add_test(NAME test-jit-${fresh_call_mm}-own_fresh_call_result COMMAND test-runner -jit -mm=${fresh_call_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_fresh_call_result.ts") + tslang_add_test(NAME test-compile-${fresh_call_mm}-own_fresh_call_result COMMAND test-runner -mm=${fresh_call_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_fresh_call_result.ts") +endforeach() + +# The ray tracer, own's copy of the benchmark at 64x64 (spec 34), under every model. Its surfaces are +# Shared handles, which count, so it is not in own_test (its no-counting check). It exports its +# `Surfaces` globals, which an object file opens to other modules (spec 31), so under own the JIT's +# entry module and `tslang --emit=exe` build it (own-exe-run.cmake), not test-runner's object file. +foreach(raytrace_mm gc none rc) + tslang_add_test(NAME test-jit-${raytrace_mm}-own_raytrace COMMAND test-runner -jit -mm=${raytrace_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_raytrace.ts") + tslang_add_test(NAME test-compile-${raytrace_mm}-own_raytrace COMMAND test-runner -mm=${raytrace_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_raytrace.ts") +endforeach() +tslang_add_test(NAME test-jit-own-own_raytrace COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/own/own_raytrace.ts") +add_test(NAME test-compile-own-own_raytrace + COMMAND ${CMAKE_COMMAND} + "-DTSLANG=$" + "-DSOURCE=${PROJECT_SOURCE_DIR}/test/tester/own/own_raytrace.ts" + "-DEXPECT=checksum: 606318" + "-DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/own-raytrace" + "-DLLVM_LIB=${LLVM_LIBRARY_DIR}" + "-DTSLANG_LIB=${CMAKE_BINARY_DIR}/lib" + -P "${CMAKE_CURRENT_SOURCE_DIR}/own-exe-run.cmake") + +# An executable's exports leave its interfaces closed (spec 31, USER decision): under own the JIT's +# entry module and `tslang --emit=exe` compile own_interface_exe_exports; an object file and a DLL +# do not (own-exe-exports.cmake). test-runner's own AOT build is an object file, so it is not run. +tslang_add_test(NAME test-jit-own-own_interface_exe_exports COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/own/own_interface_exe_exports.ts") +add_test(NAME test-own-exe-exports + COMMAND ${CMAKE_COMMAND} + "-DTSLANG=$" + "-DSOURCE=${PROJECT_SOURCE_DIR}/test/tester/own/own_interface_exe_exports.ts" + "-DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/own-exe-exports" + "-DLLVM_LIB=${LLVM_LIBRARY_DIR}" + "-DTSLANG_LIB=${CMAKE_BINARY_DIR}/lib" + -P "${CMAKE_CURRENT_SOURCE_DIR}/own-exe-exports.cmake") + # A global declared in top-level code owns what it is given, under every model that counts. foreach(global_mm rc none gc) tslang_add_test(NAME test-jit-${global_mm}-own-global-script COMMAND test-runner -jit -mm=${global_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_global_script.ts") @@ -3621,6 +3751,34 @@ foreach(shared_verify_mm own rc) PROPERTIES FAIL_REGULAR_EXPRESSION "error|Stack dump") endforeach() endforeach() +# Shared of an object literal (spec 26), global and local: the literal moves into the handle. +# Counted like every Shared test, so not in own_test (its no-counting check). +foreach(shared_literal_mm gc none rc own) + tslang_add_test(NAME test-jit-${shared_literal_mm}-own_shared_literal COMMAND test-runner -jit -mm=${shared_literal_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_shared_literal.ts") + tslang_add_test(NAME test-compile-${shared_literal_mm}-own_shared_literal COMMAND test-runner -mm=${shared_literal_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_shared_literal.ts") +endforeach() +foreach(shared_literal_verify_mm own rc) + add_test(NAME test-${shared_literal_verify_mm}-verify-ownership-own_shared_literal + COMMAND $ --emit=mlir-affine --no-default-lib -mm=${shared_literal_verify_mm} --verify-ownership + "${PROJECT_SOURCE_DIR}/test/tester/own/own_shared_literal.ts") + set_tests_properties(test-${shared_literal_verify_mm}-verify-ownership-own_shared_literal + PROPERTIES FAIL_REGULAR_EXPRESSION "error|Stack dump") +endforeach() + +# A function-typed field of an interface closed in its module, called through a global and a local +# Shared of a literal and through a parameter (spec 31). Counted like every Shared test, so not in +# own_test (its no-counting check). +foreach(fn_field_mm gc none rc own) + tslang_add_test(NAME test-jit-${fn_field_mm}-own_interface_fn_field COMMAND test-runner -jit -mm=${fn_field_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_interface_fn_field.ts") + tslang_add_test(NAME test-compile-${fn_field_mm}-own_interface_fn_field COMMAND test-runner -mm=${fn_field_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_interface_fn_field.ts") +endforeach() +foreach(fn_field_verify_mm own rc) + add_test(NAME test-${fn_field_verify_mm}-verify-ownership-own_interface_fn_field + COMMAND $ --emit=mlir-affine --no-default-lib -mm=${fn_field_verify_mm} --verify-ownership + "${PROJECT_SOURCE_DIR}/test/tester/own/own_interface_fn_field.ts") + set_tests_properties(test-${fn_field_verify_mm}-verify-ownership-own_interface_fn_field + PROPERTIES FAIL_REGULAR_EXPRESSION "error|Stack dump") +endforeach() # Across a library boundary: the library exports its functions' `__own_no_drops`, and a borrow held # across a call to one survives. Run as a -shared pair (the library loaded at run time) and, on @@ -3644,6 +3802,16 @@ if (WIN32) -P "${CMAKE_CURRENT_SOURCE_DIR}/own-shared-no-drops.cmake") endif() +# An override of a method of a class another module can name, or defines, is called from that module +# through the base's vtable (own spec 31): under own each library or importer below is an error +# (own_error_cases); the pairs are ordinary TypeScript under every other model. +foreach(override_mm rc none gc) + tslang_add_test(NAME test-compile-${override_mm}-shared-override-nameable-base COMMAND test-runner -shared -mm=${override_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_override_nameable_main.ts" "${PROJECT_SOURCE_DIR}/test/tester/own/own_err_override_nameable_base.ts") + tslang_add_test(NAME test-jit-${override_mm}-shared-override-imported-base COMMAND test-runner -jit -shared -mm=${override_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_err_override_imported_base.ts" "${PROJECT_SOURCE_DIR}/test/tester/own/own_override_base_lib.ts") + tslang_add_test(NAME test-compile-${override_mm}-shared-override-imported-base COMMAND test-runner -shared -mm=${override_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_err_override_imported_base.ts" "${PROJECT_SOURCE_DIR}/test/tester/own/own_override_base_lib.ts") + tslang_add_test(NAME test-compile-${override_mm}-shared-override-imported-class COMMAND test-runner -shared -mm=${override_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_err_override_imported_class.ts" "${PROJECT_SOURCE_DIR}/test/tester/own/own_override_base_lib.ts") +endforeach() + # The ownership verifier runs under own too, on the IR before inference erases its birth takes. add_test(NAME test-own-verify-ownership COMMAND $ --emit=mlir-affine --no-default-lib -mm=own --verify-ownership @@ -3735,7 +3903,25 @@ set(own_error_cases "own_err_param_kept_loop|is moved inside a loop" "own_err_param_kept_not_owned|is given to 'keep', which keeps it, but -mm=own cannot move it here" "own_err_delete|'delete' is not supported by -mm=own yet" - "own_err_mixed_return|'pick' returns a borrow of its argument on some paths and another value on others" + "own_err_mixed_return|is used here after its value was moved" + "own_err_mixed_return_join_use|is moved here on some paths only" + "own_err_callback_escapes|cannot make the callers of 'pick' give it up" + "own_err_callback_disagrees|a function passed in its place disagrees" + "own_err_callback_promoted_used|is used here after its value was moved" + "own_err_kept_param_let_overwritten|borrows a field but is used here after it may be released or overwritten" + "own_err_kept_param_field_overwritten|borrows a field but is used here after it may be released or overwritten" + "own_err_kept_and_borrowed_arg|is used here after its value was moved" + "own_err_kept_and_borrowed_arg_slot|is used here after its value was moved" + "own_err_kept_and_borrowed_arg_field|is used here after its value was moved" + "own_err_kept_twice|is used here after its value was moved" + "own_err_kept_twice_field|is used here after its value was moved" + "own_err_kept_twice_push|is used here after its value was moved" + "own_err_kept_arg_assigned_in_call|is used here after its value was moved" + "own_err_kept_and_borrowed_arg_assigned_in_call|is used here after its value was moved" + "own_err_kept_arg_assigned_on_arm|is used here after its value was moved" + "own_err_kept_arg_assigned_in_and|is used here after its value was moved" + "own_err_call_result_bounded|borrows a field but is used here after it may be released or overwritten" + "own_err_call_result_record|borrows a field but is used here after it may be released or overwritten" "own_err_param_returned_exported|is a parameter, returned here, but -mm=own cannot tell the callers of 'identity' that the result borrows it" "own_err_param_kept_exported|is a parameter, kept here, but -mm=own cannot make the callers of 'Animal.constructor' give it up" "own_err_param_assigned|is a parameter and cannot be assigned under -mm=own yet" @@ -3745,13 +3931,74 @@ set(own_error_cases "own_err_global_borrowed_temporary|the initializer of global 'c' holds the result of 'id', which borrows its argument" "own_err_global_kept_not_fresh|the initializer of global 'n' gives 'N.constructor' a value it keeps, but -mm=own cannot move it there" "own_err_global_returned|borrows the global 'V.def' and cannot be stored, returned or captured" + "own_err_immortal_borrowed_init|borrows 'K.list.*' and cannot be stored, returned or captured" + "own_err_immortal_field_overwritten|borrows .* but is used here after .* released or overwritten" + "own_err_immortal_region_borrow|borrows the global 'c' and cannot be stored, returned or captured" + "own_err_immortal_region_call|the initializer of global 'c' holds the result of 'id', which borrows its argument" + "own_err_global_loop_store_static|borrows the global 'K[.]w' and cannot be stored, returned or captured" + "own_err_global_loop_store_script|borrows the global 'K[.]w' and cannot be stored, returned or captured" + "own_err_global_const_store_static|borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" + "own_err_global_const_store_script|borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" + "own_err_global_const_store|borrows 'G' but is used here after 'G' is released or overwritten" + "own_err_global_record_static|borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" + "own_err_global_record|borrows 'G' but is used here after 'G' is released or overwritten" + "own_err_global_let_holder_static|borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" + "own_err_global_let_destructure|borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" + "own_err_global_let_holder|borrows 'G' but is used here after 'G' is released or overwritten" + "own_err_global_outer_holder|borrows 'G' but is used here after 'G' is released or overwritten" + "own_err_global_outer_holder_static|borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" + "own_err_global_uninit_let|borrows 'G' but is used here after 'G' is released or overwritten" + "own_err_global_uninit_let_static|borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" "own_err_global_argument_assigned|a value read out of the global 'G' is lent to a call that may release or overwrite it" "own_err_global_method_assigns|a value read out of the global 'G' is lent to a call that may release or overwrite it" + "own_err_global_argument_transitive|a value read out of the global 'G' is lent to a call that may release or overwrite it" + "own_err_global_method_transitive|a value read out of the global 'G' is lent to a call that may release or overwrite it" + "own_err_global_this_field_move|a value read out of the global 'GK' is lent to a call that may release or overwrite it" + "own_err_global_argument_field_let|a value read out of the global 'G' is lent to a call that may release or overwrite it" + "own_err_global_kept_field|a value read out of the global 'g' is lent to a call that may release or overwrite it" "own_err_global_let_call|borrows the global 'names', but -mm=own cannot prove the global keeps its value" "own_err_global_from_global|the initializer of global 'k' reads a value another owner keeps" "own_err_global_from_field|the initializer of global 'k' reads a value another owner keeps" "own_err_captured_returned|borrows a captured variable and cannot be stored, returned or captured" - "own_err_record_literal_given|an object literal that holds a value it owns is moved only into a local or returned as it is" + "own_err_literal_moved_twice|is used here after its value was moved" + "own_err_interface_keeps_literal|is a parameter, kept here, but -mm=own cannot make the callers of 'KS.take' give it up" + "own_err_interface_keeps_param|is a parameter, kept here, but -mm=own cannot make the callers of 'KV.take' give it up" + "own_err_interface_borrow_param|is a parameter, returned here, but -mm=own cannot tell the callers of 'G.get' that the result borrows it" + "own_err_interface_borrow_element|borrows an element and cannot be stored, returned or captured" + "own_err_interface_borrow_field|borrows a field and cannot be stored, returned or captured" + "own_err_interface_exported_impl|borrows .* but is used here after it may be released or overwritten" + "own_err_interface_family_disagrees|is a parameter, returned here, but -mm=own cannot tell the callers of 'GA.get' that the result borrows it" + "own_err_interface_member_drops|borrows a field but is used here after it may be released or overwritten" + "own_err_interface_from_interface|is a parameter, returned here, but -mm=own cannot tell the callers of 'G.get' that the result borrows it" + "own_err_interface_result_outlives|borrows the result of 'First.first' but is used here after it may be released or overwritten" + "own_err_interface_fn_field_drops|borrows a field but is used here after it may be released or overwritten" + "own_err_interface_fn_field_unknown|borrows a field but is used here after it may be released or overwritten" + "own_err_interface_fn_field_exported|borrows a field but is used here after it may be released or overwritten" + "own_err_interface_fn_any_param|is a parameter, returned here, but -mm=own cannot tell the callers of 'G.take' that the result borrows it" + "own_err_interface_fn_any_result|is a parameter, returned here, but -mm=own cannot tell the callers of 'G.take' that the result borrows it" + "own_err_interface_fn_any_drops|borrows a field but is used here after it may be released or overwritten" + "own_err_interface_public_exported|is a parameter, returned here, but -mm=own cannot tell the callers of 'PA.pick' that the result borrows it" + "own_err_interface_public_reachable|is a parameter, returned here, but -mm=own cannot tell the callers of 'PA.pick' that the result borrows it" + "own_err_interface_member_types|borrows a field but is used here after it may be released or overwritten" + "own_err_interface_member_methods|borrows a field but is used here after it may be released or overwritten" + "own_err_declared_callback|borrows 'gh.c' but is used here after it may be released or overwritten" + "own_err_declared_callback_global|borrows 'gh.c' but is used here after it may be released or overwritten" + "own_err_declared_callback_method|borrows 'gh.c' but is used here after it may be released or overwritten" + "own_err_declared_callback_tuple|borrows 'gh.c' but is used here after it may be released or overwritten" + "own_err_declared_callback_indirect|borrows 'gh.c' but is used here after it may be released or overwritten" + "own_err_declared_callback_wrapper|borrows 'gh.c' but is used here after it may be released or overwritten" + "own_err_declared_callback_class_method|borrows 'gh.c' but is used here after it may be released or overwritten" + "own_err_declared_callback_interface_field|borrows 'gh.c' but is used here after it may be released or overwritten" + "own_err_declared_callback_stored_global|borrows 'gh.c' but is used here after it may be released or overwritten" + "own_err_declared_callback_stored_reference|borrows 'gh.c' but is used here after it may be released or overwritten" + "own_err_declared_callback_cpp_class|borrows 'gh.c' but is used here after it may be released or overwritten" + "own_err_declared_callback_field_reference|borrows 'gh.c' but is used here after it may be released or overwritten" + "own_err_declared_callback_intrinsic|borrows 'gh.c' but is used here after it may be released or overwritten" + "own_err_interface_member_class|borrows a field but is used here after it may be released or overwritten" + "own_err_interface_member_any|borrows a field but is used here after it may be released or overwritten" + "own_err_override_nameable_base|is a parameter, kept here, but -mm=own cannot make the callers of 'Der.keep' give it up" + "own_err_override_imported_base|is a parameter, kept here, but -mm=own cannot make the callers of 'Der.keep' give it up" + "own_err_override_imported_class|is a parameter, kept here, but -mm=own cannot make the callers of 'Der.keep' give it up" "own_err_string_kept_param_known|is used here after its value was moved" "own_err_keeps_mismatch|'@keeps' of 'Pair.constructor' names 'a', but its body keeps 'b'" "own_err_borrows_not_generator|'@borrows' of 'copyOf' names 'w', but it neither makes a generator whose state borrows its arguments nor returns one" @@ -3762,7 +4009,8 @@ set(own_error_cases "own_err_lends_mismatch|'@lends' of 'pick' names 'a', but its body returns a borrow of 'b'" "own_err_lends_not_borrow|'@lends' of 'made' names 'l', but its body does not return a borrow of it" "own_err_lends_and_borrows|'firstOf' cannot say both '@lends' and '@borrows'" - "own_err_lends_two_params|'@lends' names one parameter, the one the result is a borrow of" + "own_err_lends_two_params|'@lends' of 'pick' names 'a', 'b', but its body returns a borrow of 'a'" + "own_err_lends_index_64|'@lends' of 'K[.]pick' names its argument 64, but a method's '@lends' can name only its arguments 0 to 63" "own_err_lends_outlives|borrows the result of 'firstOf' but is used here after it may be released or overwritten" "own_err_generator_method_this|'this value' is a parameter, kept here, but -mm=own cannot make the callers of 'S.chars' give it up" "own_err_string_call_keeps_twice|borrows a field and cannot be stored, returned or captured" @@ -3778,7 +4026,38 @@ set(own_error_cases "own_err_shared_place_arg|borrows a field but is used here after it may be released or overwritten" "own_err_shared_place_two_uses|borrows a field but is used here after it may be released or overwritten" "own_err_shared_place_literal|borrows a field and cannot be stored, returned or captured" - "own_err_shared_place_tuple|borrows a field and cannot be stored, returned or captured") + "own_err_shared_place_tuple|borrows a field and cannot be stored, returned or captured" + "own_err_shared_literal_borrowed|the initializer of global 'b' takes a second reference" + "own_err_iface_field_overwritten|borrows a field but is used here after it may be released or overwritten" + "own_err_iface_field_view_overwritten|borrows a field but is used here after it may be released or overwritten" + "own_err_iface_field_call|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|is used here after its value was moved" + "own_err_read_after_move_view|is used here after its value was moved" + "own_err_read_after_move_call|is used here after its value was moved" + "own_err_read_after_move_loop|is used here after its value was moved" + "own_err_read_after_move_after_loop|is used here after its value was moved" + "own_err_read_after_move_global|is used here after its value was moved" + "own_err_field_let_global_overwritten|borrows a field but is used here after it may be released or overwritten" + "own_err_conditional_move_read|is used here after its value was moved" + "own_err_conditional_move_outer|is moved inside a loop but was made outside it" + "own_err_conditional_move_read_after_join|is moved here on some paths only" + "own_err_conditional_move_field_borrow|is moved here on some paths only" + "own_err_conditional_move_catch|is moved here on some paths only" + "own_err_conditional_move_bound_method|is moved here on some paths only" + "own_err_conditional_move_view|is moved here on some paths only" + "own_err_conditional_move_old_view|is moved here on some paths only" + "own_err_borrowing_record_stored|borrows the result of .* and cannot be stored, returned or captured" + "own_err_borrowing_record_outlives|borrows the result of .* but is used here after it may be released or overwritten" + "own_err_borrowing_record_kept_arg|borrows a field and cannot be stored, returned or captured" + "own_err_borrowing_record_let_outlives|borrows '.*' but is used here after '.*' is released or overwritten" + "own_err_borrowing_record_returned_local|borrows '.*' and cannot be stored, returned or captured" + "own_err_borrowing_record_field_written|borrows a field and cannot be stored, returned or captured" + "own_err_borrowing_record_cell|borrows a captured variable but is used here after it may be released or overwritten" + "own_err_borrowing_record_kept_param|borrows '.*' but is used here after it may be released or overwritten" + "own_err_borrowing_record_place_overwritten|borrows a field but is used here after it may be released or overwritten" + "own_err_borrowing_record_open|borrows '.*' and cannot be stored, returned or captured" + "own_err_borrowing_record_borrowed_again|borrows the result of .* but is used here after it may be released or overwritten") foreach(own_error_case ${own_error_cases}) string(FIND "${own_error_case}" "|" own_error_sep) string(SUBSTRING "${own_error_case}" 0 ${own_error_sep} own_error_name) diff --git a/tslang/test/tester/own-exe-exports.cmake b/tslang/test/tester/own-exe-exports.cmake new file mode 100644 index 000000000..80905c78d --- /dev/null +++ b/tslang/test/tester/own-exe-exports.cmake @@ -0,0 +1,61 @@ +# An executable's exports are seen by no other module (own spec 31, USER decision): built with +# `tslang --emit=exe`, own_interface_exe_exports.ts - an exported interface and an exported global +# holding a value of it - compiles under -mm=own and runs; built as an object file or a DLL, which +# another module may import, the interface is open and `G.get` stays an error. test-runner builds an +# object file with --entry-point and links it itself, so it cannot show the first half. + +cmake_minimum_required(VERSION 3.17.3) + +foreach(var TSLANG SOURCE WORK_DIR LLVM_LIB TSLANG_LIB) + if(NOT DEFINED ${var}) + message(FATAL_ERROR "${var} is required") + endif() +endforeach() + +file(REMOVE_RECURSE "${WORK_DIR}") +file(MAKE_DIRECTORY "${WORK_DIR}") + +set(failures "") + +execute_process( + COMMAND "${TSLANG}" --emit=exe -mm=own --no-default-lib --opt --opt_level=3 + "--llvm-lib-path=${LLVM_LIB}" "--tslang-lib-path=${TSLANG_LIB}" + "${SOURCE}" -o=program.exe + WORKING_DIRECTORY "${WORK_DIR}" + OUTPUT_VARIABLE out + ERROR_VARIABLE err + RESULT_VARIABLE status) +if(NOT status EQUAL 0) + list(APPEND failures " --emit=exe: exit ${status}: ${out}${err}") +else() + execute_process( + COMMAND "${WORK_DIR}/program.exe" + WORKING_DIRECTORY "${WORK_DIR}" + OUTPUT_VARIABLE out + ERROR_VARIABLE err + RESULT_VARIABLE status) + if(NOT status EQUAL 0 OR NOT out MATCHES "5\n.*done\.") + list(APPEND failures " the program: exit ${status}: ${out}${err}") + endif() +endif() + +foreach(emit obj dll) + execute_process( + COMMAND "${TSLANG}" --emit=${emit} -mm=own --no-default-lib + "--llvm-lib-path=${LLVM_LIB}" "--tslang-lib-path=${TSLANG_LIB}" + "${SOURCE}" -o=library.${emit} + WORKING_DIRECTORY "${WORK_DIR}" + OUTPUT_VARIABLE out + ERROR_VARIABLE err + RESULT_VARIABLE status) + if(status EQUAL 0 OR NOT "${out}${err}" MATCHES "cannot tell the callers of 'G.get' that the result borrows it") + list(APPEND failures " --emit=${emit}: exit ${status}, expected the error on 'G.get': ${out}${err}") + endif() +endforeach() + +if(failures) + string(REPLACE ";" "\n" report "${failures}") + message(FATAL_ERROR "an executable's exports under -mm=own:\n${report}") +endif() + +message(STATUS "an executable's exports leave its interfaces closed; an object's and a DLL's do not") diff --git a/tslang/test/tester/own-exe-run.cmake b/tslang/test/tester/own-exe-run.cmake new file mode 100644 index 000000000..9b3ad5746 --- /dev/null +++ b/tslang/test/tester/own-exe-run.cmake @@ -0,0 +1,41 @@ +# Builds a program under -mm=own with `tslang --emit=exe` and runs it: one line of the output must +# be EXPECT (a regex), and the last "done.". For a program that exports from its root, whose +# exports only an executable keeps from other modules (own spec 31, USER decision): test-runner +# builds an object file with --entry-point and links it itself, which another module could +# import, so it cannot build one. + +cmake_minimum_required(VERSION 3.17.3) + +foreach(var TSLANG SOURCE EXPECT WORK_DIR LLVM_LIB TSLANG_LIB) + if(NOT DEFINED ${var}) + message(FATAL_ERROR "${var} is required") + endif() +endforeach() + +file(REMOVE_RECURSE "${WORK_DIR}") +file(MAKE_DIRECTORY "${WORK_DIR}") + +execute_process( + COMMAND "${TSLANG}" --emit=exe -mm=own --no-default-lib --opt --opt_level=3 + "--llvm-lib-path=${LLVM_LIB}" "--tslang-lib-path=${TSLANG_LIB}" + "${SOURCE}" -o=program.exe + WORKING_DIRECTORY "${WORK_DIR}" + OUTPUT_VARIABLE out + ERROR_VARIABLE err + RESULT_VARIABLE status) +if(NOT status EQUAL 0) + message(FATAL_ERROR "--emit=exe -mm=own: exit ${status}: ${out}${err}") +endif() + +execute_process( + COMMAND "${WORK_DIR}/program.exe" + WORKING_DIRECTORY "${WORK_DIR}" + OUTPUT_VARIABLE out + ERROR_VARIABLE err + RESULT_VARIABLE status) +# EXPECT is a whole line of the output, and "done." its last line +if(NOT status EQUAL 0 OR NOT out MATCHES "(^|\n)${EXPECT}\r?\n" OR NOT out MATCHES "(^|\n)done\\.\r?\n?$") + message(FATAL_ERROR "the program: exit ${status}, expected a line '${EXPECT}' and a last line 'done.': ${out}${err}") +endif() + +message(STATUS "${out}") diff --git a/tslang/test/tester/own/own_borrowing_record.ts b/tslang/test/tester/own/own_borrowing_record.ts new file mode 100644 index 000000000..9a99f2aeb --- /dev/null +++ b/tslang/test/tester/own/own_borrowing_record.ts @@ -0,0 +1,57 @@ +// @strict-null false +// -mm=own: a method that returns an object literal holding `this` and its parameter returns a +// borrowing record (spec 32): the caller owns the box, which may not outlive the receiver and the +// argument, and whose release frees only what the record owns - here nothing but itself. A call +// through a closed interface gets the record's facts; `closest` keeps the best record in a local +// (moved there on some paths only) and returns it, so its own result borrows its `ray` and `sc`. +// churn() keeps what it allocates alive, so a block freed too early is reused before it is read. +class V { + constructor(public x: number) {} + static plus(a: V, b: V) { return new V(a.x + b.x); } +} + +function churn() { + const keep: V[] = []; + for (let i = 0; i < 200; i++) { + keep.push(new V(77777)); + } + return keep.length; +} + +interface Ray { start: V; dir: V; } +interface Hit { thing: Th; ray: Ray; dist: number; } +interface Th { hit(ray: Ray): Hit; k: number; } +class A implements Th { k: number = 1; hit(ray: Ray): Hit { return { thing: this, ray: ray, dist: ray.dir.x }; } } +class B implements Th { + k: number = 2; + hit(ray: Ray): Hit { + if (ray.dir.x > 5) return null; + return { thing: this, ray: ray, dist: ray.dir.x * 2 }; + } +} +class Sc { ts: Th[] = [new A(), new B()]; } + +function closest(ray: Ray, sc: Sc) { + let best = +Infinity; + let bestHit: Hit = undefined; + for (let i in sc.ts) { + let h = sc.ts[i].hit(ray); + if (h != null && h.dist < best) { bestHit = h; best = h.dist; } + } + return bestHit; +} + +function main() { + const sc = new Sc(); + let s: number = 0; + for (let i = 0; i < 1000; i++) { + const h = closest({ start: new V(0), dir: new V(i % 10) }, sc); + churn(); + s += h.thing.k + h.dist + h.ray.start.x; + assert(h.ray.dir.x == i % 10); + } + + print(s); // 5500: A always wins (dist d against B's 2d), k 1 + assert(s == 5500); + print("done."); +} diff --git a/tslang/test/tester/own/own_borrowing_record_arg.ts b/tslang/test/tester/own/own_borrowing_record_arg.ts new file mode 100644 index 000000000..709dd0ede --- /dev/null +++ b/tslang/test/tester/own/own_borrowing_record_arg.ts @@ -0,0 +1,110 @@ +// @strict-null false +// -mm=own: object literals that borrow some fields and own others (spec 32) - a parameter, a field +// read, an owning local's value, a captured parameter, beside fresh values - as call arguments, as +// `const` and `let` locals, and returned from a method. The box is a temporary or a local's like any +// fresh block; its release frees the fresh fields and leaves the borrowed ones to their owners, which +// are read again after it is gone. churn() keeps what it allocates alive, so a block freed too early +// (or freed twice) is reused before it is read. +class V { + constructor(public x: number) {} + static plus(a: V, b: V) { return new V(a.x + b.x); } +} + +function churn() { + const keep: V[] = []; + for (let i = 0; i < 200; i++) { + keep.push(new V(77777)); + } + return keep.length; +} + +interface Ray { start: V; dir: V; } + +function len(r: Ray): number { return r.start.x + r.dir.x; } + +// two parameters: the ray tracer's `this.traceRay({ start: pos, dir: rd }, scene, depth + 1)` +function viaParams(pos: V, rd: V) { return len({ start: pos, dir: rd }); } + +// a parameter and a fresh value +function viaParamFresh(pos: V) { return len({ start: pos, dir: new V(1) }); } + +class Cam { + pos: V = new V(3); + + // a field read and a fresh value + go() { return len({ start: this.pos, dir: new V(1) }); } + + // a `const` holding the literal, read while its sources live + held() { + const r: Ray = { start: this.pos, dir: new V(2) }; + churn(); + return len(r) + r.start.x; + } + + // a `let` holding it + heldLet() { + let r: Ray = { start: this.pos, dir: new V(4) }; + churn(); + const n = len(r); + return n + r.dir.x; + } + + // returned: the result borrows `this` + mk(): Ray { return { start: this.pos, dir: new V(5) }; } +} + +// an owning local's value and a captured parameter: the ray tracer's addLight +function natural(pos: V) { + const f = (k: number) => { + let d = V.plus(pos, new V(k)); + const n = len({ start: pos, dir: d }); + churn(); + return n + d.x; + }; + return f(1) + f(2); +} + +function main() { + const p = new V(10); + const q = new V(20); + let s: number = 0; + for (let i = 0; i < 1000; i++) { + s += viaParams(p, q); + s += viaParamFresh(p); + churn(); + } + + assert(s == 41000); + + const c = new Cam(); + for (let i = 0; i < 1000; i++) { + assert(c.go() == 4); + assert(c.held() == 8); + assert(c.heldLet() == 11); + const r = c.mk(); + churn(); + assert(r.start.x == 3 && r.dir.x == 5); + } + + assert(c.pos.x == 3); + + let t: number = 0; + for (let i = 0; i < 1000; i++) { + t += natural(p); + } + + assert(t == 66000); + + // a `let` read into a field (once own_err_record_literal_let): the record borrows its value, and + // the `let` keeps its own release + let lp = new V(1); + for (let i = 0; i < 1000; i++) { + assert(len({ start: lp, dir: new V(2) }) == 3); + churn(); + } + + assert(lp.x == 1); + assert(p.x == 10 && q.x == 20); + print(s, t); + print("done."); +} diff --git a/tslang/test/tester/own/own_conditional_move.ts b/tslang/test/tester/own/own_conditional_move.ts new file mode 100644 index 000000000..0c8796563 --- /dev/null +++ b/tslang/test/tester/own/own_conditional_move.ts @@ -0,0 +1,181 @@ +// @strict-null false +// A value moved out of an owning local on some paths only (spec 30): the local's release where the +// paths join is placed instead on the paths that did not move it, so each path frees the value +// exactly once - through its new owner where it moved, through the local where it did not. +class V { + constructor(public x: number) {} + static plus(a: V, b: V) { + return new V(a.x + b.x); + } +} + +class H { + c: V | null = null; +} + +class Holder { + constructor(public v: V) {} +} + +// keeps its argument: a call of it is a move +function keeps(v: V) { + const h = new Holder(v); + return h.v.x; +} + +let finallies: number = 0; + +function mkt(n: number) { + if (n > 5) { + throw n; + } + + return new V(n); +} + +// a move on some paths only, then a call that may throw: the edge's release comes before the +// call, and the unwind path, which joins both, frees nothing of `v` (only `best`, if it moved) +function unwinds(c: boolean, n: number) { + let best: V = undefined; + let v = mkt(1); + try { + if (c) { + best = v; + } + + mkt(n); + } finally { + finallies++; + } + + return best != undefined; +} + +function main() { + const vs = [new V(1), new V(3), new V(2)]; + let best: V = undefined; + let m: number = 0; + for (let i in vs) { + let v = V.plus(vs[i], vs[i]); + if (v.x > m) { + best = v; // moved into best on this path only + m = v.x; + } + } + + print(best.x); // 6 + assert(best.x == 6); + + // straight-line conditional move, taken + let w = new V(7); + let keep: V = undefined; + if (m > 5) { + keep = w; + } else { + print("not moved"); + } + + print(keep.x); // 7 + assert(keep.x == 7); + + // ...and not taken: the local still owns it, and frees it + let w2 = new V(8); + let keep2: V = undefined; + if (m > 100) { + keep2 = w2; + } else { + print("not moved"); + } + + assert(keep2 == undefined); + + // a churn loop where a freed or double-freed block would show + let s: number = 0; + for (let k = 0; k < 100000; k++) { + let t = new V(k % 3); + let b: V = undefined; + s += t.x; + if (k % 2 == 0) { + b = t; + s += t.x; // a read through the new owner (spec 29) + } + + if (b != undefined) { + s += b.x; + } + } + + print(s); // 99999 + 50000 + 50000 + assert(s == 199999); + + // an assignment over a value that may have moved frees the old value only where it did not + let r = new V(0); + let kept: V = undefined; + for (let k = 0; k < 1000; k++) { + if (k % 3 == 0) { + kept = r; + } + + r = new V(k); + } + + print(r.x, kept.x); // 999 998 + assert(r.x == 999 && kept.x == 998); + + // moved on every path, then given a new value on some: the path that gave it one releases it + // before the paths join + let given: number = 0; + for (let k = 0; k < 1000; k++) { + const h = new H(); + let a = new V(k); + h.c = a; + if (h.c.x % 2 == 0) { + a = new V(k + 1); + given += a.x; + } + } + + print(given); // 1 + 3 + ... + 999 + assert(given == 250000); + + // moved into a call on one arm of a `?:` only (the ray tracer's `shade`) + let taken: number = 0; + for (let k = 0; k < 1000; k++) { + let p = new V(k); + let q = new V(k + 1); + taken += k % 2 == 0 ? keeps(p) : q.x; + } + + print(taken); // (0 + 2 + ... + 998) + (2 + 4 + ... + 1000) + assert(taken == 500000); + + // ...and on the right of `&&`: the edge that skips it carries the `&&`'s value, so the release + // goes in a block of its own on that edge, which passes the value on + let anded: number = 0; + for (let k = 0; k < 1000; k++) { + let p = new V(k); + if (k % 2 == 0 && keeps(p) >= 0) { + anded++; + } + } + + print(anded); // 500 + assert(anded == 500); + + let moved: number = 0; + let thrown: number = 0; + for (let k = 0; k < 20000; k++) { + try { + if (unwinds(k % 2 == 0, k % 4 < 2 ? 1 : 9)) { + moved++; + } + } catch { + thrown++; + } + } + + print(moved, thrown, finallies); // 5000 10000 20000 + assert(moved == 5000 && thrown == 10000 && finallies == 20000); + + print("done."); +} diff --git a/tslang/test/tester/own/own_declared_scalar_no_drops.ts b/tslang/test/tester/own/own_declared_scalar_no_drops.ts new file mode 100644 index 000000000..dee312ffd --- /dev/null +++ b/tslang/test/tester/own/own_declared_scalar_no_drops.ts @@ -0,0 +1,70 @@ +// -mm=own: a C function declared without `@nodrops` whose parameters and result are numbers - the +// ray tracer's `sqrt` and `floor` - is given no block of this module and destroys none (spec 31), so +// a borrow of a field survives a call that reaches it: directly, through a function, and through a +// method called through an interface closed in this module. A declaration that takes a block still +// may destroy anything (own_err_declared_drops). +declare function sqrt(v: number): number; +declare function floor(v: number): number; + +class V { + constructor(public x: number) {} +} + +class H { + c: V = new V(16); +} + +class M { + static root(v: number): number { + return floor(sqrt(v)); + } +} + +interface Shape { + size(h: H): number; +} + +class Sq implements Shape { + size(h: H): number { + return M.root(h.c.x); + } +} + +function churn() { + const keep: V[] = []; + for (let i = 0; i < 2000; i++) { + keep.push(new V(77777)); + } + + return keep.length; +} + +function direct(h: H) { + const c = h.c; + const r = sqrt(c.x); + churn(); + return r + c.x; +} + +function viaMethod(h: H) { + const c = h.c; + const r = M.root(c.x + 9); + churn(); + return r + c.x; +} + +function viaInterface(s: Shape, h: H) { + const c = h.c; + const r = s.size(h); + churn(); + return r + c.x; +} + +function main() { + const h = new H(); + assert(direct(h) == 20, "across sqrt"); + assert(viaMethod(h) == 21, "across a function that calls sqrt and floor"); + assert(viaInterface(new Sq(), h) == 20, "across a call through an interface"); + print(direct(h), viaMethod(h), viaInterface(new Sq(), h)); // 20 21 20 + print("done."); +} diff --git a/tslang/test/tester/own/own_err_borrow_bound_method.ts b/tslang/test/tester/own/own_err_borrow_bound_method.ts index 8a197cda2..4d4cf5cbc 100644 --- a/tslang/test/tester/own/own_err_borrow_bound_method.ts +++ b/tslang/test/tester/own/own_err_borrow_bound_method.ts @@ -1,6 +1,7 @@ // -mm=own, phase 2 rejects: `f` is `b.m` bound to what `b` borrows, so calling it after `a` is -// overwritten reads the old `a`. What a borrower's reads produce is the borrower too. (`a` is read -// after `let b = a`, so `b` cannot take its value over and borrows it.) +// overwritten reads the old `a`. What a borrower's reads produce is the borrower too. (`a` is given +// to two `let`s, so neither takes its value over: each borrows it. One `let` would take it over, +// and `a.x` after it would read through it, spec 29.) class C { v: number[] = []; constructor(public x: number) {} @@ -19,7 +20,8 @@ function churn() { function main() { let a = new C(1); let b = a; - print(a.x); + let c = a; + print(a.x, c.x); const f = b.m; a = new C(2); churn(); diff --git a/tslang/test/tester/own/own_err_borrow_catch_local.ts b/tslang/test/tester/own/own_err_borrow_catch_local.ts index 3d218b686..2e0e00db5 100644 --- a/tslang/test/tester/own/own_err_borrow_catch_local.ts +++ b/tslang/test/tester/own/own_err_borrow_catch_local.ts @@ -1,6 +1,6 @@ // -mm=own, phase 2 rejects: a catch clause's locals own nothing, so `x` is one more name for what -// `b` borrows - and it is read after `a` is overwritten. (`a` is read after `let b = a`, so `b` -// cannot take its value over and borrows it.) +// `b` borrows - and it is read after `a` is overwritten. (`a` is given to two `let`s, so neither +// takes its value over: each borrows it. One `let` would take it over, spec 29.) class C { v: number[] = []; constructor(public x: number) {} @@ -20,7 +20,8 @@ function main() { let a = new C(1); a.v.push(1); let b = a; - print(a.x); + let c = a; + print(a.x, c.x); try { throw 1; } catch (e) { diff --git a/tslang/test/tester/own/own_err_borrow_derived_ref.ts b/tslang/test/tester/own/own_err_borrow_derived_ref.ts index 1921f6f28..414db8ab4 100644 --- a/tslang/test/tester/own/own_err_borrow_derived_ref.ts +++ b/tslang/test/tester/own/own_err_borrow_derived_ref.ts @@ -1,5 +1,7 @@ // -mm=own, phase 2 rejects: the loop iterates `b.v`, a field of what `b` borrows, through one -// reference into it that every iteration reads - after the body has overwritten `a`. +// reference into it that every iteration reads - after the body has overwritten `a`. (`a` is given +// to two `let`s, so neither takes its value over: each borrows it. One `let` would take it over, +// and the assignments in the loop would release only what they stored, spec 30.) class C { v: number[] = []; constructor(public x: number) {} @@ -21,7 +23,8 @@ function main() { a.v.push(2); a.v.push(3); let b = a; - let t: number = 0; + let c = a; + let t: number = c.x - 1; for (const x of b.v) { a = new C(2); churn(); diff --git a/tslang/test/tester/own/own_err_borrow_finally_local.ts b/tslang/test/tester/own/own_err_borrow_finally_local.ts index cf190750d..b9b484354 100644 --- a/tslang/test/tester/own/own_err_borrow_finally_local.ts +++ b/tslang/test/tester/own/own_err_borrow_finally_local.ts @@ -1,5 +1,5 @@ -// -mm=own, phase 2 rejects: the same through a finally clause's local. (`a` is read after -// `let b = a`, so `b` cannot take its value over and borrows it.) +// -mm=own, phase 2 rejects: the same through a finally clause's local. (`a` is given to two +// `let`s, so neither takes its value over: each borrows it. One `let` would take it over, spec 29.) class C { v: number[] = []; constructor(public x: number) {} @@ -19,7 +19,8 @@ function main() { let a = new C(1); a.v.push(1); let b = a; - print(a.x); + let c = a; + print(a.x, c.x); try { print(b.x); } finally { diff --git a/tslang/test/tester/own/own_err_borrowed_result_escapes.ts b/tslang/test/tester/own/own_err_borrowed_result_escapes.ts index 00c0a6fe2..083ee4d9f 100644 --- a/tslang/test/tester/own/own_err_borrowed_result_escapes.ts +++ b/tslang/test/tester/own/own_err_borrowed_result_escapes.ts @@ -1,6 +1,8 @@ // -mm=own, phase 4 rejects: `firstOf` is passed as a value, so `apply` calls it where -mm=own // cannot see which function it is, and would take the result as its own and release it. -// `firstOf`'s body keeps rc's convention and fails, saying why. +// `firstOf`'s body keeps rc's convention and fails, saying why. (Passed straight to `apply`, the +// call through `g` reaches only `firstOf` and gets its facts, spec 33; a `let` holding it is a value +// -mm=own does not follow.) class C { constructor(public x: number) {} } @@ -19,5 +21,6 @@ function apply(g: (h: H) => C, h: H) { function main() { const h = new H(); - print(apply(firstOf, h)); + let g = firstOf; + print(apply(g, h)); } diff --git a/tslang/test/tester/own/own_err_borrowing_record_borrowed_again.ts b/tslang/test/tester/own/own_err_borrowing_record_borrowed_again.ts new file mode 100644 index 000000000..93661710c --- /dev/null +++ b/tslang/test/tester/own/own_err_borrowing_record_borrowed_again.ts @@ -0,0 +1,30 @@ +// -mm=own rejects: `id(h)` is a borrow of the record `h` (spec 32), so it may not be read after what +// `h` borrows - `a` - is given a new value, its old one freed. +class V { + constructor(public x: number) {} +} + +function churn() { + const keep: V[] = []; + for (let i = 0; i < 200; i++) { + keep.push(new V(77777)); + } + return keep.length; +} + +interface Ray { start: V; dir: V; } +interface Hit { thing: Th; ray: Ray; dist: number; } +interface Th { hit(ray: Ray): Hit; k: number; } +class A implements Th { k: number = 1; hit(ray: Ray): Hit { return { thing: this, ray: ray, dist: ray.dir.x }; } } + +function id(r: Hit): Hit { return r; } + +function main() { + let a = new A(); + const ray: Ray = { start: new V(1), dir: new V(2) }; + let h = a.hit(ray); + const h2 = id(h); + a = new A(); + churn(); + print(h2.thing.k); +} diff --git a/tslang/test/tester/own/own_err_borrowing_record_cell.ts b/tslang/test/tester/own/own_err_borrowing_record_cell.ts new file mode 100644 index 000000000..3d49aff38 --- /dev/null +++ b/tslang/test/tester/own/own_err_borrowing_record_cell.ts @@ -0,0 +1,32 @@ +// -mm=own rejects: the literal borrows the value of a captured variable (spec 32), and the closure +// gives the variable a new value - its old one freed - before the literal is read. +class V { + constructor(public x: number) {} +} + +function churn() { + const keep: V[] = []; + for (let i = 0; i < 200; i++) { + keep.push(new V(77777)); + } + return keep.length; +} + +interface Ray { start: V; dir: V; } + +function len(r: Ray) { return r.start.x + r.dir.x; } + +function run() { + let v = new V(1); + const g = () => { + const r: Ray = { start: v, dir: new V(2) }; + v = new V(5); + churn(); + return len(r); + }; + return g(); +} + +function main() { + print(run()); +} diff --git a/tslang/test/tester/own/own_err_borrowing_record_field_written.ts b/tslang/test/tester/own/own_err_borrowing_record_field_written.ts new file mode 100644 index 000000000..a3215e64a --- /dev/null +++ b/tslang/test/tester/own/own_err_borrowing_record_field_written.ts @@ -0,0 +1,23 @@ +// -mm=own rejects: a field written through a borrowing record (spec 32) would release the value the +// record borrows, and the record's release would leave the one written. +class V { + constructor(public x: number) {} +} + +interface Ray { start: V; dir: V; } + +function len(r: Ray) { return r.start.x + r.dir.x; } + +class Cam { + pos: V = new V(3); + go() { + const r: Ray = { start: this.pos, dir: new V(1) }; + r.start = new V(7); + return len(r); + } +} + +function main() { + const c = new Cam(); + print(c.go(), c.pos.x); +} diff --git a/tslang/test/tester/own/own_err_borrowing_record_kept_arg.ts b/tslang/test/tester/own/own_err_borrowing_record_kept_arg.ts new file mode 100644 index 000000000..14077b267 --- /dev/null +++ b/tslang/test/tester/own/own_err_borrowing_record_kept_arg.ts @@ -0,0 +1,39 @@ +// -mm=own rejects: the literal borrows `this.pos` (spec 32), and `take` keeps its argument, where +// it would outlive the field it borrows. +class V { + constructor(public x: number) {} +} + +function churn() { + const keep: V[] = []; + for (let i = 0; i < 200; i++) { + keep.push(new V(77777)); + } + return keep.length; +} + +interface Ray { start: V; dir: V; } + +class Sink { + rays: Ray[] = []; + take(r: Ray) { this.rays.push(r); } +} + +class Cam { + pos: V = new V(3); + send(s: Sink) { s.take({ start: this.pos, dir: new V(1) }); } +} + +class Box { + c: Cam = new Cam(); +} + +function main() { + const s = new Sink(); + const b = new Box(); + b.c.send(s); + b.c = new Cam(); + b.c.pos.x = 9; + churn(); + print(s.rays[0].start.x); +} diff --git a/tslang/test/tester/own/own_err_borrowing_record_kept_param.ts b/tslang/test/tester/own/own_err_borrowing_record_kept_param.ts new file mode 100644 index 000000000..a7cb76b37 --- /dev/null +++ b/tslang/test/tester/own/own_err_borrowing_record_kept_param.ts @@ -0,0 +1,36 @@ +// -mm=own rejects: the literal borrows the parameter `p` (spec 32), which this function keeps and +// moves into the sink before the literal is read; the sink may free it. +class V { + constructor(public x: number) {} +} + +function churn() { + const keep: V[] = []; + for (let i = 0; i < 200; i++) { + keep.push(new V(77777)); + } + return keep.length; +} + +interface Ray { start: V; dir: V; } + +function len(r: Ray) { return r.start.x + r.dir.x; } + +class Sink { + vs: V[] = []; + take(v: V) { this.vs.push(v); } + clear() { this.vs.pop(); } +} + +function give(s: Sink, p: V) { + const r: Ray = { start: p, dir: new V(2) }; + s.take(p); + s.clear(); + churn(); + return len(r); +} + +function main() { + const s = new Sink(); + print(give(s, new V(1))); +} diff --git a/tslang/test/tester/own/own_err_borrowing_record_let_outlives.ts b/tslang/test/tester/own/own_err_borrowing_record_let_outlives.ts new file mode 100644 index 000000000..a45f486aa --- /dev/null +++ b/tslang/test/tester/own/own_err_borrowing_record_let_outlives.ts @@ -0,0 +1,25 @@ +// -mm=own rejects: the literal borrows the value `d` holds (spec 32), and `d` is given a new value +// - its old one freed - before the literal is read. +class V { + constructor(public x: number) {} +} + +function churn() { + const keep: V[] = []; + for (let i = 0; i < 200; i++) { + keep.push(new V(77777)); + } + return keep.length; +} + +interface Ray { start: V; dir: V; } + +function len(r: Ray) { return r.start.x + r.dir.x; } + +function main() { + let d = new V(1); + const r: Ray = { start: d, dir: new V(2) }; + d = new V(3); + churn(); + print(len(r)); +} diff --git a/tslang/test/tester/own/own_err_borrowing_record_open.ts b/tslang/test/tester/own/own_err_borrowing_record_open.ts new file mode 100644 index 000000000..760cb2408 --- /dev/null +++ b/tslang/test/tester/own/own_err_borrowing_record_open.ts @@ -0,0 +1,25 @@ +// -mm=own rejects: `mk` is exported, so a caller in another module, which cannot see that its result +// borrows `p` (spec 32), would not bound it by `p`; it has to say so with `@lends`. +export class V { + constructor(public x: number) {} +} + +function churn() { + const keep: V[] = []; + for (let i = 0; i < 200; i++) { + keep.push(new V(77777)); + } + return keep.length; +} + +export interface Ray { start: V; dir: V; } + +export function mk(p: V): Ray { + return { start: p, dir: new V(2) }; +} + +function main() { + const r = mk(new V(1)); + churn(); + print(r.start.x); +} diff --git a/tslang/test/tester/own/own_err_borrowing_record_outlives.ts b/tslang/test/tester/own/own_err_borrowing_record_outlives.ts new file mode 100644 index 000000000..be5657ad7 --- /dev/null +++ b/tslang/test/tester/own/own_err_borrowing_record_outlives.ts @@ -0,0 +1,27 @@ +// -mm=own rejects: `h` is a borrowing record (spec 32) that holds `a`, and `a` is given a new value +// - its old one freed - before `h` is read. +class V { + constructor(public x: number) {} +} + +function churn() { + const keep: V[] = []; + for (let i = 0; i < 200; i++) { + keep.push(new V(77777)); + } + return keep.length; +} + +interface Ray { start: V; dir: V; } +interface Hit { thing: Th; ray: Ray; dist: number; } +interface Th { hit(ray: Ray): Hit; k: number; } +class A implements Th { k: number = 1; hit(ray: Ray): Hit { return { thing: this, ray: ray, dist: ray.dir.x }; } } + +function main() { + let a = new A(); + const ray: Ray = { start: new V(1), dir: new V(2) }; + let h = a.hit(ray); + a = new A(); + churn(); + print(h.thing.k); +} diff --git a/tslang/test/tester/own/own_err_borrowing_record_place_overwritten.ts b/tslang/test/tester/own/own_err_borrowing_record_place_overwritten.ts new file mode 100644 index 000000000..836880029 --- /dev/null +++ b/tslang/test/tester/own/own_err_borrowing_record_place_overwritten.ts @@ -0,0 +1,29 @@ +// -mm=own rejects: the literal borrows the field `c.pos` (spec 32), which is given a new value - its +// old one freed - before the literal is read. +class V { + constructor(public x: number) {} +} + +function churn() { + const keep: V[] = []; + for (let i = 0; i < 200; i++) { + keep.push(new V(77777)); + } + return keep.length; +} + +interface Ray { start: V; dir: V; } + +function len(r: Ray) { return r.start.x + r.dir.x; } + +class Cam { + pos: V = new V(3); +} + +function main() { + const c = new Cam(); + const r: Ray = { start: c.pos, dir: new V(1) }; + c.pos = new V(9); + churn(); + print(len(r)); +} diff --git a/tslang/test/tester/own/own_err_borrowing_record_returned_local.ts b/tslang/test/tester/own/own_err_borrowing_record_returned_local.ts new file mode 100644 index 000000000..f2ddc079d --- /dev/null +++ b/tslang/test/tester/own/own_err_borrowing_record_returned_local.ts @@ -0,0 +1,26 @@ +// -mm=own rejects: the returned literal borrows the value of a local (spec 32), which the function +// frees as it returns; only what a parameter or `this` holds may be lent to the caller. +class V { + constructor(public x: number) {} +} + +function churn() { + const keep: V[] = []; + for (let i = 0; i < 200; i++) { + keep.push(new V(77777)); + } + return keep.length; +} + +interface Ray { start: V; dir: V; } + +function mk(): Ray { + let d = new V(1); + return { start: d, dir: new V(2) }; +} + +function main() { + const r = mk(); + churn(); + print(r.start.x); +} diff --git a/tslang/test/tester/own/own_err_borrowing_record_stored.ts b/tslang/test/tester/own/own_err_borrowing_record_stored.ts new file mode 100644 index 000000000..c905b9fda --- /dev/null +++ b/tslang/test/tester/own/own_err_borrowing_record_stored.ts @@ -0,0 +1,37 @@ +// @strict-null false +// -mm=own rejects: `sc.ts[0].hit(ray)` is a borrowing record (spec 32) that holds `sc.ts[0]` and +// `ray`; stored into a global's field it would outlive both. +class V { + constructor(public x: number) {} +} + +function churn() { + const keep: V[] = []; + for (let i = 0; i < 200; i++) { + keep.push(new V(77777)); + } + return keep.length; +} + +interface Ray { start: V; dir: V; } +interface Hit { thing: Th; ray: Ray; dist: number; } +interface Th { hit(ray: Ray): Hit; k: number; } +class A implements Th { k: number = 1; hit(ray: Ray): Hit { return { thing: this, ray: ray, dist: ray.dir.x }; } } +class Sc { ts: Th[] = [new A()]; } +class Keep { h: Hit = null; } +class Holder { r: Ray = { start: new V(1), dir: new V(2) }; } + +const keep = new Keep(); + +function store(ray: Ray, sc: Sc) { + keep.h = sc.ts[0].hit(ray); +} + +function main() { + const sc = new Sc(); + const h = new Holder(); + store(h.r, sc); + h.r = { start: new V(5), dir: new V(6) }; + churn(); + print(keep.h.ray.start.x); +} diff --git a/tslang/test/tester/own/own_err_call_result_bounded.ts b/tslang/test/tester/own/own_err_call_result_bounded.ts new file mode 100644 index 000000000..e7ae1fa33 --- /dev/null +++ b/tslang/test/tester/own/own_err_call_result_bounded.ts @@ -0,0 +1,56 @@ +// A generator's state borrows what it was made from (spec 34): unlike the result of another call of +// a function this module defines, it points into its argument, so iterating it is a use of the +// borrow `s.cam`, and `replace` may free that block first. +class V { + constructor(public x: number) {} +} + +class Cam { + constructor(public f: V) {} +} + +class Scene { + constructor(public cam: Cam) {} +} + +@noinline +function makeScene(n: number) { + return new Scene(new Cam(new V(n))); +} + +function replace(s: Scene) { + s.cam = new Cam(new V(2)); +} + +function churn() { + const junk: V[] = []; + for (let i = 0; i < 50; i++) { + junk.push(new V(999)); + } + + return junk.length; +} + +function* xs(c: Cam) { + yield c.f.x; +} + +function total(s: Scene) { + const it = xs(s.cam); + replace(s); + churn(); + let t: number = 0; + for (const v of it) { + t += v; + } + + return t; +} + +function main() { + const s = makeScene(5); + const t = total(s); + print(t); + assert(t == 5); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_call_result_record.ts b/tslang/test/tester/own/own_err_call_result_record.ts new file mode 100644 index 000000000..98800e214 --- /dev/null +++ b/tslang/test/tester/own/own_err_call_result_record.ts @@ -0,0 +1,56 @@ +// A borrowing record (spec 32) returned by a call points into the arguments it was made from +// (spec 34): unlike the result of another call of a function this module defines, a use of it is a +// use of the borrow `s.cam`, and `replace` may free that block first. +class V { + constructor(public x: number) {} +} + +class Cam { + constructor(public f: V) {} +} + +class Scene { + constructor(public cam: Cam) {} +} + +interface Hit { + v: V; + n: number; +} + +@noinline +function makeScene(n: number) { + return new Scene(new Cam(new V(n))); +} + +function replace(s: Scene) { + s.cam = new Cam(new V(2)); +} + +function churn() { + const junk: V[] = []; + for (let i = 0; i < 50; i++) { + junk.push(new V(999)); + } + + return junk.length; +} + +function hit(c: Cam): Hit { + return { v: c.f, n: 1 }; +} + +function total(s: Scene) { + const h = hit(s.cam); + replace(s); + churn(); + return h.v.x + h.n; +} + +function main() { + const s = makeScene(5); + const t = total(s); + print(t); + assert(t == 6); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_callback_disagrees.ts b/tslang/test/tester/own/own_err_callback_disagrees.ts new file mode 100644 index 000000000..7f34f11f5 --- /dev/null +++ b/tslang/test/tester/own/own_err_callback_disagrees.ts @@ -0,0 +1,25 @@ +// -mm=own rejects: both callbacks reach the one `___array_reduce`, and its call through the +// callback gets what they agree on (spec 33). `pick` keeps its accumulator (it returns it on some +// paths only), `first` returns a borrow of it on every path: no caller can be told both, so both +// lose their facts, and their bodies fail, saying why. +class V { + constructor(public x: number) {} + static plus(a: V, b: V) { + return new V(a.x + b.x); + } +} + +function pick(acc: V, v: V) { + return v.x > 1 ? acc : V.plus(acc, v); +} + +function first(acc: V, v: V) { + return acc; +} + +function main() { + const a = [new V(1), new V(2)]; + const q = a.reduce(pick, new V(0)); + const r = a.reduce(first, new V(4)); + print(q.x, r.x); +} diff --git a/tslang/test/tester/own/own_err_callback_escapes.ts b/tslang/test/tester/own/own_err_callback_escapes.ts new file mode 100644 index 000000000..0cfca2c8f --- /dev/null +++ b/tslang/test/tester/own/own_err_callback_escapes.ts @@ -0,0 +1,22 @@ +// -mm=own rejects: `pick` is passed to `reduce`, whose calls through its callback get the facts +// of the functions its callers pass there (spec 33), but `pick` is also held in a `let` and called +// through it, where -mm=own cannot see which function runs: those callers cannot be told that it +// keeps `acc`, and `pick`'s body fails, saying why. +class V { + constructor(public x: number) {} + static plus(a: V, b: V) { + return new V(a.x + b.x); + } +} + +function pick(acc: V, v: V) { + return v.x > 2 ? acc : V.plus(acc, v); +} + +function main() { + const a = [new V(1), new V(3)]; + const q = a.reduce(pick, new V(0)); + let g = pick; + const r = g(new V(1), new V(5)); + print(q.x, r.x); +} diff --git a/tslang/test/tester/own/own_err_callback_promoted_used.ts b/tslang/test/tester/own/own_err_callback_promoted_used.ts new file mode 100644 index 000000000..dc3d3f40d --- /dev/null +++ b/tslang/test/tester/own/own_err_callback_promoted_used.ts @@ -0,0 +1,26 @@ +// -mm=own rejects: `add` only borrows its accumulator, but `pick`, passed to the same +// `___array_reduce`, keeps it, so `add` is given it to keep too and releases it (spec 33). +// A direct call of `add` then moves `x` in, and `x` is read after it. +class V { + constructor(public x: number) {} + static plus(a: V, b: V) { + return new V(a.x + b.x); + } +} + +function add(acc: V, v: V) { + return V.plus(acc, v); +} + +function pick(acc: V, v: V) { + return v.x > 1 ? acc : V.plus(acc, v); +} + +function main() { + const a = [new V(1), new V(2)]; + const r = a.reduce(add, new V(0)); + const q = a.reduce(pick, new V(0)); + const x = new V(5); + const y = add(x, new V(1)); + print(r.x, q.x, y.x, x.x); +} diff --git a/tslang/test/tester/own/own_err_conditional_move_bound_method.ts b/tslang/test/tester/own/own_err_conditional_move_bound_method.ts new file mode 100644 index 000000000..12a047688 --- /dev/null +++ b/tslang/test/tester/own/own_err_conditional_move_bound_method.ts @@ -0,0 +1,52 @@ +// @strict-null false +// -mm=own rejects: `m` is `v.get`, bound to `v`, `v` moves into `b` on one path only, and `m` is +// called after the paths join. Freeing `v` where the paths join, on the path that did not move it, +// would free what `m` reads (spec 30). Only the walk of what is read out of `v` sees it. +class W { + constructor(public x: number) {} +} + +class V { + constructor(public w: W) {} + get() { + return this.w.x; + } +} + +interface I { + w: W; +} + +function mkv(n: number) { + return new V(new W(n)); +} + +function churn() { + let s: number = 0; + const a: V[] = []; + for (let i = 0; i < 64; i++) { + a.push(mkv(1000 + i)); + } + + for (const e of a) { + s += e.w.x; + } + + return s; +} + +function test(c: boolean) { + let b: V = undefined; + let v = mkv(1); + const m = v.get; + if (c) { + b = v; + } + + churn(); + return m(); +} + +function main() { + print(test(true), test(false)); +} diff --git a/tslang/test/tester/own/own_err_conditional_move_catch.ts b/tslang/test/tester/own/own_err_conditional_move_catch.ts new file mode 100644 index 000000000..072ee9d4b --- /dev/null +++ b/tslang/test/tester/own/own_err_conditional_move_catch.ts @@ -0,0 +1,34 @@ +// @strict-null false +// -mm=own rejects: `v` moves into `best` on one path only inside a catch clause. The edge that +// did not move it is inside the exception handler, where -mm=own places no release of its own +// (spec 30). +class V { + constructor(public x: number) {} +} + +function mkt(n: number) { + if (n > 5) { + throw n; + } + + return new V(n); +} + +function test(c: boolean) { + let best: V = undefined; + let v = mkt(1); + try { + mkt(9); + } catch { + if (c) { + best = v; + } + } + + print(best != undefined); +} + +function main() { + test(true); + test(false); +} diff --git a/tslang/test/tester/own/own_err_conditional_move_field_borrow.ts b/tslang/test/tester/own/own_err_conditional_move_field_borrow.ts new file mode 100644 index 000000000..7af3231c5 --- /dev/null +++ b/tslang/test/tester/own/own_err_conditional_move_field_borrow.ts @@ -0,0 +1,34 @@ +// @strict-null false +// -mm=own rejects: `f` borrows a field of `v`, `v` moves into `b` on one path only, and `f` is +// read after the paths join. Freeing `v` where the paths join, on the path that did not move it, +// would free what `f` reads (spec 30). +class W { + constructor(public x: number) {} +} + +class V { + constructor(public w: W) {} +} + +function mkv(n: number) { + return new V(new W(n)); +} + +function test(c: boolean) { + let v = mkv(1); + const f = v.w; + let b: V = undefined; + if (c) { + b = v; + } + + print(f.x); + if (b != undefined) { + print(b.w.x); + } +} + +function main() { + test(true); + test(false); +} diff --git a/tslang/test/tester/own/own_err_conditional_move_old_view.ts b/tslang/test/tester/own/own_err_conditional_move_old_view.ts new file mode 100644 index 000000000..416341614 --- /dev/null +++ b/tslang/test/tester/own/own_err_conditional_move_old_view.ts @@ -0,0 +1,66 @@ +// @strict-null false +// -mm=own rejects: a bound method of `v` and an interface view of it are used after `v`, moved into +// `b` on one path only, is given a new value. The old value, released where the paths join on the +// path that did not move it, is what they read; the joined point is long past (spec 30). +class W { + constructor(public x: number) {} +} + +class V { + constructor(public w: W) {} + get() { + return this.w.x; + } +} + +interface I { + w: W; +} + +function mkv(n: number) { + return new V(new W(n)); +} + +function churn() { + let s: number = 0; + const a: V[] = []; + for (let i = 0; i < 64; i++) { + a.push(mkv(1000 + i)); + } + + for (const e of a) { + s += e.w.x; + } + + return s; +} + +function bound(c: boolean) { + let b: V = undefined; + let v = mkv(1); + const m = v.get; + if (c) { + b = v; + } + + v = mkv(2); + churn(); + return m() + v.w.x; +} + +function view(c: boolean) { + let b: V = undefined; + let v = mkv(1); + const i: I = v; + if (c) { + b = v; + } + + v = mkv(2); + churn(); + return i.w.x + v.w.x; +} + +function main() { + print(bound(true), bound(false), view(true), view(false)); +} diff --git a/tslang/test/tester/own/own_err_conditional_move_outer.ts b/tslang/test/tester/own/own_err_conditional_move_outer.ts new file mode 100644 index 000000000..b20c65d9c --- /dev/null +++ b/tslang/test/tester/own/own_err_conditional_move_outer.ts @@ -0,0 +1,24 @@ +// @strict-null false +// -mm=own rejects: `v` is declared outside the loop and moved inside it on some iterations, and +// the next iteration reads it without giving it a new value (spec 30). +class V { + constructor(public x: number) {} +} + +function mkv(n: number) { + return new V(n); +} + +function main() { + let v = mkv(1); + let best: V = undefined; + let s: number = 0; + for (let i = 0; i < 4; i++) { + s += v.x; + if (i % 2 == 0) { + best = v; + } + } + + print(s, best.x); +} diff --git a/tslang/test/tester/own/own_err_conditional_move_read.ts b/tslang/test/tester/own/own_err_conditional_move_read.ts new file mode 100644 index 000000000..4255bf5b3 --- /dev/null +++ b/tslang/test/tester/own/own_err_conditional_move_read.ts @@ -0,0 +1,26 @@ +// @strict-null false +// -mm=own rejects: `v` moves into `b` on one path only, `b` is overwritten after the paths join, +// and `v` is read after that - through a freed block on the path that moved it (spec 30). +class V { + constructor(public x: number) {} +} + +function mkv(n: number) { + return new V(n); +} + +function test(c: boolean) { + let v = mkv(1); + let b: V = undefined; + if (c) { + b = v; + } + + b = undefined; + print(v.x); +} + +function main() { + test(true); + test(false); +} diff --git a/tslang/test/tester/own/own_err_conditional_move_read_after_join.ts b/tslang/test/tester/own/own_err_conditional_move_read_after_join.ts new file mode 100644 index 000000000..18f39d62c --- /dev/null +++ b/tslang/test/tester/own/own_err_conditional_move_read_after_join.ts @@ -0,0 +1,30 @@ +// @strict-null false +// -mm=own rejects: `v` moves into `b` on one path only and is read after the paths join. On the +// path that moved it the read goes through `b` (spec 29), but on the other `v` still owns the +// value and must free it after the read: only a run-time record of the move could tell the two +// apart there (spec 30). +class V { + constructor(public x: number) {} +} + +function mkv(n: number) { + return new V(n); +} + +function test(c: boolean) { + let v = mkv(1); + let b: V = undefined; + if (c) { + b = v; + } + + print(v.x); + if (b != undefined) { + print(b.x); + } +} + +function main() { + test(true); + test(false); +} diff --git a/tslang/test/tester/own/own_err_conditional_move_view.ts b/tslang/test/tester/own/own_err_conditional_move_view.ts new file mode 100644 index 000000000..0cec985ce --- /dev/null +++ b/tslang/test/tester/own/own_err_conditional_move_view.ts @@ -0,0 +1,52 @@ +// @strict-null false +// -mm=own rejects: `i` is `v` seen through an interface, `v` moves into `b` on one path only, and +// `i` is read after the paths join. Freeing `v` where the paths join, on the path that did not move +// it, would free what `i` reads (spec 30). Only the walk of what is read out of `v` sees it. +class W { + constructor(public x: number) {} +} + +class V { + constructor(public w: W) {} + get() { + return this.w.x; + } +} + +interface I { + w: W; +} + +function mkv(n: number) { + return new V(new W(n)); +} + +function churn() { + let s: number = 0; + const a: V[] = []; + for (let i = 0; i < 64; i++) { + a.push(mkv(1000 + i)); + } + + for (const e of a) { + s += e.w.x; + } + + return s; +} + +function test(c: boolean) { + let b: V = undefined; + let v = mkv(1); + const i: I = v; + if (c) { + b = v; + } + + churn(); + return i.w.x; +} + +function main() { + print(test(true), test(false)); +} diff --git a/tslang/test/tester/own/own_err_declared_callback.ts b/tslang/test/tester/own/own_err_declared_callback.ts new file mode 100644 index 000000000..8dbc65487 --- /dev/null +++ b/tslang/test/tester/own/own_err_declared_callback.ts @@ -0,0 +1,13 @@ +// -mm=own rejects: a C function of numbers (`raise`) whose module gave another C function a +// function of its own (`signal(15, onSig as Opaque)`): `raise` may call it back, and it overwrites +// `gh.c`, which `c` borrows. A C declaration of numbers destroys nothing only in a module none of +// whose functions escape so (spec 31). +class V { constructor(public x: number) {} } +class H { c: V = new V(5); } +function churn() { let keep: V[] = []; for (let i = 0; i < 2000; i++) { keep.push(new V(77777)); } return keep.length; } +declare function signal(sig: s32, handler: Opaque): Opaque; +declare function raise(sig: s32): s32; +let gh = new H(); +function onSig(sig: s32) { gh.c = new V(9); } +function use() { const c = gh.c; raise(15); churn(); return c.x; } +function main() { signal(15, onSig as Opaque); print(use()); print(gh.c.x); } diff --git a/tslang/test/tester/own/own_err_declared_callback_class_method.ts b/tslang/test/tester/own/own_err_declared_callback_class_method.ts new file mode 100644 index 000000000..f787c3117 --- /dev/null +++ b/tslang/test/tester/own/own_err_declared_callback_class_method.ts @@ -0,0 +1,13 @@ +// -mm=own rejects: a C function of numbers (`raise`) in a module that calls a `declare class` +// method (`w.reg(onSig)`) with a function: `raise` may call it back, and it overwrites `gh.c`, which +// `c` borrows (spec 31). Compiled only: there is no C `W` to link. +class V { constructor(public x: number) {} } +class H { c: V = new V(5); } +function churn() { let keep: V[] = []; for (let i = 0; i < 2000; i++) { keep.push(new V(77777)); } return keep.length; } +declare function raise(sig: s32): s32; +let gh = new H(); +function onSig(sig: s32) { gh.c = new V(9); } +function use() { const c = gh.c; raise(15); churn(); return c.x; } +declare class W { reg(h: (s: s32) => void): void; } +declare function makeW(): W; +function main() { const w = makeW(); w.reg(onSig); print(use()); print(gh.c.x); } diff --git a/tslang/test/tester/own/own_err_declared_callback_cpp_class.ts b/tslang/test/tester/own/own_err_declared_callback_cpp_class.ts new file mode 100644 index 000000000..097906ba2 --- /dev/null +++ b/tslang/test/tester/own/own_err_declared_callback_cpp_class.ts @@ -0,0 +1,22 @@ +// -mm=own rejects: a C function of numbers (`raise`) in a module with a `@cpp` class whose method +// has a body (`MyL.on`): a `MyL` given to C++ (`addListener(new MyL())`) is called through its C++ +// vtable, so `raise` may call `on` back, and it overwrites `gh.c`, which `c` borrows (spec 31: a +// `@cpp` class with a method of its own is not scalar). Compiled only: there is no C++ +// `addListener` to link. +class V { constructor(public x: number) {} } +class H { c: V = new V(5); } +function churn() { let keep: V[] = []; for (let i = 0; i < 2000; i++) { keep.push(new V(77777)); } return keep.length; } +declare function raise(sig: s32): s32; +let gh = new H(); +function onSig(sig: s32) { gh.c = new V(9); } +function use() { const c = gh.c; raise(15); churn(); return c.x; } +@cpp +class Listener { + @slot(0) on(s: s32): void; +} +@cpp +class MyL extends Listener { + on(s: s32): void { gh.c = new V(9); } +} +declare function addListener(l: Listener): void; +function main() { addListener(new MyL()); print(use()); print(gh.c.x); } diff --git a/tslang/test/tester/own/own_err_declared_callback_field_reference.ts b/tslang/test/tester/own/own_err_declared_callback_field_reference.ts new file mode 100644 index 000000000..29ab8ef55 --- /dev/null +++ b/tslang/test/tester/own/own_err_declared_callback_field_reference.ts @@ -0,0 +1,20 @@ +// -mm=own rejects: a C function of numbers (`raise`) in a module that sees a class with a function +// field as a reference to integers (`>k`, then a load of the field), and gives the +// integer to a C function of numbers (`signal(15, ...)`): a function turned into data is C's to +// call, so `raise` may call it back, and it overwrites `gh.c`, which `c` borrows (spec 31: a class +// or object cast to anything but a class or an interface takes its fields along). +class V { constructor(public x: number) {} } +class H { c: V = new V(5); } +function churn() { let keep: V[] = []; for (let i = 0; i < 2000; i++) { keep.push(new V(77777)); } return keep.length; } +declare function signal(sig: s32, handler: u64): u64; +declare function raise(sig: s32): s32; +let gh = new H(); +function onSig(sig: s32) { gh.c = new V(9); } +function use() { const c = gh.c; raise(15); churn(); return c.x; } +class K { f: (s: s32) => void = onSig; } +let k = new K(); +function main() { + const obj = >k; + signal(15, LoadReference(obj[1])); + print(use()); print(gh.c.x); +} diff --git a/tslang/test/tester/own/own_err_declared_callback_global.ts b/tslang/test/tester/own/own_err_declared_callback_global.ts new file mode 100644 index 000000000..7bb5d2a76 --- /dev/null +++ b/tslang/test/tester/own/own_err_declared_callback_global.ts @@ -0,0 +1,13 @@ +// -mm=own rejects: a C function of numbers (`raise`) in a module that casts a function to an opaque +// pointer - here one a global was initialized with: `raise` may call it back, and it overwrites `gh.c`, +// which `c` borrows (spec 31). +class V { constructor(public x: number) {} } +class H { c: V = new V(5); } +function churn() { let keep: V[] = []; for (let i = 0; i < 2000; i++) { keep.push(new V(77777)); } return keep.length; } +declare function signal(sig: s32, handler: Opaque): Opaque; +declare function raise(sig: s32): s32; +let gh = new H(); +function onSig(sig: s32) { gh.c = new V(9); } +function use() { const c = gh.c; raise(15); churn(); return c.x; } +let cb = onSig; +function main() { signal(15, cb as Opaque); print(use()); print(gh.c.x); } diff --git a/tslang/test/tester/own/own_err_declared_callback_indirect.ts b/tslang/test/tester/own/own_err_declared_callback_indirect.ts new file mode 100644 index 000000000..564b6ef5f --- /dev/null +++ b/tslang/test/tester/own/own_err_declared_callback_indirect.ts @@ -0,0 +1,12 @@ +// -mm=own rejects: a C function of numbers (`raise`) in a module that calls a declared function +// through a function value (`sg(15, onSig)`, `sg` a local holding `signal`) with a function: `raise` may +// call it back, and it overwrites `gh.c`, which `c` borrows (spec 31). +class V { constructor(public x: number) {} } +class H { c: V = new V(5); } +function churn() { let keep: V[] = []; for (let i = 0; i < 2000; i++) { keep.push(new V(77777)); } return keep.length; } +declare function signal(sig: s32, handler: (s: s32) => void): Opaque; +declare function raise(sig: s32): s32; +let gh = new H(); +function onSig(sig: s32) { gh.c = new V(9); } +function use() { const c = gh.c; raise(15); churn(); return c.x; } +function main() { let sg = signal; sg(15, onSig); print(use()); print(gh.c.x); } diff --git a/tslang/test/tester/own/own_err_declared_callback_interface_field.ts b/tslang/test/tester/own/own_err_declared_callback_interface_field.ts new file mode 100644 index 000000000..314b5a0e9 --- /dev/null +++ b/tslang/test/tester/own/own_err_declared_callback_interface_field.ts @@ -0,0 +1,16 @@ +// -mm=own rejects: a C function of numbers (`raise`) in a module that calls a declared function +// through a function value (`r(box)`, `r` a local holding `creg`) with an interface that holds a +// function through its field: `Box` has only the field `cb`, whose interface `CB` has a method. +// `raise` may call it back, and it overwrites `gh.c`, which `c` borrows (spec 31). Compiled only: +// there is no C `creg` to link. +class V { constructor(public x: number) {} } +class H { c: V = new V(5); } +function churn() { let keep: V[] = []; for (let i = 0; i < 2000; i++) { keep.push(new V(77777)); } return keep.length; } +declare function raise(sig: s32): s32; +let gh = new H(); +interface CB { call(s: s32): void; } +class CBI implements CB { call(s: s32) { gh.c = new V(9); } } +interface Box { cb: CB; } +declare function creg(b: Box): void; +function use() { const c = gh.c; raise(15); churn(); return c.x; } +function main() { const box: Box = { cb: new CBI() }; let r = creg; r(box); print(use()); print(gh.c.x); } diff --git a/tslang/test/tester/own/own_err_declared_callback_intrinsic.ts b/tslang/test/tester/own/own_err_declared_callback_intrinsic.ts new file mode 100644 index 000000000..c099d0eda --- /dev/null +++ b/tslang/test/tester/own/own_err_declared_callback_intrinsic.ts @@ -0,0 +1,18 @@ +// -mm=own rejects: a C function of numbers (`raise`) in a module that gives a function to +// `call_intrinsic` (an identity intrinsic on a pointer, so an opaque pointer to `onSig` comes +// back), then its address to a C function of numbers (`signal(15, p)`): a function turned +// into data is C's to call, so `raise` may call it back, and it overwrites `gh.c`, which `c` +// borrows (spec 31: `inline_asm` and `call_intrinsic` given a function turn the rule off). +class V { constructor(public x: number) {} } +class H { c: V = new V(5); } +function churn() { let keep: V[] = []; for (let i = 0; i < 2000; i++) { keep.push(new V(77777)); } return keep.length; } +declare function signal(sig: s32, handler: u64): u64; +declare function raise(sig: s32): s32; +let gh = new H(); +function onSig(sig: s32) { gh.c = new V(9); } +function use() { const c = gh.c; raise(15); churn(); return c.x; } +function main() { + const p = call_intrinsic("llvm.launder.invariant.group.p0", onSig); + signal(15, p); + print(use()); print(gh.c.x); +} diff --git a/tslang/test/tester/own/own_err_declared_callback_method.ts b/tslang/test/tester/own/own_err_declared_callback_method.ts new file mode 100644 index 000000000..66cac8a5a --- /dev/null +++ b/tslang/test/tester/own/own_err_declared_callback_method.ts @@ -0,0 +1,12 @@ +// -mm=own rejects: a C function of numbers (`raise`) in a module that casts a method reference to an +// opaque pointer: `raise` may call it back, and it overwrites `gh.c`, which `c` borrows (spec 31). +class V { constructor(public x: number) {} } +class H { c: V = new V(5); } +function churn() { let keep: V[] = []; for (let i = 0; i < 2000; i++) { keep.push(new V(77777)); } return keep.length; } +declare function signal(sig: s32, handler: Opaque): Opaque; +declare function raise(sig: s32): s32; +let gh = new H(); +function onSig(sig: s32) { gh.c = new V(9); } +function use() { const c = gh.c; raise(15); churn(); return c.x; } +class K { m(s: s32) { gh.c = new V(9); } } +function main() { const k = new K(); signal(15, k.m as Opaque); print(use()); print(gh.c.x); } diff --git a/tslang/test/tester/own/own_err_declared_callback_stored_global.ts b/tslang/test/tester/own/own_err_declared_callback_stored_global.ts new file mode 100644 index 000000000..180b494d6 --- /dev/null +++ b/tslang/test/tester/own/own_err_declared_callback_stored_global.ts @@ -0,0 +1,14 @@ +// -mm=own rejects: a C function of numbers (`raise`) in a module that declares a C global of a +// function type (`declare var hook`): a function stored into it (`hook = onSig`) is C's to call, so +// `raise` may call it back, and it overwrites `gh.c`, which `c` borrows (spec 31: the rule holds +// only in a module whose every declared symbol is scalar). Compiled only: there is no C `hook` to +// link. +class V { constructor(public x: number) {} } +class H { c: V = new V(5); } +function churn() { let keep: V[] = []; for (let i = 0; i < 2000; i++) { keep.push(new V(77777)); } return keep.length; } +declare function raise(sig: s32): s32; +let gh = new H(); +function onSig(sig: s32) { gh.c = new V(9); } +function use() { const c = gh.c; raise(15); churn(); return c.x; } +declare var hook: (s: s32) => void; +function main() { hook = onSig; print(use()); print(gh.c.x); } diff --git a/tslang/test/tester/own/own_err_declared_callback_stored_reference.ts b/tslang/test/tester/own/own_err_declared_callback_stored_reference.ts new file mode 100644 index 000000000..99bbfd3d3 --- /dev/null +++ b/tslang/test/tester/own/own_err_declared_callback_stored_reference.ts @@ -0,0 +1,14 @@ +// -mm=own rejects: a C function of numbers (`raise`) in a module that declares a C function +// returning a reference to a function slot (`getSlot(): Reference<(s: s32) => void>`): a function +// stored through it (`Deref(getSlot()) = onSig`) is C's to call, so `raise` may call it back, and it +// overwrites `gh.c`, which `c` borrows (spec 31: a reference or an opaque pointer in a declared +// signature is not scalar). Compiled only: there is no C `getSlot` to link. +class V { constructor(public x: number) {} } +class H { c: V = new V(5); } +function churn() { let keep: V[] = []; for (let i = 0; i < 2000; i++) { keep.push(new V(77777)); } return keep.length; } +declare function raise(sig: s32): s32; +let gh = new H(); +function onSig(sig: s32) { gh.c = new V(9); } +function use() { const c = gh.c; raise(15); churn(); return c.x; } +declare function getSlot(): Reference<(s: s32) => void>; +function main() { const p = getSlot(); Deref(p) = onSig; print(use()); print(gh.c.x); } diff --git a/tslang/test/tester/own/own_err_declared_callback_tuple.ts b/tslang/test/tester/own/own_err_declared_callback_tuple.ts new file mode 100644 index 000000000..90e3a23c2 --- /dev/null +++ b/tslang/test/tester/own/own_err_declared_callback_tuple.ts @@ -0,0 +1,13 @@ +// -mm=own rejects: a C function of numbers (`raise`) in a module that gives a declared function a +// tuple holding a function: `raise` may call it back, and it overwrites `gh.c`, which `c` borrows +// (spec 31). Compiled only: there is no C `reg` to link. +class V { constructor(public x: number) {} } +class H { c: V = new V(5); } +function churn() { let keep: V[] = []; for (let i = 0; i < 2000; i++) { keep.push(new V(77777)); } return keep.length; } +declare function signal(sig: s32, handler: Opaque): Opaque; +declare function raise(sig: s32): s32; +let gh = new H(); +function onSig(sig: s32) { gh.c = new V(9); } +function use() { const c = gh.c; raise(15); churn(); return c.x; } +declare function reg(t: [(s: s32) => void, s32]): void; +function main() { reg([onSig, 1]); print(use()); print(gh.c.x); } diff --git a/tslang/test/tester/own/own_err_declared_callback_wrapper.ts b/tslang/test/tester/own/own_err_declared_callback_wrapper.ts new file mode 100644 index 000000000..c1c319db6 --- /dev/null +++ b/tslang/test/tester/own/own_err_declared_callback_wrapper.ts @@ -0,0 +1,13 @@ +// -mm=own rejects: a C function of numbers (`raise`) in a module whose wrapper calls its function +// parameter (`reg(signal)`, `f(15, onSig)` in `reg`) with a function: `raise` may call it back, and +// it overwrites `gh.c`, which `c` borrows (spec 31). +class V { constructor(public x: number) {} } +class H { c: V = new V(5); } +function churn() { let keep: V[] = []; for (let i = 0; i < 2000; i++) { keep.push(new V(77777)); } return keep.length; } +declare function signal(sig: s32, handler: (s: s32) => void): Opaque; +declare function raise(sig: s32): s32; +let gh = new H(); +function onSig(sig: s32) { gh.c = new V(9); } +function use() { const c = gh.c; raise(15); churn(); return c.x; } +function reg(f: (sig: s32, h: (s: s32) => void) => Opaque) { f(15, onSig); } +function main() { reg(signal); print(use()); print(gh.c.x); } diff --git a/tslang/test/tester/own/own_err_field_alias.ts b/tslang/test/tester/own/own_err_field_alias.ts index 04c466d72..31bfc76cd 100644 --- a/tslang/test/tester/own/own_err_field_alias.ts +++ b/tslang/test/tester/own/own_err_field_alias.ts @@ -1,9 +1,11 @@ -// -mm=own, phase 0 rejects: the field would be a second owner of `a`'s array. +// -mm=own rejects: `a`'s array moved into `n.v`, and `n.v` is overwritten before `a` is read: the +// overwrite frees the array `a` still names (spec 29). class N { v: number[] = []; } function main() { const a: number[] = [1]; const n = new N(); n.v = a; + n.v = [2]; print(a.length); print("done."); } diff --git a/tslang/test/tester/own/own_err_field_let_global_overwritten.ts b/tslang/test/tester/own/own_err_field_let_global_overwritten.ts new file mode 100644 index 000000000..2e7239953 --- /dev/null +++ b/tslang/test/tester/own/own_err_field_let_global_overwritten.ts @@ -0,0 +1,49 @@ +// @strict-null false +// -mm=own rejects: `x` borrows the field `h.c` of a parameter, and a global is assigned before `x` +// is read. The parameter may be the global's old value, which the assignment frees, and its field +// with it (spec 29). +class C { + x: number = 5; + v: number[] = []; +} + +class H { + c: C | null = null; +} + +function mkH(): H { + const r = new H(); + return r; +} + +function mk(): C { + const r = new C(); + r.v.push(1); + return r; +} + +function churn() { + const junk: C[] = []; + for (let i = 0; i < 64; i++) { + const c = mk(); + c.x = 999; + c.v.push(99); + junk.push(c); + } +} + +let G: H = mkH(); + +function run(h: H) { + h.c = mk(); + const x = h.c; + G = mkH(); + churn(); + print(x.x); +} + +// The rule is the callee's: it is given a fresh value here, since a written global's value lent to +// a call that may drop is rejected at the call already (#639). +function main() { + run(mkH()); +} diff --git a/tslang/test/tester/own/own_err_generator_method_this.ts b/tslang/test/tester/own/own_err_generator_method_this.ts index f4cd36b3f..2be1d7aa2 100644 --- a/tslang/test/tester/own/own_err_generator_method_this.ts +++ b/tslang/test/tester/own/own_err_generator_method_this.ts @@ -1,7 +1,8 @@ // -mm=own rejects: a generator method of a class another module may call keeps `this` in its state, // and its callers keep owning it. It compiled, and the state's release freed the caller's object -// (the program then freed it again); `@borrows("this")` says what it does. -class S { +// (the program then freed it again); `@borrows("this")` says what it does. The class is exported: a +// `public` method of a class no other module can name is not one another module may call (spec 31). +export class S { constructor(public value: string) {} public *chars() { diff --git a/tslang/test/tester/own/own_err_global_argument_field_let.ts b/tslang/test/tester/own/own_err_global_argument_field_let.ts new file mode 100644 index 000000000..27fff7eea --- /dev/null +++ b/tslang/test/tester/own/own_err_global_argument_field_let.ts @@ -0,0 +1,22 @@ +// -mm=own rejects: `x` borrows the field `h.c` of a parameter that is `G`'s object, and `run` gives +// `G` a new value before `x` is read, which frees the object and its field with it. +class C { + x = 5; +} + +class H { + c: C | null = null; +} + +let G = new H(); + +function run(h: H) { + h.c = new C(); + const x = h.c; + G = new H(); + print(x.x); +} + +function main() { + run(G); +} diff --git a/tslang/test/tester/own/own_err_global_argument_transitive.ts b/tslang/test/tester/own/own_err_global_argument_transitive.ts new file mode 100644 index 000000000..a6de56bdb --- /dev/null +++ b/tslang/test/tester/own/own_err_global_argument_transitive.ts @@ -0,0 +1,24 @@ +// -mm=own rejects: `run` borrows `G`'s object as `h` and calls `reset`, which gives `G` a new value: +// that frees the object while `h` still reads it. The callee drops it through another call. +class C { + x = 5; +} + +function mk(): C { + return new C(); +} + +let G: C = mk(); + +function reset() { + G = mk(); +} + +function run(h: C) { + reset(); + print(h.x); +} + +function main() { + run(G); +} diff --git a/tslang/test/tester/own/own_err_global_const_store.ts b/tslang/test/tester/own/own_err_global_const_store.ts new file mode 100644 index 000000000..bbf28569d --- /dev/null +++ b/tslang/test/tester/own/own_err_global_const_store.ts @@ -0,0 +1,30 @@ +// -mm=own rejects: `c` names the read of `G` itself, a borrow of the block the global owns, and `f` +// gives `G` a new value before `c` is read, which frees that block. (`let c = G` is rejected as a +// borrowing local; a `const` names the read and was not.) +class V { + a: number[] = []; + constructor(public x: number) { + for (let i = 0; i < 64; i++) { + this.a.push(i); + } + } +} + +let G = new V(0); + +function f(i: number) { + const c = G; + G = new V(i + 1); + const z = new V(999); + print(c.x, c.a.length); + z.x = 1000 + i; +} + +function main() { + for (let i = 0; i < 5; i++) { + f(i); + } + + print(G.x); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_global_const_store_script.ts b/tslang/test/tester/own/own_err_global_const_store_script.ts new file mode 100644 index 000000000..2ebadb140 --- /dev/null +++ b/tslang/test/tester/own/own_err_global_const_store_script.ts @@ -0,0 +1,29 @@ +// -mm=own rejects: `c` names the read of `K.w` itself, a borrow of the block the static owns. The +// top-level loop gives `K.w` a new value before `c` is read, which frees that block. +class V { + a: number[] = []; + constructor(public x: number) { + for (let i = 0; i < 64; i++) { + this.a.push(i); + } + } +} + +class K { + static w: V; +} + +for (let i = 0; i < 5; i++) { + const c = K.w; + K.w = new V(i + 1); + const z = new V(999); + if (i > 0) { + print(c.x, c.a.length); + } + z.x = 1000 + i; +} + +function main() { + print(K.w.x); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_global_const_store_static.ts b/tslang/test/tester/own/own_err_global_const_store_static.ts new file mode 100644 index 000000000..6820d3380 --- /dev/null +++ b/tslang/test/tester/own/own_err_global_const_store_static.ts @@ -0,0 +1,31 @@ +// -mm=own rejects: `c` names the read of `K.w` itself, a borrow of the block the static owns. The +// static block's loop gives `K.w` a new value before `c` is read, which frees that block: `K.w` is +// not immortal, its store runs once per round (spec 25). +class V { + a: number[] = []; + constructor(public x: number) { + for (let i = 0; i < 64; i++) { + this.a.push(i); + } + } +} + +class K { + static w: V; + static { + for (let i = 0; i < 5; i++) { + const c = K.w; + K.w = new V(i + 1); + const z = new V(999); + if (i > 0) { + print(c.x, c.a.length); + } + z.x = 1000 + i; + } + } +} + +function main() { + print(K.w.x); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_global_kept_field.ts b/tslang/test/tester/own/own_err_global_kept_field.ts new file mode 100644 index 000000000..d52380ecb --- /dev/null +++ b/tslang/test/tester/own/own_err_global_kept_field.ts @@ -0,0 +1,27 @@ +// -mm=own rejects: `keep` stores its parameter `c` into a field, and `main` lends it the global `g`, +// which `reset` gives another value elsewhere: `g` is not immortal (spec 25), and a call that may +// drop cannot be lent a value read out of it. +class C { + constructor(public x: number) {} +} + +class H { + c: C | null = null; +} + +let g = new C(1); + +function keep(h: H, c: C) { + h.c = c; +} + +function reset() { + g = new C(2); +} + +function main() { + const h = new H(); + keep(h, g); + print(h.c!.x); + reset(); +} diff --git a/tslang/test/tester/own/own_err_global_let_destructure.ts b/tslang/test/tester/own/own_err_global_let_destructure.ts new file mode 100644 index 000000000..6f0959f5f --- /dev/null +++ b/tslang/test/tester/own/own_err_global_let_destructure.ts @@ -0,0 +1,37 @@ +// -mm=own rejects: `let { v }` takes the read of `K.w` out of the literal - a borrow of the block +// the static owns - and the top-level loop gives `K.w` a new value before `v` is read, which frees +// that block. +class V { + a: number[] = []; + constructor(public x: number) { + for (let i = 0; i < 64; i++) { + this.a.push(i); + } + } +} + +@noinline +function churn() { + const junk: V[] = []; + for (let i = 0; i < 64; i++) { + junk.push(new V(999)); + } +} + +class K { + static w: V; +} + +for (let i = 0; i < 5; i++) { + let { v } = { v: K.w }; + K.w = new V(i + 1); + churn(); + if (i > 0) { + print(v.x, v.a.length); + } +} + +function main() { + print(K.w.x); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_global_let_holder.ts b/tslang/test/tester/own/own_err_global_let_holder.ts new file mode 100644 index 000000000..46cdd02cf --- /dev/null +++ b/tslang/test/tester/own/own_err_global_let_holder.ts @@ -0,0 +1,37 @@ +// -mm=own rejects: the `let` `l` takes the read of `G` out of the object literal `o`, a borrow of +// the block the global owns, and `f` gives `G` a new value before `l` is read, which frees it. +class V { + a: number[] = []; + constructor(public x: number) { + for (let i = 0; i < 64; i++) { + this.a.push(i); + } + } +} + +@noinline +function churn() { + const junk: V[] = []; + for (let i = 0; i < 64; i++) { + junk.push(new V(999)); + } +} + +let G = new V(0); + +function f(i: number) { + const o = { v: G }; + let l = o.v; + G = new V(i + 1); + churn(); + print(l.x, l.a.length); +} + +function main() { + for (let i = 0; i < 5; i++) { + f(i); + } + + print(G.x); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_global_let_holder_static.ts b/tslang/test/tester/own/own_err_global_let_holder_static.ts new file mode 100644 index 000000000..7a5658202 --- /dev/null +++ b/tslang/test/tester/own/own_err_global_let_holder_static.ts @@ -0,0 +1,39 @@ +// -mm=own rejects: the object literal `o` holds the read of `K.w`, and the `let` `l` takes that +// value out of it - a borrow of the block the static owns, still. The static block's loop gives +// `K.w` a new value before `l` is read, which frees that block (spec 25). +class V { + a: number[] = []; + constructor(public x: number) { + for (let i = 0; i < 64; i++) { + this.a.push(i); + } + } +} + +@noinline +function churn() { + const junk: V[] = []; + for (let i = 0; i < 64; i++) { + junk.push(new V(999)); + } +} + +class K { + static w: V; + static { + for (let i = 0; i < 5; i++) { + const o = { v: K.w }; + let l = o.v; + K.w = new V(i + 1); + churn(); + if (i > 0) { + print(l.x, l.a.length); + } + } + } +} + +function main() { + print(K.w.x); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_global_loop_store_script.ts b/tslang/test/tester/own/own_err_global_loop_store_script.ts new file mode 100644 index 000000000..86293136a --- /dev/null +++ b/tslang/test/tester/own/own_err_global_loop_store_script.ts @@ -0,0 +1,20 @@ +// -mm=own rejects: `K.w`'s one store is in a loop of top-level script code, which runs it once per +// round, so `K.w` is not immortal (spec 25): each store frees the value before it. `e` cannot be +// given the global's value, which `K.w` keeps. +class V { + constructor(public x: number) {} +} + +class K { + static w: V; +} + +for (let i = 0; i < 10; i++) { + K.w = new V(i + 1); +} + +let e = K.w; + +function main() { + print(e.x); +} diff --git a/tslang/test/tester/own/own_err_global_loop_store_static.ts b/tslang/test/tester/own/own_err_global_loop_store_static.ts new file mode 100644 index 000000000..cb0e5987a --- /dev/null +++ b/tslang/test/tester/own/own_err_global_loop_store_static.ts @@ -0,0 +1,23 @@ +// -mm=own rejects: `K.w`'s one store is in a loop of its static block, so it runs once per round and +// `K.w` is not immortal (spec 25): each store frees the value before it. `f` returns a borrow of +// the global. +class V { + constructor(public x: number) {} +} + +class K { + static w: V; + static { + for (let i = 0; i < 10; i++) { + K.w = new V(i + 1); + } + } +} + +function f() { + return K.w; +} + +function main() { + print(f().x); +} diff --git a/tslang/test/tester/own/own_err_global_method_transitive.ts b/tslang/test/tester/own/own_err_global_method_transitive.ts new file mode 100644 index 000000000..bac5f079d --- /dev/null +++ b/tslang/test/tester/own/own_err_global_method_transitive.ts @@ -0,0 +1,20 @@ +// -mm=own rejects: `G.m()` runs with `this` borrowed from `G`, and `m` calls `reset`, which gives +// `G` a new value: that frees the object `this` still is. +class C { + x = 5; + + m() { + reset(); + return this.x; + } +} + +let G = new C(); + +function reset() { + G = new C(); +} + +function main() { + print(G.m()); +} diff --git a/tslang/test/tester/own/own_err_global_outer_holder.ts b/tslang/test/tester/own/own_err_global_outer_holder.ts new file mode 100644 index 000000000..721b2158c --- /dev/null +++ b/tslang/test/tester/own/own_err_global_outer_holder.ts @@ -0,0 +1,43 @@ +// -mm=own rejects: the object literal `o` is declared before the loop, and each round reads `o.v` +// before it stores the read of `G` there. The next round's `const c = G` is a new read, but `o.v` +// still holds the one stored before `G = new V(i + 1)` freed its block: a holder is given a new +// value only where it is stored, never at the read. +class V { + a: number[] = []; + constructor(public x: number) { + for (let i = 0; i < 64; i++) { + this.a.push(i); + } + } +} + +@noinline +function churn() { + const junk: V[] = []; + for (let i = 0; i < 64; i++) { + junk.push(new V(999)); + } +} + +class Z { + static z = new V(42); +} + +let G = new V(0); + +function f() { + const o = { v: Z.z }; + for (let i = 0; i < 4; i++) { + const c = G; + print(o.v.x, o.v.a.length); + o.v = c; + G = new V(i + 1); + churn(); + } +} + +function main() { + f(); + print(G.x); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_global_outer_holder_static.ts b/tslang/test/tester/own/own_err_global_outer_holder_static.ts new file mode 100644 index 000000000..1b0e0b65b --- /dev/null +++ b/tslang/test/tester/own/own_err_global_outer_holder_static.ts @@ -0,0 +1,47 @@ +// -mm=own rejects: the object literal `o` is declared before the static block's loop, which stores +// the read of `K.w` into `o.v` in one round and reads `o.v` in the later ones, after `K.w = new +// V(i + 1)` freed that block. The read `const c = K.w` is made again each round; `o.v` is not: a +// holder is given a new value only where it is stored (spec 25). +class V { + a: number[] = []; + constructor(public x: number) { + for (let i = 0; i < 64; i++) { + this.a.push(i); + } + } +} + +@noinline +function churn() { + const junk: V[] = []; + for (let i = 0; i < 64; i++) { + junk.push(new V(999)); + } +} + +class Z { + static z = new V(42); +} + +class K { + static w: V; + static { + const o = { v: Z.z }; + for (let i = 0; i < 5; i++) { + const c = K.w; + if (i >= 2) { + print(o.v.x, o.v.a.length); + } + if (i == 1) { + o.v = c; + } + K.w = new V(i + 1); + churn(); + } + } +} + +function main() { + print(K.w.x); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_global_record.ts b/tslang/test/tester/own/own_err_global_record.ts new file mode 100644 index 000000000..1f9b429de --- /dev/null +++ b/tslang/test/tester/own/own_err_global_record.ts @@ -0,0 +1,36 @@ +// -mm=own rejects: the object literal `o` holds the read of `G` - a borrow of the block the global +// owns - and `f` gives `G` a new value before `o.v` is read, which frees that block. +class V { + a: number[] = []; + constructor(public x: number) { + for (let i = 0; i < 64; i++) { + this.a.push(i); + } + } +} + +@noinline +function churn() { + const junk: V[] = []; + for (let i = 0; i < 64; i++) { + junk.push(new V(999)); + } +} + +let G = new V(0); + +function f(i: number) { + const o = { v: G }; + G = new V(i + 1); + churn(); + print(o.v.x, o.v.a.length); +} + +function main() { + for (let i = 0; i < 5; i++) { + f(i); + } + + print(G.x); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_global_record_static.ts b/tslang/test/tester/own/own_err_global_record_static.ts new file mode 100644 index 000000000..508677379 --- /dev/null +++ b/tslang/test/tester/own/own_err_global_record_static.ts @@ -0,0 +1,39 @@ +// -mm=own rejects: the object literal `o` holds the read of `K.w` - a borrow of the block the static +// owns, not a second owner - and the static block's loop gives `K.w` a new value before `o.v` is +// read, which frees that block: `K.w` is not immortal, its store runs once per round (spec 25). +class V { + a: number[] = []; + constructor(public x: number) { + for (let i = 0; i < 64; i++) { + this.a.push(i); + } + } +} + +@noinline +function churn() { + const junk: V[] = []; + for (let i = 0; i < 64; i++) { + junk.push(new V(999)); + } +} + +class K { + static w: V; + static { + for (let i = 0; i < 5; i++) { + const c = K.w; + const o = { v: c }; + K.w = new V(i + 1); + churn(); + if (i > 0) { + print(o.v.x, o.v.a.length); + } + } + } +} + +function main() { + print(K.w.x); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_global_returned.ts b/tslang/test/tester/own/own_err_global_returned.ts index 51916450b..93a5bbaea 100644 --- a/tslang/test/tester/own/own_err_global_returned.ts +++ b/tslang/test/tester/own/own_err_global_returned.ts @@ -1,5 +1,6 @@ // -mm=own rejects: a global owns its value, and a function that returns it gives its caller a -// second owner. +// second owner. `reset` gives the global another value, so it is not immortal: the old value is +// destroyed then, and a caller holding it would hold a freed block. class V { constructor(public x: number) {} static def = new V(1); @@ -7,6 +8,10 @@ class V { function getDef() { return V.def; } +function reset() { + V.def = new V(2); +} function main() { print(getDef().x); + reset(); } diff --git a/tslang/test/tester/own/own_err_global_this_field_move.ts b/tslang/test/tester/own/own_err_global_this_field_move.ts new file mode 100644 index 000000000..6be643b96 --- /dev/null +++ b/tslang/test/tester/own/own_err_global_this_field_move.ts @@ -0,0 +1,23 @@ +// -mm=own rejects: `GK.run()` runs with `this` borrowed from `GK`; `run` moves `c` into a field of +// `this`, then gives `GK` a new value, which frees the old object and `c` with it before `c.x` is +// read. +class C { + x = 5; +} + +class K { + f: C | null = null; + + run() { + const c = new C(); + this.f = c; + GK = new K(); + print(c.x); + } +} + +let GK = new K(); + +function main() { + GK.run(); +} diff --git a/tslang/test/tester/own/own_err_global_uninit_let.ts b/tslang/test/tester/own/own_err_global_uninit_let.ts new file mode 100644 index 000000000..e95a5c639 --- /dev/null +++ b/tslang/test/tester/own/own_err_global_uninit_let.ts @@ -0,0 +1,43 @@ +// -mm=own rejects: `let l!: V` inside `f`'s loop stores nothing, so the slot keeps the value of +// the round before - the read of `G` stored in round 1 - and the later rounds read it after +// `G = new V(i + 1)` freed that block. A declaration without an initializer gives a holder no new +// value. +class V { + a: number[] = []; + constructor(public x: number) { + for (let i = 0; i < 64; i++) { + this.a.push(i); + } + } +} + +@noinline +function churn() { + const junk: V[] = []; + for (let i = 0; i < 64; i++) { + junk.push(new V(999)); + } +} + +let G = new V(0); + +function f() { + for (let i = 0; i < 4; i++) { + let l!: V; + if (i > 1) { + print(l.x, l.a.length); + } + const c = G; + if (i == 1) { + l = c; + } + G = new V(i + 1); + churn(); + } +} + +function main() { + f(); + print(G.x); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_global_uninit_let_static.ts b/tslang/test/tester/own/own_err_global_uninit_let_static.ts new file mode 100644 index 000000000..1a49497ac --- /dev/null +++ b/tslang/test/tester/own/own_err_global_uninit_let_static.ts @@ -0,0 +1,43 @@ +// -mm=own rejects: `let l!: V` inside the static block's loop stores nothing, so the slot keeps +// the value of the round before - the read of `K.w` stored in round 1 - and the later rounds read +// it after `K.w = new V(i + 1)` freed that block. A declaration without an initializer gives a +// holder no new value (spec 25). +class V { + a: number[] = []; + constructor(public x: number) { + for (let i = 0; i < 64; i++) { + this.a.push(i); + } + } +} + +@noinline +function churn() { + const junk: V[] = []; + for (let i = 0; i < 64; i++) { + junk.push(new V(999)); + } +} + +class K { + static w: V; + static { + for (let i = 0; i < 4; i++) { + let l!: V; + if (i > 1) { + print(l.x, l.a.length); + } + const c = K.w; + if (i == 1) { + l = c; + } + K.w = new V(i + 1); + churn(); + } + } +} + +function main() { + print(K.w.x); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_iface_field_call.ts b/tslang/test/tester/own/own_err_iface_field_call.ts new file mode 100644 index 000000000..35d429234 --- /dev/null +++ b/tslang/test/tester/own/own_err_iface_field_call.ts @@ -0,0 +1,33 @@ +// -mm=own rejects: `d` borrows `r.dir`, read through an interface, and a call given `r` overwrites +// that field through the interface (spec 28). +class V { + v: number[] = []; + constructor(public x: number) {} +} + +function churn() { + const junk: V[] = []; + for (let i = 0; i < 64; i++) { + const c = new V(999); + c.v.push(99); + junk.push(c); + } +} + +interface R { dir: V; } + +function setDir(r: R) { + r.dir = new V(5); +} + +function f(r: R) { + let d = r.dir; + setDir(r); + churn(); + print(d.x); +} + +function main() { + const r: R = { dir: new V(1) }; + f(r); +} diff --git a/tslang/test/tester/own/own_err_iface_field_overwritten.ts b/tslang/test/tester/own/own_err_iface_field_overwritten.ts new file mode 100644 index 000000000..2c05c131f --- /dev/null +++ b/tslang/test/tester/own/own_err_iface_field_overwritten.ts @@ -0,0 +1,29 @@ +// -mm=own rejects: `d` borrows `r.dir`, read through an interface, which is overwritten through it +// before `d` is read (spec 28). +class V { + v: number[] = []; + constructor(public x: number) {} +} + +function churn() { + const junk: V[] = []; + for (let i = 0; i < 64; i++) { + const c = new V(999); + c.v.push(99); + junk.push(c); + } +} + +interface R { dir: V; } + +function f(r: R) { + let d = r.dir; + r.dir = new V(5); + churn(); + print(d.x); +} + +function main() { + const r: R = { dir: new V(1) }; + f(r); +} diff --git a/tslang/test/tester/own/own_err_iface_field_result.ts b/tslang/test/tester/own/own_err_iface_field_result.ts new file mode 100644 index 000000000..46170aea7 --- /dev/null +++ b/tslang/test/tester/own/own_err_iface_field_result.ts @@ -0,0 +1,29 @@ +// -mm=own rejects: `dirOf` returns a borrow of its parameter's field, read through an interface; +// the result is read after that field is overwritten (spec 28). +class V { + v: number[] = []; + constructor(public x: number) {} +} + +function churn() { + const junk: V[] = []; + for (let i = 0; i < 64; i++) { + const c = new V(999); + c.v.push(99); + junk.push(c); + } +} + +interface R { dir: V; } + +function dirOf(r: R) { + return r.dir; +} + +function main() { + const r: R = { dir: new V(1) }; + const d = dirOf(r); + r.dir = new V(2); + churn(); + print(d.x); +} diff --git a/tslang/test/tester/own/own_err_iface_field_view_overwritten.ts b/tslang/test/tester/own/own_err_iface_field_view_overwritten.ts new file mode 100644 index 000000000..e0ee3feb4 --- /dev/null +++ b/tslang/test/tester/own/own_err_iface_field_view_overwritten.ts @@ -0,0 +1,27 @@ +// -mm=own rejects: `c` borrows the class field `h.c`, which is overwritten through an interface +// view of `h` before `c` is read (spec 28). +class C { + v: number[] = []; + constructor(public x: number) {} +} + +function churn() { + const junk: C[] = []; + for (let i = 0; i < 64; i++) { + const c = new C(999); + c.v.push(99); + junk.push(c); + } +} + +interface HI { c: C; } +class H { c: C = new C(0); } + +function main() { + const h = new H(); + const hi: HI = h; + const c = h.c; + hi.c = new C(9); + churn(); + print(c.x); +} diff --git a/tslang/test/tester/own/own_err_immortal_borrowed_init.ts b/tslang/test/tester/own/own_err_immortal_borrowed_init.ts new file mode 100644 index 000000000..763a47688 --- /dev/null +++ b/tslang/test/tester/own/own_err_immortal_borrowed_init.ts @@ -0,0 +1,13 @@ +// -mm=own rejects: `K.first` is never assigned, but its initializer borrows an element of +// `K.list`, which owns it. Marking that block immortal would make the array's later free of it a +// silent leak, so `K.first` stays mortal and its initializer takes a second reference. +class V { + constructor(public x: number) {} +} +class K { + static list = [new V(1)]; + static first = K.list[0]; +} +function main() { + print(K.first.x); +} diff --git a/tslang/test/tester/own/own_err_immortal_field_overwritten.ts b/tslang/test/tester/own/own_err_immortal_field_overwritten.ts new file mode 100644 index 000000000..a858532ac --- /dev/null +++ b/tslang/test/tester/own/own_err_immortal_field_overwritten.ts @@ -0,0 +1,16 @@ +// -mm=own rejects: `K.n` is immortal, but only its own block is. Its field `c` is not: assigning +// it destroys the old value, which `c` still reads. +class V { + constructor(public x: number) {} +} +class N { + constructor(public c: V) {} +} +class K { + static n = new N(new V(1)); +} +function main() { + const c = K.n.c; + K.n.c = new V(2); + print(c.x); +} diff --git a/tslang/test/tester/own/own_err_immortal_region_borrow.ts b/tslang/test/tester/own/own_err_immortal_region_borrow.ts new file mode 100644 index 000000000..cf231c7a9 --- /dev/null +++ b/tslang/test/tester/own/own_err_immortal_region_borrow.ts @@ -0,0 +1,24 @@ +// -mm=own rejects: `c` is never assigned, but its initializer is a call's result, a borrow of an +// element `list` owns. Marking that block immortal would make the array's free of it a silent +// leak, so `c` stays mortal, and `f` returns a borrow of it. The call goes through a function value +// `pick` returns, whose facts the global's region does not know (#638 cannot see it), so this is +// findImmortalGlobals' refusal of any call's result, not the region check. (`firstOf` says it +// lends its argument, so it is no error of its own.) +class V { + constructor(public x: number) {} +} +const list = [new V(1)]; +@lends("a") +function firstOf(a: V[]): V { + return a[0]; +} +function pick(): (a: V[]) => V { + return firstOf; +} +const c = pick()(list); +function f() { + return c; +} +function main() { + print(f().x); +} diff --git a/tslang/test/tester/own/own_err_immortal_region_call.ts b/tslang/test/tester/own/own_err_immortal_region_call.ts new file mode 100644 index 000000000..d0eb2476b --- /dev/null +++ b/tslang/test/tester/own/own_err_immortal_region_call.ts @@ -0,0 +1,16 @@ +// -mm=own rejects: `c` is never assigned, but its initializer is a call's result, a borrow of a +// temporary its region frees. Such a global is not immortal: no call's result is taken as the +// global's own. A global cannot hold a borrow: the initializer is the error (#638). +class V { + constructor(public x: number) {} +} +function id(v: V) { + return v; +} +const c = id(new V(1)); +function f() { + return c; +} +function main() { + print(f().x); +} diff --git a/tslang/test/tester/own/own_err_interface_borrow_element.ts b/tslang/test/tester/own/own_err_interface_borrow_element.ts new file mode 100644 index 000000000..f319b6394 --- /dev/null +++ b/tslang/test/tester/own/own_err_interface_borrow_element.ts @@ -0,0 +1,15 @@ +// -mm=own rejects: a method whose result borrows an element of `this`, reached through an exported +// interface. Another module may call through the interface without the method's facts, and would +// release the borrow as its own (a double free), so the call is open and the method's body is the +// error. The same program with the interface not exported compiles (own_interface_facts, spec 31). +class V { + constructor(public x: number) {} +} +export interface First { first(): V; } +class F implements First { items: V[] = [new V(3)]; first(): V { return this.items[0]; } } +function main() { + const f = new F(); + const i: First = f; + const x = i.first(); + print(x.x, f.items[0].x); +} diff --git a/tslang/test/tester/own/own_err_interface_borrow_field.ts b/tslang/test/tester/own/own_err_interface_borrow_field.ts new file mode 100644 index 000000000..5853e5711 --- /dev/null +++ b/tslang/test/tester/own/own_err_interface_borrow_field.ts @@ -0,0 +1,14 @@ +// -mm=own rejects: a method whose result borrows a field of `this`, reached through an exported +// interface. Another module may call through the interface without the method's facts, and would +// release the borrow as its own (a double free), so the call is open and the method's body is the +// error. The same program with the interface not exported compiles (own_interface_facts, spec 31). +class V { + constructor(public x: number) {} +} +export interface Holder { hold(): V; } +class HH implements Holder { v: V = new V(4); hold(): V { return this.v; } } +function use(h: Holder) { const x = h.hold(); return x.x; } +function main() { + const hh = new HH(); + print(use(hh), hh.v.x); +} diff --git a/tslang/test/tester/own/own_err_interface_borrow_param.ts b/tslang/test/tester/own/own_err_interface_borrow_param.ts new file mode 100644 index 000000000..edde63ece --- /dev/null +++ b/tslang/test/tester/own/own_err_interface_borrow_param.ts @@ -0,0 +1,15 @@ +// -mm=own rejects: a method whose result borrows its argument, reached through an exported +// interface. Another module may call through the interface without the method's facts, and would +// release the borrow as its own (a double free), so the call is open and the method's body is the +// error. The same program with the interface not exported compiles (own_interface_facts, spec 31). +class V { + constructor(public x: number) {} +} +export interface Getter { get(v: V): V; } +class G implements Getter { get(v: V): V { return v; } } +function main() { + const g: Getter = new G(); + const a = new V(5); + const b = g.get(a); + print(b.x, a.x); +} diff --git a/tslang/test/tester/own/own_err_interface_exported_impl.ts b/tslang/test/tester/own/own_err_interface_exported_impl.ts new file mode 100644 index 000000000..c536f4655 --- /dev/null +++ b/tslang/test/tester/own/own_err_interface_exported_impl.ts @@ -0,0 +1,18 @@ +// -mm=own rejects: the plan's closed-interface program (own_interface_facts) with one implementation +// exported. Another module may extend `B` or implement `Th` and hand this module the value, so a call +// through `Th` may reach a function this module never sees: it gets no facts, may destroy anything, +// and the borrow of `sc.ts` it is made through ends at it. +class V { + constructor(public x: number) {} + static plus(a: V, b: V) { return new V(a.x + b.x); } +} +interface Th { area(v: V): number; } +class A implements Th { area(v: V) { return v.x; } } +export class B implements Th { area(v: V) { return v.x * 2; } } +class Sc { ts: Th[] = [new A(), new B()]; } +function tot(sc: Sc, r: V) { + let best: number = 0; + for (let i in sc.ts) { let a = sc.ts[i].area(r); if (a > best) best = a; } + return best + r.x; +} +function main() { print(tot(new Sc(), new V(3))); print("done."); } diff --git a/tslang/test/tester/own/own_err_interface_family_disagrees.ts b/tslang/test/tester/own/own_err_interface_family_disagrees.ts new file mode 100644 index 000000000..06f22bc7c --- /dev/null +++ b/tslang/test/tester/own/own_err_interface_family_disagrees.ts @@ -0,0 +1,16 @@ +// -mm=own rejects: two implementations of a closed interface that disagree - one returns a borrow of +// its argument, the other a value of its own. A call through the interface cannot be told which, so +// it is given neither, and the one that borrows has to say so to its callers (spec 31). +class V { + constructor(public x: number) {} +} +interface Getter { get(v: V): V; } +class GA implements Getter { get(v: V): V { return v; } } +class GB implements Getter { get(v: V): V { return new V(v.x + 1); } } +function main() { + const gs: Getter[] = [new GA(), new GB()]; + const a = new V(5); + let s: number = 0; + for (const g of gs) { const b = g.get(a); s += b.x; } + print(s, a.x); +} diff --git a/tslang/test/tester/own/own_err_interface_fn_any_drops.ts b/tslang/test/tester/own/own_err_interface_fn_any_drops.ts new file mode 100644 index 000000000..a85d18d96 --- /dev/null +++ b/tslang/test/tester/own/own_err_interface_fn_any_drops.ts @@ -0,0 +1,17 @@ +// -mm=own rejects: a function taking an `Op2` reinterpreted through `any` as one taking an `Op`: the +// call through `Op2` inside it may reach `OB.run`, which overwrites `h.c`, so it may destroy what `c` +// borrows (spec 31). +class V { constructor(public x: number) {} } +class H { c: V = new V(5); } +function churn() { let keep: V[] = []; for (let i = 0; i < 2000; i++) { keep.push(new V(77777)); } return keep.length; } +interface Op { run(h: H): number; } +interface Op2 { run(h: H): number; } +class OA2 implements Op2 { run(h: H): number { return h.c.x; } } +class OB implements Op { run(h: H): number { h.c = new V(9); return 0; } } +function f2(o: Op2, h: H) { const c = h.c; o.run(h); churn(); return c.x; } +function main() { + const h = new H(); + const fn = f2 as any as ((o: Op, h: H) => number); + print(fn(new OB(), h)); + print(f2(new OA2(), h)); +} diff --git a/tslang/test/tester/own/own_err_interface_fn_any_param.ts b/tslang/test/tester/own/own_err_interface_fn_any_param.ts new file mode 100644 index 000000000..cc5958d18 --- /dev/null +++ b/tslang/test/tester/own/own_err_interface_fn_any_param.ts @@ -0,0 +1,18 @@ +// -mm=own rejects: a function taking an `Op2` reinterpreted through `any` as one taking an `Op`, +// so an `Op` value reaches `Op2`'s calls unseen. Both interfaces are open, and `G.take`, which returns a +// borrow of its argument, cannot tell the callers through either (spec 31). +class V { constructor(public x: number) {} } +class H { c: V = new V(5); } +function churn() { let keep: V[] = []; for (let i = 0; i < 2000; i++) { keep.push(new V(77777)); } return keep.length; } +interface Op { take(v: V): V; } +interface Op2 { get(v: V): V; } +class G implements Op { take(v: V): V { return v; } } +class G2 implements Op2 { get(v: V): V { return new V(v.x + 1); } } +function f2(o: Op2, a: V) { const b = o.get(a); return b.x; } +function main() { + const a = new V(5); + const fn = f2 as any as ((o: Op, a: V) => number); + print(fn(new G(), a)); + churn(); + print(a.x, f2(new G2(), a)); +} diff --git a/tslang/test/tester/own/own_err_interface_fn_any_result.ts b/tslang/test/tester/own/own_err_interface_fn_any_result.ts new file mode 100644 index 000000000..c36275985 --- /dev/null +++ b/tslang/test/tester/own/own_err_interface_fn_any_result.ts @@ -0,0 +1,21 @@ +// -mm=own rejects: a function returning an `Op` reinterpreted through `any` as one returning an +// `Op2`, so an `Op` value reaches `Op2`'s calls unseen. Both interfaces are open, and `G.take`, which +// returns a borrow of its argument, cannot tell the callers through either (spec 31). +class V { constructor(public x: number) {} } +class H { c: V = new V(5); } +function churn() { let keep: V[] = []; for (let i = 0; i < 2000; i++) { keep.push(new V(77777)); } return keep.length; } +interface Op { take(v: V): V; } +interface Op2 { get(v: V): V; } +class G implements Op { take(v: V): V { return v; } } +class G2 implements Op2 { get(v: V): V { return new V(v.x + 1); } } +function main() { + const a = new V(5); + const o2: Op2 = new G2(); + { const b0 = o2.get(a); print(b0.x); } + const mk = (): Op => new G(); + const mk2 = mk as any as (() => Op2); + const o = mk2(); + { const b = o.get(a); print(b.x); } + churn(); + print(a.x); +} diff --git a/tslang/test/tester/own/own_err_interface_fn_field_drops.ts b/tslang/test/tester/own/own_err_interface_fn_field_drops.ts new file mode 100644 index 000000000..cb86988f9 --- /dev/null +++ b/tslang/test/tester/own/own_err_interface_fn_field_drops.ts @@ -0,0 +1,16 @@ +// -mm=own rejects: a function-typed field of a closed interface whose function overwrites a field of +// the object it is called on. The call through the interface gets that function's facts (spec 31), +// and they say it may destroy what `v` borrows. +class V { + constructor(public x: number) {} +} +interface S { f: (x: number) => number; v: V; } +class P { s: S = { f: function (x: number) { this.v = new V(9); return x; }, v: new V(5) }; } +function use(p: P) { + const v = p.s.v; + p.s.f(1); + return v.x; +} +function main() { + print(use(new P())); +} diff --git a/tslang/test/tester/own/own_err_interface_fn_field_exported.ts b/tslang/test/tester/own/own_err_interface_fn_field_exported.ts new file mode 100644 index 000000000..ffbdb9d35 --- /dev/null +++ b/tslang/test/tester/own/own_err_interface_fn_field_exported.ts @@ -0,0 +1,9 @@ +// -mm=own rejects: a function-typed field of an exported interface. Another module may implement +// it with a function this module never sees, so the call through the field cannot be resolved and +// the borrow of `p.s` it is made through ends at it (spec 31). +export interface S { f: (x: number) => number; r: number; } +class P { s: S = { f: function (x: number) { return x + 1; }, r: 3 }; } +function h(p: P) { return p.s.f(3); } +function main() { + print(h(new P())); +} diff --git a/tslang/test/tester/own/own_err_interface_fn_field_unknown.ts b/tslang/test/tester/own/own_err_interface_fn_field_unknown.ts new file mode 100644 index 000000000..7903fb05f --- /dev/null +++ b/tslang/test/tester/own/own_err_interface_fn_field_unknown.ts @@ -0,0 +1,11 @@ +// -mm=own rejects: a function-typed field of an interface given a function of unknown origin (a +// parameter). The call through the field cannot be resolved, so it may destroy anything, and the +// borrow of `p.s` it is made through ends at it (spec 31). +interface S { f: (x: number) => number; r: number; } +function ten(x: number) { return x * 10; } +function mkS(fn: (x: number) => number): S { return { f: fn, r: 3 }; } +class P { s: S = mkS(ten); } +function h(p: P) { return p.s.f(3); } +function main() { + print(h(new P())); +} diff --git a/tslang/test/tester/own/own_err_interface_from_interface.ts b/tslang/test/tester/own/own_err_interface_from_interface.ts new file mode 100644 index 000000000..0444c38d5 --- /dev/null +++ b/tslang/test/tester/own/own_err_interface_from_interface.ts @@ -0,0 +1,15 @@ +// -mm=own rejects: a `Th` value made out of a `J` value reaches `J`'s implementations through `Th`'s +// slots, which this module does not tie to `Th`. Both interfaces are open, and `G.get`, which returns +// a borrow of its argument, cannot tell the callers through either (spec 31). +class V { + constructor(public x: number) {} +} +interface Th { get(v: V): V; } +interface J extends Th { n(): number; } +class G implements J { n(): number { return 1; } get(v: V): V { return v; } } +function via(t: Th, a: V) { const b = t.get(a); return b.x; } +function main() { + const j: J = new G(); + const a = new V(5); + print(via(j, a), a.x); +} diff --git a/tslang/test/tester/own/own_err_interface_keeps_literal.ts b/tslang/test/tester/own/own_err_interface_keeps_literal.ts new file mode 100644 index 000000000..e7c6aacbf --- /dev/null +++ b/tslang/test/tester/own/own_err_interface_keeps_literal.ts @@ -0,0 +1,16 @@ +// -mm=own rejects: a method that keeps its argument, reached through an exported interface. Another +// module may call through the interface without the method's facts, and would release the literal +// the method keeps (a double free), so the call is open and the method's body is the error. The same +// program with the interface not exported compiles (own_interface_facts, spec 31). +class V { + constructor(public x: number) {} +} +interface Ray { start: V; dir: V; } +export interface Sink { take(r: Ray): void; } +class KS implements Sink { rays: Ray[] = []; take(r: Ray) { this.rays.push(r); } } +function main() { + const ks = new KS(); + const s: Sink = ks; + s.take({ start: new V(1), dir: new V(2) }); + print(ks.rays[0].start.x, ks.rays[0].dir.x); +} diff --git a/tslang/test/tester/own/own_err_interface_keeps_param.ts b/tslang/test/tester/own/own_err_interface_keeps_param.ts new file mode 100644 index 000000000..95f8e481b --- /dev/null +++ b/tslang/test/tester/own/own_err_interface_keeps_param.ts @@ -0,0 +1,15 @@ +// -mm=own rejects: a method that keeps its argument, reached through an exported interface. Another +// module may call through the interface without the method's facts, and would release the value the +// method keeps (a double free), so the call is open and the method's body is the error. The same +// program with the interface not exported compiles (own_interface_facts, spec 31). +class V { + constructor(public x: number) {} +} +export interface SinkV { take(v: V): void; } +class KV implements SinkV { vs: V[] = []; take(v: V) { this.vs.push(v); } } +function main() { + const ks = new KV(); + const s: SinkV = ks; + s.take(new V(1)); + print(ks.vs[0].x); +} diff --git a/tslang/test/tester/own/own_err_interface_member_any.ts b/tslang/test/tester/own/own_err_interface_member_any.ts new file mode 100644 index 000000000..c8db216af --- /dev/null +++ b/tslang/test/tester/own/own_err_interface_member_any.ts @@ -0,0 +1,15 @@ +// -mm=own rejects: own_err_interface_member_class through `any`: a `Runner2` made from a class +// object, put into `any` and read back as a `Runner` (spec 31). +class V { constructor(public x: number) {} } +class H { c: V = new V(5); } +function churn() { let keep: V[] = []; for (let i = 0; i < 2000; i++) { keep.push(new V(77777)); } return keep.length; } +interface Op { run(h: H): number; } +interface Op2 { run(h: H): number; } +class OA2 implements Op2 { run(h: H): number { return h.c.x; } } +class OB implements Op { run(h: H): number { h.c = new V(9); return 0; } } +function f2(o: Op2, h: H) { const c = h.c; o.run(h); churn(); return c.x; } +interface Runner { go(o: Op, h: H): number; } +interface Runner2 { go(o: Op2, h: H): number; } +class R implements Runner2 { go(o: Op2, h: H) { const c = h.c; o.run(h); churn(); return c.x; } } +function call(a: any) { const r = a as Runner; return r.go(new OB(), new H()); } +function main() { const rr = new R(); print(rr.go(new OA2(), new H())); const r2: Runner2 = rr; print(call(r2 as any)); } diff --git a/tslang/test/tester/own/own_err_interface_member_class.ts b/tslang/test/tester/own/own_err_interface_member_class.ts new file mode 100644 index 000000000..6ca3871e6 --- /dev/null +++ b/tslang/test/tester/own/own_err_interface_member_class.ts @@ -0,0 +1,17 @@ +// -mm=own rejects: v11f's shape whose `Runner2` values are made only from a class object (the first +// call goes to the class directly): a `Runner2` seen as a `Runner` hands an `Op` to `R.go`, whose call +// through `Op2` reaches `OB.run`, which overwrites `h.c`. The class's vtable for `Runner2` gives its +// members' types (spec 31). +class V { constructor(public x: number) {} } +class H { c: V = new V(5); } +function churn() { let keep: V[] = []; for (let i = 0; i < 2000; i++) { keep.push(new V(77777)); } return keep.length; } +interface Op { run(h: H): number; } +interface Op2 { run(h: H): number; } +class OA2 implements Op2 { run(h: H): number { return h.c.x; } } +class OB implements Op { run(h: H): number { h.c = new V(9); return 0; } } +function f2(o: Op2, h: H) { const c = h.c; o.run(h); churn(); return c.x; } +interface Runner { go(o: Op, h: H): number; } +interface Runner2 { go(o: Op2, h: H): number; } +class R implements Runner2 { go(o: Op2, h: H) { const c = h.c; o.run(h); churn(); return c.x; } } +function call(r2: Runner2) { const r: Runner = r2; return r.go(new OB(), new H()); } +function main() { const rr = new R(); print(rr.go(new OA2(), new H())); const r2: Runner2 = rr; print(call(r2)); } diff --git a/tslang/test/tester/own/own_err_interface_member_drops.ts b/tslang/test/tester/own/own_err_interface_member_drops.ts new file mode 100644 index 000000000..070c1be17 --- /dev/null +++ b/tslang/test/tester/own/own_err_interface_member_drops.ts @@ -0,0 +1,18 @@ +// -mm=own rejects: one implementation of a closed interface overwrites a field of its argument, so a +// call through the interface may destroy what `c` borrows (spec 31). +class V { + constructor(public x: number) {} +} +class H { c: V = new V(5); } +interface Op { run(h: H): number; } +class OA implements Op { run(h: H): number { return h.c.x; } } +class OB implements Op { run(h: H): number { h.c = new V(9); return 0; } } +function f(o: Op, h: H) { + const c = h.c; + o.run(h); + return c.x; +} +function main() { + const h = new H(); + print(f(new OA(), h), f(new OB(), h)); +} diff --git a/tslang/test/tester/own/own_err_interface_member_methods.ts b/tslang/test/tester/own/own_err_interface_member_methods.ts new file mode 100644 index 000000000..f7b50b5e4 --- /dev/null +++ b/tslang/test/tester/own/own_err_interface_member_methods.ts @@ -0,0 +1,15 @@ +// -mm=own rejects: a `Runner2` (`go(o: Op2, ...)`) seen as a `Runner` (`go(o: Op, ...)`) hands an +// `Op` to `R.go`, whose call through `Op2` then reaches `OB.run`, which overwrites `h.c`. Every +// interface the two sides' members mention is open (spec 31). +class V { constructor(public x: number) {} } +class H { c: V = new V(5); } +function churn() { let keep: V[] = []; for (let i = 0; i < 2000; i++) { keep.push(new V(77777)); } return keep.length; } +interface Op { run(h: H): number; } +interface Op2 { run(h: H): number; } +class OA2 implements Op2 { run(h: H): number { return h.c.x; } } +class OB implements Op { run(h: H): number { h.c = new V(9); return 0; } } +interface Runner { go(o: Op, h: H): number; } +interface Runner2 { go(o: Op2, h: H): number; } +class R implements Runner2 { go(o: Op2, h: H) { const c = h.c; o.run(h); churn(); return c.x; } } +function call(r2: Runner2) { const r: Runner = r2; return r.go(new OB(), new H()); } +function main() { const r2: Runner2 = new R(); print(r2.go(new OA2(), new H())); print(call(r2)); } diff --git a/tslang/test/tester/own/own_err_interface_member_types.ts b/tslang/test/tester/own/own_err_interface_member_types.ts new file mode 100644 index 000000000..a15e0f41c --- /dev/null +++ b/tslang/test/tester/own/own_err_interface_member_types.ts @@ -0,0 +1,15 @@ +// -mm=own rejects: an interface value made out of another one whose members take different +// interfaces of the same shape: a `HasF2` (`f: (o: Op2, ...)`) seen as a `HasF` (`f: (o: Op, ...)`) +// hands an `Op` to `f2`, whose call through `Op2` then reaches `OB.run`, which overwrites `h.c`. +// Every interface the two sides' members mention is open (spec 31). +class V { constructor(public x: number) {} } +class H { c: V = new V(5); } +function churn() { let keep: V[] = []; for (let i = 0; i < 2000; i++) { keep.push(new V(77777)); } return keep.length; } +interface Op { run(h: H): number; } +interface Op2 { run(h: H): number; } +class OA2 implements Op2 { run(h: H): number { return h.c.x; } } +class OB implements Op { run(h: H): number { h.c = new V(9); return 0; } } +function f2(o: Op2, h: H) { const c = h.c; o.run(h); churn(); return c.x; } +interface HasF2 { f: (o: Op2, h: H) => number; } +interface HasF { f: (o: Op, h: H) => number; } +function main() { const x: HasF2 = { f: f2 }; print(x.f(new OA2(), new H())); const y: HasF = x; print(y.f(new OB(), new H())); } diff --git a/tslang/test/tester/own/own_err_interface_public_exported.ts b/tslang/test/tester/own/own_err_interface_public_exported.ts new file mode 100644 index 000000000..3f3846992 --- /dev/null +++ b/tslang/test/tester/own/own_err_interface_public_exported.ts @@ -0,0 +1,15 @@ +// -mm=own rejects: a `public` method of an exported class reached through an interface. Another +// module can name the class and call the method without its facts, so it may not return a borrow of +// its argument; a `public` method of a class no other module can name may (own_interface_facts, +// spec 31). +class V { + constructor(public x: number) {} +} +interface Pick { pick(v: V): V; } +export class PA implements Pick { public pick(v: V): V { return v; } } +function main() { + const p: Pick = new PA(); + const a = new V(5); + const b = p.pick(a); + print(b.x, a.x); +} diff --git a/tslang/test/tester/own/own_err_interface_public_reachable.ts b/tslang/test/tester/own/own_err_interface_public_reachable.ts new file mode 100644 index 000000000..ae5259756 --- /dev/null +++ b/tslang/test/tester/own/own_err_interface_public_reachable.ts @@ -0,0 +1,15 @@ +// -mm=own rejects: a `public` method of a class that is not exported but that an exported function +// returns. Another module gets a value of the class from it and may call the method without its +// facts, so it may not return a borrow of its argument (spec 31). Unreachable from another module, +// the same method keeps its facts (own_interface_facts). +class V { + constructor(public x: number) {} +} +class PA { public pick(v: V): V { return v; } } +export function make(): PA { return new PA(); } +function main() { + const p = make(); + const a = new V(5); + const b = p.pick(a); + print(b.x, a.x); +} diff --git a/tslang/test/tester/own/own_err_interface_result_outlives.ts b/tslang/test/tester/own/own_err_interface_result_outlives.ts new file mode 100644 index 000000000..b666e942e --- /dev/null +++ b/tslang/test/tester/own/own_err_interface_result_outlives.ts @@ -0,0 +1,13 @@ +// -mm=own rejects: the result of a call through a closed interface borrows an element of the object +// behind the interface value, and the local holding that value is given a new one before the read. +class V { + constructor(public x: number) {} +} +interface First { first(): V; } +class F implements First { items: V[] = [new V(3)]; first(): V { return this.items[0]; } } +function main() { + let i: First = new F(); + const x = i.first(); + i = new F(); + print(x.x); +} diff --git a/tslang/test/tester/own/own_err_kept_and_borrowed_arg.ts b/tslang/test/tester/own/own_err_kept_and_borrowed_arg.ts new file mode 100644 index 000000000..714df6888 --- /dev/null +++ b/tslang/test/tester/own/own_err_kept_and_borrowed_arg.ts @@ -0,0 +1,46 @@ +// -mm=own rejects: `f` keeps `p` (spec 33: it moves into a `let` that is given another value) and +// borrows `q`; `f(n, n)` gives it the same object as both, so the callee frees it under its own +// borrow. The second `n` is a use after the move into the first. +class V { + constructor(public x: number) {} +} + +class N { + constructor(public v: V) {} +} + +class H { + n: N = new N(new V(0)); +} + +@noinline +function mk(i: number) { + return new N(new V(i)); +} + +function churn() { + const hold: V[] = []; + for (let i = 0; i < 64; i++) { + hold.push(new V(777)); + } +} + +@noinline +function f(p: N, q: N) { + const inner = q.v; + let r = p; + r = new N(new V(2)); + churn(); + return inner.x + r.v.x; +} + +function main() { + let s: number = 0; + for (let i = 0; i < 1000; i++) { + const n = mk(i); + s += f(n, n); + } + + print(s); + assert(s == 501500); +} diff --git a/tslang/test/tester/own/own_err_kept_and_borrowed_arg_assigned_in_call.ts b/tslang/test/tester/own/own_err_kept_and_borrowed_arg_assigned_in_call.ts new file mode 100644 index 000000000..65b299357 --- /dev/null +++ b/tslang/test/tester/own/own_err_kept_and_borrowed_arg_assigned_in_call.ts @@ -0,0 +1,42 @@ +// -mm=own rejects: `f` keeps `p` (it moves into a `let` that is given another value, spec 33) and +// borrows `q`, and the call's own argument list gives `n` another value after both reads of it: +// `f(n, n, (n = mk(5), 0))`. The assignment releases the old object before `f` takes it through +// `p` and reads it through `q`, so the assignment is a use of `n` after the move. +class V { + constructor(public x: number) {} +} + +class N { + constructor(public v: V) {} +} + +@noinline +function mk(i: number) { + return new N(new V(i)); +} + +function churn() { + const hold: V[] = []; + for (let i = 0; i < 64; i++) { + hold.push(new V(777)); + } +} + +@noinline +function f(p: N, q: N, z: number) { + let r = p; + r = new N(new V(2)); + churn(); + return q.v.x + r.v.x - 2 + z; +} + +function main() { + let s: number = 0; + for (let i = 0; i < 1000; i++) { + let n = mk(i); + s += f(n, n, (n = mk(5), 0)) + 2; + } + + print(s); + assert(s == 501500); +} diff --git a/tslang/test/tester/own/own_err_kept_and_borrowed_arg_field.ts b/tslang/test/tester/own/own_err_kept_and_borrowed_arg_field.ts new file mode 100644 index 000000000..3e76c2a88 --- /dev/null +++ b/tslang/test/tester/own/own_err_kept_and_borrowed_arg_field.ts @@ -0,0 +1,46 @@ +// -mm=own rejects: `f` keeps `p` (stored into `h.n`) and borrows `q`; `f(h, n, n)` gives it the same +// object as both, and `h.n = new N(...)` frees it while `q` is still read. The second `n` is a use +// after the move into the first. +class V { + constructor(public x: number) {} +} + +class N { + constructor(public v: V) {} +} + +class H { + n: N = new N(new V(0)); +} + +@noinline +function mk(i: number) { + return new N(new V(i)); +} + +function churn() { + const hold: V[] = []; + for (let i = 0; i < 64; i++) { + hold.push(new V(777)); + } +} + +@noinline +function f(h: H, p: N, q: N) { + h.n = p; + h.n = new N(new V(2)); + churn(); + return q.v.x + h.n.v.x; +} + +function main() { + const h = new H(); + let s: number = 0; + for (let i = 0; i < 1000; i++) { + const n = mk(i); + s += f(h, n, n); + } + + print(s); + assert(s == 501500); +} diff --git a/tslang/test/tester/own/own_err_kept_and_borrowed_arg_slot.ts b/tslang/test/tester/own/own_err_kept_and_borrowed_arg_slot.ts new file mode 100644 index 000000000..ef904b798 --- /dev/null +++ b/tslang/test/tester/own/own_err_kept_and_borrowed_arg_slot.ts @@ -0,0 +1,49 @@ +// -mm=own rejects: as own_err_kept_and_borrowed_arg, with `n` an owning `let`: both arguments of +// `f(n, n)` are reads of its slot, and the one `f` borrows is a use after the move into the other. +class V { + constructor(public x: number) {} +} + +class N { + constructor(public v: V) {} +} + +class H { + n: N = new N(new V(0)); +} + +@noinline +function mk(i: number) { + return new N(new V(i)); +} + +function churn() { + const hold: V[] = []; + for (let i = 0; i < 64; i++) { + hold.push(new V(777)); + } +} + +@noinline +function f(p: N, q: N) { + const inner = q.v; + let r = p; + r = new N(new V(2)); + churn(); + return inner.x + r.v.x; +} + +function main() { + let s: number = 0; + for (let i = 0; i < 1000; i++) { + let n = mk(i); + if (i > 2000) { + n = mk(0); + } + + s += f(n, n); + } + + print(s); + assert(s == 501500); +} diff --git a/tslang/test/tester/own/own_err_kept_arg_assigned_in_and.ts b/tslang/test/tester/own/own_err_kept_arg_assigned_in_and.ts new file mode 100644 index 000000000..d6ce9c165 --- /dev/null +++ b/tslang/test/tester/own/own_err_kept_arg_assigned_in_and.ts @@ -0,0 +1,45 @@ +// -mm=own rejects: `f` keeps `p` (it moves into a `let` that is given another value, spec 33), and +// the right side of an `&&` in the call's own argument list gives `n` another value after `n` was +// read for `p`: `f(n, (i % 2 == 0 && (n = mk(7), true)) ? 0 : 0)`. When the left side holds, the +// assignment releases the old object before `f` takes it, so the assignment is a use of `n` after +// the move, though the other path leaves `n` alone. +class V { + constructor(public x: number) {} +} + +class N { + constructor(public v: V) {} +} + +@noinline +function mk(i: number) { + return new N(new V(i)); +} + +function churn() { + const hold: V[] = []; + for (let i = 0; i < 64; i++) { + hold.push(new V(777)); + } +} + +@noinline +function f(p: N, z: number) { + const k = p.v.x; + churn(); + let r = p; + r = new N(new V(2)); + churn(); + return r.v.x - 2 + z + k; +} + +function main() { + let s: number = 0; + for (let i = 0; i < 1000; i++) { + let n = mk(i); + s += f(n, (i % 2 == 0 && (n = mk(7), true)) ? 0 : 0) + 2; + } + + print(s); + assert(s == 501500); +} diff --git a/tslang/test/tester/own/own_err_kept_arg_assigned_in_call.ts b/tslang/test/tester/own/own_err_kept_arg_assigned_in_call.ts new file mode 100644 index 000000000..d8017cb32 --- /dev/null +++ b/tslang/test/tester/own/own_err_kept_arg_assigned_in_call.ts @@ -0,0 +1,44 @@ +// -mm=own rejects: `f` keeps `p` (it moves into a `let` that is given another value, spec 33), and +// the call's own argument list gives `n` another value after `n` was read for `p`: +// `f(n, (n = mk(7), 0))`. The assignment releases the old object before `f` takes it, so the +// assignment is a use of `n` after the move. +class V { + constructor(public x: number) {} +} + +class N { + constructor(public v: V) {} +} + +@noinline +function mk(i: number) { + return new N(new V(i)); +} + +function churn() { + const hold: V[] = []; + for (let i = 0; i < 64; i++) { + hold.push(new V(777)); + } +} + +@noinline +function f(p: N, z: number) { + const k = p.v.x; + churn(); + let r = p; + r = new N(new V(2)); + churn(); + return r.v.x - 2 + z + k; +} + +function main() { + let s: number = 0; + for (let i = 0; i < 1000; i++) { + let n = mk(i); + s += f(n, (n = mk(7), 0)) + 2; + } + + print(s); + assert(s == 501500); +} diff --git a/tslang/test/tester/own/own_err_kept_arg_assigned_on_arm.ts b/tslang/test/tester/own/own_err_kept_arg_assigned_on_arm.ts new file mode 100644 index 000000000..05e58e737 --- /dev/null +++ b/tslang/test/tester/own/own_err_kept_arg_assigned_on_arm.ts @@ -0,0 +1,45 @@ +// -mm=own rejects: `f` keeps `p` (it moves into a `let` that is given another value, spec 33), and +// one arm of a conditional in the call's own argument list gives `n` another value after `n` was +// read for `p`: `f(n, i >= 0 ? (n = mk(7), 0) : 0)`. On that arm the assignment releases the old +// object before `f` takes it, so the assignment is a use of `n` after the move, though the other +// arm leaves `n` alone. +class V { + constructor(public x: number) {} +} + +class N { + constructor(public v: V) {} +} + +@noinline +function mk(i: number) { + return new N(new V(i)); +} + +function churn() { + const hold: V[] = []; + for (let i = 0; i < 64; i++) { + hold.push(new V(777)); + } +} + +@noinline +function f(p: N, z: number) { + const k = p.v.x; + churn(); + let r = p; + r = new N(new V(2)); + churn(); + return r.v.x - 2 + z + k; +} + +function main() { + let s: number = 0; + for (let i = 0; i < 1000; i++) { + let n = mk(i); + s += f(n, i >= 0 ? (n = mk(7), 0) : 0) + 2; + } + + print(s); + assert(s == 501500); +} diff --git a/tslang/test/tester/own/own_err_kept_param_field_overwritten.ts b/tslang/test/tester/own/own_err_kept_param_field_overwritten.ts new file mode 100644 index 000000000..0a50730a7 --- /dev/null +++ b/tslang/test/tester/own/own_err_kept_param_field_overwritten.ts @@ -0,0 +1,46 @@ +// -mm=own rejects: `p` is kept (stored into `h.n`), and `inner`, read out of `p` before the move, is +// bounded by `h.n` from then on (spec 33); `h.n = new N(...)` frees `p` and `p.v`, and `inner` is +// read after it. +class V { + constructor(public x: number) {} +} + +class N { + constructor(public v: V) {} +} + +class H { + n: N = new N(new V(0)); +} + +@noinline +function mk(i: number) { + return new N(new V(i)); +} + +function churn() { + const hold: V[] = []; + for (let i = 0; i < 64; i++) { + hold.push(new V(777)); + } +} + +@noinline +function f(h: H, p: N) { + const inner = p.v; + h.n = p; + h.n = new N(new V(2)); + churn(); + return inner.x + h.n.v.x; +} + +function main() { + const h = new H(); + let s: number = 0; + for (let i = 0; i < 1000; i++) { + s += f(h, mk(i)); + } + + print(s); + assert(s == 501500); +} diff --git a/tslang/test/tester/own/own_err_kept_param_let_overwritten.ts b/tslang/test/tester/own/own_err_kept_param_let_overwritten.ts new file mode 100644 index 000000000..97d994227 --- /dev/null +++ b/tslang/test/tester/own/own_err_kept_param_let_overwritten.ts @@ -0,0 +1,41 @@ +// -mm=own rejects: `p` moves into the `let r` (spec 33: a parameter read into an owning local that +// is given another value is kept), and `inner`, read out of `p` before the move, is bounded by `r` +// from then on; `r = new N(...)` frees `p` and `p.v`, and `inner` is read after it. +class V { + constructor(public x: number) {} +} + +class N { + constructor(public v: V) {} +} + +@noinline +function mk(i: number) { + return new N(new V(i)); +} + +function churn() { + const hold: V[] = []; + for (let i = 0; i < 64; i++) { + hold.push(new V(777)); + } +} + +@noinline +function f(p: N) { + const inner = p.v; + let r = p; + r = new N(new V(2)); + churn(); + return inner.x + r.v.x; +} + +function main() { + let s: number = 0; + for (let i = 0; i < 1000; i++) { + s += f(mk(i)); + } + + print(s); + assert(s == 501500); +} diff --git a/tslang/test/tester/own/own_err_kept_twice.ts b/tslang/test/tester/own/own_err_kept_twice.ts new file mode 100644 index 000000000..5af845ab1 --- /dev/null +++ b/tslang/test/tester/own/own_err_kept_twice.ts @@ -0,0 +1,43 @@ +// -mm=own rejects: `f` keeps both `p` and `q` (each moves into a `let` that is given another +// value, spec 33), and `f(n, n)` gives it the same object twice: two owners, each of which frees it. +// The second `n` is a use after the move into the first. +class V { + constructor(public x: number) {} +} + +class N { + constructor(public v: V) {} +} + +@noinline +function mk(i: number) { + return new N(new V(i)); +} + +function churn() { + const hold: V[] = []; + for (let i = 0; i < 64; i++) { + hold.push(new V(777)); + } +} + +@noinline +function f(p: N, q: N) { + let r = p; + let t = q; + r = new N(new V(2)); + t = new N(new V(0)); + churn(); + return r.v.x + t.v.x - 2; +} + +function main() { + let s: number = 0; + for (let i = 0; i < 1000; i++) { + const n = mk(i); + s += f(n, n) + i + 2; + } + + print(s); + assert(s == 501500); +} diff --git a/tslang/test/tester/own/own_err_kept_twice_field.ts b/tslang/test/tester/own/own_err_kept_twice_field.ts new file mode 100644 index 000000000..7bd132b57 --- /dev/null +++ b/tslang/test/tester/own/own_err_kept_twice_field.ts @@ -0,0 +1,47 @@ +// -mm=own rejects: `f` keeps `p` (stored into `h.n`) and `q` (moved into a `let` given another +// value), and `f(h, n, n)` gives it the same object twice. The second `n` is a use after the move +// into the first. +class V { + constructor(public x: number) {} +} + +class N { + constructor(public v: V) {} +} + +@noinline +function mk(i: number) { + return new N(new V(i)); +} + +function churn() { + const hold: V[] = []; + for (let i = 0; i < 64; i++) { + hold.push(new V(777)); + } +} + +class H { + n: N = new N(new V(0)); +} + +@noinline +function f(h: H, p: N, q: N) { + h.n = p; + let t = q; + t = new N(new V(0)); + churn(); + return h.n.v.x + t.v.x + 2; +} + +function main() { + const h = new H(); + let s: number = 0; + for (let i = 0; i < 1000; i++) { + const n = mk(i); + s += f(h, n, n); + } + + print(s); + assert(s == 501500); +} diff --git a/tslang/test/tester/own/own_err_kept_twice_push.ts b/tslang/test/tester/own/own_err_kept_twice_push.ts new file mode 100644 index 000000000..ee9d07baa --- /dev/null +++ b/tslang/test/tester/own/own_err_kept_twice_push.ts @@ -0,0 +1,43 @@ +// -mm=own rejects: `f` pushes both `p` and `q` into `arr`, so it keeps both, and `f(arr, n, n)` gives +// it the same object twice: the array holds two owners of one block. The second `n` is a use after +// the move into the first. +class V { + constructor(public x: number) {} +} + +class N { + constructor(public v: V) {} +} + +@noinline +function mk(i: number) { + return new N(new V(i)); +} + +function churn() { + const hold: V[] = []; + for (let i = 0; i < 64; i++) { + hold.push(new V(777)); + } +} + +@noinline +function f(arr: N[], p: N, q: N) { + arr.push(p); + arr.push(q); + return 0; +} + +function main() { + let s: number = 0; + for (let i = 0; i < 1000; i++) { + const arr: N[] = []; + const n = mk(i); + s += f(arr, n, n) + i + 2; + churn(); + s += arr[0].v.x - i; + } + + print(s); + assert(s == 501500); +} diff --git a/tslang/test/tester/own/own_err_lends_index_64.ts b/tslang/test/tester/own/own_err_lends_index_64.ts new file mode 100644 index 000000000..790ee96cc --- /dev/null +++ b/tslang/test/tester/own/own_err_lends_index_64.ts @@ -0,0 +1,25 @@ +// -mm=own rejects: a method's '@lends' is kept as a mask of 64 bits for the class's declaration, +// so it can name only the method's arguments 0 to 63 (the receiver is 0); 'p63' is argument 64. +class V { + constructor(public x: number) {} +} + +class K { + @lends("p63") + pick( + p0: V, p1: V, p2: V, p3: V, p4: V, p5: V, p6: V, p7: V, + p8: V, p9: V, p10: V, p11: V, p12: V, p13: V, p14: V, p15: V, + p16: V, p17: V, p18: V, p19: V, p20: V, p21: V, p22: V, p23: V, + p24: V, p25: V, p26: V, p27: V, p28: V, p29: V, p30: V, p31: V, + p32: V, p33: V, p34: V, p35: V, p36: V, p37: V, p38: V, p39: V, + p40: V, p41: V, p42: V, p43: V, p44: V, p45: V, p46: V, p47: V, + p48: V, p49: V, p50: V, p51: V, p52: V, p53: V, p54: V, p55: V, + p56: V, p57: V, p58: V, p59: V, p60: V, p61: V, p62: V, p63: V + ): V { + return p63; + } +} + +function main() { + print("done."); +} diff --git a/tslang/test/tester/own/own_err_literal_moved_twice.ts b/tslang/test/tester/own/own_err_literal_moved_twice.ts new file mode 100644 index 000000000..9dee98988 --- /dev/null +++ b/tslang/test/tester/own/own_err_literal_moved_twice.ts @@ -0,0 +1,16 @@ +// -mm=own rejects: `p` moves into the first record, which `keepRay` keeps, and is used again in +// the second (spec 27). +class V { + constructor(public x: number) {} + static plus(a: V, b: V) { return new V(a.x + b.x); } +} +interface Ray { start: V; dir: V; } +class Keeper { rays: Ray[] = []; } +function keepRay(k: Keeper, r: Ray) { k.rays.push(r); } +function main() { + const p = new V(1); + const k = new Keeper(); + keepRay(k, { start: p, dir: new V(2) }); + keepRay(k, { start: p, dir: new V(3) }); + print(k.rays.length); +} diff --git a/tslang/test/tester/own/own_err_mixed_return.ts b/tslang/test/tester/own/own_err_mixed_return.ts index 3f67733f4..6e0d87134 100644 --- a/tslang/test/tester/own/own_err_mixed_return.ts +++ b/tslang/test/tester/own/own_err_mixed_return.ts @@ -1,6 +1,6 @@ -// -mm=own rejects: `pick` returns its argument on one path and a new object on the other, so -// its callers cannot be told whether they own the result. (A string there is copied instead, -// spec 22.) +// -mm=own rejects: `pick` returns its argument on one path and a new object on the other, so it +// keeps that parameter (spec 33) - the caller moves `c` in - and the caller still reads `c` after +// the call, which gave it up. (A string there is copied instead, spec 22.) class C { constructor(public n: string) {} } @@ -10,5 +10,5 @@ function pick(c: C, other: boolean) { } function main() { const c = new C("a"); - print(pick(c, true).n); + print(pick(c, true).n, c.n); } diff --git a/tslang/test/tester/own/own_err_mixed_return_join_use.ts b/tslang/test/tester/own/own_err_mixed_return_join_use.ts new file mode 100644 index 000000000..47e8dd0ba --- /dev/null +++ b/tslang/test/tester/own/own_err_mixed_return_join_use.ts @@ -0,0 +1,21 @@ +// -mm=own rejects: `g` returns `n` on one path and a new object on the other, so it keeps `n` and +// releases it where the paths join on the path that did not return it (spec 33). `v`, read out of +// `n` before the join, is used after it: on that path its block was released. +class V { + constructor(public x: number) {} +} + +class N { + constructor(public v: V) {} +} + +function g(n: N, k: number) { + const v = n.v; + const r = k > 1 ? n : new N(new V(1)); + print(v.x); + return r; +} + +function main() { + print(g(new N(new V(3)), 0).v.x); +} diff --git a/tslang/test/tester/own/own_err_override_imported_base.ts b/tslang/test/tester/own/own_err_override_imported_base.ts new file mode 100644 index 000000000..f4cc2471e --- /dev/null +++ b/tslang/test/tester/own/own_err_override_imported_base.ts @@ -0,0 +1,7 @@ +// -mm=own rejects: a `public` override of an imported class's abstract method. The library calls it +// through `Base`'s vtable without its facts and releases what it keeps, so it may not keep its +// argument (spec 31) - in an executable or the JIT's entry module too. +import './own_override_base_lib' +class Der extends Base { saved: V[] = []; public keep(v: V) { this.saved.push(v); } } +function churn() { const keep: V[] = []; for (let i = 0; i < 2000; i++) keep.push(new V(77777)); return keep.length; } +function main() { const d = new Der(); run(d); churn(); assert(d.saved[0].x == 5, "kept value still there"); print("done."); } diff --git a/tslang/test/tester/own/own_err_override_imported_class.ts b/tslang/test/tester/own/own_err_override_imported_class.ts new file mode 100644 index 000000000..c9beb7f98 --- /dev/null +++ b/tslang/test/tester/own/own_err_override_imported_class.ts @@ -0,0 +1,7 @@ +// -mm=own rejects: an override (not `public`) of a method of an imported class with a body. The +// library calls it through the class's vtable without its facts, so it may not keep its argument +// (spec 31). +import './own_override_base_lib' +class Der extends Concrete { saved: V[] = []; keep(v: V) { this.saved.push(v); } } +function churn() { const keep: V[] = []; for (let i = 0; i < 2000; i++) keep.push(new V(77777)); return keep.length; } +function main() { const d = new Der(); runConcrete(d); churn(); assert(d.saved[0].x == 5, "kept value still there"); print("done."); } diff --git a/tslang/test/tester/own/own_err_override_nameable_base.ts b/tslang/test/tester/own/own_err_override_nameable_base.ts new file mode 100644 index 000000000..9f4a60d6a --- /dev/null +++ b/tslang/test/tester/own/own_err_override_nameable_base.ts @@ -0,0 +1,9 @@ +// -mm=own rejects: a `public` method of a class no other module can name, that overrides a method of +// an exported class. Another module gets a `Base` from `mk()` and calls `keep` through `Base`'s +// vtable, without the method's facts, so the method may not keep its argument (spec 31). +export class V { constructor(public x: number) {} } +export abstract class Base { abstract keep(v: V): void; } +class Der extends Base { saved: V[] = []; public keep(v: V) { this.saved.push(v); } public first() { return this.saved[0].x; } } +let d = new Der(); +export function mk(): Base { return d; } +export function first() { return d.first(); } diff --git a/tslang/test/tester/own/own_err_param_kept_not_owned.ts b/tslang/test/tester/own/own_err_param_kept_not_owned.ts index f37db9bab..1920bd12e 100644 --- a/tslang/test/tester/own/own_err_param_kept_not_owned.ts +++ b/tslang/test/tester/own/own_err_param_kept_not_owned.ts @@ -1,21 +1,24 @@ // -mm=own, phase 4 rejects: `keep` keeps its parameter, but `main` passes a global, which it does -// not own and cannot move. +// not own and cannot move. `reset` gives the global another value, so it is not immortal (spec 25). +// `keep` pushes, which frees nothing: a keeper that may drop (`h.c = c` frees the old `h.c`) is +// rejected at the call already, as a written global lent to it (#639). class C { constructor(public x: number) {} } -class H { - c: C | null = null; -} - let g = new C(1); -function keep(h: H, c: C) { - h.c = c; +function keep(a: C[], c: C) { + a.push(c); +} + +function reset() { + g = new C(2); } function main() { - const h = new H(); - keep(h, g); - print(h.c!.x); + const a: C[] = []; + keep(a, g); + print(a[0].x); + reset(); } diff --git a/tslang/test/tester/own/own_err_read_after_move_after_loop.ts b/tslang/test/tester/own/own_err_read_after_move_after_loop.ts new file mode 100644 index 000000000..8108c6bd8 --- /dev/null +++ b/tslang/test/tester/own/own_err_read_after_move_after_loop.ts @@ -0,0 +1,15 @@ +// @strict-null false +// -mm=own rejects: `v` moved into `best`, and a later loop overwrites `best` before `v` is read +// after it (spec 29). +class V { + constructor(public x: number) {} +} +function main() { + let best: V = undefined; + let v = new V(1); + best = v; + for (let i = 0; i < 3; i++) { + best = new V(i); + } + print(v.x, best.x); +} diff --git a/tslang/test/tester/own/own_err_read_after_move_call.ts b/tslang/test/tester/own/own_err_read_after_move_call.ts new file mode 100644 index 000000000..26cedf3de --- /dev/null +++ b/tslang/test/tester/own/own_err_read_after_move_call.ts @@ -0,0 +1,12 @@ +// -mm=own rejects: `c` moved into `h.c`, and a call given `h` - whose facts say it may drop - comes +// before `c` is read (spec 29). +class C { x: number = 5; } +class H { c: C | null = null; } +function clear(h: H) { h.c = null; } +function main() { + const h = new H(); + const c = new C(); + h.c = c; + clear(h); + print(c.x); +} diff --git a/tslang/test/tester/own/own_err_read_after_move_global.ts b/tslang/test/tester/own/own_err_read_after_move_global.ts new file mode 100644 index 000000000..073880924 --- /dev/null +++ b/tslang/test/tester/own/own_err_read_after_move_global.ts @@ -0,0 +1,75 @@ +// @strict-null false +// -mm=own rejects: a value moved into a field of a parameter or of `this` is read after a global +// is assigned - here, or in a callee. The parameter or `this` may be the global's old value, which +// the assignment frees, and the moved value with it (spec 29). +class C { + x: number = 5; + v: number[] = []; +} + +class H { + c: C | null = null; +} + +function mkH(): H { + const r = new H(); + return r; +} + +function mk(): C { + const r = new C(); + r.v.push(1); + return r; +} + +function churn() { + const junk: C[] = []; + for (let i = 0; i < 64; i++) { + const c = mk(); + c.x = 999; + c.v.push(99); + junk.push(c); + } +} + +class K { + f: C | null = null; + run() { + const c = mk(); + this.f = c; + GK = new K(); + churn(); + print(c.x); + } +} + +let G: H = mkH(); +let GK: K = new K(); + +function reset() { + G = mkH(); +} + +function run(h: H) { + const c = mk(); + h.c = c; + G = mkH(); + churn(); + print(c.x); +} + +function runCallee(h: H) { + const c = mk(); + h.c = c; + reset(); + churn(); + print(c.x); +} + +// The rule is the callees': each is given a fresh value here, since a written global's value lent +// to a call that may drop is rejected at the call already (#639). +function main() { + run(mkH()); + new K().run(); + runCallee(mkH()); +} diff --git a/tslang/test/tester/own/own_err_read_after_move_loop.ts b/tslang/test/tester/own/own_err_read_after_move_loop.ts new file mode 100644 index 000000000..aefc1e826 --- /dev/null +++ b/tslang/test/tester/own/own_err_read_after_move_loop.ts @@ -0,0 +1,30 @@ +// @strict-null false +// -mm=own rejects: `v` moved into `best` before the loop, and the read at the top of each iteration +// is reached around the back edge after the iteration's overwrite of `best` freed it (spec 29). +class V { + constructor(public x: number) {} +} + +function mkv(n: number) { + return new V(n); +} + +function churn() { + const junk: V[] = []; + for (let i = 0; i < 64; i++) { + junk.push(mkv(999)); + } +} + +function main() { + let best: V = undefined; + let v = mkv(1); + best = v; + let s: number = 0; + for (let i = 0; i < 3; i++) { + s += v.x; + best = mkv(i); + churn(); + } + print(s, best.x); +} diff --git a/tslang/test/tester/own/own_err_read_after_move_overwritten.ts b/tslang/test/tester/own/own_err_read_after_move_overwritten.ts new file mode 100644 index 000000000..467f12c39 --- /dev/null +++ b/tslang/test/tester/own/own_err_read_after_move_overwritten.ts @@ -0,0 +1,11 @@ +// -mm=own rejects: `c` moved into `h.c`, which is overwritten on one path before `c` is read (spec +// 29). +class C { x: number = 5; } +class H { c: C | null = null; } +function main() { + const h = new H(); + const c = new C(); + h.c = c; + if (c.x > 1) { h.c = null; } + print(c.x); +} diff --git a/tslang/test/tester/own/own_err_read_after_move_view.ts b/tslang/test/tester/own/own_err_read_after_move_view.ts new file mode 100644 index 000000000..abfcc63e5 --- /dev/null +++ b/tslang/test/tester/own/own_err_read_after_move_view.ts @@ -0,0 +1,13 @@ +// -mm=own rejects: `c` moved into `h.c`, which is overwritten through an interface view of `h` +// before `c` is read (spec 29). +class C { x: number = 5; } +interface HI { c: C | null; } +class H { c: C | null = null; } +function main() { + const h = new H(); + const hv: HI = h; + const c = new C(); + h.c = c; + hv.c = null; + print(c.x); +} diff --git a/tslang/test/tester/own/own_err_reassign_some_paths.ts b/tslang/test/tester/own/own_err_reassign_some_paths.ts index c2aa8fce5..175d83d11 100644 --- a/tslang/test/tester/own/own_err_reassign_some_paths.ts +++ b/tslang/test/tester/own/own_err_reassign_some_paths.ts @@ -1,5 +1,7 @@ -// -mm=own rejects: `a` is given a new value after its value moved on one path only, so its release -// at scope exit would destroy the new value on that path and the moved one on the other. +// -mm=own rejects: `a` is given a new value after its value moved on one path only, and is read +// after the paths join: its release there would destroy the new value on that path and the moved +// one on the other, and it cannot come before the read. (Without the read, the path that gave it a +// new value releases it before the paths join, spec 30.) class C { x: number = 5; } class H { c: C | null = null; } function main() { @@ -11,5 +13,6 @@ function main() { print(a.x); } + print(a.x); print("done."); } diff --git a/tslang/test/tester/own/own_err_record_literal_given.ts b/tslang/test/tester/own/own_err_record_literal_given.ts deleted file mode 100644 index aa5ff5fd8..000000000 --- a/tslang/test/tester/own/own_err_record_literal_given.ts +++ /dev/null @@ -1,14 +0,0 @@ -// -mm=own rejects: an object literal that holds a new value is moved only into a local or returned -// as it is; given as an interface, it is boxed into a block of its own. -class Vec { - constructor(public n: number) {} -} -interface Holder { - v: Vec; -} -function make(n: number): Holder { - return { v: new Vec(n) }; -} -function main() { - print(make(1).v.n); -} diff --git a/tslang/test/tester/own/own_err_shared_literal_borrowed.ts b/tslang/test/tester/own/own_err_shared_literal_borrowed.ts new file mode 100644 index 000000000..83ca80dd4 --- /dev/null +++ b/tslang/test/tester/own/own_err_shared_literal_borrowed.ts @@ -0,0 +1,9 @@ +// Shared of an object literal (spec 26): only a value made in the global's initializer moves into +// the handle. `b`'s initializer gives `new Shared` the payload `a` holds - a second owner of `a`'s +// block, which -mm=own does not count: rejected. +interface S { f: (x: number) => number; roughness: number; } +let a = new Shared({ f: function (x: number) { return x * 2; }, roughness: 2 }); +let b = new Shared(a.value); +function main() { + print(a.value.roughness, b.value.roughness); +} diff --git a/tslang/test/tester/own/own_err_union_moved.ts b/tslang/test/tester/own/own_err_union_moved.ts index 55ad90a33..c42e652cf 100644 --- a/tslang/test/tester/own/own_err_union_moved.ts +++ b/tslang/test/tester/own/own_err_union_moved.ts @@ -1,11 +1,12 @@ -// -mm=own, phase 3 rejects: `c` is moved into the nullable field through the widening view, then -// read. +// -mm=own rejects: `c` is moved into the nullable field through the widening view, and the field +// is set to null - freeing `c` - before `c` is read (spec 29). class C { x: number = 5; } class H { c: C | null = null; } function main() { const h = new H(); const c = new C(); h.c = c; + h.c = null; print(c.x); print("done."); } diff --git a/tslang/test/tester/own/own_field_chain_let.ts b/tslang/test/tester/own/own_field_chain_let.ts new file mode 100644 index 000000000..351219943 --- /dev/null +++ b/tslang/test/tester/own/own_field_chain_let.ts @@ -0,0 +1,41 @@ +// -mm=own: a field read through an interface is a place read like a class field's (spec 28). A +// `let` initialized from one - through any chain of field reads and interface views - borrows the +// field, bounded by what it was read through; a call ends the borrow only when its facts say it +// may drop or overwrite it. A function may return such a read as a borrow of its parameter. +class V { + constructor(public x: number) {} + static plus(a: V, b: V) { return new V(a.x + b.x); } +} +interface R { dir: V; } +interface Is { ray: R; dist: number; } +class RayC implements R { constructor(public dir: V) {} } +function f(r: R) { let d = r.dir; let p = V.plus(d, d); return V.plus(p, d); } +function sh(i: Is) { let d = i.ray.dir; let pos = V.plus(d, i.ray.dir); return V.plus(d, pos); } +function dirOf(r: R) { return r.dir; } +function keep(r: R, v: V) { r.dir = v; } +function main() { + const r: R = { dir: new V(1) }; + const i: Is = { ray: { dir: new V(2) }, dist: 3 }; + print(f(r).x, sh(i).x); // 3 6 + const c = new RayC(new V(4)); + print(f(c).x, dirOf(c).x); // 12 4 + let s: number = 0; + for (let k = 0; k < 100000; k++) { + s += f(r).x + sh(i).x + dirOf(i.ray).x; + } + + print(s, r.dir.x, i.ray.dir.x, c.dir.x); // 1.1e+06 1 2 4 + assert(s == 1100000 && r.dir.x == 1 && i.ray.dir.x == 2 && c.dir.x == 4); + // a store through an interface field keeps the value: `nv` moves into `k.dir` + const k: R = { dir: new V(0) }; + const nv = new V(7); + keep(k, nv); + let t: number = 0; + for (let j = 0; j < 1000; j++) { + t += V.plus(k.dir, k.dir).x; + } + + print(t, k.dir.x); // 14000 7 + assert(t == 14000 && k.dir.x == 7); + print("done."); +} diff --git a/tslang/test/tester/own/own_fresh_call_result.ts b/tslang/test/tester/own/own_fresh_call_result.ts new file mode 100644 index 000000000..bc2ce011c --- /dev/null +++ b/tslang/test/tester/own/own_fresh_call_result.ts @@ -0,0 +1,72 @@ +// A call of a function this module defines hands back a block of the caller's own (spec 34): its +// result points into nothing it was given but what its facts name. So the vector `getPoint(x, +// scene.cam)` makes - a closure's call, a function's, one inside a record given to a call - is not +// a use of the borrow `scene.cam`, and `toDrawing`, which may destroy anything (it builds and calls a +// closure), may come between the call and the vector's last use. The ray tracer's `render` (G8). +class V { + constructor(public x: number) {} +} + +class Cam { + constructor(public f: V, public r: V) {} +} + +interface Scene { + cam: Cam; +} + +interface Ray { + start: V; + dir: V; +} + +function toDrawing(c: V) { + let legalize = (d: number) => d > 100 ? 100 : d; + return { r: legalize(c.x) }; +} + +function churn() { + const junk: V[] = []; + for (let i = 0; i < 50; i++) { + junk.push(new V(999)); + } + + return junk.length; +} + +function direct(cam: Cam, k: number) { + return new V(cam.f.x + k); +} + +class Tracer { + private trace(ray: Ray) { + return new V(ray.start.x + ray.dir.x); + } + + render(scene: Scene, n: number) { + let sum: number = 0; + const getPoint = (x: number, cam: Cam) => { + const recenter = (x: number) => x - n / 2; + return new V(cam.f.x + cam.r.x * recenter(x)); + }; + + for (let x = 0; x < n; x++) { + const p = getPoint(x, scene.cam); + const q = direct(scene.cam, x); + const t = this.trace({ start: scene.cam.f, dir: getPoint(x, scene.cam) }); + const c = toDrawing(p); + churn(); + sum += c.r + p.x + q.x + t.x; + } + + return sum; + } +} + +function main() { + const tracer = new Tracer(); + const sum = tracer.render({ cam: new Cam(new V(3), new V(2)) }, 8); + print("sum:", sum); + assert(sum == 124); + print("done."); +} diff --git a/tslang/test/tester/own/own_global_loop_store.ts b/tslang/test/tester/own/own_global_loop_store.ts new file mode 100644 index 000000000..a07cd3864 --- /dev/null +++ b/tslang/test/tester/own/own_global_loop_store.ts @@ -0,0 +1,53 @@ +// A global's one store that runs more than once - in a loop of a static block, or of top-level +// script code - leaves the global mortal under -mm=own (spec 25): each store releases the value it +// replaces, as for any other global. Taken as immortal, every value it was ever given would leak. +// A store that runs once, in an `if` of a static block, still makes its global immortal. +class V { + a: number[] = []; + constructor(public x: number) { + for (let i = 0; i < 8; i++) { + this.a.push(i); + } + } +} + +@noinline +function on() { + return true; +} + +class K { + static w: V; + static { + for (let i = 0; i < 10000; i++) { + K.w = new V(i + 1); + } + } +} + +class S { + static w: V; +} + +let i = 0; +while (i < 10000) { + S.w = new V(i + 2); + i++; +} + +class T { + static w: V; + static { + if (on()) { + T.w = new V(3); + } + } +} + +function t() { return T.w; } // immortal: returned + +function main() { + print(K.w.x, K.w.a.length, S.w.x, t().x); // 10000 8 10001 3 + assert(K.w.x == 10000 && S.w.x == 10001 && t().x == 3); + print("done."); +} diff --git a/tslang/test/tester/own/own_global_mortal_export.ts b/tslang/test/tester/own/own_global_mortal_export.ts new file mode 100644 index 000000000..679405ba3 --- /dev/null +++ b/tslang/test/tester/own/own_global_mortal_export.ts @@ -0,0 +1,19 @@ +// An exported global is mortal under -mm=own (spec 25), even when this module never writes it: +// another module may, and its store would free the block a read here was taken to own nothing of. +// `M` (a module-level `export let`) and `MK.m` (a static of an exported class) stay mortal; `I`, the +// same `let` not exported, is immortal (test-own-global-mortal-export reads the IR). +class V { + constructor(public x: number) {} +} + +export let M = new V(2); + +export class MK { + static m = new V(3); +} + +let I = new V(1); + +function main() { + print(I.x, M.x, MK.m.x); // 1 2 3 +} diff --git a/tslang/test/tester/own/own_global_mortal_static_method.ts b/tslang/test/tester/own/own_global_mortal_static_method.ts new file mode 100644 index 000000000..5d3498755 --- /dev/null +++ b/tslang/test/tester/own/own_global_mortal_static_method.ts @@ -0,0 +1,26 @@ +// A static method that stores into its class's static (`this.w = x`) is no static constructor: it +// may run any number of times, each store freeing the value before it, so `M.w` stays mortal under +// -mm=own (spec 25) though it is its only store. `I.w`, stored once in its static block, is immortal +// (test-own-global-mortal-static-method reads the IR). +class V { + constructor(public x: number) {} +} + +class M { + static w: V; + static set() { + this.w = new V(3); + } +} + +class I { + static w: V; + static { + I.w = new V(1); + } +} + +function main() { + M.set(); + print(I.w.x, M.w.x); // 1 3 +} diff --git a/tslang/test/tester/own/own_global_mortal_static_twice.ts b/tslang/test/tester/own/own_global_mortal_static_twice.ts new file mode 100644 index 000000000..db145ed83 --- /dev/null +++ b/tslang/test/tester/own/own_global_mortal_static_twice.ts @@ -0,0 +1,25 @@ +// A static block that stores into a static twice leaves it mortal under -mm=own (spec 25): a read of +// the first value, taken for the immortal block, would outlive the second store, which frees it. +// `I.w`, stored once, is immortal (test-own-global-mortal-static-twice reads the IR). +class V { + constructor(public x: number) {} +} + +class M { + static w: V; + static { + M.w = new V(2); + M.w = new V(3); + } +} + +class I { + static w: V; + static { + I.w = new V(1); + } +} + +function main() { + print(I.w.x, M.w.x); // 1 3 +} diff --git a/tslang/test/tester/own/own_global_mortal_typeof.ts b/tslang/test/tester/own/own_global_mortal_typeof.ts new file mode 100644 index 000000000..0193f5b63 --- /dev/null +++ b/tslang/test/tester/own/own_global_mortal_typeof.ts @@ -0,0 +1,23 @@ +// A static written through its class's vtable - `k.w = x` with `k: typeof M`, a `ts.VirtualSymbolRef` +// stored through - may be any class's static the slot holds, so no static is immortal under -mm=own +// (spec 25): `M.w` stays mortal, though its static block stores it once. `I`, a module-level global +// in no vtable, is immortal (test-own-global-mortal-typeof reads the IR). +class V { + constructor(public x: number) {} +} + +class M { + static w = new V(2); +} + +const I = new V(1); + +class D { + set(k: typeof M) { + k.w = I; + } +} + +function main() { + print(I.x, M.w.x); // 1 2 +} diff --git a/tslang/test/tester/own/own_immortal_global.ts b/tslang/test/tester/own/own_immortal_global.ts new file mode 100644 index 000000000..37de9769a --- /dev/null +++ b/tslang/test/tester/own/own_immortal_global.ts @@ -0,0 +1,34 @@ +// A global never given another value after its initializer is immortal under -mm=own: like a +// string literal, a release skips it, so a read of it may be returned, stored into a field, given +// to another global, or merged with a fresh value - the caller "owns" it either way, and its release +// does nothing. The fresh values `g` makes are still freed. +class V { + constructor(public x: number) {} + static plus(a: V, b: V) { return new V(a.x + b.x); } +} + +class K { + static w = new V(1); + static d = K.w; // an immortal global from another + static f() { return K.w; } // returned +} + +class H { v: V; constructor() { this.v = K.w; } } // stored into a field + +function g(n: number) { return n > 1 ? K.w : new V(n); } // immortal or fresh + +class Box { v: V = new V(0); } +function lend(b: Box, v: V) { b.v = new V(v.x + 1); return v.x; } // may drop: frees b's old v + +function main() { + const h = new H(); + print(K.f().x, K.d.x, h.v.x, g(2).x, g(0).x); // 1 1 1 1 0 + const b = new Box(); + print(lend(b, K.w), b.v.x); // lent to a call that drops: 1 2 + assert(K.w.x == 1 && b.v.x == 2); + let s: number = 0; + for (let i = 0; i < 100000; i++) { s += g(i % 3).x; } // fresh ones are freed + print(s); // 66666 + assert(s == 66666); + print("done."); +} diff --git a/tslang/test/tester/own/own_immortal_global_region.ts b/tslang/test/tester/own/own_immortal_global_region.ts new file mode 100644 index 000000000..8f94b885a --- /dev/null +++ b/tslang/test/tester/own/own_immortal_global_region.ts @@ -0,0 +1,22 @@ +// A module-level global never given another value is immortal under -mm=own, as a class static +// is (own_immortal_global): its initializer is its own region, which the lowering makes a +// constructor of, and that constructor's store marks the block. `e` is given `c`'s value. +class V { + constructor(public x: number) {} +} + +const c = new V(1); +let e = c; + +function f() { return c; } // returned +function h() { return e; } + +function g(n: number) { return n > 1 ? c : new V(n); } // immortal or fresh + +function main() { + let s: number = 0; + for (let i = 0; i < 100000; i++) { s += f().x + h().x + g(i % 3).x; } + print(s, c.x, e.x); // 266666 1 1 + assert(s == 266666 && c.x == 1 && e.x == 1); + print("done."); +} diff --git a/tslang/test/tester/own/own_interface_exe_exports.ts b/tslang/test/tester/own/own_interface_exe_exports.ts new file mode 100644 index 000000000..f84d8d6a5 --- /dev/null +++ b/tslang/test/tester/own/own_interface_exe_exports.ts @@ -0,0 +1,37 @@ +// -mm=own: an executable's exports are seen by no other module (USER decision, spec 31), so an +// exported interface, and an exported global that holds a value of it, leave the interface closed +// when the module is built into a program (`--emit=exe`, or the JIT's entry module): `Hold.g.get(a)` +// gets `G.get`'s facts and its result borrows `a`. Built as an object file or a library the same +// module is open and `G.get` is the error it was (own-exe-exports.cmake). +class V { + constructor(public x: number) {} +} + +export interface Getter { get(v: V): V; } +class G implements Getter { get(v: V): V { return v; } } + +module Hold { + export let g: Getter = new G(); +} + +function churn() { + const keep: V[] = []; + for (let i = 0; i < 2000; i++) { + keep.push(new V(77777)); + } + + return keep.length; +} + +function main() { + const a = new V(5); + { + const b = Hold.g.get(a); + assert(b.x == 5, "the borrowed argument"); + } + + churn(); + assert(a.x == 5, "the argument is still there"); + print(a.x); + print("done."); +} diff --git a/tslang/test/tester/own/own_interface_facts.ts b/tslang/test/tester/own/own_interface_facts.ts new file mode 100644 index 000000000..032330d8c --- /dev/null +++ b/tslang/test/tester/own/own_interface_facts.ts @@ -0,0 +1,137 @@ +// -mm=own: a call through an interface that only this module can see (closed: not exported, every +// implementation defined here) gets the facts its implementations agree on, as a call of a private +// class family does (spec 31). The call destroys nothing when no implementation does, so a borrow +// of the argument's field or of an element of the container the receiver came from lives across +// it; a method that keeps its argument is given it (the caller moves it in), and a result that +// borrows an argument or `this` is the caller's to read, not to release. A direct call of such a +// method keeps its facts too, and so does a `public` method of a class no other module can name. +// churn() keeps what it allocates alive, so a block freed too early is reused before it is read. +class V { + constructor(public x: number) {} + static plus(a: V, b: V) { return new V(a.x + b.x); } +} + +function churn() { + const keep: V[] = []; + for (let i = 0; i < 2000; i++) { + keep.push(new V(77777)); + } + return keep.length; +} + +// `public` members of classes no other module can name +interface Pick { pick(v: V): V; } +class PA implements Pick { public pick(v: V): V { return v; } } +class PB implements Pick { public pick(v: V): V { return v; } } +function picked(a: V) { + const ps: Pick[] = [new PA(), new PB()]; + let s: number = 0; + for (const p of ps) { const b = p.pick(a); s += b.x; } + return s; +} + +// the plan's shape: the loop reads an element of `sc.ts` and calls through it with `r` +interface Th { area(v: V): number; } +class A implements Th { area(v: V) { return v.x; } } +class B implements Th { area(v: V) { return v.x * 2; } } +class Sc { ts: Th[] = [new A(), new B()]; } +function tot(sc: Sc, r: V) { + let best: number = 0; + for (let i in sc.ts) { let a = sc.ts[i].area(r); if (a > best) best = a; } + return best + r.x; +} + +// a borrow of a field held across a call through an interface whose members destroy nothing +class H { c: V = new V(5); } +interface Op { run(h: H): number; } +class OA implements Op { run(h: H): number { return h.c.x; } } +class OB implements Op { run(h: H): number { return V.plus(h.c, h.c).x; } } +function across(o: Op, h: H) { + const c = h.c; + const n = o.run(h); + churn(); + return c.x + n; +} + +// an object literal implementing the interface is a member like a class +function literalMember(h: H) { + const ops: Op[] = [new OA(), { run(h: H): number { return h.c.x * 3; } }]; + let s: number = 0; + for (const o of ops) s += across(o, h); + return s; +} + +// a method that keeps its argument: a literal, a const, a fresh value +interface Ray { start: V; dir: V; } +interface Sink { take(r: Ray): void; } +class KS implements Sink { rays: Ray[] = []; take(r: Ray) { this.rays.push(r); } } +interface SinkV { take(v: V): void; } +class KV implements SinkV { vs: V[] = []; take(v: V) { this.vs.push(v); } } + +// a result that borrows its argument, an element of `this`, a field of `this` +interface Getter { get(v: V): V; } +class G implements Getter { get(v: V): V { return v; } } +interface First { first(): V; } +class F implements First { items: V[] = [new V(3)]; first(): V { return this.items[0]; } } +interface Holder { hold(): V; } +class HH implements Holder { v: V = new V(4); hold(): V { return this.v; } } +function use(h: Holder) { const x = h.hold(); return x.x; } + +function main() { + print(tot(new Sc(), new V(3))); + assert(tot(new Sc(), new V(3)) == 9, "the interface call's facts"); + + const h = new H(); + assert(across(new OA(), h) == 10, "a field borrowed across the call"); + assert(across(new OB(), h) == 15, "a field borrowed across the other member"); + assert(literalMember(h) == 30, "a literal member"); + + const ks = new KS(); + const s: Sink = ks; + s.take({ start: new V(1), dir: new V(2) }); + const r: Ray = { start: new V(3), dir: new V(4) }; + s.take(r); + const kv = new KV(); + const sv: SinkV = kv; + sv.take(new V(5)); + const direct = new KV(); + direct.take(new V(6)); + churn(); + assert(ks.rays[0].start.x == 1 && ks.rays[0].dir.x == 2, "a kept literal"); + assert(ks.rays[1].start.x == 3 && ks.rays[1].dir.x == 4, "a kept const"); + assert(kv.vs[0].x == 5, "a kept value"); + assert(direct.vs[0].x == 6, "a kept value, called directly"); + + const g: Getter = new G(); + const a = new V(5); + { + const b = g.get(a); + assert(b.x == 5, "a borrowed argument"); + } + const gd = new G(); + { + const b = gd.get(a); + assert(b.x == 5, "a borrowed argument, called directly"); + } + + const f = new F(); + const i: First = f; + { + const x = i.first(); + assert(x.x == 3, "a borrowed element"); + const y = f.first(); + assert(y.x == 3, "a borrowed element, called directly"); + } + + const hh = new HH(); + assert(use(hh) == 4, "a borrowed field"); + assert(picked(a) == 10, "a borrowed argument through public members"); + assert(hh.hold().x == 4, "a borrowed field, called directly"); + + churn(); + const c = new V(9); + assert(a.x == 5 && f.items[0].x == 3 && hh.v.x == 4 && c.x == 9, "what the borrows read is still there"); + print(a.x, f.items[0].x, hh.v.x, c.x); + + print("done."); +} diff --git a/tslang/test/tester/own/own_interface_fn_field.ts b/tslang/test/tester/own/own_interface_fn_field.ts new file mode 100644 index 000000000..dcda172ef --- /dev/null +++ b/tslang/test/tester/own/own_interface_fn_field.ts @@ -0,0 +1,55 @@ +// -mm=own: a function-typed field of an interface closed in its module (`f: (x: number) => number`), +// called through the interface: every function ever stored into the fields its implementing object +// literals give it is known (spec 31), so the call gets their facts - here, that it destroys +// nothing - and the borrow of the object it is called on lives across it. Through a global +// `Shared` of a literal (the ray tracer's surfaces), a local one, and a parameter with no handle, +// each after churn() reuses freed memory: a literal freed while it is borrowed reads reused memory. +interface S { f: (x: number) => number; roughness: number; } +let g = new Shared({ f: function (x: number) { return x * 2; }, roughness: 2 }); + +class Junk { + a = -1; + b = -2; +} + +function churn() { + let keep: Junk[] = []; + for (let i = 0; i < 1000; i++) { + const j = new Junk(); + j.a = -i; + keep.push(j); + } + + return keep.length; +} + +class P { + s: S = { f: function (x: number) { return x + this.roughness; }, roughness: 3 }; +} + +function viaParam(p: P) { + return p.s.f(3); +} + +function viaHandle(h: Shared) { + return h.value.f(4) + h.value.roughness; +} + +function main() { + let l = new Shared({ f: function (x: number) { return x + 1; }, roughness: 3 }); + const p = new P(); + + assert(churn() == 1000); + assert(g.value.f(3) == 6, "through the global's handle"); + assert(churn() == 1000); + assert(l.value.f(3) == 4, "through the local's handle"); + assert(churn() == 1000); + assert(viaParam(p) == 6, "through a parameter"); + assert(churn() == 1000); + assert(viaHandle(g) == 10 && viaHandle(l) == 8, "through a handle given as a parameter"); + assert(churn() == 1000); + assert(g.value.roughness == 2 && l.value.roughness == 3 && p.s.roughness == 3, "the literals are alive"); + + print(g.value.f(3), l.value.f(3), viaParam(p), viaHandle(g), viaHandle(l)); // 6 4 6 10 8 + print("done."); +} diff --git a/tslang/test/tester/own/own_lends.ts b/tslang/test/tester/own/own_lends.ts index b6cfd836e..9434662bf 100644 --- a/tslang/test/tester/own/own_lends.ts +++ b/tslang/test/tester/own/own_lends.ts @@ -36,6 +36,18 @@ export function swapFirst(n: Node, l: List): Node { return l.first; } +// a borrowing record of two arguments (spec 32): the caller owns the box, which may not outlive +// either, and whose release leaves both to their owners +export interface Pair { + a: Node; + b: Node; +} + +@lends("a", "b") +export function pairOf(a: Node, b: Node): Pair { + return { a: a, b: b }; +} + class Vec { x: number; @@ -63,5 +75,15 @@ function main() { churn(); assert(kept.v == 1 && l.second.v == 3 && l.first.v == 1); + const n1 = new Node(5); + const n2 = new Node(6); + for (let i = 0; i < 100; i++) { + const pr = pairOf(n1, n2); + churn(); + assert(pr.a.v == 5 && pr.b.v == 6); + } + + assert(n1.v == 5 && n2.v == 6); + print("done."); } diff --git a/tslang/test/tester/own/own_literal_moves.ts b/tslang/test/tester/own/own_literal_moves.ts new file mode 100644 index 000000000..03f2ba266 --- /dev/null +++ b/tslang/test/tester/own/own_literal_moves.ts @@ -0,0 +1,30 @@ +// -mm=own: an object literal that owns its fields - fresh values, or locals moved in - is a value +// like a fresh object. It moves into a call argument (the caller releases it after a callee that +// borrows it; one that keeps it takes it), an array literal's element, a nested literal's field, a +// local through an interface view, or the result (spec 27). +class V { + constructor(public x: number) {} + static plus(a: V, b: V) { return new V(a.x + b.x); } +} +interface Ray { start: V; dir: V; } +interface L { pos: V; } +interface Sc { ls: V[]; c: V; } +class Keeper { rays: Ray[] = []; } +function len(r: Ray) { return r.start.x + r.dir.x; } +function keepRay(k: Keeper, r: Ray) { k.rays.push(r); } +function mk(): Sc { return { ls: [new V(1), new V(2)], c: new V(3) }; } +function main() { + const p = new V(1); + const d = new V(2); + print(len({ start: p, dir: d })); // 3 (p, d move in) + const q = new V(1); + print(len({ start: V.plus(q, q), dir: q })); // 3 (fresh + moved) + const ls: L[] = [{ pos: new V(1) }, { pos: new V(2) }]; + const r: Ray = { start: new V(4), dir: new V(5) }; + const s = mk(); + print(ls[1].pos.x, r.dir.x, s.c.x, s.ls[1].x); // 2 5 3 2 + const k = new Keeper(); + for (let i = 0; i < 1000; i++) keepRay(k, { start: new V(i), dir: new V(1) }); + print(k.rays.length, k.rays[999].start.x); // 1000 999 + print("done."); +} diff --git a/tslang/test/tester/own/own_override_base_lib.ts b/tslang/test/tester/own/own_override_base_lib.ts new file mode 100644 index 000000000..b0b2ad1d5 --- /dev/null +++ b/tslang/test/tester/own/own_override_base_lib.ts @@ -0,0 +1,7 @@ +// The library own_err_override_imported_base and own_err_override_imported_class import: an abstract +// and a concrete base class whose `keep` it calls itself. +export class V { constructor(public x: number) {} } +export abstract class Base { abstract keep(v: V): void; } +export class Concrete { keep(v: V): void { } } +export function run(b: Base) { const a = new V(5); b.keep(a); } +export function runConcrete(b: Concrete) { const a = new V(5); b.keep(a); } diff --git a/tslang/test/tester/own/own_override_nameable_main.ts b/tslang/test/tester/own/own_override_nameable_main.ts new file mode 100644 index 000000000..8095b5acc --- /dev/null +++ b/tslang/test/tester/own/own_override_nameable_main.ts @@ -0,0 +1,7 @@ +// The importer of own_err_override_nameable_base: it calls `keep` through `Base`'s vtable, which the +// library's `Der.keep` fills, and reads the kept value after a churn. Correct under every model; under +// own the library is the error (spec 31). +import './own_err_override_nameable_base' +function churn() { const keep: V[] = []; for (let i = 0; i < 2000; i++) keep.push(new V(77777)); return keep.length; } +function put(b: Base) { const a = new V(5); b.keep(a); } +function main() { put(mk()); churn(); assert(first() == 5, "kept value still there"); print("done."); } diff --git a/tslang/test/tester/own/own_raytrace.ts b/tslang/test/tester/own/own_raytrace.ts new file mode 100644 index 000000000..200899484 --- /dev/null +++ b/tslang/test/tester/own/own_raytrace.ts @@ -0,0 +1,318 @@ +// @strict-null false +// The ray tracer under -mm=own (spec 34): benchmarks/raytrace/rtbench_own.ts - rtbench.ts with its +// two source edits for own (the surfaces are `Shared` handles; `Plane.normal` returns a +// fresh vector instead of its own field) - rendered at 64x64. Every model prints the same checksum. +declare function sqrt(v: number): number; +declare function floor(v: number): number; +declare function pow(v: number, s: number): number; +declare function fabs(v: number): number; + +class Math { + static sqrt(v: number): number { + return sqrt(v); + } + + static floor(v: number): number { + return floor(v); + } + + static pow(v: number, s: number): number { + return pow(v, s); + } + + static abs(v: number): number { + return fabs(v); + } + + static sign(v: number): number { + if (v > 0) return 1; + if (v < 0) return -1; + return 0; + } +} + +class Vector { + constructor(public x: number, + public y: number, + public z: number) { + } + static times(k: number, v: Vector) { return new Vector(k * v.x, k * v.y, k * v.z); } + static minus(v1: Vector, v2: Vector) { return new Vector(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z); } + static plus(v1: Vector, v2: Vector) { return new Vector(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z); } + static dot(v1: Vector, v2: Vector) { return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z; } + static mag(v: Vector) { return Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z); } + static norm(v: Vector) { + let mag = Vector.mag(v); + let div = (mag === 0) ? Infinity : 1.0 / mag; + return Vector.times(div, v); + } + static cross(v1: Vector, v2: Vector) { + return new Vector(v1.y * v2.z - v1.z * v2.y, + v1.z * v2.x - v1.x * v2.z, + v1.x * v2.y - v1.y * v2.x); + } +} + +class Color { + constructor(public r: number, + public g: number, + public b: number) { + } + static scale(k: number, v: Color) { return new Color(k * v.r, k * v.g, k * v.b); } + static plus(v1: Color, v2: Color) { return new Color(v1.r + v2.r, v1.g + v2.g, v1.b + v2.b); } + static times(v1: Color, v2: Color) { return new Color(v1.r * v2.r, v1.g * v2.g, v1.b * v2.b); } + static white = new Color(1.0, 1.0, 1.0); + static grey = new Color(0.5, 0.5, 0.5); + static black = new Color(0.0, 0.0, 0.0); + static background = Color.black; + static defaultColor = Color.black; + static toDrawingColor(c: Color) { + let legalize = (d: number) => d > 1 ? 1 : d; + return { + r: Math.floor(legalize(c.r) * 255), + g: Math.floor(legalize(c.g) * 255), + b: Math.floor(legalize(c.b) * 255) + } + } +} + +class Camera { + public forward: Vector; + public right: Vector; + public up: Vector; + + constructor(public pos: Vector, lookAt: Vector) { + let down = new Vector(0.0, -1.0, 0.0); + this.forward = Vector.norm(Vector.minus(lookAt, this.pos)); + this.right = Vector.times(1.5, Vector.norm(Vector.cross(this.forward, down))); + this.up = Vector.times(1.5, Vector.norm(Vector.cross(this.forward, this.right))); + } +} + +interface Ray { + start: Vector; + dir: Vector; +} + +interface Intersection { + thing: Thing; + ray: Ray; + dist: number; +} + +interface Surface { + diffuse: (pos: Vector) => Color; + specular: (pos: Vector) => Color; + reflect: (pos: Vector) => number; + roughness: number; +} + +interface Thing { + intersect(ray: Ray): Intersection; + normal(pos: Vector): Vector; + surface: Shared; +} + +interface Light { + pos: Vector; + color: Color; +} + +interface Scene { + things: Thing[]; + lights: Light[]; + camera: Camera; +} + +class Sphere implements Thing { + public radius2: number; + + constructor(public center: Vector, radius: number, public surface: Shared) { + this.radius2 = radius * radius; + } + normal(pos: Vector): Vector { return Vector.norm(Vector.minus(pos, this.center)); } + intersect(ray: Ray): Intersection { + let eo = Vector.minus(this.center, ray.start); + let v = Vector.dot(eo, ray.dir); + let dist = 0.0; + if (v >= 0) { + let disc = this.radius2 - (Vector.dot(eo, eo) - v * v); + if (disc >= 0) { + dist = v - Math.sqrt(disc); + } + } + if (dist === 0) { + return null; + } else { + return { thing: this, ray: ray, dist: dist }; + } + } +} + +class Plane implements Thing { + constructor(public norm: Vector, public offset: number, public surface: Shared) { + } + public normal(pos: Vector): Vector { return new Vector(this.norm.x, this.norm.y, this.norm.z); } + public intersect(ray: Ray): Intersection { + let denom = Vector.dot(this.norm, ray.dir); + if (denom > 0) { + return null; + } else { + let dist = (Vector.dot(this.norm, ray.start) + this.offset) / (-denom); + return { thing: this, ray: ray, dist: dist }; + } + } +} + +module Surfaces { + export let shiny = new Shared({ + diffuse: function (pos: Vector) { return Color.white; }, + specular: function (pos: Vector) { return Color.grey; }, + reflect: function (pos: Vector) { return 0.7; }, + roughness: 250.0 + }); + export let checkerboard = new Shared({ + diffuse: function (pos: Vector) { + if ((Math.floor(pos.z) + Math.floor(pos.x)) % 2 !== 0) { + return Color.white; + } else { + return Color.black; + } + }, + specular: function (pos: Vector) { return Color.white; }, + reflect: function (pos: Vector) { + if ((Math.floor(pos.z) + Math.floor(pos.x)) % 2 !== 0) { + return 0.1; + } else { + return 0.7; + } + }, + roughness: 150.0 + }); +} + + +class RayTracer { + private maxDepth = 5; + + private intersections(ray: Ray, scene: Scene) { + let closest = +Infinity; + let closestInter: Intersection = undefined; + for (let i in scene.things) { + let inter = scene.things[i].intersect(ray); + if (inter != null && inter.dist < closest) { + closestInter = inter; + closest = inter.dist; + } + } + + return closestInter; + } + + private testRay(ray: Ray, scene: Scene) { + let isect = this.intersections(ray, scene); + if (isect != null) { + return isect.dist; + } else { + return undefined; + } + } + + private traceRay(ray: Ray, scene: Scene, depth: number): Color { + let isect = this.intersections(ray, scene); + if (isect === undefined) { + return Color.background; + } else { + return this.shade(isect, scene, depth); + } + } + + private shade(isect: Intersection, scene: Scene, depth: number) { + let d = isect.ray.dir; + let pos = Vector.plus(Vector.times(isect.dist, d), isect.ray.start); + let normal = isect.thing.normal(pos); + let reflectDir = Vector.minus(d, Vector.times(2, Vector.times(Vector.dot(normal, d), normal))); + let naturalColor = Color.plus(Color.background, + this.getNaturalColor(isect.thing, pos, normal, reflectDir, scene)); + let reflectedColor = (depth >= this.maxDepth) ? Color.grey : this.getReflectionColor(isect.thing, pos, normal, reflectDir, scene, depth); + return Color.plus(naturalColor, reflectedColor); + } + + private getReflectionColor(thing: Thing, pos: Vector, normal: Vector, rd: Vector, scene: Scene, depth: number) { + return Color.scale(thing.surface.value.reflect(pos), this.traceRay({ start: pos, dir: rd }, scene, depth + 1)); + } + + private getNaturalColor(thing: Thing, pos: Vector, norm: Vector, rd: Vector, scene: Scene) { + const addLight = (col: Color, light: Light) => { + let ldis = Vector.minus(light.pos, pos); + let livec = Vector.norm(ldis); + let neatIsect = this.testRay({ start: pos, dir: livec }, scene); + let isInShadow = (neatIsect === undefined) ? false : (neatIsect <= Vector.mag(ldis)); + if (isInShadow) { + return col; + } else { + let illum = Vector.dot(livec, norm); + let lcolor = (illum > 0) ? Color.scale(illum, light.color) + : Color.defaultColor; + let specular = Vector.dot(livec, Vector.norm(rd)); + let scolor = (specular > 0) ? Color.scale(Math.pow(specular, thing.surface.value.roughness), light.color) + : Color.defaultColor; + return Color.plus(col, Color.plus(Color.times(thing.surface.value.diffuse(pos), lcolor), + Color.times(thing.surface.value.specular(pos), scolor))); + } + } + + return scene.lights.reduce(addLight, Color.defaultColor); + } + + render(scene: Scene, screenWidth: number, screenHeight: number) { + let sum: number = 0; + const getPoint = (x: number, y: number, camera: Camera) => { + const recenterX = (x: number) => (x - (screenWidth / 2.0)) / 2.0 / screenWidth; + const recenterY = (y: number) => - (y - (screenHeight / 2.0)) / 2.0 / screenHeight; + return Vector.norm(Vector.plus(camera.forward, Vector.plus(Vector.times(recenterX(x), camera.right), Vector.times(recenterY(y), camera.up)))); + } + + for (let y = 0; y < screenHeight; y++) { + for (let x = 0; x < screenWidth; x++) { + let color = this.traceRay({ start: scene.camera.pos, dir: getPoint(x, y, scene.camera) }, scene, 0); + let c = Color.toDrawingColor(color); + sum += c.r + c.g + c.b; + //ctx.fillStyle = "rgb(" + String(c.r) + ", " + String(c.g) + ", " + String(c.b) + ")"; + //ctx.fillRect(x, y, x + 1, y + 1); + } + } + return sum; + } +} + + +function defaultScene(): Scene { + return { + things: [new Plane(new Vector(0.0, 1.0, 0.0), 0.0, Surfaces.checkerboard), + new Sphere(new Vector(0.0, 1.0, -0.25), 1.0, Surfaces.shiny), + new Sphere(new Vector(-6, 3.0, -5.0), 1.0, Surfaces.shiny), + // new Sphere(new Vector(-6, 8.0, 15.0), 5.0, Surfaces.shiny), + new Sphere(new Vector(-1.0, 0.5, 1.5), 0.5, Surfaces.shiny)], + + lights: [{ pos: new Vector(-2.0, 2.5, 0.0), color: new Color(0.49, 0.07, 0.07) }, + { pos: new Vector(1.5, 2.5, 1.5), color: new Color(0.07, 0.07, 0.49) }, + { pos: new Vector(1.5, 2.5, -1.5), color: new Color(0.07, 0.49, 0.071) }, + { pos: new Vector(0.0, 3.5, 0.0), color: new Color(0.21, 0.21, 0.35) }], + camera: new Camera(new Vector(3.0, 2.0, 4.0), new Vector(-1.0, 0.5, 0.0)) + }; +} + +function exec() { + let rayTracer = new RayTracer(); + let sum = rayTracer.render(defaultScene(), 64, 64); + print("checksum:", sum); + assert(sum == 606318); +} + + +function main() { + print("start..."); + exec(); + print("done."); +} diff --git a/tslang/test/tester/own/own_read_after_move.ts b/tslang/test/tester/own/own_read_after_move.ts new file mode 100644 index 000000000..251b99ba4 --- /dev/null +++ b/tslang/test/tester/own/own_read_after_move.ts @@ -0,0 +1,101 @@ +// @strict-null false +// -mm=own: a value moved into a new owner - a field, an element, an owning local - may still be read +// after the move while that owner holds it unchanged: the read borrows it through the owner (spec +// 29). A path from the move to the read that overwrites, releases or moves the owner on stays an +// error. The values come from factories, so that stack promotion leaves them heap blocks. +class V { + constructor(public x: number) {} + static plus(a: V, b: V) { return new V(a.x + b.x); } +} +class N { v: number[] = []; } +class C { x: number = 5; } +class H { c: C | null = null; } +// another class's field of the same type at the same position is matched as the same field +// (dropsChain), so the loop's holder and K keep their fields elsewhere +class HW { k: number = 0; c: C | null = null; } +class K { + k: number = 0; + k2: number = 0; + f: C | null = null; + put(n: number) { + const c = mkc(n); + this.f = c; // moved into a field of `this` + churn(); + return c.x; + } +} +function mka(n: number) { + const r: number[] = []; + r.push(n); + return r; +} +function mkc(n: number) { + const r = new C(); + r.x = n; + return r; +} +function mkv(n: number) { + return new V(n); +} +function churn() { + let s: number = 0; + for (let i = 0; i < 64; i++) { + const junk = mkv(999); + const c = mkc(999); + const n = new N(); + n.v.push(99); + s += junk.x + c.x + n.v.length; + } + + return s; +} +function main() { + const a = mka(1); + const n = new N(); + n.v = a; + print(a.length); // 1 + const h = new H(); + const c = mkc(5); + h.c = c; + print(c.x); // 5 + const arr: C[] = [mkc(0)]; + const ce = mkc(8); + arr[0] = ce; // moved into an element + churn(); + print(ce.x); // 8 + const k = new K(); + const kx = k.put(6); + print(kx, k.f.x); // 6 6 + const vs = [mkv(1), mkv(3), mkv(2)]; + let best: V = undefined; + let s: number = 0; + for (let i in vs) { + let v = V.plus(vs[i], vs[i]); + best = v; + s += v.x; // read after the move into best + } + print(best.x, s); // 4 12 + let t: number = 0; + for (let j = 0; j < 100000; j++) { + const w = mkv(j); + const hw = new HW(); + const cw = mkc(5); + hw.c = cw; + best = w; + t += w.x - j + cw.x; + } + + // a declaration takes the value over; `p` is read through `q`, then given a new value of its own + let p = mkv(7); + let q = p; + print(p.x); // 7 + p = mkv(8); + churn(); + print(q.x, p.x); // 7 8 + assert(q.x == 7 && p.x == 8); + print(a.length, a[0], c.x, ce.x, best.x, s, t); // 1 1 5 8 99999 12 500000 + assert(a.length == 1 && a[0] == 1 && c.x == 5 && ce.x == 8); + assert(best.x == 99999 && s == 12 && t == 500000); + assert(n.v.length == 1 && h.c.x == 5 && arr[0].x == 8 && kx == 6 && k.f.x == 6); + print("done."); +} diff --git a/tslang/test/tester/own/own_record_literal_given.ts b/tslang/test/tester/own/own_record_literal_given.ts new file mode 100644 index 000000000..cbdf5fc60 --- /dev/null +++ b/tslang/test/tester/own/own_record_literal_given.ts @@ -0,0 +1,16 @@ +// An object literal that holds a new value, given as an interface, is boxed into a block of its +// own: the new value moves into the record, the record into the box, and the box is returned +// (spec 27; -mm=own rejected it before). +class Vec { + constructor(public n: number) {} +} +interface Holder { + v: Vec; +} +function make(n: number): Holder { + return { v: new Vec(n) }; +} +function main() { + print(make(1).v.n); + print("done."); +} diff --git a/tslang/test/tester/own/own_reduce.ts b/tslang/test/tester/own/own_reduce.ts new file mode 100644 index 000000000..3911957c0 --- /dev/null +++ b/tslang/test/tester/own/own_reduce.ts @@ -0,0 +1,125 @@ +// `reduce` takes its accumulator (spec 33): the initial value moves into the generated +// `___array_reduce`, each callback is given the accumulator to keep, and its result is the next +// one. A function that returns a parameter on some paths and a fresh value on others keeps that +// parameter: the path that returns it moves it out, every other path releases it exactly once - +// an early return, a `break` out of a loop, a return from inside a loop or a `try` with a +// `finally`, a `throw`. A callback that only borrows the accumulator releases it, and so does a +// direct call of one (`add`), which moves its argument in. +class V { + constructor(public x: number) {} + static plus(a: V, b: V) { + return new V(a.x + b.x); + } +} + +class K { + static zero = new V(0); +} + +function churn() { + const junk: V[] = []; + for (let i = 0; i < 64; i++) { + junk.push(new V(999)); + } +} + +// returns acc on one path +function pick(acc: V, v: V) { + return v.x > 2 ? acc : V.plus(acc, v); +} + +// only borrows acc, but is passed beside `pick`: given acc to keep, it releases it +function add(acc: V, v: V) { + return V.plus(acc, v); +} + +function early(a: V, n: number) { + if (n > 5) { + return a; + } + + for (let i = 0; i < n; i++) { + if (i == 3) { + break; + } + } + + return new V(a.x + n); +} + +function fromLoop(a: V, n: number) { + for (let i = 0; i < n; i++) { + if (i == 2) { + return a; + } + } + + return new V(a.x * 10); +} + +let finallies: number = 0; + +function guarded(a: V, n: number) { + try { + if (n > 1) { + return a; + } + } finally { + finallies++; + } + + return new V(a.x + 100); +} + +function thrower(a: V, n: number) { + if (n < 0) { + throw "negative"; + } + + if (n == 0) { + return a; + } + + return new V(a.x + 1); +} + +function main() { + const a = [new V(1), new V(2), new V(3)]; + const r = a.reduce((acc: V, v: V) => V.plus(acc, v), new V(0)); + const q = a.reduce(pick, K.zero); + churn(); + print(r.x, q.x); // 6 3 + + // every element above 2: pick returns the immortal start value itself, which no release frees + const z = [new V(5), new V(7)].reduce(pick, K.zero); + churn(); + print(z.x, K.zero.x); // 0 0 + + const s = a.reduce(add, new V(10)); + churn(); + print(s.x); // 16 + + let total: number = 0; + let caught = 0; + for (let i = 0; i < 20000; i++) { + const n = i % 9; + const e = early(new V(i % 10), n); + const f = fromLoop(new V(1), n); + const g = guarded(new V(2), n); + const d = add(new V(n), new V(1)); + let t: number = 0; + try { + const h = thrower(new V(3), n - 1); + t = h.x; + } catch (err) { + caught++; + } + + churn(); + total += e.x + f.x + g.x + t + d.x; + } + + print(total, caught, finallies); + assert(r.x == 6 && q.x == 3 && z.x == 0 && K.zero.x == 0 && s.x == 16); + print("done."); +} diff --git a/tslang/test/tester/own/own_shared_literal.ts b/tslang/test/tester/own/own_shared_literal.ts new file mode 100644 index 000000000..0cbc2b297 --- /dev/null +++ b/tslang/test/tester/own/own_shared_literal.ts @@ -0,0 +1,35 @@ +// Shared of an object literal (spec 26): the literal, a boxed object behind an interface with +// function fields (the ray tracer's surfaces), moves into the handle the way `new C()` does - in a +// module-level global's initializer and in a function. The global's handle and the local's two +// handles are read after churn() reuses freed memory: a literal freed while its handle points at it +// reads reused memory. The function fields are not called through the handles here: a call through +// an interface slot has no facts yet (G9 of the ray tracer plan), and a handle's borrow ends at such +// a call (spec 23.3). +interface S { f: (x: number) => number; roughness: number; } +let g = new Shared({ f: function (x: number) { return x * 2; }, roughness: 2 }); + +class Junk { + a = -1; + b = -2; +} + +function churn() { + let keep: Junk[] = []; + for (let i = 0; i < 1000; i++) { + const j = new Junk(); + j.a = -i; + keep.push(j); + } + + return keep.length; +} + +function main() { + let l = new Shared({ f: function (x: number) { return x + 1; }, roughness: 3 }); + const m = l; + assert(churn() == 1000); + assert(g.value.roughness == 2, "the global's literal is alive"); + assert(l.value.roughness == 3 && m.value.roughness == 3, "the local's literal is alive"); + print(g.value.roughness, l.value.roughness, m.value.roughness); // 2 3 3 + print("done."); +} diff --git a/tslang/tslang/transform.cpp b/tslang/tslang/transform.cpp index cceb0ee47..58d71398d 100644 --- a/tslang/tslang/transform.cpp +++ b/tslang/tslang/transform.cpp @@ -149,7 +149,9 @@ int runMLIRPasses(mlir::MLIRContext &context, llvm::SourceMgr &sourceMgr, mlir:: // either erases a retain or reports it, and a program it reports stops here. if (compileOptions.memoryModel == MemoryModelOwn && !ownSkipInference) { - pm.addPass(mlir::typescript::createOwnershipSignaturePass()); + // a program's exports are seen by no other module (own spec 31): an executable, or the + // module the JIT enters (a module the JIT cache compiles beside it has isJit off) + pm.addPass(mlir::typescript::createOwnershipSignaturePass(compileOptions.isExecutable || compileOptions.isJit)); pm.nest().addPass(mlir::typescript::createOwnershipInferencePass()); // an `await`'s continuation and a `for await` body, outlined by the async lowering pm.nest().addPass(mlir::typescript::createOwnershipInferencePass());