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..98c06e45f 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`: 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` 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/docs/superpowers/specs/2026-10-03-array-reference-design.md b/tslang/docs/superpowers/specs/2026-10-03-array-reference-design.md index be95778e1..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 @@ -67,12 +67,17 @@ 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 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. @@ -92,8 +97,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. @@ -121,11 +136,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/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..f489e32fe 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.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 + // 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,162 @@ 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 + // 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(); - 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)); + 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)); + 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..34cf68bf2 100644 --- a/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelperBase.h +++ b/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelperBase.h @@ -444,10 +444,28 @@ 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 - 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, compileOptions.isRefCounted() ? 1 : 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 +483,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/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); diff --git a/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h b/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h index 43e3406e8..b52389bc4 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) @@ -957,8 +987,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 +1188,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,29 +1316,53 @@ 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 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); - auto dataSlot = layout.dataAddress(arrayType, header); - auto dataValue = rewriter.create(loc, ptrTy, dataSlot); - emitIfLastReference(dataValue, [&]() { - auto elementRoutine = getOrCreateReleaseRoutine(arrayType.getElementType()); - if (!elementRoutine.empty()) + emitIfLastReference(header, [&]() { + auto dataSlot = layout.dataAddress(arrayType, header); + auto dataValue = rewriter.create(loc, ptrTy, dataSlot); + + auto releaseData = [&]() { + 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); + }; + + if (compileOptions.isRefCounted()) + { + emitIfLastReference(dataValue, releaseData); + } + else { - 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}); - }); + releaseData(); } - emitFreeBlock(dataValue); + emitFreeBlock(header); }); } diff --git a/tslang/lib/TypeScript/LowerToAffineLoops.cpp b/tslang/lib/TypeScript/LowerToAffineLoops.cpp index 6e43eb645..8676343f5 100644 --- a/tslang/lib/TypeScript/LowerToAffineLoops.cpp +++ b/tslang/lib/TypeScript/LowerToAffineLoops.cpp @@ -2708,20 +2708,28 @@ 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. + // + // 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); }; 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 +2741,7 @@ 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'"); + function.emitError("expected 'main' to be 'main(argc?: i32 | number, argv?: Ref): void | i32 | number'"); return signalPassFailure(); } } diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index 71a48b790..067c31565 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -734,6 +734,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); @@ -1687,7 +1689,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(); } @@ -2744,8 +2749,17 @@ 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())) + { + // `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); } @@ -3064,9 +3078,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); @@ -3102,8 +3113,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); @@ -3149,9 +3158,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()); @@ -3239,6 +3245,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(); @@ -3293,6 +3301,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(); @@ -3387,6 +3397,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(); @@ -3450,6 +3462,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(); @@ -3638,6 +3652,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(); @@ -3672,8 +3688,22 @@ 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); + + // 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}); return success(); @@ -7223,16 +7253,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/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index e01c6819b..178e28c78 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -952,10 +952,10 @@ 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 - // debt as `obj.f = x`. + // 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 // literal nothing releases, and a StringType, whose characters are not references at all. @@ -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 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 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 756016a1b..aec53f6a1 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -335,14 +335,19 @@ 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") 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-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") @@ -830,14 +835,19 @@ 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") 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-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") @@ -1658,10 +1668,15 @@ 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 + 00array_data_string_alias.ts 00array7.ts 00array8_tuple_spread.ts 00arrow_generic.ts @@ -2540,6 +2555,34 @@ 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`, 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 "${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() +# 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 -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") + # -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 @@ -2608,7 +2651,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/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/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."); +} 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."); +} 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_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."); +} 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_view.ts b/tslang/test/tester/tests/00array_view.ts index 34ba505e2..751f8c9c5 100644 --- a/tslang/test/tester/tests/00array_view.ts +++ b/tslang/test/tester/tests/00array_view.ts @@ -9,4 +9,39 @@ 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[]) { + // `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; +} + +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 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..15a0dbc35 100644 --- a/tslang/tslang/jit.cpp +++ b/tslang/tslang/jit.cpp @@ -475,12 +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 -// other shape of `main`, with a source location; this still checks, for an entry picked with `-e`. +// 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`. llvm::Error addEntryThunk(llvm::Module &llvmModule, llvm::StringRef entryName) { auto *entry = llvmModule.getFunction(entryName); @@ -491,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(); @@ -516,7 +517,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 +527,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();