-
Notifications
You must be signed in to change notification settings - Fork 201
Draft: Extractors proposal implementation #924
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
final Expression binding = conditionalExpression(true, yield, await, CoverExpressionError.DENY); | ||
|
||
final boolean isExtracting = binding instanceof CallNode; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do you have a proposed change here? not sure why it seems unnecessary, and don't know why extractors being nested is relevant There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe I'm missing something but it seems you could just use
Unless isExtracting is really supposed to be shallow, it won't be true for extractors nested inside a destructuring pattern. I just don't understand why you'd want such a check? |
||
final boolean isDestructuring = !(binding instanceof IdentNode) && !isExtracting; | ||
if (isDestructuring) { | ||
final int finalVarFlags = varFlags | VarNode.IS_DESTRUCTURING; | ||
verifyDestructuringBindingPattern(binding, new Consumer<IdentNode>() { | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. seems like it could be moved to |
||
@Override | ||
public boolean enterCallNode(CallNode callNode) { | ||
if (callNode.isParenthesized()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This check needs to be stricter; not every non-parenthesized |
||
throw error(AbstractParser.message(MSG_INVALID_LVALUE), callNode.getToken()); | ||
} | ||
for (final var arg : callNode.getArgs()) { | ||
if (arg instanceof IdentNode) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could probably just use |
||
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)); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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<Supplier<JavaScriptNode>, UnaryOperator<JavaScriptNode>> 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<Supplier<JavaScriptNode>, UnaryOperator<JavaScriptNode>> 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<Expression> elementExpressions = arrayLiteralNode.getElementExpressions(); | ||
|
||
return this.transformDestructuringArrayAssignment(elementExpressions, getIterator, valueTempVar, initializationAssignment); | ||
} | ||
|
||
private JavaScriptNode transformDestructuringArrayAssignment(List<Expression> 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please wrap this in a |
||
|
||
final var args = fakeCallNode.getArgs(); | ||
VarRef valueTempVar = environment.createTempVar(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. my understanding is that this is not the case, no. it's just a leftover from when i factored out the destructuring code, will remove it. |
||
return this.transformDestructuringArrayAssignment(args, invokeCustomMatcherOrThrowNode, valueTempVar, initializationAssignment); | ||
} | ||
|
||
private JavaScriptNode transformDestructuringObjectAssignment(Expression lhsExpression, JavaScriptNode assignedValue, boolean initializationAssignment) { | ||
ObjectNode objectLiteralNode = (ObjectNode) lhsExpression; | ||
List<PropertyNode> propertyExpressions = objectLiteralNode.getElements(); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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()); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this should be merged into
isDestructuringLhs
, or are there any cases where you don't want to accept both?In any case, we need to add a feature flag that enables extractors (disabled by default, enabled via an option).