Skip to content

Commit

Permalink
Add ES6 classes and 'super' - #1302
Browse files Browse the repository at this point in the history
  • Loading branch information
gfwilliams committed Feb 20, 2018
1 parent 8a494b3 commit 6881528
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 10 deletions.
1 change: 1 addition & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
Allow flash writes *from* unaligned addresses on nRF52 and ESP8266 (previously this crashed the ESP8266)
Update process.ENV.EXPORTS to bring it in line with what the compiler uses
Now set 'this' correctly for Arrow Functions
Add ES6 classes and 'super'

1v95 : nRF5x: Swap to UART fifo to avoid overrun errors at high baud rates
Ensure Exceptions/errors are reported on a blank line
Expand Down
10 changes: 9 additions & 1 deletion src/jslex.c
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,7 @@ void jslGetNextToken() {
break;
case 'c': if (jslIsToken("case", 1)) lex->tk = LEX_R_CASE;
else if (jslIsToken("catch", 1)) lex->tk = LEX_R_CATCH;
else if (jslIsToken("class", 1)) lex->tk = LEX_R_CLASS;
else if (jslIsToken("const", 1)) lex->tk = LEX_R_CONST;
else if (jslIsToken("continue", 1)) lex->tk = LEX_R_CONTINUE;
break;
Expand All @@ -402,6 +403,7 @@ void jslGetNextToken() {
else if (jslIsToken("debugger", 1)) lex->tk = LEX_R_DEBUGGER;
break;
case 'e': if (jslIsToken("else", 1)) lex->tk = LEX_R_ELSE;
else if (jslIsToken("extends", 1)) lex->tk = LEX_R_EXTENDS;
break;
case 'f': if (jslIsToken("false", 1)) lex->tk = LEX_R_FALSE;
else if (jslIsToken("finally", 1)) lex->tk = LEX_R_FINALLY;
Expand All @@ -419,7 +421,9 @@ void jslGetNextToken() {
break;
case 'r': if (jslIsToken("return", 1)) lex->tk = LEX_R_RETURN;
break;
case 's': if (jslIsToken("switch", 1)) lex->tk = LEX_R_SWITCH;
case 's': if (jslIsToken("static", 1)) lex->tk = LEX_R_STATIC;
else if (jslIsToken("super", 1)) lex->tk = LEX_R_SUPER;
else if (jslIsToken("switch", 1)) lex->tk = LEX_R_SWITCH;
break;
case 't': if (jslIsToken("this", 1)) lex->tk = LEX_R_THIS;
else if (jslIsToken("throw", 1)) lex->tk = LEX_R_THROW;
Expand Down Expand Up @@ -792,6 +796,10 @@ void jslTokenAsString(int token, char *str, size_t len) {
/*LEX_R_TYPEOF : */ "typeof\0"
/*LEX_R_VOID : */ "void\0"
/*LEX_R_DEBUGGER : */ "debugger\0"
/*LEX_R_CLASS : */ "class\0"
/*LEX_R_EXTENDS : */ "extends\0"
/*LEX_R_SUPER : */ "super\0"
/*LEX_R_STATIC : */ "static\0"
;
unsigned int p = 0;
int n = token-_LEX_OPERATOR_START;
Expand Down
6 changes: 5 additions & 1 deletion src/jslex.h
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,11 @@ _LEX_R_LIST_START,
LEX_R_TYPEOF,
LEX_R_VOID,
LEX_R_DEBUGGER,
_LEX_R_LIST_END = LEX_R_DEBUGGER /* always the last entry */
LEX_R_CLASS,
LEX_R_EXTENDS,
LEX_R_SUPER,
LEX_R_STATIC,
_LEX_R_LIST_END = LEX_R_CLASS /* always the last entry */
} LEX_TYPES;


Expand Down
144 changes: 140 additions & 4 deletions src/jsparse.c
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@ NO_INLINE JsVar *jspeFunctionDefinition(bool parseNamedFunction) {
// Parse the actual function block
jspeFunctionDefinitionInternal(funcVar, false);

// if we had a function name, add it to the end
// if we had a function name, add it to the end (if we don't it gets confused with arguments)
if (funcVar && functionInternalName)
jsvObjectSetChildAndUnLock(funcVar, JSPARSE_FUNCTION_NAME_NAME, functionInternalName);

Expand Down Expand Up @@ -1191,7 +1191,20 @@ NO_INLINE JsVar *jspeFactorFunctionCall() {
}

JsVar *parent = 0;
#ifndef SAVE_ON_FLASH
bool wasSuper = lex->tk==LEX_R_SUPER;
#endif
JsVar *a = jspeFactorMember(jspeFactor(), &parent);
#ifndef SAVE_ON_FLASH
if (wasSuper) {
/* if this was 'super.something' then we need
* to overwrite the parent, because it'll be
* set to the prototype otherwise.
*/
jsvUnLock(parent);
parent = jsvLockAgainSafe(execInfo.thisVar);
}
#endif

while ((lex->tk=='(' || (isConstructor && JSP_SHOULD_EXECUTE)) && !jspIsInterrupted()) {
JsVar *funcName = a;
Expand Down Expand Up @@ -1481,6 +1494,73 @@ NO_INLINE JsVar *jspeExpressionOrArrowFunction() {
return a;
}
}

/// Parse an ES6 class, expects LEX_R_CLASS already parsed
NO_INLINE JsVar *jspeClassDefinition(bool parseNamedClass) {
JsVar *classFunction = 0;
JsVar *classPrototype = 0;
JsVar *classInternalName = 0;

bool actuallyCreateClass = JSP_SHOULD_EXECUTE;
if (actuallyCreateClass)
classFunction = jsvNewWithFlags(JSV_FUNCTION);

if (parseNamedClass && lex->tk==LEX_ID) {
if (classFunction)
classInternalName = jslGetTokenValueAsVar(lex);
JSP_ASSERT_MATCH(LEX_ID);
}
if (classFunction) {
JsVar *prototypeName = jsvFindChildFromString(classFunction, JSPARSE_PROTOTYPE_VAR, true);
jspEnsureIsPrototype(classFunction, prototypeName); // make sure it's an object
classPrototype = jsvSkipName(prototypeName);
jsvUnLock(prototypeName);
}
if (lex->tk==LEX_R_EXTENDS) {
JSP_ASSERT_MATCH(LEX_R_EXTENDS);
JsVar *extendsFrom = actuallyCreateClass ? jsvSkipNameAndUnLock(jspGetNamedVariable(jslGetTokenValueAsString(lex))) : 0;
JSP_MATCH_WITH_CLEANUP_AND_RETURN(LEX_ID,jsvUnLock4(extendsFrom,classFunction,classInternalName,classPrototype),0);
if (classPrototype) {
if (jsvIsFunction(extendsFrom)) {
jsvObjectSetChild(classPrototype, JSPARSE_INHERITS_VAR, extendsFrom);
// link in default constructor if ours isn't supplied
jsvObjectSetChildAndUnLock(classFunction, JSPARSE_FUNCTION_CODE_NAME, jsvNewFromString("if(this.__proto__.__proto__)this.__proto__.__proto__.apply(this,arguments)"));
} else
jsExceptionHere(JSET_SYNTAXERROR, "'extends' argument should be a function, got %t", extendsFrom);
}
jsvUnLock(extendsFrom);
}
JSP_MATCH_WITH_CLEANUP_AND_RETURN('{',jsvUnLock3(classFunction,classInternalName,classPrototype),0);

while ((lex->tk==LEX_ID || lex->tk==LEX_R_STATIC) && !jspIsInterrupted()) {
bool isStatic = lex->tk==LEX_R_STATIC;
if (isStatic) JSP_ASSERT_MATCH(LEX_R_STATIC);

JsVar *funcName = jslGetTokenValueAsVar(lex);
JSP_MATCH_WITH_CLEANUP_AND_RETURN(LEX_ID,jsvUnLock3(classFunction,classInternalName,classPrototype),0);
JsVar *method = jspeFunctionDefinition(false);
if (classFunction && classPrototype) {
if (jsvIsStringEqual(funcName, "get") || jsvIsStringEqual(funcName, "set")) {
jsExceptionHere(JSET_SYNTAXERROR, "'get' and 'set' and not supported in Espruino");
} else if (jsvIsStringEqual(funcName, "constructor")) {
jswrap_function_replaceWith(classFunction, method);
} else {
funcName = jsvMakeIntoVariableName(funcName, 0);
jsvSetValueOfName(funcName, method);
jsvAddName(isStatic ? classFunction : classPrototype, funcName);
}
}
jsvUnLock2(method,funcName);
}
jsvUnLock(classPrototype);
// If we had a name, add it to the end (or it gets confused with the constructor arguments)
if (classInternalName)
jsvObjectSetChildAndUnLock(classFunction, JSPARSE_FUNCTION_NAME_NAME, classInternalName);

JSP_MATCH_WITH_CLEANUP_AND_RETURN('}',jsvUnLock(classFunction),0);
return classFunction;
}

#endif

NO_INLINE JsVar *jspeFactor() {
Expand Down Expand Up @@ -1580,6 +1660,49 @@ NO_INLINE JsVar *jspeFactor() {
if (!jspCheckStackPosition()) return 0;
JSP_ASSERT_MATCH(LEX_R_FUNCTION);
return jspeFunctionDefinition(true);
#ifndef SAVE_ON_FLASH
} else if (lex->tk==LEX_R_CLASS) {
if (!jspCheckStackPosition()) return 0;
JSP_ASSERT_MATCH(LEX_R_CLASS);
return jspeClassDefinition(true);
} else if (lex->tk==LEX_R_SUPER) {
JSP_ASSERT_MATCH(LEX_R_SUPER);
/* This is kind of nasty, since super appears to do
three different things.
* In the constructor it references the extended class's constructor
* in a method it references the constructor's prototype.
* in a static method it references the extended class's constructor (but this is different)
*/

if (jsvIsObject(execInfo.thisVar)) {
// 'this' is an object - must be calling a normal method
JsVar *proto1 = jsvObjectGetChild(execInfo.thisVar, JSPARSE_INHERITS_VAR, 0); // if we're in a method, get __proto__ first
JsVar *proto2 = jsvIsObject(proto1) ? jsvObjectGetChild(proto1, JSPARSE_INHERITS_VAR, 0) : 0; // still in method, get __proto__.__proto__
jsvUnLock(proto1);
if (!proto2) {
jsExceptionHere(JSET_SYNTAXERROR, "Calling 'super' outside of class");
return 0;
}
if (lex->tk=='(') return proto2; // eg. used in a constructor
// But if we're doing something else - eg '.' or '[' then it needs to reference the prototype
JsVar *proto3 = jsvIsFunction(proto2) ? jsvObjectGetChild(proto2, JSPARSE_PROTOTYPE_VAR, 0) : 0;
jsvUnLock(proto2);
return proto3;
} else if (jsvIsFunction(execInfo.thisVar)) {
// 'this' is a function - must be calling a static method
JsVar *proto1 = jsvObjectGetChild(execInfo.thisVar, JSPARSE_PROTOTYPE_VAR, 0);
JsVar *proto2 = jsvIsObject(proto1) ? jsvObjectGetChild(proto1, JSPARSE_INHERITS_VAR, 0) : 0;
jsvUnLock(proto1);
if (!proto2) {
jsExceptionHere(JSET_SYNTAXERROR, "Calling 'super' outside of class");
return 0;
}
return proto2;
}
jsExceptionHere(JSET_SYNTAXERROR, "Calling 'super' outside of class");
return 0;
#endif
} else if (lex->tk==LEX_R_THIS) {
JSP_ASSERT_MATCH(LEX_R_THIS);
return jsvLockAgain( execInfo.thisVar ? execInfo.thisVar : execInfo.root );
Expand Down Expand Up @@ -2460,21 +2583,29 @@ NO_INLINE JsVar *jspeStatementThrow() {
return 0;
}

NO_INLINE JsVar *jspeStatementFunctionDecl() {
NO_INLINE JsVar *jspeStatementFunctionDecl(bool isClass) {
JsVar *funcName = 0;
JsVar *funcVar;

#ifndef SAVE_ON_FLASH
JSP_ASSERT_MATCH(isClass ? LEX_R_CLASS : LEX_R_FUNCTION);
#else
JSP_ASSERT_MATCH(LEX_R_FUNCTION);
#endif

bool actuallyCreateFunction = JSP_SHOULD_EXECUTE;
if (actuallyCreateFunction) {
funcName = jsvMakeIntoVariableName(jslGetTokenValueAsVar(lex), 0);
if (!funcName) { // out of memory
jspSetError(false);
return 0;
}
}
JSP_MATCH_WITH_CLEANUP_AND_RETURN(LEX_ID, jsvUnLock(funcName), 0);
#ifndef SAVE_ON_FLASH
funcVar = isClass ? jspeClassDefinition(false) : jspeFunctionDefinition(false);
#else
funcVar = jspeFunctionDefinition(false);
#endif
if (actuallyCreateFunction) {
// find a function with the same name (or make one)
// OPT: can Find* use just a JsVar that is a 'name'?
Expand Down Expand Up @@ -2520,6 +2651,7 @@ NO_INLINE JsVar *jspeStatement() {
lex->tk==LEX_R_DELETE ||
lex->tk==LEX_R_TYPEOF ||
lex->tk==LEX_R_VOID ||
lex->tk==LEX_R_SUPER ||
lex->tk==LEX_PLUSPLUS ||
lex->tk==LEX_MINUSMINUS ||
lex->tk=='!' ||
Expand Down Expand Up @@ -2557,7 +2689,11 @@ NO_INLINE JsVar *jspeStatement() {
} else if (lex->tk==LEX_R_THROW) {
return jspeStatementThrow();
} else if (lex->tk==LEX_R_FUNCTION) {
return jspeStatementFunctionDecl();
return jspeStatementFunctionDecl(false/* function */);
#ifndef SAVE_ON_FLASH
} else if (lex->tk==LEX_R_CLASS) {
return jspeStatementFunctionDecl(true/* class */);
#endif
} else if (lex->tk==LEX_R_CONTINUE) {
JSP_ASSERT_MATCH(LEX_R_CONTINUE);
if (JSP_SHOULD_EXECUTE) {
Expand Down
7 changes: 7 additions & 0 deletions src/jsvar.c
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,13 @@ void jsvUnLock3(JsVar *var1, JsVar *var2, JsVar *var3) {
jsvUnLock(var2);
jsvUnLock(var3);
}
/// Unlock 4 variables in one go
void jsvUnLock4(JsVar *var1, JsVar *var2, JsVar *var3, JsVar *var4) {
jsvUnLock(var1);
jsvUnLock(var2);
jsvUnLock(var3);
jsvUnLock(var4);
}

/// Unlock an array of variables
NO_INLINE void jsvUnLockMany(unsigned int count, JsVar **vars) {
Expand Down
2 changes: 2 additions & 0 deletions src/jsvar.h
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,8 @@ ALWAYS_INLINE void jsvUnLock(JsVar *var);
void jsvUnLock2(JsVar *var1, JsVar *var2);
/// Unlock 3 variables in one go
void jsvUnLock3(JsVar *var1, JsVar *var2, JsVar *var3);
/// Unlock 4 variables in one go
void jsvUnLock4(JsVar *var1, JsVar *var2, JsVar *var3, JsVar *var4);

/// Unlock an array of variables
NO_INLINE void jsvUnLockMany(unsigned int count, JsVar **vars);
Expand Down
13 changes: 9 additions & 4 deletions src/jswrap_object.c
Original file line number Diff line number Diff line change
Expand Up @@ -837,7 +837,8 @@ void jswrap_object_removeAllListeners_cstr(JsVar *parent, const char *event) {
["newFunc","JsVar","The new function to replace this function with"]
]
}
This replaces the function with the one in the argument - while keeping the old function's scope. This allows inner functions to be edited, and is used when edit() is called on an inner function.
This replaces the function with the one in the argument - while keeping the old function's scope.
This allows inner functions to be edited, and is used when edit() is called on an inner function.
*/
void jswrap_function_replaceWith(JsVar *oldFunc, JsVar *newFunc) {
if (!jsvIsFunction(newFunc)) {
Expand All @@ -859,8 +860,9 @@ void jswrap_function_replaceWith(JsVar *oldFunc, JsVar *newFunc) {
oldFunc->flags = (oldFunc->flags&~JSV_VARTYPEMASK) |JSV_FUNCTION;
}

// Grab scope - the one thing we want to keep
// Grab scope and prototype - the things we want to keep
JsVar *scope = jsvFindChildFromString(oldFunc, JSPARSE_FUNCTION_SCOPE_NAME, false);
JsVar *prototype = jsvFindChildFromString(oldFunc, JSPARSE_PROTOTYPE_VAR, false);
// so now remove all existing entries
jsvRemoveAllChildren(oldFunc);
// now re-add scope
Expand All @@ -872,7 +874,8 @@ void jswrap_function_replaceWith(JsVar *oldFunc, JsVar *newFunc) {
while (jsvObjectIteratorHasValue(&it)) {
JsVar *el = jsvObjectIteratorGetKey(&it);
jsvObjectIteratorNext(&it);
if (!jsvIsStringEqual(el, JSPARSE_FUNCTION_SCOPE_NAME)) {
if (!jsvIsStringEqual(el, JSPARSE_FUNCTION_SCOPE_NAME) &&
!jsvIsStringEqual(el, JSPARSE_PROTOTYPE_VAR)) {
JsVar *copy = jsvCopy(el, true);
if (copy) {
jsvAddName(oldFunc, copy);
Expand All @@ -882,7 +885,9 @@ void jswrap_function_replaceWith(JsVar *oldFunc, JsVar *newFunc) {
jsvUnLock(el);
}
jsvObjectIteratorFree(&it);

// re-add prototype (it needs to come after other hidden vars)
if (prototype) jsvAddName(oldFunc, prototype);
jsvUnLock(prototype);
}

/*JSON{
Expand Down
62 changes: 62 additions & 0 deletions tests/test_class.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// roughly based on https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes
// and https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/super
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}

// Method
calcArea() {
return this.height * this.width;
}

static isACircle() {
return false;
}
}

var p = new Rectangle(4,3);
var ra = p.width==3 && p.height==4 && p.calcArea()==12 && !Rectangle.isACircle();

// --------------------------------------------

class Cat {
constructor(name) {
this.name = name;
}
speak() {
return this.name + ' makes a noise.';
}
static isDog() {
return false;
}
}

class Lion extends Cat {
speak() {
return super.speak()+this.name + ' roars.';
}
static isReallyADog() {
return super.isDog();
}
}

class Lion2 extends Cat {
constructor(name) {
super(name);
}
speak() {
return super.speak()+this.name + ' roars.';
}
}

var c = new Cat("Tiddles");
var l = new Lion("Alan");
var l2 = new Lion("Nigel");
var rb = c.speak()=="Tiddles makes a noise." && l.speak()=="Alan makes a noise.Alan roars." && l2.speak()=="Nigel makes a noise.Nigel roars." && Lion.isReallyADog()===false;

// --------------------------------------------

result = ra && rb;

0 comments on commit 6881528

Please sign in to comment.