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 5d17f89438a..0076b4bb538 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) && (isDestructuringLhs(lhs) || isExtractorLhs(lhs)) && (inPatternPosition || !lhs.isParenthesized())) { verifyDestructuringAssignmentPattern(lhs, CONTEXT_ASSIGNMENT_TARGET); break; } else { @@ -1067,6 +1067,11 @@ private boolean isDestructuringLhs(Expression lhs) { return false; } + private boolean isExtractorLhs(Expression lhs) { + // todo-lw: call node :( + return lhs instanceof CallNode; + } + private void verifyDestructuringAssignmentPattern(Expression pattern, String contextString) { assert pattern instanceof ObjectNode || pattern instanceof ArrayLiteralNode; pattern.accept(new VerifyDestructuringPatternNodeVisitor(new LexicalContext()) { @@ -1103,6 +1108,26 @@ public boolean enterIndexNode(IndexNode indexNode) { return false; } + @Override + public boolean enterCallNode(CallNode callNode) { + if (callNode.isParenthesized()) { + throw error(AbstractParser.message(MSG_INVALID_LVALUE), callNode.getToken()); + } + // todo-lw: surely there is a better way to do this + for (final var arg : callNode.getArgs()) { + if (arg instanceof IdentNode) { + enterIdentNode((IdentNode) arg); + } else if (arg instanceof LiteralNode) { + enterLiteralNode((LiteralNode) arg); + } else if (arg instanceof ObjectNode) { + enterObjectNode((ObjectNode) arg); + } else { + enterDefault(arg); + } + } + return false; + } + @Override protected boolean enterDefault(Node node) { throw error(String.format("unexpected node in AssignmentPattern: %s", node)); @@ -2475,9 +2500,12 @@ 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); + // Get left hand side. + // todo-lw: conditionalExpression feels way too broad here, but binding also uses it so idk + final Expression binding = conditionalExpression(true, yield, await, CoverExpressionError.DENY); + + final boolean isExtracting = binding instanceof CallNode; + final boolean isDestructuring = !(binding instanceof IdentNode) && !isExtracting; if (isDestructuring) { final int finalVarFlags = varFlags | VarNode.IS_DESTRUCTURING; verifyDestructuringBindingPattern(binding, new Consumer() { @@ -2525,7 +2553,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 (!isDestructuring && !isExtracting) { assert init != null || varType != CONST || !isStatement; final IdentNode ident = (IdentNode) binding; if (varType != VAR && ident.getName().equals(LET.getName())) { @@ -2835,6 +2863,26 @@ public boolean enterIdentNode(IdentNode identNode) { return false; } + // todo-lw: this is duplicate code + @Override + public boolean enterCallNode(CallNode callNode) { + if (callNode.isParenthesized()) { + throw error(AbstractParser.message(MSG_INVALID_LVALUE), callNode.getToken()); + } + for (final var arg : callNode.getArgs()) { + if (arg instanceof IdentNode) { + enterIdentNode((IdentNode) arg); + } else if (arg instanceof LiteralNode) { + enterLiteralNode((LiteralNode) arg); + } else if (arg instanceof ObjectNode) { + enterObjectNode((ObjectNode) arg); + } else { + enterDefault(arg); + } + } + return false; + } + @Override protected boolean enterDefault(Node node) { throw error(String.format("unexpected node in BindingPattern: %s", node)); 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/GraalJSTranslator.java b/graal-js/src/com.oracle.truffle.js.parser/src/com/oracle/truffle/js/parser/GraalJSTranslator.java index d012b0b8eb5..f30aa4b8fc6 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; @@ -2846,7 +2849,12 @@ private JavaScriptNode transformAssignmentImpl(Expression assignmentExpression, } // fall through case IDENT: - assignedNode = transformAssignmentIdent((IdentNode) lhsExpression, assignedValue, binaryOp, returnOldValue, convertLHSToNumeric, initializationAssignment); + // todo-lw: call node :( + if (lhsExpression instanceof CallNode) { + assignedNode = transformAssignmentExtractor((CallNode) lhsExpression, assignedValue, binaryOp, returnOldValue, convertLHSToNumeric, initializationAssignment); + } else { + assignedNode = transformAssignmentIdent((IdentNode) lhsExpression, assignedValue, binaryOp, returnOldValue, convertLHSToNumeric, initializationAssignment); + } break; case LBRACKET: // target[element] @@ -2891,40 +2899,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; } } } @@ -3054,14 +3060,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++) { @@ -3083,7 +3094,8 @@ private JavaScriptNode transformDestructuringArrayAssignment(Expression lhsExpre if (init != null) { rhsNode = factory.createNotUndefinedOr(rhsNode, transform(init)); } - if (lhsExpr != null && lhsExpr.isTokenType(TokenType.SPREAD_ARRAY)) { + // todo-lw: this change is kind of sus + if (lhsExpr != null && (lhsExpr.isTokenType(TokenType.SPREAD_ARRAY) || lhsExpr.isTokenType(TokenType.SPREAD_ARGUMENT))) { rhsNode = factory.createIteratorToArray(context, iteratorTempVar.createReadNode()); lhsExpr = ((UnaryNode) lhsExpr).getExpression(); } @@ -3097,6 +3109,25 @@ private JavaScriptNode transformDestructuringArrayAssignment(Expression lhsExpre return factory.createExprBlock(initIteratorTempVar, closeIfNotDone, valueTempVar.createReadNode()); } + private JavaScriptNode transformAssignmentExtractor(CallNode fakeCallNode, JavaScriptNode assignedValue, BinaryOperation binaryOp, boolean returnOldValue, boolean convertToNumeric, boolean initializationAssignment) { + // todo-lw: call node :( + + 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()); + } + + final var invokeCustomMatcherOrThrowNode = InvokeCustomMatcherOrThrowNode.create(context, function, assignedValue, receiver); + + final var args = fakeCallNode.getArgs(); + VarRef valueTempVar = environment.createTempVar(); + return this.transformDestructuringArrayAssignment(args, invokeCustomMatcherOrThrowNode, valueTempVar, initializationAssignment); + } + 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/extractors/as-default.js b/graal-js/src/com.oracle.truffle.js.test/js/extractors/as-default.js new file mode 100644 index 00000000000..53989886c71 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/js/extractors/as-default.js @@ -0,0 +1,46 @@ +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 [Date.parse(value)]; + } + } +}; + +class Book { + constructor({ + isbn, + title, + // Extract `createdAt` as an Instant + createdAt: DateExtractor(createdAt) = Date.now(), + modifiedAt: DateExtractor(modifiedAt) = createdAt + }) { + this.isbn = isbn; + this.title = title; + this.createdAt = createdAt; + this.modifiedAt = modifiedAt; + } +} + +{ + const date = Date.parse("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).valueOf(), book.createdAt.valueOf()); +} diff --git a/graal-js/src/com.oracle.truffle.js.test/js/extractors/assignment.js b/graal-js/src/com.oracle.truffle.js.test/js/extractors/assignment.js new file mode 100644 index 00000000000..c4e82433e27 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/js/extractors/assignment.js @@ -0,0 +1,86 @@ +load('../assert.js'); + +{ + class C { + #data; + + constructor(data) { + this.#data = data; + } + + static [Symbol.customMatcher](subject) { + return #data in subject && [subject.#data]; + } + } + + const subject = new C("data"); + + let x; + C(x) = subject; + + assertSame(x, "data"); +} + +{ + class C { + #data; + constructor(data) { + this.#data = data; + } + static [Symbol.customMatcher](subject) { + return #data in subject && [subject.#data]; + } + } + + const subject = new C({ x: 1, y: 2 }); + + let x, y; + C({ x, y }) = subject; + + assertSame(x, 1); + assertSame(y, 2); +} + +{ + class C { + #first; + #second; + constructor(first, second) { + this.#first = first; + this.#second = second; + } + static [Symbol.customMatcher](subject) { + return #first in subject && [subject.#first, subject.#second]; + } + } + + const subject = new C(undefined, 2); + + const C(x = -1, y) = subject; + assertSame(x, -1); + assertSame(y, 2); +} + +{ + const [a, ...b] = [1, 2, 3]; + + class C { + #first; + #second; + #third; + constructor(first, second, third) { + this.#first = first; + this.#second = second; + this.#third = third; + } + static [Symbol.customMatcher](subject) { + return #first in subject && [subject.#first, subject.#second, subject.#third]; + } + } + + const subject = new C(1, 2, 3); + + const C(x, ...y) = subject; + assertSame(x, 1); + assertSameContent(y, [2, 3]); +} diff --git a/graal-js/src/com.oracle.truffle.js.test/js/extractors/binding.js b/graal-js/src/com.oracle.truffle.js.test/js/extractors/binding.js new file mode 100644 index 00000000000..03007f0f843 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/js/extractors/binding.js @@ -0,0 +1,23 @@ +load('../assert.js'); + +class C { + #data; + constructor(data) { + this.#data = data; + } + static [Symbol.customMatcher](subject) { + return #data in subject && [subject.#data]; + } +} + +const subject = new C("data"); + +{ + let C(x) = subject; + assertSame(x, "data"); +} + +{ + const C(x) = subject; + assertSame(x, "data"); +} \ No newline at end of file diff --git a/graal-js/src/com.oracle.truffle.js.test/js/extractors/nested.js b/graal-js/src/com.oracle.truffle.js.test/js/extractors/nested.js new file mode 100644 index 00000000000..a836fde85cd --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/js/extractors/nested.js @@ -0,0 +1,27 @@ +load('../assert.js'); + +class C { + #data1; + constructor(data1) { + this.#data1 = data1; + } + static [Symbol.customMatcher](subject) { + return #data1 in subject && [subject.#data1]; + } +} + +class D { + #data2; + constructor(data2) { + this.#data2 = data2; + } + static [Symbol.customMatcher](subject) { + return #data2 in subject && [subject.#data2]; + } +} + +const subject = new C(new D("data")); + +const C(D(x)) = subject; + +assertSame(x, "data"); diff --git a/graal-js/src/com.oracle.truffle.js.test/js/extractors/object.js b/graal-js/src/com.oracle.truffle.js.test/js/extractors/object.js new file mode 100644 index 00000000000..151579e55fe --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/js/extractors/object.js @@ -0,0 +1,20 @@ +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]; + } +}; + +const obj = { + map: new Map([["a", 1], ["b", 2]]) +}; + +const { map: MapExtractor({ a, b }) } = obj; + +assertSame(a, 1); +assertSame(b, 2); diff --git a/graal-js/src/com.oracle.truffle.js.test/js/extractors/receiver.js b/graal-js/src/com.oracle.truffle.js.test/js/extractors/receiver.js new file mode 100644 index 00000000000..36e66b7fa63 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/js/extractors/receiver.js @@ -0,0 +1,20 @@ +load('../assert.js'); + +class C { + #f; + constructor(f) { + this.#f = f; + } + extractor = { + [Symbol.customMatcher](subject, _kind, receiver) { + return [receiver.#f(subject)]; + } + }; +} + +const obj = new C(data => data.toUpperCase() + "1234"); +const subject = "data"; + +const obj.extractor(x) = subject; + +assertSame(x, "DATA1234"); diff --git a/graal-js/src/com.oracle.truffle.js.test/js/extractors/regexp.js b/graal-js/src/com.oracle.truffle.js.test/js/extractors/regexp.js new file mode 100644 index 00000000000..8a05cb2d271 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/js/extractors/regexp.js @@ -0,0 +1,50 @@ +load('../assert.js'); + +// potentially built-in 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(?