diff --git a/graal-js/src/com.oracle.js.parser/src/com/oracle/js/parser/Parser.java b/graal-js/src/com.oracle.js.parser/src/com/oracle/js/parser/Parser.java index f925927e12c..e7f68b3c265 100644 --- a/graal-js/src/com.oracle.js.parser/src/com/oracle/js/parser/Parser.java +++ b/graal-js/src/com.oracle.js.parser/src/com/oracle/js/parser/Parser.java @@ -1046,7 +1046,7 @@ private Expression verifyAssignment(final long op, final Expression lhs, final E throw invalidLHSError(lhs); } break; - } else if ((opType == ASSIGN || opType == ASSIGN_INIT) && isDestructuringLhs(lhs) && (inPatternPosition || !lhs.isParenthesized())) { + } else if ((opType == ASSIGN || opType == ASSIGN_INIT) && isDestructuringOrExtractorLhs(lhs) && (inPatternPosition || !lhs.isParenthesized())) { verifyDestructuringAssignmentPattern(lhs, CONTEXT_ASSIGNMENT_TARGET); break; } else if (!(isWebCompatAssignmentTargetType(lhs) && (opType == ASSIGN || opType.isAssignmentOperator()))) { @@ -1071,8 +1071,18 @@ private boolean isDestructuringLhs(Expression lhs) { return false; } + private boolean isDestructuringOrExtractorLhs(Expression lhs) { + if (isDestructuringLhs(lhs)) { + return true; + } + if (lhs instanceof CallNode) { + return env.extractors; + } + return false; + } + private void verifyDestructuringAssignmentPattern(Expression pattern, String contextString) { - assert pattern instanceof ObjectNode || pattern instanceof ArrayLiteralNode; + assert pattern instanceof ObjectNode || pattern instanceof ArrayLiteralNode || pattern instanceof CallNode; pattern.accept(new VerifyDestructuringPatternNodeVisitor(new LexicalContext()) { @Override protected void verifySpreadElement(Expression lvalue) { @@ -2479,10 +2489,14 @@ private ForVariableDeclarationListResult variableDeclarationList(TokenType varTy final int varLine = line; final long varToken = Token.recast(token, varType); - // Get name of var. - final Expression binding = bindingIdentifierOrPattern(yield, await, CONTEXT_VARIABLE_NAME); - final boolean isDestructuring = !(binding instanceof IdentNode); - if (isDestructuring) { + // Get left hand side. + final Expression binding = leftHandSideExpression(yield, await, CoverExpressionError.DENY); + if (binding.tokenType() == TokenType.NEW) { + throw error(AbstractParser.message(MSG_INVALID_LVALUE), binding.getToken()); + } + + final boolean isDestructuringOrExtracting = !(binding instanceof IdentNode); + if (isDestructuringOrExtracting) { final int finalVarFlags = varFlags | VarNode.IS_DESTRUCTURING; verifyDestructuringBindingPattern(binding, new Consumer() { @Override @@ -2510,18 +2524,18 @@ public void accept(IdentNode identNode) { next(); // Get initializer expression. Suppress IN if not statement. - if (!isDestructuring) { + if (!isDestructuringOrExtracting) { pushDefaultName(binding); } try { init = assignmentExpression(isStatement, yield, await); } finally { - if (!isDestructuring) { + if (!isDestructuringOrExtracting) { popDefaultName(); } } } else if (isStatement) { - if (isDestructuring) { + if (isDestructuringOrExtracting) { throw error(AbstractParser.message(MSG_MISSING_DESTRUCTURING_ASSIGNMENT), token); } else if (varType == CONST) { throw error(AbstractParser.message(MSG_MISSING_CONST_ASSIGNMENT, ((IdentNode) binding).getName())); @@ -2529,7 +2543,7 @@ public void accept(IdentNode identNode) { // else, if we are in a for loop, delay checking until we know the kind of loop } - if (!isDestructuring) { + if (!isDestructuringOrExtracting) { assert init != null || varType != CONST || !isStatement; final IdentNode ident = (IdentNode) binding; if (varType != VAR && ident.getName().equals(LET.getName())) { @@ -2716,6 +2730,13 @@ private Expression bindingPattern(boolean yield, boolean await) { return arrayLiteral(yield, await, CoverExpressionError.IGNORE); } else if (type == LBRACE) { return objectLiteral(yield, await, CoverExpressionError.IGNORE); + } else if (env.extractors && type == IDENT) { + final var extractor = leftHandSideExpression(yield, await, CoverExpressionError.DENY); + if (extractor instanceof CallNode) { + return extractor; + } else { + throw error(AbstractParser.message(MSG_EXPECTED_BINDING), extractor.getToken()); + } } else { throw error(AbstractParser.message(MSG_EXPECTED_BINDING)); } @@ -2743,22 +2764,7 @@ public boolean enterLiteralNode(LiteralNode literalNode) { if (((ArrayLiteralNode) literalNode).hasSpread() && ((ArrayLiteralNode) literalNode).hasTrailingComma()) { throw error("Rest element must be last", literalNode.getElementExpressions().get(literalNode.getElementExpressions().size() - 1).getToken()); } - boolean restElement = false; - for (Expression element : literalNode.getElementExpressions()) { - if (element != null) { - if (restElement) { - throw error("Unexpected element after rest element", element.getToken()); - } - if (element.isTokenType(SPREAD_ARRAY)) { - restElement = true; - Expression lvalue = ((UnaryNode) element).getExpression(); - verifySpreadElement(lvalue); - } else { - element.accept(this); - } - } - } - return false; + return handleNodeListWithSpread(literalNode.getElementExpressions(), SPREAD_ARRAY); } else { return enterDefault(literalNode); } @@ -2771,23 +2777,7 @@ public boolean enterObjectNode(ObjectNode objectNode) { if (objectNode.isParenthesized()) { throw error(AbstractParser.message(MSG_INVALID_LVALUE), objectNode.getToken()); } - boolean restElement = false; - for (PropertyNode property : objectNode.getElements()) { - if (property != null) { - if (restElement) { - throw error("Unexpected element after rest element", property.getToken()); - } - Expression key = property.getKey(); - if (key.isTokenType(SPREAD_OBJECT)) { - restElement = true; - Expression lvalue = ((UnaryNode) key).getExpression(); - verifySpreadElement(lvalue); - } else { - property.accept(this); - } - } - } - return false; + return this.handleNodeListWithSpread(objectNode.getElements(), SPREAD_OBJECT); } @Override @@ -2810,6 +2800,39 @@ public boolean enterBinaryNode(BinaryNode binaryNode) { return enterDefault(binaryNode); } } + + @Override + public boolean enterCallNode(CallNode callNode) { + if (callNode.isParenthesized() || !env.extractors) { + throw error(AbstractParser.message(MSG_INVALID_LVALUE), callNode.getToken()); + } + return handleNodeListWithSpread(callNode.getArgs(), SPREAD_ARGUMENT); + } + + private boolean handleNodeListWithSpread(List nodes, TokenType acceptableSpreadToken) { + boolean encounteredSpread = false; + + for (final var node : nodes) { + if (node == null) { + continue; + } + if (encounteredSpread) { + throw error("Unexpected element after rest element", node.getToken()); + } + + final var key = node instanceof PropertyNode ? ((PropertyNode) node).getKey() : node; + if (key.isTokenType(acceptableSpreadToken)) { + encounteredSpread = true; + + final var lvalue = ((UnaryNode) key).getExpression(); + verifySpreadElement(lvalue); + } else { + node.accept(this); + } + } + + return false; + } } /** @@ -2817,7 +2840,7 @@ public boolean enterBinaryNode(BinaryNode binaryNode) { * declarations. */ private void verifyDestructuringBindingPattern(Expression pattern, Consumer identifierCallback) { - assert pattern instanceof ObjectNode || pattern instanceof ArrayLiteralNode; + assert pattern instanceof ObjectNode || pattern instanceof ArrayLiteralNode || pattern instanceof CallNode; pattern.accept(new VerifyDestructuringPatternNodeVisitor(new LexicalContext()) { @Override protected void verifySpreadElement(Expression lvalue) { @@ -3076,8 +3099,8 @@ private void forStatement(boolean yield, boolean await) { throw error(AbstractParser.message(MSG_MANY_VARS_IN_FOR_IN_LOOP, isForOf || isForAwaitOf ? CONTEXT_OF : CONTEXT_IN), varDeclList.secondBinding.getToken()); } init = varDeclList.firstBinding; - assert init instanceof IdentNode || isDestructuringLhs(init) : init; - if (varDeclList.declarationWithInitializerToken != 0 && (isStrictMode || type != IN || varType != VAR || isDestructuringLhs(init))) { + assert init instanceof IdentNode || isDestructuringOrExtractorLhs(init) : init; + if (varDeclList.declarationWithInitializerToken != 0 && (isStrictMode || type != IN || varType != VAR || isDestructuringOrExtractorLhs(init))) { /* * ES5 legacy: for (var i = AssignmentExpressionNoIn in Expression). * Invalid in ES6, but allowed in non-strict mode if no ES6 features @@ -3154,7 +3177,7 @@ private boolean checkValidLValue(Expression init, String contextString) { return true; } else if (init instanceof AccessNode || init instanceof IndexNode) { return !((BaseNode) init).isOptional(); - } else if (isDestructuringLhs(init)) { + } else if (isDestructuringOrExtractorLhs(init)) { verifyDestructuringAssignmentPattern(init, contextString); return true; } else { @@ -3861,7 +3884,8 @@ private void tryStatement(boolean yield, boolean await) { pattern = null; ifExpression = null; } else { - if (isBindingIdentifier() || !(ES6_DESTRUCTURING && isES6())) { + // If extractors are allowed, bindingPattern may start with an identifier so we need to look ahead + if ((isBindingIdentifier() && (!env.extractors || Token.descType(getToken(k + 1)) == RPAREN)) || !(ES6_DESTRUCTURING && isES6())) { pattern = null; IdentNode catchParameter = bindingIdentifier(yield, await, CONTEXT_CATCH_PARAMETER); exception = catchParameter.setIsCatchParameter(); @@ -5087,7 +5111,7 @@ private Expression memberExpression(boolean yield, boolean await, CoverExpressio * This helps report the first error location for cases like: ({x=i}[{y=j}]). */ private void verifyPrimaryExpression(Expression lhs, CoverExpressionError coverExpression) { - if (coverExpression != CoverExpressionError.DENY && coverExpression.hasError() && isDestructuringLhs(lhs)) { + if (coverExpression != CoverExpressionError.DENY && coverExpression.hasError() && isDestructuringOrExtractorLhs(lhs)) { /** * These token types indicate that the preceding PrimaryExpression is part of an * unfinished MemberExpression or other LeftHandSideExpression, which also means that it @@ -5570,7 +5594,7 @@ private void formalParameter(final boolean yield, final boolean await) { final long paramToken = token; final int paramLine = line; IdentNode ident; - if (isBindingIdentifier() || !(ES6_DESTRUCTURING && isES6())) { + if (lookaheadIsSimpleFormalParameter() || !(ES6_DESTRUCTURING && isES6())) { ident = bindingIdentifier(yield, await, CONTEXT_FUNCTION_PARAMETER); if (type == ASSIGN && (ES6_DEFAULT_PARAMETER && isES6())) { @@ -5609,6 +5633,18 @@ private void formalParameter(final boolean yield, final boolean await) { } } + /** + * check if this is a "simple" formal parameter; an ident followed by a comma, a closing parenthesis or an assign (default) + * i.e. not a binding pattern (in particular an extractor, which also starts with an ident) + */ + private boolean lookaheadIsSimpleFormalParameter() { + if (!isBindingIdentifier()) { + return false; + } + + return lookaheadFindTokenSkippingComments(COMMARIGHT) || lookaheadFindTokenSkippingComments(RPAREN) || lookaheadFindTokenSkippingComments(ASSIGN); + } + private void functionRestParameter(final TokenType endType, final boolean yield, final boolean await) { final long paramToken = token; final int paramLine = line; @@ -5683,7 +5719,7 @@ private static void addDefaultParameter(long paramToken, int paramFinish, int pa } private void addDestructuringParameter(long paramToken, int paramFinish, int paramLine, Expression target, Expression initializer, ParserContextFunctionNode function, boolean isRest) { - assert isDestructuringLhs(target); + assert isDestructuringOrExtractorLhs(target); // desugar to: target := (param === undefined) ? initializer : param; // we use an special positional parameter node not subjected to TDZ rules; // thereby, we forego the need for a synthetic param symbol to refer to the passed value. @@ -6440,7 +6476,7 @@ private Expression assignmentExpression(boolean in, boolean yield, boolean await assert !(exprLhs instanceof ExpressionList); if (type.isAssignment()) { - if (canBeAssignmentPattern && !isDestructuringLhs(exprLhs)) { + if (canBeAssignmentPattern && !isDestructuringOrExtractorLhs(exprLhs)) { // If LHS is not an AssignmentPattern, verify that it is a valid expression. verifyExpression(coverExprLhs); } @@ -6592,7 +6628,7 @@ private void convertArrowFunctionParameterList(Expression paramList, ParserConte return; } final int functionLine = function.getLineNumber(); - if (paramListExpr instanceof IdentNode || paramListExpr.isTokenType(ASSIGN) || isDestructuringLhs(paramListExpr) || paramListExpr.isTokenType(SPREAD_ARGUMENT)) { + if (paramListExpr instanceof IdentNode || paramListExpr.isTokenType(ASSIGN) || isDestructuringOrExtractorLhs(paramListExpr) || paramListExpr.isTokenType(SPREAD_ARGUMENT)) { convertArrowParameter(paramListExpr, 0, functionLine, function); } else if (paramListExpr instanceof BinaryNode && Token.descType(paramListExpr.getToken()) == COMMARIGHT) { List params = new ArrayList<>(); @@ -6645,7 +6681,7 @@ private void convertArrowParameter(Expression param, int index, int paramLine, P addDefaultParameter(paramToken, param.getFinish(), paramLine, ident, initializer, currentFunction); return; - } else if (isDestructuringLhs(lhs)) { + } else if (isDestructuringOrExtractorLhs(lhs)) { // binding pattern with initializer verifyDestructuringParameterBindingPattern(lhs, paramToken, paramLine); @@ -6653,7 +6689,7 @@ private void convertArrowParameter(Expression param, int index, int paramLine, P } else { throw error(AbstractParser.message(MSG_INVALID_ARROW_PARAMETER), paramToken); } - } else if (isDestructuringLhs(param)) { + } else if (isDestructuringOrExtractorLhs(param)) { // binding pattern long paramToken = param.getToken(); @@ -6669,7 +6705,7 @@ private void convertArrowParameter(Expression param, int index, int paramLine, P if (restParam instanceof IdentNode) { IdentNode ident = ((IdentNode) restParam).setIsRestParameter(); convertArrowParameter(ident, index, paramLine, currentFunction); - } else if (isDestructuringLhs(restParam)) { + } else if (isDestructuringOrExtractorLhs(restParam)) { verifyDestructuringParameterBindingPattern(restParam, restParam.getToken(), paramLine); addDestructuringParameter(restParam.getToken(), restParam.getFinish(), paramLine, restParam, null, currentFunction, true); } else { @@ -6731,19 +6767,18 @@ private boolean lookbehindNoLineTerminatorAfterAsync() { } private boolean lookaheadIsArrow() { - // find ARROW, skipping over COMMENT - int i = 1; - for (;;) { - TokenType t = T(k + i++); - if (t == ARROW) { - break; - } else if (t == COMMENT) { - continue; - } else { + return lookaheadFindTokenSkippingComments(ARROW); + } + + private boolean lookaheadFindTokenSkippingComments(TokenType target) { + for (int i = 1;; i++) { + TokenType t = T(k + i); + if (t == target) { + return true; + } else if (t != COMMENT) { return false; } } - return true; } /** diff --git a/graal-js/src/com.oracle.js.parser/src/com/oracle/js/parser/ScriptEnvironment.java b/graal-js/src/com.oracle.js.parser/src/com/oracle/js/parser/ScriptEnvironment.java index 9605e821209..c95bb2e49f7 100644 --- a/graal-js/src/com.oracle.js.parser/src/com/oracle/js/parser/ScriptEnvironment.java +++ b/graal-js/src/com.oracle.js.parser/src/com/oracle/js/parser/ScriptEnvironment.java @@ -133,6 +133,9 @@ public enum FunctionStatementBehavior { /** Are source phase imports enabled. */ final boolean sourcePhaseImports; + /** Are extractors enabled. */ + final boolean extractors; + public ScriptEnvironment(boolean strict, int ecmaScriptVersion, boolean emptyStatements, @@ -149,6 +152,7 @@ public ScriptEnvironment(boolean strict, boolean privateFieldsIn, boolean topLevelAwait, boolean v8Intrinsics, + boolean extractors, FunctionStatementBehavior functionStatementBehavior) { this.constAsVar = constAsVar; this.emptyStatements = emptyStatements; @@ -167,6 +171,7 @@ public ScriptEnvironment(boolean strict, this.privateFieldsIn = privateFieldsIn; this.topLevelAwait = topLevelAwait; this.v8Intrinsics = v8Intrinsics; + this.extractors = extractors; } public boolean isStrict() { @@ -195,6 +200,7 @@ public static final class Builder { private boolean privateFieldsIn = false; private boolean topLevelAwait = false; private boolean v8Intrinsics = false; + private boolean extractors = false; private FunctionStatementBehavior functionStatementBehavior = FunctionStatementBehavior.ERROR; private Builder() { @@ -284,9 +290,14 @@ public Builder functionStatementBehavior(FunctionStatementBehavior functionState return this; } + public Builder extractors(boolean extractors) { + this.extractors = extractors; + return this; + } + public ScriptEnvironment build() { return new ScriptEnvironment(strict, ecmaScriptVersion, emptyStatements, syntaxExtensions, scripting, shebang, constAsVar, allowBigInt, annexB, - classFields, importAttributes, importAssertions, sourcePhaseImports, privateFieldsIn, topLevelAwait, v8Intrinsics, functionStatementBehavior); + classFields, importAttributes, importAssertions, sourcePhaseImports, privateFieldsIn, topLevelAwait, v8Intrinsics, extractors, functionStatementBehavior); } } } diff --git a/graal-js/src/com.oracle.js.parser/src/com/oracle/js/parser/ir/Node.java b/graal-js/src/com.oracle.js.parser/src/com/oracle/js/parser/ir/Node.java index 18c63c0ecb0..b6c2bf44850 100644 --- a/graal-js/src/com.oracle.js.parser/src/com/oracle/js/parser/ir/Node.java +++ b/graal-js/src/com.oracle.js.parser/src/com/oracle/js/parser/ir/Node.java @@ -254,7 +254,7 @@ public long getToken() { return token; } - // on change, we have to replace the entire list, that's we can't simple do ListIterator.set + // on change, we have to replace the entire list, that's we can't simply do ListIterator.set static List accept(final NodeVisitor visitor, final List list) { final int size = list.size(); if (size == 0) { diff --git a/graal-js/src/com.oracle.truffle.js.parser/src/com/oracle/truffle/js/parser/GraalJSParserHelper.java b/graal-js/src/com.oracle.truffle.js.parser/src/com/oracle/truffle/js/parser/GraalJSParserHelper.java index 7699dc779bd..1298a5c820c 100644 --- a/graal-js/src/com.oracle.truffle.js.parser/src/com/oracle/truffle/js/parser/GraalJSParserHelper.java +++ b/graal-js/src/com.oracle.truffle.js.parser/src/com/oracle/truffle/js/parser/GraalJSParserHelper.java @@ -201,6 +201,7 @@ private static ScriptEnvironment makeScriptEnvironment(JSParserOptions parserOpt builder.privateFieldsIn(parserOptions.privateFieldsIn()); builder.topLevelAwait(parserOptions.topLevelAwait()); builder.v8Intrinsics(parserOptions.v8Intrinsics()); + builder.extractors(parserOptions.extractors()); if (parserOptions.functionStatementError()) { builder.functionStatementBehavior(FunctionStatementBehavior.ERROR); } else { diff --git a/graal-js/src/com.oracle.truffle.js.parser/src/com/oracle/truffle/js/parser/GraalJSTranslator.java b/graal-js/src/com.oracle.truffle.js.parser/src/com/oracle/truffle/js/parser/GraalJSTranslator.java index 61a1d660c27..b54e26e7db6 100644 --- a/graal-js/src/com.oracle.truffle.js.parser/src/com/oracle/truffle/js/parser/GraalJSTranslator.java +++ b/graal-js/src/com.oracle.truffle.js.parser/src/com/oracle/truffle/js/parser/GraalJSTranslator.java @@ -119,7 +119,9 @@ import com.oracle.truffle.js.nodes.access.DeclareEvalVariableNode; import com.oracle.truffle.js.nodes.access.DeclareGlobalNode; import com.oracle.truffle.js.nodes.access.GetIteratorUnaryNode; +import com.oracle.truffle.js.nodes.access.GetMethodNode; import com.oracle.truffle.js.nodes.access.GlobalPropertyNode; +import com.oracle.truffle.js.nodes.access.IteratorToArrayNode; import com.oracle.truffle.js.nodes.access.JSConstantNode; import com.oracle.truffle.js.nodes.access.JSReadFrameSlotNode; import com.oracle.truffle.js.nodes.access.JSWriteFrameSlotNode; @@ -151,6 +153,7 @@ import com.oracle.truffle.js.nodes.control.SequenceNode; import com.oracle.truffle.js.nodes.control.StatementNode; import com.oracle.truffle.js.nodes.control.SuspendNode; +import com.oracle.truffle.js.nodes.extractor.InvokeCustomMatcherOrThrowNode; import com.oracle.truffle.js.nodes.function.AbstractFunctionArgumentsNode; import com.oracle.truffle.js.nodes.function.BlockScopeNode; import com.oracle.truffle.js.nodes.function.EvalNode; @@ -2858,8 +2861,7 @@ private JavaScriptNode transformAssignmentImpl(Expression assignmentExpression, // fall through case IDENT: if (lhsExpression instanceof CallNode callNode) { - assert callNode.isWebCompatAssignmentTargetType(); - assignedNode = factory.createDual(context, transform(lhsExpression), factory.createThrowError(JSErrorType.ReferenceError, INVALID_LHS)); + assignedNode = transformAssignmentExtractor(callNode, assignedValue, initializationAssignment); } else { assignedNode = transformAssignmentIdent((IdentNode) lhsExpression, assignedValue, binaryOp, returnOldValue, convertLHSToNumeric, initializationAssignment); } @@ -2907,40 +2909,38 @@ private JavaScriptNode transformAssignmentIdent(IdentNode identNode, JavaScriptN rhs = checkMutableBinding(rhs, scopeVar.getName()); } return scopeVar.createWriteNode(rhs); + } else if (isLogicalOp(binaryOp)) { + assert !convertLHSToNumeric && !returnOldValue && assignedValue != null; + if (constAssignment) { + rhs = checkMutableBinding(rhs, scopeVar.getName()); + } + JavaScriptNode readNode = tagExpression(scopeVar.createReadNode(), identNode); + JavaScriptNode writeNode = scopeVar.createWriteNode(rhs); + return factory.createBinary(context, binaryOp, readNode, writeNode); } else { - if (isLogicalOp(binaryOp)) { - assert !convertLHSToNumeric && !returnOldValue && assignedValue != null; - if (constAssignment) { - rhs = checkMutableBinding(rhs, scopeVar.getName()); - } - JavaScriptNode readNode = tagExpression(scopeVar.createReadNode(), identNode); - JavaScriptNode writeNode = scopeVar.createWriteNode(rhs); - return factory.createBinary(context, binaryOp, readNode, writeNode); + // e.g.: lhs *= rhs => lhs = lhs * rhs + // If lhs is a side-effecting getter that deletes lhs, we must not throw a + // ReferenceError at the lhs assignment since the lhs reference is already resolved. + // We also need to ensure that HasBinding is idempotent or evaluated at most once. + Pair, UnaryOperator> pair = scopeVar.createCompoundAssignNode(); + JavaScriptNode readNode = tagExpression(pair.getFirst().get(), identNode); + if (convertLHSToNumeric) { + readNode = factory.createToNumericOperand(readNode); + } + VarRef prevValueTemp = null; + if (returnOldValue) { + prevValueTemp = environment.createTempVar(); + readNode = prevValueTemp.createWriteNode(readNode); + } + JavaScriptNode binOpNode = tagExpression(factory.createBinary(context, binaryOp, readNode, rhs), identNode); + if (constAssignment) { + binOpNode = checkMutableBinding(binOpNode, scopeVar.getName()); + } + JavaScriptNode writeNode = pair.getSecond().apply(binOpNode); + if (returnOldValue) { + return factory.createDual(context, writeNode, prevValueTemp.createReadNode()); } else { - // e.g.: lhs *= rhs => lhs = lhs * rhs - // If lhs is a side-effecting getter that deletes lhs, we must not throw a - // ReferenceError at the lhs assignment since the lhs reference is already resolved. - // We also need to ensure that HasBinding is idempotent or evaluated at most once. - Pair, UnaryOperator> pair = scopeVar.createCompoundAssignNode(); - JavaScriptNode readNode = tagExpression(pair.getFirst().get(), identNode); - if (convertLHSToNumeric) { - readNode = factory.createToNumericOperand(readNode); - } - VarRef prevValueTemp = null; - if (returnOldValue) { - prevValueTemp = environment.createTempVar(); - readNode = prevValueTemp.createWriteNode(readNode); - } - JavaScriptNode binOpNode = tagExpression(factory.createBinary(context, binaryOp, readNode, rhs), identNode); - if (constAssignment) { - binOpNode = checkMutableBinding(binOpNode, scopeVar.getName()); - } - JavaScriptNode writeNode = pair.getSecond().apply(binOpNode); - if (returnOldValue) { - return factory.createDual(context, writeNode, prevValueTemp.createReadNode()); - } else { - return writeNode; - } + return writeNode; } } } @@ -3070,14 +3070,19 @@ private JavaScriptNode transformIndexAssignment(IndexNode indexNode, JavaScriptN } private JavaScriptNode transformDestructuringArrayAssignment(Expression lhsExpression, JavaScriptNode assignedValue, boolean initializationAssignment) { + VarRef valueTempVar = environment.createTempVar(); + JavaScriptNode initValue = valueTempVar.createWriteNode(assignedValue); + JavaScriptNode getIterator = factory.createGetIterator(initValue); LiteralNode.ArrayLiteralNode arrayLiteralNode = (LiteralNode.ArrayLiteralNode) lhsExpression; List elementExpressions = arrayLiteralNode.getElementExpressions(); + + return this.transformDestructuringArrayAssignment(elementExpressions, getIterator, valueTempVar, initializationAssignment); + } + + private JavaScriptNode transformDestructuringArrayAssignment(List elementExpressions, JavaScriptNode getIterator, VarRef valueTempVar, boolean initializationAssignment) { JavaScriptNode[] initElements = javaScriptNodeArray(elementExpressions.size()); VarRef iteratorTempVar = environment.createTempVar(); - VarRef valueTempVar = environment.createTempVar(); - JavaScriptNode initValue = valueTempVar.createWriteNode(assignedValue); // By default, we use the hint to track the type of iterator. - JavaScriptNode getIterator = factory.createGetIterator(initValue); JavaScriptNode initIteratorTempVar = iteratorTempVar.createWriteNode(getIterator); for (int i = 0; i < elementExpressions.size(); i++) { @@ -3099,7 +3104,7 @@ private JavaScriptNode transformDestructuringArrayAssignment(Expression lhsExpre if (init != null) { rhsNode = factory.createNotUndefinedOr(rhsNode, transform(init)); } - if (lhsExpr != null && lhsExpr.isTokenType(TokenType.SPREAD_ARRAY)) { + if (lhsExpr != null && (lhsExpr.isTokenType(TokenType.SPREAD_ARRAY) || lhsExpr.isTokenType(TokenType.SPREAD_ARGUMENT))) { rhsNode = factory.createIteratorToArray(context, iteratorTempVar.createReadNode()); lhsExpr = ((UnaryNode) lhsExpr).getExpression(); } @@ -3117,6 +3122,32 @@ private JavaScriptNode transformDestructuringArrayAssignment(Expression lhsExpre factory.createExprBlock(resetIterator, resetValue)); } + private JavaScriptNode transformAssignmentExtractor(CallNode fakeCallNode, JavaScriptNode assignedValue, boolean initializationAssignment) { + final var functionExpr = fakeCallNode.getFunction(); + final var function = transform(functionExpr); + + var receiver = function; + if (functionExpr instanceof AccessNode) { + final AccessNode accessNode = (AccessNode) functionExpr; + receiver = transform(accessNode.getBase()); + } + + // It's important to only read from assignedValue once, otherwise we might advance an iterator multiple times + VarRef assignedValueTempVar = environment.createTempVar(); + JavaScriptNode storeAssignedValue = assignedValueTempVar.createWriteNode(assignedValue); + JavaScriptNode readAssignedValue = assignedValueTempVar.createReadNode(); + + final var invokeCustomMatcherOrThrowNode = factory.createInvokeCustomMatcherOrThrow(context, function, readAssignedValue, receiver, environment.isStrictMode()); + + final var args = fakeCallNode.getArgs(); + VarRef valueTempVar = environment.createTempVar(); + return createBlock( + storeAssignedValue, + this.transformDestructuringArrayAssignment(args, invokeCustomMatcherOrThrowNode, valueTempVar, initializationAssignment), + valueTempVar.createWriteNode(assignedValueTempVar.createReadNode()) + ); + } + private JavaScriptNode transformDestructuringObjectAssignment(Expression lhsExpression, JavaScriptNode assignedValue, boolean initializationAssignment) { ObjectNode objectLiteralNode = (ObjectNode) lhsExpression; List propertyExpressions = objectLiteralNode.getElements(); diff --git a/graal-js/src/com.oracle.truffle.js.test/js/assert.js b/graal-js/src/com.oracle.truffle.js.test/js/assert.js index 51b49599f2b..f04dae86548 100644 --- a/graal-js/src/com.oracle.truffle.js.test/js/assert.js +++ b/graal-js/src/com.oracle.truffle.js.test/js/assert.js @@ -24,11 +24,12 @@ function assertThrows(fn, errorType, msg) { throw Error('error expected for method: ' + fn); } -function assertEqual(expected, actual) { +function assertEqual(expected, actual, message) { if (expected != actual) { var error = 'Objects not equal - ' + 'expected: [' + expected + '] vs. ' - + 'actual: [' + actual +']'; + + 'actual: [' + actual +']' + + (message ? ' : ' + message : ''); throw new Error(error); } } @@ -43,11 +44,12 @@ function _isSame(a, b) { return (a !== a) && (b !== b); } -function assertSame(expected, actual) { +function assertSame(expected, actual, message) { if (!_isSame(expected, actual)) { var error = 'Objects not same - ' + 'expected: [' + expected + '] vs. ' - + 'actual: [' + actual +']'; + + 'actual: [' + actual +']' + + (message ? ' : ' + message : ''); throw new Error(error); } } @@ -60,12 +62,12 @@ function assertSameContent(expected, actual) { } } -function assertTrue(condition) { - assertSame(true, condition); +function assertTrue(condition, message) { + assertSame(true, condition, message); } -function assertFalse(condition) { - assertSame(false, condition); +function assertFalse(condition, message) { + assertSame(false, condition, message); } function fail(msg) { diff --git a/graal-js/src/com.oracle.truffle.js.test/js/extractors/binding-and-assignment.js b/graal-js/src/com.oracle.truffle.js.test/js/extractors/binding-and-assignment.js new file mode 100644 index 00000000000..b5cbd82f0e9 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/js/extractors/binding-and-assignment.js @@ -0,0 +1,371 @@ +/** + * @option js.extractors + */ + +/** + * Argument Binding and Assignment + * + * Tests that: + * - In binding contexts (variable declarations), arguments create new bindings + * - In assignment contexts, arguments must be valid LHS targets + * - Assignment targets can include property access, array indexing, etc. + */ + +load('../assert.js'); +load('common.js'); + +// Binding Context - const Creates New Bindings +{ + const p = new Pair(1, 2); + const Pair(x, y) = p; + + // x and y are new bindings created by the declaration + assertSame(1, x); + assertSame(2, y); + + // They are independent variables + const x2 = x + 10; + const y2 = y + 20; + assertSame(11, x2); + assertSame(22, y2); +} + +// Binding Context - let Creates New Bindings +{ + const p = new Pair(3, 4); + let Pair(x, y) = p; + + assertSame(3, x); + assertSame(4, y); + + // Variables can be reassigned (mutable bindings) + x = x * 2; + y = y * 2; + assertSame(6, x); + assertSame(8, y); +} + +// Binding Context - var Creates New Bindings +{ + const p = new Pair(5, 6); + var Pair(varX, varY) = p; + + assertSame(5, varX); + assertSame(6, varY); +} + +// Binding Context - Function Parameters Create New Bindings +{ + function test(Pair(x, y)) { + // x and y are parameter bindings + assertSame(7, x); + assertSame(8, y); + + // Can be modified within the function + x = x + 1; + y = y + 1; + return { x, y }; + } + + const result = test(new Pair(7, 8)); + assertSame(8, result.x); + assertSame(9, result.y); +} + +// Assignment Context - Simple Variables +{ + const p = new Pair(9, 10); + let x, y; + + // Assignment to existing variables + Pair(x, y) = p; + + assertSame(9, x); + assertSame(10, y); +} + +// Assignment Context - Object Properties +{ + const p = new Pair(11, 12); + const obj = {}; + + // Assignment to object properties + Pair(obj.x, obj.y) = p; + + assertSame(11, obj.x); + assertSame(12, obj.y); +} + +// Assignment Context - Nested Object Properties +{ + const p = new Pair(13, 14); + const obj = { nested: {} }; + + Pair(obj.nested.x, obj.nested.y) = p; + + assertSame(13, obj.nested.x); + assertSame(14, obj.nested.y); +} + +// Assignment Context - Array Elements +{ + const p = new Pair(15, 16); + const arr = []; + + // Assignment to array indices + Pair(arr[0], arr[1]) = p; + + assertSame(15, arr[0]); + assertSame(16, arr[1]); +} + +// Assignment Context - Mixed Array Indices +{ + const p = new Pair(17, 18); + const arr = new Array(10); + + Pair(arr[5], arr[9]) = p; + + assertSame(17, arr[5]); + assertSame(18, arr[9]); +} + +// Assignment Context - Computed Property Access +{ + const t = new Triple(19, 20, 21); + const obj = {}; + const keys = ['a', 'b', 'c']; + + Triple(obj[keys[0]], obj[keys[1]], obj[keys[2]]) = t; + + assertSame(19, obj.a); + assertSame(20, obj.b); + assertSame(21, obj.c); +} + +// Assignment Context - Mixed Targets +{ + const t = new Triple(22, 23, "24"); + let x; + const obj = {}; + const arr = []; + + // Mix of variable, object property, and array element + Triple(x, obj.prop, arr[0]) = t; + + assertSame(22, x); + assertSame(23, obj.prop); + assertSame("24", arr[0]); +} + +// Assignment Context - Nested Structures +{ + const t = new Triple(24, 25, 26); + const state = { data: {}, list: [] }; + + Triple(state.data.a, state.data.b, state.list[0]) = t; + + assertSame(24, state.data.a); + assertSame(25, state.data.b); + assertSame(26, state.list[0]); +} + +// Assignment Context - this Properties in Method +{ + class Storage { + store(p) { + Pair(this.x, this.y) = p; + } + + get() { + return { x: this.x, y: this.y }; + } + } + + const storage = new Storage(); + storage.store(new Pair(27, 28)); + + const result = storage.get(); + assertSame(27, result.x); + assertSame(28, result.y); +} + +// Assignment Context - Global Object Properties +{ + const p = new Pair(29, 30); + + Pair(globalThis.testX, globalThis.testY) = p; + + assertSame(29, globalThis.testX); + assertSame(30, globalThis.testY); + + // Cleanup + delete globalThis.testX; + delete globalThis.testY; +} + +// Assignment Context - Complex LHS Expressions +{ + const p = new Pair(31, 32); + const obj = { items: [{}, {}, {}] }; + + Pair(obj.items[1].x, obj.items[2].y) = p; + + assertSame(31, obj.items[1].x); + assertSame(32, obj.items[2].y); +} + +// Binding and Assignment - Default Values in Binding +{ + class Optional { + constructor(value) { + this.value = value; + } + + static [Symbol.customMatcher](obj) { + return [obj.value]; + } + } + + // Default value is used when undefined + const Optional(x = 100) = new Optional(undefined); + assertSame(100, x); + + // Default value is not used when value is present + const Optional(y = 100) = new Optional(50); + assertSame(50, y); +} + +// Assignment - Default Values in Assignment +{ + class Optional { + constructor(value) { + this.value = value; + } + + static [Symbol.customMatcher](obj) { + return [obj.value]; + } + } + + let a, b; + + // Default value in assignment context + Optional(a = 200) = new Optional(undefined); + assertSame(200, a); + + Optional(b = 200) = new Optional(75); + assertSame(75, b); +} + +// Binding Context - Rest Pattern +{ + const t = new Triple(33, 34, 35); + const Triple(first, ...rest) = t; + + assertSame(33, first); + assertSameContent([34, 35], rest); +} + +// Assignment Context - Rest Pattern +{ + const t = new Triple(36, 37, 38); + let a, b; + + Triple(a, ...b) = t; + + assertSame(36, a); + assertSameContent([37, 38], b); +} + +// Assignment Context - Nested Extractors with Property Access +{ + class Container { + constructor(value) { + this.value = value; + } + + static [Symbol.customMatcher](obj) { + return [obj.value]; + } + } + + const obj = {}; + const nested = new Container(new Pair(39, 40)); + + Container(Pair(obj.x, obj.y)) = nested; + + assertSame(39, obj.x); + assertSame(40, obj.y); +} + +// Binding and Assignment - Scoping +{ + const p = new Pair(41, 42); + + // Binding creates new scope-local variables + { + const Pair(x, y) = p; + assertSame(41, x); + assertSame(42, y); + } + + // x and y are not accessible here (would throw ReferenceError) + assertThrows(() => x, ReferenceError); + assertThrows(() => y, ReferenceError); +} + +// Assignment - Preserves Existing Variables +{ + const p1 = new Pair(43, 44); + const p2 = new Pair(45, 46); + + let x, y; + + Pair(x, y) = p1; + assertSame(43, x); + assertSame(44, y); + + // Assignment updates the same variables + Pair(x, y) = p2; + assertSame(45, x); + assertSame(46, y); +} + +// Assignment Context - Symbol Properties +{ + const p = new Pair(47, 48); + const obj = {}; + const symX = Symbol('x'); + const symY = Symbol('y'); + + Pair(obj[symX], obj[symY]) = p; + + assertSame(47, obj[symX]); + assertSame(48, obj[symY]); +} + +// Assignment Context - Dynamic Property Names +{ + const p = new Pair(49, 50); + const obj = {}; + + function getProp(name) { + return 'prop_' + name; + } + + Pair(obj[getProp('x')], obj[getProp('y')]) = p; + + assertSame(49, obj.prop_x); + assertSame(50, obj.prop_y); +} + +// Binding Context - Nested Destructuring with Extractors +{ + const p = new Pair(51, 52); + const { wrapper: Pair(x, y) } = { wrapper: p }; + + assertSame(51, x); + assertSame(52, y); +} + diff --git a/graal-js/src/com.oracle.truffle.js.test/js/extractors/common.js b/graal-js/src/com.oracle.truffle.js.test/js/extractors/common.js new file mode 100644 index 00000000000..e1b5e1f5517 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/js/extractors/common.js @@ -0,0 +1,32 @@ +/** + * Common test classes and utilities for extractor tests. + */ + +class Pair { + constructor(first, second) { + this.first = first; + this.second = second; + } + + static [Symbol.customMatcher](pair) { + return [pair.first, pair.second]; + } +} + +class Triple { + constructor(a, b, c) { + this.a = a; + this.b = b; + this.c = c; + } + + static [Symbol.customMatcher](triple) { + return [triple.a, triple.b, triple.c]; + } +} + +const PlainObjectWithCustomMatcher = { + [Symbol.customMatcher](obj) { + return Object.entries(obj).sort(); + } +} diff --git a/graal-js/src/com.oracle.truffle.js.test/js/extractors/error-handling.js b/graal-js/src/com.oracle.truffle.js.test/js/extractors/error-handling.js new file mode 100644 index 00000000000..ce47d853adb --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/js/extractors/error-handling.js @@ -0,0 +1,86 @@ +/** + * @option js.extractors + */ + +/** + * Error Handling + * + * Tests error scenarios and side effects in extractor patterns: + * - Call expression assignment errors with side effects + * - Error propagation from custom matchers + */ + +load('../assert.js'); + +// Call Assignment Error with Side Effect +// https://github.com/tc39/proposal-extractors/issues/34 +{ + let sideEffectHappened = false; + + function f() { + sideEffectHappened = true; + } + + try { + f() = 1; + } catch (error) { + if (!sideEffectHappened) { + console.log(error); + } + assertTrue(sideEffectHappened, 'Side effect should happen before ReferenceError'); + assertTrue(error instanceof ReferenceError, 'Should throw ReferenceError'); + } +} + +// CustomMatcher Error - Returns Non-Object +{ + class ReturnsNull { + static [Symbol.customMatcher]() { + return null; + } + } + + try { + const ReturnsNull(x) = new ReturnsNull(); + fail('Should have thrown TypeError'); + } catch (e) { + assertTrue(e instanceof TypeError, 'Should throw TypeError for null return'); + } +} + +// CustomMatcher Error - Throws During Execution +{ + class ThrowsError { + static [Symbol.customMatcher]() { + throw new Error("Custom matcher error"); + } + } + + try { + const ThrowsError(x) = new ThrowsError(); + fail('Should have propagated the error'); + } catch (e) { + assertSame("Custom matcher error", e.message); + } +} + +// CustomMatcher Error - Iterator Error +{ + class BadIterator { + static [Symbol.customMatcher]() { + return { + [Symbol.iterator]() { + throw new Error("Iterator creation failed"); + } + }; + } + } + + try { + const BadIterator(x) = new BadIterator(); + fail('Should have thrown error from iterator'); + } catch (e) { + assertSame("Iterator creation failed", e.message); + } +} + diff --git a/graal-js/src/com.oracle.truffle.js.test/js/extractors/examples/date-extractor.js b/graal-js/src/com.oracle.truffle.js.test/js/extractors/examples/date-extractor.js new file mode 100644 index 00000000000..01a16cb90b6 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/js/extractors/examples/date-extractor.js @@ -0,0 +1,66 @@ +/** + * @option js.extractors + */ + +load('../../assert.js'); + +const DateExtractor = { + [Symbol.customMatcher](value) { + if (value instanceof Date) { + return [value]; + } else if (typeof value === "number") { + return [new Date(value)]; + } else if (typeof value === "string") { + return [new Date(Date.parse(value))]; + } + } +}; + +class Book { + constructor({ + isbn, + title, + // Extract `createdAt` as a Date + createdAt: DateExtractor(createdAt) = new Date(), + modifiedAt: DateExtractor(modifiedAt) = createdAt + }) { + this.isbn = isbn; + this.title = title; + this.createdAt = createdAt; + this.modifiedAt = modifiedAt; + } +} + +{ + const date = new Date("1970-01-01T00:00:00Z"); + const book = new Book({ isbn: "...", title: "...", createdAt: date }); + assertSame(date.valueOf(), book.createdAt.valueOf()); +} + +{ + const msSinceEpoch = 1000; + const book = new Book({ isbn: "...", title: "...", createdAt: msSinceEpoch }); + assertSame(msSinceEpoch, book.createdAt.valueOf()); +} + +{ + const createdAt = "1970-01-01T00Z"; + const book = new Book({ isbn: "...", title: "...", createdAt }); + assertSame(Date.parse(createdAt), book.createdAt.valueOf()); +} + +{ + const createdAt = new Date("2025-01-01"); + const book = new Book({ isbn: "123", title: "Test Book", createdAt }); + assertSame(book.createdAt.valueOf(), book.modifiedAt.valueOf()); +} + +{ + const beforeCreation = Date.now(); + const book = new Book({ isbn: "456", title: "Another Book" }); + const afterCreation = Date.now(); + + assertTrue(book.createdAt.valueOf() >= beforeCreation); + assertTrue(book.createdAt.valueOf() <= afterCreation); +} + diff --git a/graal-js/src/com.oracle.truffle.js.test/js/extractors/examples/map-extractor.js b/graal-js/src/com.oracle.truffle.js.test/js/extractors/examples/map-extractor.js new file mode 100644 index 00000000000..3340f7aabf7 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/js/extractors/examples/map-extractor.js @@ -0,0 +1,116 @@ +/** + * @option js.extractors + */ + +load('../../assert.js'); + +const MapExtractor = { + [Symbol.customMatcher](map) { + const obj = {}; + for (const [key, value] of map) { + obj[typeof key === "symbol" ? key : `${key}`] = value; + } + return [obj]; + } +}; + +// Basic Map extraction +{ + const obj = { + map: new Map([["a", 1], ["b", 2]]) + }; + + const { map: MapExtractor({ a, b }) } = obj; + + assertSame(a, 1); + assertSame(b, 2); +} + +// Map with various key types +{ + const sym = Symbol('key'); + const map = new Map([ + ["string", 10], + [42, 20], + [sym, 30] + ]); + + const MapExtractor({ string, "42": numKey, [sym]: symValue }) = map; + + assertSame(10, string); + assertSame(20, numKey); + assertSame(30, symValue); +} + +// Nested object with Map +{ + const data = { + metadata: { + values: new Map([ + ["x", 100], + ["y", 200], + ["z", 300] + ]) + } + }; + + const { metadata: { values: MapExtractor({ x, y, z }) } } = data; + + assertSame(100, x); + assertSame(200, y); + assertSame(300, z); +} + +// Partial extraction from Map +{ + const map = new Map([ + ["a", 1], + ["b", 2], + ["c", 3], + ["d", 4] + ]); + + const MapExtractor({ a, c }) = map; + + assertSame(1, a); + assertSame(3, c); +} + +// Function parameter with Map extractor +{ + function processMap(MapExtractor({ x, y })) { + return x + y; + } + + const map = new Map([["x", 5], ["y", 10]]); + const result = processMap(map); + + assertSame(15, result); +} + +// Empty Map +{ + const map = new Map(); + const MapExtractor({}) = map; + // Should succeed with no bindings +} + +// Map in array destructuring +{ + const maps = [ + new Map([["value", 1]]), + new Map([["value", 2]]), + new Map([["value", 3]]) + ]; + + const [ + MapExtractor({ value: v1 }), + MapExtractor({ value: v2 }), + MapExtractor({ value: v3 }) + ] = maps; + + assertSame(1, v1); + assertSame(2, v2); + assertSame(3, v3); +} + diff --git a/graal-js/src/com.oracle.truffle.js.test/js/extractors/examples/regexp-extractor.js b/graal-js/src/com.oracle.truffle.js.test/js/extractors/examples/regexp-extractor.js new file mode 100644 index 00000000000..b99aa3d63e1 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/js/extractors/examples/regexp-extractor.js @@ -0,0 +1,187 @@ +/** + * @option js.extractors + */ + +load('../../assert.js'); + +// Add customMatcher to RegExp.prototype +// This is potentially a built-in feature as part of Pattern Matching +RegExp.prototype[Symbol.customMatcher] = function (value) { + const match = this.exec(value); + return !!match && [match]; +}; + +const input = '2025-01-02T12:34:56Z'; + +const IsoDate = /^(?\d{4})-(?\d{2})-(?\d{2})$/; +const IsoTime = /^(?\d{2}):(?\d{2}):(?\d{2})$/; +const IsoDateTime = /^(?[^TZ]+)T(?