Skip to content
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

Optional chaining #2221

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 37 additions & 36 deletions ChangeLog

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion src/jslex.c
Original file line number Diff line number Diff line change
Expand Up @@ -629,9 +629,12 @@ void jslGetNextToken() {
jslGetNextCh();
} break;
case JSJLT_QUESTION: jslSingleChar();
if(lex->currCh=='?'){ // ??
if (lex->currCh=='?') { // ??
lex->tk = LEX_NULLISH;
jslGetNextCh();
} else if(lex->currCh=='.') { //?.
lex->tk = LEX_OPTIONAL_CHAINING;
jslGetNextCh();
} break;
case JSLJT_FORWARDSLASH:
// yay! JS is so awesome.
Expand Down Expand Up @@ -864,6 +867,7 @@ const char* jslReservedWordAsString(int token) {

// operators 2
/* LEX_NULLISH : */ "??\0"
/* LEX_OPTIONAL_CHAINING*/ "?.\0"
;
unsigned int p = 0;
int n = token-_LEX_TOKENS_START;
Expand Down
3 changes: 2 additions & 1 deletion src/jslex.h
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ _LEX_R_LIST_END = LEX_R_OF, /* always the last entry for symbols */

_LEX_OPERATOR2_START = _LEX_R_LIST_END+10, // padding for adding new symbols in the future!
LEX_NULLISH = _LEX_OPERATOR2_START,
_LEX_OPERATOR2_END = LEX_NULLISH,
LEX_OPTIONAL_CHAINING,
_LEX_OPERATOR2_END = LEX_OPTIONAL_CHAINING,

_LEX_TOKENS_END = _LEX_OPERATOR2_END, /* always the last entry for symbols */
} LEX_TYPES;
Expand Down
101 changes: 77 additions & 24 deletions src/jsparse.c
Original file line number Diff line number Diff line change
Expand Up @@ -783,10 +783,10 @@ NO_INLINE JsVar *jspeFunctionCall(JsVar *function, JsVar *functionName, JsVar *t
bool hadDebuggerNextLineOnly = false;

if (execInfo.execute&EXEC_DEBUGGER_STEP_INTO) {
if (functionName)
jsiConsolePrintf("Stepping into %v\n", functionName);
else
jsiConsolePrintf("Stepping into function\n", functionName);
if (functionName)
jsiConsolePrintf("Stepping into %v\n", functionName);
else
jsiConsolePrintf("Stepping into function\n", functionName);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just noticed these spaces got replaced with tabs. Please can you try and tweak these (and change your editor so tabs don't get added?). It really screws up the layout if someone views the code and doesn't have 8 space tabs set

} else {
hadDebuggerNextLineOnly = execInfo.execute&EXEC_DEBUGGER_NEXT_LINE;
if (hadDebuggerNextLineOnly)
Expand Down Expand Up @@ -1055,13 +1055,15 @@ JsVar *jspGetVarNamedField(JsVar *object, JsVar *nameVar, bool returnName) {
else return jsvSkipNameAndUnLock(child);
}

NO_INLINE JsVar *jspeFactorMember(JsVar *a, JsVar **parentResult) {
NO_INLINE JsVar *jspeFactorMember(JsVar *a, JsVar **parentResult, bool *isOptional) {
/* The parent if we're executing a method call */
JsVar *parent = 0;

while (lex->tk=='.' || lex->tk=='[') {
if (lex->tk == '.') { // ------------------------------------- Record Access
JSP_ASSERT_MATCH('.');
while (lex->tk==LEX_OPTIONAL_CHAINING || lex->tk=='.' || lex->tk=='[') {
bool optionalTk = lex->tk == LEX_OPTIONAL_CHAINING;
*isOptional |= optionalTk;
if (lex->tk == '.' || optionalTk) { // ------------------------------------- Record Access
jslGetNextToken();
if (jslIsIDOrReservedWord()) {
if (JSP_SHOULD_EXECUTE) {
// Note: name will go away when we parse something else!
Expand All @@ -1078,6 +1080,8 @@ NO_INLINE JsVar *jspeFactorMember(JsVar *a, JsVar **parentResult) {
JsVar *nameVar = jslGetTokenValueAsVar();
child = jsvCreateNewChild(aVar, nameVar, 0);
jsvUnLock(nameVar);
} else if (*isOptional) {
child = 0; // undefined
} else {
// could have been a string...
jsExceptionHere(JSET_ERROR, "Cannot read property '%s' of %s", name, jsvIsUndefined(aVar) ? "undefined" : "null");
Expand All @@ -1090,6 +1094,20 @@ NO_INLINE JsVar *jspeFactorMember(JsVar *a, JsVar **parentResult) {
}
// skip over current token (we checked above that it was an ID or reserved word)
jslGetNextToken();
#ifndef ESPR_NO_OPTIONAL_CHAINING
} else if ((lex->tk == '(' || lex->tk == '[') && optionalTk) {
// handle a?.() and a?.[0]

JsVar *aVar = jsvSkipNameWithParent(a, true, parent);

jsvUnLock(a);
if (jsvIsNullish(aVar)) {
a = 0; // undefined
} else {
a = aVar;
}
continue;
#endif
} else {
// incorrect token - force a match fail by asking for an ID
JSP_MATCH_WITH_RETURN(LEX_ID, a);
Expand All @@ -1098,6 +1116,15 @@ NO_INLINE JsVar *jspeFactorMember(JsVar *a, JsVar **parentResult) {
JsVar *index;
JSP_ASSERT_MATCH('[');
if (!jspCheckStackPosition()) return parent;

#ifndef ESPR_NO_OPTIONAL_CHAINING
JSP_SAVE_EXECUTE();
if (jsvIsUndefined(a) && *isOptional) {
// there was a previous a?.b where a was undefined
jspSetNoExecute();
}
#endif

index = jsvSkipNameAndUnLock(jspeAssignmentExpression());
JSP_MATCH_WITH_CLEANUP_AND_RETURN(']', jsvUnLock2(parent, index);, a);
if (JSP_SHOULD_EXECUTE) {
Expand All @@ -1123,6 +1150,10 @@ NO_INLINE JsVar *jspeFactorMember(JsVar *a, JsVar **parentResult) {
jsvUnLock(aVar);
}
jsvUnLock(index);

#ifndef ESPR_NO_OPTIONAL_CHAINING
JSP_RESTORE_EXECUTE();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm afraid this breaks stuff. undefined[1] should cause an exception (like 1143) but JSP_RESTORE_EXECUTE removes the error flag, so it just quietly completes. I can imagine there might be other code you could come up with (not just errors, but return/break/etc) too

#endif
} else {
assert(0);
}
Expand Down Expand Up @@ -1185,7 +1216,8 @@ NO_INLINE JsVar *jspeFactorFunctionCall() {
#ifndef SAVE_ON_FLASH
bool wasSuper = lex->tk==LEX_R_SUPER;
#endif
JsVar *a = jspeFactorMember(jspeFactor(), &parent);
bool optional;
JsVar *a = jspeFactorMember(jspeFactor(), &parent, &optional);
#ifndef SAVE_ON_FLASH
if (wasSuper) {
/* if this was 'super.something' then we need
Expand All @@ -1196,24 +1228,44 @@ NO_INLINE JsVar *jspeFactorFunctionCall() {
parent = jsvLockAgainSafe(execInfo.thisVar);
}
#endif

while ((lex->tk=='(' || (isConstructor && JSP_SHOULD_EXECUTE)) && !jspIsInterrupted()) {
JsVar *funcName = a;
JsVar *func = jsvSkipName(funcName);

/* The constructor function doesn't change parsing, so if we're
* not executing, just short-cut it. */
if (isConstructor && JSP_SHOULD_EXECUTE) {
// If we have '(' parse an argument list, otherwise don't look for any args
bool parseArgs = lex->tk=='(';
a = jspeConstruct(func, funcName, parseArgs);
isConstructor = false; // don't treat subsequent brackets as constructors
} else
#ifndef ESPR_NO_OPTIONAL_CHAINING
if (jsvIsUndefined(a) && optional) {
JSP_SAVE_EXECUTE();
jspSetNoExecute();

JsVar *funcName = a;
JsVar *func = jsvSkipName(funcName);

a = jspeFunctionCall(func, funcName, parent, true, 0, 0);

jsvUnLock3(funcName, func, parent);
jsvUnLock3(funcName, func, parent);

JSP_RESTORE_EXECUTE();
} else if (jsvIsUndefined(a)) {
break;
} else {
#endif
JsVar *funcName = a;
JsVar *func = jsvSkipName(funcName);

/* The constructor function doesn't change parsing, so if we're
* not executing, just short-cut it. */
if (isConstructor && JSP_SHOULD_EXECUTE) {
// If we have '(' parse an argument list, otherwise don't look for any args
bool parseArgs = lex->tk=='(';
a = jspeConstruct(func, funcName, parseArgs);
isConstructor = false; // don't treat subsequent brackets as constructors
} else
a = jspeFunctionCall(func, funcName, parent, true, 0, 0);

jsvUnLock3(funcName, func, parent);
#ifndef ESPR_NO_OPTIONAL_CHAINING
}
#endif
parent=0;
a = jspeFactorMember(a, &parent);
optional=false;
a = jspeFactorMember(a, &parent, &optional);
}
#ifndef SAVE_ON_FLASH
/* If we've got something that we care about the parent of (eg. a getter/setter)
Expand Down Expand Up @@ -1403,7 +1455,8 @@ NO_INLINE JsVar *jspeFactorTypeOf() {
NO_INLINE JsVar *jspeFactorDelete() {
JSP_ASSERT_MATCH(LEX_R_DELETE);
JsVar *parent = 0;
JsVar *a = jspeFactorMember(jspeFactor(), &parent);
bool optional;
JsVar *a = jspeFactorMember(jspeFactor(), &parent, &optional);
JsVar *result = 0;
if (JSP_SHOULD_EXECUTE) {
bool ok = false;
Expand Down
1 change: 1 addition & 0 deletions src/jsutils.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
#define ESPR_NO_GET_SET 1
#define ESPR_NO_LINE_NUMBERS 1
#define ESPR_NO_LET_SCOPING 1
#define ESPR_NO_OPTIONAL_CHAINING 1
#endif

#ifndef alloca
Expand Down
11 changes: 11 additions & 0 deletions tests/test_optional_chaining.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Testing optional chaining

var a;

result = a?.b ?? true;
result &= a?.b.c ?? true;
result &= a?.b() ?? true;

a = null;

result &= a?.b ?? true;
7 changes: 7 additions & 0 deletions tests/test_optional_chaining_array.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Tests optional chaining with array access

var a = undefined;

result = a?.b[0] ?? true;

result |= a?.[0] ?? true;
17 changes: 17 additions & 0 deletions tests/test_optional_chaining_method.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Testing optional chaining method

// when a is undefined
var a;
result = a?.() ?? true;

// when a is a function
a = () => true;
result &= a?.() ?? false;

// when a.b is undefined
a = {};
result &= a.b?.() ?? true;

// when a.b is a function
a = { b: () => true };
result &= a.b?.() ?? false;