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 f489e32fe..8ea46756f 100644 --- a/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h +++ b/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h @@ -97,6 +97,86 @@ 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) 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); + 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) { @@ -117,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 067c31565..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,86 +656,65 @@ 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";); 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(); - - auto sizeOfTypeAsIndexType = rewriter.create(loc, th.getIndexType(), elementType); + mlir::Value countAsLLVMType = rewriter.create(loc, llvmIndexType, countAsIndexTypePtr); + mlir::Value newCountAsLLVMType = + rewriter.create(loc, llvmIndexType, op.getNewLength()); - 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); + // The slots between the old and the new length are zeroed, whichever way the length went. + // + // 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 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); + 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); + } - rewriter.create(loc, allocated, currentPtrPtr); + layout.zeroElements(arrayType, allocated, low, changed); - // `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). - // - // 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); 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); @@ -3173,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(); @@ -3190,8 +3167,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 +3175,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 +3213,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 +3249,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 +3300,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 +3311,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 +3358,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 +3397,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}); + // 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}); - // load last element - auto offset0 = - rewriter.create(loc, th.getPtrType(), llvmElementType, currentPtr, ValueRange{clh.createIndexConstantOf(llvmIndexType, 0)}); - auto loadedElement = rewriter.create(loc, llvmElementType, offset0); + // 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 offset1 = - rewriter.create(loc, th.getPtrType(), llvmElementType, currentPtr, ValueRange{incSize}); + auto offset1 = + rewriter.create(loc, th.getPtrType(), llvmElementType, currentPtr, ValueRange{incSize}); - auto sizeOfTypeValueMLIR = rewriter.create(loc, th.getIndexType(), storageType); - auto sizeOfTypeValue = rewriter.create(loc, llvmIndexType, sizeOfTypeValueMLIR); + auto sizeOfTypeValueMLIR = rewriter.create(loc, th.getIndexType(), storageType); + auto sizeOfTypeValue = rewriter.create(loc, llvmIndexType, sizeOfTypeValueMLIR); - auto multSizeOfTypeValue = - rewriter.create(loc, llvmIndexType, ValueRange{sizeOfTypeValue, newCountAsIndexType}); - - 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(); @@ -3531,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 @@ -3570,18 +3538,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 +3558,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 +3600,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(); @@ -3682,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 aec53f6a1..e60b0def0 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 @@ -2558,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 new file mode 100644 index 000000000..414545e5e --- /dev/null +++ b/tslang/test/tester/tests/00array_growth.ts @@ -0,0 +1,72 @@ +// 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"); + + // 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