From e9592d75d8b68599457cce6aea302fc6a78d0023 Mon Sep 17 00:00:00 2001 From: Andrea Bergia Date: Fri, 25 Oct 2024 07:04:37 +0200 Subject: [PATCH] Implement optional chaining for function call (#1702) Implements optional chaining for function calls, as well as the "eval" case: All cases are handled: ```js f?.() a.b?.() a[0]?.() a.__parent__() ``` --- .../org/mozilla/javascript/CodeGenerator.java | 82 ++++++- .../org/mozilla/javascript/IRFactory.java | 3 + .../java/org/mozilla/javascript/Icode.java | 21 +- .../org/mozilla/javascript/Interpreter.java | 69 +++++- .../java/org/mozilla/javascript/Parser.java | 50 +++-- .../org/mozilla/javascript/ScriptRuntime.java | 105 ++++++++- .../mozilla/javascript/ast/FunctionCall.java | 14 ++ .../javascript/optimizer/BodyCodegen.java | 200 +++++++++++++----- .../javascript/optimizer/Bootstrapper.java | 10 + .../javascript/optimizer/DefaultLinker.java | 16 ++ .../javascript/optimizer/OptRuntime.java | 41 +++- .../javascript/optimizer/RhinoOperation.java | 1 + .../tests/OptionalChainingOperatorTest.java | 131 ++++++++++++ tests/testsrc/test262.properties | 5 +- 14 files changed, 657 insertions(+), 91 deletions(-) diff --git a/rhino/src/main/java/org/mozilla/javascript/CodeGenerator.java b/rhino/src/main/java/org/mozilla/javascript/CodeGenerator.java index 99d62483b5..fe08849854 100644 --- a/rhino/src/main/java/org/mozilla/javascript/CodeGenerator.java +++ b/rhino/src/main/java/org/mozilla/javascript/CodeGenerator.java @@ -602,10 +602,17 @@ private void visitExpression(Node node, int contextFlags) { case Token.CALL: case Token.NEW: { + boolean isOptionalChainingCall = + node.getIntProp(Node.OPTIONAL_CHAINING, 0) == 1; + CompleteOptionalCallJump completeOptionalCallJump = null; if (type == Token.NEW) { visitExpression(child, 0); } else { - generateCallFunAndThis(child); + completeOptionalCallJump = + generateCallFunAndThis(child, isOptionalChainingCall); + if (completeOptionalCallJump != null) { + resolveForwardGoto(completeOptionalCallJump.putArgsAndDoCallLabel); + } } int argCount = 0; while ((child = child.getNext()) != null) { @@ -643,6 +650,10 @@ private void visitExpression(Node node, int contextFlags) { if (argCount > itsData.itsMaxCalleeArgs) { itsData.itsMaxCalleeArgs = argCount; } + + if (completeOptionalCallJump != null) { + resolveForwardGoto(completeOptionalCallJump.afterLabel); + } } break; @@ -1148,7 +1159,8 @@ private void finishGetElemGeneration(Node child) { stackChange(-1); } - private void generateCallFunAndThis(Node left) { + private CompleteOptionalCallJump generateCallFunAndThis( + Node left, boolean isOptionalChainingCall) { // Generate code to place on stack function and thisObj int type = left.getType(); switch (type) { @@ -1156,8 +1168,14 @@ private void generateCallFunAndThis(Node left) { { String name = left.getString(); // stack: ... -> ... function thisObj - addStringOp(Icode_NAME_AND_THIS, name); - stackChange(2); + if (isOptionalChainingCall) { + addStringOp(Icode_NAME_AND_THIS_OPTIONAL, name); + stackChange(2); + return completeOptionalCallJump(); + } else { + addStringOp(Icode_NAME_AND_THIS, name); + stackChange(2); + } break; } case Token.GETPROP: @@ -1169,12 +1187,23 @@ private void generateCallFunAndThis(Node left) { if (type == Token.GETPROP) { String property = id.getString(); // stack: ... target -> ... function thisObj - addStringOp(Icode_PROP_AND_THIS, property); - stackChange(1); + if (isOptionalChainingCall) { + addStringOp(Icode_PROP_AND_THIS_OPTIONAL, property); + stackChange(1); + return completeOptionalCallJump(); + } else { + addStringOp(Icode_PROP_AND_THIS, property); + stackChange(1); + } } else { visitExpression(id, 0); // stack: ... target id -> ... function thisObj - addIcode(Icode_ELEM_AND_THIS); + if (isOptionalChainingCall) { + addIcode(Icode_ELEM_AND_THIS_OPTIONAL); + return completeOptionalCallJump(); + } else { + addIcode(Icode_ELEM_AND_THIS); + } } break; } @@ -1182,10 +1211,35 @@ private void generateCallFunAndThis(Node left) { // Including Token.GETVAR visitExpression(left, 0); // stack: ... value -> ... function thisObj - addIcode(Icode_VALUE_AND_THIS); - stackChange(1); + if (isOptionalChainingCall) { + addIcode(Icode_VALUE_AND_THIS_OPTIONAL); + stackChange(1); + return completeOptionalCallJump(); + } else { + addIcode(Icode_VALUE_AND_THIS); + stackChange(1); + } break; } + return null; + } + + private CompleteOptionalCallJump completeOptionalCallJump() { + // If it's null or undefined, pop undefined and skip the arguments and call + addIcode(Icode_DUP); + stackChange(1); + int putArgsAndDoCallLabel = iCodeTop; + addGotoOp(Icode.Icode_IF_NOT_NULL_UNDEF); + stackChange(-1); + + // Put undefined + addIcode(Icode_POP); + addIcode(Icode_POP); + addStringOp(Token.NAME, "undefined"); + int afterLabel = iCodeTop; + addGotoOp(Token.GOTO); + + return new CompleteOptionalCallJump(putArgsAndDoCallLabel, afterLabel); } private void visitIncDec(Node node, Node child) { @@ -1695,4 +1749,14 @@ private void releaseLocal(int localSlot) { --localTop; if (localSlot != localTop) Kit.codeBug(); } + + private static final class CompleteOptionalCallJump { + private final int putArgsAndDoCallLabel; + private final int afterLabel; + + public CompleteOptionalCallJump(int putArgsAndDoCallLabel, int afterLabel) { + this.putArgsAndDoCallLabel = putArgsAndDoCallLabel; + this.afterLabel = afterLabel; + } + } } diff --git a/rhino/src/main/java/org/mozilla/javascript/IRFactory.java b/rhino/src/main/java/org/mozilla/javascript/IRFactory.java index ded26a4bd1..3d0ec068e5 100644 --- a/rhino/src/main/java/org/mozilla/javascript/IRFactory.java +++ b/rhino/src/main/java/org/mozilla/javascript/IRFactory.java @@ -661,6 +661,9 @@ private Node transformFunctionCall(FunctionCall node) { AstNode arg = args.get(i); call.addChildToBack(transform(arg)); } + if (node.isOptionalCall()) { + call.putIntProp(Node.OPTIONAL_CHAINING, 1); + } return call; } diff --git a/rhino/src/main/java/org/mozilla/javascript/Icode.java b/rhino/src/main/java/org/mozilla/javascript/Icode.java index b5a288cf32..1d79a96030 100644 --- a/rhino/src/main/java/org/mozilla/javascript/Icode.java +++ b/rhino/src/main/java/org/mozilla/javascript/Icode.java @@ -46,16 +46,21 @@ abstract class Icode { Icode_PROP_AND_THIS = Icode_NAME_AND_THIS - 1, Icode_ELEM_AND_THIS = Icode_PROP_AND_THIS - 1, Icode_VALUE_AND_THIS = Icode_ELEM_AND_THIS - 1, + Icode_NAME_AND_THIS_OPTIONAL = Icode_VALUE_AND_THIS - 1, + Icode_PROP_AND_THIS_OPTIONAL = Icode_NAME_AND_THIS_OPTIONAL - 1, + Icode_ELEM_AND_THIS_OPTIONAL = Icode_PROP_AND_THIS_OPTIONAL - 1, + Icode_VALUE_AND_THIS_OPTIONAL = Icode_ELEM_AND_THIS_OPTIONAL - 1, // Create closure object for nested functions - Icode_CLOSURE_EXPR = Icode_VALUE_AND_THIS - 1, + Icode_CLOSURE_EXPR = Icode_VALUE_AND_THIS_OPTIONAL - 1, Icode_CLOSURE_STMT = Icode_CLOSURE_EXPR - 1, // Special calls Icode_CALLSPECIAL = Icode_CLOSURE_STMT - 1, + Icode_CALLSPECIAL_OPTIONAL = Icode_CALLSPECIAL - 1, // To return undefined value - Icode_RETUNDEF = Icode_CALLSPECIAL - 1, + Icode_RETUNDEF = Icode_CALLSPECIAL_OPTIONAL - 1, // Exception handling implementation Icode_GOSUB = Icode_RETUNDEF - 1, @@ -163,6 +168,8 @@ static String bytecodeName(int bytecode) { } switch (bytecode) { + case Icode_DELNAME: + return "DELNAME"; case Icode_DUP: return "DUP"; case Icode_DUP2: @@ -197,12 +204,22 @@ static String bytecodeName(int bytecode) { return "ELEM_AND_THIS"; case Icode_VALUE_AND_THIS: return "VALUE_AND_THIS"; + case Icode_NAME_AND_THIS_OPTIONAL: + return "NAME_AND_THIS_OPTIONAL"; + case Icode_PROP_AND_THIS_OPTIONAL: + return "PROP_AND_THIS_OPTIONAL"; + case Icode_ELEM_AND_THIS_OPTIONAL: + return "ELEM_AND_THIS_OPTIONAL"; + case Icode_VALUE_AND_THIS_OPTIONAL: + return "VALUE_AND_THIS_OPTIONAL"; case Icode_CLOSURE_EXPR: return "CLOSURE_EXPR"; case Icode_CLOSURE_STMT: return "CLOSURE_STMT"; case Icode_CALLSPECIAL: return "CALLSPECIAL"; + case Icode_CALLSPECIAL_OPTIONAL: + return "CALLSPECIAL_OPTIONAL"; case Icode_RETUNDEF: return "RETUNDEF"; case Icode_GOSUB: diff --git a/rhino/src/main/java/org/mozilla/javascript/Interpreter.java b/rhino/src/main/java/org/mozilla/javascript/Interpreter.java index d1bc7ca67f..52a28b7f34 100644 --- a/rhino/src/main/java/org/mozilla/javascript/Interpreter.java +++ b/rhino/src/main/java/org/mozilla/javascript/Interpreter.java @@ -573,6 +573,7 @@ static void dumpICode(InterpreterData idata) { } case Icode_CALLSPECIAL: + case Icode_CALLSPECIAL_OPTIONAL: { int callType = iCode[pc] & 0xFF; boolean isNew = (iCode[pc + 1] != 0); @@ -830,6 +831,7 @@ private static int bytecodeSpan(int bytecode) { return 1 + 2; case Icode_CALLSPECIAL: + case Icode_CALLSPECIAL_OPTIONAL: // call type // is new // line number @@ -1742,6 +1744,15 @@ private static Object interpretLoop(Context cx, CallFrame frame, Object throwabl ++stackTop; stack[stackTop] = ScriptRuntime.lastStoredScriptable(cx); continue Loop; + case Icode_NAME_AND_THIS_OPTIONAL: + // stringReg: name + ++stackTop; + stack[stackTop] = + ScriptRuntime.getNameFunctionAndThisOptional( + stringReg, cx, frame.scope); + ++stackTop; + stack[stackTop] = ScriptRuntime.lastStoredScriptable(cx); + continue Loop; case Icode_PROP_AND_THIS: { Object obj = stack[stackTop]; @@ -1755,6 +1766,19 @@ private static Object interpretLoop(Context cx, CallFrame frame, Object throwabl stack[stackTop] = ScriptRuntime.lastStoredScriptable(cx); continue Loop; } + case Icode_PROP_AND_THIS_OPTIONAL: + { + Object obj = stack[stackTop]; + if (obj == DBL_MRK) + obj = ScriptRuntime.wrapNumber(sDbl[stackTop]); + // stringReg: property + stack[stackTop] = + ScriptRuntime.getPropFunctionAndThisOptional( + obj, stringReg, cx, frame.scope); + ++stackTop; + stack[stackTop] = ScriptRuntime.lastStoredScriptable(cx); + continue Loop; + } case Icode_ELEM_AND_THIS: { Object obj = stack[stackTop - 1]; @@ -1769,6 +1793,20 @@ private static Object interpretLoop(Context cx, CallFrame frame, Object throwabl stack[stackTop] = ScriptRuntime.lastStoredScriptable(cx); continue Loop; } + case Icode_ELEM_AND_THIS_OPTIONAL: + { + Object obj = stack[stackTop - 1]; + if (obj == DBL_MRK) + obj = ScriptRuntime.wrapNumber(sDbl[stackTop - 1]); + Object id = stack[stackTop]; + if (id == DBL_MRK) + id = ScriptRuntime.wrapNumber(sDbl[stackTop]); + stack[stackTop - 1] = + ScriptRuntime.getElemFunctionAndThisOptional( + obj, id, cx, frame.scope); + stack[stackTop] = ScriptRuntime.lastStoredScriptable(cx); + continue Loop; + } case Icode_VALUE_AND_THIS: { Object value = stack[stackTop]; @@ -1780,6 +1818,18 @@ private static Object interpretLoop(Context cx, CallFrame frame, Object throwabl stack[stackTop] = ScriptRuntime.lastStoredScriptable(cx); continue Loop; } + case Icode_VALUE_AND_THIS_OPTIONAL: + { + Object value = stack[stackTop]; + if (value == DBL_MRK) + value = ScriptRuntime.wrapNumber(sDbl[stackTop]); + stack[stackTop] = + ScriptRuntime.getValueFunctionAndThisOptional( + value, cx); + ++stackTop; + stack[stackTop] = ScriptRuntime.lastStoredScriptable(cx); + continue Loop; + } case Icode_CALLSPECIAL: { if (instructionCounting) { @@ -1788,7 +1838,18 @@ private static Object interpretLoop(Context cx, CallFrame frame, Object throwabl stackTop = doCallSpecial( cx, frame, stack, sDbl, stackTop, iCode, - indexReg); + indexReg, false); + continue Loop; + } + case Icode_CALLSPECIAL_OPTIONAL: + { + if (instructionCounting) { + cx.instructionCount += INVOCATION_COST; + } + stackTop = + doCallSpecial( + cx, frame, stack, sDbl, stackTop, iCode, + indexReg, true); continue Loop; } case Token.CALL: @@ -2968,7 +3029,8 @@ private static int doCallSpecial( double[] sDbl, int stackTop, byte[] iCode, - int indexReg) { + int indexReg, + boolean isOptionalChainingCall) { int callType = iCode[frame.pc] & 0xFF; boolean isNew = (iCode[frame.pc + 1] != 0); int sourceLine = getIndex(iCode, frame.pc + 2); @@ -3002,7 +3064,8 @@ private static int doCallSpecial( frame.thisObj, callType, frame.idata.itsSourceFile, - sourceLine); + sourceLine, + isOptionalChainingCall); } frame.pc += 4; return stackTop; diff --git a/rhino/src/main/java/org/mozilla/javascript/Parser.java b/rhino/src/main/java/org/mozilla/javascript/Parser.java index 74bab374aa..422d4b666d 100644 --- a/rhino/src/main/java/org/mozilla/javascript/Parser.java +++ b/rhino/src/main/java/org/mozilla/javascript/Parser.java @@ -2936,21 +2936,7 @@ private AstNode memberExprTail(boolean allowCallSyntax, AstNode pn) throws IOExc break tailLoop; } lineno = ts.lineno; - consumeToken(); - checkCallRequiresActivation(pn); - FunctionCall f = new FunctionCall(pos); - f.setTarget(pn); - // Assign the line number for the function call to where - // the paren appeared, not where the name expression started. - f.setLineno(lineno); - f.setLp(ts.tokenBeg - pos); - List args = argumentList(); - if (args != null && args.size() > ARGC_LIMIT) - reportError("msg.too.many.function.args"); - f.setArguments(args); - f.setRp(ts.tokenBeg - pos); - f.setLength(ts.tokenEnd - pos); - pn = f; + pn = makeFunctionCall(pn, pos, lineno, isOptionalChain); break; case Token.COMMENT: // Ignoring all the comments, because previous statement may not be terminated @@ -2973,6 +2959,27 @@ private AstNode memberExprTail(boolean allowCallSyntax, AstNode pn) throws IOExc return pn; } + private FunctionCall makeFunctionCall(AstNode pn, int pos, int lineno, boolean isOptionalChain) + throws IOException { + consumeToken(); + checkCallRequiresActivation(pn); + FunctionCall f = new FunctionCall(pos); + f.setTarget(pn); + // Assign the line number for the function call to where + // the paren appeared, not where the name expression started. + f.setLineno(lineno); + f.setLp(ts.tokenBeg - pos); + List args = argumentList(); + if (args != null && args.size() > ARGC_LIMIT) reportError("msg.too.many.function.args"); + f.setArguments(args); + f.setRp(ts.tokenBeg - pos); + f.setLength(ts.tokenEnd - pos); + if (isOptionalChain) { + f.markIsOptionalCall(); + } + return f; + } + private AstNode taggedTemplateLiteral(AstNode pn) throws IOException { AstNode templateLiteral = templateLiteral(true); TaggedTemplateLiteral tagged = new TaggedTemplateLiteral(); @@ -3062,6 +3069,15 @@ private AstNode propertyAccess(int tt, AstNode pn, boolean isOptionalChain) thro return makeErrorNode(); } + case Token.LP: + if (tt == Token.QUESTION_DOT) { + // a function call such as f?.() + return makeFunctionCall(pn, pn.getPosition(), lineno, isOptionalChain); + } else { + reportError("msg.no.name.after.dot"); + return makeErrorNode(); + } + default: if (compilerEnv.isReservedKeywordAsIdentifier()) { // allow keywords as property names, e.g. ({if: 1}) @@ -3079,7 +3095,9 @@ private AstNode propertyAccess(int tt, AstNode pn, boolean isOptionalChain) thro boolean xml = ref instanceof XmlRef; InfixExpression result = xml ? new XmlMemberGet() : new PropertyGet(); if (xml && tt == Token.DOT) result.setType(Token.DOT); - if (isOptionalChain) result.setType(Token.QUESTION_DOT); + if (isOptionalChain) { + result.setType(Token.QUESTION_DOT); + } int pos = pn.getPosition(); result.setPosition(pos); result.setLength(getNodeEnd(ref) - pos); diff --git a/rhino/src/main/java/org/mozilla/javascript/ScriptRuntime.java b/rhino/src/main/java/org/mozilla/javascript/ScriptRuntime.java index 14d725b321..0b5389d7f5 100644 --- a/rhino/src/main/java/org/mozilla/javascript/ScriptRuntime.java +++ b/rhino/src/main/java/org/mozilla/javascript/ScriptRuntime.java @@ -2044,7 +2044,7 @@ public static Object name(Context cx, Scriptable scope, String name) { return result; } - return nameOrFunction(cx, scope, parent, name, false); + return nameOrFunction(cx, scope, parent, name, false, false); } private static Object nameOrFunction( @@ -2052,7 +2052,8 @@ private static Object nameOrFunction( Scriptable scope, Scriptable parentScope, String name, - boolean asFunctionCall) { + boolean asFunctionCall, + boolean isOptionalChainingCall) { Object result; Scriptable thisObj = scope; // It is used only if asFunctionCall==true. @@ -2122,6 +2123,13 @@ private static Object nameOrFunction( if (asFunctionCall) { if (!(result instanceof Callable)) { + if (isOptionalChainingCall + && (result == Scriptable.NOT_FOUND + || result == null + || Undefined.isUndefined(result))) { + storeScriptable(cx, null); + return null; + } throw notFunctionError(result, name); } storeScriptable(cx, thisObj); @@ -2568,10 +2576,28 @@ public static boolean loadFromIterable( * caller must call ScriptRuntime.lastStoredScriptable() immediately after calling this method. */ public static Callable getNameFunctionAndThis(String name, Context cx, Scriptable scope) { + return getNameFunctionAndThisInner(name, cx, scope, false); + } + + public static Callable getNameFunctionAndThisOptional( + String name, Context cx, Scriptable scope) { + return getNameFunctionAndThisInner(name, cx, scope, true); + } + + private static Callable getNameFunctionAndThisInner( + String name, Context cx, Scriptable scope, boolean isOptionalChainingCall) { Scriptable parent = scope.getParentScope(); if (parent == null) { Object result = topScopeName(cx, scope, name); if (!(result instanceof Callable)) { + if (isOptionalChainingCall + && (result == Scriptable.NOT_FOUND + || result == null + || Undefined.isUndefined(result))) { + storeScriptable(cx, null); + return null; + } + if (result == Scriptable.NOT_FOUND) { throw notFoundError(scope, name); } @@ -2583,7 +2609,7 @@ public static Callable getNameFunctionAndThis(String name, Context cx, Scriptabl } // name will call storeScriptable(cx, thisObj); - return (Callable) nameOrFunction(cx, scope, parent, name, true); + return (Callable) nameOrFunction(cx, scope, parent, name, true, isOptionalChainingCall); } /** @@ -2607,6 +2633,16 @@ public static Callable getElemFunctionAndThis(Object obj, Object elem, Context c */ public static Callable getElemFunctionAndThis( Object obj, Object elem, Context cx, Scriptable scope) { + return getElemFunctionAndThisInner(obj, elem, cx, scope, false); + } + + public static Callable getElemFunctionAndThisOptional( + Object obj, Object elem, Context cx, Scriptable scope) { + return getElemFunctionAndThisInner(obj, elem, cx, scope, true); + } + + private static Callable getElemFunctionAndThisInner( + Object obj, Object elem, Context cx, Scriptable scope, boolean isOptionalChainingCall) { Scriptable thisObj; Object value; @@ -2632,6 +2668,13 @@ public static Callable getElemFunctionAndThis( } if (!(value instanceof Callable)) { + if (isOptionalChainingCall + && (value == Scriptable.NOT_FOUND + || value == null + || Undefined.isUndefined(value))) { + storeScriptable(cx, null); + return null; + } throw notFunctionError(value, elem); } @@ -2661,13 +2704,35 @@ public static Callable getPropFunctionAndThis(Object obj, String property, Conte */ public static Callable getPropFunctionAndThis( Object obj, String property, Context cx, Scriptable scope) { + return getPropFunctionAndThisInner(obj, property, cx, scope, false); + } + + public static Callable getPropFunctionAndThisOptional( + Object obj, String property, Context cx, Scriptable scope) { + return getPropFunctionAndThisInner(obj, property, cx, scope, true); + } + + private static Callable getPropFunctionAndThisInner( + Object obj, + String property, + Context cx, + Scriptable scope, + boolean isOptionalChainingCall) { Scriptable thisObj = toObjectOrNull(cx, obj, scope); - return getPropFunctionAndThisHelper(obj, property, cx, thisObj); + return getPropFunctionAndThisHelper(obj, property, cx, thisObj, isOptionalChainingCall); } private static Callable getPropFunctionAndThisHelper( - Object obj, String property, Context cx, Scriptable thisObj) { + Object obj, + String property, + Context cx, + Scriptable thisObj, + boolean isOptionalChainingCall) { if (thisObj == null) { + if (isOptionalChainingCall) { + storeScriptable(cx, null); + return null; + } throw undefCallError(obj, property); } @@ -2679,6 +2744,13 @@ private static Callable getPropFunctionAndThisHelper( } if (!(value instanceof Callable)) { + if (isOptionalChainingCall + && (value == Scriptable.NOT_FOUND + || value == null + || Undefined.isUndefined(value))) { + storeScriptable(cx, null); + return null; + } throw notFunctionError(thisObj, value, property); } @@ -2693,7 +2765,23 @@ private static Callable getPropFunctionAndThisHelper( * ScriptRuntime.lastStoredScriptable() immediately after calling this method. */ public static Callable getValueFunctionAndThis(Object value, Context cx) { + return getValueFunctionAndThisInner(value, cx, false); + } + + public static Callable getValueFunctionAndThisOptional(Object value, Context cx) { + return getValueFunctionAndThisInner(value, cx, true); + } + + private static Callable getValueFunctionAndThisInner( + Object value, Context cx, boolean isOptionalChainingCall) { if (!(value instanceof Callable)) { + if (isOptionalChainingCall + && (value == Scriptable.NOT_FOUND + || value == null + || Undefined.isUndefined(value))) { + storeScriptable(cx, null); + return null; + } throw notFunctionError(value); } @@ -2785,7 +2873,12 @@ public static Object callSpecial( Scriptable callerThis, int callType, String filename, - int lineNumber) { + int lineNumber, + boolean isOptionalChainingCall) { + if (fun == null && isOptionalChainingCall) { + return Undefined.instance; + } + if (callType == Node.SPECIALCALL_EVAL) { if (thisObj.getParentScope() == null && NativeGlobal.isEvalFunction(fun)) { return evalSpecial(cx, scope, callerThis, args, filename, lineNumber); diff --git a/rhino/src/main/java/org/mozilla/javascript/ast/FunctionCall.java b/rhino/src/main/java/org/mozilla/javascript/ast/FunctionCall.java index 6186e06ca6..d25efae079 100644 --- a/rhino/src/main/java/org/mozilla/javascript/ast/FunctionCall.java +++ b/rhino/src/main/java/org/mozilla/javascript/ast/FunctionCall.java @@ -20,6 +20,7 @@ public class FunctionCall extends AstNode { protected List arguments; protected int lp = -1; protected int rp = -1; + protected boolean optionalCall = false; { type = Token.CALL; @@ -123,11 +124,24 @@ public void setParens(int lp, int rp) { this.rp = rp; } + /** Marks that the call is preceded by the optional chaining operator ?. */ + public void markIsOptionalCall() { + this.optionalCall = true; + } + + /** Returns whether the call is preceded by the optional chaining operator ?. */ + public boolean isOptionalCall() { + return optionalCall; + } + @Override public String toSource(int depth) { StringBuilder sb = new StringBuilder(); sb.append(makeIndent(depth)); sb.append(target.toSource(0)); + if (optionalCall) { + sb.append("?."); + } sb.append("("); if (arguments != null) { printList(arguments, sb); diff --git a/rhino/src/main/java/org/mozilla/javascript/optimizer/BodyCodegen.java b/rhino/src/main/java/org/mozilla/javascript/optimizer/BodyCodegen.java index bde030169f..8957bc58d5 100644 --- a/rhino/src/main/java/org/mozilla/javascript/optimizer/BodyCodegen.java +++ b/rhino/src/main/java/org/mozilla/javascript/optimizer/BodyCodegen.java @@ -1013,6 +1013,7 @@ private void generateExpression(Node node, Node parent) { case Token.REF_CALL: generateFunctionAndThisObj(child, node); + pushThisFromLastScriptable(); // stack: ... functionObj thisObj child = child.getNext(); generateCallArgArray(node, child, false); @@ -2326,6 +2327,7 @@ private void visitSpecialCall(Node node, int type, int specialType, Node child) // stack: ... cx functionObj } else { generateFunctionAndThisObj(child, node); + pushThisFromLastScriptable(); // stack: ... cx functionObj thisObj } child = child.getNext(); @@ -2359,13 +2361,16 @@ private void visitSpecialCall(Node node, int type, int specialType, Node child) + "Lorg/mozilla/javascript/Scriptable;" + "I" // call type + "Ljava/lang/String;I" // filename, linenumber + + "Z" // isOptionalChainingCall + ")Ljava/lang/Object;"; + boolean isOptionalChainingCall = node.getIntProp(Node.OPTIONAL_CHAINING, 0) == 1; cfw.addALoad(variableObjectLocal); cfw.addALoad(thisObjLocal); cfw.addPush(specialType); String sourceName = scriptOrFn.getSourceName(); cfw.addPush(sourceName == null ? "" : sourceName); cfw.addPush(itsLineNumber); + cfw.addPush(isOptionalChainingCall); } addOptRuntimeInvoke(methodName, callSignature); @@ -2376,16 +2381,18 @@ private void visitStandardCall(Node node, Node child) { Node firstArgChild = child.getNext(); int childType = child.getType(); + boolean isOptionalChainingCall = node.getIntProp(Node.OPTIONAL_CHAINING, 0) == 1; String methodName; String signature; + Integer afterLabel = null; if (firstArgChild == null) { if (childType == Token.NAME) { // name() call String name = child.getString(); cfw.addPush(name); - methodName = "callName0"; + methodName = isOptionalChainingCall ? "callName0Optional" : "callName0"; signature = "(Ljava/lang/String;" + "Lorg/mozilla/javascript/Context;" @@ -2398,7 +2405,7 @@ private void visitStandardCall(Node node, Node child) { Node id = propTarget.getNext(); String property = id.getString(); cfw.addPush(property); - methodName = "callProp0"; + methodName = isOptionalChainingCall ? "callProp0Optional" : "callProp0"; signature = "(Ljava/lang/Object;" + "Ljava/lang/String;" @@ -2409,7 +2416,8 @@ private void visitStandardCall(Node node, Node child) { throw Kit.codeBug(); } else { generateFunctionAndThisObj(child, node); - methodName = "call0"; + pushThisFromLastScriptable(); + methodName = isOptionalChainingCall ? "call0Optional" : "call0"; signature = "(Lorg/mozilla/javascript/Callable;" + "Lorg/mozilla/javascript/Scriptable;" @@ -2424,15 +2432,59 @@ private void visitStandardCall(Node node, Node child) { // is not affected by arguments evaluation and currently // there are no checks for it String name = child.getString(); - generateCallArgArray(node, firstArgChild, false); - cfw.addPush(name); - methodName = "callName"; - signature = - "([Ljava/lang/Object;" - + "Ljava/lang/String;" - + "Lorg/mozilla/javascript/Context;" - + "Lorg/mozilla/javascript/Scriptable;" - + ")Ljava/lang/Object;"; + if (isOptionalChainingCall) { // name?.() + // eval name and this and push name on stack + cfw.addPush(name); + cfw.addALoad(contextLocal); + cfw.addALoad(variableObjectLocal); + addScriptRuntimeInvoke( + "getNameFunctionAndThisOptional", + "" + + "(" + + "Ljava/lang/String;" + + "Lorg/mozilla/javascript/Context;" + + "Lorg/mozilla/javascript/Scriptable;" + + ")Lorg/mozilla/javascript/Callable;"); + + // jump to afterLabel is name is not null and not undefined + afterLabel = cfw.acquireLabel(); + int doCallLabel = cfw.acquireLabel(); + cfw.add(ByteCode.DUP); + addOptRuntimeInvoke("isNullOrUndefined", "(Ljava/lang/Object;)Z"); + cfw.add(ByteCode.IFEQ, doCallLabel); + + // push undefined and jump to end + cfw.add(ByteCode.POP); + cfw.add( + ByteCode.GETSTATIC, + "org/mozilla/javascript/Undefined", + "instance", + "Ljava/lang/Object;"); + cfw.add(ByteCode.GOTO, afterLabel); + + // push this, arguments, and do call + cfw.markLabel(doCallLabel); + pushThisFromLastScriptable(); + methodName = "callN"; + generateCallArgArray(node, firstArgChild, false); + signature = + "(Lorg/mozilla/javascript/Callable;" + + "Lorg/mozilla/javascript/Scriptable;" + + "[Ljava/lang/Object;" + + "Lorg/mozilla/javascript/Context;" + + "Lorg/mozilla/javascript/Scriptable;" + + ")Ljava/lang/Object;"; + } else { + generateCallArgArray(node, firstArgChild, false); + cfw.addPush(name); + methodName = "callName"; + signature = + "([Ljava/lang/Object;" + + "Ljava/lang/String;" + + "Lorg/mozilla/javascript/Context;" + + "Lorg/mozilla/javascript/Scriptable;" + + ")Ljava/lang/Object;"; + } } else { int argCount = 0; for (Node arg = firstArgChild; arg != null; arg = arg.getNext()) { @@ -2440,6 +2492,29 @@ private void visitStandardCall(Node node, Node child) { } generateFunctionAndThisObj(child, node); // stack: ... functionObj thisObj + + if (isOptionalChainingCall) { + // jump to afterLabel is name is not null and not undefined + afterLabel = cfw.acquireLabel(); + int doCallLabel = cfw.acquireLabel(); + cfw.add(ByteCode.DUP); + addOptRuntimeInvoke("isNullOrUndefined", "(Ljava/lang/Object;)Z"); + cfw.add(ByteCode.IFEQ, doCallLabel); + + // push undefined and jump to end + cfw.add(ByteCode.POP); + cfw.add( + ByteCode.GETSTATIC, + "org/mozilla/javascript/Undefined", + "instance", + "Ljava/lang/Object;"); + cfw.add(ByteCode.GOTO, afterLabel); + + cfw.markLabel(doCallLabel); + cfw.add(ByteCode.CHECKCAST, "org/mozilla/javascript/Callable"); + } + + pushThisFromLastScriptable(); if (argCount == 1) { generateExpression(firstArgChild, node); methodName = "call1"; @@ -2450,34 +2525,47 @@ private void visitStandardCall(Node node, Node child) { + "Lorg/mozilla/javascript/Context;" + "Lorg/mozilla/javascript/Scriptable;" + ")Ljava/lang/Object;"; - } else if (argCount == 2) { - generateExpression(firstArgChild, node); - generateExpression(firstArgChild.getNext(), node); - methodName = "call2"; - signature = - "(Lorg/mozilla/javascript/Callable;" - + "Lorg/mozilla/javascript/Scriptable;" - + "Ljava/lang/Object;" - + "Ljava/lang/Object;" - + "Lorg/mozilla/javascript/Context;" - + "Lorg/mozilla/javascript/Scriptable;" - + ")Ljava/lang/Object;"; } else { - generateCallArgArray(node, firstArgChild, false); - methodName = "callN"; - signature = - "(Lorg/mozilla/javascript/Callable;" - + "Lorg/mozilla/javascript/Scriptable;" - + "[Ljava/lang/Object;" - + "Lorg/mozilla/javascript/Context;" - + "Lorg/mozilla/javascript/Scriptable;" - + ")Ljava/lang/Object;"; + if (argCount == 2) { + generateExpression(firstArgChild, node); + generateExpression(firstArgChild.getNext(), node); + methodName = "call2"; + signature = + "(Lorg/mozilla/javascript/Callable;" + + "Lorg/mozilla/javascript/Scriptable;" + + "Ljava/lang/Object;" + + "Ljava/lang/Object;" + + "Lorg/mozilla/javascript/Context;" + + "Lorg/mozilla/javascript/Scriptable;" + + ")Ljava/lang/Object;"; + } else { + generateCallArgArray(node, firstArgChild, false); + methodName = "callN"; + signature = + "(Lorg/mozilla/javascript/Callable;" + + "Lorg/mozilla/javascript/Scriptable;" + + "[Ljava/lang/Object;" + + "Lorg/mozilla/javascript/Context;" + + "Lorg/mozilla/javascript/Scriptable;" + + ")Ljava/lang/Object;"; + } } } cfw.addALoad(contextLocal); cfw.addALoad(variableObjectLocal); addOptRuntimeInvoke(methodName, signature); + if (afterLabel != null) { + cfw.markLabel(afterLabel); + } + } + + private void pushThisFromLastScriptable() { + // Get thisObj prepared by get(Name|Prop|Elem|Value)FunctionAndThis + cfw.addALoad(contextLocal); + addScriptRuntimeInvoke( + "lastStoredScriptable", + "(Lorg/mozilla/javascript/Context;" + ")Lorg/mozilla/javascript/Scriptable;"); } private void visitStandardNew(Node node, Node child) { @@ -2509,6 +2597,7 @@ private void visitOptimizedCall(Node node, OptFunctionNode target, int type, Nod generateExpression(child, node); } else { generateFunctionAndThisObj(child, node); + pushThisFromLastScriptable(); thisObjLocal = getNewWordLocal(); cfw.addAStore(thisObjLocal); } @@ -2675,6 +2764,7 @@ private void generateCallArgArray(Node node, Node argChild, boolean directCall) private void generateFunctionAndThisObj(Node node, Node parent) { // Place on stack (function object, function this) pair int type = node.getType(); + boolean isOptionalChainingCall = parent.getIntProp(Node.OPTIONAL_CHAINING, 0) == 1; switch (node.getType()) { case Token.GETPROPNOWARN: throw Kit.codeBug(); @@ -2689,14 +2779,22 @@ private void generateFunctionAndThisObj(Node node, Node parent) { String property = id.getString(); cfw.addALoad(contextLocal); cfw.addALoad(variableObjectLocal); - addDynamicInvoke("PROP:GETWITHTHIS:" + property, Signatures.PROP_GET_THIS); + String propAccessType = + isOptionalChainingCall ? "GETWITHTHISOPTIONAL" : "GETWITHTHIS"; + addDynamicInvoke( + "PROP:" + propAccessType + ":" + property, + Signatures.PROP_GET_THIS); } else { generateExpression(id, node); // id if (node.getIntProp(Node.ISNUMBER_PROP, -1) != -1) addDoubleWrap(); cfw.addALoad(contextLocal); cfw.addALoad(variableObjectLocal); + String name = + isOptionalChainingCall + ? "getElemFunctionAndThisOptional" + : "getElemFunctionAndThis"; addScriptRuntimeInvoke( - "getElemFunctionAndThis", + name, "(Ljava/lang/Object;" + "Ljava/lang/Object;" + "Lorg/mozilla/javascript/Context;" @@ -2711,25 +2809,29 @@ private void generateFunctionAndThisObj(Node node, Node parent) { String name = node.getString(); cfw.addALoad(variableObjectLocal); cfw.addALoad(contextLocal); - addDynamicInvoke("NAME:GETWITHTHIS:" + name, Signatures.NAME_GET_THIS); + String propAccessType = + isOptionalChainingCall ? "GETWITHTHISOPTIONAL" : "GETWITHTHIS"; + addDynamicInvoke( + "NAME:" + propAccessType + ":" + name, Signatures.NAME_GET_THIS); break; } default: // including GETVAR - generateExpression(node, parent); - cfw.addALoad(contextLocal); - addScriptRuntimeInvoke( - "getValueFunctionAndThis", - "(Ljava/lang/Object;" - + "Lorg/mozilla/javascript/Context;" - + ")Lorg/mozilla/javascript/Callable;"); - break; + { + generateExpression(node, parent); + cfw.addALoad(contextLocal); + String name = + isOptionalChainingCall + ? "getValueFunctionAndThisOptional" + : "getValueFunctionAndThis"; + addScriptRuntimeInvoke( + name, + "(Ljava/lang/Object;" + + "Lorg/mozilla/javascript/Context;" + + ")Lorg/mozilla/javascript/Callable;"); + break; + } } - // Get thisObj prepared by get(Name|Prop|Elem|Value)FunctionAndThis - cfw.addALoad(contextLocal); - addScriptRuntimeInvoke( - "lastStoredScriptable", - "(Lorg/mozilla/javascript/Context;" + ")Lorg/mozilla/javascript/Scriptable;"); } private void updateLineNumber(Node node) { diff --git a/rhino/src/main/java/org/mozilla/javascript/optimizer/Bootstrapper.java b/rhino/src/main/java/org/mozilla/javascript/optimizer/Bootstrapper.java index 5e67c7eb0b..d81a0180ad 100644 --- a/rhino/src/main/java/org/mozilla/javascript/optimizer/Bootstrapper.java +++ b/rhino/src/main/java/org/mozilla/javascript/optimizer/Bootstrapper.java @@ -75,6 +75,11 @@ private static Operation parseOperation(String name) throws NoSuchMethodExceptio return RhinoOperation.GETWITHTHIS .withNamespace(StandardNamespace.PROPERTY) .named(getNameSegment(tokens, name, 2)); + case "GETWITHTHISOPTIONAL": + // Similar to the above, but won't complain if prop is not found + return RhinoOperation.GETWITHTHISOPTIONAL + .withNamespace(StandardNamespace.PROPERTY) + .named(getNameSegment(tokens, name, 2)); case "GETELEMENT": // Get the value of an element from a property that is on the stack,\ // as if using "[]" notation. Could be a String, number, or Symbol @@ -111,6 +116,11 @@ private static Operation parseOperation(String name) throws NoSuchMethodExceptio return RhinoOperation.GETWITHTHIS .withNamespace(RhinoNamespace.NAME) .named(getNameSegment(tokens, name, 2)); + case "GETWITHTHISOPTIONAL": + // Similar to the above, but won't complain if prop is not found + return RhinoOperation.GETWITHTHISOPTIONAL + .withNamespace(RhinoNamespace.NAME) + .named(getNameSegment(tokens, name, 2)); case "SET": // Set an object in the context with a constant name return StandardOperation.SET diff --git a/rhino/src/main/java/org/mozilla/javascript/optimizer/DefaultLinker.java b/rhino/src/main/java/org/mozilla/javascript/optimizer/DefaultLinker.java index 46679d0392..1b3d48e5bd 100644 --- a/rhino/src/main/java/org/mozilla/javascript/optimizer/DefaultLinker.java +++ b/rhino/src/main/java/org/mozilla/javascript/optimizer/DefaultLinker.java @@ -101,6 +101,15 @@ private GuardedInvocation getPropertyInvocation( mh = bindStringParameter( lookup, mType, ScriptRuntime.class, "getPropFunctionAndThis", 1, name); + } else if (RhinoOperation.GETWITHTHISOPTIONAL.equals(op)) { + mh = + bindStringParameter( + lookup, + mType, + ScriptRuntime.class, + "getPropFunctionAndThisOptional", + 1, + name); } else if (StandardOperation.SET.equals(op)) { mh = bindStringParameter(lookup, mType, ScriptRuntime.class, "setObjectProp", 1, name); } else if (RhinoOperation.GETELEMENT.equals(op)) { @@ -152,6 +161,13 @@ private GuardedInvocation getNameInvocation( mh = lookup.findStatic(ScriptRuntime.class, "getNameFunctionAndThis", tt); mh = MethodHandles.insertArguments(mh, 0, name); mh = MethodHandles.permuteArguments(mh, mType, 1, 0); + } else if (RhinoOperation.GETWITHTHISOPTIONAL.equals(op)) { + tt = + MethodType.methodType( + Callable.class, String.class, Context.class, Scriptable.class); + mh = lookup.findStatic(ScriptRuntime.class, "getNameFunctionAndThisOptional", tt); + mh = MethodHandles.insertArguments(mh, 0, name); + mh = MethodHandles.permuteArguments(mh, mType, 1, 0); } else if (StandardOperation.SET.equals(op)) { mh = bindStringParameter(lookup, mType, ScriptRuntime.class, "setName", 4, name); } else if (RhinoOperation.SETSTRICT.equals(op)) { diff --git a/rhino/src/main/java/org/mozilla/javascript/optimizer/OptRuntime.java b/rhino/src/main/java/org/mozilla/javascript/optimizer/OptRuntime.java index 2c3a5dd175..4a818d3d7c 100644 --- a/rhino/src/main/java/org/mozilla/javascript/optimizer/OptRuntime.java +++ b/rhino/src/main/java/org/mozilla/javascript/optimizer/OptRuntime.java @@ -29,6 +29,14 @@ public static Object call0(Callable fun, Scriptable thisObj, Context cx, Scripta return fun.call(cx, scope, thisObj, ScriptRuntime.emptyArgs); } + public static Object call0Optional( + Callable fun, Scriptable thisObj, Context cx, Scriptable scope) { + if (fun == null) { + return Undefined.instance; + } + return call0(fun, thisObj, cx, scope); + } + /** Implement ....(arg) call shrinking optimizer code. */ public static Object call1( Callable fun, Scriptable thisObj, Object arg0, Context cx, Scriptable scope) { @@ -66,6 +74,15 @@ public static Object callName0(String name, Context cx, Scriptable scope) { return f.call(cx, scope, thisObj, ScriptRuntime.emptyArgs); } + public static Object callName0Optional(String name, Context cx, Scriptable scope) { + Callable f = getNameFunctionAndThisOptional(name, cx, scope); + if (f == null) { + return Undefined.instance; + } + Scriptable thisObj = lastStoredScriptable(cx); + return f.call(cx, scope, thisObj, ScriptRuntime.emptyArgs); + } + /** Implement x.property() call shrinking optimizer code. */ public static Object callProp0(Object value, String property, Context cx, Scriptable scope) { Callable f = getPropFunctionAndThis(value, property, cx, scope); @@ -73,6 +90,16 @@ public static Object callProp0(Object value, String property, Context cx, Script return f.call(cx, scope, thisObj, ScriptRuntime.emptyArgs); } + public static Object callProp0Optional( + Object value, String property, Context cx, Scriptable scope) { + Callable f = getPropFunctionAndThisOptional(value, property, cx, scope); + if (f == null) { + return Undefined.instance; + } + Scriptable thisObj = lastStoredScriptable(cx); + return f.call(cx, scope, thisObj, ScriptRuntime.emptyArgs); + } + public static Object add(Object val1, double val2, Context cx) { if (val1 instanceof Double) { return ((Double) val1) + val2; @@ -131,9 +158,19 @@ public static Object callSpecial( Scriptable callerThis, int callType, String fileName, - int lineNumber) { + int lineNumber, + boolean isOptionalChainingCall) { return ScriptRuntime.callSpecial( - cx, fun, thisObj, args, scope, callerThis, callType, fileName, lineNumber); + cx, + fun, + thisObj, + args, + scope, + callerThis, + callType, + fileName, + lineNumber, + isOptionalChainingCall); } public static Object newObjectSpecial( diff --git a/rhino/src/main/java/org/mozilla/javascript/optimizer/RhinoOperation.java b/rhino/src/main/java/org/mozilla/javascript/optimizer/RhinoOperation.java index 63e9f7060a..f60ead750b 100644 --- a/rhino/src/main/java/org/mozilla/javascript/optimizer/RhinoOperation.java +++ b/rhino/src/main/java/org/mozilla/javascript/optimizer/RhinoOperation.java @@ -11,6 +11,7 @@ public enum RhinoOperation implements Operation { BIND, GETNOWARN, GETWITHTHIS, + GETWITHTHISOPTIONAL, GETELEMENT, GETINDEX, SETSTRICT, diff --git a/tests/src/test/java/org/mozilla/javascript/tests/OptionalChainingOperatorTest.java b/tests/src/test/java/org/mozilla/javascript/tests/OptionalChainingOperatorTest.java index 4c7ccf6ae0..16b915e9e0 100644 --- a/tests/src/test/java/org/mozilla/javascript/tests/OptionalChainingOperatorTest.java +++ b/tests/src/test/java/org/mozilla/javascript/tests/OptionalChainingOperatorTest.java @@ -101,6 +101,137 @@ public void shortCircuits() { Utils.assertWithAllOptimizationLevelsES6(Undefined.instance, "a = {}; a.b?.c.d.e"); } + @Test + public void standardFunctionCall() { + // Various combination of arguments for compiled mode, where we have special cases for 0, 1, + // and 2 args + + Utils.assertWithAllOptimizationLevelsES6(1, "function f() {return 1;} f?.()"); + Utils.assertWithAllOptimizationLevelsES6(Undefined.instance, "f = null; f?.()"); + Utils.assertWithAllOptimizationLevelsES6(Undefined.instance, "f = undefined; f?.()"); + + Utils.assertWithAllOptimizationLevelsES6(1, "function f(x) {return x;} f?.(1)"); + Utils.assertWithAllOptimizationLevelsES6(Undefined.instance, "f = null; f?.(1)"); + Utils.assertWithAllOptimizationLevelsES6(Undefined.instance, "f = undefined; f?.(1)"); + + Utils.assertWithAllOptimizationLevelsES6(2, "function f(x, y) {return y;} f?.(1, 2)"); + Utils.assertWithAllOptimizationLevelsES6(Undefined.instance, "f = null; f?.(1, 2)"); + Utils.assertWithAllOptimizationLevelsES6(Undefined.instance, "f = undefined; f?.(1, 2)"); + + Utils.assertWithAllOptimizationLevelsES6(3, "function f(x, y, z) {return z;} f?.(1, 2, 3)"); + Utils.assertWithAllOptimizationLevelsES6(Undefined.instance, "f = null; f?.(1, 2, 3)"); + Utils.assertWithAllOptimizationLevelsES6(Undefined.instance, "f = undefined; f?.(1, 2, 3)"); + } + + @Test + public void standardFunctionCallWithParentScope() { + // Needed because there are some special paths in ScriptRuntime when we have a parent scope. + // A "with" block is the easiest way to get one. + Utils.assertWithAllOptimizationLevelsES6( + 1, "function f(x) {return x;} x = {}; with (x) { f?.(1) }"); + Utils.assertWithAllOptimizationLevelsES6( + Undefined.instance, "x = {}; with (x) { f = null; f?.(1) }"); + Utils.assertWithAllOptimizationLevelsES6( + Undefined.instance, "x = {}; with (x) { f = undefined; f?.(1) }"); + } + + @Test + public void specialFunctionCall() { + Utils.assertWithAllOptimizationLevelsES6( + 1, "a = { __parent__: function(x) {return x;} }; a.__parent__?.(1)"); + Utils.assertWithAllOptimizationLevelsES6( + Undefined.instance, "a = { __parent__: null }; a.__parent__?.(1)"); + Utils.assertWithAllOptimizationLevelsES6( + Undefined.instance, "a = { __parent__: undefined }; a.__parent__?.(1)"); + } + + @Test + public void memberFunctionCall() { + Utils.assertWithAllOptimizationLevelsES6(1, "a = { f: function() {return 1;} }; a.f?.()"); + Utils.assertWithAllOptimizationLevelsES6(Undefined.instance, "a = {f: null}; a.f?.()"); + Utils.assertWithAllOptimizationLevelsES6( + Undefined.instance, "a = {f: undefined}; a.f?.(1)"); + Utils.assertWithAllOptimizationLevelsES6(Undefined.instance, "a = {}; a.f?.()"); + + Utils.assertWithAllOptimizationLevelsES6(1, "a = { f: function(x) {return x;} }; a.f?.(1)"); + Utils.assertWithAllOptimizationLevelsES6(Undefined.instance, "a = {f: null}; a.f?.(1)"); + Utils.assertWithAllOptimizationLevelsES6( + Undefined.instance, "a = {f: undefined}; a.f?.(1)"); + Utils.assertWithAllOptimizationLevelsES6(Undefined.instance, "a = {}; a.f?.(1)"); + + Utils.assertWithAllOptimizationLevelsES6( + 2, "a = { f: function(x, y) {return y;} }; a.f?.(1, 2)"); + Utils.assertWithAllOptimizationLevelsES6(Undefined.instance, "a = {f: null}; a.f?.(1, 2)"); + Utils.assertWithAllOptimizationLevelsES6( + Undefined.instance, "a = {f: undefined}; a.f?.(1, 2)"); + Utils.assertWithAllOptimizationLevelsES6(Undefined.instance, "a = {}; a.f?.(1, 2)"); + + Utils.assertWithAllOptimizationLevelsES6( + 3, "a = { f: function(x, y, z) {return z;} }; a.f?.(1, 2, 3)"); + Utils.assertWithAllOptimizationLevelsES6( + Undefined.instance, "a = {f: null}; a.f?.(1, 2, 3)"); + Utils.assertWithAllOptimizationLevelsES6( + Undefined.instance, "a = {f: undefined}; a.f?.(1, 2, 3)"); + Utils.assertWithAllOptimizationLevelsES6(Undefined.instance, "a = {}; a.f?.(1, 2, 3)"); + } + + @Test + public void expressionFunctionCall() { + Utils.assertWithAllOptimizationLevelsES6(1, "a = [ function(x) {return x;} ]; a[0]?.(1)"); + Utils.assertWithAllOptimizationLevelsES6(Undefined.instance, "a = [null]; a[0]?.(1)"); + Utils.assertWithAllOptimizationLevelsES6(Undefined.instance, "a = [undefined]; a[0]?.(1)"); + Utils.assertWithAllOptimizationLevelsES6(Undefined.instance, "a = []; a[0]?.(1)"); + } + + @Test + public void specialCall() { + Utils.assertWithAllOptimizationLevelsES6( + 1, "eval = function () { return 1 };\n" + "eval?.()"); + Utils.assertWithAllOptimizationLevelsES6(Undefined.instance, "eval = null;\n" + "eval?.()"); + Utils.assertWithAllOptimizationLevelsES6( + Undefined.instance, "eval = undefined;\n" + "eval?.()"); + Utils.assertWithAllOptimizationLevelsES6(Undefined.instance, "delete eval;\n" + "eval?.()"); + } + + @Test + public void shortCircuitsFunctionCalls() { + Utils.assertWithAllOptimizationLevelsES6(Undefined.instance, "a = undefined; a?.b()"); + Utils.assertWithAllOptimizationLevelsES6(Undefined.instance, "a = {}; a.b?.c().d()"); + } + + @Test + public void shortCircuitArgumentEvaluation() { + Utils.assertWithAllOptimizationLevelsES6(1, "c = 0; f = function(x){}; f?.(c++); c"); + Utils.assertWithAllOptimizationLevelsES6(0, "c = 0; f = undefined; f?.(c++); c"); + Utils.assertWithAllOptimizationLevelsES6(0, "c = 0; f = null; f?.(c++); c"); + + Utils.assertWithAllOptimizationLevelsES6(1, "c = 0; a = {f: function() {}}; a.f?.(c++); c"); + Utils.assertWithAllOptimizationLevelsES6(0, "c = 0; a = {f: undefined}; a.f?.(c++); c"); + Utils.assertWithAllOptimizationLevelsES6(0, "c = 0; a = {f: null}; a.f?.(c++); c"); + Utils.assertWithAllOptimizationLevelsES6(0, "c = 0; a = {}; a.f?.(c++); c"); + + Utils.assertWithAllOptimizationLevelsES6(1, "c = 0; a = [function() {}]; a[0]?.(c++); c"); + Utils.assertWithAllOptimizationLevelsES6(0, "c = 0; a = [undefined]; a[0]?.(c++); c"); + Utils.assertWithAllOptimizationLevelsES6(0, "c = 0; a = [null]; a[0]?.(c++); c"); + Utils.assertWithAllOptimizationLevelsES6(0, "c = 0; a = []; a[0]?.(c++); c"); + + Utils.assertWithAllOptimizationLevelsES6( + 1, "c = 0; a = {__parent__: function() {}}; a.__parent__?.(c++); c"); + Utils.assertWithAllOptimizationLevelsES6( + 0, "c = 0; a = {__parent__: undefined}; a.__parent__?.(c++); c"); + Utils.assertWithAllOptimizationLevelsES6( + 0, "c = 0; a = {__parent__: null}; a.__parent__?.(c++); c"); + Utils.assertWithAllOptimizationLevelsES6(0, "c = 0; a = {}; a.__parent__.f?.(c++); c"); + } + + @Test + public void toStringOfOptionalChaining() { + Utils.assertWithAllOptimizationLevelsES6( + "function f() { a?.b }", "function f() { a?.b } f.toString()"); + Utils.assertWithAllOptimizationLevelsES6( + "function f() { a?.() }", "function f() { a?.() } f.toString()"); + } + @Test public void optionalChainingOperatorFollowedByDigitsIsAHook() { Utils.assertWithAllOptimizationLevelsES6(0.5, "true ?.5 : false"); diff --git a/tests/testsrc/test262.properties b/tests/testsrc/test262.properties index ddcf84c039..fe3c0c336a 100644 --- a/tests/testsrc/test262.properties +++ b/tests/testsrc/test262.properties @@ -5794,7 +5794,7 @@ language/expressions/object 790/1169 (67.58%) yield-non-strict-access.js non-strict yield-non-strict-syntax.js non-strict -language/expressions/optional-chaining 21/38 (55.26%) +language/expressions/optional-chaining 18/38 (47.37%) call-expression.js early-errors-tail-position-null-optchain-template-string.js early-errors-tail-position-null-optchain-template-string-esi.js @@ -5809,12 +5809,9 @@ language/expressions/optional-chaining 21/38 (55.26%) member-expression-async-literal.js {unsupported: [async]} member-expression-async-this.js {unsupported: [async]} new-target-optional-call.js - optional-call-preserves-this.js - optional-chain.js optional-chain-async-optional-chain-square-brackets.js {unsupported: [async]} optional-chain-async-square-brackets.js {unsupported: [async]} optional-chain-prod-arguments.js - short-circuiting.js super-property-optional-call.js language/expressions/postfix-decrement 9/37 (24.32%)