diff --git a/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h b/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h index a3549f38b..a1e6ce4bc 100644 --- a/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h +++ b/tslang/include/TypeScript/LowerToLLVM/ArrayLayout.h @@ -6,6 +6,7 @@ #include "TypeScript/MLIRLogic/MLIRHelper.h" #include "TypeScript/LowerToLLVM/LLVMCodeHelperBase.h" #include "TypeScript/LowerToLLVM/CodeLogicHelper.h" +#include "TypeScript/LowerToLLVM/AssertLogic.h" #include "mlir/Dialect/LLVMIR/LLVMDialect.h" @@ -106,6 +107,9 @@ class ArrayLayout : public LLVMCodeHelperBase // MemoryRealloc, which keeps an rc data block's count across the move and births one grown // from null with the header's reference. // + // A `needed` past 2^32 - 1, a byte count that overflows, or a failed allocation stops the + // program before anything is stored or zeroed (#483). + // // A static header (makeStatic) must never reach the ops that change an array in place: it lives // in a constant global with capacity = length over static data, and pop, shift, splice and // `length =` would store into that global and zero slots of the static data. MLIRGen copies @@ -139,11 +143,14 @@ class ArrayLayout : public LLVMCodeHelperBase rewriter.create(loc, LLVM::ICmpPredicate::ult, newCapacity, constant(4)); newCapacity = rewriter.create(loc, belowMinimum, constant(4), newCapacity); + checkLength(needed); + auto newBytes = bytesFor(arrayType, newCapacity); + auto grown = MemoryRealloc(currentData, newBytes); + checkAllocated(grown, newBytes); + 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}); @@ -159,6 +166,56 @@ class ArrayLayout : public LLVMCodeHelperBase [&](OpBuilder &, Location) -> mlir::Value { return currentData; }); } + // A length is at most 2^32 - 1, as in TypeScript (#483): past it - `length = 2^42`, or a + // negative integer, which is huge once sign-extended to an index - the program stops as a + // failing assert does. With a 32-bit index every value is in range, and the byte count and + // allocation checks below catch what does not fit. + void checkLength(mlir::Value length) + { + TypeHelper th(rewriter); + auto loc = op->getLoc(); + auto llvmIndexType = typeConverter->convertType(th.getIndexType()); + if (llvmIndexType.getIntOrFloatBitWidth() <= 32) + { + return; + } + + auto maxLength = rewriter.create(loc, llvmIndexType, + rewriter.getIntegerAttr(llvmIndexType, 0xFFFFFFFFll)); + auto valid = rewriter.create(loc, LLVM::ICmpPredicate::ule, length, maxLength); + AssertLogic(op, rewriter, typeConverter, loc, compileOptions).check(valid, "Invalid array length"); + } + + // the bytes `count` elements take; the program stops when they do not fit in an index (#483) + mlir::Value bytesFor(mlir_ts::ArrayType arrayType, 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 allOnes = rewriter.create(loc, llvmIndexType, rewriter.getIntegerAttr(llvmIndexType, -1)); + auto maxCount = rewriter.create(loc, llvmIndexType, ValueRange{allOnes, sizeOfElement}); + auto fits = rewriter.create(loc, LLVM::ICmpPredicate::ule, count, maxCount); + AssertLogic(op, rewriter, typeConverter, loc, compileOptions).check(fits, "Invalid array length"); + return rewriter.create(loc, llvmIndexType, ValueRange{sizeOfElement, count}); + } + + // the program stops when an allocation of `bytes` (more than none) gave no block (#483) + void checkAllocated(mlir::Value block, mlir::Value bytes) + { + TypeHelper th(rewriter); + auto loc = op->getLoc(); + auto llvmIndexType = typeConverter->convertType(th.getIndexType()); + auto got = rewriter.create(loc, LLVM::ICmpPredicate::ne, block, + rewriter.create(loc, th.getPtrType())); + auto none = rewriter.create( + loc, LLVM::ICmpPredicate::eq, bytes, + rewriter.create(loc, llvmIndexType, rewriter.getIntegerAttr(llvmIndexType, 0))); + auto allocated = rewriter.create(loc, got, none); + AssertLogic(op, rewriter, typeConverter, loc, compileOptions).check(allocated, "Out of memory"); + } + // 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) diff --git a/tslang/include/TypeScript/LowerToLLVM/AssertLogic.h b/tslang/include/TypeScript/LowerToLLVM/AssertLogic.h index 237b74810..b646e2fde 100644 --- a/tslang/include/TypeScript/LowerToLLVM/AssertLogic.h +++ b/tslang/include/TypeScript/LowerToLLVM/AssertLogic.h @@ -8,7 +8,7 @@ #include "TypeScript/TypeScriptOps.h" #include "TypeScript/LowerToLLVM/CodeLogicHelper.h" -#include "TypeScript/LowerToLLVM/LLVMCodeHelper.h" +#include "TypeScript/LowerToLLVM/LLVMCodeHelperBase.h" #include "TypeScript/LowerToLLVM/TypeConverterHelper.h" #include "TypeScript/LowerToLLVM/TypeHelper.h" #include "TypeScript/LowerToLLVM/LocationHelper.h" @@ -28,7 +28,7 @@ class AssertLogic Operation *op; PatternRewriter &rewriter; TypeHelper th; - LLVMCodeHelper ch; + LLVMCodeHelperBase ch; CodeLogicHelper clh; Location loc; @@ -44,14 +44,29 @@ class AssertLogic typeOfValueType = th.getPtrType(); } + AssertLogic(Operation *op, PatternRewriter &rewriter, const TypeConverter *typeConverter, Location loc, CompileOptions &compileOptions) + : op(op), rewriter(rewriter), th(rewriter), ch(op, rewriter, typeConverter, compileOptions), clh(op, rewriter), loc(loc) + { + sizeType = th.getIndexType(); + typeOfValueType = th.getPtrType(); + } + // `message`, when given, is the text known only at run time and is shown instead of `msg` mlir::LogicalResult logic(mlir::Value condValue, std::string msg, mlir::Value message = mlir::Value()) { -#ifdef WIN32 - return logicWin32(condValue, msg, message); -#else - return logicUnix(condValue, msg, message); -#endif + // the test replaces the assert op, which the split left at the top of the continuation + failUnless(condValue, msg, message); + rewriter.eraseOp(op); + return success(); + } + + // A check in the middle of another lowering (#483): execution goes on at the insertion point + // when `condValue` holds, and stops with `msg` and the operation's file and line, as a failing + // assert does, when it does not. + void check(mlir::Value condValue, std::string msg) + { + auto *continuationBlock = failUnless(condValue, msg, mlir::Value()); + rewriter.setInsertionPointToStart(continuationBlock); } // `_assert` and `__assert_fail` abort, which flushes no stream: what the program printed before @@ -76,84 +91,33 @@ class AssertLogic return rewriter.create(loc, isNull, msgCst, message); } - mlir::LogicalResult logicWin32(mlir::Value condValue, std::string msg, mlir::Value message) + private: + // Splits the block at the insertion point and ends the first half with a branch on + // `condValue`: on to the continuation, which is returned, or to a new block that reports the + // failure (`_assert` on Windows, `__assert_fail` elsewhere) and never returns. + mlir::Block *failUnless(mlir::Value condValue, std::string msg, mlir::Value message) { auto unreachable = clh.FindUnreachableBlockOrCreate(); auto [fileName, lineAndColumn] = LLVMLocationHelper::getLineAndColumnAndFileName(loc); auto [line, column] = lineAndColumn; - // Insert the `_assert` declaration if necessary. auto i8PtrTy = th.getPtrType(); +#ifdef WIN32 auto assertFuncOp = ch.getOrInsertFunction("_assert", th.getFunctionType(th.getVoidType(), {i8PtrTy, i8PtrTy, rewriter.getI32Type()})); - - // Split block at `assert` operation. - auto *opBlock = rewriter.getInsertionBlock(); - auto opPosition = rewriter.getInsertionPoint(); - auto *continuationBlock = rewriter.splitBlock(opBlock, opPosition); - - // Generate IR to call `assert`. - auto *failureBlock = rewriter.createBlock(opBlock->getParent()); - - std::stringstream msgWithNUL; - msgWithNUL << msg; - - auto opHash = std::hash{}(msgWithNUL.str()); - - std::stringstream msgVarName; - msgVarName << "m_" << opHash; - - std::stringstream fileVarName; - fileVarName << "f_" << hash_value(fileName); - - std::stringstream fileWithNUL; - fileWithNUL << fileName.str(); - - auto msgCst = ch.getOrCreateGlobalString(msgVarName.str(), msgWithNUL.str()); - - auto fileCst = ch.getOrCreateGlobalString(fileVarName.str(), fileName.str()); - - // auto nullCst = rewriter.create(loc, getI8PtrType(context)); - - mlir::Value lineNumberRes = rewriter.create(loc, rewriter.getI32Type(), rewriter.getI32IntegerAttr(line)); - - flushOutput(); - rewriter.create(loc, assertFuncOp, ValueRange{messageOrConstant(message, msgCst), fileCst, lineNumberRes}); - // rewriter.create(loc); - rewriter.create(loc, unreachable); - - // Generate assertion test. - rewriter.setInsertionPointToEnd(opBlock); - rewriter.replaceOpWithNewOp(op, condValue, continuationBlock, failureBlock); - - return success(); - } - - mlir::LogicalResult logicUnix(mlir::Value condValue, std::string msg, mlir::Value message) - { - auto unreachable = clh.FindUnreachableBlockOrCreate(); - - auto [fileName, lineAndColumn] = LLVMLocationHelper::getLineAndColumnAndFileName(loc); - auto [line, column] = lineAndColumn; - - // Insert the `_assert` declaration if necessary. - auto i8PtrTy = th.getPtrType(); +#else auto assertFuncOp = ch.getOrInsertFunction( "__assert_fail", th.getFunctionType(th.getVoidType(), {i8PtrTy, i8PtrTy, rewriter.getI32Type(), i8PtrTy})); +#endif - // Split block at `assert` operation. auto *opBlock = rewriter.getInsertionBlock(); auto opPosition = rewriter.getInsertionPoint(); auto *continuationBlock = rewriter.splitBlock(opBlock, opPosition); - // Generate IR to call `assert`. auto *failureBlock = rewriter.createBlock(opBlock->getParent()); - std::stringstream msgWithNUL; - msgWithNUL << msg; - - auto opHash = std::hash{}(msgWithNUL.str()); + auto opHash = std::hash{}(msg); std::stringstream msgVarName; msgVarName << "m_" << opHash; @@ -161,28 +125,24 @@ class AssertLogic std::stringstream fileVarName; fileVarName << "f_" << hash_value(fileName); - std::stringstream fileWithNUL; - fileWithNUL << fileName.str(); - - auto msgCst = ch.getOrCreateGlobalString(msgVarName.str(), msgWithNUL.str()); - + auto msgCst = ch.getOrCreateGlobalString(msgVarName.str(), msg); auto fileCst = ch.getOrCreateGlobalString(fileVarName.str(), fileName.str()); - // auto nullCst = rewriter.create(loc, getI8PtrType(context)); - mlir::Value lineNumberRes = rewriter.create(loc, rewriter.getI32Type(), rewriter.getI32IntegerAttr(line)); - mlir::Value funcName = rewriter.create(loc, i8PtrTy); flushOutput(); +#ifdef WIN32 + rewriter.create(loc, assertFuncOp, ValueRange{messageOrConstant(message, msgCst), fileCst, lineNumberRes}); +#else + mlir::Value funcName = rewriter.create(loc, i8PtrTy); rewriter.create(loc, assertFuncOp, ValueRange{messageOrConstant(message, msgCst), fileCst, lineNumberRes, funcName}); - // rewriter.create(loc); +#endif rewriter.create(loc, unreachable); - // Generate assertion test. rewriter.setInsertionPointToEnd(opBlock); - rewriter.replaceOpWithNewOp(op, condValue, continuationBlock, failureBlock); + rewriter.create(loc, condValue, continuationBlock, failureBlock); - return success(); + return continuationBlock; } }; } // namespace typescript diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index f14a74bf1..ee545257f 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -3124,20 +3124,20 @@ struct NewArrayOpLowering : public TsLlvmPattern auto llvmIndexType = tch.convertType(th.getIndexType()); auto llvmElementType = tch.convertType(elementType); - auto sizeOfTypeValueMLIR = rewriter.create(loc, th.getIndexType(), elementType); - auto sizeOfTypeValue = rewriter.create(loc, llvmIndexType, sizeOfTypeValueMLIR); - auto countAsIndexTypeMLIR = rewriter.create(loc, th.getIndexType(), transformed.getCount()); auto countAsIndexType = rewriter.create(loc, llvmIndexType, countAsIndexTypeMLIR); - auto multSizeOfTypeValue = - rewriter.create(loc, llvmIndexType, ValueRange{sizeOfTypeValue, countAsIndexType}); + // a length past 2^32 - 1 (a negative one included), a size that overflows, or a failed + // allocation stops the program (#483) + ArrayLayout layout(newArrOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); + layout.checkLength(countAsIndexType); + auto multSizeOfTypeValue = layout.bytesFor(arrayType, countAsIndexType); // zeroed: its elements are not set yet, and under `-mm=rc` the first store into one // releases what the slot held - `malloc`'s leftovers would be released as a reference auto allocated = ch.MemoryAlloc(multSizeOfTypeValue, MemoryAllocSet::Zero); + layout.checkAllocated(allocated, multSizeOfTypeValue); - ArrayLayout layout(newArrOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); auto structValue3 = layout.make(arrayType, allocated, transformed.getCount()); rewriter.replaceOp(newArrOp, ValueRange{structValue3}); diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index a242b28f0..4ac35df3d 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -5947,6 +5947,37 @@ class MLIRGenImpl return mlir::failure(); } + // A length is an integer in [0, 2^32 - 1] (TypeScript throws a RangeError). A number is + // checked here, before its conversion to an index drops a fraction and makes a negative + // or a NaN a huge size; an integer is checked against the upper bound where it is + // lowered (SetLengthOfOpLowering), which a negative one exceeds once sign-extended. A + // failed check stops the program as a failing assert does (#483). + if (isa(savingValue.getType())) + { + auto numberConstant = [&](double value) -> mlir::Value { +#ifdef NUMBER_F64 + return builder.create(location, getNumberType(), builder.getF64FloatAttr(value)); +#else + return builder.create(location, getNumberType(), builder.getF32FloatAttr(value)); +#endif + }; + + auto checkLength = [&](SyntaxKind opCode, mlir::Value left, mlir::Value right) { + auto holds = builder.create( + location, getBooleanType(), builder.getI32IntegerAttr((int)opCode), left, right); + builder.create(location, holds, mlir::Value(), + builder.getStringAttr("Invalid array length")); + }; + + checkLength(SyntaxKind::GreaterThanEqualsToken, savingValue, numberConstant(0)); + checkLength(SyntaxKind::LessThanEqualsToken, savingValue, numberConstant(4294967295.0)); + + // an integer survives the round trip through an index + CAST_A(asIndex, location, builder.getIndexType(), savingValue, genContext); + CAST_A(backToNumber, location, getNumberType(), asIndex, genContext); + checkLength(SyntaxKind::EqualsEqualsToken, backToNumber, savingValue); + } + // special case to resize array syncSavingValue(lengthOf.getResult().getType()); builder.create(location, arrayValueLoaded, savingValue); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 4b82368fa..91e1c62fa 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -348,6 +348,7 @@ tslang_add_test(NAME test-compile-00-array-rest-copy COMMAND test-runner "${PROJ 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-literal-copy COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_literal_copy.ts") +tslang_add_test(NAME test-compile-00-array-length-valid COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_length_valid.ts") tslang_add_test(NAME test-compile-00-string-view-copy COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_view_copy.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") @@ -851,6 +852,7 @@ tslang_add_test(NAME test-jit-00-array-rest-copy COMMAND test-runner -jit "${PRO 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-literal-copy COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_literal_copy.ts") +tslang_add_test(NAME test-jit-00-array-length-valid COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_length_valid.ts") tslang_add_test(NAME test-jit-00-string-view-copy COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_view_copy.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") @@ -1683,6 +1685,7 @@ set(TSLANG_CORPUS 00array6.ts 00array_static_nested.ts 00array_literal_copy.ts + 00array_length_valid.ts 00string_view_copy.ts 00array_data_string_alias.ts 00array7.ts @@ -3116,6 +3119,20 @@ set_tests_properties(test-jit-assert-message-failing PROPERTIES PASS_REGULAR_EXPRESSION "printed before.*assertion failed: n is 7" FAIL_REGULAR_EXPRESSION "Stack dump|unlinked") +# An invalid array length stops the program with a message, after the output printed before it, +# under every model (#483). The JIT reports it itself on every platform, as for a failing assert. +foreach(array_length_name negative huge fraction negative_integer new_negative) + foreach(array_length_model gc rc none own) + add_test(NAME test-jit-${array_length_model}-array-length-${array_length_name} + COMMAND $ --emit=jit --no-default-lib -mm=${array_length_model} + --shared-libs=$ + "${PROJECT_SOURCE_DIR}/test/tester/array-length/${array_length_name}.ts") + set_tests_properties(test-jit-${array_length_model}-array-length-${array_length_name} + PROPERTIES PASS_REGULAR_EXPRESSION "printed before.*assertion failed: Invalid array length" + FAIL_REGULAR_EXPRESSION "Stack dump|PLEASE submit|length: ") + endforeach() +endforeach() + # --export=all exports the module's variables, not the locals of its functions. add_test(NAME test-compile-export-all-no-locals COMMAND $ --emit=mlir --no-default-lib --export=all diff --git a/tslang/test/tester/array-length/fraction.ts b/tslang/test/tester/array-length/fraction.ts new file mode 100644 index 000000000..929de827e --- /dev/null +++ b/tslang/test/tester/array-length/fraction.ts @@ -0,0 +1,8 @@ +// `length =` with a number that is not an integer stops the program (#483) +function main() { + print("printed before"); + const a: number[] = [1, 2, 3]; + let n = 2.5; + a.length = n; + print("length: ", a.length); +} diff --git a/tslang/test/tester/array-length/huge.ts b/tslang/test/tester/array-length/huge.ts new file mode 100644 index 000000000..34b119f28 --- /dev/null +++ b/tslang/test/tester/array-length/huge.ts @@ -0,0 +1,7 @@ +// `length =` past 2^32 - 1 stops the program (#483) +function main() { + print("printed before"); + const a: number[] = [1, 2, 3]; + a.length = 4294967296 * 1024; + print("length: ", a.length); +} diff --git a/tslang/test/tester/array-length/negative.ts b/tslang/test/tester/array-length/negative.ts new file mode 100644 index 000000000..8edacb4e4 --- /dev/null +++ b/tslang/test/tester/array-length/negative.ts @@ -0,0 +1,11 @@ +// `length =` with a negative number stops the program (#483) +function shrink(a: number[], n: number) { + a.length = n; +} + +function main() { + print("printed before"); + const a: number[] = [1, 2, 3]; + shrink(a, -1); + print("length: ", a.length); +} diff --git a/tslang/test/tester/array-length/negative_integer.ts b/tslang/test/tester/array-length/negative_integer.ts new file mode 100644 index 000000000..781f58cff --- /dev/null +++ b/tslang/test/tester/array-length/negative_integer.ts @@ -0,0 +1,8 @@ +// `length =` with a negative integer stops the program (#483) +function main() { + print("printed before"); + const a: number[] = [1, 2, 3]; + let n = -1; + a.length = n; + print("length: ", a.length); +} diff --git a/tslang/test/tester/array-length/new_negative.ts b/tslang/test/tester/array-length/new_negative.ts new file mode 100644 index 000000000..b882c6672 --- /dev/null +++ b/tslang/test/tester/array-length/new_negative.ts @@ -0,0 +1,9 @@ +// a new array of a negative length stops the program (#483) +type Numbers = number[]; + +function main() { + print("printed before"); + let n = -1; + const a = new Numbers(n); + print("length: ", a.length); +} diff --git a/tslang/test/tester/tests/00array_length_valid.ts b/tslang/test/tester/tests/00array_length_valid.ts new file mode 100644 index 000000000..fcd73e548 --- /dev/null +++ b/tslang/test/tester/tests/00array_length_valid.ts @@ -0,0 +1,19 @@ +// Array lengths that are valid keep working with the checks of #483 in place. +type Numbers = number[]; + +function main() { + const a: number[] = [1, 2, 3]; + let n: number = 5; + a.length = n; + assert(a.length == 5 && a[4] == 0, "a number length"); + a.length = 1; + assert(a.length == 1 && a[0] == 1, "shrink"); + for (let i = 0; i < 100; i++) a.push(i); + assert(a.length == 101 && a[100] == 99, "growth"); + let k = 3; + const b = new Numbers(k); + assert(b.length == 3 && b[2] == 0, "a new array of a length"); + a.length = 0; + assert(a.length == 0, "empty"); + print("done."); +}