diff --git a/tslang/docs/superpowers/plans/2026-10-03-own-shared.md b/tslang/docs/superpowers/plans/2026-10-03-own-shared.md new file mode 100644 index 000000000..0baaf893e --- /dev/null +++ b/tslang/docs/superpowers/plans/2026-10-03-own-shared.md @@ -0,0 +1,1266 @@ +# `Shared` 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:** A built-in `Shared`, a counted handle to a block that holds one `T`, which `-mm=own` +leaves to rc's counting while everything else stays single-owner (own phase 8). + +**Architecture:** A new dialect type `!ts.shared` and three ops (`ts.SharedNew`, +`ts.SharedValueRef`, `ts.SharedCount`). `s.value` is a `ts.Load` of the `ts.SharedValueRef` place +and `s.value = x` is the field-store pattern on it, so MLIRGen's existing ownership ops and own's +existing place-read machinery apply. Retains and releases of a handle lower to rc's +`__tslang_inc_ref`/`__tslang_dec_ref` in every model that tracks ownership. The own inference +pass leaves them alone, and a borrow read through a handle is ended by any write that could reach +it through another handle and by any call that may drop. + +**Tech Stack:** C++17, MLIR/LLVM (TableGen ODS), the tslang test-runner, CMake/ctest, PowerShell +(`measure.ps1`), WSL GCC for Linux. + +**Spec:** `tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md`, §23 (approved), +with §2, §15 and §22 as background. + +## Global Constraints + +- Branch `own-shared` (spec §23 is commit 394c8b8d, on main 8b9d6316). +- **Form (§23):** a wrapper; the count lives in the handle's block header and the block's payload + is one `T`. `Shared` is built in (it works with `--no-default-lib`). A program's own type named + `Shared` is found first and wins. +- **Members (§23.1):** `new Shared(x)`, `s.value` read, `s.value = x`, `===`/`!==` on handles, and + `Shared.count(s)` (the count under rc and own, `-1` under gc and none). +- **Under own (§23.2):** every retain and release of a handle stays and is counted. A handle is a + value of type `!ts.shared`, or a tag-free union or an optional of one with `null` or + `undefined`. The pass never erases or reports one. +- **The release of a handle (§23.2):** `__tslang_dec_ref`, and at zero, release the payload `T` with + `T`'s routine and free the block, in every model that tracks ownership (rc and own). +- **Reading through a handle (§23.3):** such a borrow ends at a write into a place reached through + any `Shared`, or at a call that is not `__own_no_drops`. A handle copied out is a counted retain, + not a borrow. The error is the existing "borrows a field but is used here after it may be + released or overwritten". +- **Cycles (§23.4):** they leak; there is no `WeakRef` in this phase. +- **Never branch on the memory model in MLIRGen** (§4.3). Model differences go in the lowering and + in the own passes. +- **Unchanged where `Shared` is not used:** the corpus (453 / 593 plain, 451 / 593 `--opt`) and its + LLVM IR for every file that compiles. +- **Line endings:** sources are CRLF. Do not edit them with Git Bash `sed -i`, which rewrites them + to LF. Use the Edit tool, or Python with `newline=''`. +- **Build:** `cmake --build __build/tslang/windows-msbuild-2026-release --config Release --target tslang --parallel 8`. + - Editing a `.td` file regenerates the dialect. If MSVC fails with C1060, rebuild with + `--parallel 3`. + - Run tests with `ctest -C Release` (without `-C`, every test is "Not Run") and `--timeout 300`. +- **Commits** are GPG-signed. Never pass `--no-gpg-sign`. If signing times out, stage everything and + hand the commit to the user with the message in a file. End commit messages with + `Co-Authored-By: Claude Opus 5.5 `. +- **Test rules:** never read a catch variable's value in a test. Printing a class instance needs its + own `toString`, so print numbers and strings only. Integer literals are `s32`. + +## Review Focus + +1. **`Shared | null` in a condition.** `while (cur)`, `if (h.s)` and `cur !== null` must narrow + to the handle, and `null` must compare. Expected: they behave as for a class. The union is one + pointer with no tag. Task 2's `own_shared_graph` walks such a list. +2. **A handle given to a function and returned from one.** The count across a call, and rc's + owned-return consumption of `return new Shared(...)`. Expected: no extra and no missing + reference. Task 2's `own_shared_count` asserts `Shared.count` after `make()` and `keepOne()`. +3. **A payload that is not a class:** `Shared` and `Shared`. Expected: the payload + is released by its own routine (a string), or by nothing (a number). Task 1's `own_shared_basic` + holds a string. +4. **A handle reassigned to the block it already holds** (`b = a` when `b` is `a`). Expected: the + count goes up before it goes down, so the block is not freed in between. Task 2's + `own_shared_count` does it and asserts the count. +5. **A handle created and dropped many times in a loop.** Expected: the count returns to where it + was and memory stays flat. Task 2's `own_shared_count` does it 1000 times, and Task 5 measures + 200,000. + +--- + +## File map + +| File | Change | Task | +| --- | --- | --- | +| `include/TypeScript/TypeScriptTypes.td` | `TypeScript_Shared` type | 1 | +| `include/TypeScript/TypeScriptOps.td` | `SharedNew`, `SharedValueRef`, `SharedCount` | 1 | +| `lib/TypeScript/MLIRGenTypes.cpp` | `Shared` as an embedded name, `Shared` | 1 | +| `lib/TypeScript/MLIRGenExpressions.cpp` | `new Shared(x)`, `Shared.count(s)` | 1, 2 | +| `lib/TypeScript/MLIRGenAccessCall.cpp` | `s.value` | 1 | +| `include/TypeScript/MLIRLogic/MLIRTypeCore.h` | `isNullableTypeNoUnion`, `isNullableOrOptionalType` | 1 | +| `include/TypeScript/MLIRLogic/MLIRTypeIterator.h` | recurse into `Shared` | 1 | +| `include/TypeScript/MLIRLogic/MLIRPrinter.h` | print `Shared` | 1 | +| `lib/TypeScript/LowerToLLVM.cpp` | converter, three lowerings, retain gates, descriptor retain | 1, 3, 4 | +| `lib/TypeScript/LowerToAffineLoops.cpp` | legal ops | 1 | +| `include/TypeScript/MLIRLogic/MLIRTypeHelper.h` | `ownsHeapMemory`, `isSharedHandleType` | 2 | +| `include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h` | counted routines for a handle | 2 | +| `lib/TypeScript/MLIRGenImpl.h` | `markFreshBlockOwned`, `isOwningSlot` for the payload | 2 | +| `lib/TypeScript/OwnershipFacts.h` | `isHandle`, `isPlace` | 3 | +| `lib/TypeScript/OwnershipInferencePass.cpp` | step aside; reads through a handle | 3 | +| `lib/TypeScript/OwnershipSignaturePass.cpp` | no facts on handles; drops | 3 | +| `include/TypeScript/MLIRLogic/TypeOfOpHelper.h` | a `typeof` name, so it gets a descriptor | 4 | +| `lib/TypeScript/MLIRGenCast.cpp` | `any` to `Shared` | 4 | +| `test/tester/own/own_shared_*.ts`, `own_err_shared_*.ts` | tests | 1-4 | +| `test/tester/CMakeLists.txt` | registration | 1-4 | +| spec §23 | amendments (Task 1), results §23.7 (Task 5) | 1, 5 | + +All paths below are relative to `tslang/`. Line numbers were read on 8b9d6316; find the code by the +names given, because lines move. + +--- + +### Task 1: the type, the ops, and MLIRGen (no counting yet) + +**Files:** +- Modify: `include/TypeScript/TypeScriptTypes.td`, `include/TypeScript/TypeScriptOps.td` +- Modify: `lib/TypeScript/MLIRGenTypes.cpp`, `lib/TypeScript/MLIRGenExpressions.cpp`, + `lib/TypeScript/MLIRGenAccessCall.cpp` +- Modify: `include/TypeScript/MLIRLogic/MLIRTypeCore.h`, `MLIRTypeIterator.h`, `MLIRPrinter.h` +- Modify: `lib/TypeScript/LowerToLLVM.cpp`, `lib/TypeScript/LowerToAffineLoops.cpp` +- Create: `test/tester/own/own_shared_basic.ts` +- Modify: `test/tester/CMakeLists.txt` +- Modify: the spec, §23.1 and §23.6 (Step 9) + +**Interfaces:** +- Produces: + - `mlir_ts::SharedType` with `getElementType()`, built by `mlir_ts::SharedType::get(elementType)`. + - `mlir_ts::SharedNewOp::create(builder, loc, SharedType, mlir::Value value)` with `getValue()`. + - `mlir_ts::SharedValueRefOp(loc, mlir_ts::RefType resultType, mlir::Value shared)` with + `getShared()` and `getResult()`. + - `mlir_ts::SharedCountOp(loc, NumberType, mlir::Value shared)`. + - The CMake variable `shared_models` and the foreach that registers `own_shared_*` tests per model. + +- [x] **Step 1: Write the test.** `test/tester/own/own_shared_basic.ts`: + +```ts +// Shared (spec 23): two handles to one block, `.value` read and write, `===` on handles, and a +// payload that is a string. Each handle dropped is followed by reusing freed memory before the +// other is read. +class Node { + v = 0; + name = ""; +} + +function churn() { + let keep: string[] = []; + for (let i = 0; i < 1000; i++) keep.push("k" + i); + return keep.length; +} + +function main() { + let a = new Shared(new Node()); + a.value.name = "first" + 1; + const b = a; + assert(a === b, "two handles to one block are equal"); + a.value.v = 5; + assert(b.value.v == 5, "a write through one handle is seen through the other"); + + const c = new Shared(new Node()); + assert(a !== c, "two blocks are not equal"); + + a = c; + assert(churn() == 1000); + assert(b.value.v == 5 && b.value.name == "first1", "the other handle keeps the block"); + + const d = b; + d.value = new Node(); + d.value.v = 7; + assert(b.value.v == 7, "the value replaced through one handle is seen through the other"); + + const t = new Shared("text" + 2); + const u = t; + assert(u.value == "text2"); + t.value = "other" + 3; + assert(churn() == 1000); + assert(u.value == "other3"); + + print("done."); +} +``` + +- [x] **Step 2: Register it under gc and none, and watch it fail.** + - In `test/tester/CMakeLists.txt`, after the `strings_mm` foreach (search + `foreach(strings_mm rc none gc)`), add: + +```cmake +# Shared (spec 23): counted handles. These are not in the own_test list, whose +# test-own-no-counting check forbids __tslang_inc_ref: a handle is counted under own by design. +set(shared_models gc none) +foreach(shared_mm ${shared_models}) + foreach(shared_test own_shared_basic) + tslang_add_test(NAME test-jit-${shared_mm}-${shared_test} COMMAND test-runner -jit -mm=${shared_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/${shared_test}.ts") + tslang_add_test(NAME test-compile-${shared_mm}-${shared_test} COMMAND test-runner -mm=${shared_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/${shared_test}.ts") + endforeach() +endforeach() +``` + + - Build, then run `ctest -C Release -R own_shared -j 8 --timeout 300`. + - Expected: 4 tests, all FAIL, with "can't resolve name" or similar for `Shared`. + +- [x] **Step 3: The type.** In `include/TypeScript/TypeScriptTypes.td`, after `def TypeScript_Ref` + (the block that ends with its `builders`), add: + +```tablegen +// Shared (own spec 23): a counted handle to a block whose payload is one T. +def TypeScript_Shared : TypeScript_Type<"Shared"> { + let mnemonic = "shared"; + + let description = [{ + Syntax: + + ``` + shared-type ::= `shared` `<` type `>` + ``` + + Examples: + + ```mlir + shared> + ``` + }]; + let parameters = (ins "Type":$elementType); + + let assemblyFormat = "`<` params `>`"; + + let builders = [ + TypeBuilderWithInferredContext<(ins "Type":$elementType), [{ + return Base::get(elementType.getContext(), elementType); + }]> + ]; +} +``` + +- [x] **Step 4: The ops.** In `include/TypeScript/TypeScriptOps.td`, after `def TypeScript_StringCopyOp`, + add: + +```tablegen +// Shared (own spec 23). `new Shared(x)`: a block of one T, which `value` moves into. +def TypeScript_SharedNewOp : TypeScript_Op<"SharedNew"> { + let arguments = (ins AnyType:$value); + let results = (outs Res:$result); +} + +// The place `s.value`: a reference to the handle's payload. It is read with ts.Load and written +// with ts.Store, as a field is. +def TypeScript_SharedValueRefOp : TypeScript_Op<"SharedValueRef"> { + let arguments = (ins TypeScript_Shared:$shared); + let results = (outs TypeScript_Ref:$result); +} + +// `Shared.count(s)`: the number of handles under rc and own, -1 under gc and none. +def TypeScript_SharedCountOp : TypeScript_Op<"SharedCount"> { + let arguments = (ins TypeScript_Shared:$shared); + let results = (outs TypeScript_Number:$result); +} +``` + + Build with `--parallel 3`. Expected: it compiles. If `TypeScript_Shared` cannot be used as an + operand constraint, wrap it the way `TypeScript_AnyClassOrValueRef` is + (`TypeScriptTypes.td`, `AnyTypeOf<[...]>`): `def TypeScript_SharedLike : AnyTypeOf<[TypeScript_Shared]>;` + +- [x] **Step 5: MLIRGen.** Each change is small. + - **The embedded name** (`lib/TypeScript/MLIRGenTypes.cpp`): + - Add `{"Shared", true}` to the static maps in `isEmbededTypeWithBuiltins` and + `isEmbededTypeWithNoBuiltins`, next to `{"BoxedObject", true}`. + - In both `getEmbeddedTypeWithParamBuiltins` and `getEmbeddedTypeWithParamNoBuiltins`, add + `Shared` to the `EmbeddedType` enum, add `.Case("Shared", EmbeddedType::Shared)` to the + `StringSwitch`, and add this case to the `switch (kind)`: + +```cpp + case EmbeddedType::Shared: + return mlir_ts::SharedType::get(type); +``` + + - **`new Shared(x)`** (`lib/TypeScript/MLIRGenExpressions.cpp`, `mlirGen(NewExpression ...)`): + in the `if (result.failed())` / `typeExpression == SyntaxKind::Identifier` fallback, before + `findEmbeddedType`, add: + +```cpp + if (name == "Shared") + { + return mlirGenNewShared(location, newExpression, genContext); + } +``` + + Then add the method to `MLIRGenImpl` (declare it in `MLIRGenImpl.h` beside the other + `mlirGen(NewExpression ...)` helpers; define it in `MLIRGenExpressions.cpp`): + +```cpp + // `new Shared(x)` (own spec 23): T is the type argument, else the receiver's, else x's. + ValueOrLogicalResult MLIRGenImpl::mlirGenNewShared(mlir::Location location, NewExpression newExpression, + const GenContext &genContext) + { + if (!newExpression->arguments || newExpression->arguments->size() != 1) + { + emitError(location, "new Shared(value) takes one argument"); + return mlir::failure(); + } + + auto result = mlirGen(newExpression->arguments->front(), genContext); + EXIT_IF_FAILED_OR_NO_VALUE(result) + auto value = V(result); + + mlir::Type elementType; + if (newExpression->typeArguments && newExpression->typeArguments->size() == 1) + { + elementType = getType(newExpression->typeArguments->front(), genContext); + } + else if (auto receiver = dyn_cast_or_null(genContext.receiverType)) + { + elementType = receiver.getElementType(); + } + else + { + elementType = mth.wideStorageType(value.getType()); + } + + if (!elementType) + { + return mlir::failure(); + } + + CAST_A(stored, location, elementType, value, genContext); + return V(builder.create(location, mlir_ts::SharedType::get(elementType), stored)); + } +``` + + Use the casting macro this file already uses for "cast a value to a type" (search + `CAST_A(` in `MLIRGenExpressions.cpp`). If the receiver-type field has another name, use the + one `NewClassInstance` reads (search `receiverType` in `mlirGen(NewExpression`). + - **`s.value`** (`lib/TypeScript/MLIRGenAccessCall.cpp`, the `mlir::TypeSwitch` over + `actualType` in `mlirGenPropertyAccessExpressionBaseLogic`): add a case beside + `.Case`: + +```cpp + .Case([&](auto sharedType) -> mlir::Value { + if (name != "value") + { + emitError(location, "Shared has no member '") << name << "'"; + return mlir::Value(); + } + + auto elementType = sharedType.getElementType(); + auto valueRef = builder.create( + location, mlir_ts::RefType::get(elementType), objectValue); + return builder.create(location, elementType, valueRef); + }) +``` + + Use the local names that function already uses for the member name and the object value. + `s.value = x` then needs no change: `mlirGenSaveLogicOneItem` (`MLIRGenImpl.h`) finds the + `LoadOp` and stores through its reference, the `SharedValueRef`. + - **`Shared.count(s)`** (`mlirGen(CallExpression ...)` in `MLIRGenExpressions.cpp`): before the + general call path, add: + +```cpp + // `Shared.count(s)` (own spec 23.1) + if (auto access = callExpression->expression.as(); + callExpression->expression == SyntaxKind::PropertyAccessExpression && + access->expression == SyntaxKind::Identifier && + MLIRHelper::getName(access->expression.as()) == "Shared" && + MLIRHelper::getName(access->name) == "count" && !resolve("Shared", genContext)) + { + if (!callExpression->arguments || callExpression->arguments->size() != 1) + { + emitError(location, "Shared.count(handle) takes one argument"); + return mlir::failure(); + } + + auto result = mlirGen(callExpression->arguments->front(), genContext); + EXIT_IF_FAILED_OR_NO_VALUE(result) + auto handle = V(result); + if (!isa(handle.getType())) + { + emitError(location, "Shared.count takes a Shared"); + return mlir::failure(); + } + + return V(builder.create(location, getNumberType(), handle)); + } +``` + + `!resolve("Shared", genContext)` stands for "no program symbol named `Shared`". Use whatever + lookup the new-expression fallback relies on to find that `mlirGen(identifier)` failed (for + example, a `findDeclaration`/`resolveIdentifier` call; search how `mlirGen(location, name)` + decides "can't resolve name"). +- [x] **Step 6: Type helpers.** + - **`include/TypeScript/MLIRLogic/MLIRTypeCore.h`:** add `mlir_ts::SharedType` to the lists in + `isNullableTypeNoUnion` and `isNullableOrOptionalType`, beside `ClassType`. This makes `===` + compare pointers and `null` compare and assign. + - **`include/TypeScript/MLIRLogic/MLIRTypeIterator.h`:** add a case beside the `RefType` one that + visits `getElementType()`, so a generic `T` inside `Shared` is found. + - **`include/TypeScript/MLIRLogic/MLIRPrinter.h`, `printType`:** add, beside the `RefType` case: + +```cpp + .template Case([&](auto t) { + out << "Shared<"; + printType(out, t.getElementType()); + out << ">"; + }) +``` + +- [x] **Step 7: Lowering.** + - **`lib/TypeScript/LowerToLLVM.cpp`, `populateTypeScriptConversionPatterns`:** add beside the + `RefType` conversion: + +```cpp + converter.addConversion([&](mlir_ts::SharedType type) { + return LLVM::LLVMPointerType::get(m.getContext()); + }); +``` + + - **The lowerings**, beside `StringCopyOpLowering`: + +```cpp +// `new Shared(x)` (own spec 23): a heap block with the header every block has (a count under rc +// and own, born 0 like any block), whose payload is the value. +struct SharedNewOpLowering : public TsLlvmPattern +{ + using TsLlvmPattern::TsLlvmPattern; + + LogicalResult matchAndRewrite(mlir_ts::SharedNewOp op, Adaptor transformed, + ConversionPatternRewriter &rewriter) const final + { + LLVMCodeHelper ch(op, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); + auto elementType = cast(op.getType()).getElementType(); + auto handle = ch.MemoryAlloc(elementType, MemoryAllocSet::Zero); + rewriter.create(op->getLoc(), transformed.getValue(), handle); + rewriter.replaceOp(op, ValueRange{handle}); + return success(); + } +}; + +// `s.value`: the payload starts at the handle. +struct SharedValueRefOpLowering : public TsLlvmPattern +{ + using TsLlvmPattern::TsLlvmPattern; + + LogicalResult matchAndRewrite(mlir_ts::SharedValueRefOp op, Adaptor transformed, + ConversionPatternRewriter &rewriter) const final + { + rewriter.replaceOp(op, ValueRange{transformed.getShared()}); + return success(); + } +}; + +// `Shared.count(s)`: the header word under rc and own; gc and none never write it. +struct SharedCountOpLowering : public TsLlvmPattern +{ + using TsLlvmPattern::TsLlvmPattern; + + LogicalResult matchAndRewrite(mlir_ts::SharedCountOp op, Adaptor transformed, + ConversionPatternRewriter &rewriter) const final + { + auto loc = op->getLoc(); + TypeHelper th(rewriter); + LLVMCodeHelper ch(op, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); + auto f64Type = rewriter.getF64Type(); + if (!tsLlvmContext->compileOptions.tracksOwnership()) + { + rewriter.replaceOpWithNewOp(op, f64Type, rewriter.getF64FloatAttr(-1.0)); + return success(); + } + + auto llvmIndexType = ch.getIndexType(); + auto blockPtr = ch.getBlockPtrFromPayloadPtr(loc, transformed.getShared(), llvmIndexType); + auto count = rewriter.create(loc, llvmIndexType, blockPtr); + rewriter.replaceOpWithNewOp(op, f64Type, count); + return success(); + } +}; +``` + + Use the pattern base class, adaptor and helper names that `StringCopyOpLowering` uses in this + file, and the index-type getter `_MemoryAlloc` uses (`LLVMCodeHelperBase.h`). If `number` + lowers to another type than `f64`, use `getTypeConverter()->convertType(op.getType())`. + - Add the three patterns to the `patterns.insert<...>` list beside `StringCopyOpLowering`. + - **`lib/TypeScript/LowerToAffineLoops.cpp`, `AddTsAffineLegalOps`:** add + `mlir_ts::SharedNewOp, mlir_ts::SharedValueRefOp, mlir_ts::SharedCountOp` to the + `addLegalOp<...>` list beside `mlir_ts::StringCopyOp`. +- [x] **Step 8: Run the test.** + - Build, then run `ctest -C Release -R own_shared -j 8 --timeout 300`. Expected: 4/4 pass. + - Also run `test-runner -mm=rc ` once by hand. It should pass too, but it leaks: rc does + not count handles until Task 2. + - Check the cross-module type: write a scratch module exporting + `export function make(): Shared { return new Shared(1); }`, compile it with + `--emit=mlir` and `--export`, the way the `export_*` corpus files are, and confirm that + `__decls` prints `Shared`. +- [x] **Step 9: The spec.** In §23.1: + - Replace the bullets for `s.value` and `s.value = x` with one bullet: "**`s.value`** is a + `ts.Load` of the place `ts.SharedValueRef(s)`, and **`s.value = x`** stores into that place + the way a field store does (MLIRGen's retain, `ts.ReleaseSlot` and `ts.Store`), so `x` moves in + and the old `T` is released. One op instead of two lets the place-read and store machinery of + MLIRGen and own apply unchanged." + - In the decisions table and §23.6, rename the graph test's content: "`own_shared_graph`: a DAG + (one item under two parents) and a singly linked list walked with `cur = cur.value.next`". Move + "a doubly linked list and a node with a parent link" to `own_shared_cycle`, since both are + cycles and leak (§23.4). + - Keep CRLF. +- [x] **Step 10: Commit.** + +```bash +git add include/TypeScript lib/TypeScript test/tester/own/own_shared_basic.ts test/tester/CMakeLists.txt docs/superpowers/specs/2026-09-24-own-memory-model-design.md +git commit -m "-mm=own Shared: the type, its ops and MLIRGen (spec 23.1)" +``` + +--- + +### Task 2: counting a handle (rc) + +**Files:** +- Modify: `include/TypeScript/MLIRLogic/MLIRTypeHelper.h` +- Modify: `include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h` +- Modify: `lib/TypeScript/MLIRGenImpl.h`, `lib/TypeScript/MLIRGenExpressions.cpp` +- Modify: `lib/TypeScript/LowerToLLVM.cpp` (the GC root list) +- Create: `test/tester/own/own_shared_count.ts`, `test/tester/own/own_shared_graph.ts` +- Modify: `test/tester/CMakeLists.txt` + +**Interfaces:** +- Consumes: `SharedType`, `SharedNewOp`, `SharedValueRefOp`, `SharedCountOp`, `shared_models` (Task 1). +- Produces: `static bool MLIRTypeHelper::isSharedHandleType(mlir::Type type)`. It is true for a + `SharedType`, or an `OptionalType` of one, or a `UnionType` whose members are one `SharedType` and + `NullType`/`UndefinedType` only. Tasks 3 and 4 use it. + +- [x] **Step 1: Write the tests.** `test/tester/own/own_shared_count.ts` (rc and own only, since + gc and none keep no count): + +```ts +// Shared (spec 23.1): Shared.count after each copy and drop - a handle made, copied, given to a +// function that keeps it, returned from one, reassigned to the block it holds, and copied in a loop. +class Node { + v = 0; +} + +function keepOne(s: Shared, into: Shared[]) { + into.push(s); +} + +function dropLast(list: Shared[]) { + list.pop(); +} + +function make() { + return new Shared(new Node()); +} + +function main() { + const a = new Shared(new Node()); + assert(Shared.count(a) == 1, "one handle"); + + let b = a; + assert(Shared.count(a) == 2, "copied"); + + b = a; + assert(Shared.count(a) == 2, "reassigned to the block it holds"); + + const list: Shared[] = []; + keepOne(a, list); + assert(Shared.count(a) == 3, "kept by a callee"); + + b = new Shared(new Node()); + assert(Shared.count(a) == 2 && Shared.count(b) == 1, "one handle moved to another block"); + + dropLast(list); + assert(Shared.count(a) == 1, "the kept one dropped"); + + const m = make(); + assert(Shared.count(m) == 1, "returned from a function"); + + for (let i = 0; i < 1000; i++) { + const t = a; + assert(Shared.count(t) == 2, "copied in a loop"); + } + + assert(Shared.count(a) == 1, "every copy in the loop dropped"); + + print("done."); +} +``` + + `test/tester/own/own_shared_graph.ts`: + +```ts +// Shared (spec 23): a DAG - one item under two parents - and a singly linked list walked with +// `cur = cur.value.next`. Handles are dropped and their memory reused before the rest is read. +class Item { + v = 0; + name = ""; + next: Shared | null = null; +} + +class Parent { + child: Shared | null = null; +} + +function churn() { + let keep: string[] = []; + for (let i = 0; i < 1000; i++) keep.push("k" + i); + return keep.length; +} + +function list(n: number) { + let head: Shared | null = null; + for (let i = 0; i < n; i++) { + const item = new Shared(new Item()); + item.value.v = i; + item.value.next = head; + head = item; + } + + return head; +} + +function main() { + const child = new Shared(new Item()); + child.value.name = "child" + 1; + let p1 = new Parent(); + const p2 = new Parent(); + p1.child = child; + p2.child = child; + p1 = new Parent(); + assert(churn() == 1000); + const c = p2.child; + assert(c !== null && c.value.name == "child1", "the other parent keeps the child"); + + let sum = 0; + let cur = list(10); + while (cur) { + sum += cur.value.v; + cur = cur.value.next; + } + + assert(sum == 45, "the list walked to its end"); + assert(cur === null); + + print("done."); +} +``` + +- [x] **Step 2: Register them and watch them fail.** In `CMakeLists.txt`: + - change `set(shared_models gc none)` to `set(shared_models gc none rc)`; + - add `own_shared_graph` to the inner `foreach(shared_test ...)` list; + - add after that foreach: + +```cmake +# Shared.count is a count only under rc and own. +foreach(shared_count_mm rc) + tslang_add_test(NAME test-jit-${shared_count_mm}-own_shared_count COMMAND test-runner -jit -mm=${shared_count_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_shared_count.ts") + tslang_add_test(NAME test-compile-${shared_count_mm}-own_shared_count COMMAND test-runner -mm=${shared_count_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_shared_count.ts") +endforeach() +``` + + Build, then run `ctest -C Release -R own_shared -j 8 --timeout 300`. Expected: + - `own_shared_count` fails under rc (the count stays 0: nothing counts a handle yet); + - `own_shared_basic` and `own_shared_graph` may pass under rc, because a leak is invisible; + - gc and none pass. + + If `own_shared_graph` fails to compile under gc or none (a `Shared | null` narrowing), + that is Review Focus 1: fix it in this task. The union must be tag-free and narrow like a class + union; check `isUnionTypeNeedsTag` and the narrowing helpers. +- [x] **Step 3: The type predicates** (`include/TypeScript/MLIRLogic/MLIRTypeHelper.h`): + - In `ownsHeapMemory(location, type, visiting)`, add `isa(type)` to the first + "owns its own block" test, beside `ClassType`. Do not add `SharedType` to + `getOwnershipFieldTypes`: the block's payload is released by the handle's routine. + - Add: + +```cpp + // A handle (own spec 23.2): Shared, or a tag-free union or an optional of one with null or + // undefined - one pointer whose block is counted in every model that tracks ownership. + static bool isSharedHandleType(mlir::Type type) + { + if (isa(type)) + { + return true; + } + + if (auto optionalType = dyn_cast(type)) + { + return isSharedHandleType(optionalType.getElementType()); + } + + if (auto unionType = dyn_cast(type)) + { + mlir::Type shared; + for (auto member : unionType.getTypes()) + { + if (isa(member)) + { + continue; + } + + if (!isa(member) || (shared && shared != member)) + { + return false; + } + + shared = member; + } + + return !!shared; + } + + return false; + } +``` + +- [x] **Step 4: The routines** (`include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h`): + - In `buildRetainBody`, add `isa(type)` to the "reference to a block of its + own" condition beside `ClassType`, so it is `emitIncRef` of the loaded pointer. + - In `buildBody`, after the class/object branch, add: + +```cpp + // a handle (own spec 23.2) is counted in every model that tracks ownership: the last one + // releases the payload and frees the block + if (auto sharedType = dyn_cast(type)) + { + auto handle = rewriter.create(loc, ptrTy, slotPtr); + emitIfLastReference(handle, [&]() { + releaseSlot(sharedType.getElementType(), handle); + emitFreeBlock(handle); + }, /*counted=*/true); + return; + } +``` + + - Give `emitIfLastReference` a `bool counted = false` parameter, and choose the test with it: + +```cpp + auto wasLast = compileOptions.memoryModel == MemoryModelOwn && !counted ? emitIsMortal(payloadPtr) + : emitDecRef(payloadPtr); +``` + + `releaseSlot(type, slotPtr)` is the helper that calls a type's release routine on a slot. The + handle points at the payload, which is the `T`'s slot. If the helper has another name, use the + one `releaseFields` calls per field. + - **A tag-free union `Shared | null`** is released and retained through the union branches. + Check with `--emit=llvm -mm=rc` on `own_shared_graph.ts` that the `next` field's release calls + the handle's `tsrel_` routine, not a tag dispatch. If it dispatches on a tag the union does not + have, make the tag-free union of one `SharedType` use the `SharedType` routine (the same way a + tag-free `C | null` uses the class routine; find that branch and add `SharedType` beside + `ClassType`). +- [x] **Step 5: MLIRGen's ownership ops for a handle** (`lib/TypeScript/MLIRGenImpl.h`): + - In `markFreshBlockOwned`, widen the test to + `isa(value.getType())`. A new + handle then carries its birth reference the way a new string does (§9.25 of the rc document). + - In `mlirGenNewShared` (Task 1), give the value to the block, then mark the new handle: + +```cpp + CAST_A(stored, location, elementType, value, genContext); + // the block owns what it holds, as an array literal owns its elements + mlirGenRetainCaptured(location, mlir::ValueRange{stored}); + auto handle = builder.create(location, mlir_ts::SharedType::get(elementType), stored); + markFreshBlockOwned(location, handle); + return V(handle); +``` + + - **`isOwningSlot`:** add an `isOwnedSharedValueSlot` beside `isOwnedFieldSlot`, and include it + in `isOwningSlot`'s `||` chain: + +```cpp + // `s.value` (own spec 23): the payload is owned by the handle's block, as a field by its object + bool isOwnedSharedValueSlot(mlir::Location location, mlir::Value reference) + { + auto valueRef = reference.getDefiningOp(); + return valueRef && + mth.ownsHeapMemory(location, cast(reference.getType()).getElementType()); + } +``` + + - **`LowerToLLVM.cpp`:** in the GC root type test (the list that names `ClassType` near the + `GC_ENABLE` code in `VariableOpLowering`), add `SharedType` beside `ClassType`. +- [x] **Step 6: Run the tests.** + - Build, then run `ctest -C Release -R own_shared -j 8 --timeout 300`. Expected: all pass (gc, + none and rc, plus `own_shared_count` under rc). + - Run `test-runner -noopt -mm=rc` and `test-runner -noopt -jit -mm=rc` on all three files. + Expected: they pass. + - Run `ctest -C Release -R "own|rc" -j 8 --timeout 300`. Expected: no new failure (nothing + existing uses `Shared`). +- [x] **Step 7: Commit.** + +```bash +git add include/TypeScript lib/TypeScript test/tester/own/own_shared_count.ts test/tester/own/own_shared_graph.ts test/tester/CMakeLists.txt +git commit -m "-mm=own Shared: a handle is counted, under rc first (spec 23.2)" +``` + +--- + +### Task 3: own steps aside, and reads through a handle + +**Files:** +- Modify: `lib/TypeScript/OwnershipFacts.h`, `lib/TypeScript/OwnershipInferencePass.cpp`, + `lib/TypeScript/OwnershipSignaturePass.cpp` +- Modify: `lib/TypeScript/LowerToLLVM.cpp` (the retain gates) +- Create: `test/tester/own/own_err_shared_borrow_write.ts`, `own_err_shared_borrow_call.ts`, + `own_err_shared_borrow_push.ts`, `own_err_shared_moved_in.ts` +- Modify: `test/tester/CMakeLists.txt` + +**Interfaces:** +- Consumes: `MLIRTypeHelper::isSharedHandleType` (Task 2), `SharedValueRefOp`, `SharedNewOp` (Task 1). +- Produces: + - `inline bool isHandle(mlir::Value value)` in `OwnershipFacts.h`; + - `inline bool touchesHandle(mlir::Operation *op)`, true for a `Retain`/`Release` of a handle + value, or a `RetainSlot`/`ReleaseSlot` of a slot holding one. + +- [x] **Step 1: Write the negative tests.** + + `own_err_shared_borrow_write.ts`: + +```ts +// -mm=own rejects (spec 23.3): a borrow read through one handle, used after a write through another +// handle that may reach the same block. +class Node { + v = 0; +} + +function main() { + const a = new Shared(new Node()); + const b = a; + const n = a.value; + b.value = new Node(); + print(n.v); +} +``` + + `own_err_shared_borrow_call.ts`: + +```ts +// -mm=own rejects (spec 23.3): a borrow read through a handle, used after a call that may drop - +// `touch` writes through a handle it reaches from a global. +class Node { + v = 0; +} + +let kept: Shared | null = null; + +function touch() { + const k = kept; + if (k) k.value = new Node(); +} + +function main() { + const a = new Shared(new Node()); + kept = a; + const n = a.value; + touch(); + print(n.v); +} +``` + + `own_err_shared_borrow_push.ts`: + +```ts +// -mm=own rejects (spec 23.3): an element borrowed through one handle, used after a push through +// another, which may move the elements. +class Node { + v = 0; +} + +class Bag { + items: Node[] = []; +} + +function main() { + const a = new Shared(new Bag()); + const b = a; + a.value.items.push(new Node()); + const first = a.value.items[0]; + b.value.items.push(new Node()); + print(first.v); +} +``` + + `own_err_shared_moved_in.ts`: + +```ts +// -mm=own rejects: a class value given to `new Shared(x)` moves into the block, so it cannot be used +// after (spec 23.1, 2.3). +class Node { + v = 0; +} + +function main() { + const n = new Node(); + const s = new Shared(n); + print(n.v, s.value.v); +} +``` + +- [x] **Step 2: Register them; add own to the positives; watch them fail.** In `CMakeLists.txt`: + - add to `own_error_cases`: + +```cmake + "own_err_shared_borrow_write|but is used here after it may be released or overwritten" + "own_err_shared_borrow_call|but is used here after it may be released or overwritten" + "own_err_shared_borrow_push|but is used here after it may be released or overwritten" + "own_err_shared_moved_in|is used here after its value was moved" +``` + + - change `set(shared_models gc none rc)` to `set(shared_models gc none rc own)`, and + `foreach(shared_count_mm rc)` to `foreach(shared_count_mm rc own)`; + - add the verifier over every positive, under own and rc: + +```cmake +foreach(shared_verify_mm own rc) + foreach(shared_test own_shared_basic own_shared_graph own_shared_count) + add_test(NAME test-${shared_verify_mm}-verify-ownership-${shared_test} + COMMAND $ --emit=mlir-affine --no-default-lib -mm=${shared_verify_mm} --verify-ownership + "${PROJECT_SOURCE_DIR}/test/tester/own/${shared_test}.ts") + set_tests_properties(test-${shared_verify_mm}-verify-ownership-${shared_test} + PROPERTIES FAIL_REGULAR_EXPRESSION "error|Stack dump") + endforeach() +endforeach() +``` + + Build, then run `ctest -C Release -R "own_shared|own-err-own_err_shared|verify-ownership-own_shared" -j 8 --timeout 300`. + Expected: + - the own positives fail, with "takes a second reference" or "left a retain behind"; + - `own_err_shared_moved_in` passes (a taker already moves), and the other three negatives give + their own first errors or compile; + - the rest pass. + + Record each negative's actual first message now. +- [x] **Step 3: The facts** (`lib/TypeScript/OwnershipFacts.h`): + +```cpp +// A handle (spec 23.2): counted in every model that tracks ownership, and left alone by own. +inline bool isHandle(mlir::Value value) +{ + return value && mlir_ts::MLIRTypeHelper::isSharedHandleType(value.getType()); +} + +inline bool touchesHandle(mlir::Operation *op) +{ + if (mlir::isa(op)) + { + return isHandle(op->getOperand(0)); + } + + if (mlir::isa(op)) + { + auto slotType = mlir::cast(op->getOperand(0).getType()); + return mlir_ts::MLIRTypeHelper::isSharedHandleType(slotType.getElementType()); + } + + return false; +} +``` + + Use the namespace `MLIRTypeHelper` is in, as the file's other uses show. In `isPlace(ref)`, add + `SharedValueRefOp` beside `PropertyRefOp`/`ElementRefOp`: `s.value` is a place. +- [x] **Step 4: The inference pass steps aside** (`lib/TypeScript/OwnershipInferencePass.cpp`). The + goal is "never erased, never reported" for a handle. The safety net comes first: + - **Never erased.** Immediately before the loop that erases `toErase` in `analyze`, add: + +```cpp + // a handle's retains and releases are rc's, and stay (spec 23.2) + toErase.remove_if([](mlir::Operation *op) { return touchesHandle(op); }); +``` + + - **Never decided.** In the walk that collects `retains` and `candidates`, skip ops for which + `touchesHandle(op)` holds: do not push them into `retains`, and do not add their value to + `candidates`, `drops` or `slotLoadOf` lists. + - In the top-level retain loop, add `if (touchesHandle(op)) { continue; }` as the first statement. + - For a call's kept arguments (the `keepingCalls` loop that calls `reportGivenNotOwned`), skip an + argument with `isHandle(argument)`. + - In `decideClosures`/`decideEscaping`, do not erase or report a capture retain whose value is a + handle (`touchesHandle`). + - **A handle read out of a place is a counted copy, not a borrow (§23.3).** In `placeReadOf`, + return null when the loaded value `isHandle`. + - **A read through a handle (§23.3).** `s.value` is now a place (`isPlace`), so `Load(SharedValueRef)` + is a place read, and `chainOf` walks through it. Make `chainOf` treat a `SharedValueRefOp` link + as a place whose root is the handle, with root kind `NotOwned`. A handle is reachable through + aliases, so it is never "owned". Then: + - `dropsChain`'s call rule already ends a borrow with a not-owned root at any call that is not + `callNoDrops`; + - a `ReleaseSlot` of a `SharedValueRef` place (a store `x.value = …` through any handle) must + match a chain that has a `SharedValueRef` place of the same element type. Add that beside + the position-and-type match `dropsChain` does for a `PropertyRef`; + - a store into a field of a `T` through any handle (`x.value.f = …`) is a `ReleaseSlot` of a + `PropertyRef` of the same position and type, which `dropsChain` already matches by type; + - add `ArrayPushOp`, and `ArrayUnshiftOp` if it exists, to the ops the walk collects into + `drops` beside `ArrayPopOp`, but only when the array operand's `chainOf` reaches a + `SharedValueRef`. It is a drop for an element place of the same element type, as `pop` is. + - `describePlace` should name a `SharedValueRef` place `'.value'` (e.g. `'a.value'`). +- [x] **Step 5: The signature pass** (`lib/TypeScript/OwnershipSignaturePass.cpp`): + - **No facts on handles:** in `setFacts`, leave out of `__own_params` any parameter whose type + `isSharedHandleType`, and do not set `__own_result_borrows` for a result that is a handle. + - **`keeps()`:** a `SharedNewOp` whose operand is the parameter keeps it, like a store into a + place. + - **`dropsInBody`:** + - a `ReleaseOp` of a handle value, or a `ReleaseSlotOp` of a slot holding one, is a drop. It may + be the last handle, and its payload's release can free anything the payload owns; + - a `ReleaseSlotOp` of a `SharedValueRef` place is a drop. In `reachesOutside`, treat a + `SharedValueRef` as reaching outside: any handle may be shared. +- [x] **Step 6: The lowering gates** (`lib/TypeScript/LowerToLLVM.cpp`): + - In `RetainOpLowering` and `RetainSlotOpLowering`, compute + `auto counted = MLIRTypeHelper::isSharedHandleType()`. Use the reference's type, + and for a slot, the `RefType`'s element type. Then: + - raise "ownership inference left a retain behind" under own only when `!counted`; + - emit the retain when `isRefCounted() || (tracksOwnership() && counted)`. + - `RetainCellOpLowering` is unchanged, since a cell is not a handle. + - In the two `VariableOpLowering` sites gated on `isRefCounted()` that retain a captured slot's + initial value with `emitRetainSlot`, use the same `|| (tracksOwnership() && counted)` on the + slot's element type. +- [x] **Step 7: Run the tests.** + - Build, then run + `ctest -C Release -R "own_shared|own-err-own_err_shared|verify-ownership-own_shared" -j 8 --timeout 300`. + Expected: all pass. That is every positive under gc, none, rc and own (AOT and JIT), the count + test under rc and own, the four negatives with their regexes, and the verifier quiet. + - If a negative's first error differs from its regex, keep a regex that names the §23.3 rule or + the move, and say so in the commit message. + - Run `test-runner -noopt -mm=own` and `-noopt -jit -mm=own` on every positive. + - Run the full `ctest -C Release -R own -j 8 --timeout 300`. Expected: every existing own test + still passes (490 before this phase). +- [x] **Step 8: Teeth.** Commit or back up first. Each switch is temporary. Clear + `test/tester/own/__jit` and the build's `own/__jit` between JIT runs. + - (a) Remove the `toErase.remove_if(...)` line and the `touchesHandle` skips in the retain loop: + the own positives must fail (to compile, or at run time). + - (b) Make `chainOf` treat a `SharedValueRef` root as owned: `own_err_shared_borrow_call` must + compile. + - (c) Drop the `SharedValueRef` match from `dropsChain`: `own_err_shared_borrow_write` must + compile. + - (d) Drop the push rule: `own_err_shared_borrow_push` must compile. If it errors anyway, the + existing rules already end that borrow. Record which, for §23.7. + - Revert all four. +- [x] **Step 9: Commit.** + +```bash +git add lib/TypeScript test/tester/own/own_err_shared_*.ts test/tester/CMakeLists.txt +git commit -m "-mm=own Shared: own leaves handles to the count; reads through a handle (spec 23.2, 23.3)" +``` + +--- + +### Task 4: handles in containers, `any`, closures, and cycles + +**Files:** +- Modify: `include/TypeScript/MLIRLogic/TypeOfOpHelper.h`, `lib/TypeScript/LowerToLLVM.cpp` + (`TypeDescriptorOpLowering`), `lib/TypeScript/MLIRGenCast.cpp`, and wherever the test shows a gap +- Create: `test/tester/own/own_shared_containers.ts`, `test/tester/own/own_shared_cycle.ts` +- Modify: `test/tester/CMakeLists.txt` + +**Interfaces:** +- Consumes: everything above. + +- [x] **Step 1: Write the tests.** `own_shared_containers.ts`: + +```ts +// Shared (spec 23) in containers: one node in two arrays, a `Shared | null` field, a closure +// capturing a handle, a handle boxed into `any` and back, and a handle returned from a function. +// Each owner but one is dropped and its memory reused before the last is read. +class Node { + v = 0; + tag = ""; +} + +class Holder { + s: Shared | null = null; +} + +function churn() { + let keep: string[] = []; + for (let i = 0; i < 1000; i++) keep.push("k" + i); + return keep.length; +} + +function make(v: number) { + const s = new Shared(new Node()); + s.value.v = v; + s.value.tag = "t" + v; + return s; +} + +function main() { + const s = make(3); + + let left: Shared[] = [s]; + const right: Shared[] = [s]; + left = []; + assert(churn() == 1000); + assert(right[0].value.v == 3 && right[0].value.tag == "t3", "the other array keeps the node"); + + const h = new Holder(); + h.s = right[0]; + const read = () => s.value.v; + assert(read() == 3, "a closure reads through its handle"); + + const boxed: any = h.s; + const back = >boxed; + assert(back === s, "a handle boxed into any comes back the same"); + assert(back.value.tag == "t3"); + + h.s = null; + assert(churn() == 1000); + assert(read() == 3 && back.value.v == 3, "the closure and the unboxed handle keep the node"); + + print("done."); +} +``` + + `own_shared_cycle.ts`: + +```ts +// Shared (spec 23.4): a doubly linked pair and a child with a link to its parent are cycles, +// which are never freed (as under rc); the program still runs. +class Item { + v = 0; + next: Shared | null = null; + prev: Shared | null = null; +} + +function main() { + for (let i = 0; i < 100; i++) { + const a = new Shared(new Item()); + const b = new Shared(new Item()); + a.value.next = b; + b.value.prev = a; + a.value.v = i; + const p = b.value.prev; + assert(p !== null && p.value.v == i); + + const parent = new Shared(new Item()); + const child = new Shared(new Item()); + parent.value.next = child; + child.value.prev = parent; + } + + print("done."); +} +``` + +- [x] **Step 2: Register them and watch them fail.** Add `own_shared_containers own_shared_cycle` + to the inner `foreach(shared_test ...)` list and to the `shared_verify_mm` list. Build, then run + `ctest -C Release -R own_shared -j 8 --timeout 300`. Expected: `own_shared_containers` fails in + some models. The `any` box has no type descriptor for a handle yet, and the `>` + cast from `any` is not supported. `own_shared_cycle` may already pass. +- [x] **Step 3: A descriptor for a handle.** + - **`TypeOfOpHelper.h`, `typeOfAsString`:** return `"object"` for a `SharedType`. That is what + JavaScript's `typeof` would say of a wrapper object. The descriptor is keyed by the type's hash, + so `Shared` and `Shared` still get distinct descriptors. + - **`LowerToLLVM.cpp`, `TypeDescriptorOpLowering`:** under own, the retain routine name is empty + for every type. Keep it for a handle: + +```cpp + auto retainRoutineName = + tsLlvmContext->compileOptions.memoryModel == MemoryModelOwn && + !MLIRTypeHelper::isSharedHandleType(descriptorType) + ? std::string() + : orl.getOrCreateRetainRoutine(descriptorType); +``` + +- [x] **Step 4: `any` back to `Shared`** (`lib/TypeScript/MLIRGenCast.cpp`). Find where a cast + from `any` to a class type is generated: it checks the box's type tag and unboxes. Make + `SharedType` take the same path as `ClassType`. The tag it compares against is the handle's + descriptor from Step 3. + - If that path compares the `typeof` name only ("object"), an `any` holding a plain object would + also pass the check. Record that in §23.7 as a limit. + - If the class path cannot be reused, stop and report the reason (BLOCKED). +- [x] **Step 5: Run the tests.** + - Build, then run `ctest -C Release -R "own_shared|verify-ownership-own_shared" -j 8 --timeout 300`. + Expected: all pass under gc, none, rc and own, AOT and JIT. + - Run `-noopt` under own and rc for both files. + - If own rejects `own_shared_containers` with an error that names a non-handle value, read it: + - a rule of §23.3 means the test is wrong; rewrite the shape and say so in the commit; + - anything else is a gap in Task 3's skips. Fix it there, with this test as its proof. +- [x] **Step 6: Commit.** + +```bash +git add include/TypeScript lib/TypeScript test/tester/own/own_shared_containers.ts test/tester/own/own_shared_cycle.ts test/tester/CMakeLists.txt +git commit -m "-mm=own Shared: handles in containers, any and closures; cycles (spec 23.1, 23.4)" +``` + +--- + +### Task 5: measure, corpus, results, Linux + +**Files:** +- Modify: the spec, a new §23.7 "Results" +- Modify: this plan (tick the boxes) + +- [x] **Step 1: Measure.** Write `$SCRATCH/shared_loop.ts`: + +```ts +class N { + v = 0; + kids: Shared[] = []; +} + +function main() { + let total = 0; + for (let i = 0; i < 200000; i++) { + const leaf = new Shared(new N()); + const a = new Shared(new N()); + const b = new Shared(new N()); + a.value.kids.push(leaf); + b.value.kids.push(leaf); + leaf.value.v = i; + total += a.value.kids[0].value.v == b.value.kids[0].value.v ? 1 : 0; + } + + print(total); +} +``` + + - Run `pwsh test/tester/tools/measure.ps1 -Source $SCRATCH/shared_loop.ts`. It needs one plain + test-runner run first, so that `compile.bat` exists. + - Run it again at 20000 iterations. + - Expected: own and rc peaks are the same at both counts, within 0.5 MB; none grows; gc stays + bounded. + - Record both rows. +- [x] **Step 2: Corpus.** Run the corpus scripts (Appendix A) plain and with `OPTS=--opt`, with + this branch's binary. + - Expected: 453 / 593 and 451 / 593, each with the same file set as main + (`join -t $'\t' base.tsv new.tsv | awk -F'\t' '$2 != $3'` prints nothing). + - Make `base.tsv`/`baseopt.tsv` from main's binary first: build main once, or reuse a saved + `tslang-main.exe`, before this branch's final build. +- [x] **Step 3: Unchanged where it compiles.** For every file ok in `base.tsv`, compare + `--emit=llvm -mm=own --no-default-lib -o .ll` from main's binary and this branch's. + - Normalise the run-to-run names first: `([A-Za-z])_[0-9]{4,}` → `\1_N`, `\.[0-9]{6,}\.` → + `.N.`, and the `[N x i8]` size on lines containing `FH`. + - Expected: identical, apart from any file shown to differ between two runs of main itself. + - Also run `--emit=llvm -mm=rc` on 50 of those files. Expected: identical. rc's routines changed + only for `SharedType`. +- [x] **Step 4: Spec results.** Append §23.7 "Results" in the style of §22.7: + - what was built, with the op list, including Task 1's `SharedValueRef` amendment; + - the tests and models; + - the negatives with their messages; + - the teeth (Task 3 Step 8), and what (d) showed; + - the measure rows; + - the corpus and IR-unchanged results; + - the limits found: the `any` check from Task 4 Step 4, if it applies; cycles; anything a task + recorded. + + Tick this plan's boxes, and annotate this step "committed; push and PR left to the user". + + Committed; push and PR left to the user. +- [x] **Step 5: Linux.** Build the branch with GCC in WSL and run + `ctest -R "own_shared|own-err|own-no-counting|own-verify|verify-ownership-own_shared"` there. + - The WSL clone is `~/ts/TypeScriptCompiler`, whose origin is `/mnt/i/TypeScriptCompiler`. + Fetch this branch, and do not fetch `main`. + - The build dir is `__build/tslang/ninja/release`. + - Write a `.sh` into the scratchpad and run it with `wsl bash -l ` from PowerShell. +- [x] **Step 6: Commit.** Do not push and do not open a PR. That is left to the user. Committed; push and PR left to the user. + +```bash +git add docs/superpowers/ +git commit -m "-mm=own Shared: measure, corpus and results (spec 23.7)" +``` + +--- + +## Appendix A: the corpus tooling (scratchpad only, never committed) + +`one.sh`, the first error of one corpus file (`T` is the compiler to run): + +```sh +#!/bin/sh +f="$1"; T=${T:-/i/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-release/bin/tslang.exe} +out=$(cd /i/TypeScriptCompiler/tslang/test/tester/tests && timeout 120 "$T" --emit=llvm -mm=own --no-default-lib $OPTS "$(basename "$f")" -o NUL 2>&1) +rc=$? +first=$(printf '%s\n' "$out" | grep -m1 -E "error" | sed -E 's/^.*error: //' | tr -d '\r') +if [ $rc -eq 0 ]; then echo "$(basename "$f") ok"; else echo "$(basename "$f") ${first:-rc=$rc}"; fi +``` + +`run.sh`, every file into `$1`: + +```sh +#!/bin/sh +cd "$(dirname "$0")" +ls /i/TypeScriptCompiler/tslang/test/tester/tests/*.ts | xargs -P 12 -n 1 sh ./one.sh | sort > "${1:-results.tsv}" +echo "$(grep -c ' ok$' "${1:-results.tsv}") / $(wc -l < "${1:-results.tsv}") ok" +``` 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 4e13ead95..2611590f1 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 @@ -7,8 +7,9 @@ amendments folded in. Phase 0 merged as #399 (results §11); phase 1 signatures and `any`) merged as #406-#410 (results §15); phase 5a (closures that do not escape) merged as #436 (results §16); phase 5b (closures that escape) merged as #437 (results §17); phase 7a (generators, async) merged as #439 (results §18); phase 7b (generators that borrow) merged as -#441 (results §19); phase 6 (the corpus report) on branch `own-phase-6`, after #444 (results -§20). Plans in `docs/superpowers/plans/`. +#441 (results §19); phase 6 (the corpus report) merged as #444, #445 and #449 (results +§20, §21); strings as values merged as #452 (§22); phase 8 (`Shared`) designed in §23. Plans +in `docs/superpowers/plans/`. ## 1. Purpose @@ -2073,3 +2074,683 @@ positives under own, rc, none and gc, AOT and JIT) passes 48 of 48. After the fi answer). Both disagreements are safe: where the pass sees a tag there is no copy, and the error stays; where only the lowering sees one, `ts.StringCopy` fails to match (`notifyMatchFailure`), and the build fails loudly. + +## 23. `Shared` (design, 2026-10-03) + +Phase 8 (§7). §2.7 deferred it: an explicit, counted generic for graphs that a tree of single +owners cannot express (a parent link, a node in two lists, a DAG), reusing rc's retain and release +routines for that type only. Everything else stays single-owner and uncounted. + +Decisions taken in the design conversation: + +| Question | Decision | +| --- | --- | +| Form | A wrapper: a handle to a block that holds one `T` and the count. Not a marker on `T` itself | +| Where it is declared | Built into the compiler, so `--no-default-lib` programs and every model have it | +| Members | `.value` read and write, `===`/`!==` on handles, `Shared.count(s)` | +| Handles under own | Own steps aside: every rc retain and release of a handle stays and is counted | +| Cycles | Leak, as under rc, and documented; `WeakRef` under own is a later phase | + +### 23.1 The type and its members + +```ts +class Node { + v = 0; + next: Shared | null = null; +} + +const a = new Shared(new Node()); +const b = a; // a second handle: the count is 2 +a.value.v = 1; +print(b.value.v); // 1 +b.value = new Node(); // the old Node is released, the new one moves in +print(Shared.count(a), a === b); +``` + +- **The type.** `Shared` is resolved by name in `MLIRGenTypes` beside `Reference` and + `BoxedObject`, to a new `!ts.shared`. A program's own declaration of a type named `Shared` + is found first and wins, as for every embedded name. +- **At run time** a handle is a pointer to a heap block: the header word is the count, and the + payload is one `T` in its storage type. +- **`new Shared(x)`** is `ts.SharedNew`: it allocates the block and stores `x` into it, which is a + store into a container (§2.3): `x` moves in. +- **`s.value`** is a `ts.Load` of the place `ts.SharedValueRef(s)`, and **`s.value = x`** stores + into that place the way a field store does (MLIRGen's retain, `ts.ReleaseSlot` and `ts.Store`), + so `x` moves in and the old `T` is released. One op instead of two lets the place-read and store + machinery of MLIRGen and own apply unchanged. +- **`a === b` and `a !== b`** compare the pointers. +- **`Shared.count(s)`** reads the header: the number of handles under rc and own, `-1` under gc + and none, which keep no count. +- **A program means the same under every model.** gc and none get a wrapper that is never + counted. rc counts it as it counts every block. Own counts this type and no other. +- **Other uses of the type.** A handle can be a field, an element, a union member (`Shared | + null` is one pointer), a capture, a parameter, a result, or boxed into `any`. It goes into a + module's `__decls` as `Shared`, so a module that imports it sees the same type. + +### 23.2 Handles under own: the analysis steps aside + +- **MLIRGen emits rc's ops for handles under own too.** It emits the same `ts.Retain`, + `ts.RetainSlot`, releases and `__owned_consumed` declarations for a `!ts.shared` value under rc + and own, because both track ownership (§3.1). That is unchanged. +- **The inference pass leaves every retain and release of a handle in place.** A handle is a value of + one of these types (`MLIRTypeHelper::isSharedHandleType`): + - `!ts.shared`, or an optional of a handle; + - a union with at least one `Shared` member whose other members own no heap memory: `null`, + `undefined`, a number, a boolean, a literal, another `Shared`. `Shared | null` is one + pointer (§22.1). `Shared | number` is tagged: its routines dispatch on the tag through + descriptors, a `Shared` member's counting and the others' doing nothing. + + The pass never erases such a retain or release, and never reports one. A handle's count is + therefore exactly rc's, which rc's own suite tests. +- **A `Shared` in a union with a member that owns a block** (`Shared | string`, + `Shared | Node`, `Shared[] | Shared`) is rejected under own where a handle is put into + one: `a Shared in a union with '', which -mm=own does not count, is not supported`. + Counting the union would count a single-owner block, and not counting it would lose the + handle's count. gc, none and rc accept it. +- **Lowering.** Under own, a surviving retain of a `!ts.shared` value no longer reports "left a + retain behind"; it lowers to `__tslang_inc_ref`, as under rc. +- **The release routine** of a `!ts.shared` value is the same in every model that tracks + ownership: `__tslang_dec_ref`, and at zero, release the payload `T` and free the block. +- **The `T` is released by its model's routine.** Under own, that is a single-owner drop: free + unless immortal, after releasing the `T`'s own fields. +- **Inside a container.** Everything outside the handle is ordinary own analysis. A handle stored + in a field, element, union or closure is a value the container owns, and the container's drop + releases it through the routine above. +- **The type descriptor.** An `any` box, a union or an interface releases what it holds through the + type's descriptor, so `!ts.shared` gets a descriptor whose release slot is its routine. +- **Not in this phase.** Eliding the count for a handle own could prove moved or borrowed. That + would need a birth convention for handles that differs from rc's (rc counts the birth with a + retain own would erase), so it is left for a measured later step. + +### 23.3 Reading through a handle + +Under own, no other path reaches a block while its owner holds it. That is why a borrow out of a +container ends only at the container's own release or overwrite. A `Shared` breaks that: two +handles can reach one block. + +```ts +const n = a.value; // a borrow of the Node +b.value = new Node(); // if b is a, this frees the Node n borrows +print(n.v); // rejected +``` + +- **The rule.** A borrow read through a handle, at any depth (`s.value`, `s.value.name`, + `s.value.items[0]`), ends at the first of: + - the handle's own release or overwrite (the existing rule); + - a write into any place reached through any `Shared`: `x.value = …`, `x.value.f = …`, an + element store, `push` or `pop` on an array reached through one; + - a call that is not known to drop nothing (`__own_no_drops`, §15). +- **A handle copied out is a counted retain, not a borrow.** `cur = cur.value.next` gives `cur` a + handle of its own, which no write ends. That is how a graph is walked, so walking is not + restricted. +- **The error** is the existing "borrows a field but is used here after it may be released or + overwritten". +- **Conservative by design.** It does not check whether two handles could be the same: any write + through any handle, and any call that may drop, ends every such borrow. The alternative is a + runtime borrow flag, Rust's `RefCell`, which costs at run time; it is not taken. +- **A string read through a handle** is a borrow too. Where one of rc's retains survives, §22 + makes it a copy, as for any field. + +### 23.4 Cycles + +`a.value.next = b; b.value.next = a` leaks both blocks, as under rc. The test +`own_shared_cycle` shows that it compiles and runs, and §23.7 (the results) records it. + +### 23.5 The verifier + +`--verify-ownership` (§9.18 of the rc document) runs before the own passes. It checks that every +slot that takes a reference gives it back on every path. Handles keep rc's pairing, so it needs no +change. It runs on every `own_shared_*` test, under own and rc. + +### 23.6 Tests and measures + +- **Positive**, under own, rc, none and gc, AOT and JIT: + - `own_shared_basic`: two handles, `.value` read and write, `===`. + - `own_shared_user_named`: a program's own class named `Shared`, and its static `count`, win over + the built-in (§23.1). + - `own_shared_graph`: a DAG (one item under two parents) and a singly linked list walked with + `cur = cur.value.next`. + - `own_shared_containers`: one node in two arrays, a `Shared | null` field, a closure + capturing a handle, a handle boxed into `any` and back, a handle returned from a function. + - `own_shared_cycle`: a cycle that is never freed, which still runs: a doubly linked list and a + node with a parent link, both cycles that leak (§23.4). + - `own_shared_union`: a handle in a `Shared | number` local, field and element, each switched + to a number and back, with rc's counts under own (§23.2). + + Each frees every handle but one, then allocates over the freed memory (the churn pattern) + before reading through the one left. An early free then reads reused memory. +- **Under own and rc only:** `own_shared_count`, which asserts `Shared.count` after each copy and + drop. +- **Negative:** + - `own_err_shared_borrow_write`: the §23.3 example; + - `own_err_shared_borrow_call`: a borrow through a handle used after a call that may drop; + - `own_err_shared_moved_in`: a class value given to `new Shared(x)` and then used, which is a + use after move. + - `own_err_shared_union_owned`: a handle put into a `Shared | string` (§23.2). +- **Teeth:** + - with the inference pass erasing a handle's retains as for any value, the positives fail; + - with §23.3's rule off, its negatives compile. +- **Verifier:** `--verify-ownership` is quiet on every `own_shared_*` file. +- **Measured:** a loop that builds and drops a small graph of handles 200,000 times stays flat + under own and rc (`measure.ps1`), and grows under none. +- **Unchanged:** no corpus file uses `Shared`, so the corpus (453 / 593 plain, 451 with `--opt`) + must be unchanged, and its LLVM IR identical for every file that compiles, as in §22.7. + +### 23.7 Results, 2026-10-03 + +Plan: `docs/superpowers/plans/2026-10-03-own-shared.md`. Changed: `TypeScriptTypes.td`, +`TypeScriptOps.td`, `MLIRTypeCore.h`, `MLIRTypeHelper.h`, `MLIRTypeIterator.h`, `MLIRPrinter.h`, +`TypeOfOpHelper.h` (the type), `MLIRGenTypes.cpp`, `MLIRGenExpressions.cpp`, +`MLIRGenAccessCall.cpp`, `MLIRGenCast.cpp`, `MLIRGenImpl.h` (MLIRGen), `OwnershipRoutineLogic.h`, +`CastLogicHelper.h`, `LowerToAffineLoops.cpp`, `LowerToLLVM.cpp` (the routines and lowering), +`OwnershipFacts.h`, `OwnershipInferencePass.cpp`, `OwnershipSignaturePass.cpp` (own), +`OwnedReturnConsumptionPass.cpp` (a handle global's initializer, every model). Every new +branch keys on `!ts.shared`, a `ts.SharedValueRef`, or `isSharedHandleType`, so a program without +`Shared` compiles as before (see "Unchanged where it compiles"). + +#### What was built + +- **The type** `!ts.shared` (`mlir_ts::SharedType`), lowered to `ptr`. `Shared` is an embedded + name in `MLIRGenTypes`, so a program's own `Shared` is found first and wins + (`own_shared_user_named`). It is nullable (`===`, `null`), its `typeof` is `"object"`, and it + prints as `Shared`, in messages and in `__decls`. +- **The ops**, three, not the two of §23.1's first draft: + - `ts.SharedNew` (`MemAlloc`): allocates the block with the allocator every model's `new` uses, + and stores the value; + - `ts.SharedValueRef`: the place `!ts.ref` of the payload, the identity in lowering. + **`s.value` is a `ts.Load` of it**, and `s.value = x` stores through it (§23.1), so the place + machinery of MLIRGen and own applies unchanged; + - `ts.SharedCount`: `-1` where nothing is counted (gc, none), otherwise the header word as a + `number`. +- **MLIRGen.** + - `new Shared(x)` is reached only when `Shared` does not resolve. `T` comes from a type argument, + then a `Shared` receiver, then the value: **`new Shared(1)` is a `Shared`**, as + `let x = 1` is an `s32`. `new Shared()`, two arguments and two type arguments are errors. + - `Shared.count(s)` is intercepted at the call when `Shared` does not resolve. It takes a bare + handle, and a narrowed `Shared | null` or optional one (`if (s)` keeps the union's type), + which it unwraps as `.value` does. + - The payload goes in with `mlirGenRetainCaptured` (a move, §2.3), and `ts.SharedNew`'s result is + a fresh owned reference (`markFreshBlockOwned`). A `.value` place is an owning slot + (`isOwnedSharedValueSlot`), so `s.value = x` is rc's `Retain`, `ReleaseSlot`, `Store`. + - **A `const` of a handle gets identity storage** (`needsIdentityStorage`, as a `const` array + does), so `const b = a` is a second, counted handle (§23.1). That is any handle + (`isSharedHandleType`): a bare `Shared`, and a union or optional one (`const c = p.child`, + `const c: Shared | null = a`). A narrowed view (`if (c)`) stays a view. Until the final + review's fix round, only a bare `Shared` had storage: a union `const` was folded into the + place it was read from, held no count, and got no check, so unlinking a node + (`const nxt = head.value.next; head.value.next = null`) left `nxt` reading freed memory under + rc and own. + - **A handle global initialised by a value that carries a reference is born holding it.** A + value given to `ts.GlobalResult` straight keeps its reference (a terminator is no use §9.30 + releases after). One given through a view of the global's type - `ts.Cast` to + `Shared | null`, `ts.OptionalValue` to `Shared | undefined`, `ts.CreateUnionInstance` - + was a discarded temporary, given back at the end of the initializer, so the global held a block + at count 0, under rc as well. `OwnedReturnConsumptionPass` now claims it through any such view + (`claimHandleGlobalInitializers`), from `new Shared(..)` or from a call: a call's result is + marked as carrying a reference only there, once its callee is classified, which is why + MLIRGen cannot see it. Keyed on the global's type. The first fix round claimed only a cast of + `new Shared(..)`, in MLIRGen; `Shared | undefined = new Shared(..)` and + `Shared | null = mk()` stayed at count 0 until the second (`own_shared_global_init`). + **A handle read out of a field or an element carries no reference**, and since the third fix + round the claim gives it one: a `ts.Retain` right after the read, before the end of the + initializer gives back a temporary object or array (`own_shared_global_field`: + `holder.child`, `new P().child`, `mkP().child`, `mkArr()[0]`). Until then such a global was + born at count 0 under rc and own alike and crashed when read (the re-review's `globfield`, + `tmpfield`, `callfield`: `count 0`, then 0xC0000005). #446 did not hide that: it hides only a + global initialised from another global. The class equivalents (`let g: Node = new P().child`) + read freed memory under rc on main too (`cls_globfield`, `cls_tmpfield`); they are not changed. + **Not covered:** a handle global initialised from another global (`let h = root`) takes no + count of its own; #446 makes such an initializer read null in every model (the review's + `glob.ts` reads null for `d` and `h`), which hides it. A global object literal holding a handle + from a call (`let o = { s: mkS(4) }`) gives the call's reference back and crashes under rc and + own (`objonly`), as its class equivalent reads freed memory under rc on main (`cls_obj`). +- **The routines.** `ownsHeapMemory` is true for a handle. Its release routine is + `__tslang_dec_ref`, and at zero the payload's routine and `__tslang_free_block`; its retain is + `__tslang_inc_ref`. `emitIfLastReference(counted)` makes own count a handle where it tests + mortality for everything else. A tag-free `Shared | null` needs no routine of its own. +- **Under own, the analysis steps aside** (§23.2): + - the facts `isHandle` and `touchesHandle`; `isPlace` includes `ts.SharedValueRef`; + - the inference walk returns early for every op that touches a handle, except a `ReleaseSlot` of + a field or element holding one, which stays a drop (a handle overwritten there may be the last + one). A handle never becomes a candidate, by any of the four ways in, and a safety net takes + any handle op out of the erase list; + - the signature pass gives no parameter or result fact to a handle, and `moveIntoGlobals` leaves + a handle's retain in a global's initializer in place: it is the global's count. Before the fix + round it erased it as a move, so a global handle was born at count 0 and freed while still + held, and a `Shared | null` global was rejected as "takes a second reference"; + - a generator over a handle parameter is not a borrowing state (`findBorrowingStates`): the + parameter's cell holds a count of its own, which only the box's release gives back, so its + box owns what it holds; + - the lowering emits a surviving handle retain under own (`__tslang_inc_ref`), without "left a + retain behind"; + - a cell holding a handle (a captured handle parameter) is released by `ReleaseCell`, and a box + may take it (`decideCells`, `decideEscaping`): the cell holds its own count. +- **The handle, widened** (Ruling 6, §23.2): a `Shared`, an optional of one, or a union with a + `Shared` member whose other members own no heap. `Shared | number` and + `Shared | Shared | null | number` are counted under own exactly as rc counts them. + `Shared | string`, `Shared | Node` and `Shared[] | Shared` are rejected under own + where a handle is put into one, with a named error (`reportHandleInOwningUnion`). +- **Reading through a handle** (§23.3), as built: + - `chainOf` treats a base read through by `ts.SharedValueRef` as a `NotOwned` root and records + it as a handle; a handle loaded out of a place walks on up, so `p.child.value` sees `p.child`; + - `rootEnds`: a borrow through a handle also ends at that handle's own `ReleaseSlot` or `Store` + (a local) or `ts.Release` (a temporary). A copy of the handle does not end it; + - `dropsChain`: a `ReleaseSlot` of a `.value` place ends every borrow read through any handle, + **whatever the element type** (Ruling 4); + - a `push` or `unshift` on an array reached through a handle (`reachesSharedValue`) is a drop, + inline and inside a callee (`dropsInBody`); + - a function that stores a parameter into a handle's payload keeps that parameter (`keeps()` + sees `s.value = x` as a store into a place); + - **a handle read out of a place is a borrow of the place** (`placeReadOf`), checked as a field + read is (`checkPlaceRead`), unless every use of it is one rc counts (`isCountedHandleRead`, + `handleReadUses`, Rulings 7 and 9). **Every use is classified, and a use not known to count + is a borrow** (fourth fix round, Ruling 9). A counted use is a receiver with rc's retain for + that very use (`isCountedHandleReceiver`): a store into a field, an element, a global or an + owning local (`countedStoreRetain`: `ts.Retain` of the value, then the store, or the + `ts.ReleaseSlot` of the old value and then the store); a return, which after the affine + lowering is a store into the result slot after the scope exit's releases (`ts.Retain`, then + `ts.Release` and `ts.ReleaseSlot` of the frame's locals, then the store); a push, an unshift, + `new Shared`, an array literal's element and an `any` box, each right after the row of + retains MLIRGen puts before what it fills (`retainedRightBefore`); or an owning local's slot + (`ts.RetainSlot`). A cast or view that is still a handle, a merge and a local that owns + nothing pass the value on, and their uses are classified in turn. Everything else is a + borrow: a use `isBorrow` lists, and every use that is neither of the above - a tuple's element, + an object literal's field, a capture, a `yield`, a cast to what is not a handle, a receiver + with no retain of its own, any op not named here; the borrow walk reports one that keeps the + value as a keep. rc's retains count for nothing on their own: each is the count of the + receiver right after it. A handle argument is always a borrow, never a kept one, since a + handle gets no parameter facts: `keep(k, q.child = p.child)`, `keep` storing it, is rejected. + `cur = cur.value.next` + is counted only, so walking stays legal. `f(p, p.child)`, where `f` overwrites `p.child`, is + rejected (`own_err_shared_place_arg`). Before the first fix round every handle read was + exempt, and that program read freed memory under own. It still does under rc, which retains + no argument; + - **the exemption is per use, not per read** (second fix round). One read with a counted use and + one that reads through the place (`f(p, q, q.child = p.child)`) is a borrow: the counted uses + are a copy of their own in the borrow walk (`walkBorrowed`'s `countedHandle`), and the other + use is tenure-checked, so that program, where `f` overwrites both fields, is rejected + (`own_err_shared_place_two_uses`). The release of the old value a counted store makes right + before storing the retained read (`ts.Retain`, `ts.ReleaseSlot`, `ts.Store`) is not a drop + of the read (`collectCountedStoreReleases`): the block holds the retain's count by then, even + when the two fields are one. So `Shared.count(q.child = p.child)` and + `look(q.child = p.child)` stay legal (`own_shared_place_two_uses`). Until this round any one + counted use exempted the whole read, and that program read freed memory under own; + - **a store counts only when rc retains for it** (third fix round). `isCountedHandleReceiver` + took every `ts.Store` of the read as counted, and the borrow walk skipped it as a copy. In + `const o = { s: (q.child = p.child) }` the read has two stores: the one into `q.child`, which + rc retains for, and the one into the object literal's field, which rc makes no retain for. The + second kept the read with no count, so after `dropBoth(p, q)` own read freed memory and crashed + (the re-review's `e2_objlit`). The second round had put the remaining gap in "an op outside + `isBorrow`'s list"; this one was the store receiver. That program is now rejected with the + plain `{ s: p.child }`'s message, `borrows a field and cannot be stored, returned or captured` + (`own_err_shared_place_literal`). The array literal, return, constructor argument and `any` + shapes (`e3`, `e6`, `e7`, `e9`) are retained by rc, compile, and count as rc does; + - **a use not known to count is a borrow** (fourth fix round, Ruling 9). `handleReadUses` still + let a use that was neither a counted receiver nor in `isBorrow`'s list fall through, so in + `const t: [Shared, number] = [(q.child = p.child), 1]` the counted store into `q.child` + exempted the read and the tuple kept it with no count: after `dropBoth(p, q)` own read freed + memory and crashed (the re-review's `c7`, and `as const` and `[Shared | null, number]`, + `d4` and `d10`). The default is now inverted: only a receiver with rc's retain for that use + counts, and every other use makes the read a borrow, which the borrow walk reports at the tuple + (`own_err_shared_place_tuple`, the plain `[p.child, 1]`'s message). Push, unshift, + `new Shared` and a return now need their retain too, where before they counted on sight; an + array literal's element is now a receiver, with its retain (before, its bare retain alone + counted the read). A return and an `any` box are classified before `passesOn` would hand + them on (`isHandOff`), in `handleReadUses` and in the borrow walk alike, and + `ts.ReturnInternal` is no receiver any more: it is reached only through the result slot. + **The pairing is exact in effect.** `countedStoreRetain` and `retainedRightBefore` match a + retain by position, and a retain has one next op, so no two receivers count the same retain. + A retain made for one receiver but sitting right before another, uncounted, store of the value + would be taken by that store; the receiver it was made for then finds no retain of its own and + makes the read a borrow, so the read is never exempt with fewer retains than counted + receivers. No MLIRGen site emits that order (each emits its retain right before its own op), + and nothing more was built for it; + - `ts.SharedValueRef` and `ts.SharedCount` read a handle and keep nothing (`isBorrow`), so + `s.value` and `Shared.count(s)` are borrows of whatever `s` borrows. +- **`any`.** A handle boxes under its own descriptor, named `"object"`, whose release and retain + slots are the handle's routines, kept under own. A tag-free `Shared | null` boxes under the + handle's descriptor, so a null in it is `typeof` `"object"`. `>a` goes through + `___unbox>`, which checks the name and throws otherwise. The counts round-trip + exactly under rc and own. + +#### Tests and teeth + +- **Positive**, under gc, none, rc and own, AOT and JIT (88 runs): `own_shared_basic`, + `own_shared_user_named`, `own_shared_graph`, `own_shared_containers`, `own_shared_cycle`, + `own_shared_union`, two from the final review's fix round, two from its second, and one from its + third: + - `own_shared_global_field`: handle globals initialised from `holder.child` (born at 2: the + field keeps its count), `new P().child`, `mkP(..).child` and `mkArr(..)[0]` (born at 1: the + temporary gave its count back). Each drops the other handle where there is one, reuses the + memory, and reads through the global; + - `own_shared_global_init`: `Shared | undefined` from `new Shared(...)`, `let` and `const` + `Shared | null` from a call, an explicit cast of a call's result, `Shared | undefined` + and `Shared | number` from a call. Each asserts the global's count of 1, copies it, drops + the global's handle (the copy's, for the `const`), reuses the memory, and reads what is left; + - `own_shared_place_two_uses`: one read of `p.child` stored into `q.child` and read in the same + expression (`Shared.count(q.child = p.child)`, `look(q.child = p.child)`), and the same field + on both sides; `p.child` is then replaced, and `q.child` is read after the memory is reused; + - `own_shared_global`: a `const`, a `let` and a `Shared | null` global, each initialised by + `new Shared(...)` at module level and born with one count. Each `let` global's handle is + dropped while a copy holds the block, the memory is reused, and the copy is read; + - `own_shared_unlink`: a list node unlinked from its head (§23.1's `Node`), a parent's child + taken into a `const` before the field is cleared, and a `Shared | null` const of a `let` + that is then given another block. In each the `const` is the only handle left when it is read. + + **Under rc and own** (4 runs): `own_shared_count`, with every count after a copy, a drop, an + outer block's death, a `.value` write, a narrowed union, an `any` round trip, a captured handle + parameter (two calls), an escaping closure over one (100 iterations), a generator over a handle + parameter (100 calls), handles copied inside a generator, and (second fix round) a generator + that reads its handle again after the caller has replaced its own and the memory is reused + (`2`, `42`, then `1`). The fix round also asserts the + counts of a `const` union (`own_shared_union`: 4, not 3) and of a `const` read out of a field + (`own_shared_graph`: 3, under rc and own). **`--verify-ownership`**, under own and rc, is quiet + on the eleven counted files (22 runs). +- **`-O0`**: test-runner's `-noopt` does nothing in a Release build, so each positive was also run + by hand with `--opt_level=0` under own and rc, AOT and JIT. +- **Negative** (12). Messages with `--di`; without it the borrower is `'this value'` and the place + "a field" or "an element", as for every own error: + +| test | message | +|---|---| +| `own_err_shared_borrow_write` | `'n' borrows 'a.value' but is used here after it may be released or overwritten` (the §23.3 example) | +| `own_err_shared_borrow_call` | the same, after a call that may drop | +| `own_err_shared_borrow_push` | `'first' borrows 'a.value.items[]' but is used here after …` (a push through another handle) | +| `own_err_shared_borrow_push_call` | `… borrows 'a.value' but is used here after …` (the push inside a callee) | +| `own_err_shared_borrow_other_type` | `'n' borrows 'a.value' but is used here after …` (a write through a `Shared` alias) | +| `own_err_shared_borrow_reassigned` | `'n' borrows 'a.value' but is used here after …` (the handle itself reassigned) | +| `own_err_shared_moved_in` | `is used here after its value was moved` | +| `own_err_shared_union_owned` | `a Shared in a union with 'string', which -mm=own does not count, is not supported` | +| `own_err_shared_place_arg` | `'this value' borrows 'p.child' but is used here after …`, at `f(p, p.child)`, where `f` overwrites `p.child` | +| `own_err_shared_place_two_uses` | `'this value' borrows a field but is used here after …`, at `f(p, q, q.child = p.child)`, where `f` overwrites both fields; the same with `--opt` | +| `own_err_shared_place_literal` | `'this value' borrows a field and cannot be stored, returned or captured`, at `{ s: (q.child = p.child) }` (31:15), then `dropBoth(p, q)`; the same with `--opt` | +| `own_err_shared_place_tuple` | `'this value' borrows a field and cannot be stored, returned or captured`, at `[(q.child = p.child), 1]` (31:39), then `dropBoth(p, q)`; the same with `--opt` | + +- **Suites:** the Shared set is 126 tests, `ctest -R own` 616 and `ctest -R "own|rc"` 1570, all + passing on this branch's final build (72, 562 and 1516 before the final review's fix round; + 93, 583 and 1537 after it; 114, 604 and 1558 after the second; 125, 615 and 1569 after the + third). +- **Teeth**, each a temporary switch, with the JIT cache cleared: + +| switch | result | +|---|---| +| Task 1: before the type existed (its RED) | `own_shared_basic` fails under gc and none, `can't resolve name: Shared` | +| Task 2: the payload's release taken out of the handle routine | `own_shared_count` fails under rc and own: `the payload released when the outer block died` | +| (a) the safety net and the retain loop's handle skip removed | nothing fails: the walk's early return keeps handles out first | +| (a) extended: the walk's early return removed too | `own_shared_basic`, `own_shared_graph` and `own_shared_count` under own (AOT and JIT), and `own_shared_count`'s own verifier run, fail to compile | +| (b) a handle root `Owned`, not `NotOwned` | `own_err_shared_borrow_call` compiles | +| (c) the `.value` match removed from `dropsChain` | `own_err_shared_borrow_write` compiles | +| (d) the inline push rule off | `own_err_shared_borrow_push` compiles | +| the element-type comparison restored for `.value` places | `own_err_shared_borrow_other_type` compiles | +| the push rule in `dropsInBody` off | `own_err_shared_borrow_push_call` compiles | +| the `rootEnds` handle clause off | `own_err_shared_borrow_reassigned` compiles | +| the `decideCells` handle return removed | `own_shared_count` under own: `a captured parameter's count given back` (a count leaked per call) | +| the widened handle off | `own_shared_union` is rejected by the named rule | +| the widened handle and the named rejection off (a tagged union is single-owner) | `own_shared_union`: `is used here after its value was moved`; a field loop crashes; the counts drop below rc's | +| fix round: `moveIntoGlobals` erases a handle's retain again | `own_shared_global` under own (AOT, JIT, verifier): `the initializer of global 'maybe' takes a second reference`; the reviewer's `gl1` prints `start 0 0` and crashes | +| fix round: a union global no longer takes over its initializer's reference | `own_shared_global` under rc and own: `a nullable global is born with one count; copied` | +| fix round: identity storage for a bare `Shared` only | `own_shared_unlink` under rc: `the field and the const`; under own it is rejected (`borrows a field but is used here after …`: the place-read check now sees the folded `const`); `own_shared_graph` and `own_shared_union` fail their counts under rc and own | +| fix round: the `placeReadOf` exemption for every handle restored | `own_err_shared_place_arg` compiles | +| fix round: `ts.SharedValueRef` and `ts.SharedCount` out of `isBorrow` | under own, `own_shared_count`: `borrows a field and cannot be stored, returned or captured` (handles copied inside a generator); `own_shared_containers`: `borrows an element and cannot be stored …` (`right[0].value.v`) | +| fix round: a generator over a handle parameter a borrowing state again | `own_shared_count` under own: `the generator's count given back` (a count leaked per call) | +| second fix round: the global claim back to the first round's (a cast of `new Shared` only) | `own_shared_global_init` under rc and own, JIT and AOT: `an optional global from new Shared is born with one count`; the review's `v1` and `v3` print 0, `opt_new_min` 0, `null_call_min` garbage | +| second fix round: any one counted use exempts the whole read again | `own_err_shared_place_two_uses` compiles; the review's `hole.ts` crashes under own (`count 0`) | +| second fix round: a counted store's release of the old value a drop again | `own_shared_place_two_uses` under own (JIT, AOT, verifier): `borrows a field but is used here after …` | +| second fix round: the counted uses not a copy in the borrow walk | `own_shared_place_two_uses` under own (JIT, AOT, verifier) and `own_err_shared_place_two_uses`: `borrows a field and cannot be stored, returned or captured` | +| third fix round: any store of the read counted again (`isCountedHandleReceiver`) | `own_err_shared_place_literal` compiles (`--emit=obj` exits 0); the re-review's `e2_objlit` crashes under own (0xC0000005) | +| third fix round: a store with no retain not marked uncounted (`isUncountedStoreOf` off, the predicate kept) | the same: the store falls through both lists, the read stays exempt, `own_err_shared_place_literal` compiles and `e2_objlit` crashes | +| third fix round: the field-read retain in the global claim off | `own_shared_global_field` under rc and own, JIT and AOT: `a global from a live object's field takes a count of its own`; `globfield`, `tmpfield`, `callfield` crash again (`count 0`) | +| third fix round: the element read left out of the claim | `own_shared_global_field` under rc and own, JIT and AOT: `a global from a temporary array's element is born with one count` | +| fourth fix round: the fall-through restored (a use neither counted nor in `isBorrow`'s list, nor a store, is neither) | `own_err_shared_place_tuple` fails, the only one of the 126 (`--emit=obj` exits 0); the re-review's `c7`, `d4` and `d10` crash under own (0xC0000005) | +| fourth fix round: a return and an `any` box no longer classified before `passesOn` (`isHandOff` off) | `own_shared_containers` under own (JIT, AOT, verifier) fails to compile, at its `any` box (`const boxed: any = h.s`); the re-review's `e6`, `e9`, `b9`, `c2` and this round's return and `any` probes are rejected with `borrows a field and cannot be stored, returned or captured` | + +- **What (d) showed:** no existing rule ends an element borrow across a push through another + handle; the push rule is the only one. Each other §23.3 rule also has its own negative. +- **The containers test is weak as teeth:** several owners keep its node alive, so an early free + there may read live memory. The counts are asserted in `own_shared_count` and `own_shared_union`. + +#### Measured + +AOT, `measure.ps1`, in MB. The first program is the plan's: per iteration, three handles, one of +them in two arrays, every block given back at the end of the iteration. The second builds a +two-node cycle (`a.value.next = b; b.value.next = a`) per iteration. + +| program | iterations | gc | rc | none | own | +|---|---|---|---|---|---| +| a leaf in two arrays | 20000 | 5.9 | 4.4 | 10.6 | 4.4 | +| a leaf in two arrays | 200000 | 5.9 | 4.4 | 65.9 | 4.4 | +| a two-node cycle | 200000 | 5.9 | 35.2 | 35.2 | 35.2 | + +Own and rc are flat and equal; none grows. A cycle leaks under rc and own exactly as under none, +and gc collects it (§23.4). + +#### Corpus + +`--emit=llvm -mm=own --no-default-lib`, every file of `test/tester/tests`, main's binary against +this branch's, plain and with `--opt`: 453 / 593 and 451 / 593 on both, each file with the same +first error. Two messages (`00types`, `callWithSpread`) name a function by its run-to-run hash +(`FH`); with it normalised, the two lists are identical. + +#### Unchanged where it compiles + +The 453 files that compile on main were compiled with main's binary and this branch's, +`--emit=llvm -mm=own --no-default-lib`, with §22.7's normalisations; the `[n x i8]` size is +normalised on any line holding an `FH` name. 451 are identical. The other two differ only as +main differs from itself between runs, in the order of a union's members in a synthesized name, +and so in the order of the functions and constants that follow it: + +| file | main, 12 runs | this branch, 12 runs | +|---|---|---| +| `00union_null_undefined_nonstrict` (§22.7) | 11 one order, 1 the other | 10, 2 | +| `00union_bin_ops2` (`___bin_op_plus>`) | 8, 4 | 7, 5 | + +Each binary gives exactly the same two normalised outputs, byte for byte. After the final review's +fix round, the corpus is again 453 / 593 and 451 / 593 with the same first errors, and all 453 +files compile to the same normalised IR as main's (in that run both flipping files came out in +main's recorded order). Under `-mm=rc`, 50 of +those files (every ninth) were compared the same way: 49 are identical, and `00union_bin_ops2` +flips the same way (main 8 and 4, this branch 11 and 1, the same two outputs). + +After the second fix round the corpus is again 453 / 593 and 451 / 593 with the same first +errors. Under own, 451 of the 453 are identical. `00union_null_undefined_nonstrict` came out in +main's other order, and so did `00funcs`, a third file of the same kind that had not flipped in +the earlier runs: the order of a union's members in `___cast,…>`. +Over 12 runs, main gives 11 and 1 of two outputs and this branch 10 and 2, the same two. Under rc, +49 of the 50 are identical, and `00union_bin_ops2` is main's other output again. + +After the third fix round, a spot check of 40 corpus files (every fifteenth, and one more), +`--emit=llvm --no-default-lib` against main's binary under own and rc, normalised as above: all +70 that compile are identical (40 under rc, 30 under own); the other ten fail under own on both +binaries with the same first error. None of the three flippers was in the sample. + +After the fourth fix round, another 40 (every fifteenth from the seventh), the same way: 66 of +the 67 that compile are identical; the other is `00union_bin_ops2`, main's other output, under +rc in one run and under own in the rerun on the final build (main gives it 1 time in 10 here +too). The 13 that fail fail on both binaries with the same first error. + +#### The final review and its fix round + +The final review of the branch found three critical defects and one important one. Every test +then kept a second owner alive, which is how all three critical ones got through. The fix round +fixed all four, each with a test that drops the only other owner before the read, and a tooth +(above): + +- **Critical 1, a global handle born at count 0.** `moveIntoGlobals` erased a global's handle + retain as a move: the global was freed while held (gl1), and a `Shared | null` global was + rejected. Under rc too, a `Shared | null` global gave back its initializer's reference + through the cast. Both are fixed (`own_shared_global`). +- **Critical 2, a union `const` folded.** `const c = p.child` and `const c: Shared | null = a` + held no count and got no check: §23.1's own unlink read freed memory under rc and own. Every + handle `const` now has storage (`own_shared_unlink`). +- **Critical 3, a handle argument neither counted nor checked.** `f(p, p.child)`, where `f` + overwrites `p.child`, freed the block `f` was reading. A handle read out of a place is now a + borrow unless rc counts it, and that program is rejected (`own_err_shared_place_arg`). +- **Important 4, a generator over a handle falsely rejected.** `isBorrow` did not list + `ts.SharedValueRef` and `ts.SharedCount`. With them listed, such a generator compiled but leaked + a count per call under own, because its state borrowed the box that holds the handle's cell. A + handle parameter now makes the box owned (`own_shared_count`). + +The re-review found Critical 1 fixed only in part, and one new important defect. A second fix +round, approved by the user, fixed both: + +- **Critical 1, the rest.** The first round's claim covered only a cast of `new Shared(..)`. A + `Shared | undefined` global from `new Shared(..)` (through `ts.OptionalValue`), and a + `Shared | null` global, `let` or `const`, from a call or a cast of one, were still born at + count 0: own read garbage and crashed, rc printed count 0. The claim moved to + `OwnedReturnConsumptionPass`, which sees through every view and knows a call's result + (`own_shared_global_init`). +- **Important, a counted use exempted its read.** `isCountedHandleRead` exempted a read when any + one of its uses was counted, so `f(p, q, q.child = p.child)`, where `f` overwrites both fields, + compiled under own and freed the block `f` read. The exemption is now per use: a read with an + uncounted use is a borrow, and its counted uses are a copy (`own_err_shared_place_two_uses`, + `own_shared_place_two_uses`). +- Also: a generator that outlives its caller's handle is asserted (`own_shared_count`). + +The second re-review found one more important defect and a gap in this section. A third fix +round, approved by the user, fixed both: + +- **Important, a store with no count exempted its read.** Every `ts.Store` of a handle read was + a counted receiver, so in `{ s: (q.child = p.child) }` the object literal's field, which rc + makes no retain for, kept the read uncounted, and own crashed after both fields were replaced. + A store now counts only when rc retained for it (`countedStoreRetain`), and one it did not + makes the read a borrow (`own_err_shared_place_literal`). +- **The global gap was understated.** A handle global initialised from a field read was born at + count 0 and crashed under rc and own; #446 hides only the global-from-global case. The claim now + retains a handle read out of a field or an element (`own_shared_global_field`). + +The third re-review found one more important defect, of the same kind: a use outside both lists +fell through. A fourth fix round, approved by the user, inverted the default (Ruling 9): + +- **Important, a tuple's element exempted its read.** `[(q.child = p.child), 1]`, in a `const` + tuple (`as const`, or with a `Shared | null` element), kept the read with no count beside a + counted store; own crashed after both fields were replaced. Every use of a handle read is now + classified, a receiver counts only with rc's retain for that very use, and anything else is a + borrow (`own_err_shared_place_tuple`). + +#### Linux + +GCC, in WSL, the branch at `bbac9ba8`: it builds with no errors, and +`ctest -R "own_shared|own-err|own-no-counting|own-verify|verify-ownership-own_shared"` passes 213 +of 213, the same 213 tests the Windows build lists for that pattern. After the final review's fix +round, at `fad98c53` (the later commits change only documents), it builds with no errors and the +same pattern passes 234 of 234, again the Windows build's count. After the second fix round +(its changes applied to `476c4d8f` as a patch), it builds with +no errors and the same pattern passes 255 of 255, the Windows build's count. After the third fix +round, at `3a86e20d`, it builds with no errors and the same pattern passes 266 of 266, the Windows +build's count. After the fourth (its changes applied to `ceedafa5` as a patch), it builds with no +errors and the same pattern passes 267 of 267, the Windows build's count. + +#### Outside the rule + +- **The covariance hole.** `Shared` is assignable to `Shared`, so + `b.value = new Base()` through the `Base` view leaves a `Base` in a `Derived` block. That is + TypeScript's own unsoundness (mutable covariance) and is not fixed: the `Derived` view then + reads fields a `Base` does not have (p2c, under gc and rc). Own lets no borrow survive such a + write: any write through any handle ends every borrow read through a handle (Ruling 4). +- **Conservative rejections**, false errors rather than miscompiles: + - a function that gives back a handle (a `ts.Release`, or a `ReleaseSlot` of a local holding + one) loses `__own_no_drops`, so a call to it ends borrows its caller holds; + - a getter returning `s.value` is rejected (`borrows 's.value' and cannot be stored, returned or + captured`): `borrowedParam` does not see through `ts.SharedValueRef`; + - a write through a handle of an unrelated type ends borrows through every handle (p1d); + - a push or unshift through a handle ends element borrows, inline and in a callee alike; + - a handle read out of a place and given straight to a call that may drop is a borrow (Ruling + 7), so the call ends it even where the callee retains the handle first. With `p` a parameter, + `keep(k, p.child)` is rejected when `keep` stores into a field. Copying it into a local first + (`const c = p.child; keep(k, c)`) is accepted, and counts; + - a handle read whose count goes into a tuple, beside a counted use, is rejected whether or not + anything drops it (Ruling 9): `[(q.child = p.child), 1]` with nothing dropped after it (the + re-review's `c7c`, `c7d`), and `const [a, n] = [(q.child = p.child), 1]` (`d3`, which counted + 1 under rc and own). All three compiled before this round; the plain `[p.child, 1]` was + already rejected. `const x = p.child; const t = [x, 1]` is accepted, and counts; + - a return whose retain is not followed by only the scope exit's `ts.Release` and + `ts.ReleaseSlot` ops (in any order) before the store into the result slot is rejected. One + such shape was found: a scope exit split across blocks, as a `using` disposal makes it + (`using r = …; return p.child`, in a function or an arrow), compiled and counted 1 before this + round and is now rejected ("borrows a field and cannot be stored, returned or captured"). + `return p.child` after `const` and `let` locals, after a single `let`, inside a loop, inside + `try`/`finally`, from an arrow or a method, into an optional or a union result all compile + and count as rc does; + - so is one read of a handle with a counted use and a use given to a call that may drop: with + `k` an owning local, `f(p, k = p.child)`, where `f` overwrites `p.child`, is rejected even + though `k` keeps the block alive (the review's `hole2.ts`). `k = p.child; f(p, k)` is + accepted; + - a generator over a handle parameter and another parameter that holds a block, that reads the + handle (`function* g(s: Shared, xs: number[])`), is rejected: the handle's cell makes the + box own what it holds, and the other parameter is the caller's. With the handle unread it + compiles (the review's `gen3.ts`; `gen3b.ts` reads it); + - a generator function expression capturing a local handle (`const it = function* () { yield + Shared.count(a); }`) is rejected as an escaping closure that cannot move the variable, as for + any local. +- **`any` is checked by name only.** `>a` accepts any value tagged `"object"`, so a + `Shared` passes as a `Shared` and its fields are read as `A`'s. Literals, numbers and + class instances are rejected. A wrong-type unbox cannot be asserted in a test, because a `let` + in `try` whose initializer throws is not caught (#456). +- **Through null.** `Shared.count(u)` and `u.value` on an un-narrowed `Shared | null` read + through null when it is null; neither is a compile error. +- **Misused names.** `Shared.foo(s)` and a bare `Shared` value report only "failed statement", + with no cause, as `BoxedObject` and `Reference` do. A member other than `value` gets the generic + "Can't resolve property". When a program has its own `Shared`, the `Shared.count` guard leaves + one dead op behind. +- **A rejected mixed union** (`Shared | string`) also reports own's ordinary error on the same + function after the named one (`borrows … and cannot be assigned`). +- **The union check's options.** `ownsHeapMemoryOfUnionMember` builds its `MLIRTypeHelper` with a + default `CompileOptions`, as `isCopyableString` (§22.7) and `OwnershipFacts`' `ownsHeap` do. +- **rc is no oracle for borrows.** rc takes no retain for `const n = obj.f`, so it runs the + use-after-free shapes own rejects (`own_err_shared_borrow_write`, p2c, r1) and prints whatever + the freed memory holds. Own and rc differ there by design. +- **No count is elided.** Own counts every handle as rc does (§23.2's last bullet); a handle own + could prove moved or borrowed is still counted. +- **A bare `{ }` block** does not end a folded `const boxed: any = h`: §9.30 releases a fresh box + at the end of its MLIR block, and a bare block in `main` is not one. Loops and functions are + exact. +- **A handle returned from an imported function is counted one extra**, under rc and own alike. The + importer's `ts.CallIndirect` through the DLL's global carries no `__owned_result`, so its + reference is never given back: `const a = make(3)` is at 2. A class leaks the same way through + an import, before this phase as after (final review, `xm/import_w`). There is no test. +- **Not tested:** a generic `function f(s: Shared)`; nothing rebuilds a `SharedType` with a + substituted element. +- **Resolved during the phase:** an escaping closure capturing a handle parameter, rejected at + first, is accepted (its cell holds a count) and asserted in `own_shared_count`; `Shared.count` + on a narrowed `Shared | null` const, an error at first, is accepted; a tagged + `Shared | number`, single-owner at first, is counted (Ruling 6). + +#### Found, not fixed (every model) + +- #453: a push onto an array parameter is lost to the caller (gc sees the old length, rc frees the + data block the caller still holds). `own_shared_count` keeps its handles in a class for this. +- #454: under rc, `const b = a` of a `let` class value takes no reference, so `a = …` frees what + `b` reads. A `const` handle has its own storage and is not affected: a bare one from the start, + a union or optional one since the final review's fix round. Before it, `const c: Shared | null + = a` was folded, and `a = …` freed what `c` read, under rc and own. +- #455: a `Node | null` cast to `any` crashes the compiler (a null tag). Fixed for + `Shared | null` only. +- #456: a `let` inside `try` whose initializer throws is not caught. +- #457: a throw in a `try` that declares a local crashes: the final review's t1, t6 and t10 under + rc and own, and t8 and t9 under rc (own rejects those two). +- #459: a union global of a class (`let G: Node | null = mk()`) gives back its initializer's + reference through the cast, as a handle one did, so it is freed at once: under own `G.v` reads + 0, under rc it reads reused memory. Fixed for handle globals only, through any view of the + global's type (`claimHandleGlobalInitializers`), since a general fix changes the IR of programs + without `Shared`. +- #458: an unbraced `else` whose body is a `for … of` over a generator, inside a loop, fails with + "operand does not dominate this use" (the re-review's `gen2.ts`; braced, `gen2b.ts` compiles and + counts as rc does). +- Not filed: an owning local initialised through a view of its type + (`let b: Shared | undefined = new Shared(..)`, `let e: Shared | null = mk()`) holds two + counts until the end of its block, where the initializer's reference is given back as a + discarded temporary (§9.30): `Shared.count` reads one high there (`gl/loc.ts`: 2, under rc and + own alike). It is balanced, not a leak. +- Not filed: a global whose initializer is a conditional (`let x = on() ? 1 : 2`) is a compile + error in every model (`'ts.Global' op expects region #0 to have 0 or 1 blocks`), so a handle + global cannot be initialised by one. +- Not filed: ` | string>a`, an `any` unboxed to a union holding a string, is a + compile error in every model (`'ts.Cast' op type ['!ts.any'] can't be stored in …`; the + final review's u1). +- Not filed: a global object literal initialised with a call's result (`let o = { s: mk(4) }`) + gives the call's reference back at the end of the initializer: the class version reads freed + memory under rc on main (`cls_obj`), the handle version crashes under rc and own (`objonly`). +- Not filed: a discarded `arr.pop()` does not give back the element it removes, so a handle + popped out of an array keeps its count (2 after the pop of a two-handle block, under rc and own + alike). The re-review's `arronly` (a `const` read out of a global array, then `arr = []`) + prints 2 under rc and 1 under own, as before this round. diff --git a/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h b/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h index bb2327ede..bc9ad98e8 100644 --- a/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h +++ b/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h @@ -1175,6 +1175,12 @@ class CastLogicHelper LLVM_DEBUG(llvm::dbgs() << "\n!! union storage type: " << storageType << "\n";); valueForBoxing = rewriter.create(loc, storageType, in); } + else if (MLIRTypeHelper::isSharedHandleType(unionType)) + { + // `Shared | null` (own spec 23.1) is one pointer, boxed under the handle's own + // descriptor: the union has no `typeof` name, so it had no tag at all + typeOfValue = toh.typeOfLogic(loc, baseType); + } else { typeOfValue = toh.typeOfLogic(loc, inType); diff --git a/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h b/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h index bc52c31e0..a40fd9c5b 100644 --- a/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h +++ b/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h @@ -627,8 +627,9 @@ class OwnershipRoutineLogic // Runs `thenBody` -- the destroy half: release what the value owns, then free it -- only // when `payloadPtr` is non-null and the reference being dropped was the last one. Under // own there is no count: the one owner is giving the block up, so it goes unless it is - // immortal (a literal, a `typeof` tag). - void emitIfLastReference(mlir::Value payloadPtr, llvm::function_ref thenBody) + // immortal (a literal, a `typeof` tag). A `counted` block - a Shared handle's (own spec + // 23.2) - is counted under own too. + void emitIfLastReference(mlir::Value payloadPtr, llvm::function_ref thenBody, bool counted = false) { TypeHelper th(rewriter); auto loc = op->getLoc(); @@ -643,7 +644,8 @@ class OwnershipRoutineLogic rewriter.create(loc, ValueRange{}, continuationBlock); rewriter.setInsertionPointToEnd(decBlock); - auto wasLast = compileOptions.memoryModel == MemoryModelOwn ? emitIsMortal(payloadPtr) : emitDecRef(payloadPtr); + auto wasLast = compileOptions.memoryModel == MemoryModelOwn && !counted ? emitIsMortal(payloadPtr) + : emitDecRef(payloadPtr); rewriter.create(loc, wasLast, thenBlock, continuationBlock); rewriter.setInsertionPointToEnd(currentBlock); @@ -976,6 +978,18 @@ class OwnershipRoutineLogic return; } + // a handle (own spec 23.2) is counted in every model that tracks ownership: the last one + // releases the payload and frees the block + if (auto sharedType = dyn_cast(type)) + { + auto handle = rewriter.create(loc, ptrTy, slotPtr); + emitIfLastReference(handle, [&]() { + releaseSlot(sharedType.getElementType(), handle); + emitFreeBlock(handle); + }, /*counted=*/true); + return; + } + // an "any" box owns its own block, and its payload's type is only known through the // tag if (isa(type)) @@ -1135,9 +1149,9 @@ class OwnershipRoutineLogic auto ptrTy = th.getPtrType(); // each of these is a reference to a block of its own: string, class and object - // instances, and an "any" box - if (isa(type) || isa(type) || isa(type) || - isa(type)) + // instances, a Shared handle, and an "any" box + if (isa(type) || isa(type) || isa(type) || + isa(type) || isa(type)) { emitIncRef(rewriter.create(loc, ptrTy, slotPtr)); return; diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRPrinter.h b/tslang/include/TypeScript/MLIRLogic/MLIRPrinter.h index 0f877d05a..029498ab2 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRPrinter.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRPrinter.h @@ -437,6 +437,11 @@ class MLIRPrinter printType(out, t.getElementType()); out << ">"; }) + .template Case([&](auto t) { + out << "Shared<"; + printType(out, t.getElementType()); + out << ">"; + }) .template Case([&](auto t) { printTupleOrObjectType(out, t); }) diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRTypeCore.h b/tslang/include/TypeScript/MLIRLogic/MLIRTypeCore.h index 75b061a13..ab84ce9f4 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRTypeCore.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRTypeCore.h @@ -38,6 +38,7 @@ class MLIRTypeCore || isa(typeIn) || isa(typeIn) || isa(typeIn) + || isa(typeIn) || isa(typeIn) || isa(typeIn) || isa(typeIn) @@ -72,6 +73,7 @@ class MLIRTypeCore || isa(typeIn) || isa(typeIn) || isa(typeIn) + || isa(typeIn) || isa(typeIn) || isa(typeIn) || isa(typeIn) diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h b/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h index bb10fdbb4..c60903023 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h @@ -2118,6 +2118,71 @@ class MLIRTypeHelper return ownsHeapMemory(location, type, visiting); } + // A handle (own spec 23.2): a value counted in every model that tracks ownership. That is a + // Shared, an optional of a handle, or a union of at least one Shared whose other members + // own no heap memory: null, undefined, a number, a boolean, a literal, another Shared. + // `Shared | null` is one pointer; `Shared | number` is tagged, and its routines dispatch + // on the tag through descriptors, each Shared member's counting and the others' doing nothing. + static bool isSharedHandleType(mlir::Type type) + { + if (isa(type)) + { + return true; + } + + if (auto optionalType = dyn_cast(type)) + { + return isSharedHandleType(optionalType.getElementType()); + } + + if (auto unionType = dyn_cast(type)) + { + auto shared = false; + for (auto member : unionType.getTypes()) + { + if (isa(member)) + { + shared = true; + continue; + } + + if (ownsHeapMemoryOfUnionMember(member)) + { + return false; + } + } + + return shared; + } + + return false; + } + + // A union with a Shared member that is not a handle: another member owns a block of its own + // (`Shared | string`, `Shared | Node`), which -mm=own does not count (spec 23.2). + static bool isSharedInOwningUnionType(mlir::Type type) + { + if (auto optionalType = dyn_cast(type)) + { + return isSharedInOwningUnionType(optionalType.getElementType()); + } + + auto unionType = dyn_cast(type); + return unionType && llvm::any_of(unionType.getTypes(), [](mlir::Type member) { return isa(member); }) && + !isSharedHandleType(unionType); + } + + static bool ownsHeapMemoryOfUnionMember(mlir::Type member) + { + if (isa(member)) + { + return false; + } + + MLIRTypeHelper mth(member.getContext(), CompileOptions{}); + return mth.ownsHeapMemory(mlir::UnknownLoc::get(member.getContext()), member); + } + // Field types of a record-shaped type, empty for anything else. llvm::SmallVector getOwnershipFieldTypes(mlir::Type type) { @@ -3785,9 +3850,10 @@ class MLIRTypeHelper return false; } - // owns its own block + // owns its own block. A Shared handle (own spec 23) does too, and its block's payload + // is released by the handle's routine, so it is not a record here if (isa(type) || isa(type) || isa(type) || - isa(type) || isa(type)) + isa(type) || isa(type) || isa(type)) { return true; } diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRTypeIterator.h b/tslang/include/TypeScript/MLIRLogic/MLIRTypeIterator.h index 2bc7679ef..b90afa03a 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRTypeIterator.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRTypeIterator.h @@ -241,6 +241,11 @@ class MLIRTypeIterator return false; return true; }) + .Case([&](auto t) { + if (!iterate(t.getElementType())) + return false; + return true; + }) .Case([&](auto t) { for (auto subType : t.getFields()) { diff --git a/tslang/include/TypeScript/MLIRLogic/TypeOfOpHelper.h b/tslang/include/TypeScript/MLIRLogic/TypeOfOpHelper.h index 0527ac090..c93cf40b5 100644 --- a/tslang/include/TypeScript/MLIRLogic/TypeOfOpHelper.h +++ b/tslang/include/TypeScript/MLIRLogic/TypeOfOpHelper.h @@ -193,6 +193,13 @@ class TypeOfOpHelper return "object"; } + // A Shared handle (own spec 23.2) is what JavaScript's `typeof` calls a wrapper object. + // Its descriptor is keyed by the type, so Shared and Shared still get distinct ones. + if (isa(type)) + { + return "object"; + } + if (isa(type)) { return "symbol"; diff --git a/tslang/include/TypeScript/TypeScriptOps.td b/tslang/include/TypeScript/TypeScriptOps.td index 0da880b31..818fdf6d5 100644 --- a/tslang/include/TypeScript/TypeScriptOps.td +++ b/tslang/include/TypeScript/TypeScriptOps.td @@ -2382,6 +2382,25 @@ def TypeScript_StringCopyOp : TypeScript_Op<"StringCopy", [AllTypesMatch<["in", let results = (outs Res:$result); } +// Shared (own spec 23). `new Shared(x)`: a block of one T, which `value` moves into. +def TypeScript_SharedNewOp : TypeScript_Op<"SharedNew"> { + let arguments = (ins AnyType:$value); + let results = (outs Res:$result); +} + +// The place `s.value`: a reference to the handle's payload. It is read with ts.Load and written +// with ts.Store, as a field is. +def TypeScript_SharedValueRefOp : TypeScript_Op<"SharedValueRef"> { + let arguments = (ins TypeScript_Shared:$shared); + let results = (outs TypeScript_Ref:$result); +} + +// `Shared.count(s)`: the number of handles under rc and own, -1 under gc and none. +def TypeScript_SharedCountOp : TypeScript_Op<"SharedCount"> { + let arguments = (ins TypeScript_Shared:$shared); + let results = (outs TypeScript_Number:$result); +} + def TypeScript_StringCompareOp : TypeScript_Op<"StringCompare"> { let arguments = (ins TypeScript_String:$op1, TypeScript_String:$op2, I32Attr:$code); let results = (outs TypeScript_Boolean:$result); diff --git a/tslang/include/TypeScript/TypeScriptTypes.td b/tslang/include/TypeScript/TypeScriptTypes.td index 512e50ddc..3120cfec0 100644 --- a/tslang/include/TypeScript/TypeScriptTypes.td +++ b/tslang/include/TypeScript/TypeScriptTypes.td @@ -104,6 +104,34 @@ def TypeScript_Ref : TypeScript_Type<"Ref"> { ]; } +// Shared (own spec 23): a counted handle to a block whose payload is one T. +def TypeScript_Shared : TypeScript_Type<"Shared"> { + let mnemonic = "shared"; + + let description = [{ + Syntax: + + ``` + shared-type ::= `shared` `<` type `>` + ``` + + Examples: + + ```mlir + shared> + ``` + }]; + let parameters = (ins "Type":$elementType); + + let assemblyFormat = "`<` params `>`"; + + let builders = [ + TypeBuilderWithInferredContext<(ins "Type":$elementType), [{ + return Base::get(elementType.getContext(), elementType); + }]> + ]; +} + def TypeScript_ValueRef : TypeScript_Type<"ValueRef"> { let mnemonic = "value_ref"; diff --git a/tslang/lib/TypeScript/LowerToAffineLoops.cpp b/tslang/lib/TypeScript/LowerToAffineLoops.cpp index 227dcf7de..6e43eb645 100644 --- a/tslang/lib/TypeScript/LowerToAffineLoops.cpp +++ b/tslang/lib/TypeScript/LowerToAffineLoops.cpp @@ -2657,7 +2657,7 @@ void AddTsAffineLegalOps(ConversionTarget &target) mlir_ts::ElementRefOp, mlir_ts::PointerOffsetRefOp, mlir_ts::FuncOp, mlir_ts::GlobalOp, mlir_ts::GlobalResultOp, mlir_ts::DefaultOp, mlir_ts::HasValueOp, mlir_ts::ValueOp, mlir_ts::ValueOrDefaultOp, mlir_ts::NullOp, mlir_ts::ParseFloatOp, mlir_ts::ParseIntOp, mlir_ts::IsNaNOp, mlir_ts::PrintOp, mlir_ts::ConvertFOp, mlir_ts::SizeOfOp, mlir_ts::TypeDescriptorOp, mlir_ts::RetainOp, mlir_ts::ReleaseOp, mlir_ts::RetainSlotOp, mlir_ts::ReleaseSlotOp, mlir_ts::RetainCellOp, mlir_ts::ReleaseCellOp, mlir_ts::StoreOp, mlir_ts::SymbolRefOp, mlir_ts::LengthOfOp, mlir_ts::SetLengthOfOp, - mlir_ts::StringLengthOp, mlir_ts::StringResizeOp, mlir_ts::StringConcatOp, mlir_ts::StringCopyOp, mlir_ts::StringCompareOp, mlir_ts::AnyCompareOp, + mlir_ts::StringLengthOp, mlir_ts::StringResizeOp, mlir_ts::StringConcatOp, mlir_ts::StringCopyOp, mlir_ts::SharedNewOp, mlir_ts::SharedValueRefOp, mlir_ts::SharedCountOp, mlir_ts::StringCompareOp, mlir_ts::AnyCompareOp, mlir_ts::LoadOp, mlir_ts::LoadSaveOp, mlir_ts::NewOp, mlir_ts::CreateTupleOp, mlir_ts::DeconstructTupleOp, mlir_ts::CreateArrayOp, mlir_ts::NewEmptyArrayOp, mlir_ts::NewArrayOp, mlir_ts::DeleteOp, mlir_ts::PropertyRefOp, mlir_ts::InsertPropertyOp, mlir_ts::ExtractPropertyOp, mlir_ts::LogicalBinaryOp, mlir_ts::UndefOp, mlir_ts::VariableOp, mlir_ts::DebugVariableOp, mlir_ts::AllocaOp, diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index d3c0273e0..a01c87f55 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -400,10 +400,13 @@ class TypeDescriptorOpLowering : public TsLlvmPattern auto releaseRoutineName = orl.getOrCreateReleaseRoutine(descriptorType); // Under own nothing retains: the descriptor's retain slot is read only from inside // retain routines (retainViaDescriptor), none of which can run, and building one would - // bring `__tslang_inc_ref` into a model that promises it is never referenced. - auto retainRoutineName = tsLlvmContext->compileOptions.memoryModel == MemoryModelOwn - ? std::string() - : orl.getOrCreateRetainRoutine(descriptorType); + // bring `__tslang_inc_ref` into a model that promises it is never referenced. A Shared + // handle is the exception: own counts it (spec 23.2), so its retain is kept. + auto retainRoutineName = + tsLlvmContext->compileOptions.memoryModel == MemoryModelOwn && + !MLIRTypeHelper::isSharedHandleType(descriptorType) + ? std::string() + : orl.getOrCreateRetainRoutine(descriptorType); rewriter.replaceOp(op, ch.getOrCreateTypeDescriptorName(descriptorType, name, TypeOfOpHelper::typeKindFromName(name), @@ -425,14 +428,17 @@ class RetainOpLowering : public TsLlvmPattern LogicalResult matchAndRewrite(mlir_ts::RetainOp op, Adaptor transformed, ConversionPatternRewriter &rewriter) const final { + // A handle is counted under own too, and inference leaves its retains (spec 23.2). + auto counted = MLIRTypeHelper::isSharedHandleType(op.getReference().getType()); + // Under own, ownership inference erased every retain it could prove, and reported the // rest; one that reaches here was neither, and erasing it would ship a double free. - if (tsLlvmContext->compileOptions.memoryModel == MemoryModelOwn) + if (tsLlvmContext->compileOptions.memoryModel == MemoryModelOwn && !counted) { return op.emitError("ownership inference left a retain behind"); } - if (tsLlvmContext->compileOptions.isRefCounted()) + if (tsLlvmContext->compileOptions.isRefCounted() || (tsLlvmContext->compileOptions.tracksOwnership() && counted)) { OwnershipRoutineLogic orl(op, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); orl.emitRetainValue(op.getReference().getType(), transformed.getReference()); @@ -472,14 +478,19 @@ class RetainSlotOpLowering : public TsLlvmPattern LogicalResult matchAndRewrite(mlir_ts::RetainSlotOp op, Adaptor transformed, ConversionPatternRewriter &rewriter) const final { + // A slot holding a handle is counted under own too, and inference leaves its retains + // (spec 23.2). + auto counted = + MLIRTypeHelper::isSharedHandleType(cast(op.getSlot().getType()).getElementType()); + // Under own, ownership inference erased every retain it could prove, and reported the // rest; one that reaches here was neither, and erasing it would ship a double free. - if (tsLlvmContext->compileOptions.memoryModel == MemoryModelOwn) + if (tsLlvmContext->compileOptions.memoryModel == MemoryModelOwn && !counted) { return op.emitError("ownership inference left a retain behind"); } - if (tsLlvmContext->compileOptions.isRefCounted()) + if (tsLlvmContext->compileOptions.isRefCounted() || (tsLlvmContext->compileOptions.tracksOwnership() && counted)) { OwnershipRoutineLogic orl(op, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); orl.emitRetainSlot(cast(op.getSlot().getType()).getElementType(), transformed.getSlot()); @@ -949,6 +960,68 @@ class StringCopyOpLowering : public TsLlvmPattern } }; +// `new Shared(x)` (own spec 23): a heap block with the header every block has (a count under rc +// and own, born 0 like any block), whose payload is the value. +class SharedNewOpLowering : public TsLlvmPattern +{ + public: + using TsLlvmPattern::TsLlvmPattern; + + LogicalResult matchAndRewrite(mlir_ts::SharedNewOp op, Adaptor transformed, + ConversionPatternRewriter &rewriter) const final + { + LLVMCodeHelper ch(op, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); + auto elementType = mlir::cast(op.getType()).getElementType(); + auto handle = ch.MemoryAlloc(elementType, MemoryAllocSet::Zero); + rewriter.create(op->getLoc(), transformed.getValue(), handle); + rewriter.replaceOp(op, ValueRange{handle}); + return success(); + } +}; + +// `s.value`: the payload starts at the handle. +class SharedValueRefOpLowering : public TsLlvmPattern +{ + public: + using TsLlvmPattern::TsLlvmPattern; + + LogicalResult matchAndRewrite(mlir_ts::SharedValueRefOp op, Adaptor transformed, + ConversionPatternRewriter &rewriter) const final + { + rewriter.replaceOp(op, ValueRange{transformed.getShared()}); + return success(); + } +}; + +// `Shared.count(s)`: the header word under rc and own; gc and none never write it. +class SharedCountOpLowering : public TsLlvmPattern +{ + public: + using TsLlvmPattern::TsLlvmPattern; + + LogicalResult matchAndRewrite(mlir_ts::SharedCountOp op, Adaptor transformed, + ConversionPatternRewriter &rewriter) const final + { + auto loc = op->getLoc(); + TypeHelper th(rewriter); + TypeConverterHelper tch(getTypeConverter()); + LLVMCodeHelper ch(op, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); + + auto numberType = tch.convertType(op.getType()); + if (!tsLlvmContext->compileOptions.tracksOwnership()) + { + rewriter.replaceOpWithNewOp(op, numberType, rewriter.getFloatAttr(numberType, -1.0)); + return success(); + } + + auto llvmIndexType = tch.convertType(th.getIndexType()); + auto blockPtr = ch.getBlockPtrFromPayloadPtr(loc, transformed.getShared(), llvmIndexType); + auto count = rewriter.create(loc, llvmIndexType, blockPtr); + rewriter.replaceOpWithNewOp(op, numberType, count); + return success(); + } +}; + class StringCompareOpLowering : public TsLlvmPattern { public: @@ -2611,9 +2684,12 @@ struct VariableOpLowering : public TsLlvmPattern if (!isCaptured) { auto tsStorageType = referenceType.getElementType(); - if (isa(tsStorageType) || isa(tsStorageType) || - isa(tsStorageType) || isa(tsStorageType) || - isa(tsStorageType)) + if (isa(tsStorageType) || + isa(tsStorageType) || + isa(tsStorageType) || + isa(tsStorageType) || + isa(tsStorageType) || + isa(tsStorageType)) { TypeHelper th(rewriter); @@ -2681,7 +2757,10 @@ struct VariableOpLowering : public TsLlvmPattern // Not under own, which has no count to take: such a cell borrows the value, and its // release frees the cell alone (OWN_CELL_BORROWS_ATTR_NAME, which ownership inference // sets, or rejects the program). - if (isCaptured && tsLlvmContext->compileOptions.isRefCounted() && + // A handle is counted under own too (spec 23.2). + auto counted = MLIRTypeHelper::isSharedHandleType(referenceType.getElementType()); + if (isCaptured && + (tsLlvmContext->compileOptions.isRefCounted() || (tsLlvmContext->compileOptions.tracksOwnership() && counted)) && !varOp->hasAttr(CAPTURE_BOX_ATTR_NAME) && !varOp->hasAttr(OWNED_LOCAL_ATTR_NAME)) { OwnershipRoutineLogic orl(varOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); @@ -7138,6 +7217,11 @@ static void populateTypeScriptConversionPatterns(LLVMTypeConverter &converter, m return LLVM::LLVMPointerType::get(m.getContext()); }); + // Shared (own spec 23): a pointer to the handle's payload + converter.addConversion([&](mlir_ts::SharedType type) { + return LLVM::LLVMPointerType::get(m.getContext()); + }); + converter.addConversion([&](mlir_ts::ValueRefType type) { return LLVM::LLVMPointerType::get(m.getContext()); }); @@ -8029,6 +8113,7 @@ void TypeScriptToLLVMLoweringPass::runOnOperation() ArrayPopOpLowering, ArrayUnshiftOpLowering, ArrayShiftOpLowering, ArraySpliceOpLowering, ArrayViewOpLowering, DeleteOpLowering, ParseFloatOpLowering, ParseIntOpLowering, IsNaNOpLowering, PrintOpLowering, ConvertFOpLowering, StoreOpLowering, SizeOfOpLowering, TypeDescriptorOpLowering, RetainOpLowering, ReleaseOpLowering, RetainSlotOpLowering, ReleaseSlotOpLowering, RetainCellOpLowering, ReleaseCellOpLowering, InsertPropertyOpLowering, LengthOfOpLowering, SetLengthOfOpLowering, StringLengthOpLowering, StringResizeOpLowering, StringConcatOpLowering, StringCopyOpLowering, + SharedNewOpLowering, SharedValueRefOpLowering, SharedCountOpLowering, StringCompareOpLowering, AnyCompareOpLowering, CharToStringOpLowering, UndefOpLowering, CopyStructOpLowering, MemoryCopyOpLowering, MemoryMoveOpLowering, LoadSaveValueLowering, ThrowUnwindOpLowering, ThrowCallOpLowering, VariableOpLowering, DebugVariableOpLowering, AllocaOpLowering, InvokeOpLowering, InvokeHybridOpLowering, VirtualSymbolRefOpLowering, ThisVirtualSymbolRefOpLowering, InterfaceSymbolRefOpLowering, diff --git a/tslang/lib/TypeScript/MLIRGenAccessCall.cpp b/tslang/lib/TypeScript/MLIRGenAccessCall.cpp index 16f4c69b1..479890491 100644 --- a/tslang/lib/TypeScript/MLIRGenAccessCall.cpp +++ b/tslang/lib/TypeScript/MLIRGenAccessCall.cpp @@ -305,6 +305,20 @@ namespace mlirgen return mlir::Value(); }) .Case([&](auto refType) { return cl.Ref(refType); }) + // `s.value` (own spec 23.1): a ts.Load of the place ts.SharedValueRef(s), so `s.value = x` + // stores through that place the way a field store does (mlirGenSaveLogicOneItem) + .Case([&](auto sharedType) -> mlir::Value { + // any other member: an extension function, else "Can't resolve property" below + if (name != "value") + { + return mlir::Value(); + } + + auto elementType = sharedType.getElementType(); + auto valueRef = builder.create( + location, mlir_ts::RefType::get(elementType), objectValue); + return builder.create(location, elementType, valueRef); + }) .Case([&](auto objectType) { if (auto value = cl.Object(objectType)) { diff --git a/tslang/lib/TypeScript/MLIRGenCast.cpp b/tslang/lib/TypeScript/MLIRGenCast.cpp index 2ecb6e0cd..2a47e6afd 100644 --- a/tslang/lib/TypeScript/MLIRGenCast.cpp +++ b/tslang/lib/TypeScript/MLIRGenCast.cpp @@ -1541,7 +1541,9 @@ namespace mlirgen || isa(type) || isa(type) || isa(type) - || isa(type)) + || isa(type) + // a Shared handle (own spec 23.1) is checked by its `typeof` name, "object" + || isa(type)) { return castPrimitiveTypeFromAny(location, type, value, genContext); } @@ -1905,6 +1907,10 @@ namespace mlirgen .Case([&](auto _) { typeOfs["function"] = true; }) .Case([&](auto classType_) { typeOfs["class"] = true; classInstances.push_back(classType_); }) .Case([&](auto _) { typeOfs["interface"] = true; }) + // A handle's tag names it "object" (TypeOfOpHelper), and there is no `instanceof` for a + // handle, so the name is all that is checked: an `any` holding a Shared passes for a + // Shared, as would any other value tagged "object" (own spec 23.7). + .Case([&](auto _) { typeOfs["object"] = true; }) // TODO: we can't use null type here and undefined otherwise code will be cycling // due to issue with TypeOf == 'null' as it should denounce UnionType into Single Type // review code to use null in "TypeGuard" diff --git a/tslang/lib/TypeScript/MLIRGenExpressions.cpp b/tslang/lib/TypeScript/MLIRGenExpressions.cpp index dcde6624d..a5af69853 100644 --- a/tslang/lib/TypeScript/MLIRGenExpressions.cpp +++ b/tslang/lib/TypeScript/MLIRGenExpressions.cpp @@ -1000,6 +1000,20 @@ namespace mlirgen auto callExpr = callExpression->expression.as(); + // `Shared.count(s)` (own spec 23.1), unless the program declares its own `Shared`, which + // wins as it does in type position and in `new`. + if (callExpr == SyntaxKind::PropertyAccessExpression) + { + auto access = callExpr.as(); + if (access->expression == SyntaxKind::Identifier + && MLIRHelper::getName(access->expression.as()) == "Shared" + && MLIRHelper::getName(access->name) == "count" + && !resolveIdentifier(location, "Shared", genContext)) + { + return mlirGenSharedCount(location, callExpression, genContext); + } + } + auto result = mlirGen(callExpr, genContext); // in case of detecting value for recursive calls we need to ignore failed calls // last condition we need to reduce posobilities to ignore legitimate failure @@ -1071,6 +1085,53 @@ namespace mlirgen return mlirGenCallExpression(location, funcResult, callExpression->typeArguments, operands, genContext); } + // `Shared.count(s)` (own spec 23.1): the number of handles under rc and own, -1 under gc and none. + ValueOrLogicalResult MLIRGenImpl::mlirGenSharedCount(mlir::Location location, CallExpression callExpression, + const GenContext &genContext) + { + if (callExpression->arguments.size() != 1) + { + emitError(location, "Shared.count(handle) takes one argument"); + return mlir::failure(); + } + + auto result = mlirGen(callExpression->arguments.front(), genContext); + EXIT_IF_FAILED_OR_NO_VALUE(result) + auto handle = V(result); + // `Shared | null` narrowed by `if (s)`, which keeps the union's type: unwrapped to the + // handle, as `.value` collapses a tag-free union (mlirGenPropertyAccessExpressionBaseLogic) + if (MLIRTypeHelper::isSharedHandleType(handle.getType()) && !isa(handle.getType())) + { + auto handleType = handle.getType(); + if (auto optionalType = dyn_cast(handleType)) + { + handleType = optionalType.getElementType(); + } + + mlir::Type baseType; + if (auto unionType = dyn_cast(handleType)) + { + if (!mth.isUnionTypeNeedsTag(location, unionType, baseType)) + { + handleType = baseType; + } + } + + if (isa(handleType)) + { + CAST(handle, location, handleType, handle, genContext); + } + } + + if (!isa(handle.getType())) + { + emitError(location, "Shared.count takes a Shared, not ") << to_print(handle.getType()); + return mlir::failure(); + } + + return V(builder.create(location, getNumberType(), handle)); + } + ValueOrLogicalResult MLIRGenImpl::mlirGen(NewExpression newExpression, const GenContext &genContext) { auto location = loc(newExpression); @@ -1086,6 +1147,11 @@ namespace mlirgen { // TODO: review it, seems it should be resolved earlier auto name = MLIRHelper::getName(typeExpression.as()); + if (name == "Shared") + { + return mlirGenNewShared(location, newExpression, genContext); + } + type = findEmbeddedType(location, name, newExpression->typeArguments, genContext); if (type) { @@ -1202,6 +1268,58 @@ namespace mlirgen return NewClassInstance(location, value, newExpression->arguments, newExpression->typeArguments, suppressConstructorCall, genContext); } + // `new Shared(x)` (own spec 23): T is the type argument, else the receiver's, else x's. + ValueOrLogicalResult MLIRGenImpl::mlirGenNewShared(mlir::Location location, NewExpression newExpression, + const GenContext &genContext) + { + if (newExpression->arguments.size() != 1) + { + emitError(location, "new Shared(value) takes one argument"); + return mlir::failure(); + } + + if (newExpression->typeArguments.size() > 1) + { + emitError(location, "Shared takes one type argument"); + return mlir::failure(); + } + + mlir::Type elementType; + if (newExpression->typeArguments.size() == 1) + { + elementType = getType(newExpression->typeArguments.front(), genContext); + if (!elementType) + { + return mlir::failure(); + } + } + else if (auto receiver = mlir::dyn_cast_or_null(genContext.receiverType)) + { + elementType = receiver.getElementType(); + } + + // the value is given T as its receiver, not the handle's type + GenContext valueGenContext(genContext); + valueGenContext.clearReceiverTypes(); + valueGenContext.receiverType = elementType; + + auto result = mlirGen(newExpression->arguments.front(), valueGenContext); + EXIT_IF_FAILED_OR_NO_VALUE(result) + auto value = V(result); + + if (!elementType) + { + elementType = mth.wideStorageType(value.getType()); + } + + CAST_A(stored, location, elementType, value, genContext); + // the block owns what it holds, as an array literal owns its elements + mlirGenRetainCaptured(location, mlir::ValueRange{stored}); + auto handle = builder.create(location, mlir_ts::SharedType::get(elementType), stored); + markFreshBlockOwned(location, handle); + return V(handle); + } + mlir::LogicalResult MLIRGenImpl::mlirGen(DeleteExpression deleteExpression, const GenContext &genContext) { diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 30bd02dc3..3d8f95dcd 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -1055,17 +1055,17 @@ class MLIRGenImpl } } - // Gives a freshly built string, or a freshly made `any` box, the same standing as every other - // producer of a new heap value: the retain makes the reference real, and the mark says a - // receiver may take it over rather than adding one of its own - which is also what lets §9.30 - // give it back where nothing receives it at all. + // Gives a freshly built string, a freshly made `any` box, or a new Shared handle (own spec + // 23), the same standing as every other producer of a new heap value: the retain makes the + // reference real, and the mark says a receiver may take it over rather than adding one of its + // own - which is also what lets §9.30 give it back where nothing receives it at all. // // Both halves are needed together, and the retain is what makes this safe to be generous // with: a value wrongly counted as fresh gains a reference and a release for it, which is // balanced, where a mark on its own would hand a receiver a reference nobody took. void markFreshBlockOwned(mlir::Location location, mlir::Value value) { - if (!value || !isa(value.getType())) + if (!value || !isa(value.getType())) { return; } @@ -1092,7 +1092,19 @@ class MLIRGenImpl return isOwnedLocalSlot(reference) || isCapturedVariableCell(reference) || isCapturedCellSlot(reference) || isOwnedGlobalSlot(location, reference) || isOwnedFieldSlot(location, reference) || isOwnedElementSlot(location, reference) || - isOwnedInterfaceFieldSlot(location, reference); + isOwnedInterfaceFieldSlot(location, reference) || isOwnedSharedValueSlot(location, reference); + } + + // `s.value` (own spec 23): the payload is owned by the handle's block, as a field by its object + bool isOwnedSharedValueSlot(mlir::Location location, mlir::Value reference) + { + if (!reference.getDefiningOp()) + { + return false; + } + + auto refType = mlir::dyn_cast(reference.getType()); + return refType && mth.ownsHeapMemory(location, refType.getElementType()); } // A field reached through an interface: it belongs to the object behind the interface, which releases @@ -1482,9 +1494,15 @@ class MLIRGenImpl // aliases the storage it was read from, so it keeps that, and a narrowed view // (addSafeCastStatement, a SafeCastOp) is the narrowed variable seen through // another type, not an array of its own. + // + // A `Shared` handle (own spec 23.1) as well, bare or in a union or optional + // (`Shared | null`): `const b = a` is a second handle, and counts as one. A folded + // name would be `a` itself, with no reference of its own, so `a = c` would free the + // block `b` still reads, and `Shared.count` would miss it. MLIRTypeHelper mth(builder.getContext(), compileOptions); needsIdentityStorage = mth.hasBoundMethodField(type) || isa(type) - || (isa(type) && !(varClass == VariableType::ConstRef) + || ((isa(type) || MLIRTypeHelper::isSharedHandleType(type)) + && !(varClass == VariableType::ConstRef) && !(initial && initial.getDefiningOp())); if (needsIdentityStorage) { @@ -1976,10 +1994,13 @@ class MLIRGenImpl variableDeclarationInfo.initial = builder.create(location, variableDeclarationInfo.type); } + // A handle global (own spec 23.1) takes over the reference its initializer carries, through + // a view of its type as well (`let g: Shared | null = mk()`): OwnedReturnConsumptionPass + // claims it (claimHandleGlobalInitializers), once a call's result is known to carry one. builder.create(location, mlir::ValueRange{variableDeclarationInfo.initial}); return mlir::success(); - } + } mlir::LogicalResult createGlobalVariableUndefinedInitialization(mlir::Location location, mlir_ts::GlobalOp globalOp, struct VariableDeclarationInfo &variableDeclarationInfo) { @@ -8661,6 +8682,10 @@ class MLIRGenImpl ValueOrLogicalResult mlirGen(NewExpression newExpression, const GenContext &genContext); + ValueOrLogicalResult mlirGenNewShared(mlir::Location location, NewExpression newExpression, const GenContext &genContext); + + ValueOrLogicalResult mlirGenSharedCount(mlir::Location location, CallExpression callExpression, const GenContext &genContext); + mlir::LogicalResult mlirGen(DeleteExpression deleteExpression, const GenContext &genContext); ValueOrLogicalResult mlirGen(VoidExpression voidExpression, const GenContext &genContext); diff --git a/tslang/lib/TypeScript/MLIRGenTypes.cpp b/tslang/lib/TypeScript/MLIRGenTypes.cpp index d5febf363..e178604f3 100644 --- a/tslang/lib/TypeScript/MLIRGenTypes.cpp +++ b/tslang/lib/TypeScript/MLIRGenTypes.cpp @@ -919,6 +919,7 @@ namespace mlirgen {"Reference", true }, // to support dll import {"Ref", true }, // alias of Reference {"BoxedObject", true }, // an object held by reference, see MLIRPrinter::printBoxedObjectTypes + {"Shared", true }, // a counted handle, own spec 23 {"Readonly", true }, {"Partial", true }, {"Required", true }, @@ -1008,6 +1009,7 @@ namespace mlirgen {"Reference", true }, // to support dll import {"Ref", true }, // alias of Reference {"BoxedObject", true }, // an object held by reference, see MLIRPrinter::printBoxedObjectTypes + {"Shared", true }, // a counted handle, own spec 23 {"ThisType", true }, //{"Array", true } }; @@ -1161,7 +1163,7 @@ namespace mlirgen { enum class EmbeddedType { - None, TypeOf, Reference, BoxedObject, FirstTypeArgument, NonNullable, Array, ReadonlyArray, ReturnType, + None, TypeOf, Reference, BoxedObject, Shared, FirstTypeArgument, NonNullable, Array, ReadonlyArray, ReturnType, Parameters, ThisParameterType, OmitThisParameter, Uppercase, Lowercase, Capitalize, Uncapitalize }; @@ -1169,6 +1171,7 @@ namespace mlirgen .Case("TypeOf", EmbeddedType::TypeOf) .Cases("Reference", "Ref", EmbeddedType::Reference) .Case("BoxedObject", EmbeddedType::BoxedObject) + .Case("Shared", EmbeddedType::Shared) .Cases("Readonly", "Partial", "Required", "ThisType", EmbeddedType::FirstTypeArgument) .Case("NonNullable", EmbeddedType::NonNullable) #ifdef ARRAY_TYPE_AS_ARRAY_CLASS @@ -1204,6 +1207,8 @@ namespace mlirgen return mlir_ts::RefType::get(type); case EmbeddedType::BoxedObject: return getObjectType(type); + case EmbeddedType::Shared: + return mlir_ts::SharedType::get(type); case EmbeddedType::FirstTypeArgument: return type; case EmbeddedType::NonNullable: @@ -1260,13 +1265,14 @@ namespace mlirgen { enum class EmbeddedType { - None, TypeOf, Reference, BoxedObject, ThisType, Array + None, TypeOf, Reference, BoxedObject, Shared, ThisType, Array }; auto kind = llvm::StringSwitch(name) .Case("TypeOf", EmbeddedType::TypeOf) .Cases("Reference", "Ref", EmbeddedType::Reference) .Case("BoxedObject", EmbeddedType::BoxedObject) + .Case("Shared", EmbeddedType::Shared) .Case("ThisType", EmbeddedType::ThisType) #ifdef ARRAY_TYPE_AS_ARRAY_CLASS .Case("Array", EmbeddedType::Array) @@ -1287,6 +1293,9 @@ namespace mlirgen return mlir_ts::RefType::get(type); case EmbeddedType::BoxedObject: return getObjectType(type); + case EmbeddedType::Shared: + // the type argument is not checked above in this variant + return type ? mlir_ts::SharedType::get(type) : mlir::Type(); case EmbeddedType::ThisType: return type; #ifdef ARRAY_TYPE_AS_ARRAY_CLASS diff --git a/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp b/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp index aebd26023..908fe00b6 100644 --- a/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp +++ b/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp @@ -123,6 +123,7 @@ class OwnedReturnConsumptionPass // back even when no function here is classified as returning owned. if (returnsOwned.empty()) { + claimHandleGlobalInitializers(module); releaseDiscardedTemporaries(mth, module); return; } @@ -235,9 +236,89 @@ class OwnedReturnConsumptionPass consumeConditionalResults(mth, module); + claimHandleGlobalInitializers(module); + releaseDiscardedTemporaries(mth, module); } + // A handle global (own spec 23.1) is born holding the reference its initializer carries. + // + // The initializer region ends in `ts.GlobalResult`, a terminator, so a value given to it + // straight is never released as a discarded temporary (§9.30): `let g: Shared = mk()` + // keeps the call's +1. A value given to it through a view of the global's type is: `ts.Cast` + // to `Shared | null`, `ts.OptionalValue` to `Shared | undefined`, `ts.CreateUnionInstance` + // - the view is the one use the producer has, and it is releasable, so the producer's + // reference was given back at the end of the region and the global was born at count 0. + // + // The producer is anything that carries an owned reference by now: `new Shared(..)`, marked + // where MLIRGen builds it, or a call, marked above once its callee is classified - which is + // why MLIRGen alone cannot see it. A handle read out of a field or an element carries none, + // and gets a retain of its own. Keyed on the global's type: no other global changes. A read of another + // global is left alone (#446: it reads null today in every model). + void claimHandleGlobalInitializers(mlir::ModuleOp module) + { + module.walk([&](mlir_ts::GlobalOp globalOp) { + if (!MLIRTypeHelper::isSharedHandleType(globalOp.getType())) + { + return; + } + + auto *block = globalOp.getInitializerBlock(); + if (!block || block->empty()) + { + return; + } + + auto resultOp = mlir::dyn_cast(block->getTerminator()); + if (!resultOp || resultOp->getNumOperands() != 1) + { + return; + } + + auto value = resultOp->getOperand(0); + while (auto *definingOp = value.getDefiningOp()) + { + if (auto castOp = mlir::dyn_cast(definingOp)) + { + value = castOp.getIn(); + } + else if (auto optionalValueOp = mlir::dyn_cast(definingOp)) + { + value = optionalValueOp.getIn(); + } + else if (auto createUnionInstanceOp = mlir::dyn_cast(definingOp)) + { + value = createUnionInstanceOp.getIn(); + } + else + { + break; + } + } + + if (producesOwnedResult(value)) + { + value.getDefiningOp()->setAttr(OWNED_RESULT_CONSUMED_ATTR_NAME, mlir::UnitAttr::get(&getContext())); + return; + } + + // A field or element read (`holder.child`, `new P().child`, `mkP().child`, + // `mkArr()[0]`) carries no reference: the object or array keeps its count, and a + // temporary one gives it back at the end of the region. The global takes a count of its + // own, retained right where it is read - before the temporary's release. Erased under + // gc and none. + auto loadOp = value.getDefiningOp(); + auto *place = loadOp ? loadOp.getReference().getDefiningOp() : nullptr; + if (place && MLIRTypeHelper::isSharedHandleType(value.getType()) && + mlir::isa(place)) + { + mlir::OpBuilder builder(loadOp->getContext()); + builder.setInsertionPointAfter(loadOp); + builder.create(loadOp->getLoc(), value); + } + }); + } + // A conditional expression whose branches disagree about ownership. // // `cond ? a : f()` builds a `ts.If` with a result, and the two branches hand back references diff --git a/tslang/lib/TypeScript/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h index 7d1c7c8ea..67c917cea 100644 --- a/tslang/lib/TypeScript/OwnershipFacts.h +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -7,6 +7,7 @@ #include "mlir/Interfaces/ControlFlowInterfaces.h" +#include "llvm/ADT/SmallPtrSet.h" #include "llvm/ADT/SmallVector.h" #include "llvm/ADT/STLFunctionalExtras.h" @@ -327,9 +328,34 @@ 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). inline bool isPlace(mlir::Value ref) { - return mlir::isa_and_nonnull(ref.getDefiningOp()); + return mlir::isa_and_nonnull( + ref.getDefiningOp()); +} + +// A handle (spec 23.2): counted in every model that tracks ownership, and left alone by own. +inline bool isHandle(mlir::Value value) +{ + return value && MLIRTypeHelper::isSharedHandleType(value.getType()); +} + +// A retain or release of a handle, or of a slot holding one: rc's, which own leaves in place. +inline bool touchesHandle(mlir::Operation *op) +{ + if (mlir::isa(op)) + { + return isHandle(op->getOperand(0)); + } + + if (mlir::isa(op)) + { + auto slotType = mlir::dyn_cast(op->getOperand(0).getType()); + return slotType && MLIRTypeHelper::isSharedHandleType(slotType.getElementType()); + } + + return false; } // The values the branches into its block pass for `argument`, each seen through its views. @@ -373,6 +399,49 @@ inline bool mergedInto(mlir::BlockArgument argument, llvm::SmallVectorImpl work{ref}; + llvm::SmallPtrSet seen; + while (!work.empty()) + { + auto value = rootOf(work.pop_back_val()); + if (!seen.insert(value).second) + { + continue; + } + + if (value.getDefiningOp()) + { + return true; + } + + if (auto propertyRefOp = value.getDefiningOp()) + { + work.push_back(propertyRefOp.getObjectRef()); + } + else if (auto elementRefOp = value.getDefiningOp()) + { + work.push_back(elementRefOp.getArray()); + } + else if (auto loadOp = value.getDefiningOp(); loadOp && isPlace(loadOp.getReference())) + { + work.push_back(loadOp.getReference()); + } + else if (auto argument = mlir::dyn_cast(value)) + { + llvm::SmallVector merged; + if (mergedInto(argument, merged)) + { + work.append(merged.begin(), merged.end()); + } + } + } + + return false; +} + // The load of an owning local that `value` was read from, through its views (a class widened // to a union with null, an upcast, a union made from it). None when the value is anything // else - a parameter, a field, a boxing cast - which phase 1 does not move out of. diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index c7d320809..c2055a990 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -138,6 +138,19 @@ class OwnershipInferencePass // A view of a block is that block: the candidate is always the root. f.walk([&](mlir::Operation *op) { + // A handle put into a union with a member that owns a block of its own: counting the + // union would count that member's single-owner block, and not counting it would lose + // the handle's count (spec 23.2). + reportHandleInOwningUnion(op); + + // A handle's retains and releases are rc's, counted, and nobody's to decide (spec 23.2). + // An overwrite of a field or an element that holds one is still a drop: the old handle + // may be the last, and its payload goes with it. + if (touchesHandle(op) && !mlir::isa(op)) + { + return; + } + if (auto retainOp = mlir::dyn_cast(op)) { retains.push_back(op); @@ -153,7 +166,7 @@ class OwnershipInferencePass } else if (auto varOp = mlir::dyn_cast(op)) { - if (varOp.getInitializer() && isOwningVariable(varOp)) + if (varOp.getInitializer() && isOwningVariable(varOp) && !isHandle(varOp.getInitializer())) { candidates.insert(rootOf(varOp.getInitializer())); } @@ -191,6 +204,14 @@ class OwnershipInferencePass drops.push_back(op); } } + else if (mlir::isa(op)) + { + // through a handle, it may move the elements another handle's borrow reads (spec 23.3) + if (reachesSharedValue(op->getOperand(0))) + { + drops.push_back(op); + } + } else if (mlir::isa(op) || isCall(op)) @@ -209,7 +230,7 @@ class OwnershipInferencePass for (auto result : op->getResults()) { - if (!isView(op) && isFresh(result) && ownsHeap(result)) + if (!isView(op) && isFresh(result) && ownsHeap(result) && !isHandle(result)) { candidates.insert(result); } @@ -242,6 +263,11 @@ class OwnershipInferencePass llvm::MapVector> paramReceivers; for (auto *op : retains) { + if (touchesHandle(op)) + { + continue; + } + if (captureRetains.contains(op)) { continue; // a borrowing box's copy of a value: decideClosures decides it @@ -340,8 +366,9 @@ class OwnershipInferencePass auto args = callArgs(call); for (auto index : ownedParams(call)) { + // a handle given to a callee that keeps it is a counted copy (spec 23.2) if (static_cast(index) >= args.size() || holdsNoBlock(args[index]) || - isImmortalLiteral(args[index])) + isImmortalLiteral(args[index]) || isHandle(args[index])) { continue; } @@ -384,6 +411,8 @@ class OwnershipInferencePass return failures == 0; } + // a handle's retains and releases are rc's, and stay (spec 23.2) + toErase.remove_if([](mlir::Operation *op) { return touchesHandle(op); }); for (auto *op : toErase) { op->erase(); @@ -1506,9 +1535,10 @@ class OwnershipInferencePass auto root = rootOf(value); captured = ownerName(root); Chain chain; - if (holdsNoBlock(value) || placeReadOf(root) || borrowedParam(root) >= 0) + if (holdsNoBlock(value) || placeReadOf(root) || borrowedParam(root) >= 0 || isHandle(root)) { - // nothing to own, a read the read's check bounds, or a parameter + // nothing to own, a read the read's check bounds, a parameter, or a handle, + // whose copy in the box is counted (spec 23.2) } else if (auto slotLoad = slotLoadOf(root)) { @@ -1624,8 +1654,10 @@ class OwnershipInferencePass auto name = varName(varOp); MLIRTypeHelper mth(&getContext(), CompileOptions{}); - if (!isOwningVariable(varOp) && !closure.borrowsValues && - mth.ownsHeapMemory(varOp.getLoc(), mlir::cast(value.getType()).getElementType())) + auto elementType = mlir::cast(value.getType()).getElementType(); + // a handle's cell holds a count of its own, whoever's the value was (spec 23.2) + if (!isOwningVariable(varOp) && !closure.borrowsValues && mth.ownsHeapMemory(varOp.getLoc(), elementType) && + !MLIRTypeHelper::isSharedHandleType(elementType)) { reportEscapingCellNotOwned(move, closure.escape, "its value is the caller's"); ok = false; @@ -1709,7 +1741,15 @@ class OwnershipInferencePass } MLIRTypeHelper mth(&getContext(), CompileOptions{}); - if (!mth.ownsHeapMemory(varOp.getLoc(), mlir::cast(cell.getType()).getElementType())) + auto elementType = mlir::cast(cell.getType()).getElementType(); + if (!mth.ownsHeapMemory(varOp.getLoc(), elementType)) + { + return; + } + + // A handle's cell holds a count of its own, taken where it is made (spec 23.2): its + // release gives that back, as under rc, and an assignment is a counted copy. + if (MLIRTypeHelper::isSharedHandleType(elementType)) { return; } @@ -2171,7 +2211,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}) + Keeping keeping = Keeping{nullptr, false}, bool countedHandle = false) { struct Pending { @@ -2221,6 +2261,13 @@ class OwnershipInferencePass if (pending.borrowed) { + // a handle read that rc counts (spec 23.3): a return or an `any` box rc + // retained for is a counted copy of its own; passesOn would hand them on + if (countedHandle && isCountedHandOff(user, pending.value)) + { + continue; + } + llvm::SmallVector same; llvm::SmallVector renewedAt; if (passesOn(use, same, &renewedAt)) @@ -2236,6 +2283,14 @@ class OwnershipInferencePass continue; } + // a handle read that rc counts (spec 23.3): its retain, and what takes the value + // it retains for, are a counted copy of their own, as a string copy is; the + // read's other uses are the borrow's (Ruling 7) + if (countedHandle && isCountedHandleReceiver(user, pending.value)) + { + continue; + } + if (isBorrower && isBorrower(user, pending.kills)) { continue; @@ -2317,7 +2372,9 @@ class OwnershipInferencePass { auto root = rootOf(value); auto *def = root.getDefiningOp(); - if (!def || !ownsHeap(root)) + // 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; } @@ -2337,6 +2394,255 @@ class OwnershipInferencePass 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); + } + + if (auto pushOp = mlir::dyn_cast(user)) + { + return retainedRightBefore(user, pushOp.getItems(), value); + } + + if (auto unshiftOp = mlir::dyn_cast(user)) + { + return retainedRightBefore(user, unshiftOp.getItems(), value); + } + + if (auto castOp = mlir::dyn_cast(user)) + { + return mlir::isa(castOp.getType()) && retainedRightBefore(user, user->getOperands(), value); + } + + // `ts.ReturnVal` is the return before the affine lowering, which makes it a store into + // the result slot + if (mlir::isa(user)) + { + return retainedRightBefore(user, user->getOperands(), value); + } + + return false; + } + + // A use that passesOn hands on but that takes the value: a return's store into the function's + // result slot (isResultSlot), and a cast that boxes it into `any`. + static bool isHandOff(mlir::Operation *user, mlir::Value value) + { + if (auto storeOp = mlir::dyn_cast(user)) + { + auto slot = storeOp.getReference().getDefiningOp(); + return storeOp.getValue() == value && slot && isResultSlot(slot); + } + + auto castOp = mlir::dyn_cast(user); + return castOp && castOp.getIn() == value && mlir::isa(castOp.getType()); + } + + // A return or an `any` box of a handle that rc counts (walkBorrowed). + static bool isCountedHandOff(mlir::Operation *user, mlir::Value value) + { + return isHandOff(user, value) && isCountedHandleReceiver(user, value); + } + + // rc's retains for what `op` takes: MLIRGen puts them in a row right before it + // (mlirGenRetainCaptured, retainInsertedElements), one for each operand it does not take + // over. Answers whether that row retains `value` once for each time `operands` hold it. A + // retain is right before one op only, so no other receiver can count it as well. + static bool retainedRightBefore(mlir::Operation *op, mlir::ValueRange operands, mlir::Value value) + { + auto needed = llvm::count(operands, value); + if (needed == 0) + { + return false; + } + + for (auto *previous = op->getPrevNode(); needed > 0 && previous; previous = previous->getPrevNode()) + { + auto retainOp = mlir::dyn_cast(previous); + if (!retainOp) + { + break; + } + + if (retainOp.getReference() == value) + { + --needed; + } + } + + return needed == 0; + } + + // The releases of the old value that rc's counted stores of a handle read make: `ts.Retain` of + // the value, `ts.ReleaseSlot` of the slot, `ts.Store` of the value into it, in a row. Seen + // through the read's casts, merges and locals that own nothing, as handleReadUses sees it. + static void collectCountedStoreReleases(mlir::Value value, llvm::SmallPtrSetImpl &releases) + { + llvm::SmallVector values{value}; + llvm::SmallPtrSet seen{value}; + while (!values.empty()) + { + auto current = values.pop_back_val(); + for (auto &use : current.getUses()) + { + llvm::SmallVector same; + if (passesOn(use, same)) + { + for (auto next : same) + { + if (isHandle(next) && seen.insert(next).second) + { + values.push_back(next); + } + } + + continue; + } + + auto storeOp = mlir::dyn_cast(use.getOwner()); + if (!storeOp || storeOp.getValue() != current || !countedStoreRetain(storeOp)) + { + continue; + } + + if (auto releaseSlotOp = mlir::dyn_cast_or_null(storeOp->getPrevNode())) + { + releases.insert(releaseSlotOp); + } + } + } + } + + // rc's retain for a store of a value, when it made one: `ts.Retain` of the value right before + // the store, or before the `ts.ReleaseSlot` of the old value right before it - + // `Retain(v)`, [`ReleaseSlot(slot)`], `Store(v, slot)`. A return's store into the result + // slot comes after the scope exit's releases of the frame's locals and their slots: + // `Retain(v)`, [`Release(..)`, `ReleaseSlot(..)`...], `Store(v, result)`. Null for a store rc + // counts nothing for. + static mlir::Operation *countedStoreRetain(mlir_ts::StoreOp storeOp) + { + auto *previous = storeOp->getPrevNode(); + if (auto slot = storeOp.getReference().getDefiningOp(); slot && isResultSlot(slot)) + { + // the scope exit's releases, of locals and of their slots, in any order + while (mlir::isa_and_nonnull(previous) && + previous->getOperand(0) != storeOp.getValue()) + { + previous = previous->getPrevNode(); + } + } + else if (auto releaseSlotOp = mlir::dyn_cast_or_null(previous)) + { + previous = releaseSlotOp.getSlot() == storeOp.getReference() ? releaseSlotOp->getPrevNode() : nullptr; + } + + auto retainOp = mlir::dyn_cast_or_null(previous); + return retainOp && retainOp.getReference() == storeOp.getValue() ? retainOp.getOperation() : nullptr; + } + // The call a value is the result of, when that result borrows an argument. static mlir::Operation *borrowingCall(mlir::Value value) { @@ -2357,9 +2663,14 @@ class OwnershipInferencePass NotOwned // a parameter, `this`, a global, anything else }; - // a field's position and reference type; an element is position -1 + // a field's position and reference type; an element is position -1, a handle's payload + // (`s.value`) is SharedValue llvm::SmallVector> places; + static constexpr int64_t SharedValue = -2; 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. + llvm::SmallVector handles; // Somewhere under a root, but where is not known: a result that borrows an argument may // be any field or element under it. Every overwrite and every removal drops it. bool anyPlace = false; @@ -2386,6 +2697,8 @@ class OwnershipInferencePass } llvm::SmallPtrSet seen; + // the bases a `s.value` was read through, and what merges into them + llvm::SmallPtrSet handleBases; while (!refs.empty() || !first.empty()) { llvm::SmallVector bases; @@ -2402,6 +2715,12 @@ class OwnershipInferencePass chain.places.push_back({propertyRefOp.getPosition(), propertyRefOp.getType()}); base = propertyRefOp.getObjectRef(); } + else if (auto sharedValueRefOp = ref.getDefiningOp()) + { + chain.places.push_back({Chain::SharedValue, sharedValueRefOp.getType()}); + base = sharedValueRefOp.getShared(); + handleBases.insert(rootOf(base)); + } else { auto elementRefOp = ref.getDefiningOp(); @@ -2420,6 +2739,29 @@ class OwnershipInferencePass continue; } + // A handle (spec 23.3): other handles may reach its block, so this function owns + // nothing under it. A handle read out of a place still has that place above it, and + // one merged from several has each of them. + if (handleBases.contains(base) || isHandle(base)) + { + llvm::SmallVector merged; + if (auto argument = mlir::dyn_cast(base); argument && mergedInto(argument, merged)) + { + handleBases.insert(merged.begin(), merged.end()); + bases.append(merged.begin(), merged.end()); + continue; + } + + chain.roots.push_back({base, Chain::NotOwned}); + chain.handles.push_back(base); + if (auto loadOp = base.getDefiningOp(); loadOp && isPlace(loadOp.getReference())) + { + refs.push_back(loadOp.getReference()); + } + + continue; + } + if (isPlace(base)) { refs.push_back(base); // a field of a value-type field: the same object @@ -2478,6 +2820,33 @@ class OwnershipInferencePass llvm::SmallVector ends; for (auto [root, kind] : chain.roots) { + // A handle (spec 23.3), never owned: where this one is given back or overwritten - a + // local's release and assignments, a temporary's release; a global's assignments are + // below. A copy of it is counted, and moves nothing away. + if (llvm::is_contained(chain.handles, root)) + { + auto loadOp = root.getDefiningOp(); + if (auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp()) + { + for (auto *user : varOp->getUsers()) + { + if (mlir::isa(user)) + { + ends.push_back(user); + } + } + } + else if (!loadOp) + { + forEachUse(root, [&](mlir::Operation *use, mlir::Value) { + if (mlir::isa(use)) + { + ends.push_back(use); + } + }); + } + } + if (kind == Chain::Slot) { for (auto *user : root.getUsers()) @@ -2637,6 +3006,13 @@ class OwnershipInferencePass return !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()) + { + return llvm::any_of(chain.places, [&](auto &place) { return place.first == Chain::SharedValue; }); + } + int64_t position = -1; if (auto propertyRefOp = slot.getDefiningOp()) { @@ -2670,7 +3046,7 @@ class OwnershipInferencePass }); } - // pop, shift, splice, `length =` + // pop, shift, splice, `length =`; push and unshift on an array reached through a handle auto refType = mlir::dyn_cast(drop->getOperand(0).getType()); auto arrayType = refType ? mlir::dyn_cast(refType.getElementType()) : mlir_ts::ArrayType(); if (!arrayType) @@ -2693,6 +3069,17 @@ class OwnershipInferencePass auto loadOp = mlir::dyn_cast(read); auto place = loadOp ? describePlace(loadOp.getReference()) : describeCall(read); auto result = read->getResult(0); + // A handle read rc counts (spec 23.3): its counted uses are copies (walkBorrowed), and the + // release of the old value a counted store makes, right before it stores the retained + // read, cannot free the block read: it holds the retain's count by then + // (`Shared.count(q.child = p.child)`). + auto countedHandle = isHandle(result) && handleReadUses(result).counted; + llvm::SmallPtrSet countedStoreReleases; + if (countedHandle) + { + collectCountedStoreReleases(result, countedStoreReleases); + } + llvm::StringRef name = ownerName(result); llvm::SmallVector uses; llvm::SmallVector bookkeeping; @@ -2717,7 +3104,7 @@ class OwnershipInferencePass } return true; - }, keeping); + }, keeping, countedHandle); if (kept || letKept) { @@ -2760,6 +3147,11 @@ class OwnershipInferencePass for (auto *drop : drops) { + if (countedStoreReleases.contains(drop)) + { + continue; + } + if (dropsChain(drop, chain, read) && outlives(drop)) { return false; @@ -2861,6 +3253,11 @@ class OwnershipInferencePass base = elementRefOp.getArray(); step = "[]"; } + else if (auto sharedValueRefOp = ref.getDefiningOp()) + { + base = sharedValueRefOp.getShared(); + step = ".value"; + } else { return {}; @@ -3272,6 +3669,12 @@ class OwnershipInferencePass return true; } + // `s.value`'s place and `Shared.count(s)` read the handle and keep nothing (spec 23.1) + if (mlir::isa(user)) + { + return true; + } + // `--di`'s record of a folded const's value for the debugger: names it, keeps nothing if (mlir::isa(user)) { @@ -4210,6 +4613,75 @@ class OwnershipInferencePass signalPassFailure(); } + // A handle made into a union whose other members include one that owns a block (spec 23.2): + // `Shared | string` given a Shared. gc, none and rc accept it. + void reportHandleInOwningUnion(mlir::Operation *op) + { + auto handleIn = llvm::any_of(op->getOperandTypes(), [](mlir::Type type) { + return MLIRTypeHelper::isSharedHandleType(type); + }); + if (!handleIn) + { + return; + } + + for (auto resultType : op->getResultTypes()) + { + if (!MLIRTypeHelper::isSharedInOwningUnionType(resultType)) + { + continue; + } + + if (!reporting()) + { + return; + } + + auto unionType = resultType; + if (auto optionalType = mlir::dyn_cast(unionType)) + { + unionType = optionalType.getElementType(); + } + + mlir::Type shared; + mlir::Type owning; + for (auto member : mlir::cast(unionType).getTypes()) + { + if (mlir::isa(member)) + { + shared = shared ? shared : member; + } + else if (!owning && MLIRTypeHelper::ownsHeapMemoryOfUnionMember(member)) + { + owning = member; + } + } + + auto diag = op->emitError("a ") << printed(shared) << " in a union with "; + if (owning) + { + diag << "'" << printed(owning) << "', which -mm=own does not count,"; + } + else + { + diag << "a member -mm=own does not count,"; + } + + diag << " is not supported"; + signalPassFailure(); + return; + } + } + + static std::string printed(mlir::Type type) + { + llvm::SmallString<128> text; + llvm::raw_svector_ostream out(text); + MLIRPrinter printer{}; + printer.printType(out, type); + return text.str().str(); + } + void reportGivenNotOwned(mlir::Operation *call, mlir::Value arg) { if (!reporting()) diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp index 6f1e5d119..05c34e093 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp @@ -122,6 +122,12 @@ class OwnershipSignaturePass : public mlir::PassWrapper= 0 && index != found)) { @@ -1047,6 +1059,12 @@ class OwnershipSignaturePass : public mlir::PassWrapper(user)) + { + return sharedNewOp.getValue() == used; + } + if (auto unshiftOp = mlir::dyn_cast(user)) { return llvm::is_contained(unshiftOp.getItems(), used); @@ -1080,6 +1098,12 @@ class OwnershipSignaturePass : public mlir::PassWrapper(); cellOp && isParameterCell(cellOp, maker, index)) { auto cellType = mlir::cast(cellOp.getType()); + // a handle parameter's cell holds a count of its own (spec 23.2), which only + // the box's release gives back: a box holding one owns what it holds + if (MLIRTypeHelper::isSharedHandleType(cellType.getElementType())) + { + return; + } + if (ownsHeap(cellOp.getLoc(), cellType.getElementType())) { state.bounded.push_back(index); @@ -1721,18 +1752,31 @@ class OwnershipSignaturePass : public mlir::PassWrapper(op)) + // A handle given back may be the last one, and its payload goes with it, and with that + // anything the payload owns (spec 23.3). One held by a field or an element is a place's + // overwrite, judged below like any other: a constructor filling its own `this` is not + // a drop. + if (mlir::isa(op) && touchesHandle(op)) + { + drops = true; + } + else if (auto releaseSlotOp = mlir::dyn_cast(op)) { // a captured variable assigned through its cell, by a closure's body: the variable // is its declaring function's auto slot = releaseSlotOp.getSlot(); drops = slot.getDefiningOp() || (isPlace(slot) && reachesOutside(slot, funcOp)) || - isLoadedCell(slot); + isLoadedCell(slot) || (!isPlace(slot) && touchesHandle(op)); } else if (mlir::isa(op)) { drops = reachesOutside(op->getOperand(0), funcOp); } + else if (mlir::isa(op)) + { + // through a handle, it may move the elements another handle's borrow reads (spec 23.3) + drops = reachesSharedValue(op->getOperand(0)); + } else if (mlir::isa(op)) { drops = true; @@ -1765,6 +1809,12 @@ class OwnershipSignaturePass : public mlir::PassWrapper()) + { + return true; + } + if (auto propertyRefOp = value.getDefiningOp()) { work.push_back(propertyRefOp.getObjectRef()); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 70954639c..2ba3fadfa 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2609,6 +2609,36 @@ add_test(NAME test-own-string-copy-in-store-block "-DFILE=${PROJECT_SOURCE_DIR}/test/tester/own/own_string_copy_branch.ts" -P "${CMAKE_CURRENT_SOURCE_DIR}/own-string-copy-block.cmake") +# Shared (spec 23): counted handles. These are not in the own_test list, whose +# test-own-no-counting check forbids __tslang_inc_ref: a handle is counted under own by design. +# own_shared_user_named: a program's own class named Shared wins over the built-in (spec 23.1). +set(shared_models gc none rc own) +foreach(shared_mm ${shared_models}) + foreach(shared_test own_shared_basic own_shared_user_named own_shared_graph own_shared_containers own_shared_cycle own_shared_union + own_shared_global own_shared_unlink own_shared_global_init + own_shared_place_two_uses own_shared_global_field) + tslang_add_test(NAME test-jit-${shared_mm}-${shared_test} COMMAND test-runner -jit -mm=${shared_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/${shared_test}.ts") + tslang_add_test(NAME test-compile-${shared_mm}-${shared_test} COMMAND test-runner -mm=${shared_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/${shared_test}.ts") + endforeach() +endforeach() +# Shared.count is a count only under rc and own. +foreach(shared_count_mm rc own) + tslang_add_test(NAME test-jit-${shared_count_mm}-own_shared_count COMMAND test-runner -jit -mm=${shared_count_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_shared_count.ts") + tslang_add_test(NAME test-compile-${shared_count_mm}-own_shared_count COMMAND test-runner -mm=${shared_count_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_shared_count.ts") +endforeach() +# --verify-ownership is quiet on every positive (spec 23.5): handles keep rc's pairing. +foreach(shared_verify_mm own rc) + foreach(shared_test own_shared_basic own_shared_graph own_shared_count own_shared_containers own_shared_cycle own_shared_union + own_shared_global own_shared_unlink own_shared_global_init + own_shared_place_two_uses own_shared_global_field) + add_test(NAME test-${shared_verify_mm}-verify-ownership-${shared_test} + COMMAND $ --emit=mlir-affine --no-default-lib -mm=${shared_verify_mm} --verify-ownership + "${PROJECT_SOURCE_DIR}/test/tester/own/${shared_test}.ts") + set_tests_properties(test-${shared_verify_mm}-verify-ownership-${shared_test} + PROPERTIES FAIL_REGULAR_EXPRESSION "error|Stack dump") + endforeach() +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 # Windows, checked against a library that lists nothing (own-shared-no-drops.cmake). @@ -2731,7 +2761,19 @@ set(own_error_cases "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_string_kept_param_known|is used here after its value was moved" "own_err_string_call_keeps_twice|borrows a field and cannot be stored, returned or captured" - "own_err_string_pushed_twice|is used here after its value was moved") + "own_err_string_pushed_twice|is used here after its value was moved" + "own_err_shared_borrow_write|but is used here after it may be released or overwritten" + "own_err_shared_borrow_call|but is used here after it may be released or overwritten" + "own_err_shared_borrow_push|but is used here after it may be released or overwritten" + "own_err_shared_moved_in|is used here after its value was moved" + "own_err_shared_borrow_other_type|but is used here after it may be released or overwritten" + "own_err_shared_borrow_push_call|but is used here after it may be released or overwritten" + "own_err_shared_borrow_reassigned|but is used here after it may be released or overwritten" + "own_err_shared_union_owned|a Shared in a union with 'string', which -mm=own does not count, is not supported" + "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") foreach(own_error_case ${own_error_cases}) string(FIND "${own_error_case}" "|" own_error_sep) string(SUBSTRING "${own_error_case}" 0 ${own_error_sep} own_error_name) diff --git a/tslang/test/tester/own/own_err_shared_borrow_call.ts b/tslang/test/tester/own/own_err_shared_borrow_call.ts new file mode 100644 index 000000000..a0a59e90d --- /dev/null +++ b/tslang/test/tester/own/own_err_shared_borrow_call.ts @@ -0,0 +1,20 @@ +// -mm=own rejects (spec 23.3): a borrow read through a handle, used after a call that may drop - +// `touch` writes through a handle it reaches from a global. +class Node { + v = 0; +} + +let kept: Shared | null = null; + +function touch() { + const k = kept; + if (k) k.value = new Node(); +} + +function main() { + const a = new Shared(new Node()); + kept = a; + const n = a.value; + touch(); + print(n.v); +} diff --git a/tslang/test/tester/own/own_err_shared_borrow_other_type.ts b/tslang/test/tester/own/own_err_shared_borrow_other_type.ts new file mode 100644 index 000000000..107f0c2db --- /dev/null +++ b/tslang/test/tester/own/own_err_shared_borrow_other_type.ts @@ -0,0 +1,30 @@ +// -mm=own rejects (spec 23.3): a borrow read through one handle, used after a write through a +// handle of another static type that reaches the same block - a Shared converts to a +// Shared - so any write through any handle ends it. +class Base { + v = 0; +} + +class Derived extends Base { + w = 1; +} + +class Box { + items: Derived[] = []; +} + +function main() { + const a = new Shared(new Derived()); + const b: Shared = a; + const n = a.value; + b.value = new Base(); + const box = new Box(); + for (let i = 0; i < 100; i++) { + const d = new Derived(); + d.w = 77; + d.v = 66; + box.items.push(d); + } + + print(n.v, n.w, Shared.count(a), box.items.length); +} diff --git a/tslang/test/tester/own/own_err_shared_borrow_push.ts b/tslang/test/tester/own/own_err_shared_borrow_push.ts new file mode 100644 index 000000000..9e8254876 --- /dev/null +++ b/tslang/test/tester/own/own_err_shared_borrow_push.ts @@ -0,0 +1,18 @@ +// -mm=own rejects (spec 23.3): an element borrowed through one handle, used after a push through +// another, which may move the elements. +class Node { + v = 0; +} + +class Bag { + items: Node[] = []; +} + +function main() { + const a = new Shared(new Bag()); + const b = a; + a.value.items.push(new Node()); + const first = a.value.items[0]; + b.value.items.push(new Node()); + print(first.v); +} diff --git a/tslang/test/tester/own/own_err_shared_borrow_push_call.ts b/tslang/test/tester/own/own_err_shared_borrow_push_call.ts new file mode 100644 index 000000000..d03274281 --- /dev/null +++ b/tslang/test/tester/own/own_err_shared_borrow_push_call.ts @@ -0,0 +1,21 @@ +// -mm=own rejects (spec 23.3): an element borrowed through a handle, used after a call that pushes +// through another handle to the same block, which may move the elements. +class Node { + v = 0; +} + +class Bag { + items: Node[] = []; +} + +function grow(s: Shared) { + s.value.items.push(new Node()); +} + +function main() { + const a = new Shared(new Bag()); + a.value.items.push(new Node()); + const first = a.value.items[0]; + grow(a); + print(first.v); +} diff --git a/tslang/test/tester/own/own_err_shared_borrow_reassigned.ts b/tslang/test/tester/own/own_err_shared_borrow_reassigned.ts new file mode 100644 index 000000000..ea5790926 --- /dev/null +++ b/tslang/test/tester/own/own_err_shared_borrow_reassigned.ts @@ -0,0 +1,12 @@ +// -mm=own rejects (spec 23.3): a borrow read through a handle, used after that handle is given a +// new block - the old one may have been the last handle, and its payload went with it. +class Node { + v = 0; +} + +function main() { + let a = new Shared(new Node()); + const n = a.value; + a = new Shared(new Node()); + print(n.v); +} diff --git a/tslang/test/tester/own/own_err_shared_borrow_write.ts b/tslang/test/tester/own/own_err_shared_borrow_write.ts new file mode 100644 index 000000000..6cbbed831 --- /dev/null +++ b/tslang/test/tester/own/own_err_shared_borrow_write.ts @@ -0,0 +1,13 @@ +// -mm=own rejects (spec 23.3): a borrow read through one handle, used after a write through another +// handle that may reach the same block. +class Node { + v = 0; +} + +function main() { + const a = new Shared(new Node()); + const b = a; + const n = a.value; + b.value = new Node(); + print(n.v); +} diff --git a/tslang/test/tester/own/own_err_shared_moved_in.ts b/tslang/test/tester/own/own_err_shared_moved_in.ts new file mode 100644 index 000000000..e388aacb0 --- /dev/null +++ b/tslang/test/tester/own/own_err_shared_moved_in.ts @@ -0,0 +1,11 @@ +// -mm=own rejects: a class value given to `new Shared(x)` moves into the block, so it cannot be used +// after (spec 23.1, 2.3). +class Node { + v = 0; +} + +function main() { + const n = new Node(); + const s = new Shared(n); + print(n.v, s.value.v); +} diff --git a/tslang/test/tester/own/own_err_shared_place_arg.ts b/tslang/test/tester/own/own_err_shared_place_arg.ts new file mode 100644 index 000000000..9d333d363 --- /dev/null +++ b/tslang/test/tester/own/own_err_shared_place_arg.ts @@ -0,0 +1,31 @@ +// Shared (spec 23.3): a handle read out of a field and given straight to a call is a borrow of +// the field, not a counted copy. The callee overwrites the field, which may give back the last +// count of the block the argument points to: rejected, as for a class read out of a field. +class Node { + v = 0; + tag = ""; +} + +class P { + child: Shared = new Shared(new Node()); +} + +function churn() { + let keep: string[] = []; + for (let i = 0; i < 1000; i++) keep.push("k" + i); + return keep.length; +} + +function f(p: P, s: Shared) { + p.child = new Shared(new Node()); + churn(); + print(Shared.count(s)); + print(s.value.tag); +} + +function main() { + const p = new P(); + p.child.value.tag = "alive"; + f(p, p.child); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_shared_place_literal.ts b/tslang/test/tester/own/own_err_shared_place_literal.ts new file mode 100644 index 000000000..c91ab1fcc --- /dev/null +++ b/tslang/test/tester/own/own_err_shared_place_literal.ts @@ -0,0 +1,36 @@ +// Shared (spec 23.3, Ruling 7): one read of a handle out of a field with two uses. The store into +// `q.child` gives that use a counted owner of its own (rc retains for it); the object literal's +// field does not (rc retains nothing for it), so that store keeps a borrow of `p.child`. `dropBoth` +// overwrites both fields and gives back the last count of the block the literal points to: +// rejected. A store counts only when rc retains for it; the counted store does not exempt the other. +class Node { + v = 0; + tag = ""; +} + +class P { + child: Shared = new Shared(new Node()); +} + +function churn() { + let keep: string[] = []; + for (let i = 0; i < 1000; i++) keep.push("k" + i); + return keep.length; +} + +function dropBoth(p: P, q: P) { + p.child = new Shared(new Node()); + q.child = new Shared(new Node()); + churn(); +} + +function main() { + const p = new P(); + const q = new P(); + p.child.value.tag = "alive"; + const o = { s: (q.child = p.child) }; + dropBoth(p, q); + print(Shared.count(o.s)); + print(o.s.value.tag); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_shared_place_tuple.ts b/tslang/test/tester/own/own_err_shared_place_tuple.ts new file mode 100644 index 000000000..97c6e18ac --- /dev/null +++ b/tslang/test/tester/own/own_err_shared_place_tuple.ts @@ -0,0 +1,36 @@ +// Shared (spec 23.3, Rulings 7 and 9): one read of a handle out of a field with two uses. The +// store into `q.child` gives that use a counted owner of its own (rc retains for it); the tuple's +// element does not (rc retains nothing for it, and a tuple is no receiver rc counts), so the tuple +// keeps a borrow of `p.child`. `dropBoth` overwrites both fields and gives back the last count of +// the block the tuple points to: rejected. A use that is not known to count is a borrow (Ruling 9). +class Node { + v = 0; + tag = ""; +} + +class P { + child: Shared = new Shared(new Node()); +} + +function churn() { + let keep: string[] = []; + for (let i = 0; i < 1000; i++) keep.push("k" + i); + return keep.length; +} + +function dropBoth(p: P, q: P) { + p.child = new Shared(new Node()); + q.child = new Shared(new Node()); + churn(); +} + +function main() { + const p = new P(); + const q = new P(); + p.child.value.tag = "alive"; + const t: [Shared, number] = [(q.child = p.child), 1]; + dropBoth(p, q); + print(Shared.count(t[0])); + print(t[0].value.tag); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_shared_place_two_uses.ts b/tslang/test/tester/own/own_err_shared_place_two_uses.ts new file mode 100644 index 000000000..722df3e3e --- /dev/null +++ b/tslang/test/tester/own/own_err_shared_place_two_uses.ts @@ -0,0 +1,34 @@ +// Shared (spec 23.3, Ruling 7): one read of a handle out of a field with two uses. The store into +// `q.child` gives that use a counted owner of its own; the argument given to `f` does not, so it +// is a borrow of `p.child`. `f` overwrites both fields, which may give back the last count of the +// block the argument points to: rejected. The counted use does not exempt the other one. +class Node { + v = 0; + tag = ""; +} + +class P { + child: Shared = new Shared(new Node()); +} + +function churn() { + let keep: string[] = []; + for (let i = 0; i < 1000; i++) keep.push("k" + i); + return keep.length; +} + +function f(p: P, q: P, s: Shared) { + p.child = new Shared(new Node()); + q.child = new Shared(new Node()); + churn(); + print(Shared.count(s)); + print(s.value.tag); +} + +function main() { + const p = new P(); + const q = new P(); + p.child.value.tag = "alive"; + f(p, q, q.child = p.child); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_shared_union_owned.ts b/tslang/test/tester/own/own_err_shared_union_owned.ts new file mode 100644 index 000000000..2bd2e1530 --- /dev/null +++ b/tslang/test/tester/own/own_err_shared_union_owned.ts @@ -0,0 +1,12 @@ +// Shared (spec 23.2) in a union with a member that owns a block of its own: -mm=own counts a +// handle and owns a string singly, so it cannot do both for one value. Rejected under own only. +class Node { + v = 0; +} + +function main() { + const a = new Shared(new Node()); + let u: Shared | string = a; + u = "text"; + print(Shared.count(a)); +} diff --git a/tslang/test/tester/own/own_shared_basic.ts b/tslang/test/tester/own/own_shared_basic.ts new file mode 100644 index 000000000..cefb39314 --- /dev/null +++ b/tslang/test/tester/own/own_shared_basic.ts @@ -0,0 +1,43 @@ +// Shared (spec 23): two handles to one block, `.value` read and write, `===` on handles, and a +// payload that is a string. Each handle dropped is followed by reusing freed memory before the +// other is read. +class Node { + v = 0; + name = ""; +} + +function churn() { + let keep: string[] = []; + for (let i = 0; i < 1000; i++) keep.push("k" + i); + return keep.length; +} + +function main() { + let a = new Shared(new Node()); + a.value.name = "first" + 1; + const b = a; + assert(a === b, "two handles to one block are equal"); + a.value.v = 5; + assert(b.value.v == 5, "a write through one handle is seen through the other"); + + const c = new Shared(new Node()); + assert(a !== c, "two blocks are not equal"); + + a = c; + assert(churn() == 1000); + assert(b.value.v == 5 && b.value.name == "first1", "the other handle keeps the block"); + + const d = b; + d.value = new Node(); + d.value.v = 7; + assert(b.value.v == 7, "the value replaced through one handle is seen through the other"); + + const t = new Shared("text" + 2); + const u = t; + assert(u.value == "text2"); + t.value = "other" + 3; + assert(churn() == 1000); + assert(u.value == "other3"); + + print("done."); +} diff --git a/tslang/test/tester/own/own_shared_containers.ts b/tslang/test/tester/own/own_shared_containers.ts new file mode 100644 index 000000000..8c824f853 --- /dev/null +++ b/tslang/test/tester/own/own_shared_containers.ts @@ -0,0 +1,50 @@ +// Shared (spec 23) in containers: one node in two arrays, a `Shared | null` field, a closure +// capturing a handle, a handle boxed into `any` and back, and a handle returned from a function. +// Each owner but one is dropped and its memory reused before the last is read. +class Node { + v = 0; + tag = ""; +} + +class Holder { + s: Shared | null = null; +} + +function churn() { + let keep: string[] = []; + for (let i = 0; i < 1000; i++) keep.push("k" + i); + return keep.length; +} + +function make(v: number) { + const s = new Shared(new Node()); + s.value.v = v; + s.value.tag = "t" + v; + return s; +} + +function main() { + const s = make(3); + + let left: Shared[] = [s]; + const right: Shared[] = [s]; + left = []; + assert(churn() == 1000); + assert(right[0].value.v == 3 && right[0].value.tag == "t3", "the other array keeps the node"); + + const h = new Holder(); + h.s = right[0]; + const read = () => s.value.v; + assert(read() == 3, "a closure reads through its handle"); + + const boxed: any = h.s; + const back = >boxed; + assert(back === s, "a handle boxed into any comes back the same"); + assert(back.value.tag == "t3"); + + h.s = null; + assert(churn() == 1000); + assert(read() == 3 && back.value.v == 3, "the closure and the unboxed handle keep the node"); + + print("done."); +} diff --git a/tslang/test/tester/own/own_shared_count.ts b/tslang/test/tester/own/own_shared_count.ts new file mode 100644 index 000000000..f5b0502b8 --- /dev/null +++ b/tslang/test/tester/own/own_shared_count.ts @@ -0,0 +1,184 @@ +// Shared (spec 23.1): Shared.count after each copy and drop - a handle made, copied, given to a +// function that keeps it, returned from one, reassigned to the block it holds, copied in a loop, held +// by another block that dies, replaced in a block by `s.value = x`, and captured as a parameter by a +// closure and by one that escapes; a narrowed `Shared | null`, a handle boxed into `any` and read +// back out, and a handle given to a generator, also one that outlives the caller's handle. +class Node { + v = 0; +} + +// a holder rather than a bare array parameter: an array is passed as a value, so a callee's push +// grows the callee's copy and not the caller's +class Keeper { + kept: Shared[] = []; +} + +function keepOne(s: Shared, into: Keeper) { + into.kept.push(s); +} + +function dropLast(list: Keeper) { + list.kept.pop(); +} + +function make() { + return new Shared(new Node()); +} + +// a handle parameter captured by a closure: its cell holds a count, and gives it back +function capture(s: Shared) { + let t = 1; + const g = () => s.value.v + t; + t = 2; + return g(); +} + +// a closure that escapes with a handle parameter: its box holds the count +function reader(s: Shared) { + return () => s.value.v; +} + +// a generator over a handle parameter: its box holds the parameter's cell, which holds a count, and +// the generator reads through it with Shared.count and .value +function* counts(s: Shared) { + yield Shared.count(s); + yield s.value.v; +} + +function walkCounts(s: Shared) { + const seen: number[] = []; + for (const c of counts(s)) seen.push(c); + return seen; +} + +// a generator that reads its handle parameter again after the caller has dropped its own handle: +// the generator's count is the block's last +function* readsLater(s: Shared) { + yield Shared.count(s); + yield s.value.v; + yield Shared.count(s); +} + +function churn() { + let keep: Node[] = []; + for (let i = 0; i < 1000; i++) { + const n = new Node(); + n.v = -i; + keep.push(n); + } + + return keep.length; +} + +// handles copied inside a generator live in its state object, and are read by Shared.count there +function* copies() { + const s = new Shared(new Node()); + const t = s; + yield Shared.count(t); + const u = t; + yield Shared.count(u); +} + +function main() { + const a = new Shared(new Node()); + assert(Shared.count(a) == 1, "one handle"); + + let b = a; + assert(Shared.count(a) == 2, "copied"); + + b = a; + assert(Shared.count(a) == 2, "reassigned to the block it holds"); + + const list = new Keeper(); + keepOne(a, list); + assert(Shared.count(a) == 3, "kept by a callee"); + + b = new Shared(new Node()); + assert(Shared.count(a) == 2 && Shared.count(b) == 1, "one handle moved to another block"); + + dropLast(list); + assert(Shared.count(a) == 1, "the kept one dropped"); + + const m = make(); + assert(Shared.count(m) == 1, "returned from a function"); + + for (let i = 0; i < 1000; i++) { + const t = a; + assert(Shared.count(t) == 2, "copied in a loop"); + } + + assert(Shared.count(a) == 1, "every copy in the loop dropped"); + + // the payload is released when the outer block dies + const inner = new Shared(new Node()); + let outer = new Shared(inner); + assert(Shared.count(inner) == 2, "an outer block holds inner"); + outer = new Shared(new Shared(new Node())); + assert(Shared.count(inner) == 1, "the payload released when the outer block died"); + + // `s.value = x` releases the old payload and takes a count of the new one + const other = new Shared(new Node()); + const o2 = new Shared(inner); + assert(Shared.count(inner) == 2, "o2 holds inner"); + o2.value = other; + assert(Shared.count(inner) == 1 && Shared.count(other) == 2, "s.value = x hands over"); + + capture(a); + capture(a); + assert(Shared.count(a) == 1, "a captured parameter's count given back"); + + for (let i = 0; i < 100; i++) { + const r = reader(a); + assert(r() == 0 && Shared.count(a) == 2, "held by an escaping closure"); + } + + assert(Shared.count(a) == 1, "every escaping closure gave its count back"); + + // a `Shared | null` narrowed by `if (u)` keeps the union's type, and is still a handle here + let u: Shared | null = null; + u = a; + if (u) { + assert(Shared.count(u) == 2, "a narrowed union is a handle"); + } + + if (u !== null) { + assert(Shared.count(u) == 2, "a union narrowed by !== null is a handle"); + } + + u = null; + assert(Shared.count(a) == 1, "the union's count given back"); + + // an `any` box holds a count, and a handle read back out of it holds its own + for (let i = 0; i < 100; i++) { + const boxed: any = a; + assert(Shared.count(a) == 2, "boxed into any"); + const back = >boxed; + assert(Shared.count(back) == 3, "read back out of any"); + } + + assert(Shared.count(a) == 1, "every box and every handle read out of one gave its count back"); + + a.value.v = 7; + for (let i = 0; i < 100; i++) { + const seen = walkCounts(a); + assert(seen.length == 2 && seen[0] == 2 && seen[1] == 7, "a generator over a handle holds a count"); + assert(Shared.count(a) == 1, "the generator's count given back"); + } + + const copied: number[] = []; + for (const c of copies()) copied.push(c); + assert(copied.length == 2 && copied[0] == 2 && copied[1] == 3, "handles copied inside a generator"); + + // the caller's handle replaced while a generator holds the block: the generator's count keeps it + let d = new Shared(new Node()); + d.value.v = 42; + const later = readsLater(d); + assert(later.next().value == 2, "the generator holds a count"); + d = new Shared(new Node()); + assert(churn() == 1000); + assert(later.next().value == 42, "the generator's block is alive after the caller's handle is dropped"); + assert(later.next().value == 1, "the generator's count is the block's last"); + assert(Shared.count(d) == 1); + + print("done."); +} diff --git a/tslang/test/tester/own/own_shared_cycle.ts b/tslang/test/tester/own/own_shared_cycle.ts new file mode 100644 index 000000000..52a0ab6cd --- /dev/null +++ b/tslang/test/tester/own/own_shared_cycle.ts @@ -0,0 +1,26 @@ +// Shared (spec 23.4): a doubly linked pair and a child with a link to its parent are cycles, +// which are never freed (as under rc); the program still runs. +class Item { + v = 0; + next: Shared | null = null; + prev: Shared | null = null; +} + +function main() { + for (let i = 0; i < 100; i++) { + const a = new Shared(new Item()); + const b = new Shared(new Item()); + a.value.next = b; + b.value.prev = a; + a.value.v = i; + const p = b.value.prev; + assert(p !== null && p.value.v == i); + + const parent = new Shared(new Item()); + const child = new Shared(new Item()); + parent.value.next = child; + child.value.prev = parent; + } + + print("done."); +} diff --git a/tslang/test/tester/own/own_shared_global.ts b/tslang/test/tester/own/own_shared_global.ts new file mode 100644 index 000000000..a028c3630 --- /dev/null +++ b/tslang/test/tester/own/own_shared_global.ts @@ -0,0 +1,82 @@ +// Shared (spec 23.1) in module-level globals, bare and `Shared | null`, each initialised by +// `new Shared(...)` at module level: each global is born holding one count. Each test copies a +// global, drops the global's own handle, reuses the memory, then reads through the copy; and the +// global that keeps its handle is read after its copy is dropped. Counts are checked where there +// is one (rc, own); gc and none report -1. +class Node { + v = 0; + tag = ""; +} + +function churn() { + let keep: Node[] = []; + for (let i = 0; i < 1000; i++) { + const n = new Node(); + n.v = -i; + n.tag = "junk" + i; + keep.push(n); + } + + return keep.length; +} + +function countIs(s: Shared, expected: number) { + const c = Shared.count(s); + return c < 0 || c == expected; +} + +function mk(tag: string, v: number) { + const n = new Node(); + n.tag = tag; + n.v = v; + return n; +} + +const root = new Shared(mk("root", 1)); +let bare = new Shared(mk("bare", 2)); +let maybe: Shared | null = new Shared(mk("maybe", 3)); + +// a const global keeps its handle: a copy is dropped, and the global still reads its block +function constGlobal() { + assert(countIs(root, 1), "a const global is born with one count"); + let x = root; + assert(countIs(root, 2), "copied"); + x = new Shared(mk("other", 9)); + assert(churn() == 1000); + assert(countIs(root, 1), "the copy dropped"); + assert(root.value.v == 1 && root.value.tag == "root", "the const global's block is alive"); + assert(countIs(x, 1)); +} + +// a let global's handle replaced: the copy is the only handle left +function bareGlobal() { + assert(countIs(bare, 1), "a let global is born with one count"); + const h = bare; + assert(countIs(h, 2), "copied"); + bare = new Shared(mk("replaced", 5)); + assert(churn() == 1000); + assert(countIs(h, 1), "the copy is the only handle left"); + assert(h.value.v == 2 && h.value.tag == "bare", "the copy's block is alive"); + assert(countIs(bare, 1) && bare.value.tag == "replaced"); +} + +// a `Shared | null` global set to null: the copy is the only handle left +function nullableGlobal() { + const g = maybe; + if (g) assert(countIs(g, 2), "a nullable global is born with one count; copied"); + maybe = null; + assert(churn() == 1000); + if (g) { + assert(countIs(g, 1), "the copy is the only handle left"); + assert(g.value.v == 3 && g.value.tag == "maybe", "the copy's block is alive"); + } + + assert(g !== null && maybe === null); +} + +function main() { + constGlobal(); + bareGlobal(); + nullableGlobal(); + print("done."); +} diff --git a/tslang/test/tester/own/own_shared_global_field.ts b/tslang/test/tester/own/own_shared_global_field.ts new file mode 100644 index 000000000..d68bfa4cd --- /dev/null +++ b/tslang/test/tester/own/own_shared_global_field.ts @@ -0,0 +1,112 @@ +// Shared (spec 23.1) in module-level globals initialised from a field or an element read: of +// another global's object (`holder.child`), of a temporary (`new P().child`), of a call's result +// (`mkP().child`), and of a call's array (`mkArr()[0]`). Such a read carries no reference of its +// own, so the global takes one: it is born at count 2 when the field's object lives on, and at +// count 1 when the object or array was a temporary given back at the end of the initializer. Each test drops the other handle, reuses +// the memory, then reads through the global. Counts are checked where there is one (rc, own); gc +// and none report -1. +class Node { + v = 0; + tag = ""; +} + +function churn() { + let keep: Node[] = []; + for (let i = 0; i < 1000; i++) { + const n = new Node(); + n.v = -i; + n.tag = "junk" + i; + keep.push(n); + } + + return keep.length; +} + +function countIs(s: Shared, expected: number) { + const c = Shared.count(s); + return c < 0 || c == expected; +} + +function mk(tag: string, v: number) { + const n = new Node(); + n.tag = tag; + n.v = v; + return n; +} + +class P { + child: Shared = new Shared(mk("child", 9)); +} + +function mkP(tag: string, v: number) { + const p = new P(); + p.child.value.tag = tag; + p.child.value.v = v; + return p; +} + +function mkArr(tag: string, v: number) { + return [mkP(tag, v).child]; +} + +const holder = mkP("held", 1); +let fromHeld: Shared | null = holder.child; +let fromTemp: Shared = new P().child; +let fromCall: Shared | null = mkP("called", 3).child; +let fromElem: Shared | null = mkArr("elem", 4)[0]; + +// `holder.child`: the field and the global hold one count each +function fromHeldGlobal() { + if (fromHeld) assert(countIs(fromHeld, 2), "a global from a live object's field takes a count of its own"); + holder.child = new Shared(mk("other", 0)); + assert(churn() == 1000); + if (fromHeld) { + assert(countIs(fromHeld, 1), "the global is the only handle left"); + assert(fromHeld.value.v == 1 && fromHeld.value.tag == "held", "the global's block is alive"); + } + + assert(fromHeld !== null); +} + +// `new P().child`: the temporary gave its count back; the global's is the only one +function fromTempGlobal() { + assert(countIs(fromTemp, 1), "a global from a temporary's field is born with one count"); + assert(churn() == 1000); + assert(countIs(fromTemp, 1), "still one"); + assert(fromTemp.value.v == 9 && fromTemp.value.tag == "child", "the global's block is alive"); +} + +// `mkP(..).child`: as the temporary, through a call +function fromCallGlobal() { + if (fromCall) assert(countIs(fromCall, 1), "a global from a call result's field is born with one count"); + const g = fromCall; + if (g) assert(countIs(g, 2), "copied"); + fromCall = null; + assert(churn() == 1000); + if (g) { + assert(countIs(g, 1), "the copy is the only handle left"); + assert(g.value.v == 3 && g.value.tag == "called", "the copy's block is alive"); + } + + assert(g !== null && fromCall === null); +} + +// `mkArr(..)[0]`: an element of a temporary array +function fromElemGlobal() { + if (fromElem) assert(countIs(fromElem, 1), "a global from a temporary array's element is born with one count"); + assert(churn() == 1000); + if (fromElem) { + assert(countIs(fromElem, 1), "still one"); + assert(fromElem.value.v == 4 && fromElem.value.tag == "elem", "the global's block is alive"); + } + + assert(fromElem !== null); +} + +function main() { + fromHeldGlobal(); + fromTempGlobal(); + fromCallGlobal(); + fromElemGlobal(); + print("done."); +} diff --git a/tslang/test/tester/own/own_shared_global_init.ts b/tslang/test/tester/own/own_shared_global_init.ts new file mode 100644 index 000000000..d900dae6b --- /dev/null +++ b/tslang/test/tester/own/own_shared_global_init.ts @@ -0,0 +1,147 @@ +// Shared (spec 23.1) in module-level globals whose initializer reaches the global through a +// view of its type, or comes from a call: `Shared | undefined` from `new Shared(...)`, +// `Shared | null` (`let` and `const`) from a call, an explicit cast of a call's result, and +// `Shared | number`. Each global is born holding one count. Each test checks that count, copies +// the global, drops the global's own handle (or the copy, for the const), reuses the memory, then +// reads through what is left. Counts are checked where there is one (rc, own); gc and none +// report -1. +class Node { + v = 0; + tag = ""; +} + +function churn() { + let keep: Node[] = []; + for (let i = 0; i < 1000; i++) { + const n = new Node(); + n.v = -i; + n.tag = "junk" + i; + keep.push(n); + } + + return keep.length; +} + +function countIs(s: Shared, expected: number) { + const c = Shared.count(s); + return c < 0 || c == expected; +} + +function mk(tag: string, v: number) { + const n = new Node(); + n.tag = tag; + n.v = v; + return n; +} + +function mkS(tag: string, v: number) { + return new Shared(mk(tag, v)); +} + +let optNew: Shared | undefined = new Shared(mk("optNew", 1)); +let nullCall: Shared | null = mkS("nullCall", 2); +const constCall: Shared | null = mkS("constCall", 3); +let castCall = | null>mkS("castCall", 4); +let optCall: Shared | undefined = mkS("optCall", 5); +let numCall: Shared | number = mkS("numCall", 6); + +// `Shared | undefined` from `new Shared(...)`: the initializer goes through `ts.OptionalValue` +function optNewGlobal() { + if (optNew) assert(countIs(optNew, 1), "an optional global from new Shared is born with one count"); + const g = optNew; + if (g) assert(countIs(g, 2), "copied"); + optNew = undefined; + assert(churn() == 1000); + if (g) { + assert(countIs(g, 1), "the copy is the only handle left"); + assert(g.value.v == 1 && g.value.tag == "optNew", "the copy's block is alive"); + } + + assert(g !== undefined && optNew === undefined); +} + +// a `let` `Shared | null` from a call: the call's reference goes through the cast +function nullCallGlobal() { + if (nullCall) assert(countIs(nullCall, 1), "a nullable let global from a call is born with one count"); + const g = nullCall; + if (g) assert(countIs(g, 2), "copied"); + nullCall = null; + assert(churn() == 1000); + if (g) { + assert(countIs(g, 1), "the copy is the only handle left"); + assert(g.value.v == 2 && g.value.tag == "nullCall", "the copy's block is alive"); + } + + assert(g !== null && nullCall === null); +} + +// a `const` `Shared | null` from a call: it cannot be dropped, so the copy is, and the const +// still reads its block +function constCallGlobal() { + if (constCall) assert(countIs(constCall, 1), "a nullable const global from a call is born with one count"); + let x = constCall; + if (x) assert(countIs(x, 2), "copied"); + x = null; + assert(churn() == 1000); + if (constCall) { + assert(countIs(constCall, 1), "the copy dropped"); + assert(constCall.value.v == 3 && constCall.value.tag == "constCall", "the const global's block is alive"); + } + + assert(constCall !== null && x === null); +} + +// an explicit cast of a call's result +function castCallGlobal() { + if (castCall) assert(countIs(castCall, 1), "a cast call result global is born with one count"); + const g = castCall; + if (g) assert(countIs(g, 2), "copied"); + castCall = null; + assert(churn() == 1000); + if (g) { + assert(countIs(g, 1), "the copy is the only handle left"); + assert(g.value.v == 4 && g.value.tag == "castCall", "the copy's block is alive"); + } + + assert(g !== null && castCall === null); +} + +// `Shared | undefined` from a call +function optCallGlobal() { + if (optCall) assert(countIs(optCall, 1), "an optional global from a call is born with one count"); + const g = optCall; + if (g) assert(countIs(g, 2), "copied"); + optCall = undefined; + assert(churn() == 1000); + if (g) { + assert(countIs(g, 1), "the copy is the only handle left"); + assert(g.value.v == 5 && g.value.tag == "optCall", "the copy's block is alive"); + } + + assert(g !== undefined && optCall === undefined); +} + +// `Shared | number` from a call +function numCallGlobal() { + if (typeof numCall != "number") assert(countIs(numCall, 1), "a handle-or-number global from a call is born with one count"); + const g = numCall; + if (typeof g != "number") assert(countIs(g, 2), "copied"); + numCall = 7; + assert(churn() == 1000); + if (typeof g != "number") { + assert(countIs(g, 1), "the copy is the only handle left"); + assert(g.value.v == 6 && g.value.tag == "numCall", "the copy's block is alive"); + } + + assert(typeof g != "number" && typeof numCall == "number"); +} + +function main() { + optNewGlobal(); + nullCallGlobal(); + constCallGlobal(); + castCallGlobal(); + optCallGlobal(); + numCallGlobal(); + print("done."); +} diff --git a/tslang/test/tester/own/own_shared_graph.ts b/tslang/test/tester/own/own_shared_graph.ts new file mode 100644 index 000000000..6f9646090 --- /dev/null +++ b/tslang/test/tester/own/own_shared_graph.ts @@ -0,0 +1,57 @@ +// Shared (spec 23): a DAG - one item under two parents - and a singly linked list walked with +// `cur = cur.value.next`. Handles are dropped and their memory reused before the rest is read. +class Item { + v = 0; + name = ""; + next: Shared | null = null; +} + +class Parent { + child: Shared | null = null; +} + +function churn() { + let keep: string[] = []; + for (let i = 0; i < 1000; i++) keep.push("k" + i); + return keep.length; +} + +function list(n: number) { + let head: Shared | null = null; + for (let i = 0; i < n; i++) { + const item = new Shared(new Item()); + item.value.v = i; + item.value.next = head; + head = item; + } + + return head; +} + +function main() { + const child = new Shared(new Item()); + child.value.name = "child" + 1; + let p1 = new Parent(); + const p2 = new Parent(); + p1.child = child; + p2.child = child; + p1 = new Parent(); + assert(churn() == 1000); + const c = p2.child; + assert(c !== null && c.value.name == "child1", "the other parent keeps the child"); + // a const copied out of a field is a handle of its own (spec 23.1): child, p2.child and c + const n = Shared.count(child); + assert(n < 0 || n == 3, "the const counts"); + + let sum = 0; + let cur = list(10); + while (cur) { + sum += cur.value.v; + cur = cur.value.next; + } + + assert(sum == 45, "the list walked to its end"); + assert(cur === null); + + print("done."); +} diff --git a/tslang/test/tester/own/own_shared_place_two_uses.ts b/tslang/test/tester/own/own_shared_place_two_uses.ts new file mode 100644 index 000000000..fb88a162c --- /dev/null +++ b/tslang/test/tester/own/own_shared_place_two_uses.ts @@ -0,0 +1,87 @@ +// Shared (spec 23.3, Ruling 7): one read of a handle out of a field with two uses, a store into +// another field, which rc counts, and a read through it, which borrows the field read from. With +// nothing that may drop the field in between, both are legal: the store's release of the field's +// old value cannot free the block read, which already holds the store's count. The positive twin +// of own_err_shared_place_two_uses. Each test then drops the field read from, reuses its memory, and +// reads through the field stored into. Counts are checked where there is one (rc, own); gc and none +// report -1. +class Node { + v = 0; + tag = ""; +} + +class P { + child: Shared = new Shared(new Node()); +} + +function churn() { + let keep: Node[] = []; + for (let i = 0; i < 1000; i++) { + const n = new Node(); + n.v = -i; + n.tag = "junk" + i; + keep.push(n); + } + + return keep.length; +} + +function look(s: Shared) { + return s.value.v; +} + +function mkP(tag: string, v: number) { + const p = new P(); + p.child.value.tag = tag; + p.child.value.v = v; + return p; +} + +// `Shared.count` of the assignment: the store holds one count, the field read from the other +function countOfAssignment(p: P, q: P) { + return Shared.count(q.child = p.child); +} + +// the assignment given to a call that drops nothing +function lookAtAssignment(p: P, q: P) { + return look(q.child = p.child); +} + +function storedAndCounted() { + const p = mkP("first", 1); + const q = mkP("other", 9); + const c = countOfAssignment(p, q); + assert(c < 0 || c == 2, "the store and the field each hold a count"); + p.child = new Shared(new Node()); + assert(churn() == 1000); + const n = Shared.count(q.child); + assert(n < 0 || n == 1, "the store's count is the last"); + assert(q.child.value.v == 1 && q.child.value.tag == "first", "the stored block is alive"); +} + +function storedAndRead() { + const p = mkP("second", 2); + const q = mkP("other", 9); + assert(lookAtAssignment(p, q) == 2, "read through the assignment"); + p.child = new Shared(new Node()); + assert(churn() == 1000); + const n = Shared.count(q.child); + assert(n < 0 || n == 1, "the store's count is the last"); + assert(q.child.value.v == 2 && q.child.value.tag == "second", "the stored block is alive"); +} + +// the same field on both sides: the retain comes before the release of the old value +function sameObject() { + const p = mkP("same", 3); + const c = countOfAssignment(p, p); + assert(c < 0 || c == 1, "a field assigned its own value keeps one count"); + assert(churn() == 1000); + assert(p.child.value.v == 3 && p.child.value.tag == "same", "the block is alive"); +} + +function main() { + storedAndCounted(); + storedAndRead(); + sameObject(); + print("done."); +} diff --git a/tslang/test/tester/own/own_shared_union.ts b/tslang/test/tester/own/own_shared_union.ts new file mode 100644 index 000000000..8b26fdc26 --- /dev/null +++ b/tslang/test/tester/own/own_shared_union.ts @@ -0,0 +1,118 @@ +// Shared (spec 23.2) in a tagged union whose other members own no block: `Shared | number` +// is a handle, counted as rc counts it. A local, a field and an array element each take a handle, +// switch to a number and back; each owner but one is dropped and its memory reused before the +// last is read. Counts are checked where there is one (rc, own); gc and none report -1. +class Node { + v = 0; +} + +class H { + u: Shared | number = 0; +} + +function churn() { + let keep: string[] = []; + for (let i = 0; i < 1000; i++) keep.push("k" + i); + return keep.length; +} + +function countIs(s: Shared, expected: number) { + const c = Shared.count(s); + return c < 0 || c == expected; +} + +function valueOf(u: Shared | number) { + if (typeof u == "number") return -1; + return u.value.v; +} + +// a copy of a parameter put into a union local: the copy and the union each hold a count +function inLet(a: Shared) { + const b = a; + let u: Shared | number = b; + assert(countIs(a, 4), "a let union holds a count"); + assert(valueOf(u) == 7); +} + +// a const union is a handle of its own too (spec 23.1), as a let is +function inConst(a: Shared) { + const b = a; + const u: Shared | number = b; + assert(countIs(a, 4), "a const union holds a count"); + assert(valueOf(u) == 7); +} + +function inField(a: Shared) { + const b = a; + const h = new H(); + h.u = b; + assert(countIs(a, 4), "a union field holds a count"); + assert(valueOf(h.u) == 7); +} + +// a handle whose only owner, once this returns, is the union +function lastInUnion(v: number) { + const s = new Shared(new Node()); + s.value.v = v; + const u: Shared | number = s; + return u; +} + +function main() { + const a = new Shared(new Node()); + a.value.v = 7; + const keep = a; + assert(countIs(a, 2), "start"); + + inLet(a); + assert(countIs(a, 2), "the let union gave its count back"); + inConst(a); + assert(countIs(a, 2), "the const union gave its count back"); + inField(a); + assert(countIs(a, 2), "the field gave its count back"); + + // a field switched to a number and back + for (let i = 0; i < 1000; i++) { + const b = a; + const h = new H(); + h.u = b; + h.u = i; + assert(valueOf(h.u) == -1); + h.u = b; + assert(valueOf(h.u) == 7); + } + + assert(countIs(a, 2), "every field gave its count back"); + + // a local switched to a number and back + for (let i = 0; i < 1000; i++) { + const b = a; + let u: Shared | number = b; + u = 1; + assert(countIs(a, 3), "a union switched to a number gave its count back"); + u = b; + assert(valueOf(u) == 7); + } + + assert(countIs(a, 2), "every local gave its count back"); + + // an element switched to a number and back + const list: (Shared | number)[] = [a, 1]; + assert(countIs(a, 3), "an element holds a count"); + list[0] = 2; + assert(countIs(a, 2), "the element switched to a number"); + list[1] = a; + assert(countIs(a, 3), "another element holds it"); + + // a union is the last owner: every other handle is dropped and its memory reused + const only = lastInUnion(9); + const elements: (Shared | number)[] = [lastInUnion(10), 3]; + const field = new H(); + field.u = lastInUnion(11); + assert(churn() == 1000); + assert(valueOf(only) == 9, "a union local keeps the node"); + assert(valueOf(elements[0]) == 10 && valueOf(elements[1]) == -1, "an element keeps the node"); + assert(valueOf(field.u) == 11, "a field keeps the node"); + + print("done."); +} diff --git a/tslang/test/tester/own/own_shared_unlink.ts b/tslang/test/tester/own/own_shared_unlink.ts new file mode 100644 index 000000000..659e23dab --- /dev/null +++ b/tslang/test/tester/own/own_shared_unlink.ts @@ -0,0 +1,99 @@ +// Shared (spec 23.1): a const handle copied out of a place, or out of a local, is a handle of its +// own, bare or in a union. Each test drops the only other owner, reuses its memory, then reads +// through the const: unlinking a list node (spec 23.1's Node), taking a parent's child before +// clearing the field, and a `Shared | null` const of a let that is then reassigned. Counts are +// checked where there is one (rc, own); gc and none report -1. +class Node { + v = 0; + tag = ""; + next: Shared | null = null; +} + +class Parent { + child: Shared | null = null; +} + +function churn() { + let keep: Node[] = []; + for (let i = 0; i < 1000; i++) { + const n = new Node(); + n.v = -i; + n.tag = "junk" + i; + keep.push(n); + } + + return keep.length; +} + +function countIs(s: Shared, expected: number) { + const c = Shared.count(s); + return c < 0 || c == expected; +} + +function mk(tag: string, v: number) { + const n = new Node(); + n.tag = tag; + n.v = v; + return n; +} + +function link(head: Shared) { + head.value.next = new Shared(mk("second", 2)); +} + +function fill(p: Parent) { + p.child = new Shared(mk("child", 3)); +} + +// the list node unlinked from its head: `nxt` is its only handle +function unlink() { + const head = new Shared(new Node()); + link(head); + const nxt = head.value.next; + if (nxt) assert(countIs(nxt, 2), "the field and the const"); + head.value.next = null; + assert(churn() == 1000); + if (nxt) { + assert(countIs(nxt, 1), "the const is the only handle left"); + assert(nxt.value.v == 2 && nxt.value.tag == "second", "the unlinked node is alive"); + } + + assert(nxt !== null); +} + +// a parent's child taken into a const before the field is cleared +function takeChild() { + const p = new Parent(); + fill(p); + const c = p.child; + p.child = null; + assert(churn() == 1000); + if (c) { + assert(countIs(c, 1), "the const is the only handle left"); + assert(c.value.v == 3 && c.value.tag == "child", "the child is alive"); + } + + assert(c !== null); +} + +// a `Shared | null` const of a let, which is then given another block +function constOfLet() { + let a = new Shared(mk("alive", 42)); + const c: Shared | null = a; + assert(countIs(a, 2), "the let and the const"); + a = new Shared(mk("other", 7)); + assert(churn() == 1000); + if (c) { + assert(countIs(c, 1), "the const is the only handle left"); + assert(c.value.v == 42 && c.value.tag == "alive", "the const's block is alive"); + } + + assert(countIs(a, 1) && a.value.tag == "other"); +} + +function main() { + unlink(); + takeChild(); + constOfLet(); + print("done."); +} diff --git a/tslang/test/tester/own/own_shared_user_named.ts b/tslang/test/tester/own/own_shared_user_named.ts new file mode 100644 index 000000000..8d4a790dd --- /dev/null +++ b/tslang/test/tester/own/own_shared_user_named.ts @@ -0,0 +1,14 @@ +// A program's own class named Shared wins over the built-in (spec 23.1). +class Shared { + constructor(public value: number) {} + static count(s: Shared) { + return 42; + } +} + +function main() { + const x = new Shared(1); + assert(x.value == 1, "the program's own Shared"); + assert(Shared.count(x) == 42, "the program's own static count"); + print("done."); +}