Skip to content

Commit

Permalink
Implement optional chaining for function call (#1702)
Browse files Browse the repository at this point in the history
Implements optional chaining for function calls, as well as the "eval" case:

All cases are handled:
```js
f?.()
a.b?.()
a[0]?.()
a.__parent__()
```
  • Loading branch information
andreabergia authored Oct 25, 2024
1 parent e8b23f5 commit e9592d7
Show file tree
Hide file tree
Showing 14 changed files with 657 additions and 91 deletions.
82 changes: 73 additions & 9 deletions rhino/src/main/java/org/mozilla/javascript/CodeGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -1148,16 +1159,23 @@ 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) {
case Token.NAME:
{
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:
Expand All @@ -1169,23 +1187,59 @@ 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;
}
default:
// 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) {
Expand Down Expand Up @@ -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;
}
}
}
3 changes: 3 additions & 0 deletions rhino/src/main/java/org/mozilla/javascript/IRFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
21 changes: 19 additions & 2 deletions rhino/src/main/java/org/mozilla/javascript/Icode.java
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -163,6 +168,8 @@ static String bytecodeName(int bytecode) {
}

switch (bytecode) {
case Icode_DELNAME:
return "DELNAME";
case Icode_DUP:
return "DUP";
case Icode_DUP2:
Expand Down Expand Up @@ -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:
Expand Down
69 changes: 66 additions & 3 deletions rhino/src/main/java/org/mozilla/javascript/Interpreter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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];
Expand All @@ -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];
Expand All @@ -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];
Expand All @@ -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) {
Expand All @@ -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:
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -3002,7 +3064,8 @@ private static int doCallSpecial(
frame.thisObj,
callType,
frame.idata.itsSourceFile,
sourceLine);
sourceLine,
isOptionalChainingCall);
}
frame.pc += 4;
return stackTop;
Expand Down
Loading

0 comments on commit e9592d7

Please sign in to comment.