From 7cc90387f63c4eb508c1c53c16185e2e269a9771 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 28 Sep 2026 10:49:18 +0100 Subject: [PATCH] push/unshift/splice take spread arguments; push takes more than one item `a.push(1, 2)` failed to verify: ArrayPush checked its items with a RangedTypesMatchWith against a one-type range, which only a single item matches. It now has a verifier that checks every item against the element type. A spread argument (`a.push(...xs)`, `a.unshift(...xs)`, `a.splice(start, count, ...xs)`) has a length known only at run time, and the builtins take one operand per item. Every item argument, spread or not, now goes into one array literal (which spreads at run time), and its elements go in one at a time through the same builtin without a spread, so the casts and retains stay the builtin's. splice deletes first, then inserts at the start JavaScript normalizes to (negative from the end, clamped to the array). A spread in splice's start or delete count position is an error. Co-Authored-By: Claude Opus 5.5 --- tslang/include/TypeScript/TypeScriptOps.td | 10 +- tslang/lib/TypeScript/MLIRGenAccessCall.cpp | 192 +++++++++++++++++- tslang/lib/TypeScript/MLIRGenExpressions.cpp | 5 + tslang/lib/TypeScript/MLIRGenImpl.h | 5 +- tslang/lib/TypeScript/TypeScriptOps.cpp | 18 ++ tslang/test/tester/CMakeLists.txt | 3 + .../test/tester/tests/00array_spread_args.ts | 63 ++++++ 7 files changed, 289 insertions(+), 7 deletions(-) create mode 100644 tslang/test/tester/tests/00array_spread_args.ts diff --git a/tslang/include/TypeScript/TypeScriptOps.td b/tslang/include/TypeScript/TypeScriptOps.td index 1133f6c04..8400332d6 100644 --- a/tslang/include/TypeScript/TypeScriptOps.td +++ b/tslang/include/TypeScript/TypeScriptOps.td @@ -2302,13 +2302,13 @@ def TypeScript_BodyResultInternalOp : TypeScript_YieldOpBase<"BodyResultInternal let summary = "termination operation"; } -def TypeScript_ArrayPushOp : TypeScript_Op<"ArrayPush", [ - RangedTypesMatchWith<"array element type match all items types", - "op", "items", - "TypeRange((cast(cast($_self).getElementType()).getElementType()))"> -]> { +def TypeScript_ArrayPushOp : TypeScript_Op<"ArrayPush"> { let arguments = (ins Arg:$op, Variadic:$items); let results = (outs Index:$new_size); + + // every item is of the element type (a RangedTypesMatchWith against a one-type range + // accepted only a single item) + let hasVerifier = 1; } def TypeScript_ArrayPopOp : TypeScript_Op<"ArrayPop", [ diff --git a/tslang/lib/TypeScript/MLIRGenAccessCall.cpp b/tslang/lib/TypeScript/MLIRGenAccessCall.cpp index e6c9f9ac1..61126340a 100644 --- a/tslang/lib/TypeScript/MLIRGenAccessCall.cpp +++ b/tslang/lib/TypeScript/MLIRGenAccessCall.cpp @@ -1365,10 +1365,200 @@ namespace mlirgen functionName, typeArgs, operands, - [this](mlir::Location location, mlir::Type type, mlir::Value value, const GenContext &genContext, bool disableStrictNullCheck) { return cast(location, type, value, genContext, disableStrictNullCheck); }, + [this](mlir::Location location, mlir::Type type, mlir::Value value, const GenContext &genContext, bool disableStrictNullCheck) { return cast(location, type, value, genContext, disableStrictNullCheck); }, genContext); } + // `a.push(...xs)`, `a.unshift(...xs)`, `a.splice(start, count, ...xs)`. The builtins take one + // operand per item, and a spread, whose length is known only at run time, has none to give: + // it reached ArrayPush as the array itself and failed to verify. Every item argument, spread + // or not, goes into one array literal instead (a literal spreads at run time), and its elements + // go in one at a time through the same builtin with no spread, so the casts and the retains + // stay the builtin's. + std::optional MLIRGenImpl::mlirGenArrayInsertWithSpread( + mlir::Location location, mlir::Value funcResult, NodeArray arguments, const GenContext &genContext) + { + auto thisSymbolRefOp = funcResult.getDefiningOp(); + if (!thisSymbolRefOp || !mth.isBuiltinFunctionType(funcResult) + || llvm::none_of(arguments, [](auto argument) { return argument == SyntaxKind::SpreadElement; })) + { + return std::nullopt; + } + + auto calleeName = thisSymbolRefOp->getAttrOfType(StringRef(IDENTIFIER_ATTR_NAME)); + auto functionName = calleeName ? calleeName.getValue() : StringRef(); + auto isPush = functionName == "__array_push"; + auto isSplice = functionName == "__array_splice"; + if (!isPush && !isSplice && functionName != "__array_unshift") + { + return std::nullopt; + } + + auto thisValue = thisSymbolRefOp.getThisVal(); + auto arrayType = dyn_cast(thisValue.getType()); + if (!arrayType) + { + return std::nullopt; + } + + auto generate = [&]() -> ValueOrLogicalResult { + MLIRCodeLogic mcl(builder, compileOptions); + auto arrayRef = mcl.GetReferenceFromValue(location, thisValue); + if (!arrayRef) + { + emitError(location) << "Can't get reference of the array, ensure const array is not used"; + return mlir::failure(); + } + + // splice's start and delete count are positions, not items, and come first + auto itemsFrom = isSplice ? 2 : 0; + SmallVector positions; + for (auto index = 0; index < itemsFrom; index++) + { + if (index >= (int)arguments.size() || arguments[index] == SyntaxKind::SpreadElement) + { + emitError(location) << "splice with spread items needs its start and delete count as arguments of their own"; + return mlir::failure(); + } + + auto result = mlirGen(arguments[index], genContext); + EXIT_IF_FAILED_OR_NO_VALUE(result) + auto value = V(result); + CAST(value, location, getNumberType(), value, genContext); + positions.push_back(value); + } + + NodeFactory nf(NodeFactoryFlags::None); + + NodeArray itemArguments; + for (auto index = itemsFrom; index < (int)arguments.size(); index++) + { + itemArguments.push_back(arguments[index]); + } + + GenContext itemsGenContext(genContext); + itemsGenContext.clearReceiverTypes(); + itemsGenContext.receiverType = arrayType; + auto itemsResult = mlirGen(nf.createArrayLiteralExpression(itemArguments), itemsGenContext); + EXIT_IF_FAILED_OR_NO_VALUE(itemsResult) + auto items = V(itemsResult); + if (items.getType() != arrayType) + { + CAST(items, location, arrayType, items, genContext); + } + + if (isPush) + { + if (mlir::failed(mlirGenAppendArrayByEachElement(location, arrayRef, items, genContext))) + { + return mlir::failure(); + } + } + else + { + SymbolTableScopeT varScope(symbolTable); + + auto dstArrayVarDecl = std::make_shared(".dst_array", arrayType, location); + dstArrayVarDecl->setReadWriteAccess(true); + DECLARE(dstArrayVarDecl, arrayRef); + + auto itemsVarDecl = std::make_shared(".items", arrayType, location); + DECLARE(itemsVarDecl, items); + + auto _dst_array = nf.createIdentifier(S(".dst_array")); + auto _items = nf.createIdentifier(S(".items")); + auto _at = nf.createIdentifier(S(".at")); + auto _i = nf.createIdentifier(S(".i")); + auto _length = [&](Identifier array) { + return nf.createPropertyAccessExpression(array, nf.createIdentifier(S(LENGTH_FIELD_NAME))); + }; + auto _splice = [&](Expression start, Expression count, Expression item) { + NodeArray spliceArguments; + spliceArguments.push_back(start); + spliceArguments.push_back(count); + if (item) + { + spliceArguments.push_back(item); + } + + return nf.createExpressionStatement(nf.createCallExpression( + nf.createPropertyAccessExpression(_dst_array, nf.createIdentifier(S("splice"))), undefined, spliceArguments)); + }; + auto _assign = [&](Expression value) { + return nf.createExpressionStatement(nf.createBinaryExpression(_at, nf.createToken(SyntaxKind::EqualsToken), value)); + }; + auto _zero = [&]() { return nf.createNumericLiteral(S("0")); }; + + // let .at = 0 (unshift), or splice's start where JavaScript puts it: a negative one + // counts from the end and stops at 0, one past the end is the end + NodeArray atDeclarations; + if (isSplice) + { + auto startVarDecl = std::make_shared(".start", getNumberType(), location); + DECLARE(startVarDecl, positions[0]); + auto countVarDecl = std::make_shared(".count", getNumberType(), location); + DECLARE(countVarDecl, positions[1]); + + auto _start = nf.createIdentifier(S(".start")); + atDeclarations.push_back(nf.createVariableDeclaration(_at, undefined, undefined, _start)); + if (mlir::failed(mlirGen(nf.createVariableStatement(undefined, nf.createVariableDeclarationList(atDeclarations, NodeFlags::Let)), genContext))) + { + return mlir::failure(); + } + + // if (.at < 0) { .at = .at + .dst_array.length; if (.at < 0) .at = 0; } + // else if (.at > .dst_array.length) .at = .dst_array.length; + NodeArray fromEnd; + fromEnd.push_back(_assign(nf.createBinaryExpression(_at, nf.createToken(SyntaxKind::PlusToken), _length(_dst_array)))); + fromEnd.push_back(nf.createIfStatement( + nf.createBinaryExpression(_at, nf.createToken(SyntaxKind::LessThanToken), _zero()), _assign(_zero()))); + auto normalize = nf.createIfStatement( + nf.createBinaryExpression(_at, nf.createToken(SyntaxKind::LessThanToken), _zero()), + nf.createBlock(fromEnd), + nf.createIfStatement( + nf.createBinaryExpression(_at, nf.createToken(SyntaxKind::GreaterThanToken), _length(_dst_array)), + _assign(_length(_dst_array)))); + if (mlir::failed(mlirGen(normalize, genContext))) + { + return mlir::failure(); + } + + // the delete runs first; everything before .at stays where it is + if (mlir::failed(mlirGen(_splice(_start, nf.createIdentifier(S(".count")), undefined), genContext))) + { + return mlir::failure(); + } + } + else + { + atDeclarations.push_back(nf.createVariableDeclaration(_at, undefined, undefined, _zero())); + if (mlir::failed(mlirGen(nf.createVariableStatement(undefined, nf.createVariableDeclarationList(atDeclarations, NodeFlags::Let)), genContext))) + { + return mlir::failure(); + } + } + + // for (let .i = 0; .i < .items.length; ++.i) .dst_array.splice(.at + .i, 0, .items[.i]); + 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), _length(_items)); + auto incr = nf.createPrefixUnaryExpression(nf.createToken(SyntaxKind::PlusPlusToken), _i); + auto insert = _splice( + nf.createBinaryExpression(_at, nf.createToken(SyntaxKind::PlusToken), _i), _zero(), nf.createElementAccessExpression(_items, _i)); + if (mlir::failed(mlirGen(nf.createForStatement(initVars, cond, incr, insert), genContext))) + { + return mlir::failure(); + } + } + + auto loadedArray = builder.create(location, arrayType, arrayRef); + return V(builder.create(location, builder.getIndexType(), loadedArray)); + }; + + return generate(); + } + ValueOrLogicalResult MLIRGenImpl::mlirGenCallExpression(mlir::Location location, mlir::Value funcResult, NodeArray typeArguments, SmallVector &operands, const GenContext &genContext) diff --git a/tslang/lib/TypeScript/MLIRGenExpressions.cpp b/tslang/lib/TypeScript/MLIRGenExpressions.cpp index f48afc288..052b6b73b 100644 --- a/tslang/lib/TypeScript/MLIRGenExpressions.cpp +++ b/tslang/lib/TypeScript/MLIRGenExpressions.cpp @@ -1032,6 +1032,11 @@ namespace mlirgen mth.isGenericType(funcResult.getType()) && callExpression->typeArguments.size() == 0; + if (auto spreadResult = mlirGenArrayInsertWithSpread(location, funcResult, callExpression->arguments, genContext)) + { + return *spreadResult; + } + SmallVector operands; auto offsetArgs = isa(funcType) || isa(funcType) ? 1 : 0; if (mlir::failed(mlirGenOperands(callExpression->arguments, operands, funcResult.getType(), genContext, offsetArgs, noReceiverTypesForGenericCall))) diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 034bc0acc..1c5ea278b 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -7337,9 +7337,12 @@ class MLIRGenImpl const GenContext &genContext); ValueOrLogicalResult mlirGenCallBuiltInFunction( - mlir::Location location, mlir::Value actualFuncRefValue, NodeArray typeArguments, + mlir::Location location, mlir::Value actualFuncRefValue, NodeArray typeArguments, SmallVector &operands, const GenContext &genContext); + std::optional mlirGenArrayInsertWithSpread( + mlir::Location location, mlir::Value funcResult, NodeArray arguments, const GenContext &genContext); + ValueOrLogicalResult mlirGenCallExpression(mlir::Location location, mlir::Value funcResult, NodeArray typeArguments, SmallVector &operands, const GenContext &genContext); diff --git a/tslang/lib/TypeScript/TypeScriptOps.cpp b/tslang/lib/TypeScript/TypeScriptOps.cpp index 82db55cb6..d19957ca5 100644 --- a/tslang/lib/TypeScript/TypeScriptOps.cpp +++ b/tslang/lib/TypeScript/TypeScriptOps.cpp @@ -1062,6 +1062,24 @@ LogicalResult mlir_ts::FuncOp::verify() return success(); } +//===----------------------------------------------------------------------===// +// ArrayPushOp +//===----------------------------------------------------------------------===// + +LogicalResult mlir_ts::ArrayPushOp::verify() +{ + auto elementType = mlir::cast(mlir::cast(getOp().getType()).getElementType()).getElementType(); + for (auto item : getItems()) + { + if (item.getType() != elementType) + { + return emitOpError("item of type ") << item.getType() << " does not match array element type " << elementType; + } + } + + return success(); +} + //===----------------------------------------------------------------------===// // InvokeOp //===----------------------------------------------------------------------===// diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index dabe5728a..a76d3db15 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -335,6 +335,7 @@ tslang_add_test(NAME test-compile-00-arrays4-push-pop COMMAND test-runner "${PRO 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-arrays6 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array6.ts") tslang_add_test(NAME test-compile-00-arrays7 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array7.ts") @@ -787,6 +788,7 @@ tslang_add_test(NAME test-jit-00-arrays4-push-pop COMMAND test-runner -jit "${PR 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-arrays6 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array6.ts") tslang_add_test(NAME test-jit-00-arrays7 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array7.ts") @@ -1551,6 +1553,7 @@ set(TSLANG_CORPUS 00array_of.ts 00array_shift.ts 00array_splice.ts + 00array_spread_args.ts 00array_view.ts 00const_array_push.ts 00array.ts diff --git a/tslang/test/tester/tests/00array_spread_args.ts b/tslang/test/tester/tests/00array_spread_args.ts new file mode 100644 index 000000000..48aa4e12c --- /dev/null +++ b/tslang/test/tester/tests/00array_spread_args.ts @@ -0,0 +1,63 @@ +// push/unshift/splice with spread arguments, whose count is known only at run time, and push with +// more than one item (the op's verifier accepted exactly one). +function same(a: number[], expected: number[]) { + if (a.length != expected.length) return false; + for (let i = 0; i < a.length; i++) { + if (a[i] != expected[i]) return false; + } + + return true; +} + +function spreadOf(values: number[]) { + let a: number[] = [1, 2]; + a.push(...values); + return a; +} + +function main() { + let m: number[] = []; + assert(m.push(1, 2) == 2, "push two: result"); + assert(same(m, [1, 2]), "push two"); + + const extra = [3, 4, 5]; + + let a: number[] = [1, 2]; + assert(a.push(...extra) == 5, "push spread: result"); + assert(same(a, [1, 2, 3, 4, 5]), "push spread"); + + a.push(0, ...extra, 9); + assert(same(a, [1, 2, 3, 4, 5, 0, 3, 4, 5, 9]), "push mixed"); + + assert(same(spreadOf([]), [1, 2]), "push empty spread"); + assert(same(spreadOf([7, 8]), [1, 2, 7, 8]), "push parameter spread"); + + let b: number[] = [9]; + b.unshift(...extra); + assert(same(b, [3, 4, 5, 9]), "unshift spread"); + b.unshift(7, ...[8]); + assert(same(b, [7, 8, 3, 4, 5, 9]), "unshift mixed"); + + let c: number[] = [1, 2, 3]; + c.splice(1, 1, ...extra); + assert(same(c, [1, 3, 4, 5, 3]), "splice spread"); + c.splice(-1, 1, ...[10, 11]); + assert(same(c, [1, 3, 4, 5, 10, 11]), "splice negative start"); + c.splice(100, 0, ...[12]); + assert(same(c, [1, 3, 4, 5, 10, 11, 12]), "splice start past the end"); + c.splice(-100, 2, ...[0]); + assert(same(c, [0, 4, 5, 10, 11, 12]), "splice start before the beginning"); + + const ints = [1, 2]; + let nums: number[] = []; + nums.push(...ints); + assert(same(nums, [1, 2]), "push s32 spread into number[]"); + + let ss: string[] = []; + ss.push(...["a"], "b"); + assert(ss.length == 2, "strings: length"); + assert(ss[0] == "a", "strings: first"); + assert(ss[1] == "b", "strings: second"); + + print("done."); +}