From 6b09dad7a9180c85b7f44fe51e8f6a7cd0131126 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 3 Oct 2026 22:48:51 +0100 Subject: [PATCH 01/19] Spec: arrays as references (#453, #477) An array value becomes one pointer to a heap header { data, length, capacity } that never moves, so every alias sees push/pop/splice/length=. Rollout in three PRs: a behaviour-free layout helper, the switch, growth. Co-Authored-By: Claude Opus 5.5 --- .../2026-10-03-array-reference-design.md | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 tslang/docs/superpowers/specs/2026-10-03-array-reference-design.md diff --git a/tslang/docs/superpowers/specs/2026-10-03-array-reference-design.md b/tslang/docs/superpowers/specs/2026-10-03-array-reference-design.md new file mode 100644 index 000000000..8f52e4532 --- /dev/null +++ b/tslang/docs/superpowers/specs/2026-10-03-array-reference-design.md @@ -0,0 +1,178 @@ +# Arrays as references + +Date: 2026-10-03. Status: design approved in conversation (sections 1-4); written spec awaiting +review. Fixes #453 (a push onto an array parameter is lost to the caller) and #477 (a +destructuring rest element aliases its source). Plans go in `docs/superpowers/plans/`. + +## 1. Purpose + +TypeScript arrays are references: every variable, parameter, field and element that holds an +array holds the same array, and `push`, `pop`, `splice` and `length =` through any of them are +seen through all of them. tslang lowers an array to a two-word value `{ data, length }`. Copies +share the elements until the first change of length, and then part ways: the copy that changed +has a new length and, because `push` reallocates on every call, often a new data block, while +every other copy keeps the old length and a pointer the reallocation may have freed. + +This design makes an array value one pointer to a heap header that never moves. + +Decisions taken in the design conversation (do not re-litigate): + +| Question | Decision | +|---|---| +| Fix the parameter case only (pass mutated array parameters by reference)? | No. That leaves local aliases, record fields, element aliases and arrays stored into fields broken (§2), and needs a signature marker carried across modules and function types. | +| Header contents | `{ data, length, capacity }`. | +| Growth | Amortised doubling (PR 3). PR 2 keeps capacity equal to length. | +| Rollout | Three PRs: a behaviour-free refactor through one layout helper, the switch, then growth (§7). | +| Where the spec lives | Its own file: the change is not specific to `-mm=own`. | + +## 2. The evidence + +Fourteen shapes, `tslang --emit=jit --no-default-lib`, main 9a5cc05b with #476's change (global +constructor order only). "Works" means the change made through the second name is seen through +the first. Under own the shapes were one file, which the four compile errors stopped; "-" means +not run separately, and shapes 2 and 13 were run on their own. + +| # | Shape | gc / rc / none | own | +|---|---|---|---| +| 1 | element write through a parameter | works | - | +| 2 | `push` through a parameter | length stale | crash (exit 127) | +| 3 | `pop` through a parameter | length stale | - | +| 4 | `length = 0` through a parameter | length stale | - | +| 5 | local alias `const b = a; b.push(..)` | length stale | compile error (borrow) | +| 6 | class field in place, `h.items.push(..)` through a parameter | works | - | +| 7 | field alias `const it = h.items; it.push(..)` | length stale | compile error (borrow) | +| 8 | object-literal field through a parameter | length stale | - | +| 9 | closure capture | works | - | +| 10 | `aa[0].push(..)` | works | - | +| 11 | element alias `const x = aa[0]; x.push(..)` | length stale | compile error (borrow) | +| 12 | `a === b` after `b = a` | works | - | +| 13 | four pushes through a parameter, then read `a[0]` | gc reads 0 (old block); rc crashes | crash | +| 14 | array stored into a field, then pushed through the original | length stale | compile error (move) | + +The shapes that work are the ones where the struct itself lives in memory both sides address (a +class instance, a capture cell, the outer array's data block). Every shape that copies the +struct fails. Under rc a parameter push exits with 127 and prints nothing. + +## 3. What an array is + +- An array value (`T[]`, the typed-array aliases `Int8Array` .. `BigUint64Array`, `string[]`) is + one pointer to a heap **header** `{ data: ptr, length: index, capacity: index }`, in that + order. The header is allocated when the array is made and never moves; only `data` is + reallocated. +- Assigning, passing, returning, storing into a field or element, capturing and boxing into `any` + or a union copy the pointer. `===` and `!==` compare header pointers. +- An empty array is a header with `data = null`, `length = 0`, `capacity = 0`. There is no shared + empty header: each `[]` is a distinct array. +- `ConstArray` (a literal's static data) is unchanged. The cast from `ConstArray` to `T[]` makes a + header and copies the data into a fresh block on both of its paths; the path that today points + the struct at the static data (`byValue = false`) copies as well, since `push` would otherwise + reallocate static memory. +- `[a, ...rest] = src` gives `rest` a new array holding a copy of the slice (#477); `ts.ArrayView` + becomes a copy. +- Layout: an array field, element, tuple slot or union payload shrinks from two words to one + pointer; `T[] | undefined` is the pointer and its tag. +- ABI: every function that takes or returns an array changes. Objects built before the switch do + not link with objects built after it. The default library has no native code that reads an + array (its C++ wrappers take none), so its sources are unchanged; it is rebuilt with the + switch, for every model and both build types. + +## 4. Ownership in each model + +Only the lowering of the array case in the retain/release routines +(`OwnershipRoutineLogic.h`) changes. The MLIR-level ownership passes work by type and are +unchanged. + +- **gc.** Header and data block come from `GC_malloc`; the header keeps the data block alive. + The data block keeps today's allocation call. +- **none.** As today: nothing is freed. The header is one more allocation. +- **rc.** The counted block is the header. + - Retain increments the header's count (today: the data block's). + - Release, on the last reference: release elements `[0, length)`, free the data block, free + the header. The data block's own count is never read: the header holds its only reference. + - Slots `[length, capacity)` are never released (and, from PR 3, hold zero). + - `pop` and `shift` hand the removed element's reference to the caller, as today. +- **own.** The header is a single-owned block, like a class instance. + - A borrowed array parameter is the owner's header pointer: `push` through it changes the + owner's array, as `h.items.push` through a borrowed object already does. + - The rules do not change. Local and field aliases (shapes 5, 7, 11, 14) stay compile + errors; moves, element moves and borrows are as today. +- **`Shared`** holds the header pointer as its payload. Nothing specific. +- **Cycles.** `a.push(a)` is a reference cycle: under rc and own it leaks like a class cycle, under + gc it is collected. + +## 5. The code that changes + +- **`include/TypeScript/LowerToLLVM/ArrayLayout.h` (new)** is the only code that knows the + layout. It reads and writes `data`, `length` and `capacity`, gives an element's address, and + makes an array from a data pointer and a length (and, from PR 2, a capacity). PR 1 implements it + over today's struct; PR 2 changes its insides to the header. +- **Op signatures stay.** `ArrayPush`, `ArrayPop`, `ArrayShift`, `ArrayUnshift`, `ArraySplice` and + `SetLengthOf` keep taking a reference to the slot; in PR 2 they load the header pointer from it. + Narrowing them to take the value is out of scope. +- **Where arrays are made:** `NewEmptyArray`, `NewArray`, `CreateArray`, the `ConstArray` cast + (`CastLogicHelper.h`, both paths), the spread builder, `ArrayView` (a copy, §3). +- **Where arrays are read:** `LengthOf`, `ElementRef` and element access, for-of, printing, the + array casts in `CastLogicHelper.h` (to string, to an opaque pointer, `any` boxing and unboxing), + the retain/release routines (§4). +- **Debug info** (`LLVMDebugInfo.h`): an array's debug type becomes a pointer to a + `{ data, length, capacity }` struct, so a debugger still shows the elements. +- **`main(argc, argv: string[])`:** lowering emits a C `main(int, char **)` adapter that builds a + `string[]` header from `argc`/`argv` and calls the user's `main`. Today the struct is read from + C's `argv` and `envp`, which is the open `argv.length` = envp bug; the adapter fixes it. A + `Ref` argv still receives C's `char **` directly. Both AOT and JIT. +- **32-bit x86:** the header is three pointer-sized words; nothing is specific to x86. + +## 6. Growth (PR 3) + +- `push`, `unshift` and `splice` that need more room than `capacity` grow it to + `max(4, 2 * capacity, needed)`; otherwise they write into the existing block. +- `pop`, `shift` and a smaller `length =` keep the block. They zero each slot they vacate, so gc + does not keep a removed element alive and a later growth reads zero. +- `length = n` larger than `length` zeroes `[length, n)` in every model, gc included: after a + `pop` the slots past `length` are no longer fresh memory. Today's behaviour (new slots read as + zero; the default library's `Set`, `Map` and `Array.map` rely on it, see the comment in + `SetLengthOfOpLowering`) is kept. + +## 7. Rollout and testing + +### PR 1: the layout helper (no behaviour change) + +- Every layout access in lowering goes through `ArrayLayout`: the 29 `ARRAY_DATA_INDEX` / + `ARRAY_SIZE_INDEX` uses in `LowerToLLVM.cpp`, `CastLogicHelper.h`, `LLVMCodeHelper.h` and + `OwnershipRoutineLogic.h`, and every place that builds or takes apart the struct with raw + indexes. +- Gate: `--emit=llvm` output byte-identical for every suite test before and after; the full + Windows Release suite; no `ARRAY_DATA_INDEX` / `ARRAY_SIZE_INDEX` outside the helper. + +### PR 2: the switch + +- §3, §4 and §5: the representation, the creation and read sites, the retain/release routines, + debug info, the `main` adapter, the rest copy. Capacity equals length. +- Tests: + - `00array_reference.ts`: shapes 1-14 of §2 as asserts; compile and JIT, rc and none corpora. + - An own test with the shapes own accepts, including a push through a borrowed parameter. + - `00array_rest_copy.ts` (#477). + - A `main(argc, argv: string[])` test for AOT and JIT. +- Gates: the full Windows Release suite; the full Linux (WSL) suite, which is required because + the Windows heap hides double frees; the own corpus with no file lost; own's memory measurement + flat; the default library rebuilt and its suite run under every model. + +### PR 3: growth + +- §6. +- Tests: many pushes then reads, through aliases; `pop` then `length =` larger reads zero; + `shift`/`unshift` around a growth; a push-in-a-loop timing check against PR 2. + +### Done when + +- #453 and #477 are closed. +- Shapes 1-14 work under gc, rc and none, and under own where own accepts them. +- No `ARRAY_*_INDEX` outside `ArrayLayout.h`. +- Windows and Linux suites are green and the default library is rebuilt. + +## 8. Out of scope + +- Op signatures taking the array value instead of a slot reference. +- Shrinking the data block. +- A shared empty-array sentinel. +- Collecting reference cycles under rc and own (`WeakRef` is a separate item). From 4dd14cac85dd84d4d51bb432ac46037809e30cca Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 3 Oct 2026 23:00:37 +0100 Subject: [PATCH 02/19] Plan: arrays as references (#453, #477); spec takes the plan's rulings Eight tasks in three PRs. Rulings folded into the spec: a null header reads as an empty array (R1), main's string[] argv is made by a new op (R2), the rest element is built in MLIRGen (R3). Co-Authored-By: Claude Opus 5.5 --- .../plans/2026-10-03-array-reference.md | 930 ++++++++++++++++++ .../2026-10-03-array-reference-design.md | 18 +- 2 files changed, 942 insertions(+), 6 deletions(-) create mode 100644 tslang/docs/superpowers/plans/2026-10-03-array-reference.md diff --git a/tslang/docs/superpowers/plans/2026-10-03-array-reference.md b/tslang/docs/superpowers/plans/2026-10-03-array-reference.md new file mode 100644 index 000000000..e7e107bf0 --- /dev/null +++ b/tslang/docs/superpowers/plans/2026-10-03-array-reference.md @@ -0,0 +1,930 @@ +# Arrays as References 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:** Make a tslang array value one pointer to a heap header `{ data, length, capacity }` that never moves, so every alias of an array sees `push`/`pop`/`splice`/`length =` (#453), and make a destructuring rest element a copy (#477). + +**Architecture:** Three PRs on branch `array-reference`. PR 1 routes every access to the array layout in lowering through one new helper, `ArrayLayout`, with no change to the generated IR. PR 2 changes the helper's insides and the type conversion to the header, plus the sites that need more than the helper (rc/own routines, static arrays, the `main` argv, the rest copy, debug info). PR 3 adds capacity growth. + +**Tech Stack:** C++17, MLIR/LLVM dialect conversion patterns (`TsLlvmPattern`), the tslang test runner (ctest), Git Bash + WSL. + +**Spec:** `tslang/docs/superpowers/specs/2026-10-03-array-reference-design.md` (approved 2026-10-03). + +## Global Constraints + +- Header field order: `data` (ptr), `length` (index), `capacity` (index) - indexes `ARRAY_DATA_INDEX 0`, `ARRAY_SIZE_INDEX 1`, `ARRAY_CAPACITY_INDEX 2` (`tslang/include/TypeScript/Defines.h`). +- After PR 1, no `ARRAY_DATA_INDEX` / `ARRAY_SIZE_INDEX` / `ARRAY_CAPACITY_INDEX` outside `tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h` (and `Defines.h`). +- Growth (PR 3): new capacity = `max(4, 2 * capacity, needed)`. +- Every memory model: `gc`, `rc`, `none`, `own`. own stays count-free: no `__tslang_inc_ref`/`__tslang_dec_ref` in what own emits (`test-own-no-counting-*`). +- Commits are GPG-signed; on a signing timeout retry once, then hand the user the exact `git commit -F ` command. Never bypass signing. Commit messages end with `Co-Authored-By: Claude Opus 5.5 `. +- Source files are CRLF: edit with the Edit tool or Python with `newline=''`; never `sed -i` (it strips CRLF). +- Build: `cmake --build __build/tslang/windows-msbuild-2026-release --config Release --parallel 8 --target tslang`; full suite: `cd __build/tslang/windows-msbuild-2026-release && ctest -C Release -j 12 --timeout 300`. Compiler binary: `__build/tslang/windows-msbuild-2026-release/bin/tslang.exe`. +- Scratch paths used by the steps: `S="C:/Users/duzha/AppData/Local/Temp/claude/I--TypeScriptCompiler/f5d6099f-c9ad-473f-a7eb-6f10e2a42c61/scratchpad/arrref"`, `R="C:/Users/duzha/AppData/Local/Temp/claude/I--TypeScriptCompiler/f5d6099f-c9ad-473f-a7eb-6f10e2a42c61/scratchpad/reinsert.py"` (registers a test: `python "$R" `, run from `I:/TypeScriptCompiler`). +- Known unrelated failure: `test-compile-gc-defaultlib-collector` (stale local default lib in `I:\tslang`, `Error..size` 32 vs 48) until the default lib is rebuilt in Task 7. + +## Rulings (spec gaps decided while planning) + +- **R1, a null header is an empty array.** Today zeroed memory is a valid empty array: elements of `new Array(n)`, class fields of array type with no initializer, and `undefined` cast to an array (`CastLogicHelper::castToArrayType`, `isUndef`). So: reading `data`/`length` of a null header gives null/0; an op that changes an array through its slot (`push`, `pop`, `shift`, `unshift`, `splice`, `length =`) first stores a fresh empty header into a slot that holds null; rc/own release of null does nothing (`emitIfLastReference` already skips null). Cost if wrong: one compare and branch per array read. +- **R2, `main`'s `argv: string[]` is built by a new op, not a C adapter.** Spec §5 says lowering emits a C `main(int, char **)` adapter. The array must come from the model's allocator and be seen by the ownership passes as fresh, which only MLIR-level code gets. So MLIRGen gives `main` a `Ref` second parameter and binds `argv` to a new `ts.ArrayFromCStrings(argc, argv)` op (a copy of each C string into an array). The JIT thunk's pointer branch and the C runtime then pass `char **` as they already can. Cost if wrong: one more op to teach to two passes. +- **R3, the rest copy is built in MLIRGen.** Spec §3 says `ts.ArrayView` becomes a copy. A copy must own its elements under rc/own; MLIRGen's existing "fresh array + synthesized loop" idiom (`MLIRGenCast.cpp`, numeric array widening) gets that from the passes for free. So MLIRGen stops emitting `ts.ArrayView` for a rest element and builds `const .rest: T[] = []; for (let .i = index; .i < .src.length; .i++) .rest.push(.src[.i]);` instead. `ArrayViewOpLowering` stays for other users (none known; see Task 3 step 1). + +## Review Focus + +1. **Zeroed arrays (R1):** `const aa = new Array(2); aa[0].push(1)` and a class field `items: number[];` pushed in a method must keep working - tests in Task 4 (`00array_zero_slots.ts`). +2. **Truthiness and null:** `[]` is truthy and `[] === null` is false after the switch (today both are wrong: an empty array has a null data pointer); `let a: number[] | null = null; if (a)` stays falsy - tests in Task 4. +3. **Constant arrays inside global data:** `const t: [number, number[]] = [1, [2, 3]]` and `const aa: number[][] = [[1], [2]]` at module level must still read correctly under rc (immortal static header) - tests in Task 4 (`00array_static_nested.ts`). +4. **Arrays crossing `any` and unions:** `const x: any = arr; (x as number[]).push(4)` is seen through `arr` - test in Task 4. +5. **Rest of a shorter array:** `const [a, b, ...rest] = [1]` gives `rest.length === 0` (no negative count) - test in Task 3. + +--- + +## PR 1: the layout helper (no behaviour change) + +### Task 1: `ArrayLayout` and the sites in `LowerToLLVM.cpp` + +**Files:** +- Create: `tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h` +- Modify: `tslang/lib/TypeScript/LowerToLLVM.cpp` - `LengthOfOpLowering` (~line 618), `SetLengthOfOpLowering` (~638), `CreateArrayOpLowering` (~3000), `NewEmptyArrayOpLowering` (~3070), `NewArrayOpLowering` (~3113), `ArrayPushOpLowering` (~3170), `ArrayPopOpLowering` (~3256), `ArrayUnshiftOpLowering` (~3305), `ArrayShiftOpLowering` (~3400), `ArraySpliceOpLowering` (~3465), `ArrayViewOpLowering` (~3650) +- Create (scratchpad, never committed): `ir-snapshot.sh` + +**Interfaces:** +- Produces (used by every later task): + +```cpp +class ArrayLayout : public LLVMCodeHelperBase +{ + public: + ArrayLayout(mlir::Operation *op, PatternRewriter &rewriter, const TypeConverter *typeConverter, CompileOptions &compileOptions); + + // what the field GEPs are based on, for an op that changes the array held in `slot` + mlir::Value headerForUpdate(mlir_ts::ArrayType arrayType, mlir::Value slot); + // the same, for a routine that only reads (never materialises: R1) + mlir::Value headerForRead(mlir_ts::ArrayType arrayType, mlir::Value slot); + mlir::Value dataAddress(mlir_ts::ArrayType arrayType, mlir::Value header); + mlir::Value lengthAddress(mlir_ts::ArrayType arrayType, mlir::Value header); + // of an array value + mlir::Value data(mlir_ts::ArrayType arrayType, mlir::Value array); + mlir::Value length(mlir_ts::ArrayType arrayType, mlir::Value array); + // what `===` and truthiness compare + mlir::Value identity(mlir_ts::ArrayType arrayType, mlir::Value array); + // a new array over `data` (owned by the array) with `length` elements + mlir::Value make(mlir_ts::ArrayType arrayType, mlir::Value data, mlir::Value length); + // a constant array inside a global's initializer: `data` is static, nothing is allocated + // (Task 4 replaces this with makeStatic(arrayType, dataGlobalName, dataOffsetBytes, length)) + mlir::Value makeStatic(mlir_ts::ArrayType arrayType, mlir::Value data, mlir::Value length); + mlir::Value zero(mlir_ts::ArrayType arrayType); + mlir::Value undef(mlir_ts::ArrayType arrayType); +}; +``` + +- [ ] **Step 1: Record the IR baseline.** Build the branch base (before any change) and save the compiler: + +```bash +cd /i/TypeScriptCompiler && git log --oneline -1 +cmake --build __build/tslang/windows-msbuild-2026-release --config Release --parallel 8 --target tslang +S="C:/Users/duzha/AppData/Local/Temp/claude/I--TypeScriptCompiler/f5d6099f-c9ad-473f-a7eb-6f10e2a42c61/scratchpad/arrref" +mkdir -p "$S" && cp __build/tslang/windows-msbuild-2026-release/bin/tslang.exe "$S/tslang-base.exe" +``` + +Write `$S/ir-snapshot.sh`: + +```bash +#!/bin/bash +# ir-snapshot.sh : --emit=llvm of every suite test under gc, rc and own +T="$1"; OUT="$2"; mkdir -p "$OUT" +ls /i/TypeScriptCompiler/tslang/test/tester/tests/*.ts /i/TypeScriptCompiler/tslang/test/tester/own/*.ts | + xargs -P 12 -I{} bash -c ' + f="{}"; b=$(basename "$f" .ts); d=$(basename $(dirname "$f")) + for mm in gc rc own; do + "'"$T"'" --emit=llvm --no-default-lib -mm=$mm "$f" -o "'"$OUT"'/$d-$b-$mm.ll" > "'"$OUT"'/$d-$b-$mm.err" 2>&1 + done' +``` + +Run it: `bash "$S/ir-snapshot.sh" "$S/tslang-base.exe" "$S/base"`. Expected: about 3 x 1,100 `.ll` files (some empty with an error in `.err`; that is fine, the comparison includes the errors). + +- [ ] **Step 2: Create `ArrayLayout.h`** with the PR 1 implementation. Follow the include guard and namespace style of `LLVMCodeHelperBase.h`. + +```cpp +#ifndef MLIR_TYPESCRIPT_LOWERTOLLVM_ARRAYLAYOUT_H_ +#define MLIR_TYPESCRIPT_LOWERTOLLVM_ARRAYLAYOUT_H_ + +#include "TypeScript/Defines.h" +#include "TypeScript/TypeScriptOps.h" +#include "TypeScript/MLIRLogic/MLIRHelper.h" +#include "TypeScript/LowerToLLVM/LLVMCodeHelperBase.h" + +// The one place that knows how an array value is laid out +// (docs/superpowers/specs/2026-10-03-array-reference-design.md). An array value is a +// { data, length } struct; a `slot` is a pointer to one. +class ArrayLayout : public LLVMCodeHelperBase +{ + public: + ArrayLayout(mlir::Operation *op, PatternRewriter &rewriter, const TypeConverter *typeConverter, + CompileOptions &compileOptions) + : LLVMCodeHelperBase(op, rewriter, typeConverter, compileOptions) + { + } + + mlir::Value headerForUpdate(mlir_ts::ArrayType, mlir::Value slot) + { + return slot; + } + + mlir::Value headerForRead(mlir_ts::ArrayType, mlir::Value slot) + { + return slot; + } + + mlir::Value dataAddress(mlir_ts::ArrayType arrayType, mlir::Value header) + { + return fieldAddress(arrayType, header, ARRAY_DATA_INDEX); + } + + mlir::Value lengthAddress(mlir_ts::ArrayType arrayType, mlir::Value header) + { + return fieldAddress(arrayType, header, ARRAY_SIZE_INDEX); + } + + mlir::Value data(mlir_ts::ArrayType, mlir::Value array) + { + TypeHelper th(rewriter); + return rewriter.create(op->getLoc(), th.getPtrType(), array, + MLIRHelper::getStructIndex(rewriter, ARRAY_DATA_INDEX)); + } + + mlir::Value length(mlir_ts::ArrayType, mlir::Value array) + { + TypeHelper th(rewriter); + return rewriter.create(op->getLoc(), typeConverter->convertType(th.getIndexType()), array, + MLIRHelper::getStructIndex(rewriter, ARRAY_SIZE_INDEX)); + } + + mlir::Value identity(mlir_ts::ArrayType arrayType, mlir::Value array) + { + return data(arrayType, array); + } + + mlir::Value make(mlir_ts::ArrayType arrayType, mlir::Value data, mlir::Value length) + { + auto loc = op->getLoc(); + auto llvmArrayType = typeConverter->convertType(arrayType); + mlir::Value value = rewriter.create(loc, llvmArrayType); + value = rewriter.create(loc, llvmArrayType, value, data, + MLIRHelper::getStructIndex(rewriter, ARRAY_DATA_INDEX)); + return rewriter.create(loc, llvmArrayType, value, length, + MLIRHelper::getStructIndex(rewriter, ARRAY_SIZE_INDEX)); + } + + mlir::Value makeStatic(mlir_ts::ArrayType arrayType, mlir::Value data, mlir::Value length) + { + return make(arrayType, data, length); + } + + mlir::Value zero(mlir_ts::ArrayType arrayType) + { + return rewriter.create(op->getLoc(), typeConverter->convertType(arrayType)); + } + + mlir::Value undef(mlir_ts::ArrayType arrayType) + { + return rewriter.create(op->getLoc(), typeConverter->convertType(arrayType)); + } + + private: + mlir::Value fieldAddress(mlir_ts::ArrayType arrayType, mlir::Value header, int32_t index) + { + TypeHelper th(rewriter); + return rewriter.create(op->getLoc(), th.getPtrType(), typeConverter->convertType(arrayType), header, + ArrayRef{0, index}); + } +}; + +#endif // MLIR_TYPESCRIPT_LOWERTOLLVM_ARRAYLAYOUT_H_ +``` + +If `LLVMCodeHelperBase.h` puts its class in a namespace or needs `using namespace` lines, mirror them exactly. + +- [ ] **Step 3: Route the `LowerToLLVM.cpp` sites through it.** Add `#include "TypeScript/LowerToLLVM/ArrayLayout.h"` next to the other `LowerToLLVM/` includes. In each pattern, construct `ArrayLayout layout(op, rewriter, getTypeConverter(), tsLlvmContext->compileOptions);` and replace, keeping every other line and the order of the remaining instructions: + - `LengthOfOpLowering`: `rewriter.replaceOp(op, layout.length(arrayType, transformed.getOp()))` where `arrayType = cast(op.getOp().getType())`. If `LengthOf` also accepts strings or const arrays (`TypeScript_ArrayLike`), keep the existing code for those types and use the layout only for `ArrayType`. + - `SetLengthOf`, `ArrayPush`, `ArrayPop`, `ArrayUnshift`, `ArrayShift`, `ArraySplice`: replace the two GEPs `currentPtrPtr = GEP(llvmArrayType, transformed.getOp(), {0, ARRAY_DATA_INDEX})` and `countAsIndexTypePtr = GEP(..., {0, ARRAY_SIZE_INDEX})` with + + ```cpp + auto header = layout.headerForUpdate(arrayType, transformed.getOp()); + auto currentPtrPtr = layout.dataAddress(arrayType, header); + // ... (the load of currentPtr stays where it is) + auto countAsIndexTypePtr = layout.lengthAddress(arrayType, header); + ``` + - `CreateArray`, `NewEmptyArray`, `NewArray`: replace the `UndefOp` + two `InsertValueOp` with `layout.make(arrayType, allocated, )`. `NewEmptyArray` inserts `size0` with a raw index 1 - same call. + - `ArrayViewOpLowering`: `arrayPtr = layout.data(arrayType, transformed.getOp())`, result `layout.make(arrayType, arrayOffset, transformed.getCount())`. + +- [ ] **Step 4: Build and compare the IR.** + +```bash +cmake --build __build/tslang/windows-msbuild-2026-release --config Release --parallel 8 --target tslang 2>&1 | grep -E "error C|error:" | head +bash "$S/ir-snapshot.sh" /i/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-release/bin/tslang.exe "$S/t1" +diff -rq "$S/base" "$S/t1" | head +``` + +Expected: no build error; `diff -rq` prints nothing. Any difference is a refactoring mistake: open the two files with `diff` and fix the site (usually an instruction order change). + +- [ ] **Step 5: Commit.** + +```bash +git add tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h tslang/lib/TypeScript/LowerToLLVM.cpp +git commit -m "Lowering: one helper knows the array layout (array ops in LowerToLLVM.cpp) + +ArrayLayout reads and writes an array's data and length, and makes one. +No change to the generated IR: --emit=llvm identical for every suite test +under gc, rc and own. + +Co-Authored-By: Claude Opus 5.5 " +``` + +### Task 2: the header sites, then PR 1 + +**Files:** +- Modify: `tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h` (array to `Ref`/opaque ~223-240; `castToArrayType` ~1095-1150; truthiness ~475; `extractArrayPtr` ~1293) +- Modify: `tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelper.h` (`getReadOnlyRTArray` ~436; `GetAddressOfArrayElement` ~849) +- Modify: `tslang/include/TypeScript/LowerToLLVM/UnaryBinLogicalOrHelper.h` (array `===` ~271) +- Modify: `tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h` (retain ~1162; `buildArrayBody` ~1280) + +**Interfaces:** +- Consumes: `ArrayLayout` from Task 1. +- Produces: `CastLogicHelper::extractArrayPtr` now returns `layout.identity(...)` (it is only used for identity: truthiness and `===`); a new `CastLogicHelper::extractArrayData` returns `layout.data(...)` for the `Ref`/opaque casts. + +- [ ] **Step 1: Include and route.** Include `ArrayLayout.h` in each of the four headers (check for an include cycle: `ArrayLayout.h` must not include `LLVMCodeHelper.h` or `CastLogicHelper.h`). Then: + - `CastLogicHelper.h` ~223-240: both `ExtractValueOp ... ARRAY_DATA_INDEX` become `ArrayLayout(op, rewriter, tch.typeConverter, compileOptions).data(arrayType, in)`. Use whatever `TypeConverter` pointer `CastLogicHelper` already holds (it constructs `LLVMCodeHelper` somewhere - copy that). + - `castToArrayType`: `isUndef` returns `layout.zero(arrayType)` / `layout.undef(arrayType)` in its two branches; the final `UndefOp`+`InsertValueOp`x2 becomes `layout.make(arrayType, arrayPtr, sizeValue)`. + - `extractArrayPtr` (~1293): keep the `DialectCastOp`, then `return layout.identity(arrayType, inAsLLVMType);`. + - `LLVMCodeHelper::getReadOnlyRTArray`: `return ArrayLayout(op, rewriter, typeConverter, compileOptions).makeStatic(originalArrayType, itemValArrayPtr, sizeValue);` (keep `itemValArrayPtr` and `sizeValue` computed as now). + - `LLVMCodeHelper::GetAddressOfArrayElement`: for `ArrayType`, `dataPtr = ArrayLayout(...).data(arrayType, arrayOrStringOrTuple)`. + - `UnaryBinLogicalOrHelper.h`: nothing to change if it only calls `castLogic.extractArrayPtr` (it does) - verify. + - `OwnershipRoutineLogic.h` retain (~1162): `emitIncRef(LoadOp(ptr, layout.dataAddress(arrayType, layout.headerForRead(arrayType, slotPtr))))`; `buildArrayBody`: `dataSlot = layout.dataAddress(arrayType, layout.headerForRead(arrayType, slotPtr))`, `sizeSlot = layout.lengthAddress(arrayType, )`. Compute the header once, before `emitIfLastReference`. + +- [ ] **Step 2: Check none are left.** + +```bash +cd /i/TypeScriptCompiler/tslang && grep -rn "ARRAY_DATA_INDEX\|ARRAY_SIZE_INDEX" lib include | grep -v "ArrayLayout.h\|Defines.h" +``` + +Expected: no output. Also check `grep -n "getStructIndex(rewriter, [01])" include/TypeScript/LowerToLLVM/LLVMCodeHelper.h lib/TypeScript/LowerToLLVM.cpp` and confirm none of the remaining hits builds or reads an array value. + +- [ ] **Step 3: Build, compare the IR, run the suite.** + +```bash +cmake --build __build/tslang/windows-msbuild-2026-release --config Release --parallel 8 --target tslang 2>&1 | grep -E "error C|error:" | head +bash "$S/ir-snapshot.sh" /i/TypeScriptCompiler/__build/tslang/windows-msbuild-2026-release/bin/tslang.exe "$S/t2" +diff -rq "$S/base" "$S/t2" | head +cmake --build __build/tslang/windows-msbuild-2026-release --config Release --parallel 8 +cd __build/tslang/windows-msbuild-2026-release && ctest -C Release -j 12 --timeout 300 2>&1 | grep -E "tests passed|^\s+[0-9]+ - " | head +``` + +Expected: `diff` prints nothing; ctest shows only the known `test-compile-gc-defaultlib-collector` failure. + +- [ ] **Step 4: Commit, push, open PR 1.** + +```bash +git add tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelper.h tslang/include/TypeScript/LowerToLLVM/UnaryBinLogicalOrHelper.h tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h +git commit -m "Lowering: casts, constants and ownership routines read arrays through ArrayLayout + +The last places outside the helper that knew the { data, length } layout. +No change to the generated IR. + +Co-Authored-By: Claude Opus 5.5 " +gh auth switch --user ASDAlexander77 +git push -u origin array-reference +gh pr create --base main --head array-reference --title "Lowering: one helper knows the array layout (#453 step 1)" --body-file +``` + +The PR body states: refactor only, IR byte-identical for every suite test under gc/rc/own, step 1 of the spec, ends with `🤖 Generated with [Claude Code](https://claude.com/claude-code)`. After PR 1 merges, PR 2's work continues on a new branch `array-reference-switch` from `origin/main` (`git checkout --no-track -b array-reference-switch origin/main`). + +--- + +## PR 2: the switch + +### Task 3: a rest element is a copy (#477) + +**Files:** +- Modify: `tslang/lib/TypeScript/MLIRGenVariables.cpp` (~lines 330-395, the two `isDotDotDot` branches) +- Create: `tslang/test/tester/tests/00array_rest_copy.ts` +- Modify: `tslang/test/tester/CMakeLists.txt` (register; rc and none corpora) + +**Interfaces:** +- Consumes: nothing new (old layout still in place - this task is layout-independent). +- Produces: MLIRGen no longer emits `ts.ArrayView` for a rest element. + +- [ ] **Step 1: Write the failing test** `00array_rest_copy.ts` (CRLF line endings, like the other tests): + +```ts +// A destructuring rest element is a new array, not a view into the source (#477). +class Box { + constructor(public v: number) {} +} + +function main() { + const src: number[] = [1, 2, 3]; + const [first, ...rest] = src; + assert(first == 1, "first"); + rest[0] = 20; + assert(src[1] == 2, "a write to the rest leaks into the source"); + rest.push(4); + assert(rest.length == 3 && rest[2] == 4, "push onto the rest"); + assert(src.length == 3 && src[2] == 3, "the source is unchanged"); + + const [a, b, ...none] = [1]; + assert(none.length == 0, "the rest of a shorter array is empty"); + + const boxes: Box[] = [new Box(1), new Box(2), new Box(3)]; + const [head, ...tail] = boxes; + tail.push(new Box(4)); + assert(head.v == 1 && tail.length == 3 && tail[0].v == 2 && tail[2].v == 4, "class elements"); + + const words: string[] = ["a", "b", "c"]; + const [w0, ...ws] = words; + ws.push("d"); + assert(w0 == "a" && ws.length == 3 && ws[2] == "d" && words.length == 3, "string elements"); + + print("done."); +} +``` + +Run: `__build/tslang/windows-msbuild-2026-release/bin/tslang.exe --emit=jit --no-default-lib -mm=gc tslang/test/tester/tests/00array_rest_copy.ts`. Expected: assertion failure "a write to the rest leaks into the source". Under `-mm=rc`: exit 127. + +Also check `ts.ArrayView` has no other creator: `grep -rn "create" tslang/lib`. Expected: only the two in `MLIRGenVariables.cpp`. + +- [ ] **Step 2: Build the copy in MLIRGen.** In both `isDotDotDot` branches replace the `LengthOfOp`/`ArithmeticBinaryOp`/`ArrayViewOp` lines with a call to a new `MLIRGenImpl` helper, declared in `MLIRGenImpl.h` next to the other `mlirGen*` helpers and defined in `MLIRGenVariables.cpp`: + +```cpp +// `[a, ...rest] = src`: rest is a new array holding src[index..] (#477) - built as +// const .rest: T[] = []; for (let .i = index; .i < .src.length; .i++) .rest.push(.src[.i]); +// so the ownership passes see a fresh array and element pushes, as for any other array +ValueOrLogicalResult MLIRGenImpl::mlirGenArrayRestCopy(mlir::Location location, mlir_ts::ArrayType arrayType, + mlir::Value source, int64_t index, const GenContext &genContext) +{ + SymbolTableScopeT varScope(symbolTable); + + auto srcVarDecl = std::make_shared(".rest_src", arrayType, location); + DECLARE(srcVarDecl, source); + + auto empty = builder.create(location, arrayType); + auto restVarDecl = std::make_shared(".rest", arrayType, location); + DECLARE(restVarDecl, empty); + + NodeFactory nf(NodeFactoryFlags::None); + auto _src = nf.createIdentifier(S(".rest_src")); + auto _rest = nf.createIdentifier(S(".rest")); + auto _i = nf.createIdentifier(S(".rest_i")); + + NodeArray declarations; + declarations.push_back(nf.createVariableDeclaration( + _i, undefined, undefined, nf.createNumericLiteral(S(std::to_string(index).c_str())))); + auto initVars = nf.createVariableDeclarationList(declarations, NodeFlags::Let); + auto cond = nf.createBinaryExpression(_i, nf.createToken(SyntaxKind::LessThanToken), + nf.createPropertyAccessExpression(_src, nf.createIdentifier(S(LENGTH_FIELD_NAME)))); + auto incr = nf.createPrefixUnaryExpression(nf.createToken(SyntaxKind::PlusPlusToken), _i); + NodeArray pushArgs; + pushArgs.push_back(nf.createElementAccessExpression(_src, _i)); + auto push = nf.createExpressionStatement(nf.createCallExpression( + nf.createPropertyAccessExpression(_rest, nf.createIdentifier(S("push"))), undefined, pushArgs)); + + if (mlir::failed(mlirGen(nf.createForStatement(initVars, cond, incr, push), genContext))) + { + return mlir::failure(); + } + + return resolveIdentifier(location, ".rest", genContext); +} +``` + +Copy the exact `DECLARE`, `NodeFactory`, `S(...)` and `resolveIdentifier` usage from the numeric-widening code in `MLIRGenCast.cpp` (~line 1140-1185): match how it returns `.dst_array` after the loop (it may read the variable differently from `resolveIdentifier`; use the same call it uses). In the `ConstArrayType` branch pass the already-cast `arrayValue`; in the `ArrayType` branch pass `init`. + +- [ ] **Step 3: Run the test under every model.** + +```bash +for mm in gc rc none own; do __build/tslang/windows-msbuild-2026-release/bin/tslang.exe --emit=jit --no-default-lib -mm=$mm tslang/test/tester/tests/00array_rest_copy.ts; echo "$mm exit=$?"; done +``` + +Expected: `done.` and exit 0 for gc, rc, none. Under own, an ownership error on the class-element or string section is acceptable (own's rules); the number section must compile - if own rejects the whole file, keep the test out of own and say so in the report. + +- [ ] **Step 4: Register the test** with the scratchpad script (it adds `test-compile-00-` and `test-jit-00-` after the anchor's lines and the file to the rc/none corpus list after the corpus anchor, keeping CRLF): + +```bash +cd /i/TypeScriptCompiler && python "$R" array-rest-copy 00array_rest_copy.ts 00array5_deconst.ts 00array5_deconst.ts +``` + +where `R="C:/Users/duzha/AppData/Local/Temp/claude/I--TypeScriptCompiler/f5d6099f-c9ad-473f-a7eb-6f10e2a42c61/scratchpad/reinsert.py"`. Anchors used by this plan (each occurs once in its list): `00array5_deconst.ts` (rest copy), `00array2.ts` (reference), `00array3.ts` (zero slots), `00array6.ts` (static nested), `00array7.ts` (main argv), `00class_new.ts` (growth). + +- [ ] **Step 5: Full suite, commit.** + +```bash +cmake --build __build/tslang/windows-msbuild-2026-release --config Release --parallel 8 +cd __build/tslang/windows-msbuild-2026-release && ctest -C Release -j 12 --timeout 300 2>&1 | grep -E "tests passed|^\s+[0-9]+ - " | head +cd /i/TypeScriptCompiler +git add tslang/lib/TypeScript/MLIRGenVariables.cpp tslang/lib/TypeScript/MLIRGenImpl.h tslang/test/tester/tests/00array_rest_copy.ts tslang/test/tester/CMakeLists.txt +git commit -m "A destructuring rest element is a new array (#477) + +Closes #477 + +Co-Authored-By: Claude Opus 5.5 " +``` + +### Task 4: the switch + +**Files:** +- Modify: `tslang/include/TypeScript/Defines.h` (add `#define ARRAY_CAPACITY_INDEX 2` after `ARRAY_SIZE_INDEX`) +- Modify: `tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h` (the header implementation) +- Modify: `tslang/lib/TypeScript/LowerToLLVM.cpp` (type conversion ~7229: `ArrayType` -> `ptr`) +- Modify: `tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h` (retain and `buildArrayBody`) +- Modify: `tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h` (`castToArrayType` `byValue = false` path copies) +- Modify: `tslang/tslang/jit.cpp` (`addEntryThunk`: drop the `{ptr, iN}` struct branch) +- Create: `tslang/test/tester/tests/00array_reference.ts`, `00array_zero_slots.ts`, `00array_static_nested.ts` +- Modify: `tslang/test/tester/CMakeLists.txt` + +**Interfaces:** +- Consumes: `ArrayLayout` API from Task 1 (unchanged signatures). +- Produces: `ArrayLayout::headerType()` returning `LLVM::LLVMStructType` `{ptr, index, index}`; `ArrayLayout::capacityAddress(arrayType, header)`; an array's LLVM type is `ptr`. + +- [ ] **Step 1: Write the failing tests.** `00array_reference.ts` - the 14 shapes of spec §2 as asserts: + +```ts +// An array is a reference: a change of length through any name is seen through every other (#453). +class Holder { items: number[] = [1]; } +type Rec = { items: number[] }; + +function pushParam(into: number[]) { into.push(2); } +function pushField(h: Holder) { h.items.push(2); } +function pushRec(r: Rec) { r.items.push(2); } +function grow(into: number[]) { into.push(2); into.push(3); into.push(4); into.push(5); } +function setLen(into: number[]) { into.length = 0; } +function popParam(into: number[]) { into.pop(); } +function setElem(into: number[]) { into[0] = 9; } + +function main() { + const a1: number[] = [1]; setElem(a1); assert(a1[0] == 9, "1 element write through a parameter"); + const a2: number[] = [1]; pushParam(a2); assert(a2.length == 2 && a2[1] == 2, "2 push through a parameter"); + const a3: number[] = [1, 2]; popParam(a3); assert(a3.length == 1, "3 pop through a parameter"); + const a4: number[] = [1, 2]; setLen(a4); assert(a4.length == 0, "4 length= through a parameter"); + const a5: number[] = [1]; const b5 = a5; b5.push(2); assert(a5.length == 2, "5 local alias"); + const h6 = new Holder(); pushField(h6); assert(h6.items.length == 2, "6 class field"); + const h7 = new Holder(); const it7 = h7.items; it7.push(2); assert(h7.items.length == 2, "7 field alias"); + const r8: Rec = { items: [1] }; pushRec(r8); assert(r8.items.length == 2, "8 record field"); + const a9: number[] = [1]; const f9 = () => { a9.push(2); }; f9(); assert(a9.length == 2, "9 closure"); + const aa10: number[][] = [[1]]; aa10[0].push(2); assert(aa10[0].length == 2, "10 nested"); + const aa11: number[][] = [[1]]; const in11 = aa11[0]; in11.push(2); assert(aa11[0].length == 2, "11 nested alias"); + const a12: number[] = [1]; const b12 = a12; assert(a12 === b12, "12 identity"); + const a13: number[] = [7]; grow(a13); assert(a13.length == 5 && a13[0] == 7 && a13[4] == 5, "13 growth"); + const a14: number[] = [1]; const h14 = new Holder(); h14.items = a14; a14.push(2); assert(h14.items.length == 2, "14 stored into a field"); + + const a15: number[] = [1]; const x15: any = a15; (x15 as number[]).push(2); assert(a15.length == 2, "15 through any"); + const c16: number[] = []; const d16: number[] = []; + assert(!(c16 === d16), "16 two empty arrays are different arrays"); + let truthy = false; if (c16) { truthy = true; } assert(truthy, "16 an empty array is truthy"); + let n16: number[] | null = null; let falsy = true; if (n16) { falsy = false; } assert(falsy, "16 null is falsy"); + + print("done."); +} +``` + +`00array_zero_slots.ts` (R1): + +```ts +// Zeroed memory is an empty array: nested arrays made by length, and a field with no initializer. +class Bag { + items: number[]; + add(v: number) { this.items.push(v); } +} + +function main() { + const aa = new Array(2); + assert(aa[0].length == 0, "a zeroed element reads as empty"); + aa[0].push(1); + aa[0].push(2); + assert(aa[0].length == 2 && aa[0][1] == 2 && aa[1].length == 0, "push into a zeroed element"); + + const b = new Bag(); + b.add(5); + assert(b.items.length == 1 && b.items[0] == 5, "push into a field with no initializer"); + + print("done."); +} +``` + +`00array_static_nested.ts` (Review Focus 3): + +```ts +// Constant arrays inside global data keep their elements, and are never freed. +const aa: number[][] = [[1], [2, 3]]; +const t: [number, number[]] = [1, [4, 5]]; + +function main() { + assert(aa.length == 2 && aa[1].length == 2 && aa[1][1] == 3, "nested constant array"); + assert(t[1].length == 2 && t[1][0] == 4, "array in a constant tuple"); + for (let i = 0; i < 3; i++) { + const inner = aa[1]; + assert(inner[0] == 2, "read through a local, repeatedly"); + } + print("done."); +} +``` + +Run each with `--emit=jit --no-default-lib` under gc and rc. Expected before the switch: `00array_reference.ts` fails at "2 push through a parameter" (gc) / exit 127 (rc); `00array_zero_slots.ts` and `00array_static_nested.ts` pass (they pin today's behaviour that the switch must keep). If `new Array(2)` or a field with no initializer does not compile today, drop that line and note it in the report. + +- [ ] **Step 2: Switch the representation in `ArrayLayout.h`.** Replace the PR 1 bodies: + +```cpp + // { data, length, capacity }: what an array value points to + LLVM::LLVMStructType headerType() + { + TypeHelper th(rewriter); + auto llvmIndexType = typeConverter->convertType(th.getIndexType()); + return LLVM::LLVMStructType::getLiteral(rewriter.getContext(), {th.getPtrType(), llvmIndexType, llvmIndexType}, + false); + } + + // a slot holding null - a zeroed element or field, `undefined` as an array - gets an empty + // header first, so a change made through the slot is kept (spec ruling R1) + mlir::Value headerForUpdate(mlir_ts::ArrayType arrayType, mlir::Value slot) + { + TypeHelper th(rewriter); + CodeLogicHelper clh(op, rewriter); + auto loc = op->getLoc(); + auto ptrType = th.getPtrType(); + mlir::Value header = rewriter.create(loc, ptrType, slot); + auto isNull = rewriter.create(loc, LLVM::ICmpPredicate::eq, header, + rewriter.create(loc, ptrType)); + return clh.conditionalExpressionLowering( + loc, ptrType, isNull, + [&](OpBuilder &, Location) -> mlir::Value { + auto fresh = MemoryAlloc(headerType(), MemoryAllocSet::Zero); + rewriter.create(loc, fresh, slot); + return fresh; + }, + [&](OpBuilder &, Location) -> mlir::Value { return header; }); + } + + mlir::Value headerForRead(mlir_ts::ArrayType, mlir::Value slot) + { + TypeHelper th(rewriter); + return rewriter.create(op->getLoc(), th.getPtrType(), slot); + } + + mlir::Value capacityAddress(mlir_ts::ArrayType arrayType, mlir::Value header) + { + return fieldAddress(arrayType, header, ARRAY_CAPACITY_INDEX); + } + + // a null header reads as an empty array (R1) + mlir::Value data(mlir_ts::ArrayType arrayType, mlir::Value array) + { + TypeHelper th(rewriter); + return loadOrZero(array, th.getPtrType(), ARRAY_DATA_INDEX); + } + + mlir::Value length(mlir_ts::ArrayType arrayType, mlir::Value array) + { + TypeHelper th(rewriter); + return loadOrZero(array, typeConverter->convertType(th.getIndexType()), ARRAY_SIZE_INDEX); + } + + mlir::Value identity(mlir_ts::ArrayType, mlir::Value array) + { + return array; + } + + mlir::Value make(mlir_ts::ArrayType arrayType, mlir::Value data, mlir::Value length) + { + auto loc = op->getLoc(); + auto header = MemoryAlloc(headerType()); + rewriter.create(loc, data, fieldAddress(arrayType, header, ARRAY_DATA_INDEX)); + rewriter.create(loc, length, fieldAddress(arrayType, header, ARRAY_SIZE_INDEX)); + rewriter.create(loc, length, fieldAddress(arrayType, header, ARRAY_CAPACITY_INDEX)); + return header; + } + + mlir::Value zero(mlir_ts::ArrayType) + { + TypeHelper th(rewriter); + return rewriter.create(op->getLoc(), th.getPtrType()); + } + + mlir::Value undef(mlir_ts::ArrayType arrayType) + { + return zero(arrayType); + } + + private: + mlir::Value fieldAddress(mlir_ts::ArrayType, mlir::Value header, int32_t index) + { + TypeHelper th(rewriter); + return rewriter.create(op->getLoc(), th.getPtrType(), headerType(), header, + ArrayRef{0, index}); + } + + mlir::Value loadOrZero(mlir::Value header, mlir::Type fieldType, int32_t index) + { + TypeHelper th(rewriter); + CodeLogicHelper clh(op, rewriter); + auto loc = op->getLoc(); + auto isSet = rewriter.create(loc, LLVM::ICmpPredicate::ne, header, + rewriter.create(loc, th.getPtrType())); + return clh.conditionalExpressionLowering( + loc, fieldType, isSet, + [&](OpBuilder &, Location) -> mlir::Value { + return rewriter.create( + loc, fieldType, + rewriter.create(loc, th.getPtrType(), headerType(), header, ArrayRef{0, index})); + }, + [&](OpBuilder &, Location) -> mlir::Value { return rewriter.create(loc, fieldType); }); + } +``` + +`CodeLogicHelper::conditionalExpressionLowering` is used the same way by `StringCopyOpLowering` (`LowerToLLVM.cpp` ~920); if `ArrayLayout.h` cannot include `CodeLogicHelper.h` without a cycle, build the two blocks by hand as `SetLengthOfOpLowering` does (~700-720). `MemoryAlloc(mlir::Type, MemoryAllocSet)` is in `LLVMCodeHelperBase`. + +**Constant arrays in global data.** `makeStatic(arrayType, data, length)` from Task 1 cannot work with a header: `getReadOnlyRTArray` runs inside a global's initializer region, where nothing can be allocated, and a header global's own initializer cannot use a value computed in another region. Replace it with: + +```cpp + // a constant array in global data: a header global "ah_" holding + // { data, length, capacity = length }, preceded under rc and own by an immortal block word (as + // getOrCreateGlobalArray does for the data, LLVMCodeHelper.h ~555). Returns its address. + mlir::Value makeStatic(mlir_ts::ArrayType arrayType, StringRef dataGlobalName, int64_t dataOffsetBytes, + int64_t length) + { + auto loc = op->getLoc(); + auto parentModule = op->getParentOfType(); + TypeHelper th(rewriter); + auto llvmIndexType = typeConverter->convertType(th.getIndexType()); + auto withWord = compileOptions.tracksOwnership(); + auto headerName = ("ah_" + dataGlobalName).str(); + + auto global = parentModule.lookupSymbol(headerName); + if (!global) + { + OpBuilder::InsertionGuard guard(rewriter); + rewriter.setInsertionPointToStart(parentModule.getBody()); + mlir::Type globalType = headerType(); + if (withWord) + { + globalType = LLVM::LLVMStructType::getLiteral(rewriter.getContext(), {llvmIndexType, headerType()}, + /*isPacked=*/true); + } + + global = rewriter.create(loc, globalType, /*isConstant=*/true, LLVM::Linkage::Internal, + headerName, mlir::Attribute{}); + global.setAlignment(getHeapBlockHeaderSize()); + + auto ®ion = global.getInitializerRegion(); + rewriter.setInsertionPointToStart(rewriter.createBlock(®ion)); + auto llvmLength = rewriter.create(loc, llvmIndexType, + rewriter.getIntegerAttr(llvmIndexType, length)); + mlir::Value dataPtr = rewriter.create(loc, th.getPtrType(), dataGlobalName); + if (dataOffsetBytes != 0) + { + dataPtr = rewriter.create( + loc, th.getPtrType(), th.getI8Type(), dataPtr, + ValueRange{rewriter.create(loc, llvmIndexType, + rewriter.getIntegerAttr(llvmIndexType, dataOffsetBytes))}); + } + + mlir::Value headerVal = rewriter.create(loc, headerType()); + headerVal = rewriter.create(loc, headerVal, dataPtr, MLIRHelper::getStructIndex(rewriter, ARRAY_DATA_INDEX)); + headerVal = rewriter.create(loc, headerVal, llvmLength, MLIRHelper::getStructIndex(rewriter, ARRAY_SIZE_INDEX)); + headerVal = rewriter.create(loc, headerVal, llvmLength, MLIRHelper::getStructIndex(rewriter, ARRAY_CAPACITY_INDEX)); + mlir::Value globalVal = headerVal; + if (withWord) + { + globalVal = rewriter.create(loc, globalType); + globalVal = rewriter.create( + loc, globalVal, + rewriter.create(loc, llvmIndexType, rewriter.getIntegerAttr(llvmIndexType, HEAP_BLOCK_IMMORTAL)), + MLIRHelper::getStructIndex(rewriter, 0)); + globalVal = rewriter.create(loc, globalVal, headerVal, MLIRHelper::getStructIndex(rewriter, 1)); + } + + rewriter.create(loc, ValueRange{globalVal}); + } + + mlir::Value address = rewriter.create(loc, global); + return withWord ? getPayloadPtrFromBlockPtr(loc, address, llvmIndexType) : address; + } +``` + +In `getReadOnlyRTArray` (`LLVMCodeHelper.h` ~436), call `makeStatic(originalArrayType, , , arrayValue.size())`. The data global's name is the `vecVarName` `getOrCreateGlobalArray(originalElementType, size, arrayAttr)` computes - split that overload so the name is returned too - and the payload offset is `getHeapBlockHeaderSize()` when `tracksOwnership()`, else 0 (what `getOrCreateGlobalArray` adds to the global's address). Match how `getOrCreateGlobalArray` sets the insertion point for a new global (`seekLast`) if globals must stay ordered. + +- [ ] **Step 3: Type conversion.** `LowerToLLVM.cpp` ~7229: + +```cpp + // an array is a reference to its header { data, length, capacity } (ArrayLayout.h) + converter.addConversion([&](mlir_ts::ArrayType type) { + return LLVM::LLVMPointerType::get(m.getContext()); + }); +``` + +- [ ] **Step 4: rc and own routines** (`OwnershipRoutineLogic.h`): + - retain (~1162): `emitIncRef(layout.headerForRead(arrayType, slotPtr));` (the header is the counted block). + - `buildArrayBody`: `header = layout.headerForRead(arrayType, slotPtr)`; `emitIfLastReference(header, ...)` whose body: release elements `[0, length)` reading `data`/`length` through `layout.dataAddress(arrayType, header)` / `layout.lengthAddress(arrayType, header)` (the header is non-null inside), then `emitFreeBlock()`, then `emitFreeBlock(header)`. Read the current body first: if it already frees the data block, keep that and add the header's free after it. Update the two comments ("an array value is { data, length }...") to describe the header. + +- [ ] **Step 5: The const-array cast copies on both paths** (`CastLogicHelper.h` `castToArrayType`, the `else` of `if (byValue)`): make it allocate and `MemoryCopyOp` exactly as the `byValue` branch does (delete the "copy ptr only" branch and its TODO), so `push` never reallocates static data. + +- [ ] **Step 6: JIT thunk** (`tslang/tslang/jit.cpp` `addEntryThunk`): delete the `index == 1 && arrayType && ...` struct branch and the `arrayType` local; update the comment above the function (an array is now a pointer; `string[]` argv arrives in Task 5). Until Task 5, a `main(argc, argv: string[])` is broken - no suite test uses one (`grep -rln "argv: string\[\]" tslang/test` is empty). + +- [ ] **Step 7: Build and run the new tests under every model.** + +```bash +cmake --build __build/tslang/windows-msbuild-2026-release --config Release --parallel 8 --target tslang 2>&1 | grep -E "error C|error:" | head +for t in 00array_reference 00array_zero_slots 00array_static_nested 00array_rest_copy; do for mm in gc rc none; do + __build/tslang/windows-msbuild-2026-release/bin/tslang.exe --emit=jit --no-default-lib -mm=$mm tslang/test/tester/tests/$t.ts > "$S/o.txt" 2>&1; echo "$t $mm exit=$? $(tail -1 "$S/o.txt")"; done; done +``` + +Expected: every line `exit=0 done.` + +- [ ] **Step 8: Register the three tests** (Task 3 step 4's script; anchors `00array2.ts`, `00array3.ts`, `00array6.ts`): + +```bash +python "$R" array-reference 00array_reference.ts 00array2.ts 00array2.ts +python "$R" array-zero-slots 00array_zero_slots.ts 00array3.ts 00array3.ts +python "$R" array-static-nested 00array_static_nested.ts 00array6.ts 00array6.ts +``` + +Then run the full suite: + +```bash +cmake --build __build/tslang/windows-msbuild-2026-release --config Release --parallel 8 +cd __build/tslang/windows-msbuild-2026-release && ctest -C Release -j 12 --timeout 300 2>&1 | grep -E "tests passed|^\s+[0-9]+ - " | head -40 +``` + +Expected: only `test-compile-gc-defaultlib-collector` fails. Any other failure is a site the switch missed: reproduce it with `--emit=jit --no-default-lib` and the runner's flags, read its `--emit=llvm`, find the array access that still assumes a struct (an `extractvalue`/`insertvalue` on a `ptr`, or a two-word union payload), and route it through `ArrayLayout`. Record each in the report. + +- [ ] **Step 9: Commit.** + +```bash +git add -A tslang/include tslang/lib tslang/tslang/jit.cpp tslang/test/tester/tests/00array_reference.ts tslang/test/tester/tests/00array_zero_slots.ts tslang/test/tester/tests/00array_static_nested.ts tslang/test/tester/CMakeLists.txt +git commit -m "An array is a reference to a header that never moves (#453) + +An array value is one pointer to { data, length, capacity }. Assigning, +passing and storing an array copy the pointer, so a push, pop, splice or +length= through any name is seen through every other. Under rc the header +is the counted block; a null header reads as an empty array and gets a +header before a change through its slot. + +Co-Authored-By: Claude Opus 5.5 " +``` + +### Task 5: `main(argc, argv: string[])` + +**Files:** +- Modify: `tslang/include/TypeScript/TypeScriptOps.td` (new op; build with `--parallel 3` - editing this file needs `-j 3`/`-j 4` or MSVC fails with C1060) +- Modify: `tslang/lib/TypeScript/MLIRGenFunctions.cpp` (`mlirGenParameters` ~192, `mlirGenFunctionParams` ~1360) +- Modify: `tslang/lib/TypeScript/LowerToLLVM.cpp` (new lowering pattern; add to the pattern list ~8140) +- Modify: `tslang/lib/TypeScript/LowerToAffineLoops.cpp` (~2660 legal-op list) +- Modify: `tslang/lib/TypeScript/OwnershipFacts.h` (~298 fresh-op list) +- Create: `tslang/test/tester/tests/00main_argv_array.ts`; Modify: `tslang/test/tester/CMakeLists.txt` + +**Interfaces:** +- Consumes: `ArrayLayout::make`. +- Produces: `ts.ArrayFromCStrings`. + +- [ ] **Step 1: Write the failing test** `00main_argv_array.ts`: + +```ts +// `argv` as a `string[]`: an array of the program's arguments, each a copy of the C string, made +// with the memory model's allocator. argv[0] is the program (the input file under the JIT). +function main(argc: int, argv: string[]): int { + assert(argv.length == argc, "argv.length"); + assert(argv.length >= 1 && argv[0].length > 0, "argv[0]"); + argv.push("extra"); + assert(argv.length == argc + 1, "argv is an ordinary array"); + print("done."); + return 0; +} +``` + +Run under JIT and compiled (`test-runner` style): `tslang.exe --emit=jit --no-default-lib tslang/test/tester/tests/00main_argv_array.ts`. Expected now: crash or assertion failure (Task 4 removed the struct branch). + +- [ ] **Step 2: The op** (`TypeScriptOps.td`, next to `TypeScript_StringCopyOp`): + +```tablegen +// `main(argc, argv: string[])`: an array holding a copy of each of C's `argc` strings at `argv` +def TypeScript_ArrayFromCStringsOp : TypeScript_Op<"ArrayFromCStrings"> { + let arguments = (ins Index:$count, TypeScript_Ref:$argv); + let results = (outs Res:$result); +} +``` + +Use the operand constraint other ops use for a `Ref` operand (look at how `TypeScript_RefType`/`AnyRef` is spelled in the file). Add `mlir_ts::ArrayFromCStringsOp` to the legal-op list in `LowerToAffineLoops.cpp` (~2660) and to the fresh-op `isa<...>` in `OwnershipFacts.h` (~298). + +- [ ] **Step 3: Lowering.** A pattern in `LowerToLLVM.cpp`: allocate `count` pointers (`ch.MemoryAlloc(count * sizeof(ptr), MemoryAllocSet::Zero)`), loop `i` in `[0, count)` storing a copy of `argv[i]` (the same `strlen + 1` / `MemoryAlloc` / `MemcpyOp` sequence as `StringCopyOpLowering`, ~920; extract that lambda into a shared static function rather than duplicating it), then `ArrayLayout::make(arrayType, data, count)`. Build the loop with blocks as `OwnershipRoutineLogic::emitCountedLoop` does (or reuse it if reachable). + +- [ ] **Step 4: MLIRGen.** In `mlirGenParameters`, when the function is `MAIN_ENTRY_NAME`, has exactly two parameters and the second's type is `ArrayType` of `StringType`: give the second parameter the type `mlir_ts::RefType::get(getStringType())` and the name `.argv`, and remember (in the `FunctionParamDOM` or a flag on the prototype) that `argv` must be made. In `mlirGenFunctionParams`, after the loop, for that function: load `argc` (cast to `index` with `mlir_ts::CastOp`), load `.argv`, create `ts.ArrayFromCStrings`, and `DECLARE` a `VariableDeclarationDOM` named with the original parameter name, type `string[]`, holding the op's result. `LowerToAffineLoops`'s shape check already accepts `Ref` for argv; the JIT thunk's pointer branch passes C's `char **`; the C runtime does too when compiled. + +- [ ] **Step 5: Run.** + +```bash +cmake --build __build/tslang/windows-msbuild-2026-release --config Release --parallel 3 --target tslang 2>&1 | grep -E "error C|error:" | head +for mm in gc rc none own; do __build/tslang/windows-msbuild-2026-release/bin/tslang.exe --emit=jit --no-default-lib -mm=$mm tslang/test/tester/tests/00main_argv_array.ts; echo "$mm exit=$?"; done +``` + +Expected: `done.` and exit 0 for every model. Register it with `python "$R" main-argv-array 00main_argv_array.ts 00array7.ts 00array7.ts` (the corpora run it under rc and none the same way; if a corpus run fails only because it passes no arguments, remove the corpus line by hand and say so in the report). Also run `00main_argc_argv.ts` (the `Ref` form) to confirm it still passes. + +- [ ] **Step 6: Full suite, commit** (`git commit -m "main(argc, argv: string[]) gets an array of copies of the C strings ..."`; mention that compiled `argv.length` was C's `envp` before). + +### Task 6: debug info + +**Files:** +- Modify: `tslang/include/TypeScript/LowerToLLVM/LLVMDebugInfo.h` (~197: `ArrayType`) +- Modify: `tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h` (~2430: `getFields` for `ArrayType`) + +- [ ] **Step 1:** `getFields` for `ArrayType` adds a third field `capacity` (`mlir::IndexType`) after `length`, keeping the order (the TODO there says debug info depends on it). Check its other callers first: `grep -rn "getFields(" tslang/lib tslang/include | head -30` - if any caller indexes array fields by position for something other than debug info, keep `getFields` as is and build the three fields inside `LLVMDebugInfo.h` instead. +- [ ] **Step 2:** In `getDITypeScriptType`, return `getDIPointerType(getDITypeWithFields(location, arrayType, to_print(arrayType), false, file, line, scope), file, line)` for an `ArrayType` - the same shape as a class (~line 455). +- [ ] **Step 3:** Run the debug-info tests: `cd __build/tslang/windows-msbuild-2026-release && ctest -C Release -R "debug-info|--di|-di-" --timeout 300`. Expected: all pass. Compile `00array_reference.ts` with `--di` (`tslang.exe --emit=exe --di ...` as the `debug-info-*.cmake` tests do) and check it links and runs. +- [ ] **Step 4:** Commit ("Debug info: an array is a pointer to { data, length, capacity }"). + +### Task 7: own, the gates, the default library, PR 2 + +**Files:** +- Create: `tslang/test/tester/own/own_array_param_push.ts`; Modify: `tslang/test/tester/CMakeLists.txt` (own test list ~2591) + +- [ ] **Step 1: own test** `own_array_param_push.ts`: + +```ts +// A borrowed array parameter is the owner's array: a push through it is the owner's (#453). +function keep(into: number[]) { + into.push(2); + into.push(3); +} + +function main() { + let a: number[] = [7]; + keep(a); + assert(a.length == 3 && a[0] == 7 && a[2] == 3, "push through a borrowed parameter"); + print("done."); +} +``` + +Add `own_array_param_push` to the `foreach(own_test ...)` list. Run `ctest -C Release -R "own-array-param-push"`: the JIT, compile and no-counting tests must pass. + +- [ ] **Step 2: own corpus.** With the corpus scripts from the own work (`C:/Users/duzha/AppData/Local/Temp/claude/I--TypeScriptCompiler/f5d6099f-c9ad-473f-a7eb-6f10e2a42c61/scratchpad/corpus465/run.sh `; it reads `OPTS` from the environment and compiles with the current build), first take a baseline with the PR base's compiler (`main.tsv`/`mainopt.tsv` there are from main 9a5cc05b and may be reused if main has not changed lowering since), then run plain and `OPTS=--opt` on the branch and compare with `join -t "$(printf '\t')" main.tsv new.tsv | awk -F'\t' '($2=="ok") != ($3=="ok")'`: no file may go from ok to failing. Files that go from failing to ok are expected (shapes own rejected for a struct reason). +- [ ] **Step 3: own memory measurement.** Run the own measurement (`measure.ps1`, after one plain test-runner run) and compare with the numbers in the own spec's last results section: flat (within noise) is the gate. +- [ ] **Step 4: Linux.** Apply the branch's diff on a WSL clone of `origin/main` and run the full suite there (`git diff origin/main > fix.patch`; a script like `scratchpad/wsl446.sh` that applies it, builds with ninja, runs `ctest -j 8 --timeout 300`, then restores the clone). Expected: 0 failures. Windows' heap hides double frees; this run is required. +- [ ] **Step 5: Default library.** Rebuild `I:\TypeScriptCompilerDefaultLib` with the new compiler for every model and both build types, into the layout `defaultlib/{dll,lib}/{debug,release}/{gc,rc,none}` (memory: set the `*_LIB_PATH` env vars per mode; they point at `I:\tslang`). Then run the default-library tests (`ctest -C Release -R defaultlib`) - `test-compile-gc-defaultlib-collector` must now pass - and the default library's own suite under every model. +- [ ] **Step 6: Commit the own test, push, open PR 2.** The PR body lists: the representation, R1-R3, the tests, the gates with their numbers, "closes #453, closes #477", and that the default library must be rebuilt after merge (CI's default-lib artifact too). Ends with `🤖 Generated with [Claude Code](https://claude.com/claude-code)`. + +--- + +## PR 3: growth + +### Task 8: capacity growth + +**Files:** +- Modify: `tslang/lib/TypeScript/LowerToLLVM.cpp` (`ArrayPush`, `ArrayUnshift`, `ArraySplice`, `ArrayPop`, `ArrayShift`, `SetLengthOf` lowerings) +- Modify: `tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h` (`ensureCapacity`) +- Create: `tslang/test/tester/tests/00array_growth.ts`; Modify: `tslang/test/tester/CMakeLists.txt` + +**Interfaces:** +- Produces: `mlir::Value ArrayLayout::ensureCapacity(mlir_ts::ArrayType arrayType, mlir::Value header, mlir::Value needed)` - returns the (possibly new) data pointer, already stored in the header, with `capacity >= needed`; new slots past the old capacity are zero. + +- [ ] **Step 1: Write the failing test** `00array_growth.ts`: + +```ts +// Arrays grow by doubling; what pop and shift vacate reads as zero when length grows again. +class Box { constructor(public v: number) {} } + +function fill(into: number[], n: number) { for (let i = 0; i < n; i++) into.push(i); } + +function main() { + const a: number[] = []; + const alias = a; + fill(a, 1000); + assert(alias.length == 1000 && alias[0] == 0 && alias[999] == 999, "1000 pushes through an alias"); + + a.pop(); + a.length = 1000; + assert(a[999] == 0, "a popped slot reads zero when length grows again"); + + const s: number[] = [1, 2, 3]; + s.shift(); + s.unshift(9); + s.push(4); + assert(s.length == 4 && s[0] == 9 && s[1] == 2 && s[3] == 4, "shift, unshift and push around a growth"); + + const boxes: Box[] = []; + for (let i = 0; i < 100; i++) boxes.push(new Box(i)); + for (let i = 0; i < 50; i++) boxes.pop(); + assert(boxes.length == 50 && boxes[49].v == 49, "class elements across growth and pops"); + + print("done."); +} +``` + +Run under gc and rc. Expected before the change: passes (behaviour is already right with exact reallocation), except possibly "a popped slot reads zero" under gc - the test pins the behaviour PR 3 must keep. Then write a timing check (not committed) in the scratchpad: 1,000,000 pushes onto an array, measured with `--emit=exe` before and after. + +- [ ] **Step 2: `ensureCapacity`** in `ArrayLayout.h`: load `capacity`; if `needed > capacity`: `newCapacity = max(4, 2 * capacity, needed)`, `data = MemoryRealloc(data, newCapacity * sizeof(element))`, zero `[capacity, newCapacity)` with `LLVM::MemsetOp`, store `data` and `newCapacity`; return the data pointer. Element size comes from `mlir_ts::SizeOfOp` as in the existing lowerings. +- [ ] **Step 3: Use it.** `push`/`unshift`/`splice` call `ensureCapacity(header, newLength)` instead of `MemoryRealloc(currentPtr, newLength * size)`. `pop`/`shift`/`splice` (removal) no longer reallocate; they zero each slot they vacate (`MemsetOp` of one element, or of the tail for `shift`'s move). `SetLengthOf`: growing calls `ensureCapacity` and zeroes `[length, n)` in every model, gc included (delete the `needsGCRuntime()` exception and rewrite its comment: after a `pop` the slots past `length` are no longer fresh memory); shrinking zeroes `[n, length)` and keeps the block. +- [ ] **Step 4: Register and run.** `python "$R" array-growth 00array_growth.ts 00class_new.ts 00class_new.ts`. Run `00array_growth.ts` under gc, rc, none (and the own corpus check from Task 7 step 2), the timing check (expect a large drop for 1,000,000 pushes), then the full Windows suite and the WSL suite. +- [ ] **Step 5: Commit, push, open PR 3** ("Arrays grow by doubling (#453 step 3)"). + +--- + +## Self-review notes + +- Spec coverage: §3 (representation, empty array, const cast both paths, rest copy, layout, ABI) - Tasks 3, 4; §4 (ownership per model) - Task 4 step 4, Task 7; §5 (helper, op signatures kept, creation/read sites, DI, main, x86) - Tasks 1, 2, 4, 5, 6 (x86: no specific code; the WSL/Windows suites include the x86 tests that exist); §6 growth - Task 8; §7 gates - Tasks 1, 2, 4, 7, 8; §8 out of scope respected. +- R2 and R3 change how §5's adapter and §3's view-as-copy are built, not what they do; the spec is amended alongside this plan. diff --git a/tslang/docs/superpowers/specs/2026-10-03-array-reference-design.md b/tslang/docs/superpowers/specs/2026-10-03-array-reference-design.md index 8f52e4532..be95778e1 100644 --- a/tslang/docs/superpowers/specs/2026-10-03-array-reference-design.md +++ b/tslang/docs/superpowers/specs/2026-10-03-array-reference-design.md @@ -63,12 +63,17 @@ struct fails. Under rc a parameter push exits with 127 and prints nothing. or a union copy the pointer. `===` and `!==` compare header pointers. - An empty array is a header with `data = null`, `length = 0`, `capacity = 0`. There is no shared empty header: each `[]` is a distinct array. +- A null header (zeroed memory: an element of `new Array(n)`, a field with no initializer, + `undefined` cast to an array) reads as an empty array, and an op that changes an array through + its slot stores a fresh empty header into a slot holding null first. This keeps today's + behaviour, where a zeroed `{ data, length }` is an empty array (plan ruling R1). - `ConstArray` (a literal's static data) is unchanged. The cast from `ConstArray` to `T[]` makes a header and copies the data into a fresh block on both of its paths; the path that today points the struct at the static data (`byValue = false`) copies as well, since `push` would otherwise reallocate static memory. -- `[a, ...rest] = src` gives `rest` a new array holding a copy of the slice (#477); `ts.ArrayView` - becomes a copy. +- `[a, ...rest] = src` gives `rest` a new array holding a copy of the slice (#477). MLIRGen builds + it from a new empty array and a loop of pushes instead of emitting `ts.ArrayView`, so the + ownership passes see a fresh array (plan ruling R3). - Layout: an array field, element, tuple slot or union payload shrinks from two words to one pointer; `T[] | undefined` is the pointer and its tag. - ABI: every function that takes or returns an array changes. Objects built before the switch do @@ -116,10 +121,11 @@ unchanged. the retain/release routines (§4). - **Debug info** (`LLVMDebugInfo.h`): an array's debug type becomes a pointer to a `{ data, length, capacity }` struct, so a debugger still shows the elements. -- **`main(argc, argv: string[])`:** lowering emits a C `main(int, char **)` adapter that builds a - `string[]` header from `argc`/`argv` and calls the user's `main`. Today the struct is read from - C's `argv` and `envp`, which is the open `argv.length` = envp bug; the adapter fixes it. A - `Ref` argv still receives C's `char **` directly. Both AOT and JIT. +- **`main(argc, argv: string[])`:** MLIRGen gives `main` a `Ref` second parameter, so the + entry point takes C's `char **`, and binds `argv` to a new op `ts.ArrayFromCStrings(argc, argv)` + that makes the array with the model's allocator, copying each string (plan ruling R2). Today + the struct is read from C's `argv` and `envp`, which is the open `argv.length` = envp bug; this + fixes it. A `Ref` argv still receives C's `char **` directly. Both AOT and JIT. - **32-bit x86:** the header is three pointer-sized words; nothing is specific to x86. ## 6. Growth (PR 3) From 6b5fc83ae813aa68c6b3e0eb627b21ae57841961 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 3 Oct 2026 23:15:16 +0100 Subject: [PATCH 03/19] Lowering: one helper knows the array layout (array ops in LowerToLLVM.cpp) ArrayLayout reads and writes an array's data and length, and makes one. No change to the generated IR: --emit=llvm identical for every suite test under gc, rc and own. Co-Authored-By: Claude Opus 5.5 --- .../TypeScript/LowerToLLVM/ArrayLayout.h | 109 ++++++++++++++++++ tslang/lib/TypeScript/LowerToLLVM.cpp | 91 +++++++-------- 2 files changed, 150 insertions(+), 50 deletions(-) create mode 100644 tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h diff --git a/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h b/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h new file mode 100644 index 000000000..1b8ddf02b --- /dev/null +++ b/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h @@ -0,0 +1,109 @@ +#ifndef MLIR_TYPESCRIPT_LOWERTOLLVM_ARRAYLAYOUT_H_ +#define MLIR_TYPESCRIPT_LOWERTOLLVM_ARRAYLAYOUT_H_ + +#include "TypeScript/Defines.h" +#include "TypeScript/TypeScriptOps.h" +#include "TypeScript/MLIRLogic/MLIRHelper.h" +#include "TypeScript/LowerToLLVM/LLVMCodeHelperBase.h" + +#include "mlir/Dialect/LLVMIR/LLVMDialect.h" + +namespace typescript +{ + +// The one place that knows how an array value is laid out +// (docs/superpowers/specs/2026-10-03-array-reference-design.md). An array value is a +// { data, length } struct; a `slot` is a pointer to one. +class ArrayLayout : public LLVMCodeHelperBase +{ + public: + ArrayLayout(mlir::Operation *op, PatternRewriter &rewriter, const TypeConverter *typeConverter, + CompileOptions &compileOptions) + : LLVMCodeHelperBase(op, rewriter, typeConverter, compileOptions) + { + } + + // what the field GEPs are based on, for an op that changes the array held in `slot` + mlir::Value headerForUpdate(mlir_ts::ArrayType, mlir::Value slot) + { + return slot; + } + + // the same, for a routine that only reads (never materialises: R1) + mlir::Value headerForRead(mlir_ts::ArrayType, mlir::Value slot) + { + return slot; + } + + mlir::Value dataAddress(mlir_ts::ArrayType arrayType, mlir::Value header) + { + return fieldAddress(arrayType, header, ARRAY_DATA_INDEX); + } + + mlir::Value lengthAddress(mlir_ts::ArrayType arrayType, mlir::Value header) + { + return fieldAddress(arrayType, header, ARRAY_SIZE_INDEX); + } + + // of an array value + mlir::Value data(mlir_ts::ArrayType, mlir::Value array) + { + TypeHelper th(rewriter); + return rewriter.create(op->getLoc(), th.getPtrType(), array, + MLIRHelper::getStructIndex(rewriter, ARRAY_DATA_INDEX)); + } + + mlir::Value length(mlir_ts::ArrayType, mlir::Value array) + { + TypeHelper th(rewriter); + return rewriter.create(op->getLoc(), typeConverter->convertType(th.getIndexType()), array, + MLIRHelper::getStructIndex(rewriter, ARRAY_SIZE_INDEX)); + } + + // what `===` and truthiness compare + mlir::Value identity(mlir_ts::ArrayType arrayType, mlir::Value array) + { + return data(arrayType, array); + } + + // a new array over `data` (owned by the array) with `length` elements + mlir::Value make(mlir_ts::ArrayType arrayType, mlir::Value data, mlir::Value length) + { + auto loc = op->getLoc(); + auto llvmArrayType = typeConverter->convertType(arrayType); + mlir::Value value = rewriter.create(loc, llvmArrayType); + value = rewriter.create(loc, llvmArrayType, value, data, + MLIRHelper::getStructIndex(rewriter, ARRAY_DATA_INDEX)); + return rewriter.create(loc, llvmArrayType, value, length, + MLIRHelper::getStructIndex(rewriter, ARRAY_SIZE_INDEX)); + } + + // a constant array inside a global's initializer: `data` is static, nothing is allocated + // (Task 4 replaces this with makeStatic(arrayType, dataGlobalName, dataOffsetBytes, length)) + mlir::Value makeStatic(mlir_ts::ArrayType arrayType, mlir::Value data, mlir::Value length) + { + return make(arrayType, data, length); + } + + mlir::Value zero(mlir_ts::ArrayType arrayType) + { + return rewriter.create(op->getLoc(), typeConverter->convertType(arrayType)); + } + + mlir::Value undef(mlir_ts::ArrayType arrayType) + { + return rewriter.create(op->getLoc(), typeConverter->convertType(arrayType)); + } + + private: + mlir::Value fieldAddress(mlir_ts::ArrayType arrayType, mlir::Value header, int32_t index) + { + TypeHelper th(rewriter); + return rewriter.create(op->getLoc(), th.getPtrType(), typeConverter->convertType(arrayType), header, + ArrayRef{0, index}); + } +}; + +} // namespace typescript + +#endif // MLIR_TYPESCRIPT_LOWERTOLLVM_ARRAYLAYOUT_H_ diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index 454eb7531..cb7ac5ce5 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -38,6 +38,7 @@ #include "TypeScript/LowerToLLVMLogic.h" #include "TypeScript/LowerToLLVM/LLVMDebugInfo.h" #include "TypeScript/LowerToLLVM/LLVMDebugInfoFixer.h" +#include "TypeScript/LowerToLLVM/ArrayLayout.h" #include "scanner_enums.h" @@ -628,6 +629,13 @@ class LengthOfOpLowering : public TsLlvmPattern auto loc = op->getLoc(); + if (auto arrayType = dyn_cast(op.getOp().getType())) + { + ArrayLayout layout(op, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); + rewriter.replaceOp(op, layout.length(arrayType, transformed.getOp())); + return success(); + } + rewriter.replaceOpWithNewOp(op, typeConverter->convertType(th.getIndexType()), transformed.getOp(), MLIRHelper::getStructIndex(rewriter, ARRAY_SIZE_INDEX)); @@ -661,12 +669,12 @@ class SetLengthOfOpLowering : public TsLlvmPattern LLVM_DEBUG(llvm::dbgs() << "arrayType: elementType: " << elementType << "\n";); LLVM_DEBUG(llvm::dbgs() << "arrayType: llvm: " << tch.convertType(arrayType) << "\n";); - auto currentPtrPtr = rewriter.create(loc, ptrType, llvmArrayType, transformed.getOp(), - ArrayRef{0, ARRAY_DATA_INDEX}); + ArrayLayout layout(op, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); + auto header = layout.headerForUpdate(arrayType, transformed.getOp()); + auto currentPtrPtr = layout.dataAddress(arrayType, header); auto currentPtr = rewriter.create(loc, ptrType, currentPtrPtr); - auto countAsIndexTypePtr = rewriter.create(loc, ptrType, llvmArrayType, transformed.getOp(), - ArrayRef{0, ARRAY_SIZE_INDEX}); + auto countAsIndexTypePtr = layout.lengthAddress(arrayType, header); auto countAsIndexType = rewriter.create(loc, llvmIndexType, countAsIndexTypePtr); auto newCountAsIndexType = op.getNewLength(); @@ -3055,12 +3063,8 @@ struct CreateArrayOpLowering : public TsLlvmPattern // create array type auto llvmRtArrayStructType = tch.convertType(arrayType); - auto structValue = rewriter.create(loc, llvmRtArrayStructType); - auto structValue2 = rewriter.create(loc, llvmRtArrayStructType, structValue, allocated, - MLIRHelper::getStructIndex(rewriter, 0)); - - auto structValue3 = rewriter.create(loc, llvmRtArrayStructType, structValue2, - newCountAsIndexType, MLIRHelper::getStructIndex(rewriter, 1)); + ArrayLayout layout(createArrayOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); + auto structValue3 = layout.make(arrayType, allocated, newCountAsIndexType); rewriter.replaceOp(createArrayOp, ValueRange{structValue3}); return success(); @@ -3098,13 +3102,9 @@ struct NewEmptyArrayOpLowering : public TsLlvmPattern auto llvmRtArrayStructType = tch.convertType(arrayType); auto llvmIndexType = tch.convertType(th.getIndexType()); - auto structValue = rewriter.create(loc, llvmRtArrayStructType); - auto structValue2 = rewriter.create(loc, llvmRtArrayStructType, structValue, allocated, - MLIRHelper::getStructIndex(rewriter, 0)); - + ArrayLayout layout(newEmptyArrOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); auto size0 = clh.createIndexConstantOf(llvmIndexType, 0); - auto structValue3 = rewriter.create(loc, llvmRtArrayStructType, structValue2, size0, - MLIRHelper::getStructIndex(rewriter, 1)); + auto structValue3 = layout.make(arrayType, allocated, size0); rewriter.replaceOp(newEmptyArrOp, ValueRange{structValue3}); return success(); @@ -3148,12 +3148,8 @@ struct NewArrayOpLowering : public TsLlvmPattern // create array type auto llvmRtArrayStructType = tch.convertType(arrayType); - auto structValue = rewriter.create(loc, llvmRtArrayStructType); - auto structValue2 = rewriter.create(loc, llvmRtArrayStructType, structValue, allocated, - MLIRHelper::getStructIndex(rewriter, ARRAY_DATA_INDEX)); - - auto structValue3 = rewriter.create(loc, llvmRtArrayStructType, structValue2, - transformed.getCount(), MLIRHelper::getStructIndex(rewriter, ARRAY_SIZE_INDEX)); + ArrayLayout layout(newArrOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); + auto structValue3 = layout.make(arrayType, allocated, transformed.getCount()); rewriter.replaceOp(newArrOp, ValueRange{structValue3}); return success(); @@ -3183,12 +3179,12 @@ struct ArrayPushOpLowering : public TsLlvmPattern auto llvmIndexType = tch.convertType(th.getIndexType()); // TODO: use GetAddressOfArrayElement method to sync code - auto currentPtrPtr = rewriter.create(loc, ptrType, llvmArrayType, transformed.getOp(), - ArrayRef{0, ARRAY_DATA_INDEX}); + ArrayLayout layout(pushOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); + auto header = layout.headerForUpdate(arrayType, transformed.getOp()); + auto currentPtrPtr = layout.dataAddress(arrayType, header); auto currentPtr = rewriter.create(loc, ptrType, currentPtrPtr); - auto countAsIndexTypePtr = rewriter.create(loc, ptrType, llvmArrayType, transformed.getOp(), - ArrayRef{0, ARRAY_SIZE_INDEX}); + auto countAsIndexTypePtr = layout.lengthAddress(arrayType, header); auto countAsIndexType = rewriter.create(loc, llvmIndexType, countAsIndexTypePtr); auto incSize = clh.createIndexConstantOf(llvmIndexType, transformed.getItems().size()); @@ -3268,12 +3264,12 @@ struct ArrayPopOpLowering : public TsLlvmPattern auto llvmElementType = tch.convertType(elementType); auto llvmIndexType = tch.convertType(th.getIndexType()); - auto currentPtrPtr = rewriter.create(loc, ptrType, llvmArrayType, transformed.getOp(), - ArrayRef{0, ARRAY_DATA_INDEX}); + ArrayLayout layout(popOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); + auto header = layout.headerForUpdate(arrayType, transformed.getOp()); + auto currentPtrPtr = layout.dataAddress(arrayType, header); auto currentPtr = rewriter.create(loc, ptrType, currentPtrPtr); - auto countAsIndexTypePtr = rewriter.create(loc, ptrType, llvmArrayType, transformed.getOp(), - ArrayRef{0, ARRAY_SIZE_INDEX}); + auto countAsIndexTypePtr = layout.lengthAddress(arrayType, header); auto countAsIndexType = rewriter.create(loc, llvmIndexType, countAsIndexTypePtr); auto incSize = clh.createIndexConstantOf(llvmIndexType, 1); @@ -3323,12 +3319,12 @@ struct ArrayUnshiftOpLowering : public TsLlvmPattern auto llvmElementType = tch.convertType(elementType); auto llvmIndexType = tch.convertType(th.getIndexType()); - auto currentPtrPtr = rewriter.create(loc, ptrType, llvmArrayType, transformed.getOp(), - ArrayRef{0, ARRAY_DATA_INDEX}); + ArrayLayout layout(unshiftOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); + auto header = layout.headerForUpdate(arrayType, transformed.getOp()); + auto currentPtrPtr = layout.dataAddress(arrayType, header); auto currentPtr = rewriter.create(loc, ptrType, currentPtrPtr); - auto countAsIndexTypePtr = rewriter.create(loc, ptrType, llvmArrayType, transformed.getOp(), - ArrayRef{0, ARRAY_SIZE_INDEX}); + auto countAsIndexTypePtr = layout.lengthAddress(arrayType, header); auto countAsIndexType = rewriter.create(loc, llvmIndexType, countAsIndexTypePtr); auto incSize = clh.createIndexConstantOf(llvmIndexType, transformed.getItems().size()); @@ -3421,12 +3417,12 @@ struct ArrayShiftOpLowering : public TsLlvmPattern mlir::Type storageType = elementType; // storageType = MLIRHelper::getStorageTypeFrom(elementType); - auto currentPtrPtr = rewriter.create(loc, ptrType, llvmArrayType, transformed.getOp(), - ArrayRef{0, ARRAY_DATA_INDEX}); + ArrayLayout layout(shiftOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); + auto header = layout.headerForUpdate(arrayType, transformed.getOp()); + auto currentPtrPtr = layout.dataAddress(arrayType, header); auto currentPtr = rewriter.create(loc, ptrType, currentPtrPtr); - auto countAsIndexTypePtr = rewriter.create(loc, ptrType, llvmArrayType, transformed.getOp(), - ArrayRef{0, ARRAY_SIZE_INDEX}); + auto countAsIndexTypePtr = layout.lengthAddress(arrayType, header); auto countAsIndexType = rewriter.create(loc, llvmIndexType, countAsIndexTypePtr); auto incSize = clh.createIndexConstantOf(llvmIndexType, 1); @@ -3483,12 +3479,12 @@ struct ArraySpliceOpLowering : public TsLlvmPattern auto indexType = th.getIndexType(); auto llvmIndexType = tch.convertType(indexType); - auto currentPtrPtr = rewriter.create(loc, ptrType, llvmArrayType, transformed.getOp(), - ArrayRef{0, ARRAY_DATA_INDEX}); + ArrayLayout layout(spliceOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); + auto header = layout.headerForUpdate(arrayType, transformed.getOp()); + auto currentPtrPtr = layout.dataAddress(arrayType, header); auto currentPtr = rewriter.create(loc, ptrType, currentPtrPtr); - auto countAsIndexTypePtr = rewriter.create(loc, ptrType, llvmArrayType, transformed.getOp(), - ArrayRef{0, ARRAY_SIZE_INDEX}); + auto countAsIndexTypePtr = layout.lengthAddress(arrayType, header); auto countAsIndexType = rewriter.create(loc, llvmIndexType, countAsIndexTypePtr); auto startIndexAsIndexType = spliceOp.getStart(); @@ -3674,17 +3670,12 @@ struct ArrayViewOpLowering : public TsLlvmPattern // TODO: add size check !!! - auto arrayPtr = rewriter.create(loc, th.getPtrType(), - transformed.getOp(), MLIRHelper::getStructIndex(rewriter, ARRAY_DATA_INDEX)); + ArrayLayout layout(arrayViewOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); + auto arrayPtr = layout.data(arrayType, transformed.getOp()); auto arrayOffset = ch.GetAddressOfPointerOffset(elementType, arrayPtr, transformed.getOffset()); // create array type - auto structValue = rewriter.create(loc, llvmArrayType); - auto structValue2 = rewriter.create(loc, llvmArrayType, structValue, arrayOffset, - MLIRHelper::getStructIndex(rewriter, ARRAY_DATA_INDEX)); - - auto structValue3 = rewriter.create(loc, llvmArrayType, structValue2, - transformed.getCount(), MLIRHelper::getStructIndex(rewriter, ARRAY_SIZE_INDEX)); + auto structValue3 = layout.make(arrayType, arrayOffset, transformed.getCount()); rewriter.replaceOp(arrayViewOp, ValueRange{structValue3}); return success(); From 5eafedd102c8c1f20de3ea11e18acb2fca3cdb70 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 3 Oct 2026 23:21:07 +0100 Subject: [PATCH 04/19] Lowering: casts, constants and ownership routines read arrays through ArrayLayout The last places outside the helper that knew the { data, length } layout. Also drops the llvmArrayType/loc locals Task 1 left unused. No change to the generated IR. Co-Authored-By: Claude Opus 5.5 --- .../TypeScript/LowerToLLVM/CastLogicHelper.h | 37 ++++++++----------- .../TypeScript/LowerToLLVM/LLVMCodeHelper.h | 14 ++----- .../LowerToLLVM/OwnershipRoutineLogic.h | 15 ++++---- tslang/lib/TypeScript/LowerToLLVM.cpp | 9 ----- 4 files changed, 27 insertions(+), 48 deletions(-) diff --git a/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h b/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h index 065c8385a..7a3fa7eef 100644 --- a/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h +++ b/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h @@ -18,6 +18,7 @@ #include "TypeScript/LowerToLLVM/ConvertLogic.h" #include "TypeScript/LowerToLLVM/AnyLogic.h" #include "TypeScript/LowerToLLVM/LLVMCodeHelperBase.h" +#include "TypeScript/LowerToLLVM/ArrayLayout.h" #include "mlir/Dialect/Index/IR/IndexDialect.h" #include "mlir/Dialect/Index/IR/IndexOps.h" @@ -226,15 +227,13 @@ class CastLogicHelper { if (arrayType.getElementType() == refType.getElementType()) { - return rewriter.create(loc, resLLVMType, in, - MLIRHelper::getStructIndex(rewriter, ARRAY_DATA_INDEX)); + return extractArrayData(in, arrayType); } } if (auto opaqueType = dyn_cast(resType)) { - auto ptrOfElementValue = rewriter.create(loc, th.getPtrType(), in, - MLIRHelper::getStructIndex(rewriter, ARRAY_DATA_INDEX)); + auto ptrOfElementValue = extractArrayData(in, arrayType); return rewriter.create(loc, th.getPtrType(), ptrOfElementValue); } @@ -1095,7 +1094,8 @@ class CastLogicHelper } auto ptrType = th.getPtrType(); - auto llvmRtArrayStructType = tch.convertType(arrayType); + auto arrayTypeTs = mlir::cast(arrayType); + ArrayLayout layout(op, rewriter, tch.typeConverter, compileOptions); auto llvmIndexType = tch.convertType(th.getIndexType()); auto sizeValue = clh.createIndexConstantOf(llvmIndexType, size); auto destArrayElement = mlir::cast(arrayType).getElementType(); @@ -1110,14 +1110,12 @@ class CastLogicHelper // retains the result before it looks at `done`. if (compileOptions.tracksOwnership()) { - return rewriter.create(loc, llvmRtArrayStructType).getResult(); + return layout.zero(arrayTypeTs); } - return rewriter.create(loc, llvmRtArrayStructType).getResult(); + return layout.undef(arrayTypeTs); } - auto structValue = rewriter.create(loc, llvmRtArrayStructType); - auto arrayValueSize = LLVM::LLVMArrayType::get(llvmSrcElementType, size); mlir::Value arrayPtr; @@ -1141,13 +1139,7 @@ class CastLogicHelper arrayPtr = in; } - auto structValue2 = - rewriter.create(loc, llvmRtArrayStructType, structValue, arrayPtr, MLIRHelper::getStructIndex(rewriter, ARRAY_DATA_INDEX)); - - auto structValue3 = - rewriter.create(loc, llvmRtArrayStructType, structValue2, sizeValue, MLIRHelper::getStructIndex(rewriter, ARRAY_SIZE_INDEX)); - - return structValue3; + return layout.make(arrayTypeTs, arrayPtr, sizeValue); } mlir::Value castToAny(mlir::Value in, mlir::Type inType, mlir::Type inLLVMType) @@ -1292,13 +1284,16 @@ class CastLogicHelper mlir::Value extractArrayPtr(mlir::Value in, mlir_ts::ArrayType arrayType) { - auto llvmType = tch.convertType(arrayType.getElementType()); - auto ptrType = th.getPtrType(); - mlir::Value inAsLLVMType = rewriter.create(loc, tch.convertType(in.getType()), in); - mlir::Value ptrVal = rewriter.create(loc, ptrType, inAsLLVMType, MLIRHelper::getStructIndex(rewriter, ARRAY_DATA_INDEX)); - return ptrVal; + // only used for identity: truthiness and `===` + return ArrayLayout(op, rewriter, tch.typeConverter, compileOptions).identity(arrayType, inAsLLVMType); + } + + // the data pointer of an array value, for the casts to a ref of the element and to opaque + mlir::Value extractArrayData(mlir::Value in, mlir_ts::ArrayType arrayType) + { + return ArrayLayout(op, rewriter, tch.typeConverter, compileOptions).data(arrayType, in); } }; diff --git a/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelper.h b/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelper.h index 3248087c9..cbbc0e57f 100644 --- a/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelper.h +++ b/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelper.h @@ -14,6 +14,7 @@ #include "TypeScript/LowerToLLVM/CodeLogicHelper.h" #include "TypeScript/LowerToLLVM/CastLogicHelper.h" #include "TypeScript/LowerToLLVM/LLVMCodeHelperBase.h" +#include "TypeScript/LowerToLLVM/ArrayLayout.h" #include "TypeScript/LowerToLLVM/TypeDescriptorLogic.h" #include "mlir/Transforms/DialectConversion.h" @@ -440,17 +441,10 @@ class LLVMCodeHelper : public LLVMCodeHelperBase auto itemValArrayPtr = getOrCreateGlobalArray(originalArrayType.getElementType(), size, arrayValue); // create ReadOnlyRuntimeArrayType - auto structValue = rewriter.create(loc, llvmArrayType); auto sizeValue = rewriter.create(loc, typeConverter->convertType(rewriter.getIndexType()), rewriter.getIndexAttr(arrayValue.size())); - auto structValue2 = rewriter.create(loc, llvmArrayType, structValue, itemValArrayPtr, - MLIRHelper::getStructIndex(rewriter, 0)); - - auto structValue3 = rewriter.create(loc, llvmArrayType, structValue2, sizeValue, - MLIRHelper::getStructIndex(rewriter, 1)); - - return structValue3; + return ArrayLayout(op, rewriter, typeConverter, compileOptions).makeStatic(originalArrayType, itemValArrayPtr, sizeValue); } mlir::Value getArrayValue(mlir::Type originalElementType, mlir::Type llvmElementType, unsigned size, @@ -861,8 +855,8 @@ class LLVMCodeHelper : public LLVMCodeHelperBase if (isa(arrayOrStringOrTupleMlirTSType)) { // extract pointer from struct - dataPtr = rewriter.create(loc, ptrType, arrayOrStringOrTuple, - MLIRHelper::getStructIndex(rewriter, ARRAY_DATA_INDEX)); + dataPtr = ArrayLayout(op, rewriter, typeConverter, compileOptions) + .data(cast(arrayOrStringOrTupleMlirTSType), arrayOrStringOrTuple); } auto addr = rewriter.create(loc, ptrType, llvmElementType, dataPtr, ArrayRef{index}); diff --git a/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h b/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h index a40fd9c5b..43e3406e8 100644 --- a/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h +++ b/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h @@ -11,6 +11,7 @@ #include "TypeScript/LowerToLLVM/TypeConverterHelper.h" #include "TypeScript/LowerToLLVM/CodeLogicHelper.h" #include "TypeScript/LowerToLLVM/LLVMCodeHelperBase.h" +#include "TypeScript/LowerToLLVM/ArrayLayout.h" #include "TypeScript/LowerToLLVM/TypeDescriptorLogic.h" #include "mlir/Dialect/LLVMIR/LLVMDialect.h" @@ -1161,9 +1162,8 @@ class OwnershipRoutineLogic // already holds whatever the elements own if (auto arrayType = dyn_cast(type)) { - auto llvmArrayType = tch.convertType(arrayType); - auto dataSlot = rewriter.create(loc, ptrTy, llvmArrayType, slotPtr, - ArrayRef{0, ARRAY_DATA_INDEX}); + ArrayLayout layout(op, rewriter, typeConverter, compileOptions); + auto dataSlot = layout.dataAddress(arrayType, layout.headerForRead(arrayType, slotPtr)); emitIncRef(rewriter.create(loc, ptrTy, dataSlot)); return; } @@ -1285,18 +1285,17 @@ class OwnershipRoutineLogic auto loc = op->getLoc(); auto ptrTy = th.getPtrType(); auto llvmIndexType = tch.convertType(th.getIndexType()); - auto llvmArrayType = tch.convertType(arrayType); - auto dataSlot = rewriter.create(loc, ptrTy, llvmArrayType, slotPtr, - ArrayRef{0, ARRAY_DATA_INDEX}); + ArrayLayout layout(op, rewriter, typeConverter, compileOptions); + auto header = layout.headerForRead(arrayType, slotPtr); + auto dataSlot = layout.dataAddress(arrayType, header); auto dataValue = rewriter.create(loc, ptrTy, dataSlot); emitIfLastReference(dataValue, [&]() { auto elementRoutine = getOrCreateReleaseRoutine(arrayType.getElementType()); if (!elementRoutine.empty()) { - auto sizeSlot = rewriter.create(loc, ptrTy, llvmArrayType, slotPtr, - ArrayRef{0, ARRAY_SIZE_INDEX}); + auto sizeSlot = layout.lengthAddress(arrayType, header); auto sizeValue = rewriter.create(loc, llvmIndexType, sizeSlot); emitCountedLoop(sizeValue, [&](mlir::Value index) { diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index cb7ac5ce5..4ee4bd669 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -627,8 +627,6 @@ class LengthOfOpLowering : public TsLlvmPattern { TypeHelper th(rewriter); - auto loc = op->getLoc(); - if (auto arrayType = dyn_cast(op.getOp().getType())) { ArrayLayout layout(op, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); @@ -662,7 +660,6 @@ class SetLengthOfOpLowering : public TsLlvmPattern auto arrayType = cast(cast(op.getOp().getType()).getElementType()); auto elementType = arrayType.getElementType(); - auto llvmArrayType = tch.convertType(arrayType); auto llvmElementType = tch.convertType(elementType); auto llvmIndexType = tch.convertType(th.getIndexType()); @@ -3174,7 +3171,6 @@ struct ArrayPushOpLowering : public TsLlvmPattern auto arrayType = cast(cast(pushOp.getOp().getType()).getElementType()); auto elementType = arrayType.getElementType(); - auto llvmArrayType = tch.convertType(arrayType); auto llvmElementType = tch.convertType(elementType); auto llvmIndexType = tch.convertType(th.getIndexType()); @@ -3260,7 +3256,6 @@ struct ArrayPopOpLowering : public TsLlvmPattern auto arrayType = cast(cast(popOp.getOp().getType()).getElementType()); auto elementType = arrayType.getElementType(); - auto llvmArrayType = tch.convertType(arrayType); auto llvmElementType = tch.convertType(elementType); auto llvmIndexType = tch.convertType(th.getIndexType()); @@ -3315,7 +3310,6 @@ struct ArrayUnshiftOpLowering : public TsLlvmPattern auto arrayType = cast(cast(unshiftOp.getOp().getType()).getElementType()); auto elementType = arrayType.getElementType(); - auto llvmArrayType = tch.convertType(arrayType); auto llvmElementType = tch.convertType(elementType); auto llvmIndexType = tch.convertType(th.getIndexType()); @@ -3410,7 +3404,6 @@ struct ArrayShiftOpLowering : public TsLlvmPattern auto arrayType = cast(cast(shiftOp.getOp().getType()).getElementType()); auto elementType = arrayType.getElementType(); - auto llvmArrayType = tch.convertType(arrayType); auto llvmElementType = tch.convertType(elementType); auto llvmIndexType = tch.convertType(th.getIndexType()); @@ -3474,7 +3467,6 @@ struct ArraySpliceOpLowering : public TsLlvmPattern auto arrayType = cast(cast(spliceOp.getOp().getType()).getElementType()); auto elementType = arrayType.getElementType(); - auto llvmArrayType = tch.convertType(arrayType); auto llvmElementType = tch.convertType(elementType); auto indexType = th.getIndexType(); auto llvmIndexType = tch.convertType(indexType); @@ -3665,7 +3657,6 @@ struct ArrayViewOpLowering : public TsLlvmPattern auto arrayType = arrayViewOp.getOp().getType(); auto elementType = arrayType.getElementType(); - auto llvmArrayType = tch.convertType(arrayType); auto llvmIndexType = tch.convertType(th.getIndexType()); // TODO: add size check !!! From 591b707e41c2e0f9452aab2c87b44529d12aecef Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 3 Oct 2026 23:32:47 +0100 Subject: [PATCH 05/19] A destructuring rest element is a new array (#477) Closes #477 Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenImpl.h | 3 + tslang/lib/TypeScript/MLIRGenVariables.cpp | 105 +++++++++++------- tslang/test/tester/CMakeLists.txt | 3 + tslang/test/tester/tests/00array_rest_copy.ts | 35 ++++++ 4 files changed, 108 insertions(+), 38 deletions(-) create mode 100644 tslang/test/tester/tests/00array_rest_copy.ts diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index e01c6819b..a6aaa19cb 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -12116,6 +12116,9 @@ class MLIRGenImpl std::optional castConstArrayToArray(mlir::Location location, mlir::Type type, mlir::Value value, mlir::Type valueType, const GenContext &genContext); std::optional castArrayElementwise(mlir::Location location, mlir_ts::ArrayType arrayType, mlir::Value value, mlir_ts::ArrayType srcArrayType, const GenContext &genContext); + // `[a, ...rest] = src`: rest as a new array filled by a push loop (#477) + ValueOrLogicalResult mlirGenArrayRestCopy(mlir::Location location, mlir_ts::ArrayType arrayType, mlir::Value source, int64_t index, const GenContext &genContext); + // reports which part of a function signature does not match the one it is assigned to void emitFunctionTypeMismatch(mlir::Location location, mlir::Type valueType, mlir::Type type, MatchResult match); diff --git a/tslang/lib/TypeScript/MLIRGenVariables.cpp b/tslang/lib/TypeScript/MLIRGenVariables.cpp index 7a5545367..8f9422146 100644 --- a/tslang/lib/TypeScript/MLIRGenVariables.cpp +++ b/tslang/lib/TypeScript/MLIRGenVariables.cpp @@ -7,6 +7,71 @@ namespace typescript namespace mlirgen { + // `[a, ...rest] = src`: rest is a new array holding src[index..] (#477) - built as + // let .rest_n = .rest_src.length - index; if (.rest_n < 0) .rest_n = 0; + // .rest = new array(.rest_n); for (let .rest_i = 0; .rest_i < .rest_n; ++.rest_i) .rest[.rest_i] = .rest_src[.rest_i + index]; + // the elements are written into the new array in place, so no second variable holds the same storage + ValueOrLogicalResult MLIRGenImpl::mlirGenArrayRestCopy(mlir::Location location, mlir_ts::ArrayType arrayType, mlir::Value source, + int64_t index, const GenContext &genContext) + { + SymbolTableScopeT varScope(symbolTable); + + auto srcVarDecl = std::make_shared(".rest_src", arrayType, location); + DECLARE(srcVarDecl, source); + + NodeFactory nf(NodeFactoryFlags::None); + auto _src = nf.createIdentifier(S(".rest_src")); + auto _dst = nf.createIdentifier(S(".rest")); + auto _n = nf.createIdentifier(S(".rest_n")); + auto _i = nf.createIdentifier(S(".rest_i")); + auto _index = [&]() { return nf.createNumericLiteral(stows(std::to_string(index))); }; + auto _zero = [&]() { return nf.createNumericLiteral(S("0")); }; + + // let .rest_n = .rest_src.length - index; if (.rest_n < 0) .rest_n = 0; + NodeArray countDeclarations; + countDeclarations.push_back(nf.createVariableDeclaration( + _n, undefined, undefined, + nf.createBinaryExpression(nf.createPropertyAccessExpression(_src, nf.createIdentifier(S(LENGTH_FIELD_NAME))), + nf.createToken(SyntaxKind::MinusToken), _index()))); + if (mlir::failed(mlirGen(nf.createVariableStatement(undefined, nf.createVariableDeclarationList(countDeclarations, NodeFlags::Let)), genContext))) + { + return mlir::failure(); + } + + auto clamp = nf.createIfStatement( + nf.createBinaryExpression(_n, nf.createToken(SyntaxKind::LessThanToken), _zero()), + nf.createExpressionStatement(nf.createBinaryExpression(_n, nf.createToken(SyntaxKind::EqualsToken), _zero())), undefined); + if (mlir::failed(mlirGen(clamp, genContext))) + { + return mlir::failure(); + } + + auto count = resolveIdentifier(location, ".rest_n", genContext); + auto countAsIndex = cast(location, builder.getIndexType(), count, genContext); + EXIT_IF_FAILED_OR_NO_VALUE(countAsIndex) + + auto newArray = builder.create(location, arrayType, countAsIndex); + auto dstVarDecl = std::make_shared(".rest", arrayType, location); + DECLARE(dstVarDecl, newArray); + + // for (let .rest_i = 0; .rest_i < .rest_n; ++.rest_i) .rest[.rest_i] = .rest_src[.rest_i + index]; + NodeArray declarations; + declarations.push_back(nf.createVariableDeclaration(_i, undefined, undefined, _zero())); + auto initVars = nf.createVariableDeclarationList(declarations, NodeFlags::Let); + auto cond = nf.createBinaryExpression(_i, nf.createToken(SyntaxKind::LessThanToken), _n); + auto incr = nf.createPrefixUnaryExpression(nf.createToken(SyntaxKind::PlusPlusToken), _i); + auto assign = nf.createExpressionStatement(nf.createBinaryExpression( + nf.createElementAccessExpression(_dst, _i), nf.createToken(SyntaxKind::EqualsToken), + nf.createElementAccessExpression(_src, nf.createBinaryExpression(_i, nf.createToken(SyntaxKind::PlusToken), _index())))); + + if (mlir::failed(mlirGen(nf.createForStatement(initVars, cond, incr, assign), genContext))) + { + return mlir::failure(); + } + + return V(newArray); + } + ValueOrLogicalResult MLIRGenImpl::registerVariableInThisContext(mlir::Location location, StringRef name, mlir::Type type, const GenContext &genContext) { @@ -328,8 +393,6 @@ namespace mlirgen .template Case([&](auto constArrayType) { if (isDotDotDot) { - auto indexType = builder.getIndexType(); - auto arrayType = mth.removeConstType(constArrayType); auto arrayValue = cast(location, arrayType, init, genContext); @@ -338,23 +401,7 @@ namespace mlirgen return mlir::Value(); } - auto constIndex = builder.create( - location, indexType, builder.getIndexAttr(index)); - - auto length = builder.create(location, indexType, arrayValue); - - auto count = builder.create( - location, indexType, builder.getI32IntegerAttr(static_cast(SyntaxKind::MinusToken)), length, constIndex); - - mlir::Value arrayViewValue = - builder.create( - location, - arrayType, - arrayValue, - constIndex, - count); - - return arrayViewValue; + return V(mlirGenArrayRestCopy(location, mlir::cast(arrayType), arrayValue, index, genContext)); } // TODO: unify it with ElementAccess @@ -368,25 +415,7 @@ namespace mlirgen if (isDotDotDot) { - auto indexType = builder.getIndexType(); - - auto constIndex = builder.create( - location, indexType, builder.getIndexAttr(index)); - - auto length = builder.create(location, indexType, init); - - auto count = builder.create( - location, indexType, builder.getI32IntegerAttr(static_cast(SyntaxKind::MinusToken)), length, constIndex); - - mlir::Value arrayViewValue = - builder.create( - location, - arrayType, - init, - constIndex, - count); - - return arrayViewValue; + return V(mlirGenArrayRestCopy(location, arrayType, init, index, genContext)); } // TODO: unify it with ElementAccess diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 64362f211..7d29fe6f7 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -342,6 +342,7 @@ tslang_add_test(NAME test-compile-00-array-splice COMMAND test-runner "${PROJECT tslang_add_test(NAME test-compile-00-const-array-push COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_array_push.ts") tslang_add_test(NAME test-compile-00-array-spread-args COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_spread_args.ts") tslang_add_test(NAME test-compile-00-arrays5-deconstruct COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array5_deconst.ts") +tslang_add_test(NAME test-compile-00-array-rest-copy COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_rest_copy.ts") tslang_add_test(NAME test-compile-00-arrays6 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array6.ts") tslang_add_test(NAME test-compile-00-arrays7 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array7.ts") tslang_add_test(NAME test-compile-00-arrays8-tuple-spread COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array8_tuple_spread.ts") @@ -836,6 +837,7 @@ tslang_add_test(NAME test-jit-00-array-splice COMMAND test-runner -jit "${PROJEC tslang_add_test(NAME test-jit-00-const-array-push COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_array_push.ts") tslang_add_test(NAME test-jit-00-array-spread-args COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_spread_args.ts") tslang_add_test(NAME test-jit-00-arrays5-deconstruct COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array5_deconst.ts") +tslang_add_test(NAME test-jit-00-array-rest-copy COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_rest_copy.ts") tslang_add_test(NAME test-jit-00-arrays6 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array6.ts") tslang_add_test(NAME test-jit-00-arrays7 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array7.ts") tslang_add_test(NAME test-jit-00-arrays8-tuple-spread COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array8_tuple_spread.ts") @@ -1659,6 +1661,7 @@ set(TSLANG_CORPUS 00array3.ts 00array4_push_pop.ts 00array5_deconst.ts + 00array_rest_copy.ts 00array6.ts 00array7.ts 00array8_tuple_spread.ts diff --git a/tslang/test/tester/tests/00array_rest_copy.ts b/tslang/test/tester/tests/00array_rest_copy.ts new file mode 100644 index 000000000..9d9b15d9a --- /dev/null +++ b/tslang/test/tester/tests/00array_rest_copy.ts @@ -0,0 +1,35 @@ +// A destructuring rest element is a new array, not a view into the source (#477). +class Box { + constructor(public v: number) {} +} + +function main() { + const src: number[] = [1, 2, 3]; + const [first, ...rest] = src; + assert(first == 1, "first"); + rest[0] = 20; + assert(src[1] == 2, "a write to the rest leaks into the source"); + rest.push(4); + assert(rest.length == 3 && rest[2] == 4, "push onto the rest"); + assert(src.length == 3 && src[2] == 3, "the source is unchanged"); + + const [a, b, ...none] = [1]; + assert(none.length == 0, "the rest of a shorter array is empty"); + + const boxes: Box[] = [new Box(1), new Box(2), new Box(3)]; + const [head, ...tail] = boxes; + tail.push(new Box(4)); + assert(head.v == 1 && tail.length == 3 && tail[0].v == 2 && tail[2].v == 4, "class elements"); + + const words: string[] = ["a", "b", "c"]; + const [w0, ...ws] = words; + ws.push("d"); + assert(w0 == "a" && ws.length == 3 && ws[2] == "d" && words.length == 3, "string elements"); + + const [p0, ...p1] = src; + const [q0, ...q1] = p1; + q1.push(9); + assert(p0 == 1 && q0 == 2 && q1.length == 2 && q1[1] == 9 && p1.length == 2 && p1[1] == 3, "nested and repeated rest"); + + print("done."); +} From bea7503a4d877d77f79d47b0e187be3edf209f15 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 3 Oct 2026 23:50:39 +0100 Subject: [PATCH 06/19] An array is a reference to a header that never moves (#453) An array value is one pointer to { data, length, capacity }. Assigning, passing and storing an array copy the pointer, so a push, pop, splice or length= through any name is seen through every other. Under rc the header is the counted block; a null header reads as an empty array and gets a header before a change through its slot. Also: realloc and free of a null payload (an empty array's data) pass null on instead of null minus the block word; `null` as an array is a null header; .view() copies its slice; a constant array inside global data gets a static, immortal header. Co-Authored-By: Claude Opus 5.5 --- tslang/include/TypeScript/Defines.h | 1 + .../TypeScript/LowerToLLVM/ArrayLayout.h | 196 +++++++++++++++--- .../TypeScript/LowerToLLVM/CastLogicHelper.h | 42 ++-- .../TypeScript/LowerToLLVM/LLVMCodeHelper.h | 36 ++-- .../LowerToLLVM/LLVMCodeHelperBase.h | 28 ++- .../LowerToLLVM/OwnershipRoutineLogic.h | 26 ++- tslang/lib/TypeScript/LowerToLLVM.cpp | 28 +-- tslang/test/tester/CMakeLists.txt | 9 + tslang/test/tester/tests/00array_reference.ts | 36 ++++ .../tester/tests/00array_static_nested.ts | 17 ++ .../test/tester/tests/00array_zero_slots.ts | 21 ++ tslang/tslang/jit.cpp | 19 +- 12 files changed, 348 insertions(+), 111 deletions(-) create mode 100644 tslang/test/tester/tests/00array_reference.ts create mode 100644 tslang/test/tester/tests/00array_static_nested.ts create mode 100644 tslang/test/tester/tests/00array_zero_slots.ts diff --git a/tslang/include/TypeScript/Defines.h b/tslang/include/TypeScript/Defines.h index ddab22dfb..87e4ad0bf 100644 --- a/tslang/include/TypeScript/Defines.h +++ b/tslang/include/TypeScript/Defines.h @@ -225,6 +225,7 @@ #define ARRAY_DATA_INDEX 0 #define ARRAY_SIZE_INDEX 1 +#define ARRAY_CAPACITY_INDEX 2 #define OPTIONAL_VALUE_INDEX 0 #define OPTIONAL_HASVALUE_INDEX 1 diff --git a/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h b/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h index 1b8ddf02b..2275d4c79 100644 --- a/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h +++ b/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h @@ -5,6 +5,7 @@ #include "TypeScript/TypeScriptOps.h" #include "TypeScript/MLIRLogic/MLIRHelper.h" #include "TypeScript/LowerToLLVM/LLVMCodeHelperBase.h" +#include "TypeScript/LowerToLLVM/CodeLogicHelper.h" #include "mlir/Dialect/LLVMIR/LLVMDialect.h" @@ -12,8 +13,14 @@ namespace typescript { // The one place that knows how an array value is laid out -// (docs/superpowers/specs/2026-10-03-array-reference-design.md). An array value is a -// { data, length } struct; a `slot` is a pointer to one. +// (docs/superpowers/specs/2026-10-03-array-reference-design.md, section 3). An array value is one +// pointer to a heap header { data, length, capacity } that never moves; only `data` is +// reallocated. Copying an array copies the pointer, so a change of length through one name is +// seen through every other (#453). A `slot` is a pointer to where an array value is stored. +// +// A null header - zeroed memory: an element of an array grown by `length =`, a field with no +// initializer, `undefined` or `null` as an array - reads as an empty array, and an op that +// changes an array through its slot gives the slot a fresh empty header first (ruling R1). class ArrayLayout : public LLVMCodeHelperBase { public: @@ -23,18 +30,58 @@ class ArrayLayout : public LLVMCodeHelperBase { } - // what the field GEPs are based on, for an op that changes the array held in `slot` - mlir::Value headerForUpdate(mlir_ts::ArrayType, mlir::Value slot) + // { data, length, capacity }: what an array value points to + LLVM::LLVMStructType headerType() { - return slot; + TypeHelper th(rewriter); + auto llvmIndexType = typeConverter->convertType(th.getIndexType()); + return LLVM::LLVMStructType::getLiteral(rewriter.getContext(), {th.getPtrType(), llvmIndexType, llvmIndexType}, + false); } - // the same, for a routine that only reads (never materialises: R1) + // the header an op that changes the array held in `slot` works on. A slot holding null - a + // zeroed element or field, `undefined` as an array - gets an empty header first, so a change + // made through the slot is kept (R1) + mlir::Value headerForUpdate(mlir_ts::ArrayType arrayType, mlir::Value slot) + { + TypeHelper th(rewriter); + CodeLogicHelper clh(op, rewriter); + auto loc = op->getLoc(); + auto ptrType = th.getPtrType(); + mlir::Value header = rewriter.create(loc, ptrType, slot); + auto isNull = rewriter.create(loc, LLVM::ICmpPredicate::eq, header, + rewriter.create(loc, ptrType)); + return clh.conditionalExpressionLowering( + loc, ptrType, isNull, + [&](OpBuilder &, Location) -> mlir::Value { + auto fresh = MemoryAlloc(headerType(), MemoryAllocSet::Zero); + if (compileOptions.memoryModel == MemoryModelRC) + { + // the slot is the header's one reference, and nothing above lowering knows + // the header exists to take it: born at zero, the owner's release would + // underflow the count to immortal and leak it + auto llvmIndexType = typeConverter->convertType(th.getIndexType()); + rewriter.create( + loc, + rewriter.create(loc, llvmIndexType, rewriter.getIntegerAttr(llvmIndexType, 1)), + getBlockPtrFromPayloadPtr(loc, fresh, llvmIndexType)); + } + + rewriter.create(loc, fresh, slot); + return fresh; + }, + [&](OpBuilder &, Location) -> mlir::Value { return header; }); + } + + // the same, for a routine that only reads: the header held in `slot`, possibly null (never + // materialised: R1) mlir::Value headerForRead(mlir_ts::ArrayType, mlir::Value slot) { - return slot; + TypeHelper th(rewriter); + return rewriter.create(op->getLoc(), th.getPtrType(), slot); } + // the field addresses of a non-null header mlir::Value dataAddress(mlir_ts::ArrayType arrayType, mlir::Value header) { return fieldAddress(arrayType, header, ARRAY_DATA_INDEX); @@ -45,63 +92,148 @@ class ArrayLayout : public LLVMCodeHelperBase return fieldAddress(arrayType, header, ARRAY_SIZE_INDEX); } - // of an array value - mlir::Value data(mlir_ts::ArrayType, mlir::Value array) + mlir::Value capacityAddress(mlir_ts::ArrayType arrayType, mlir::Value header) + { + return fieldAddress(arrayType, header, ARRAY_CAPACITY_INDEX); + } + + // of an array value; a null header reads as an empty array (R1) + mlir::Value data(mlir_ts::ArrayType arrayType, mlir::Value array) { TypeHelper th(rewriter); - return rewriter.create(op->getLoc(), th.getPtrType(), array, - MLIRHelper::getStructIndex(rewriter, ARRAY_DATA_INDEX)); + return loadOrZero(array, th.getPtrType(), ARRAY_DATA_INDEX); } - mlir::Value length(mlir_ts::ArrayType, mlir::Value array) + mlir::Value length(mlir_ts::ArrayType arrayType, mlir::Value array) { TypeHelper th(rewriter); - return rewriter.create(op->getLoc(), typeConverter->convertType(th.getIndexType()), array, - MLIRHelper::getStructIndex(rewriter, ARRAY_SIZE_INDEX)); + return loadOrZero(array, typeConverter->convertType(th.getIndexType()), ARRAY_SIZE_INDEX); } - // what `===` and truthiness compare - mlir::Value identity(mlir_ts::ArrayType arrayType, mlir::Value array) + // what `===` and truthiness compare: the header pointer + mlir::Value identity(mlir_ts::ArrayType, mlir::Value array) { - return data(arrayType, array); + return array; } // a new array over `data` (owned by the array) with `length` elements mlir::Value make(mlir_ts::ArrayType arrayType, mlir::Value data, mlir::Value length) { auto loc = op->getLoc(); - auto llvmArrayType = typeConverter->convertType(arrayType); - mlir::Value value = rewriter.create(loc, llvmArrayType); - value = rewriter.create(loc, llvmArrayType, value, data, - MLIRHelper::getStructIndex(rewriter, ARRAY_DATA_INDEX)); - return rewriter.create(loc, llvmArrayType, value, length, - MLIRHelper::getStructIndex(rewriter, ARRAY_SIZE_INDEX)); + auto header = MemoryAlloc(headerType()); + rewriter.create(loc, data, fieldAddress(arrayType, header, ARRAY_DATA_INDEX)); + rewriter.create(loc, length, fieldAddress(arrayType, header, ARRAY_SIZE_INDEX)); + rewriter.create(loc, length, fieldAddress(arrayType, header, ARRAY_CAPACITY_INDEX)); + return header; } - // a constant array inside a global's initializer: `data` is static, nothing is allocated - // (Task 4 replaces this with makeStatic(arrayType, dataGlobalName, dataOffsetBytes, length)) - mlir::Value makeStatic(mlir_ts::ArrayType arrayType, mlir::Value data, mlir::Value length) + // a constant array in global data: a header global "ah_" holding + // { data, length, capacity = length }, preceded under rc and own by an immortal block word (as + // getOrCreateGlobalArray does for the data). Nothing is allocated, so this works inside + // another global's initializer. Returns the header's address. + mlir::Value makeStatic(mlir_ts::ArrayType arrayType, StringRef dataGlobalName, int64_t dataOffsetBytes, + int64_t length) { - return make(arrayType, data, length); + auto loc = op->getLoc(); + auto parentModule = op->getParentOfType(); + TypeHelper th(rewriter); + auto llvmIndexType = typeConverter->convertType(th.getIndexType()); + auto withWord = compileOptions.tracksOwnership(); + auto headerName = ("ah_" + dataGlobalName).str(); + + auto global = parentModule.lookupSymbol(headerName); + if (!global) + { + OpBuilder::InsertionGuard guard(rewriter); + rewriter.setInsertionPointToStart(parentModule.getBody()); + mlir::Type globalType = headerType(); + if (withWord) + { + globalType = LLVM::LLVMStructType::getLiteral(rewriter.getContext(), {llvmIndexType, headerType()}, + /*isPacked=*/true); + } + + global = rewriter.create(loc, globalType, /*isConstant=*/true, LLVM::Linkage::Internal, + headerName, mlir::Attribute{}); + global.setAlignment(getHeapBlockHeaderSize()); + + auto ®ion = global.getInitializerRegion(); + rewriter.setInsertionPointToStart(rewriter.createBlock(®ion)); + auto llvmLength = + rewriter.create(loc, llvmIndexType, rewriter.getIntegerAttr(llvmIndexType, length)); + mlir::Value dataPtr = rewriter.create(loc, th.getPtrType(), dataGlobalName); + if (dataOffsetBytes != 0) + { + dataPtr = rewriter.create( + loc, th.getPtrType(), th.getI8Type(), dataPtr, + ValueRange{rewriter.create( + loc, llvmIndexType, rewriter.getIntegerAttr(llvmIndexType, dataOffsetBytes))}); + } + + mlir::Value headerVal = rewriter.create(loc, headerType()); + headerVal = rewriter.create(loc, headerVal, dataPtr, + MLIRHelper::getStructIndex(rewriter, ARRAY_DATA_INDEX)); + headerVal = rewriter.create(loc, headerVal, llvmLength, + MLIRHelper::getStructIndex(rewriter, ARRAY_SIZE_INDEX)); + headerVal = rewriter.create(loc, headerVal, llvmLength, + MLIRHelper::getStructIndex(rewriter, ARRAY_CAPACITY_INDEX)); + mlir::Value globalVal = headerVal; + if (withWord) + { + globalVal = rewriter.create(loc, globalType); + globalVal = rewriter.create( + loc, globalVal, + rewriter.create(loc, llvmIndexType, + rewriter.getIntegerAttr(llvmIndexType, HEAP_BLOCK_IMMORTAL)), + MLIRHelper::getStructIndex(rewriter, 0)); + globalVal = rewriter.create(loc, globalVal, headerVal, + MLIRHelper::getStructIndex(rewriter, 1)); + } + + rewriter.create(loc, ValueRange{globalVal}); + } + + mlir::Value address = rewriter.create(loc, global); + return withWord ? getPayloadPtrFromBlockPtr(loc, address, llvmIndexType) : address; } - mlir::Value zero(mlir_ts::ArrayType arrayType) + // no array at all: a null header, which reads as empty (R1) + mlir::Value zero(mlir_ts::ArrayType) { - return rewriter.create(op->getLoc(), typeConverter->convertType(arrayType)); + TypeHelper th(rewriter); + return rewriter.create(op->getLoc(), th.getPtrType()); } mlir::Value undef(mlir_ts::ArrayType arrayType) { - return rewriter.create(op->getLoc(), typeConverter->convertType(arrayType)); + return zero(arrayType); } private: - mlir::Value fieldAddress(mlir_ts::ArrayType arrayType, mlir::Value header, int32_t index) + mlir::Value fieldAddress(mlir_ts::ArrayType, mlir::Value header, int32_t index) { TypeHelper th(rewriter); - return rewriter.create(op->getLoc(), th.getPtrType(), typeConverter->convertType(arrayType), header, + return rewriter.create(op->getLoc(), th.getPtrType(), headerType(), header, ArrayRef{0, index}); } + + mlir::Value loadOrZero(mlir::Value header, mlir::Type fieldType, int32_t index) + { + TypeHelper th(rewriter); + CodeLogicHelper clh(op, rewriter); + auto loc = op->getLoc(); + auto isSet = rewriter.create(loc, LLVM::ICmpPredicate::ne, header, + rewriter.create(loc, th.getPtrType())); + return clh.conditionalExpressionLowering( + loc, fieldType, isSet, + [&](OpBuilder &, Location) -> mlir::Value { + return rewriter.create( + loc, fieldType, + rewriter.create(loc, th.getPtrType(), headerType(), header, + ArrayRef{0, index})); + }, + [&](OpBuilder &, Location) -> mlir::Value { return rewriter.create(loc, fieldType); }); + } }; } // namespace typescript diff --git a/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h b/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h index 7a3fa7eef..8b7dbc461 100644 --- a/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h +++ b/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h @@ -1116,30 +1116,20 @@ class CastLogicHelper return layout.undef(arrayTypeTs); } - auto arrayValueSize = LLVM::LLVMArrayType::get(llvmSrcElementType, size); - - mlir::Value arrayPtr; - if (byValue) + if (!byValue) { - auto bytesSize = rewriter.create(loc, th.getIndexType(), arrayValueSize); - // TODO: create MemRef which will store information about memory. stack of heap, to use in array push to realloc - // auto copyAllocated = ch.Alloca(arrayPtrType, bytesSize); - auto copyAllocated = ch.MemoryAlloc(bytesSize); - - auto ptrToArraySrc = in; - auto ptrToArrayDst = copyAllocated; - rewriter.create(loc, ptrToArrayDst, ptrToArraySrc, bytesSize); - - arrayPtr = copyAllocated; - } - else - { - // copy ptr only (const ptr -> ptr) - // TODO: here we need to clone body to make it writable (and remove logic from VariableOp) - arrayPtr = in; + // `null` as an array: no array, which reads as empty (R1) and compares equal to null + return layout.zero(arrayTypeTs); } - return layout.make(arrayTypeTs, arrayPtr, sizeValue); + // a constant array's data is static: the array gets a copy of it, so a push reallocates + // the copy and never the read-only original + auto arrayValueSize = LLVM::LLVMArrayType::get(llvmSrcElementType, size); + auto bytesSize = rewriter.create(loc, th.getIndexType(), arrayValueSize); + auto copyAllocated = ch.MemoryAlloc(bytesSize); + rewriter.create(loc, copyAllocated, in, bytesSize); + + return layout.make(arrayTypeTs, copyAllocated, sizeValue); } mlir::Value castToAny(mlir::Value in, mlir::Type inType, mlir::Type inLLVMType) @@ -1178,11 +1168,6 @@ class CastLogicHelper auto nullTag = toh.typeOfLogic(loc, mlir_ts::NullType::get(rewriter.getContext())); mlir::Value pointer = in.getType() == inLLVMType ? in : rewriter.create(loc, inLLVMType, in); - if (isa(pointer.getType())) - { - // an array is { data, length }: null has no data - pointer = rewriter.create(loc, pointer, 0); - } auto isSet = rewriter.create(loc, LLVM::ICmpPredicate::ne, pointer, rewriter.create(loc, th.getPtrType())); auto tagType = setTag.getType(); @@ -1293,7 +1278,10 @@ class CastLogicHelper // the data pointer of an array value, for the casts to a ref of the element and to opaque mlir::Value extractArrayData(mlir::Value in, mlir_ts::ArrayType arrayType) { - return ArrayLayout(op, rewriter, tch.typeConverter, compileOptions).data(arrayType, in); + auto llvmArrayType = tch.convertType(arrayType); + mlir::Value inAsLLVMType = + in.getType() == llvmArrayType ? in : rewriter.create(loc, llvmArrayType, in); + return ArrayLayout(op, rewriter, tch.typeConverter, compileOptions).data(arrayType, inAsLLVMType); } }; diff --git a/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelper.h b/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelper.h index cbbc0e57f..520bc611a 100644 --- a/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelper.h +++ b/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelper.h @@ -426,25 +426,33 @@ class LLVMCodeHelper : public LLVMCodeHelperBase return rewriter.create(loc, th.getPtrType(), descriptorType, globalPtr, ArrayRef{0, 1, 0}); } - mlir::Value getOrCreateGlobalArray(mlir::Type originalElementType, unsigned size, ArrayAttr arrayAttr) + // the name of the global holding a constant array's data + std::string getGlobalArrayName(mlir::Type originalElementType, unsigned size, ArrayAttr arrayAttr) { std::stringstream ss; ss << "a_" << size; - auto vecVarName = calc_hash_value(arrayAttr, originalElementType, ss.str().c_str()); + return calc_hash_value(arrayAttr, originalElementType, ss.str().c_str()); + } + + mlir::Value getOrCreateGlobalArray(mlir::Type originalElementType, unsigned size, ArrayAttr arrayAttr) + { + auto vecVarName = getGlobalArrayName(originalElementType, size, arrayAttr); return getOrCreateGlobalArray(originalElementType, vecVarName, size, arrayAttr); } - mlir::Value getReadOnlyRTArray(mlir::Location loc, mlir_ts::ArrayType originalArrayType, LLVM::LLVMStructType llvmArrayType, - ArrayAttr arrayValue) + // a constant array as an array value: a static header over the static data, nothing + // allocated, so it can sit inside another global's initializer (ArrayLayout::makeStatic) + mlir::Value getReadOnlyRTArray(mlir::Location loc, mlir_ts::ArrayType originalArrayType, ArrayAttr arrayValue) { auto size = arrayValue.size(); - auto itemValArrayPtr = getOrCreateGlobalArray(originalArrayType.getElementType(), size, arrayValue); - - // create ReadOnlyRuntimeArrayType - auto sizeValue = rewriter.create(loc, typeConverter->convertType(rewriter.getIndexType()), - rewriter.getIndexAttr(arrayValue.size())); - - return ArrayLayout(op, rewriter, typeConverter, compileOptions).makeStatic(originalArrayType, itemValArrayPtr, sizeValue); + auto vecVarName = getGlobalArrayName(originalArrayType.getElementType(), size, arrayValue); + // creates the data global; its address is taken again inside the header's initializer + getOrCreateGlobalArray(originalArrayType.getElementType(), vecVarName, size, arrayValue); + + // getOrCreateGlobalArray puts an immortal block word in front of the data under rc and own + auto dataOffsetBytes = compileOptions.tracksOwnership() ? getHeapBlockHeaderSize() : 0; + return ArrayLayout(op, rewriter, typeConverter, compileOptions) + .makeStatic(originalArrayType, vecVarName, dataOffsetBytes, size); } mlir::Value getArrayValue(mlir::Type originalElementType, mlir::Type llvmElementType, unsigned size, @@ -494,7 +502,7 @@ class LLVMCodeHelper : public LLVMCodeHelperBase for (auto item : arrayAttr.getValue()) { auto arrayValue = mlir::cast(item); - auto itemVal = getReadOnlyRTArray(loc, originalArrayType, mlir::cast(llvmElementType), arrayValue); + auto itemVal = getReadOnlyRTArray(loc, originalArrayType, arrayValue); arrayVal = rewriter.create(loc, arrayVal, itemVal, MLIRHelper::getStructIndex(rewriter, position++)); } @@ -747,7 +755,7 @@ class LLVMCodeHelper : public LLVMCodeHelperBase OpBuilder::InsertionGuard guard(rewriter); auto itemVal = - getReadOnlyRTArray(loc, mlir::cast(arrayType), mlir::cast(llvmType), subArrayAttr); + getReadOnlyRTArray(loc, mlir::cast(arrayType), subArrayAttr); tupleVal = rewriter.create(loc, tupleVal, itemVal, MLIRHelper::getStructIndex(rewriter, position++)); */ } @@ -757,7 +765,7 @@ class LLVMCodeHelper : public LLVMCodeHelperBase OpBuilder::InsertionGuard guard(rewriter); - auto itemVal = getReadOnlyRTArray(loc, arrayType, mlir::cast(llvmType), subArrayAttr); + auto itemVal = getReadOnlyRTArray(loc, arrayType, subArrayAttr); tupleVal = rewriter.create(loc, tupleVal, itemVal, MLIRHelper::getStructIndex(rewriter, position++)); } else if (auto constTupleType = dyn_cast(type)) diff --git a/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelperBase.h b/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelperBase.h index 13cde311f..27849b8a3 100644 --- a/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelperBase.h +++ b/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelperBase.h @@ -444,10 +444,26 @@ class LLVMCodeHelperBase // block must stay large enough for the header it carries auto headerSizeValue = createHeapBlockHeaderSizeConstant(loc, llvmIndexType); mlir::Value paddedSize = rewriter.create(loc, llvmIndexType, ValueRange{effectiveSize, headerSizeValue}); - auto blockPtrValue = getBlockPtrFromPayloadPtr(loc, ptrValue, llvmIndexType); + // A null payload has no block: an empty array's data (a header with data = null) is + // reallocated by its first push, and realloc(NULL, n) - GC_realloc's too - is malloc(n) + auto nullPtr = rewriter.create(loc, i8PtrTy); + auto isNull = rewriter.create(loc, LLVM::ICmpPredicate::eq, ptrValue, nullPtr); + mlir::Value blockPtrValue = rewriter.create( + loc, isNull, nullPtr, getBlockPtrFromPayloadPtr(loc, ptrValue, llvmIndexType)); auto callResults = rewriter.create(loc, mallocFuncOp, ValueRange{blockPtrValue, paddedSize}); - return getPayloadPtrFromBlockPtr(loc, callResults.getResult(), llvmIndexType); + mlir::Value newBlockPtr = callResults.getResult(); + if (compileOptions.tracksOwnership()) + { + // a block that was just made has the word an allocation is born with (_MemoryAlloc) + auto word = rewriter.create(loc, llvmIndexType, newBlockPtr); + auto bornWord = + rewriter.create(loc, llvmIndexType, rewriter.getIntegerAttr(llvmIndexType, 0)); + rewriter.create(loc, rewriter.create(loc, isNull, bornWord, word), + newBlockPtr); + } + + return getPayloadPtrFromBlockPtr(loc, newBlockPtr, llvmIndexType); } template mlir::LogicalResult _MemoryFree(mlir::Value ptrValue) @@ -465,9 +481,13 @@ class LLVMCodeHelperBase auto casted = rewriter.create(loc, i8PtrTy, ptrValue); - // the incoming pointer addresses the payload; free must see the block base + // the incoming pointer addresses the payload; free must see the block base. A null + // payload - an empty array's data - has no block, and free(NULL) does nothing auto llvmIndexType = tch.convertType(th.getIndexType()); - auto blockPtrValue = getBlockPtrFromPayloadPtr(loc, casted, llvmIndexType); + auto nullPtr = rewriter.create(loc, i8PtrTy); + auto isNull = rewriter.create(loc, LLVM::ICmpPredicate::eq, casted, nullPtr); + mlir::Value blockPtrValue = rewriter.create( + loc, isNull, nullPtr, getBlockPtrFromPayloadPtr(loc, casted, llvmIndexType)); rewriter.create(loc, freeFuncOp, ValueRange{blockPtrValue}); diff --git a/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h b/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h index 43e3406e8..e7c06d672 100644 --- a/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h +++ b/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h @@ -957,8 +957,8 @@ class OwnershipRoutineLogic return; } - // an array value is { data, length }; it owns the data block and, through it, the - // elements + // an array value is a pointer to its header { data, length, capacity }; the header owns + // the data block and, through it, the elements if (auto arrayType = dyn_cast(type)) { buildArrayBody(arrayType, slotPtr); @@ -1158,13 +1158,13 @@ class OwnershipRoutineLogic return; } - // an array value is { data, length }: the copy shares the data block, and the block - // already holds whatever the elements own + // an array value is a pointer to its header { data, length, capacity }, and the header + // is the counted block: the copy shares it, and it already holds whatever the elements + // own. A null header (an empty array, R1) is skipped by emitIncRef if (auto arrayType = dyn_cast(type)) { ArrayLayout layout(op, rewriter, typeConverter, compileOptions); - auto dataSlot = layout.dataAddress(arrayType, layout.headerForRead(arrayType, slotPtr)); - emitIncRef(rewriter.create(loc, ptrTy, dataSlot)); + emitIncRef(layout.headerForRead(arrayType, slotPtr)); return; } @@ -1286,12 +1286,18 @@ class OwnershipRoutineLogic auto ptrTy = th.getPtrType(); auto llvmIndexType = tch.convertType(th.getIndexType()); + // the header { data, length, capacity } is the counted block (spec section 4). On its last + // reference: release the elements [0, length), free the data block - the header holds + // its only reference, so its own count is never read - and free the header. A null + // header (an empty array, R1) holds nothing and is skipped by emitIfLastReference; a + // static header (a constant array in global data) is immortal and never freed ArrayLayout layout(op, rewriter, typeConverter, compileOptions); auto header = layout.headerForRead(arrayType, slotPtr); - auto dataSlot = layout.dataAddress(arrayType, header); - auto dataValue = rewriter.create(loc, ptrTy, dataSlot); - emitIfLastReference(dataValue, [&]() { + emitIfLastReference(header, [&]() { + auto dataSlot = layout.dataAddress(arrayType, header); + auto dataValue = rewriter.create(loc, ptrTy, dataSlot); + auto elementRoutine = getOrCreateReleaseRoutine(arrayType.getElementType()); if (!elementRoutine.empty()) { @@ -1308,7 +1314,9 @@ class OwnershipRoutineLogic }); } + // null for an empty array; MemoryFree passes that on to free(NULL) emitFreeBlock(dataValue); + emitFreeBlock(header); }); } diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index 4ee4bd669..b96f10565 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -1680,7 +1680,10 @@ struct ConstantOpLowering : public TsLlvmPattern if (auto arrayType = dyn_cast(type)) { - getOrCreateGlobalArray(constantOp, arrayType, rewriter); + // an array value is a header pointer: a static header over the static data + LLVMCodeHelper ch(constantOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); + rewriter.replaceOp(constantOp, ch.getReadOnlyRTArray(constantOp->getLoc(), arrayType, + cast(constantOp.getValue()))); return success(); } @@ -3665,8 +3668,16 @@ struct ArrayViewOpLowering : public TsLlvmPattern auto arrayPtr = layout.data(arrayType, transformed.getOp()); auto arrayOffset = ch.GetAddressOfPointerOffset(elementType, arrayPtr, transformed.getOffset()); - // create array type - auto structValue3 = layout.make(arrayType, arrayOffset, transformed.getCount()); + + // a copy of the slice: an array owns its data block, and a header over the middle of + // another array's block would free (and realloc) an interior pointer + auto sizeOfTypeValueMLIR = rewriter.create(loc, th.getIndexType(), elementType); + auto sizeOfTypeValue = rewriter.create(loc, llvmIndexType, sizeOfTypeValueMLIR); + auto bytes = rewriter.create(loc, llvmIndexType, ValueRange{sizeOfTypeValue, transformed.getCount()}); + auto copy = ch.MemoryAlloc(bytes); + rewriter.create(loc, copy, arrayOffset, bytes, /*isVolatile=*/false); + + auto structValue3 = layout.make(arrayType, copy, transformed.getCount()); rewriter.replaceOp(arrayViewOp, ValueRange{structValue3}); return success(); @@ -7208,16 +7219,9 @@ static void populateTypeScriptConversionPatterns(LLVMTypeConverter &converter, m return LLVM::LLVMArrayType::get(converter.convertType(type.getElementType()), type.getSize()); }); + // an array is a reference to its header { data, length, capacity } (ArrayLayout.h) converter.addConversion([&](mlir_ts::ArrayType type) { - TypeHelper th(m.getContext()); - - SmallVector rtArrayType; - // pointer to data type - rtArrayType.push_back(th.getPtrType()); - // field which store length of array - rtArrayType.push_back(converter.convertType(th.getIndexType())); - - return LLVM::LLVMStructType::getLiteral(type.getContext(), rtArrayType, false); + return LLVM::LLVMPointerType::get(m.getContext()); }); converter.addConversion([&](mlir_ts::RefType type) { diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 7d29fe6f7..b1fd21229 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -335,7 +335,9 @@ tslang_add_test(NAME test-compile-00-globals2 COMMAND test-runner "${PROJECT_SOU tslang_add_test(NAME test-compile-00-globals3 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00globals3.ts") tslang_add_test(NAME test-compile-00-arrays COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array.ts") tslang_add_test(NAME test-compile-00-arrays2 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array2.ts") +tslang_add_test(NAME test-compile-00-array-reference COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_reference.ts") tslang_add_test(NAME test-compile-00-arrays3 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array3.ts") +tslang_add_test(NAME test-compile-00-array-zero-slots COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_zero_slots.ts") tslang_add_test(NAME test-compile-00-arrays4-push-pop COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array4_push_pop.ts") tslang_add_test(NAME test-compile-00-array-shift COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_shift.ts") tslang_add_test(NAME test-compile-00-array-splice COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_splice.ts") @@ -344,6 +346,7 @@ tslang_add_test(NAME test-compile-00-array-spread-args COMMAND test-runner "${PR tslang_add_test(NAME test-compile-00-arrays5-deconstruct COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array5_deconst.ts") tslang_add_test(NAME test-compile-00-array-rest-copy COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_rest_copy.ts") tslang_add_test(NAME test-compile-00-arrays6 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array6.ts") +tslang_add_test(NAME test-compile-00-array-static-nested COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_static_nested.ts") tslang_add_test(NAME test-compile-00-arrays7 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array7.ts") tslang_add_test(NAME test-compile-00-arrays8-tuple-spread COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array8_tuple_spread.ts") tslang_add_test(NAME test-compile-00-arrays-assignment5 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_assignment5.ts") @@ -830,7 +833,9 @@ tslang_add_test(NAME test-jit-00-globals2 COMMAND test-runner -jit "${PROJECT_SO tslang_add_test(NAME test-jit-00-globals3 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00globals3.ts") tslang_add_test(NAME test-jit-00-arrays COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array.ts") tslang_add_test(NAME test-jit-00-arrays2 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array2.ts") +tslang_add_test(NAME test-jit-00-array-reference COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_reference.ts") tslang_add_test(NAME test-jit-00-arrays3 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array3.ts") +tslang_add_test(NAME test-jit-00-array-zero-slots COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_zero_slots.ts") tslang_add_test(NAME test-jit-00-arrays4-push-pop COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array4_push_pop.ts") tslang_add_test(NAME test-jit-00-array-shift COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_shift.ts") tslang_add_test(NAME test-jit-00-array-splice COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_splice.ts") @@ -839,6 +844,7 @@ tslang_add_test(NAME test-jit-00-array-spread-args COMMAND test-runner -jit "${P tslang_add_test(NAME test-jit-00-arrays5-deconstruct COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array5_deconst.ts") tslang_add_test(NAME test-jit-00-array-rest-copy COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_rest_copy.ts") tslang_add_test(NAME test-jit-00-arrays6 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array6.ts") +tslang_add_test(NAME test-jit-00-array-static-nested COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_static_nested.ts") tslang_add_test(NAME test-jit-00-arrays7 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array7.ts") tslang_add_test(NAME test-jit-00-arrays8-tuple-spread COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array8_tuple_spread.ts") tslang_add_test(NAME test-jit-00-arrays-assignment5 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_assignment5.ts") @@ -1658,11 +1664,14 @@ set(TSLANG_CORPUS 00const_array_push.ts 00array.ts 00array2.ts + 00array_reference.ts 00array3.ts + 00array_zero_slots.ts 00array4_push_pop.ts 00array5_deconst.ts 00array_rest_copy.ts 00array6.ts + 00array_static_nested.ts 00array7.ts 00array8_tuple_spread.ts 00arrow_generic.ts diff --git a/tslang/test/tester/tests/00array_reference.ts b/tslang/test/tester/tests/00array_reference.ts new file mode 100644 index 000000000..ec1e2fc20 --- /dev/null +++ b/tslang/test/tester/tests/00array_reference.ts @@ -0,0 +1,36 @@ +// An array is a reference: a change of length through any name is seen through every other (#453). +class Holder { items: number[] = [1]; } +type Rec = { items: number[] }; + +function pushParam(into: number[]) { into.push(2); } +function pushField(h: Holder) { h.items.push(2); } +function pushRec(r: Rec) { r.items.push(2); } +function grow(into: number[]) { into.push(2); into.push(3); into.push(4); into.push(5); } +function setLen(into: number[]) { into.length = 0; } +function popParam(into: number[]) { into.pop(); } +function setElem(into: number[]) { into[0] = 9; } + +function main() { + const a1: number[] = [1]; setElem(a1); assert(a1[0] == 9, "1 element write through a parameter"); + const a2: number[] = [1]; pushParam(a2); assert(a2.length == 2 && a2[1] == 2, "2 push through a parameter"); + const a3: number[] = [1, 2]; popParam(a3); assert(a3.length == 1, "3 pop through a parameter"); + const a4: number[] = [1, 2]; setLen(a4); assert(a4.length == 0, "4 length= through a parameter"); + const a5: number[] = [1]; const b5 = a5; b5.push(2); assert(a5.length == 2, "5 local alias"); + const h6 = new Holder(); pushField(h6); assert(h6.items.length == 2, "6 class field"); + const h7 = new Holder(); const it7 = h7.items; it7.push(2); assert(h7.items.length == 2, "7 field alias"); + const r8: Rec = { items: [1] }; pushRec(r8); assert(r8.items.length == 2, "8 record field"); + const a9: number[] = [1]; const f9 = () => { a9.push(2); }; f9(); assert(a9.length == 2, "9 closure"); + const aa10: number[][] = [[1]]; aa10[0].push(2); assert(aa10[0].length == 2, "10 nested"); + const aa11: number[][] = [[1]]; const in11 = aa11[0]; in11.push(2); assert(aa11[0].length == 2, "11 nested alias"); + const a12: number[] = [1]; const b12 = a12; assert(a12 === b12, "12 identity"); + const a13: number[] = [7]; grow(a13); assert(a13.length == 5 && a13[0] == 7 && a13[4] == 5, "13 growth"); + const a14: number[] = [1]; const h14 = new Holder(); h14.items = a14; a14.push(2); assert(h14.items.length == 2, "14 stored into a field"); + + const a15: number[] = [1]; const x15: any = a15; const y15 = x15 as number[]; y15.push(2); assert(a15.length == 2, "15 through any"); + const c16: number[] = []; const d16: number[] = []; + assert(!(c16 === d16), "16 two empty arrays are different arrays"); + let truthy = false; if (c16) { truthy = true; } assert(truthy, "16 an empty array is truthy"); + let n16: number[] | null = null; let falsy = true; if (n16) { falsy = false; } assert(falsy, "16 null is falsy"); + + print("done."); +} diff --git a/tslang/test/tester/tests/00array_static_nested.ts b/tslang/test/tester/tests/00array_static_nested.ts new file mode 100644 index 000000000..0bf812350 --- /dev/null +++ b/tslang/test/tester/tests/00array_static_nested.ts @@ -0,0 +1,17 @@ +// Constant arrays inside global data keep their elements, and are never freed. +const aa: number[][] = [[1], [2, 3]]; +const t: [number, number[]] = [1, [4, 5]]; +// inferred, so the inner array stays in global data: a static header over static data +const st = [1, [6, 7]]; + +function main() { + assert(aa.length == 2 && aa[1].length == 2 && aa[1][1] == 3, "nested constant array"); + assert(t[1].length == 2 && t[1][0] == 4, "array in a constant tuple"); + for (let i = 0; i < 3; i++) { + const inner = aa[1]; + assert(inner[0] == 2, "read through a local, repeatedly"); + const sinner = st[1]; + assert(sinner.length == 2 && sinner[1] == 7, "a static header, read through a local repeatedly"); + } + print("done."); +} diff --git a/tslang/test/tester/tests/00array_zero_slots.ts b/tslang/test/tester/tests/00array_zero_slots.ts new file mode 100644 index 000000000..b00ff6d42 --- /dev/null +++ b/tslang/test/tester/tests/00array_zero_slots.ts @@ -0,0 +1,21 @@ +// Zeroed memory is an empty array: nested arrays made by length, and a field with no initializer. +class Bag { + items: number[]; + add(v: number) { this.items.push(v); } +} + +function main() { + // `new Array(2)` needs the default library; growing by `length =` zeroes the same way + const aa: number[][] = []; + aa.length = 2; + assert(aa[0].length == 0, "a zeroed element reads as empty"); + aa[0].push(1); + aa[0].push(2); + assert(aa[0].length == 2 && aa[0][1] == 2 && aa[1].length == 0, "push into a zeroed element"); + + const b = new Bag(); + b.add(5); + assert(b.items.length == 1 && b.items[0] == 5, "push into a field with no initializer"); + + print("done."); +} diff --git a/tslang/tslang/jit.cpp b/tslang/tslang/jit.cpp index ecc2bc661..69ce3e104 100644 --- a/tslang/tslang/jit.cpp +++ b/tslang/tslang/jit.cpp @@ -475,11 +475,13 @@ static bool callEntryThunk(JitEntryThunkFn entryThunk, int argc, char **argv, in } // The entry point keeps whatever signature its TS declaration lowered to - `void @main()`, -// `double @main()`, `i32 @main(i32, { ptr, i64 })`, ... - and calling it through a C++ function +// `double @main()`, `i32 @main(i32, ptr)`, ... - and calling it through a C++ function // pointer of the wrong type reads garbage arguments. So give the JIT one signature to call, the C // entry point's, and let this thunk adapt it in IR, where the entry's own types are known: `argc` -// converted to the first parameter's type, `argv` and `argc` as the `string[]` (data, length) of the -// second or `argv` alone for a `Ref`, and the result converted to the exit code. LowerToAffineLoops has already refused any +// converted to the first parameter's type, `argv` as it is for a second parameter that is a +// pointer, and the result converted to the exit code. A `Ref` second parameter is C's +// `char **`; a `string[]` one is a pointer to an array header too, and gets the `char **` until +// MLIRGen builds the array from it (ts.ArrayFromCStrings, array-reference spec section 5). LowerToAffineLoops has already refused any // other shape of `main`, with a source location; this still checks, for an entry picked with `-e`. llvm::Error addEntryThunk(llvm::Module &llvmModule, llvm::StringRef entryName) { @@ -516,7 +518,6 @@ llvm::Error addEntryThunk(llvm::Module &llvmModule, llvm::StringRef entryName) llvm::SmallVector args; for (auto [index, paramType] : llvm::enumerate(entryType->params())) { - auto *arrayType = llvm::dyn_cast(paramType); if (index == 0 && paramType->isIntegerTy()) { args.push_back(builder.CreateSExtOrTrunc(argc, paramType)); @@ -527,17 +528,9 @@ llvm::Error addEntryThunk(llvm::Module &llvmModule, llvm::StringRef entryName) } else if (index == 1 && paramType->isPointerTy()) { - // `Ref`: the C `char **` as it is + // the C `char **` as it is args.push_back(argv); } - else if (index == 1 && arrayType && arrayType->getNumElements() == 2 && - arrayType->getElementType(0)->isPointerTy() && arrayType->getElementType(1)->isIntegerTy()) - { - llvm::Value *array = llvm::UndefValue::get(arrayType); - array = builder.CreateInsertValue(array, argv, 0); - array = builder.CreateInsertValue(array, builder.CreateSExtOrTrunc(argc, arrayType->getElementType(1)), 1); - args.push_back(array); - } else { thunk->eraseFromParent(); From 07c407062bbd7187372fe5859958a0fb21dc72c7 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 3 Oct 2026 23:55:19 +0100 Subject: [PATCH 07/19] A declared array with no initializer starts as no array (#453) `let g: C[];` left its slot unwritten. Reading an element of it now compares the header pointer with null and branches on the result, and a branch on an unwritten slot is undefined behaviour the optimizer took: newWithSpread.ts faulted in every model. The slot starts null instead, which reads as an empty array (ruling R1). Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/LowerToLLVM.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index b96f10565..1998b9926 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -2744,6 +2744,14 @@ struct VariableOpLowering : public TsLlvmPattern // a collected build is meant to come out of this step byte-identical. rewriter.create(location, rewriter.create(location, storageType), allocated); } + else if (!value && isa(referenceType.getElementType())) + { + // `let a: T[];`: an array is a header pointer, and reading one out of an unwritten + // slot - even only to its data - branches on whatever the frame held, which the + // optimizer may take as unreachable (newWithSpread.ts's `let g: C[]; g[1]` faulted). + // Null is no array, which reads as empty (array-reference spec, ruling R1) + rewriter.create(location, rewriter.create(location, storageType), allocated); + } if (value) { From 4f76d1204a4ea094a724c8c740ab48513deed876 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 4 Oct 2026 00:11:34 +0100 Subject: [PATCH 08/19] A view retains what it copies; capacity follows length (#453) `.view()` copies its slice into a new array, and the new array releases its elements like any other; under rc it now takes a reference to each one it copied, so releasing the view no longer frees elements the source still holds. push, pop, shift, unshift, splice and length= write the capacity with the length, keeping capacity = length until growth uses it. The spec records the accepted limit of ruling R1 for a null slot passed by value, and stale { data, length } comments describe the header. Co-Authored-By: Claude Opus 5.5 --- .../2026-10-03-array-reference-design.md | 4 +++ .../TypeScript/LowerToLLVM/ArrayLayout.h | 2 +- .../LowerToLLVM/OwnershipRoutineLogic.h | 30 +++++++++++++++++ tslang/lib/TypeScript/LowerToLLVM.cpp | 29 ++++++++++++----- tslang/lib/TypeScript/MLIRGenImpl.h | 6 ++-- tslang/test/tester/tests/00array_view.ts | 32 +++++++++++++++++++ 6 files changed, 91 insertions(+), 12 deletions(-) diff --git a/tslang/docs/superpowers/specs/2026-10-03-array-reference-design.md b/tslang/docs/superpowers/specs/2026-10-03-array-reference-design.md index be95778e1..f35f1066f 100644 --- a/tslang/docs/superpowers/specs/2026-10-03-array-reference-design.md +++ b/tslang/docs/superpowers/specs/2026-10-03-array-reference-design.md @@ -67,6 +67,10 @@ struct fails. Under rc a parameter push exits with 127 and prints nothing. `undefined` cast to an array) reads as an empty array, and an op that changes an array through its slot stores a fresh empty header into a slot holding null first. This keeps today's behaviour, where a zeroed `{ data, length }` is an empty array (plan ruling R1). +- A null slot passed by value to a parameter (an unset field, an element made by `length =`): + a push through the parameter materialises a header in the parameter's copy, so the slot's + owner does not see the change and, under rc and own, that header leaks. Accepted: such a slot + holds `undefined` in TypeScript terms, where `.push` would throw (ruling T4-D). - `ConstArray` (a literal's static data) is unchanged. The cast from `ConstArray` to `T[]` makes a header and copies the data into a fresh block on both of its paths; the path that today points the struct at the static data (`byValue = false`) copies as well, since `push` would otherwise diff --git a/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h b/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h index 2275d4c79..b424a3d58 100644 --- a/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h +++ b/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h @@ -55,7 +55,7 @@ class ArrayLayout : public LLVMCodeHelperBase loc, ptrType, isNull, [&](OpBuilder &, Location) -> mlir::Value { auto fresh = MemoryAlloc(headerType(), MemoryAllocSet::Zero); - if (compileOptions.memoryModel == MemoryModelRC) + if (compileOptions.isRefCounted()) { // the slot is the header's one reference, and nothing above lowering knows // the header exists to take it: born at zero, the owner's release would diff --git a/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h b/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h index e7c06d672..1e37e410f 100644 --- a/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h +++ b/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h @@ -370,6 +370,36 @@ class OwnershipRoutineLogic }); } + // Takes one more reference for each of the `count` elements at `dataPtr`: a copy of them + // into a new array's block (`.view()`), whose release will release them again. Only under + // rc - own rejects a second reference to an element at compile time. + void emitRetainArrayElements(mlir::Type elementType, mlir::Value dataPtr, mlir::Value count) + { + if (!compileOptions.isRefCounted()) + { + return; + } + + auto routineName = getOrCreateRetainRoutine(elementType); + if (routineName.empty()) + { + return; + } + + TypeHelper th(rewriter); + TypeConverterHelper tch(typeConverter); + + auto loc = op->getLoc(); + auto ptrTy = th.getPtrType(); + auto llvmElementType = tch.convertType(elementType); + + emitCountedLoop(count, [&](mlir::Value index) { + auto elementPtr = rewriter.create(loc, ptrTy, llvmElementType, dataPtr, ValueRange{index}); + rewriter.create(loc, TypeRange{}, FlatSymbolRefAttr::get(rewriter.getContext(), routineName), + ValueRange{elementPtr}); + }); + } + private: // Field types of a record-shaped type, empty for anything else. llvm::SmallVector getFieldTypes(mlir::Type type) diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index 1998b9926..d4219d134 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -727,6 +727,8 @@ class SetLengthOfOpLowering : public TsLlvmPattern auto newCountAsLLVMType = rewriter.create(loc, llvmIndexType, newCountAsIndexType); rewriter.create(loc, newCountAsLLVMType, countAsIndexTypePtr); + // capacity = length until growth by doubling (spec section 6) uses it + rewriter.create(loc, newCountAsLLVMType, layout.capacityAddress(arrayType, header)); rewriter.eraseOp(op); @@ -2740,8 +2742,9 @@ struct VariableOpLowering : public TsLlvmPattern // never written. Not a corner - it is any captured declaration without an // initializer, which an owned local can never be (§9.36). // - // Only under -mm=rc: nothing reads the slot before its store in any other model, and - // a collected build is meant to come out of this step byte-identical. + // Only under rc and own: in the other models nothing reads the slot before its store, + // so this step stores nothing there - except for an array, below, which every model + // starts as null. rewriter.create(location, rewriter.create(location, storageType), allocated); } else if (!value && isa(referenceType.getElementType())) @@ -3068,8 +3071,6 @@ struct CreateArrayOpLowering : public TsLlvmPattern next = true; } - // create array type - auto llvmRtArrayStructType = tch.convertType(arrayType); ArrayLayout layout(createArrayOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); auto structValue3 = layout.make(arrayType, allocated, newCountAsIndexType); @@ -3106,8 +3107,6 @@ struct NewEmptyArrayOpLowering : public TsLlvmPattern auto allocated = rewriter.create(loc, th.getPtrType()); - // create array type - auto llvmRtArrayStructType = tch.convertType(arrayType); auto llvmIndexType = tch.convertType(th.getIndexType()); ArrayLayout layout(newEmptyArrOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); @@ -3153,8 +3152,6 @@ struct NewArrayOpLowering : public TsLlvmPattern // releases what the slot held - `malloc`'s leftovers would be released as a reference auto allocated = ch.MemoryAlloc(multSizeOfTypeValue, MemoryAllocSet::Zero); - // create array type - auto llvmRtArrayStructType = tch.convertType(arrayType); ArrayLayout layout(newArrOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); auto structValue3 = layout.make(arrayType, allocated, transformed.getCount()); @@ -3243,6 +3240,8 @@ struct ArrayPushOpLowering : public TsLlvmPattern rewriter.create(loc, allocated, currentPtrPtr); rewriter.create(loc, newCountAsIndexType, countAsIndexTypePtr); + // capacity = length until growth by doubling (spec section 6) uses it + rewriter.create(loc, newCountAsIndexType, layout.capacityAddress(arrayType, header)); rewriter.replaceOp(pushOp, ValueRange{newCountAsIndexType}); return success(); @@ -3297,6 +3296,8 @@ struct ArrayPopOpLowering : public TsLlvmPattern rewriter.create(loc, allocated, currentPtrPtr); rewriter.create(loc, newCountAsIndexType, countAsIndexTypePtr); + // capacity = length until growth by doubling (spec section 6) uses it + rewriter.create(loc, newCountAsIndexType, layout.capacityAddress(arrayType, header)); rewriter.replaceOp(popOp, ValueRange{loadedElement}); return success(); @@ -3391,6 +3392,8 @@ struct ArrayUnshiftOpLowering : public TsLlvmPattern rewriter.create(loc, allocated, currentPtrPtr); rewriter.create(loc, newCountAsIndexType, countAsIndexTypePtr); + // capacity = length until growth by doubling (spec section 6) uses it + rewriter.create(loc, newCountAsIndexType, layout.capacityAddress(arrayType, header)); rewriter.replaceOp(unshiftOp, ValueRange{newCountAsIndexType}); return success(); @@ -3454,6 +3457,8 @@ struct ArrayShiftOpLowering : public TsLlvmPattern rewriter.create(loc, allocated, currentPtrPtr); rewriter.create(loc, newCountAsIndexType, countAsIndexTypePtr); + // capacity = length until growth by doubling (spec section 6) uses it + rewriter.create(loc, newCountAsIndexType, layout.capacityAddress(arrayType, header)); rewriter.replaceOp(shiftOp, ValueRange{loadedElement}); return success(); @@ -3642,6 +3647,8 @@ struct ArraySpliceOpLowering : public TsLlvmPattern rewriter.create(loc, allocated, currentPtrPtr); rewriter.create(loc, newCountAsLLVMType, countAsIndexTypePtr); + // capacity = length until growth by doubling (spec section 6) uses it + rewriter.create(loc, newCountAsLLVMType, layout.capacityAddress(arrayType, header)); rewriter.replaceOp(spliceOp, ValueRange{newCountAsLLVMType}); return success(); @@ -3685,6 +3692,12 @@ struct ArrayViewOpLowering : public TsLlvmPattern auto copy = ch.MemoryAlloc(bytes); rewriter.create(loc, copy, arrayOffset, bytes, /*isVolatile=*/false); + // the copy holds a second reference to each element, and its release gives them back + { + OwnershipRoutineLogic orl(arrayViewOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); + orl.emitRetainArrayElements(elementType, copy, transformed.getCount()); + } + auto structValue3 = layout.make(arrayType, copy, transformed.getCount()); rewriter.replaceOp(arrayViewOp, ValueRange{structValue3}); diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index a6aaa19cb..8b89dd906 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -952,9 +952,9 @@ class MLIRGenImpl } // Does this reference address an element of an array that will release what the element - // holds? A `T[]` value is { data, length }, and its release routine walks the elements of - // the data block before freeing it (`buildArrayBody` in OwnershipRoutineLogic) - the exact - // mirror of what `releaseFields` does for an instance. So `arr[i] = x` carries the same + // holds? A `T[]` value points to a header { data, length, capacity }, and its release + // routine walks the elements of the data block before freeing it (`buildArrayBody` in + // OwnershipRoutineLogic) - the exact mirror of what `releaseFields` does for an instance. So `arr[i] = x` carries the same // debt as `obj.f = x`. // // Only ArrayType. `ts.ElementRef` also addresses a ConstArrayType, whose data is a static diff --git a/tslang/test/tester/tests/00array_view.ts b/tslang/test/tester/tests/00array_view.ts index 34ba505e2..e1e7a3d0c 100644 --- a/tslang/test/tester/tests/00array_view.ts +++ b/tslang/test/tester/tests/00array_view.ts @@ -9,4 +9,36 @@ assert(arr2.length === 3); assert(arr2[0] === 2); assert(arr2[2] === 4); +// a view is a copy that holds its own reference to each element: releasing it (it goes out of +// scope in the called function) must leave the source's elements alive. Filler allocations of +// the same size would reuse a block freed by mistake. +class Item { v: number; constructor(v: number) { this.v = v; } } + +function peekItem(a: Item[]) { + const w = a.view(0, 1); + return w[0].v + w[1].v; +} + +function peekText(a: string[]) { + const w = a.view(1, 1); + return w[0].length; +} + +function viewsReleased() { + const items: Item[] = [new Item(7), new Item(8)]; + assert(peekItem(items) == 15, "a view of class elements"); + const keepItems: Item[] = []; + for (let i = 0; i < 64; i++) { keepItems.push(new Item(100 + i)); } + assert(items[0].v == 7 && items[1].v == 8, "class elements alive after their view was released"); + + const texts: string[] = []; + for (let i = 0; i < 3; i++) { texts.push("text-" + i); } + assert(peekText(texts) == 6, "a view of string elements"); + const keepTexts: string[] = []; + for (let i = 0; i < 64; i++) { keepTexts.push("fill-" + i); } + assert(texts[1] == "text-1" && texts[2] == "text-2", "string elements alive after their view was released"); +} + +viewsReleased(); + print("done."); \ No newline at end of file From aab5e260595545a0929de99f30c1a0b6cbd09cb9 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 4 Oct 2026 00:17:28 +0100 Subject: [PATCH 09/19] The view test states how many elements each view has (#453) view(from, to) includes `to`, so view(0, 1) is two elements: the class case asserts it, and the string case asserts its one. Also reflows a comment and drops two doubled blank lines. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/LowerToLLVM.cpp | 2 -- tslang/lib/TypeScript/MLIRGenImpl.h | 4 ++-- tslang/test/tester/tests/00array_view.ts | 3 +++ 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index d4219d134..72297ea67 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -3071,7 +3071,6 @@ struct CreateArrayOpLowering : public TsLlvmPattern next = true; } - ArrayLayout layout(createArrayOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); auto structValue3 = layout.make(arrayType, allocated, newCountAsIndexType); @@ -3152,7 +3151,6 @@ struct NewArrayOpLowering : public TsLlvmPattern // releases what the slot held - `malloc`'s leftovers would be released as a reference auto allocated = ch.MemoryAlloc(multSizeOfTypeValue, MemoryAllocSet::Zero); - ArrayLayout layout(newArrOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); auto structValue3 = layout.make(arrayType, allocated, transformed.getCount()); diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 8b89dd906..e0b9d1e97 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -954,8 +954,8 @@ class MLIRGenImpl // Does this reference address an element of an array that will release what the element // holds? A `T[]` value points to a header { data, length, capacity }, and its release // routine walks the elements of the data block before freeing it (`buildArrayBody` in - // OwnershipRoutineLogic) - the exact mirror of what `releaseFields` does for an instance. So `arr[i] = x` carries the same - // debt as `obj.f = x`. + // OwnershipRoutineLogic) - the exact mirror of what `releaseFields` does for an instance. + // So `arr[i] = x` carries the same debt as `obj.f = x`. // // Only ArrayType. `ts.ElementRef` also addresses a ConstArrayType, whose data is a static // literal nothing releases, and a StringType, whose characters are not references at all. diff --git a/tslang/test/tester/tests/00array_view.ts b/tslang/test/tester/tests/00array_view.ts index e1e7a3d0c..751f8c9c5 100644 --- a/tslang/test/tester/tests/00array_view.ts +++ b/tslang/test/tester/tests/00array_view.ts @@ -15,12 +15,15 @@ assert(arr2[2] === 4); class Item { v: number; constructor(v: number) { this.v = v; } } function peekItem(a: Item[]) { + // `view(from, to)` includes `to`: two elements const w = a.view(0, 1); + assert(w.length == 2, "view(0, 1) has two elements"); return w[0].v + w[1].v; } function peekText(a: string[]) { const w = a.view(1, 1); + assert(w.length == 1, "view(1, 1) has one element"); return w[0].length; } From e212487e3a090b18ce49c57813a0e1db2a8a4c50 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 4 Oct 2026 00:42:22 +0100 Subject: [PATCH 10/19] main's argv is Ref only; a string[] argv is a compile error A string[] argv would read C's char ** as an array header. The main shape check now accepts only Ref and, for an array argv, says to use Ref and Deref(argv[i]). Spec section 5 and the plan are updated (owner's ruling 2026-10-04; the ArrayFromCStrings op is dropped). Co-Authored-By: Claude Opus 5.5 --- .../plans/2026-10-03-array-reference.md | 62 ++----------------- .../2026-10-03-array-reference-design.md | 6 +- tslang/lib/TypeScript/LowerToAffineLoops.cpp | 24 +++---- tslang/test/tester/CMakeLists.txt | 10 +++ .../lowering-errors/main_argv_string_array.ts | 1 + tslang/tslang/jit.cpp | 7 +-- 6 files changed, 34 insertions(+), 76 deletions(-) create mode 100644 tslang/test/tester/lowering-errors/main_argv_string_array.ts diff --git a/tslang/docs/superpowers/plans/2026-10-03-array-reference.md b/tslang/docs/superpowers/plans/2026-10-03-array-reference.md index e7e107bf0..05665c219 100644 --- a/tslang/docs/superpowers/plans/2026-10-03-array-reference.md +++ b/tslang/docs/superpowers/plans/2026-10-03-array-reference.md @@ -25,7 +25,7 @@ ## Rulings (spec gaps decided while planning) - **R1, a null header is an empty array.** Today zeroed memory is a valid empty array: elements of `new Array(n)`, class fields of array type with no initializer, and `undefined` cast to an array (`CastLogicHelper::castToArrayType`, `isUndef`). So: reading `data`/`length` of a null header gives null/0; an op that changes an array through its slot (`push`, `pop`, `shift`, `unshift`, `splice`, `length =`) first stores a fresh empty header into a slot that holds null; rc/own release of null does nothing (`emitIfLastReference` already skips null). Cost if wrong: one compare and branch per array read. -- **R2, `main`'s `argv: string[]` is built by a new op, not a C adapter.** Spec §5 says lowering emits a C `main(int, char **)` adapter. The array must come from the model's allocator and be seen by the ownership passes as fresh, which only MLIR-level code gets. So MLIRGen gives `main` a `Ref` second parameter and binds `argv` to a new `ts.ArrayFromCStrings(argc, argv)` op (a copy of each C string into an array). The JIT thunk's pointer branch and the C runtime then pass `char **` as they already can. Cost if wrong: one more op to teach to two passes. +- **R2 (replaced 2026-10-04), `main`'s argv is `Ref` only.** `main`'s argv is `Ref` only (C's `char **`); a `string[]` argv is a compile error that names the `Ref` form (owner's ruling 2026-10-04; replaces plan ruling R2). Spec section 5 has the same text. The original Task 5 (a `ts.ArrayFromCStrings` op) is dropped. - **R3, the rest copy is built in MLIRGen.** Spec §3 says `ts.ArrayView` becomes a copy. A copy must own its elements under rc/own; MLIRGen's existing "fresh array + synthesized loop" idiom (`MLIRGenCast.cpp`, numeric array widening) gets that from the passes for free. So MLIRGen stops emitting `ts.ArrayView` for a rest element and builds `const .rest: T[] = []; for (let .i = index; .i < .src.length; .i++) .rest.push(.src[.i]);` instead. `ArrayViewOpLowering` stays for other users (none known; see Task 3 step 1). ## Review Focus @@ -769,63 +769,13 @@ header before a change through its slot. Co-Authored-By: Claude Opus 5.5 " ``` -### Task 5: `main(argc, argv: string[])` +### Task 5: `main`'s argv is `Ref` only -**Files:** -- Modify: `tslang/include/TypeScript/TypeScriptOps.td` (new op; build with `--parallel 3` - editing this file needs `-j 3`/`-j 4` or MSVC fails with C1060) -- Modify: `tslang/lib/TypeScript/MLIRGenFunctions.cpp` (`mlirGenParameters` ~192, `mlirGenFunctionParams` ~1360) -- Modify: `tslang/lib/TypeScript/LowerToLLVM.cpp` (new lowering pattern; add to the pattern list ~8140) -- Modify: `tslang/lib/TypeScript/LowerToAffineLoops.cpp` (~2660 legal-op list) -- Modify: `tslang/lib/TypeScript/OwnershipFacts.h` (~298 fresh-op list) -- Create: `tslang/test/tester/tests/00main_argv_array.ts`; Modify: `tslang/test/tester/CMakeLists.txt` - -**Interfaces:** -- Consumes: `ArrayLayout::make`. -- Produces: `ts.ArrayFromCStrings`. - -- [ ] **Step 1: Write the failing test** `00main_argv_array.ts`: - -```ts -// `argv` as a `string[]`: an array of the program's arguments, each a copy of the C string, made -// with the memory model's allocator. argv[0] is the program (the input file under the JIT). -function main(argc: int, argv: string[]): int { - assert(argv.length == argc, "argv.length"); - assert(argv.length >= 1 && argv[0].length > 0, "argv[0]"); - argv.push("extra"); - assert(argv.length == argc + 1, "argv is an ordinary array"); - print("done."); - return 0; -} -``` - -Run under JIT and compiled (`test-runner` style): `tslang.exe --emit=jit --no-default-lib tslang/test/tester/tests/00main_argv_array.ts`. Expected now: crash or assertion failure (Task 4 removed the struct branch). - -- [ ] **Step 2: The op** (`TypeScriptOps.td`, next to `TypeScript_StringCopyOp`): - -```tablegen -// `main(argc, argv: string[])`: an array holding a copy of each of C's `argc` strings at `argv` -def TypeScript_ArrayFromCStringsOp : TypeScript_Op<"ArrayFromCStrings"> { - let arguments = (ins Index:$count, TypeScript_Ref:$argv); - let results = (outs Res:$result); -} -``` - -Use the operand constraint other ops use for a `Ref` operand (look at how `TypeScript_RefType`/`AnyRef` is spelled in the file). Add `mlir_ts::ArrayFromCStringsOp` to the legal-op list in `LowerToAffineLoops.cpp` (~2660) and to the fresh-op `isa<...>` in `OwnershipFacts.h` (~298). - -- [ ] **Step 3: Lowering.** A pattern in `LowerToLLVM.cpp`: allocate `count` pointers (`ch.MemoryAlloc(count * sizeof(ptr), MemoryAllocSet::Zero)`), loop `i` in `[0, count)` storing a copy of `argv[i]` (the same `strlen + 1` / `MemoryAlloc` / `MemcpyOp` sequence as `StringCopyOpLowering`, ~920; extract that lambda into a shared static function rather than duplicating it), then `ArrayLayout::make(arrayType, data, count)`. Build the loop with blocks as `OwnershipRoutineLogic::emitCountedLoop` does (or reuse it if reachable). - -- [ ] **Step 4: MLIRGen.** In `mlirGenParameters`, when the function is `MAIN_ENTRY_NAME`, has exactly two parameters and the second's type is `ArrayType` of `StringType`: give the second parameter the type `mlir_ts::RefType::get(getStringType())` and the name `.argv`, and remember (in the `FunctionParamDOM` or a flag on the prototype) that `argv` must be made. In `mlirGenFunctionParams`, after the loop, for that function: load `argc` (cast to `index` with `mlir_ts::CastOp`), load `.argv`, create `ts.ArrayFromCStrings`, and `DECLARE` a `VariableDeclarationDOM` named with the original parameter name, type `string[]`, holding the op's result. `LowerToAffineLoops`'s shape check already accepts `Ref` for argv; the JIT thunk's pointer branch passes C's `char **`; the C runtime does too when compiled. - -- [ ] **Step 5: Run.** - -```bash -cmake --build __build/tslang/windows-msbuild-2026-release --config Release --parallel 3 --target tslang 2>&1 | grep -E "error C|error:" | head -for mm in gc rc none own; do __build/tslang/windows-msbuild-2026-release/bin/tslang.exe --emit=jit --no-default-lib -mm=$mm tslang/test/tester/tests/00main_argv_array.ts; echo "$mm exit=$?"; done -``` - -Expected: `done.` and exit 0 for every model. Register it with `python "$R" main-argv-array 00main_argv_array.ts 00array7.ts 00array7.ts` (the corpora run it under rc and none the same way; if a corpus run fails only because it passes no arguments, remove the corpus line by hand and say so in the report). Also run `00main_argc_argv.ts` (the `Ref` form) to confirm it still passes. +Revised 2026-10-04 (owner's ruling; the `ts.ArrayFromCStrings` op is dropped). A `main` whose second parameter is `string[]` would read C's `char **` as an array header, so it becomes a compile error: -- [ ] **Step 6: Full suite, commit** (`git commit -m "main(argc, argv: string[]) gets an array of copies of the C strings ..."`; mention that compiled `argv.length` was C's `envp` before). +- `LowerToAffineLoops.cpp`: the `main` shape check accepts only `Ref` for argv; any other shape is an error with a source location, and an array argv gets the message `'main' takes argv as Ref (C's char **), not string[]; read an argument with Deref(argv[i])`. +- `jit.cpp` `addEntryThunk`: comment and `unsupported()` message name `Ref` only; no behaviour change. +- Test `tslang/test/tester/lowering-errors/main_argv_string_array.ts`, registered in `tslang/test/tester/CMakeLists.txt`, passes only when the compile output contains `takes argv as Ref`. `tests/00main_argc_argv.ts` (the `Ref` form) keeps passing. ### Task 6: debug info diff --git a/tslang/docs/superpowers/specs/2026-10-03-array-reference-design.md b/tslang/docs/superpowers/specs/2026-10-03-array-reference-design.md index f35f1066f..95656d17c 100644 --- a/tslang/docs/superpowers/specs/2026-10-03-array-reference-design.md +++ b/tslang/docs/superpowers/specs/2026-10-03-array-reference-design.md @@ -125,11 +125,7 @@ unchanged. the retain/release routines (§4). - **Debug info** (`LLVMDebugInfo.h`): an array's debug type becomes a pointer to a `{ data, length, capacity }` struct, so a debugger still shows the elements. -- **`main(argc, argv: string[])`:** MLIRGen gives `main` a `Ref` second parameter, so the - entry point takes C's `char **`, and binds `argv` to a new op `ts.ArrayFromCStrings(argc, argv)` - that makes the array with the model's allocator, copying each string (plan ruling R2). Today - the struct is read from C's `argv` and `envp`, which is the open `argv.length` = envp bug; this - fixes it. A `Ref` argv still receives C's `char **` directly. Both AOT and JIT. +- `main`'s argv is `Ref` only (C's `char **`); a `string[]` argv is a compile error that names the `Ref` form (owner's ruling 2026-10-04; replaces plan ruling R2). - **32-bit x86:** the header is three pointer-sized words; nothing is specific to x86. ## 6. Growth (PR 3) diff --git a/tslang/lib/TypeScript/LowerToAffineLoops.cpp b/tslang/lib/TypeScript/LowerToAffineLoops.cpp index 6e43eb645..65878e82f 100644 --- a/tslang/lib/TypeScript/LowerToAffineLoops.cpp +++ b/tslang/lib/TypeScript/LowerToAffineLoops.cpp @@ -2708,20 +2708,15 @@ void TypeScriptToAffineLoweringTSFuncPass::runOnFunction() // We only lower the main function as we expect that all other functions have been inlined. // The JIT calls `main` through a thunk with the C entry point's signature (see jit.cpp), which - // can only adapt the shapes a C `main` has: `argc` as an integer or a number, `argv` as a - // `string[]` or as the C `char **` itself, a `Ref`, and an exit code that is an integer - // or a number. + // can only adapt the shapes a C `main` has: `argc` as an integer or a number, `argv` as the C + // `char **` itself, a `Ref` (never a `string[]`: that is an array header, not C's + // `char **`), and an exit code that is an integer or a number. if (tsContext.compileOptions.isJit && function.getName() == MAIN_ENTRY_NAME) { auto isArgc = [](mlir::Type type) { return isa(type) || isa(type); }; auto isArgv = [](mlir::Type type) { - if (auto refType = dyn_cast(type)) - { - return isa(refType.getElementType()); - } - - auto arrayType = dyn_cast(type); - return arrayType && isa(arrayType.getElementType()); + auto refType = dyn_cast(type); + return refType && isa(refType.getElementType()); }; auto isExitCode = [&](mlir::Type type) { return isa(type) || isArgc(type); }; @@ -2733,7 +2728,14 @@ void TypeScriptToAffineLoweringTSFuncPass::runOnFunction() auto validResults = results.size() <= 1 && llvm::all_of(results, isExitCode); if (!validInputs || !validResults) { - function.emitError("expected 'main' to be 'main(argc?: i32 | number, argv?: string[] | Ref): void | i32 | number'"); + if (inputs.size() == 2 && isa(inputs[1])) + { + function.emitError("'main' takes argv as Ref (C's char **), not string[]; read an argument with Deref(argv[i])"); + } + else + { + function.emitError("expected 'main' to be 'main(argc?: i32 | number, argv?: Ref): void | i32 | number'"); + } return signalPassFailure(); } } diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 63ed4906a..40ac8fc11 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2552,6 +2552,16 @@ add_test(NAME test-compile-lowering-error-exit -o "${CMAKE_CURRENT_BINARY_DIR}/lowering-error-exit.obj") set_tests_properties(test-compile-lowering-error-exit PROPERTIES WILL_FAIL TRUE) +# `main`'s argv is `Ref` (C's `char **`) only. A `string[]` argv would read that `char **` +# as an array header, so it is a compile error that says to use `Ref`. Run under --emit=jit, +# where `main` is called, so a program that wrongly compiled would run instead of matching. +add_test(NAME test-jit-main-argv-string-array-error + COMMAND $ --emit=jit --no-default-lib + "${PROJECT_SOURCE_DIR}/test/tester/lowering-errors/main_argv_string_array.ts") +set_tests_properties(test-jit-main-argv-string-array-error + PROPERTIES PASS_REGULAR_EXPRESSION "takes argv as Ref" + FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") + # -mm=own is accepted. Phase 0: a program that allocates nothing compiles and runs under it. add_test(NAME test-own-flag-accepted COMMAND $ --emit=mlir --no-default-lib -mm=own diff --git a/tslang/test/tester/lowering-errors/main_argv_string_array.ts b/tslang/test/tester/lowering-errors/main_argv_string_array.ts new file mode 100644 index 000000000..f08f4a132 --- /dev/null +++ b/tslang/test/tester/lowering-errors/main_argv_string_array.ts @@ -0,0 +1 @@ +function main(argc: int, argv: string[]): int { print(argv.length); return 0; } diff --git a/tslang/tslang/jit.cpp b/tslang/tslang/jit.cpp index 69ce3e104..b08ed1fe6 100644 --- a/tslang/tslang/jit.cpp +++ b/tslang/tslang/jit.cpp @@ -480,9 +480,8 @@ static bool callEntryThunk(JitEntryThunkFn entryThunk, int argc, char **argv, in // entry point's, and let this thunk adapt it in IR, where the entry's own types are known: `argc` // converted to the first parameter's type, `argv` as it is for a second parameter that is a // pointer, and the result converted to the exit code. A `Ref` second parameter is C's -// `char **`; a `string[]` one is a pointer to an array header too, and gets the `char **` until -// MLIRGen builds the array from it (ts.ArrayFromCStrings, array-reference spec section 5). LowerToAffineLoops has already refused any -// other shape of `main`, with a source location; this still checks, for an entry picked with `-e`. +// `char **`; a `string[]` one is not supported (it is an array header, not C's `char **`). +// LowerToAffineLoops has already refused any other shape of `main`, with a source location; this still checks, for an entry picked with `-e`. llvm::Error addEntryThunk(llvm::Module &llvmModule, llvm::StringRef entryName) { auto *entry = llvmModule.getFunction(entryName); @@ -493,7 +492,7 @@ llvm::Error addEntryThunk(llvm::Module &llvmModule, llvm::StringRef entryName) auto unsupported = [&]() { return llvm::createStringError("entry point '" + entryName + - "' must be '(argc?: i32 | number, argv?: string[] | Ref) => void | i32 | number'"); + "' must be '(argc?: i32 | number, argv?: Ref) => void | i32 | number'"); }; auto &context = llvmModule.getContext(); From 3908d65e4d52218c9918b4f5a938b4df736105d3 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 4 Oct 2026 00:46:16 +0100 Subject: [PATCH 11/19] A string[] argv of main is refused ahead of time too (not in a DLL) Co-Authored-By: Claude Opus 5.5 --- .../plans/2026-10-03-array-reference.md | 4 ++-- tslang/lib/TypeScript/LowerToAffineLoops.cpp | 22 ++++++++++++------- tslang/test/tester/CMakeLists.txt | 22 +++++++++++++++---- tslang/tslang/jit.cpp | 4 ++-- 4 files changed, 36 insertions(+), 16 deletions(-) diff --git a/tslang/docs/superpowers/plans/2026-10-03-array-reference.md b/tslang/docs/superpowers/plans/2026-10-03-array-reference.md index 05665c219..98c06e45f 100644 --- a/tslang/docs/superpowers/plans/2026-10-03-array-reference.md +++ b/tslang/docs/superpowers/plans/2026-10-03-array-reference.md @@ -773,9 +773,9 @@ Co-Authored-By: Claude Opus 5.5 " Revised 2026-10-04 (owner's ruling; the `ts.ArrayFromCStrings` op is dropped). A `main` whose second parameter is `string[]` would read C's `char **` as an array header, so it becomes a compile error: -- `LowerToAffineLoops.cpp`: the `main` shape check accepts only `Ref` for argv; any other shape is an error with a source location, and an array argv gets the message `'main' takes argv as Ref (C's char **), not string[]; read an argument with Deref(argv[i])`. +- `LowerToAffineLoops.cpp`: in every build but a DLL (JIT and AOT) an array argv of `main` is an error with a source location, with the message `'main' takes argv as Ref (C's char **), not string[]; read an argument with Deref(argv[i])`. - `jit.cpp` `addEntryThunk`: comment and `unsupported()` message name `Ref` only; no behaviour change. -- Test `tslang/test/tester/lowering-errors/main_argv_string_array.ts`, registered in `tslang/test/tester/CMakeLists.txt`, passes only when the compile output contains `takes argv as Ref`. `tests/00main_argc_argv.ts` (the `Ref` form) keeps passing. +- Test `tslang/test/tester/lowering-errors/main_argv_string_array.ts`, registered in `tslang/test/tester/CMakeLists.txt` for `--emit=jit`, `--emit=exe` and `--emit=obj` (and as a no-error case for `--emit=dll`), passes only when the compile output contains `takes argv as Ref`. `tests/00main_argc_argv.ts` (the `Ref` form) keeps passing. ### Task 6: debug info diff --git a/tslang/lib/TypeScript/LowerToAffineLoops.cpp b/tslang/lib/TypeScript/LowerToAffineLoops.cpp index 65878e82f..8676343f5 100644 --- a/tslang/lib/TypeScript/LowerToAffineLoops.cpp +++ b/tslang/lib/TypeScript/LowerToAffineLoops.cpp @@ -2711,6 +2711,19 @@ void TypeScriptToAffineLoweringTSFuncPass::runOnFunction() // can only adapt the shapes a C `main` has: `argc` as an integer or a number, `argv` as the C // `char **` itself, a `Ref` (never a `string[]`: that is an array header, not C's // `char **`), and an exit code that is an integer or a number. + // + // A `string[]` argv is refused in every build where `main` is the entry point (all but a DLL): + // ahead of time, the C runtime passes `char **` too, which would be read as an array header. + if (!tsContext.compileOptions.isDLL && function.getName() == MAIN_ENTRY_NAME) + { + auto mainInputs = function.getFunctionType().getInputs(); + if (mainInputs.size() == 2 && isa(mainInputs[1])) + { + function.emitError("'main' takes argv as Ref (C's char **), not string[]; read an argument with Deref(argv[i])"); + return signalPassFailure(); + } + } + if (tsContext.compileOptions.isJit && function.getName() == MAIN_ENTRY_NAME) { auto isArgc = [](mlir::Type type) { return isa(type) || isa(type); }; @@ -2728,14 +2741,7 @@ void TypeScriptToAffineLoweringTSFuncPass::runOnFunction() auto validResults = results.size() <= 1 && llvm::all_of(results, isExitCode); if (!validInputs || !validResults) { - if (inputs.size() == 2 && isa(inputs[1])) - { - function.emitError("'main' takes argv as Ref (C's char **), not string[]; read an argument with Deref(argv[i])"); - } - else - { - function.emitError("expected 'main' to be 'main(argc?: i32 | number, argv?: Ref): void | i32 | number'"); - } + function.emitError("expected 'main' to be 'main(argc?: i32 | number, argv?: Ref): void | i32 | number'"); return signalPassFailure(); } } diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 40ac8fc11..ea48bee55 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2553,14 +2553,28 @@ add_test(NAME test-compile-lowering-error-exit set_tests_properties(test-compile-lowering-error-exit PROPERTIES WILL_FAIL TRUE) # `main`'s argv is `Ref` (C's `char **`) only. A `string[]` argv would read that `char **` -# as an array header, so it is a compile error that says to use `Ref`. Run under --emit=jit, -# where `main` is called, so a program that wrongly compiled would run instead of matching. +# as an array header, so it is a compile error that says to use `Ref`, under the JIT (where +# `main` is called, so a program that wrongly compiled would run instead of matching) and ahead of +# time. A DLL has no `main` entry point, so the same file must not draw the error there. +set(main_argv_file "${PROJECT_SOURCE_DIR}/test/tester/lowering-errors/main_argv_string_array.ts") add_test(NAME test-jit-main-argv-string-array-error - COMMAND $ --emit=jit --no-default-lib - "${PROJECT_SOURCE_DIR}/test/tester/lowering-errors/main_argv_string_array.ts") + COMMAND $ --emit=jit --no-default-lib "${main_argv_file}") set_tests_properties(test-jit-main-argv-string-array-error PROPERTIES PASS_REGULAR_EXPRESSION "takes argv as Ref" FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") +foreach(argv_emit exe obj) + add_test(NAME test-compile-main-argv-string-array-error-${argv_emit} + COMMAND $ --emit=${argv_emit} --no-default-lib "${main_argv_file}" + -o "${CMAKE_CURRENT_BINARY_DIR}/main-argv-string-array.${argv_emit}") + set_tests_properties(test-compile-main-argv-string-array-error-${argv_emit} + PROPERTIES PASS_REGULAR_EXPRESSION "takes argv as Ref" + FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") +endforeach() +add_test(NAME test-compile-main-argv-string-array-dll-no-error + COMMAND $ --emit=dll --no-default-lib "${main_argv_file}" + -o "${CMAKE_CURRENT_BINARY_DIR}/main-argv-string-array.dll") +set_tests_properties(test-compile-main-argv-string-array-dll-no-error + PROPERTIES FAIL_REGULAR_EXPRESSION "takes argv as Ref|Stack dump|Assertion failed") # -mm=own is accepted. Phase 0: a program that allocates nothing compiles and runs under it. add_test(NAME test-own-flag-accepted diff --git a/tslang/tslang/jit.cpp b/tslang/tslang/jit.cpp index b08ed1fe6..15a0dbc35 100644 --- a/tslang/tslang/jit.cpp +++ b/tslang/tslang/jit.cpp @@ -480,8 +480,8 @@ static bool callEntryThunk(JitEntryThunkFn entryThunk, int argc, char **argv, in // entry point's, and let this thunk adapt it in IR, where the entry's own types are known: `argc` // converted to the first parameter's type, `argv` as it is for a second parameter that is a // pointer, and the result converted to the exit code. A `Ref` second parameter is C's -// `char **`; a `string[]` one is not supported (it is an array header, not C's `char **`). -// LowerToAffineLoops has already refused any other shape of `main`, with a source location; this still checks, for an entry picked with `-e`. +// `char **`; a `string[]` one is not supported (it is an array header, not C's +// `char **`). LowerToAffineLoops has already refused any other shape of `main`, with a source location; this still checks, for an entry picked with `-e`. llvm::Error addEntryThunk(llvm::Module &llvmModule, llvm::StringRef entryName) { auto *entry = llvmModule.getFunction(entryName); From b27609026a97e66f1dc00afb511c3788c5e405d4 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 4 Oct 2026 00:48:30 +0100 Subject: [PATCH 12/19] Debug info: an array is a pointer to { data, length, capacity } Co-Authored-By: Claude Opus 5.5 --- tslang/include/TypeScript/LowerToLLVM/LLVMDebugInfo.h | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tslang/include/TypeScript/LowerToLLVM/LLVMDebugInfo.h b/tslang/include/TypeScript/LowerToLLVM/LLVMDebugInfo.h index d4ad68f65..942f7ace5 100644 --- a/tslang/include/TypeScript/LowerToLLVM/LLVMDebugInfo.h +++ b/tslang/include/TypeScript/LowerToLLVM/LLVMDebugInfo.h @@ -196,7 +196,8 @@ class LLVMDebugInfoHelper #ifndef ENABLE_DEBUGINFO_PATCH_INFO if (auto arrayType = dyn_cast(type)) { - return getDITypeWithFields(location, arrayType, to_print(arrayType), false, file, line, scope); + // an array value is a pointer to its heap header { data, length, capacity }, same shape as a class + return getDIPointerType(getDITypeWithFields(location, arrayType, to_print(arrayType), false, file, line, scope), file, line); } #endif @@ -500,6 +501,11 @@ class LLVMDebugInfoHelper MLIRTypeHelper mth(context, compileOptions); llvm::SmallVector destTupleFields; auto hasFields = mlir::succeeded(mth.getFields(typeWithFields, destTupleFields, true)); + if (isa(typeWithFields)) + { + // the array header also holds the capacity (see ArrayLayout.h); getFields() has no such field + destTupleFields.push_back({ MLIRHelper::TupleFieldName("capacity", context), mlir::IndexType::get(context), false, mlir_ts::AccessLevel::Public }); + } CompositeSizesTrack sizesTrack(llvmtch); From 483ea95dd4ee31d7a12db29a32261dd9cf269e89 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 4 Oct 2026 00:53:18 +0100 Subject: [PATCH 13/19] own: a push through a borrowed array parameter is the owner's (#453) Co-Authored-By: Claude Opus 5.5 --- tslang/test/tester/CMakeLists.txt | 2 +- tslang/test/tester/own/own_array_param_push.ts | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 tslang/test/tester/own/own_array_param_push.ts diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index ea48bee55..d9ca5c59d 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2644,7 +2644,7 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o own_nullable_view own_declared_constant own_spread_build own_interface_field_call own_string_copy_field own_string_copy_after_move own_string_copy_nullable own_string_copy_exported own_string_copy_written own_string_copy_mixed_return - own_string_copy_branch) + own_string_copy_branch own_array_param_push) tslang_add_test(NAME test-jit-own-${own_test} COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/own/${own_test}.ts") tslang_add_test(NAME test-compile-own-${own_test} COMMAND test-runner -mm=own "${PROJECT_SOURCE_DIR}/test/tester/own/${own_test}.ts") # The promise: no counting at all in what own emits. diff --git a/tslang/test/tester/own/own_array_param_push.ts b/tslang/test/tester/own/own_array_param_push.ts new file mode 100644 index 000000000..ede0d833a --- /dev/null +++ b/tslang/test/tester/own/own_array_param_push.ts @@ -0,0 +1,12 @@ +// A borrowed array parameter is the owner's array: a push through it is the owner's (#453). +function keep(into: number[]) { + into.push(2); + into.push(3); +} + +function main() { + let a: number[] = [7]; + keep(a); + assert(a.length == 3 && a[0] == 7 && a[2] == 3, "push through a borrowed parameter"); + print("done."); +} From 88b48dd67a27f277ab490c71648b5de0a6ec8766 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 4 Oct 2026 00:59:59 +0100 Subject: [PATCH 14/19] The argv DLL test links from the build tree, not the environment On Linux (and wherever GC_LIB_PATH/TSLANG_LIB_PATH are unset) the DLL link could not find gc or the runtime. -mm=none needs only the runtime, which --tslang-lib-path points at in the build tree; the check is model-independent. Co-Authored-By: Claude Opus 5.5 --- tslang/test/tester/CMakeLists.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index d9ca5c59d..2773e0ee0 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2570,8 +2570,12 @@ foreach(argv_emit exe obj) PROPERTIES PASS_REGULAR_EXPRESSION "takes argv as Ref" FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") endforeach() +# The DLL is linked, so it is given what it links from the build tree rather than the environment: +# -mm=none needs only the runtime library (gc would need the shared collector too), and the check +# does not depend on the model. add_test(NAME test-compile-main-argv-string-array-dll-no-error - COMMAND $ --emit=dll --no-default-lib "${main_argv_file}" + COMMAND $ --emit=dll --no-default-lib -mm=none + "--tslang-lib-path=${CMAKE_BINARY_DIR}/lib" "${main_argv_file}" -o "${CMAKE_CURRENT_BINARY_DIR}/main-argv-string-array.dll") set_tests_properties(test-compile-main-argv-string-array-dll-no-error PROPERTIES FAIL_REGULAR_EXPRESSION "takes argv as Ref|Stack dump|Assertion failed") From d9248e7aac1cd156d3623353b88feb0069a195ba Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 4 Oct 2026 01:19:56 +0100 Subject: [PATCH 15/19] rc: the header holds a counted reference to its data block A string made over an array's data block (`Ref(buffer[0])`, the default library's convertNumber, convertInteger and date formatters) takes a reference to the block. The release routine freed the block when the header died, without reading its count, and a string over it then read freed memory: five default-library tests failed ahead of time under rc. The data block now gets a count of 1 when a header is made over it (or when an empty array's first change allocates it), and the header's release drops that reference; the elements are released and the block freed on its last reference, as on main. Spec section 4 amended. 00array_data_string_alias fails under rc (JIT, -O0 and -O3) without the change. Co-Authored-By: Claude Opus 5.5 --- .../2026-10-03-array-reference-design.md | 14 +++++- .../TypeScript/LowerToLLVM/ArrayLayout.h | 16 ++++++- .../LowerToLLVM/LLVMCodeHelperBase.h | 8 ++-- .../LowerToLLVM/OwnershipRoutineLogic.h | 47 ++++++++++--------- tslang/test/tester/CMakeLists.txt | 3 ++ .../tester/tests/00array_data_string_alias.ts | 25 ++++++++++ 6 files changed, 86 insertions(+), 27 deletions(-) create mode 100644 tslang/test/tester/tests/00array_data_string_alias.ts diff --git a/tslang/docs/superpowers/specs/2026-10-03-array-reference-design.md b/tslang/docs/superpowers/specs/2026-10-03-array-reference-design.md index 95656d17c..66df313c4 100644 --- a/tslang/docs/superpowers/specs/2026-10-03-array-reference-design.md +++ b/tslang/docs/superpowers/specs/2026-10-03-array-reference-design.md @@ -96,8 +96,18 @@ unchanged. - **none.** As today: nothing is freed. The header is one more allocation. - **rc.** The counted block is the header. - Retain increments the header's count (today: the data block's). - - Release, on the last reference: release elements `[0, length)`, free the data block, free - the header. The data block's own count is never read: the header holds its only reference. + - The header holds one counted reference to its data block (the block is given a count of 1 + when the header is made over it, or when an empty array's first change allocates it). + - Release, on the header's last reference: drop the header's reference to the data block and + free the header; on the data block's last reference, release elements `[0, length)` and free + the block. + - Amended in Task 7: the first version freed the data block outright ("the header holds its + only reference"). The default library makes strings over an array's data block + (`Ref(buffer[0])` in `convertNumber`, `convertInteger` and the date + formatters), and such a string takes a reference to the block; under rc the block was freed + under it (five default-library tests failed ahead of time). Counting the header's reference + restores what main does. A reallocating change (`push`, and PR 3's growth) still moves the + block under such a string, as it does on main. - Slots `[length, capacity)` are never released (and, from PR 3, hold zero). - `pop` and `shift` hand the removed element's reference to the caller, as today. - **own.** The header is a single-owned block, like a class instance. diff --git a/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h b/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h index b424a3d58..f489e32fe 100644 --- a/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h +++ b/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h @@ -116,10 +116,24 @@ class ArrayLayout : public LLVMCodeHelperBase return array; } - // a new array over `data` (owned by the array) with `length` elements + // a new array over `data` (a block just allocated, or null for an empty array) with `length` + // elements mlir::Value make(mlir_ts::ArrayType arrayType, mlir::Value data, mlir::Value length) { auto loc = op->getLoc(); + if (compileOptions.isRefCounted() && !data.getDefiningOp()) + { + // under rc the header holds a counted reference to its data block, as every copy of + // the array did before the switch: a string made over the block + // (`Ref(buffer[0])`, the default library's convertNumber) takes a + // reference of its own, and the block is freed by whichever of the two lets go last + TypeHelper th(rewriter); + auto llvmIndexType = typeConverter->convertType(th.getIndexType()); + rewriter.create( + loc, rewriter.create(loc, llvmIndexType, rewriter.getIntegerAttr(llvmIndexType, 1)), + getBlockPtrFromPayloadPtr(loc, data, llvmIndexType)); + } + auto header = MemoryAlloc(headerType()); rewriter.create(loc, data, fieldAddress(arrayType, header, ARRAY_DATA_INDEX)); rewriter.create(loc, length, fieldAddress(arrayType, header, ARRAY_SIZE_INDEX)); diff --git a/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelperBase.h b/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelperBase.h index 27849b8a3..34cf68bf2 100644 --- a/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelperBase.h +++ b/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelperBase.h @@ -455,10 +455,12 @@ class LLVMCodeHelperBase mlir::Value newBlockPtr = callResults.getResult(); if (compileOptions.tracksOwnership()) { - // a block that was just made has the word an allocation is born with (_MemoryAlloc) + // a block that was just made - only an empty array's data reaches here with null - gets + // the header's reference under rc (ArrayLayout::make), and otherwise the word an + // allocation is born with (_MemoryAlloc) auto word = rewriter.create(loc, llvmIndexType, newBlockPtr); - auto bornWord = - rewriter.create(loc, llvmIndexType, rewriter.getIntegerAttr(llvmIndexType, 0)); + auto bornWord = rewriter.create( + loc, llvmIndexType, rewriter.getIntegerAttr(llvmIndexType, compileOptions.isRefCounted() ? 1 : 0)); rewriter.create(loc, rewriter.create(loc, isNull, bornWord, word), newBlockPtr); } diff --git a/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h b/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h index 1e37e410f..8e4090abc 100644 --- a/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h +++ b/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h @@ -1317,10 +1317,13 @@ class OwnershipRoutineLogic auto llvmIndexType = tch.convertType(th.getIndexType()); // the header { data, length, capacity } is the counted block (spec section 4). On its last - // reference: release the elements [0, length), free the data block - the header holds - // its only reference, so its own count is never read - and free the header. A null - // header (an empty array, R1) holds nothing and is skipped by emitIfLastReference; a - // static header (a constant array in global data) is immortal and never freed + // reference the header gives up its reference to the data block, and frees itself. The + // data block keeps its own count, as it did before the switch: a string made over it + // (`Ref(buffer[0])`, the default library's convertNumber) holds a + // reference to it and keeps it alive. On the data block's last reference: release the + // elements [0, length) and free it. A null header (an empty array, R1) or a null data + // block holds nothing and is skipped by emitIfLastReference; a static header (a constant + // array in global data) is immortal and never freed ArrayLayout layout(op, rewriter, typeConverter, compileOptions); auto header = layout.headerForRead(arrayType, slotPtr); @@ -1328,24 +1331,26 @@ class OwnershipRoutineLogic auto dataSlot = layout.dataAddress(arrayType, header); auto dataValue = rewriter.create(loc, ptrTy, dataSlot); - auto elementRoutine = getOrCreateReleaseRoutine(arrayType.getElementType()); - if (!elementRoutine.empty()) - { - auto sizeSlot = layout.lengthAddress(arrayType, header); - auto sizeValue = rewriter.create(loc, llvmIndexType, sizeSlot); - - emitCountedLoop(sizeValue, [&](mlir::Value index) { - auto llvmElementType = tch.convertType(arrayType.getElementType()); - auto elementPtr = rewriter.create(loc, ptrTy, llvmElementType, dataValue, - ValueRange{index}); - rewriter.create(loc, TypeRange{}, - FlatSymbolRefAttr::get(rewriter.getContext(), elementRoutine), - ValueRange{elementPtr}); - }); - } + emitIfLastReference(dataValue, [&]() { + auto elementRoutine = getOrCreateReleaseRoutine(arrayType.getElementType()); + if (!elementRoutine.empty()) + { + auto sizeSlot = layout.lengthAddress(arrayType, header); + auto sizeValue = rewriter.create(loc, llvmIndexType, sizeSlot); + + emitCountedLoop(sizeValue, [&](mlir::Value index) { + auto llvmElementType = tch.convertType(arrayType.getElementType()); + auto elementPtr = rewriter.create(loc, ptrTy, llvmElementType, dataValue, + ValueRange{index}); + rewriter.create(loc, TypeRange{}, + FlatSymbolRefAttr::get(rewriter.getContext(), elementRoutine), + ValueRange{elementPtr}); + }); + } + + emitFreeBlock(dataValue); + }); - // null for an empty array; MemoryFree passes that on to free(NULL) - emitFreeBlock(dataValue); emitFreeBlock(header); }); } diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 2773e0ee0..aec53f6a1 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -347,6 +347,7 @@ tslang_add_test(NAME test-compile-00-arrays5-deconstruct COMMAND test-runner "${ tslang_add_test(NAME test-compile-00-array-rest-copy COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_rest_copy.ts") tslang_add_test(NAME test-compile-00-arrays6 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array6.ts") tslang_add_test(NAME test-compile-00-array-static-nested COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_static_nested.ts") +tslang_add_test(NAME test-compile-00-array-data-string-alias COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_data_string_alias.ts") tslang_add_test(NAME test-compile-00-arrays7 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array7.ts") tslang_add_test(NAME test-compile-00-arrays8-tuple-spread COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array8_tuple_spread.ts") tslang_add_test(NAME test-compile-00-arrays-assignment5 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_assignment5.ts") @@ -846,6 +847,7 @@ tslang_add_test(NAME test-jit-00-arrays5-deconstruct COMMAND test-runner -jit "$ tslang_add_test(NAME test-jit-00-array-rest-copy COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_rest_copy.ts") tslang_add_test(NAME test-jit-00-arrays6 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array6.ts") tslang_add_test(NAME test-jit-00-array-static-nested COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_static_nested.ts") +tslang_add_test(NAME test-jit-00-array-data-string-alias COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_data_string_alias.ts") tslang_add_test(NAME test-jit-00-arrays7 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array7.ts") tslang_add_test(NAME test-jit-00-arrays8-tuple-spread COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array8_tuple_spread.ts") tslang_add_test(NAME test-jit-00-arrays-assignment5 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_assignment5.ts") @@ -1674,6 +1676,7 @@ set(TSLANG_CORPUS 00array_rest_copy.ts 00array6.ts 00array_static_nested.ts + 00array_data_string_alias.ts 00array7.ts 00array8_tuple_spread.ts 00arrow_generic.ts diff --git a/tslang/test/tester/tests/00array_data_string_alias.ts b/tslang/test/tester/tests/00array_data_string_alias.ts new file mode 100644 index 000000000..1f9fe9b70 --- /dev/null +++ b/tslang/test/tester/tests/00array_data_string_alias.ts @@ -0,0 +1,25 @@ +// A string made over an array's data block keeps the block alive after the array is released, +// as the default library's convertNumber does (`Ref(buffer[0])`): the header owns +// a reference to the data block, it does not own the block outright (#453). +function text(): string { + let buffer: char[] = []; + buffer.length = 40; + buffer[0] = 111; + buffer[1] = 107; + const s = Ref(buffer[0]); + return s; +} + +function main() { + const s = text(); + // reuse the memory a wrongly freed block would leave behind + for (let i = 0; i < 4; i++) { + let filler: char[] = []; + filler.length = 40; + for (let j = 0; j < 40; j++) filler[j] = 88; + print(filler.length); + } + print(s); + assert(s == "ok", "a string over an array's data block outlives the array"); + print("done."); +} From 7ab0f381c00085a915786eea37f4f2b539cebe81 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 4 Oct 2026 01:24:29 +0100 Subject: [PATCH 16/19] own: the header's release frees its data block outright, as before Only rc counts the data block (the header's reference plus any string made over it). Under own the header is the block's only owner; the count test there kept LLVM from removing a dead block, and own_try_local's known throw-path leak grew from 6.7 to 8.3 MB. Co-Authored-By: Claude Opus 5.5 --- .../LowerToLLVM/OwnershipRoutineLogic.h | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h b/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h index 8e4090abc..b52389bc4 100644 --- a/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h +++ b/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h @@ -1317,13 +1317,15 @@ class OwnershipRoutineLogic auto llvmIndexType = tch.convertType(th.getIndexType()); // the header { data, length, capacity } is the counted block (spec section 4). On its last - // reference the header gives up its reference to the data block, and frees itself. The - // data block keeps its own count, as it did before the switch: a string made over it - // (`Ref(buffer[0])`, the default library's convertNumber) holds a - // reference to it and keeps it alive. On the data block's last reference: release the - // elements [0, length) and free it. A null header (an empty array, R1) or a null data - // block holds nothing and is skipped by emitIfLastReference; a static header (a constant - // array in global data) is immortal and never freed + // reference the header lets go of its data block, and frees itself. Under rc the data + // block keeps a count of its own, as it did before the switch, and the header holds one + // reference to it (ArrayLayout::make): a string made over it + // (`Ref(buffer[0])`, the default library's convertNumber) holds another + // and keeps it alive. On the data block's last reference: release the elements + // [0, length) and free it. Under own the header is the data block's only owner, so the + // block goes with it, as before. A null header (an empty array, R1) holds nothing and is + // skipped by emitIfLastReference; a static header (a constant array in global data) is + // immortal and never freed; a null data block is passed on to free(NULL) ArrayLayout layout(op, rewriter, typeConverter, compileOptions); auto header = layout.headerForRead(arrayType, slotPtr); @@ -1331,7 +1333,7 @@ class OwnershipRoutineLogic auto dataSlot = layout.dataAddress(arrayType, header); auto dataValue = rewriter.create(loc, ptrTy, dataSlot); - emitIfLastReference(dataValue, [&]() { + auto releaseData = [&]() { auto elementRoutine = getOrCreateReleaseRoutine(arrayType.getElementType()); if (!elementRoutine.empty()) { @@ -1349,7 +1351,16 @@ class OwnershipRoutineLogic } emitFreeBlock(dataValue); - }); + }; + + if (compileOptions.isRefCounted()) + { + emitIfLastReference(dataValue, releaseData); + } + else + { + releaseData(); + } emitFreeBlock(header); }); From db6a74b84853b1da1e217ca7bfedb126f767c672 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 4 Oct 2026 10:15:52 +0100 Subject: [PATCH 17/19] Spec and MLIRGen comment: the rest copy is an element copy, not a push loop Co-Authored-By: Claude Opus 5.5 --- .../superpowers/specs/2026-10-03-array-reference-design.md | 3 ++- tslang/lib/TypeScript/MLIRGenImpl.h | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tslang/docs/superpowers/specs/2026-10-03-array-reference-design.md b/tslang/docs/superpowers/specs/2026-10-03-array-reference-design.md index 66df313c4..bfad8eaac 100644 --- a/tslang/docs/superpowers/specs/2026-10-03-array-reference-design.md +++ b/tslang/docs/superpowers/specs/2026-10-03-array-reference-design.md @@ -76,7 +76,8 @@ struct fails. Under rc a parameter push exits with 127 and prints nothing. the struct at the static data (`byValue = false`) copies as well, since `push` would otherwise reallocate static memory. - `[a, ...rest] = src` gives `rest` a new array holding a copy of the slice (#477). MLIRGen builds - it from a new empty array and a loop of pushes instead of emitting `ts.ArrayView`, so the + it as a new array of the slice's length and a loop that copies each element (ruling T3-A; the + plan had a loop of pushes) instead of emitting `ts.ArrayView`, so the ownership passes see a fresh array (plan ruling R3). - Layout: an array field, element, tuple slot or union payload shrinks from two words to one pointer; `T[] | undefined` is the pointer and its tag. diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index e0b9d1e97..178e28c78 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -12116,7 +12116,7 @@ class MLIRGenImpl std::optional castConstArrayToArray(mlir::Location location, mlir::Type type, mlir::Value value, mlir::Type valueType, const GenContext &genContext); std::optional castArrayElementwise(mlir::Location location, mlir_ts::ArrayType arrayType, mlir::Value value, mlir_ts::ArrayType srcArrayType, const GenContext &genContext); - // `[a, ...rest] = src`: rest as a new array filled by a push loop (#477) + // `[a, ...rest] = src`: rest as a new array of the slice's length, filled element by element (#477) ValueOrLogicalResult mlirGenArrayRestCopy(mlir::Location location, mlir_ts::ArrayType arrayType, mlir::Value source, int64_t index, const GenContext &genContext); // reports which part of a function signature does not match the one it is assigned to From 33a1af8c979242b767a4966be3d223353760953a Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 4 Oct 2026 10:47:04 +0100 Subject: [PATCH 18/19] Arrays grow by doubling; pop, shift and shrink keep the block (#453) push, unshift and splice grow the data block through ArrayLayout::ensureCapacity to max(4, 2 * capacity, needed) elements and otherwise write into the block they have, instead of reallocating to the exact new length on every call. pop, shift, a shrinking splice and a smaller `length =` keep the block and zero each slot they vacate, so gc does not keep a removed element alive and a later growth reads zero. `length = n` zeroes the slots between the old and the new length in every model, gc included: after a pop the slots past length are no longer fresh memory. pop and shift of an empty array leave it empty and give a zeroed element. ensureCapacity still goes through MemoryRealloc, which keeps an rc data block's count across a move and births a block grown from null with the header's reference. 1,000,000 pushes onto a number[] (AOT, --opt): gc 515 -> 18 ms, rc 2280 -> 15 ms. Spec section 6 (docs/superpowers/specs/2026-10-03-array-reference-design.md). Co-Authored-By: Claude Opus 5.5 --- .../TypeScript/LowerToLLVM/ArrayLayout.h | 75 ++++++ tslang/lib/TypeScript/LowerToLLVM.cpp | 246 +++++++----------- tslang/test/tester/CMakeLists.txt | 3 + tslang/test/tester/tests/00array_growth.ts | 28 ++ 4 files changed, 199 insertions(+), 153 deletions(-) create mode 100644 tslang/test/tester/tests/00array_growth.ts diff --git a/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h b/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h index f489e32fe..635165318 100644 --- a/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h +++ b/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h @@ -97,6 +97,81 @@ class ArrayLayout : public LLVMCodeHelperBase return fieldAddress(arrayType, header, ARRAY_CAPACITY_INDEX); } + // room for `needed` elements in the non-null `header` (spec section 6): when `needed` is past + // the capacity, the data block grows to max(4, 2 * capacity, needed) elements and what lies + // past the old capacity is zeroed. Returns the data pointer, already stored in the header. + // + // Every slot in [length, capacity) reads zero: growth zeroes the new slots here, and pop, + // shift, splice and a smaller `length =` zero each slot they vacate. The block goes through + // MemoryRealloc, which keeps an rc data block's count across the move and births one grown + // from null with the header's reference. A static header (makeStatic) has capacity = length + // and is never written in place by growth. + mlir::Value ensureCapacity(mlir_ts::ArrayType arrayType, mlir::Value header, mlir::Value needed) + { + TypeHelper th(rewriter); + CodeLogicHelper clh(op, rewriter); + auto loc = op->getLoc(); + auto ptrType = th.getPtrType(); + auto llvmIndexType = typeConverter->convertType(th.getIndexType()); + + auto dataPtr = dataAddress(arrayType, header); + auto capacityPtr = capacityAddress(arrayType, header); + mlir::Value currentData = rewriter.create(loc, ptrType, dataPtr); + mlir::Value capacity = rewriter.create(loc, llvmIndexType, capacityPtr); + auto mustGrow = rewriter.create(loc, LLVM::ICmpPredicate::ugt, needed, capacity); + return clh.conditionalExpressionLowering( + loc, ptrType, mustGrow, + [&](OpBuilder &, Location) -> mlir::Value { + auto constant = [&](int64_t value) -> mlir::Value { + return rewriter.create(loc, llvmIndexType, + rewriter.getIntegerAttr(llvmIndexType, value)); + }; + + mlir::Value doubled = rewriter.create(loc, llvmIndexType, ValueRange{capacity, constant(2)}); + auto doubledFits = rewriter.create(loc, LLVM::ICmpPredicate::uge, doubled, needed); + mlir::Value newCapacity = rewriter.create(loc, doubledFits, doubled, needed); + auto belowMinimum = + rewriter.create(loc, LLVM::ICmpPredicate::ult, newCapacity, constant(4)); + newCapacity = rewriter.create(loc, belowMinimum, constant(4), newCapacity); + + auto sizeOfElement = rewriter.create( + loc, llvmIndexType, + rewriter.create(loc, th.getIndexType(), arrayType.getElementType())); + auto newBytes = rewriter.create(loc, llvmIndexType, ValueRange{sizeOfElement, newCapacity}); + auto grown = MemoryRealloc(currentData, newBytes); + + auto oldBytes = rewriter.create(loc, llvmIndexType, ValueRange{sizeOfElement, capacity}); + auto tailStart = rewriter.create(loc, ptrType, th.getI8Type(), grown, ValueRange{oldBytes}); + auto tailBytes = rewriter.create(loc, llvmIndexType, ValueRange{newBytes, oldBytes}); + rewriter.create( + loc, tailStart, rewriter.create(loc, th.getI8Type(), rewriter.getI8IntegerAttr(0)), + tailBytes, /*isVolatile=*/false); + + rewriter.create(loc, grown, dataPtr); + rewriter.create(loc, newCapacity, capacityPtr); + return grown; + }, + [&](OpBuilder &, Location) -> mlir::Value { return currentData; }); + } + + // zero `count` elements of `data` from `index` on: the slots pop, shift, splice and a smaller + // `length =` vacate, so gc does not keep what they held alive and a later growth reads zero + void zeroElements(mlir_ts::ArrayType arrayType, mlir::Value data, mlir::Value index, mlir::Value count) + { + TypeHelper th(rewriter); + auto loc = op->getLoc(); + auto llvmIndexType = typeConverter->convertType(th.getIndexType()); + auto sizeOfElement = rewriter.create( + loc, llvmIndexType, rewriter.create(loc, th.getIndexType(), arrayType.getElementType())); + auto start = rewriter.create(loc, th.getPtrType(), th.getI8Type(), data, + ValueRange{rewriter.create( + loc, llvmIndexType, ValueRange{sizeOfElement, index})}); + auto bytes = rewriter.create(loc, llvmIndexType, ValueRange{sizeOfElement, count}); + rewriter.create( + loc, start, rewriter.create(loc, th.getI8Type(), rewriter.getI8IntegerAttr(0)), bytes, + /*isVolatile=*/false); + } + // of an array value; a null header reads as an empty array (R1) mlir::Value data(mlir_ts::ArrayType arrayType, mlir::Value array) { diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index 067c31565..1fc7810f1 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -675,67 +675,37 @@ class SetLengthOfOpLowering : public TsLlvmPattern ArrayLayout layout(op, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); auto header = layout.headerForUpdate(arrayType, transformed.getOp()); - auto currentPtrPtr = layout.dataAddress(arrayType, header); - auto currentPtr = rewriter.create(loc, ptrType, currentPtrPtr); auto countAsIndexTypePtr = layout.lengthAddress(arrayType, header); - auto countAsIndexType = rewriter.create(loc, llvmIndexType, countAsIndexTypePtr); - auto newCountAsIndexType = op.getNewLength(); + mlir::Value countAsLLVMType = rewriter.create(loc, llvmIndexType, countAsIndexTypePtr); + mlir::Value newCountAsLLVMType = + rewriter.create(loc, llvmIndexType, op.getNewLength()); - auto sizeOfTypeAsIndexType = rewriter.create(loc, th.getIndexType(), elementType); - - auto multSizeOfTypeValue = - rewriter.create(loc, th.getIndexType(), ValueRange{sizeOfTypeAsIndexType, newCountAsIndexType}); + // A longer array grows its block by doubling (a no-op while the capacity suffices), and a + // shorter one keeps it (spec section 6). + auto allocated = layout.ensureCapacity(arrayType, header, newCountAsLLVMType); - auto allocated = ch.MemoryRealloc(currentPtr, multSizeOfTypeValue); - - rewriter.create(loc, allocated, currentPtrPtr); - - // `arr.length = n` on a grown array exposes slots the allocator has not written. They read - // as zero: the default library's Set and Map grow their `int[]` buckets this way and take - // an unwritten bucket for an empty one, and under rc and own a store into a slot gives up - // what the slot held first (`isOwnedElementSlot`), so the release would read whatever was - // last in that memory - `result.length = this.length` followed by `result[i] = ..` is the - // default library's `Array.map`, and `arrS.map(e => e + "_")` crashed about one run in - // three (§9.37). + // The slots between the old and the new length are zeroed, whichever way the length went. // - // Every model but gc, whose collector hands back cleared memory - also when a block - // shrunk in place grows again, GC_realloc having cleared what the shrink gave up. - if (!tsLlvmContext->compileOptions.needsGCRuntime()) - { - auto oldBytes = rewriter.create(loc, th.getIndexType(), - ValueRange{sizeOfTypeAsIndexType, - rewriter.create( - loc, th.getIndexType(), countAsIndexType)}); - auto grew = rewriter.create(loc, mlir::index::IndexCmpPredicate::UGT, - multSizeOfTypeValue, oldBytes); - - auto *currentBlock = rewriter.getInsertionBlock(); - auto *continuationBlock = rewriter.splitBlock(currentBlock, rewriter.getInsertionPoint()); - auto *zeroBlock = rewriter.createBlock(continuationBlock); - - rewriter.setInsertionPointToEnd(zeroBlock); - auto tailStart = rewriter.create(loc, ptrType, th.getI8Type(), allocated, - ValueRange{rewriter.create( - loc, llvmIndexType, oldBytes)}); - auto tailBytes = rewriter.create(loc, th.getIndexType(), - multSizeOfTypeValue, oldBytes); - rewriter.create( - loc, tailStart, - rewriter.create(loc, th.getI8Type(), rewriter.getI8IntegerAttr(0)), - rewriter.create(loc, llvmIndexType, tailBytes), /*isVolatile=*/false); - rewriter.create(loc, ValueRange{}, continuationBlock); - - rewriter.setInsertionPointToEnd(currentBlock); - rewriter.create(loc, grew, zeroBlock, continuationBlock); - - rewriter.setInsertionPointToStart(continuationBlock); - } - - auto newCountAsLLVMType = rewriter.create(loc, llvmIndexType, newCountAsIndexType); + // Growing: `arr.length = n` exposes slots past the old length, and they read as zero: the + // default library's Set and Map grow their `int[]` buckets this way and take an unwritten + // bucket for an empty one, and under rc and own a store into a slot gives up what the slot + // held first (`isOwnedElementSlot`), so the release would read whatever was last in that + // memory - `result.length = this.length` followed by `result[i] = ..` is the default + // library's `Array.map`, and `arrS.map(e => e + "_")` crashed about one run in three + // (§9.37). This holds in every model, gc included: the block is no longer reallocated to + // the exact length, so after a `pop` or a shrink the slots past `length` are not fresh + // memory from the allocator but slots the array used before. + // + // Shrinking: the slots given up are zeroed, so gc does not keep what they held alive and + // a later growth reads zero. + auto grows = rewriter.create(loc, LLVM::ICmpPredicate::ugt, newCountAsLLVMType, countAsLLVMType); + auto low = rewriter.create(loc, grows, countAsLLVMType, newCountAsLLVMType); + auto high = rewriter.create(loc, grows, newCountAsLLVMType, countAsLLVMType); + layout.zeroElements(arrayType, allocated, low, + rewriter.create(loc, llvmIndexType, ValueRange{high, low})); + rewriter.create(loc, newCountAsLLVMType, countAsIndexTypePtr); - // capacity = length until growth by doubling (spec section 6) uses it - rewriter.create(loc, newCountAsLLVMType, layout.capacityAddress(arrayType, header)); rewriter.eraseOp(op); @@ -3190,8 +3160,6 @@ struct ArrayPushOpLowering : public TsLlvmPattern // TODO: use GetAddressOfArrayElement method to sync code ArrayLayout layout(pushOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); auto header = layout.headerForUpdate(arrayType, transformed.getOp()); - auto currentPtrPtr = layout.dataAddress(arrayType, header); - auto currentPtr = rewriter.create(loc, ptrType, currentPtrPtr); auto countAsIndexTypePtr = layout.lengthAddress(arrayType, header); auto countAsIndexType = rewriter.create(loc, llvmIndexType, countAsIndexTypePtr); @@ -3200,13 +3168,8 @@ struct ArrayPushOpLowering : public TsLlvmPattern auto newCountAsIndexType = rewriter.create(loc, llvmIndexType, ValueRange{countAsIndexType, incSize}); - auto sizeOfTypeValueMLIR = rewriter.create(loc, th.getIndexType(), elementType); - auto sizeOfTypeValue = rewriter.create(loc, llvmIndexType, sizeOfTypeValueMLIR); - - auto multSizeOfTypeValue = - rewriter.create(loc, llvmIndexType, ValueRange{sizeOfTypeValue, newCountAsIndexType}); - - auto allocated = ch.MemoryRealloc(currentPtr, multSizeOfTypeValue); + // room for the new elements: the block grows by doubling (spec section 6) + auto allocated = layout.ensureCapacity(arrayType, header, newCountAsIndexType); mlir::Value index = countAsIndexType; auto next = false; @@ -3243,10 +3206,7 @@ struct ArrayPushOpLowering : public TsLlvmPattern next = true; } - rewriter.create(loc, allocated, currentPtrPtr); rewriter.create(loc, newCountAsIndexType, countAsIndexTypePtr); - // capacity = length until growth by doubling (spec section 6) uses it - rewriter.create(loc, newCountAsIndexType, layout.capacityAddress(arrayType, header)); rewriter.replaceOp(pushOp, ValueRange{newCountAsIndexType}); return success(); @@ -3282,27 +3242,28 @@ struct ArrayPopOpLowering : public TsLlvmPattern auto countAsIndexTypePtr = layout.lengthAddress(arrayType, header); auto countAsIndexType = rewriter.create(loc, llvmIndexType, countAsIndexTypePtr); - auto incSize = clh.createIndexConstantOf(llvmIndexType, 1); - auto newCountAsIndexType = - rewriter.create(loc, llvmIndexType, ValueRange{countAsIndexType, incSize}); - - // load last element - auto offset = - rewriter.create(loc, th.getPtrType(), llvmElementType, currentPtr, ValueRange{newCountAsIndexType}); - auto loadedElement = rewriter.create(loc, llvmElementType, offset); - - auto sizeOfTypeValueMLIR = rewriter.create(loc, th.getIndexType(), elementType); - auto sizeOfTypeValue = rewriter.create(loc, llvmIndexType, sizeOfTypeValueMLIR); - - auto multSizeOfTypeValue = - rewriter.create(loc, llvmIndexType, ValueRange{sizeOfTypeValue, newCountAsIndexType}); - - auto allocated = ch.MemoryRealloc(currentPtr, multSizeOfTypeValue); - - rewriter.create(loc, allocated, currentPtrPtr); - rewriter.create(loc, newCountAsIndexType, countAsIndexTypePtr); - // capacity = length until growth by doubling (spec section 6) uses it - rewriter.create(loc, newCountAsIndexType, layout.capacityAddress(arrayType, header)); + // the block is kept (spec section 6): the last element is read, its slot zeroed and the + // length shortened. An empty array stays empty and gives a zeroed element - the slot + // before the first is an rc block's count word + auto isEmpty = rewriter.create(loc, LLVM::ICmpPredicate::eq, countAsIndexType, + clh.createIndexConstantOf(llvmIndexType, 0)); + auto loadedElement = clh.conditionalExpressionLowering( + loc, llvmElementType, isEmpty, + [&](OpBuilder &, Location) -> mlir::Value { return rewriter.create(loc, llvmElementType); }, + [&](OpBuilder &, Location) -> mlir::Value { + auto incSize = clh.createIndexConstantOf(llvmIndexType, 1); + auto newCountAsIndexType = + rewriter.create(loc, llvmIndexType, ValueRange{countAsIndexType, incSize}); + + // load last element + auto offset = rewriter.create(loc, th.getPtrType(), llvmElementType, currentPtr, + ValueRange{newCountAsIndexType}); + mlir::Value element = rewriter.create(loc, llvmElementType, offset); + + layout.zeroElements(arrayType, currentPtr, newCountAsIndexType, incSize); + rewriter.create(loc, newCountAsIndexType, countAsIndexTypePtr); + return element; + }); rewriter.replaceOp(popOp, ValueRange{loadedElement}); return success(); @@ -3332,8 +3293,6 @@ struct ArrayUnshiftOpLowering : public TsLlvmPattern ArrayLayout layout(unshiftOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); auto header = layout.headerForUpdate(arrayType, transformed.getOp()); - auto currentPtrPtr = layout.dataAddress(arrayType, header); - auto currentPtr = rewriter.create(loc, ptrType, currentPtrPtr); auto countAsIndexTypePtr = layout.lengthAddress(arrayType, header); auto countAsIndexType = rewriter.create(loc, llvmIndexType, countAsIndexTypePtr); @@ -3345,12 +3304,9 @@ struct ArrayUnshiftOpLowering : public TsLlvmPattern auto sizeOfTypeValueMLIR = rewriter.create(loc, th.getIndexType(), elementType); auto sizeOfTypeValue = rewriter.create(loc, llvmIndexType, sizeOfTypeValueMLIR); - auto multSizeOfTypeValue = - rewriter.create(loc, llvmIndexType, ValueRange{sizeOfTypeValue, newCountAsIndexType}); - - auto allocated = ch.MemoryRealloc(currentPtr, multSizeOfTypeValue); + // room for the new elements: the block grows by doubling (spec section 6) + auto allocated = layout.ensureCapacity(arrayType, header, newCountAsIndexType); - // realloc auto offset0 = allocated; auto offsetN = rewriter.create(loc, th.getPtrType(), llvmElementType, allocated, ValueRange{incSize}); @@ -3395,10 +3351,7 @@ struct ArrayUnshiftOpLowering : public TsLlvmPattern next = true; } - rewriter.create(loc, allocated, currentPtrPtr); rewriter.create(loc, newCountAsIndexType, countAsIndexTypePtr); - // capacity = length until growth by doubling (spec section 6) uses it - rewriter.create(loc, newCountAsIndexType, layout.capacityAddress(arrayType, header)); rewriter.replaceOp(unshiftOp, ValueRange{newCountAsIndexType}); return success(); @@ -3437,33 +3390,41 @@ struct ArrayShiftOpLowering : public TsLlvmPattern auto countAsIndexTypePtr = layout.lengthAddress(arrayType, header); auto countAsIndexType = rewriter.create(loc, llvmIndexType, countAsIndexTypePtr); - auto incSize = clh.createIndexConstantOf(llvmIndexType, 1); - auto newCountAsIndexType = - rewriter.create(loc, llvmIndexType, ValueRange{countAsIndexType, incSize}); - - // load last element - auto offset0 = - rewriter.create(loc, th.getPtrType(), llvmElementType, currentPtr, ValueRange{clh.createIndexConstantOf(llvmIndexType, 0)}); - auto loadedElement = rewriter.create(loc, llvmElementType, offset0); + // the block is kept (spec section 6): the first element is read, the rest moved down over + // it, the slot that frees at the end zeroed and the length shortened. An empty array stays + // empty and gives a zeroed element, as pop does + auto isEmpty = rewriter.create(loc, LLVM::ICmpPredicate::eq, countAsIndexType, + clh.createIndexConstantOf(llvmIndexType, 0)); + auto loadedElement = clh.conditionalExpressionLowering( + loc, llvmElementType, isEmpty, + [&](OpBuilder &, Location) -> mlir::Value { return rewriter.create(loc, llvmElementType); }, + [&](OpBuilder &, Location) -> mlir::Value { + auto incSize = clh.createIndexConstantOf(llvmIndexType, 1); + auto newCountAsIndexType = + rewriter.create(loc, llvmIndexType, ValueRange{countAsIndexType, incSize}); - auto offset1 = - rewriter.create(loc, th.getPtrType(), llvmElementType, currentPtr, ValueRange{incSize}); + // load first element + auto offset0 = rewriter.create(loc, th.getPtrType(), llvmElementType, currentPtr, + ValueRange{clh.createIndexConstantOf(llvmIndexType, 0)}); + mlir::Value element = rewriter.create(loc, llvmElementType, offset0); - auto sizeOfTypeValueMLIR = rewriter.create(loc, th.getIndexType(), storageType); - auto sizeOfTypeValue = rewriter.create(loc, llvmIndexType, sizeOfTypeValueMLIR); + auto offset1 = + rewriter.create(loc, th.getPtrType(), llvmElementType, currentPtr, ValueRange{incSize}); - auto multSizeOfTypeValue = - rewriter.create(loc, llvmIndexType, ValueRange{sizeOfTypeValue, newCountAsIndexType}); + auto sizeOfTypeValueMLIR = rewriter.create(loc, th.getIndexType(), storageType); + auto sizeOfTypeValue = rewriter.create(loc, llvmIndexType, sizeOfTypeValueMLIR); - auto multSizeOfTypeValueAdapt = rewriter.create(loc, th.getIndexType(), multSizeOfTypeValue); - rewriter.create(loc, offset0, offset1, multSizeOfTypeValueAdapt); + auto multSizeOfTypeValue = + rewriter.create(loc, llvmIndexType, ValueRange{sizeOfTypeValue, newCountAsIndexType}); - auto allocated = ch.MemoryRealloc(currentPtr, multSizeOfTypeValue); + auto multSizeOfTypeValueAdapt = + rewriter.create(loc, th.getIndexType(), multSizeOfTypeValue); + rewriter.create(loc, offset0, offset1, multSizeOfTypeValueAdapt); - rewriter.create(loc, allocated, currentPtrPtr); - rewriter.create(loc, newCountAsIndexType, countAsIndexTypePtr); - // capacity = length until growth by doubling (spec section 6) uses it - rewriter.create(loc, newCountAsIndexType, layout.capacityAddress(arrayType, header)); + layout.zeroElements(arrayType, currentPtr, newCountAsIndexType, incSize); + rewriter.create(loc, newCountAsIndexType, countAsIndexTypePtr); + return element; + }); rewriter.replaceOp(shiftOp, ValueRange{loadedElement}); return success(); @@ -3570,18 +3531,19 @@ struct ArraySpliceOpLowering : public TsLlvmPattern auto sizeOfTypeValueMLIR = rewriter.create(loc, indexType, elementType); auto sizeOfTypeValue = rewriter.create(loc, llvmIndexType, sizeOfTypeValueMLIR); - auto multSizeOfTypeValue = - rewriter.create(loc, llvmIndexType, ValueRange{sizeOfTypeValue, newCountAsLLVMType}); - - auto increaseArrayFunc = [&](OpBuilder &builder, Location location) -> mlir::Value { - auto allocated = ch.MemoryRealloc(currentPtr, multSizeOfTypeValue); + // The block grows by doubling when the array gets longer, and is kept when it gets shorter + // (spec section 6). Growth comes first - it may move the block - and is a no-op while the + // capacity suffices; then the tail after the deleted range moves to just after the + // inserted one, and the slots a shorter array leaves at its end, [newCount, count), are + // zeroed (none when it does not get shorter). + auto allocated = layout.ensureCapacity(arrayType, header, newCountAsLLVMType); + { auto moveCountAsLLVMType = rewriter.create(loc, llvmIndexType, ValueRange{countAsIndexType, startIndexAsLLVMType}); moveCountAsLLVMType = rewriter.create(loc, llvmIndexType, ValueRange{moveCountAsLLVMType, decSizeAsLLVMType}); - // realloc auto offsetStart = rewriter.create(loc, ptrType, llvmElementType, allocated, ValueRange{startIndexAsLLVMType}); auto offsetFrom = rewriter.create(loc, ptrType, llvmElementType, offsetStart, ValueRange{decSizeAsLLVMType}); auto offsetTo = rewriter.create(loc, ptrType, llvmElementType, offsetStart, ValueRange{incSizeAsLLVMType}); @@ -3589,31 +3551,12 @@ struct ArraySpliceOpLowering : public TsLlvmPattern auto moveBytesAdapt = rewriter.create(loc, indexType, moveBytes); rewriter.create(loc, offsetTo, offsetFrom, moveBytesAdapt); - return allocated; - }; - - auto decreaseArrayFunc = [&](OpBuilder &builder, Location location) -> mlir::Value { - - auto moveCountAsLLVMType = - rewriter.create(loc, llvmIndexType, ValueRange{countAsIndexType, startIndexAsLLVMType}); - moveCountAsLLVMType = - rewriter.create(loc, llvmIndexType, ValueRange{moveCountAsLLVMType, decSizeAsLLVMType}); - - // realloc - auto offsetStart = rewriter.create(loc, ptrType, llvmElementType, currentPtr, ValueRange{startIndexAsLLVMType}); - auto offsetFrom = rewriter.create(loc, ptrType, llvmElementType, offsetStart, ValueRange{decSizeAsLLVMType}); - auto offsetTo = rewriter.create(loc, ptrType, llvmElementType, offsetStart, ValueRange{incSizeAsLLVMType}); - - auto moveBytes = rewriter.create(loc, llvmIndexType, ValueRange{sizeOfTypeValue, moveCountAsLLVMType}); - auto moveBytesAdapt = rewriter.create(loc, indexType, moveBytes); - rewriter.create(loc, offsetTo, offsetFrom, moveBytesAdapt); - - auto allocated = ch.MemoryRealloc(currentPtr, multSizeOfTypeValue); - return allocated; - }; - - auto cond = rewriter.create(loc, LLVM::ICmpPredicate::ugt, incSizeAsLLVMType, decSizeAsLLVMType); - auto allocated = clh.conditionalExpressionLowering(loc, ptrType, cond, increaseArrayFunc, decreaseArrayFunc); + auto shrinks = rewriter.create(loc, LLVM::ICmpPredicate::ugt, decSizeAsLLVMType, incSizeAsLLVMType); + auto vacated = rewriter.create( + loc, shrinks, rewriter.create(loc, llvmIndexType, ValueRange{decSizeAsLLVMType, incSizeAsLLVMType}), + clh.createIndexConstantOf(llvmIndexType, 0)); + layout.zeroElements(arrayType, allocated, newCountAsLLVMType, vacated); + } mlir::Value indexAsLLVMType = startIndexAsLLVMType; auto next = false; @@ -3650,10 +3593,7 @@ struct ArraySpliceOpLowering : public TsLlvmPattern next = true; } - rewriter.create(loc, allocated, currentPtrPtr); rewriter.create(loc, newCountAsLLVMType, countAsIndexTypePtr); - // capacity = length until growth by doubling (spec section 6) uses it - rewriter.create(loc, newCountAsLLVMType, layout.capacityAddress(arrayType, header)); rewriter.replaceOp(spliceOp, ValueRange{newCountAsLLVMType}); return success(); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index aec53f6a1..7dd3124bc 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -484,6 +484,7 @@ tslang_add_test(NAME test-compile-00-nullable-to-any COMMAND test-runner "${PROJ tslang_add_test(NAME test-compile-00-ternary-new-or-null COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00ternary_new_or_null.ts") tslang_add_test(NAME test-compile-00-class COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class.ts") tslang_add_test(NAME test-compile-00-class-new COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_new.ts") +tslang_add_test(NAME test-compile-00-array-growth COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_growth.ts") tslang_add_test(NAME test-compile-00-global-from-global COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_from_global.ts") tslang_add_test(NAME test-compile-00-try-cleanup-then-catch COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_cleanup_then_catch.ts") tslang_add_test(NAME test-compile-00-global-union-from-call COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_union_from_call.ts") @@ -983,6 +984,7 @@ tslang_add_test(NAME test-jit-00-nullable-to-any COMMAND test-runner -jit "${PRO tslang_add_test(NAME test-jit-00-ternary-new-or-null COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00ternary_new_or_null.ts") tslang_add_test(NAME test-jit-00-class COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class.ts") tslang_add_test(NAME test-jit-00-class-new COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_new.ts") +tslang_add_test(NAME test-jit-00-array-growth COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_growth.ts") tslang_add_test(NAME test-jit-00-global-from-global COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_from_global.ts") tslang_add_test(NAME test-jit-00-try-cleanup-then-catch COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_cleanup_then_catch.ts") tslang_add_test(NAME test-jit-00-global-union-from-call COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_union_from_call.ts") @@ -1723,6 +1725,7 @@ set(TSLANG_CORPUS 00class_local_decl.ts 00class_nested.ts 00class_new.ts + 00array_growth.ts 00class_or_interface_to_tuple.ts 00class_protected_constr.ts 00class_recursive.ts diff --git a/tslang/test/tester/tests/00array_growth.ts b/tslang/test/tester/tests/00array_growth.ts new file mode 100644 index 000000000..fe87faf90 --- /dev/null +++ b/tslang/test/tester/tests/00array_growth.ts @@ -0,0 +1,28 @@ +// Arrays grow by doubling; what pop and shift vacate reads as zero when length grows again. +class Box { constructor(public v: number) {} } + +function fill(into: number[], n: number) { for (let i = 0; i < n; i++) into.push(i); } + +function main() { + const a: number[] = []; + const alias = a; + fill(a, 1000); + assert(alias.length == 1000 && alias[0] == 0 && alias[999] == 999, "1000 pushes through an alias"); + + a.pop(); + a.length = 1000; + assert(a[999] == 0, "a popped slot reads zero when length grows again"); + + const s: number[] = [1, 2, 3]; + s.shift(); + s.unshift(9); + s.push(4); + assert(s.length == 4 && s[0] == 9 && s[1] == 2 && s[3] == 4, "shift, unshift and push around a growth"); + + const boxes: Box[] = []; + for (let i = 0; i < 100; i++) boxes.push(new Box(i)); + for (let i = 0; i < 50; i++) boxes.pop(); + assert(boxes.length == 50 && boxes[49].v == 49, "class elements across growth and pops"); + + print("done."); +} From c7ff7fcad2c4249be2a58f513c40383786cae8aa Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 4 Oct 2026 11:16:03 +0100 Subject: [PATCH 19/19] Arrays: a shrinking length= releases, view() clamps, main refuses any array parameter Final whole-branch review fixes for the arrays-as-references work (#453). - SetLengthOf: a smaller `length =` releases the elements it drops, [new length, old length), before zeroing their slots, as splice does (emitReleaseArrayElements: nothing under gc and none). 200k rounds of ten pushed strings and `length = 0` peaked at 65.9 MB under rc and own; now 4.4 MB, the same as the loop with `splice(0, 10)`. - `main` with any array-typed parameter is refused in non-DLL builds, not only a second one: `main(args: string[])` compiled and faulted ahead of time. New error test main_args_string_array.ts for jit, exe and obj. - ArrayView clamps the offset to [0, length] and the count to [0, length - offset] at run time: `view(1, 40)` of three elements copied 40 and, under rc, retained the garbage past the end. - LengthOf's fallback no longer extracts ARRAY_SIZE_INDEX from a struct no type lowers to; anything but an array is an error naming the type. - ArrayLayout: ensureCapacity's comment says ops on a static header are broken (#479); make() states its contract (literal null or fresh allocation) and asserts against static data. - Comments: splice's release ordering is described against ensureCapacity. - Unused locals removed (push, SetLengthOf, the ConstArray cast). - Tests: 00array_growth pins PR 3 (empty pop/shift, unshift across growth, splice shrink/grow, shrink then regrow reads zero, length = 0 drops strings and class elements); 00array_rest_copy adds the index-0 rest and a push onto an empty rest; 00array_view adds clamped views; 00array_reference checks an empty array is not null. - Spec and plan: status, the byValue=false null path (T4-A), the ABI note (mixed objects likely link and fail at run time), #481, the argv error tests, empty pop/shift and the shrink release; plan R3 is an indexed copy. Co-Authored-By: Claude Opus 5.5 --- .../plans/2026-10-03-array-reference.md | 2 +- .../2026-10-03-array-reference-design.md | 27 ++++-- .../TypeScript/LowerToLLVM/ArrayLayout.h | 20 ++++- .../TypeScript/LowerToLLVM/CastLogicHelper.h | 3 - tslang/lib/TypeScript/LowerToAffineLoops.cpp | 4 +- tslang/lib/TypeScript/LowerToLLVM.cpp | 85 ++++++++++++------- tslang/test/tester/CMakeLists.txt | 28 +++--- .../lowering-errors/main_args_string_array.ts | 1 + tslang/test/tester/tests/00array_growth.ts | 44 ++++++++++ tslang/test/tester/tests/00array_reference.ts | 1 + tslang/test/tester/tests/00array_rest_copy.ts | 10 +++ tslang/test/tester/tests/00array_view.ts | 14 +++ 12 files changed, 181 insertions(+), 58 deletions(-) create mode 100644 tslang/test/tester/lowering-errors/main_args_string_array.ts diff --git a/tslang/docs/superpowers/plans/2026-10-03-array-reference.md b/tslang/docs/superpowers/plans/2026-10-03-array-reference.md index 98c06e45f..dcf2ed7e5 100644 --- a/tslang/docs/superpowers/plans/2026-10-03-array-reference.md +++ b/tslang/docs/superpowers/plans/2026-10-03-array-reference.md @@ -26,7 +26,7 @@ - **R1, a null header is an empty array.** Today zeroed memory is a valid empty array: elements of `new Array(n)`, class fields of array type with no initializer, and `undefined` cast to an array (`CastLogicHelper::castToArrayType`, `isUndef`). So: reading `data`/`length` of a null header gives null/0; an op that changes an array through its slot (`push`, `pop`, `shift`, `unshift`, `splice`, `length =`) first stores a fresh empty header into a slot that holds null; rc/own release of null does nothing (`emitIfLastReference` already skips null). Cost if wrong: one compare and branch per array read. - **R2 (replaced 2026-10-04), `main`'s argv is `Ref` only.** `main`'s argv is `Ref` only (C's `char **`); a `string[]` argv is a compile error that names the `Ref` form (owner's ruling 2026-10-04; replaces plan ruling R2). Spec section 5 has the same text. The original Task 5 (a `ts.ArrayFromCStrings` op) is dropped. -- **R3, the rest copy is built in MLIRGen.** Spec §3 says `ts.ArrayView` becomes a copy. A copy must own its elements under rc/own; MLIRGen's existing "fresh array + synthesized loop" idiom (`MLIRGenCast.cpp`, numeric array widening) gets that from the passes for free. So MLIRGen stops emitting `ts.ArrayView` for a rest element and builds `const .rest: T[] = []; for (let .i = index; .i < .src.length; .i++) .rest.push(.src[.i]);` instead. `ArrayViewOpLowering` stays for other users (none known; see Task 3 step 1). +- **R3, the rest copy is built in MLIRGen.** Spec §3 says `ts.ArrayView` becomes a copy. A copy must own its elements under rc/own; MLIRGen's existing "fresh array + synthesized loop" idiom (`MLIRGenCast.cpp`, numeric array widening) gets that from the passes for free. So MLIRGen stops emitting `ts.ArrayView` for a rest element and builds a copy instead. (As implemented, ruling T3-A: not a loop of pushes but an indexed element copy into a new array of the slice's length - `.rest = new array(.rest_n); for (let .rest_i = 0; .rest_i < .rest_n; ++.rest_i) .rest[.rest_i] = .rest_src[.rest_i + index];`, with `.rest_n = max(0, .rest_src.length - index)`; see `MLIRGenVariables.cpp`.) `ArrayViewOpLowering` stays for other users (none known; see Task 3 step 1). ## Review Focus diff --git a/tslang/docs/superpowers/specs/2026-10-03-array-reference-design.md b/tslang/docs/superpowers/specs/2026-10-03-array-reference-design.md index bfad8eaac..ceadbedd8 100644 --- a/tslang/docs/superpowers/specs/2026-10-03-array-reference-design.md +++ b/tslang/docs/superpowers/specs/2026-10-03-array-reference-design.md @@ -1,7 +1,7 @@ # Arrays as references -Date: 2026-10-03. Status: design approved in conversation (sections 1-4); written spec awaiting -review. Fixes #453 (a push onto an array parameter is lost to the caller) and #477 (a +Date: 2026-10-03. Status: design approved; implemented in #478 (the layout helper), #480 (the +switch) and PR 3 (growth). Fixes #453 (a push onto an array parameter is lost to the caller) and #477 (a destructuring rest element aliases its source). Plans go in `docs/superpowers/plans/`. ## 1. Purpose @@ -72,17 +72,19 @@ struct fails. Under rc a parameter push exits with 127 and prints nothing. owner does not see the change and, under rc and own, that header leaks. Accepted: such a slot holds `undefined` in TypeScript terms, where `.push` would throw (ruling T4-D). - `ConstArray` (a literal's static data) is unchanged. The cast from `ConstArray` to `T[]` makes a - header and copies the data into a fresh block on both of its paths; the path that today points - the struct at the static data (`byValue = false`) copies as well, since `push` would otherwise - reallocate static memory. + header and copies the data into a fresh block, since `push` would otherwise reallocate static + memory. The other path (`byValue = false`) turned out to be reached only by `null` as an array, + and returns a null header, which reads as empty (controller ruling T4-A). - `[a, ...rest] = src` gives `rest` a new array holding a copy of the slice (#477). MLIRGen builds it as a new array of the slice's length and a loop that copies each element (ruling T3-A; the plan had a loop of pushes) instead of emitting `ts.ArrayView`, so the ownership passes see a fresh array (plan ruling R3). - Layout: an array field, element, tuple slot or union payload shrinks from two words to one pointer; `T[] | undefined` is the pointer and its tag. -- ABI: every function that takes or returns an array changes. Objects built before the switch do - not link with objects built after it. The default library has no native code that reads an +- ABI: every function that takes or returns an array changes. Symbol names carry no types, so an + object built before the switch most likely still links with one built after it, and then fails + at run time (one side reads a `{ data, length }` struct where the other passes a header + pointer). Every object and the default library must be rebuilt. The default library has no native code that reads an array (its C++ wrappers take none), so its sources are unchanged; it is rebuilt with the switch, for every model and both build types. @@ -108,7 +110,7 @@ unchanged. formatters), and such a string takes a reference to the block; under rc the block was freed under it (five default-library tests failed ahead of time). Counting the header's reference restores what main does. A reallocating change (`push`, and PR 3's growth) still moves the - block under such a string, as it does on main. + block under such a string, as it does on main (#481). - Slots `[length, capacity)` are never released (and, from PR 3, hold zero). - `pop` and `shift` hand the removed element's reference to the caller, as today. - **own.** The header is a single-owned block, like a class instance. @@ -145,6 +147,11 @@ unchanged. `max(4, 2 * capacity, needed)`; otherwise they write into the existing block. - `pop`, `shift` and a smaller `length =` keep the block. They zero each slot they vacate, so gc does not keep a removed element alive and a later growth reads zero. +- `pop` and `shift` on an empty array return a zeroed element (0, null, an empty string) and the + length stays 0. +- A smaller `length =` releases the elements it drops, `[new length, old length)`, before it + zeroes their slots, as `splice` releases the elements it removes (nothing under gc and none). + Without it, under rc and own, every element dropped that way leaked. - `length = n` larger than `length` zeroes `[length, n)` in every model, gc included: after a `pop` the slots past `length` are no longer fresh memory. Today's behaviour (new slots read as zero; the default library's `Set`, `Map` and `Array.map` rely on it, see the comment in @@ -169,7 +176,9 @@ unchanged. - `00array_reference.ts`: shapes 1-14 of §2 as asserts; compile and JIT, rc and none corpora. - An own test with the shapes own accepts, including a push through a borrowed parameter. - `00array_rest_copy.ts` (#477). - - A `main(argc, argv: string[])` test for AOT and JIT. + - `main`'s argv error tests (`test/tester/lowering-errors/main_argv_string_array.ts` and + `main_args_string_array.ts`): an array-typed parameter of `main` is a compile error under the + JIT, `--emit=exe` and `--emit=obj`, and not in a DLL (§5). - Gates: the full Windows Release suite; the full Linux (WSL) suite, which is required because the Windows heap hides double frees; the own corpus with no file lost; own's memory measurement flat; the default library rebuilt and its suite run under every model. diff --git a/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h b/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h index 635165318..8ea46756f 100644 --- a/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h +++ b/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h @@ -104,8 +104,13 @@ class ArrayLayout : public LLVMCodeHelperBase // Every slot in [length, capacity) reads zero: growth zeroes the new slots here, and pop, // shift, splice and a smaller `length =` zero each slot they vacate. The block goes through // MemoryRealloc, which keeps an rc data block's count across the move and births one grown - // from null with the header's reference. A static header (makeStatic) has capacity = length - // and is never written in place by growth. + // from null with the header's reference. + // + // A static header (makeStatic) is broken under the ops that change an array in place (#479): + // it lives in a constant global with capacity = length over static data, and pop, shift, + // splice and `length =` store the new length into that constant global and zero the + // vacated slots in the static data - undefined behaviour, which today reads back silently + // wrong values. Nothing here guards against it. mlir::Value ensureCapacity(mlir_ts::ArrayType arrayType, mlir::Value header, mlir::Value needed) { TypeHelper th(rewriter); @@ -192,10 +197,19 @@ class ArrayLayout : public LLVMCodeHelperBase } // a new array over `data` (a block just allocated, or null for an empty array) with `length` - // elements + // elements. + // + // The contract: `data` is either a literal null (an LLVM::ZeroOp) or a fresh non-null heap + // allocation (MemoryAlloc) that nothing else holds. Whether there is a block to count is + // decided by how `data` is spelled, not by its value at run time, so a pointer that is null + // only at run time would have its count stored through null, and a pointer into static data + // (an AddressOfOp: makeStatic's job) or into another array's block would have a count stored + // over memory that is not a block header. mlir::Value make(mlir_ts::ArrayType arrayType, mlir::Value data, mlir::Value length) { auto loc = op->getLoc(); + assert(data.getDefiningOp() && !data.getDefiningOp() && + "ArrayLayout::make takes a literal null or a fresh allocation"); if (compileOptions.isRefCounted() && !data.getDefiningOp()) { // under rc the header holds a counted reference to its data block, as every copy of diff --git a/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h b/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h index 8b7dbc461..7fa678c56 100644 --- a/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h +++ b/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h @@ -1093,13 +1093,10 @@ class CastLogicHelper return mlir::Value(); } - auto ptrType = th.getPtrType(); auto arrayTypeTs = mlir::cast(arrayType); ArrayLayout layout(op, rewriter, tch.typeConverter, compileOptions); auto llvmIndexType = tch.convertType(th.getIndexType()); auto sizeValue = clh.createIndexConstantOf(llvmIndexType, size); - auto destArrayElement = mlir::cast(arrayType).getElementType(); - auto llvmDestArrayElement = tch.convertType(destArrayElement); if (isUndef) { diff --git a/tslang/lib/TypeScript/LowerToAffineLoops.cpp b/tslang/lib/TypeScript/LowerToAffineLoops.cpp index 8676343f5..d591ac238 100644 --- a/tslang/lib/TypeScript/LowerToAffineLoops.cpp +++ b/tslang/lib/TypeScript/LowerToAffineLoops.cpp @@ -2714,10 +2714,12 @@ void TypeScriptToAffineLoweringTSFuncPass::runOnFunction() // // A `string[]` argv is refused in every build where `main` is the entry point (all but a DLL): // ahead of time, the C runtime passes `char **` too, which would be read as an array header. + // Any array-typed parameter is refused, not only a second one: `main(args: string[])` takes + // `argc` as an array header and faulted ahead of time. if (!tsContext.compileOptions.isDLL && function.getName() == MAIN_ENTRY_NAME) { auto mainInputs = function.getFunctionType().getInputs(); - if (mainInputs.size() == 2 && isa(mainInputs[1])) + if (llvm::any_of(mainInputs, [](mlir::Type type) { return isa(type); })) { function.emitError("'main' takes argv as Ref (C's char **), not string[]; read an argument with Deref(argv[i])"); return signalPassFailure(); diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index 1fc7810f1..f14a74bf1 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -632,8 +632,6 @@ class LengthOfOpLowering : public TsLlvmPattern LogicalResult matchAndRewrite(mlir_ts::LengthOfOp op, Adaptor transformed, ConversionPatternRewriter &rewriter) const final { - TypeHelper th(rewriter); - if (auto arrayType = dyn_cast(op.getOp().getType())) { ArrayLayout layout(op, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); @@ -641,10 +639,12 @@ class LengthOfOpLowering : public TsLlvmPattern return success(); } - rewriter.replaceOpWithNewOp(op, typeConverter->convertType(th.getIndexType()), transformed.getOp(), - MLIRHelper::getStructIndex(rewriter, ARRAY_SIZE_INDEX)); - - return success(); + // MLIRGen emits LengthOf only on an array (a ConstArray's length is a constant, a string's + // is StringLength). The op's ArrayLike operand also admits ConstArray, String, + // ConstArrayValueRef and Ref, none of which lowers to a { data, length } struct any more, + // so there is no length to extract from one: refuse it rather than read a struct field + // that is not there. + return op.emitError("length of a value of type ") << op.getOp().getType() << " is not supported"; } }; @@ -656,19 +656,16 @@ class SetLengthOfOpLowering : public TsLlvmPattern LogicalResult matchAndRewrite(mlir_ts::SetLengthOfOp op, Adaptor transformed, ConversionPatternRewriter &rewriter) const final { - LLVMCodeHelper ch(op, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); CodeLogicHelper clh(op, rewriter); TypeConverterHelper tch(getTypeConverter()); TypeHelper th(rewriter); auto loc = op.getLoc(); - auto ptrType = th.getPtrType(); auto arrayType = cast(cast(op.getOp().getType()).getElementType()); auto elementType = arrayType.getElementType(); - auto llvmElementType = tch.convertType(elementType); - auto llvmIndexType = tch.convertType(th.getIndexType()); + auto llvmIndexType = tch.convertType(th.getIndexType()); LLVM_DEBUG(llvm::dbgs() << "arrayType: elementType: " << elementType << "\n";); LLVM_DEBUG(llvm::dbgs() << "arrayType: llvm: " << tch.convertType(arrayType) << "\n";); @@ -697,13 +694,25 @@ class SetLengthOfOpLowering : public TsLlvmPattern // the exact length, so after a `pop` or a shrink the slots past `length` are not fresh // memory from the allocator but slots the array used before. // - // Shrinking: the slots given up are zeroed, so gc does not keep what they held alive and - // a later growth reads zero. + // Shrinking: the elements in the slots given up, [new length, old length), are released + // first - under rc and own each slot held a reference, and zeroing it dropped that + // reference on the floor: 200k rounds of ten pushed strings and `length = 0` peaked at + // 65.9 MB against 4.4 for the same loop with `splice(0, 10)`, which releases the same way + // (emitReleaseArrayElements emits nothing under gc and none). The slots are then zeroed, + // so gc does not keep what they held alive and a later growth reads zero. Growing + // releases nothing (the count is 0), and on this branch `allocated` is the block the + // elements are in: a shorter length never grows it. auto grows = rewriter.create(loc, LLVM::ICmpPredicate::ugt, newCountAsLLVMType, countAsLLVMType); auto low = rewriter.create(loc, grows, countAsLLVMType, newCountAsLLVMType); auto high = rewriter.create(loc, grows, newCountAsLLVMType, countAsLLVMType); - layout.zeroElements(arrayType, allocated, low, - rewriter.create(loc, llvmIndexType, ValueRange{high, low})); + auto changed = rewriter.create(loc, llvmIndexType, ValueRange{high, low}); + { + auto released = rewriter.create(loc, grows, clh.createIndexConstantOf(llvmIndexType, 0), changed); + OwnershipRoutineLogic orl(op, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); + orl.emitReleaseArrayElements(elementType, allocated, low, released); + } + + layout.zeroElements(arrayType, allocated, low, changed); rewriter.create(loc, newCountAsLLVMType, countAsIndexTypePtr); @@ -3143,14 +3152,12 @@ struct ArrayPushOpLowering : public TsLlvmPattern LogicalResult matchAndRewrite(mlir_ts::ArrayPushOp pushOp, Adaptor transformed, ConversionPatternRewriter &rewriter) const final { - LLVMCodeHelper ch(pushOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); CodeLogicHelper clh(pushOp, rewriter); TypeConverterHelper tch(getTypeConverter()); TypeHelper th(rewriter); auto loc = pushOp.getLoc(); - auto ptrType = th.getPtrType(); auto arrayType = cast(cast(pushOp.getOp().getType()).getElementType()); auto elementType = arrayType.getElementType(); @@ -3492,14 +3499,14 @@ struct ArraySpliceOpLowering : public TsLlvmPattern // Give back what the removed elements were holding, before anything moves or frees them. // - // `splice` memmoves the tail over the deleted range and reallocs; the references those - // slots held are simply overwritten, so under `-mm=rc` every element it removes leaked. - // Measured on a loop that splices two of three boxed strings away: 16.4 MB against 4.1 - // for the same program without the splice. See §9.74. + // `splice` moves the tail over the deleted range (and may grow the block first); the + // references those slots held are simply overwritten, so under `-mm=rc` every element it + // removes leaked. Measured on a loop that splices two of three boxed strings away: 16.4 MB + // against 4.1 for the same program without the splice. See §9.74. // - // This runs on `currentPtr` and before `conditionalExpressionLowering` below, which is - // the only correct place: the growing branch reallocs *first*, and a realloc may move the - // block, so releasing afterwards would read the deleted elements through a stale pointer. + // This runs on `currentPtr`, before `layout.ensureCapacity` below, which is the only + // correct place: growth reallocates the block, and a realloc may move it, so releasing + // afterwards would read the deleted elements through a stale pointer. // // The delete count is first clamped to what is actually in the array, which JavaScript's // `splice` also does ("if greater than the number of elements after start, then all of @@ -3622,28 +3629,46 @@ struct ArrayViewOpLowering : public TsLlvmPattern auto llvmIndexType = tch.convertType(th.getIndexType()); - // TODO: add size check !!! - ArrayLayout layout(arrayViewOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); auto arrayPtr = layout.data(arrayType, transformed.getOp()); - - auto arrayOffset = ch.GetAddressOfPointerOffset(elementType, arrayPtr, transformed.getOffset()); + auto length = layout.length(arrayType, transformed.getOp()); + + // The slice is clamped to the array: the offset to [0, length] and the count to + // [0, length - offset]. `view(from, to)` arrives as offset `from` and count + // `to - from + 1`, unchecked, so `[a, b, c].view(1, 40)` copied 40 elements out of a + // block of 3 - and under rc retained each of the 37 garbage words as a reference. Both + // are read as signed: a negative offset is 0, and a negative count (`to < from`) is + // nothing. + auto zero = clh.createIndexConstantOf(llvmIndexType, 0); + mlir::Value offset = transformed.getOffset(); + auto offsetIsNegative = rewriter.create(loc, LLVM::ICmpPredicate::slt, offset, zero); + auto offsetFits = rewriter.create(loc, LLVM::ICmpPredicate::ule, offset, length); + offset = rewriter.create(loc, offsetIsNegative, zero, + rewriter.create(loc, offsetFits, offset, length)); + auto available = rewriter.create(loc, llvmIndexType, ValueRange{length, offset}); + mlir::Value count = transformed.getCount(); + auto countIsNegative = rewriter.create(loc, LLVM::ICmpPredicate::slt, count, zero); + auto countFits = rewriter.create(loc, LLVM::ICmpPredicate::ule, count, available); + count = rewriter.create(loc, countIsNegative, zero, + rewriter.create(loc, countFits, count, available)); + + auto arrayOffset = ch.GetAddressOfPointerOffset(elementType, arrayPtr, offset); // a copy of the slice: an array owns its data block, and a header over the middle of // another array's block would free (and realloc) an interior pointer auto sizeOfTypeValueMLIR = rewriter.create(loc, th.getIndexType(), elementType); auto sizeOfTypeValue = rewriter.create(loc, llvmIndexType, sizeOfTypeValueMLIR); - auto bytes = rewriter.create(loc, llvmIndexType, ValueRange{sizeOfTypeValue, transformed.getCount()}); + auto bytes = rewriter.create(loc, llvmIndexType, ValueRange{sizeOfTypeValue, count}); auto copy = ch.MemoryAlloc(bytes); rewriter.create(loc, copy, arrayOffset, bytes, /*isVolatile=*/false); // the copy holds a second reference to each element, and its release gives them back { OwnershipRoutineLogic orl(arrayViewOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); - orl.emitRetainArrayElements(elementType, copy, transformed.getCount()); + orl.emitRetainArrayElements(elementType, copy, count); } - auto structValue3 = layout.make(arrayType, copy, transformed.getCount()); + auto structValue3 = layout.make(arrayType, copy, count); rewriter.replaceOp(arrayViewOp, ValueRange{structValue3}); return success(); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 7dd3124bc..e60b0def0 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2561,20 +2561,26 @@ set_tests_properties(test-compile-lowering-error-exit PROPERTIES WILL_FAIL TRUE) # `main`'s argv is `Ref` (C's `char **`) only. A `string[]` argv would read that `char **` # as an array header, so it is a compile error that says to use `Ref`, under the JIT (where # `main` is called, so a program that wrongly compiled would run instead of matching) and ahead of -# time. A DLL has no `main` entry point, so the same file must not draw the error there. +# time. A DLL has no `main` entry point, so the same file must not draw the error there. Any +# array-typed parameter is refused, so `main(args: string[])` - one parameter, which takes argc's +# place and compiled to a program that faulted - is checked too (main-args-string-array). set(main_argv_file "${PROJECT_SOURCE_DIR}/test/tester/lowering-errors/main_argv_string_array.ts") -add_test(NAME test-jit-main-argv-string-array-error - COMMAND $ --emit=jit --no-default-lib "${main_argv_file}") -set_tests_properties(test-jit-main-argv-string-array-error - PROPERTIES PASS_REGULAR_EXPRESSION "takes argv as Ref" - FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") -foreach(argv_emit exe obj) - add_test(NAME test-compile-main-argv-string-array-error-${argv_emit} - COMMAND $ --emit=${argv_emit} --no-default-lib "${main_argv_file}" - -o "${CMAKE_CURRENT_BINARY_DIR}/main-argv-string-array.${argv_emit}") - set_tests_properties(test-compile-main-argv-string-array-error-${argv_emit} +foreach(argv_case main-argv-string-array main-args-string-array) + string(REPLACE "-" "_" argv_case_file "${argv_case}") + set(argv_case_path "${PROJECT_SOURCE_DIR}/test/tester/lowering-errors/${argv_case_file}.ts") + add_test(NAME test-jit-${argv_case}-error + COMMAND $ --emit=jit --no-default-lib "${argv_case_path}") + set_tests_properties(test-jit-${argv_case}-error PROPERTIES PASS_REGULAR_EXPRESSION "takes argv as Ref" FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") + foreach(argv_emit exe obj) + add_test(NAME test-compile-${argv_case}-error-${argv_emit} + COMMAND $ --emit=${argv_emit} --no-default-lib "${argv_case_path}" + -o "${CMAKE_CURRENT_BINARY_DIR}/${argv_case}.${argv_emit}") + set_tests_properties(test-compile-${argv_case}-error-${argv_emit} + PROPERTIES PASS_REGULAR_EXPRESSION "takes argv as Ref" + FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") + endforeach() endforeach() # The DLL is linked, so it is given what it links from the build tree rather than the environment: # -mm=none needs only the runtime library (gc would need the shared collector too), and the check diff --git a/tslang/test/tester/lowering-errors/main_args_string_array.ts b/tslang/test/tester/lowering-errors/main_args_string_array.ts new file mode 100644 index 000000000..a810df422 --- /dev/null +++ b/tslang/test/tester/lowering-errors/main_args_string_array.ts @@ -0,0 +1 @@ +function main(args: string[]) { print(args.length); print("done."); } diff --git a/tslang/test/tester/tests/00array_growth.ts b/tslang/test/tester/tests/00array_growth.ts index fe87faf90..414545e5e 100644 --- a/tslang/test/tester/tests/00array_growth.ts +++ b/tslang/test/tester/tests/00array_growth.ts @@ -24,5 +24,49 @@ function main() { for (let i = 0; i < 50; i++) boxes.pop(); assert(boxes.length == 50 && boxes[49].v == 49, "class elements across growth and pops"); + // pop and shift on an empty array return a zeroed element and leave the length at 0 + const e: number[] = []; + const p = e.pop(); + const sh = e.shift(); + assert(e.length == 0 && p == 0 && sh == 0, "pop and shift on an empty array"); + e.push(1); + assert(e.length == 1 && e[0] == 1, "a push after pop and shift on an empty array"); + + const u: number[] = []; + for (let i = 0; i < 20; i++) u.unshift(i); + assert(u.length == 20 && u[0] == 19 && u[19] == 0, "20 unshifts across growth"); + + const sp: number[] = [1, 2, 3, 4, 5, 6, 7, 8]; + sp.splice(2, 4); + assert(sp.length == 4 && sp[0] == 1 && sp[2] == 7 && sp[3] == 8, "a splice that shrinks"); + sp.length = 8; + assert(sp[4] == 0 && sp[7] == 0, "the slots a splice vacated read zero when length grows again"); + sp.splice(1, 0, 10, 11, 12, 13, 14, 15, 16, 17, 18); + assert(sp.length == 17 && sp[1] == 10 && sp[9] == 18 && sp[10] == 2, "a splice that inserts 9 and grows"); + + // a smaller `length =` gives back the elements it drops (under rc and own a release each), + // and what it vacated reads zero when the length grows again + const shrink: number[] = [1, 2, 3, 4, 5, 6]; + shrink.length = 2; + shrink.length = 6; + assert(shrink.length == 6 && shrink[1] == 2 && shrink[2] == 0 && shrink[5] == 0, "a shrink then a regrow reads zero"); + + const texts: string[] = []; + for (let r = 0; r < 50; r++) { + for (let i = 0; i < 10; i++) texts.push("text-" + r + "-" + i); + texts.length = 0; + } + for (let i = 0; i < 10; i++) texts.push("again-" + i); + assert(texts.length == 10 && texts[0] == "again-0" && texts[9] == "again-9", "strings dropped by length = 0"); + + const kept: Box[] = []; + for (let i = 0; i < 8; i++) kept.push(new Box(200 + i)); + kept.length = 3; + const churn: Box[] = []; + for (let i = 0; i < 64; i++) churn.push(new Box(900 + i)); + assert(kept.length == 3 && kept[0].v == 200 && kept[2].v == 202, "class elements kept by a shrink stay alive"); + kept.length = 8; + assert(kept[3] == null && kept[7] == null, "class slots a shrink vacated read null"); + print("done."); } diff --git a/tslang/test/tester/tests/00array_reference.ts b/tslang/test/tester/tests/00array_reference.ts index ec1e2fc20..43b5f20e1 100644 --- a/tslang/test/tester/tests/00array_reference.ts +++ b/tslang/test/tester/tests/00array_reference.ts @@ -29,6 +29,7 @@ function main() { const a15: number[] = [1]; const x15: any = a15; const y15 = x15 as number[]; y15.push(2); assert(a15.length == 2, "15 through any"); const c16: number[] = []; const d16: number[] = []; assert(!(c16 === d16), "16 two empty arrays are different arrays"); + assert(c16 !== null, "16 an empty array is not null"); let truthy = false; if (c16) { truthy = true; } assert(truthy, "16 an empty array is truthy"); let n16: number[] | null = null; let falsy = true; if (n16) { falsy = false; } assert(falsy, "16 null is falsy"); diff --git a/tslang/test/tester/tests/00array_rest_copy.ts b/tslang/test/tester/tests/00array_rest_copy.ts index 9d9b15d9a..cba07685d 100644 --- a/tslang/test/tester/tests/00array_rest_copy.ts +++ b/tslang/test/tester/tests/00array_rest_copy.ts @@ -31,5 +31,15 @@ function main() { q1.push(9); assert(p0 == 1 && q0 == 2 && q1.length == 2 && q1[1] == 9 && p1.length == 2 && p1[1] == 3, "nested and repeated rest"); + // a rest from index 0 copies the whole source, and a push onto an empty rest grows it + const letters: string[] = ["a", "b"]; + const [...all] = letters; + all.push("c"); + assert(all.length == 3 && letters.length == 2 && all[0] == "a", "a rest from index 0"); + const [l0, l1, l2, ...noLetters] = letters; + noLetters.push("q"); + noLetters.push("r"); + assert(noLetters.length == 2 && noLetters[1] == "r", "a push onto an empty rest"); + print("done."); } diff --git a/tslang/test/tester/tests/00array_view.ts b/tslang/test/tester/tests/00array_view.ts index 751f8c9c5..f3e680926 100644 --- a/tslang/test/tester/tests/00array_view.ts +++ b/tslang/test/tester/tests/00array_view.ts @@ -44,4 +44,18 @@ function viewsReleased() { viewsReleased(); +// a view past the end is clamped to the array: `view(1, 40)` of three elements has two, and +// under rc nothing past the end is retained +class Boxed { constructor(public v: number) {} } + +function viewsClamped() { + const boxes: Boxed[] = [new Boxed(1), new Boxed(2), new Boxed(3)]; + const w = boxes.view(1, 40); + assert(w.length == 2 && w[0].v == 2 && w[1].v == 3, "a view past the end is clamped"); + const n = [1, 2, 3, 4]; + assert(n.view(4, 9).length == 0, "a view starting at the end is empty"); + assert(n.view(3, 1).length == 0, "a view that ends before it starts is empty");} + +viewsClamped(); + print("done."); \ No newline at end of file