Skip to content

Commit 6881528

Browse files
committed
Add ES6 classes and 'super' - #1302
1 parent 8a494b3 commit 6881528

File tree

8 files changed

+235
-10
lines changed

8 files changed

+235
-10
lines changed

ChangeLog

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
Allow flash writes *from* unaligned addresses on nRF52 and ESP8266 (previously this crashed the ESP8266)
4343
Update process.ENV.EXPORTS to bring it in line with what the compiler uses
4444
Now set 'this' correctly for Arrow Functions
45+
Add ES6 classes and 'super'
4546

4647
1v95 : nRF5x: Swap to UART fifo to avoid overrun errors at high baud rates
4748
Ensure Exceptions/errors are reported on a blank line

src/jslex.c

+9-1
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,7 @@ void jslGetNextToken() {
393393
break;
394394
case 'c': if (jslIsToken("case", 1)) lex->tk = LEX_R_CASE;
395395
else if (jslIsToken("catch", 1)) lex->tk = LEX_R_CATCH;
396+
else if (jslIsToken("class", 1)) lex->tk = LEX_R_CLASS;
396397
else if (jslIsToken("const", 1)) lex->tk = LEX_R_CONST;
397398
else if (jslIsToken("continue", 1)) lex->tk = LEX_R_CONTINUE;
398399
break;
@@ -402,6 +403,7 @@ void jslGetNextToken() {
402403
else if (jslIsToken("debugger", 1)) lex->tk = LEX_R_DEBUGGER;
403404
break;
404405
case 'e': if (jslIsToken("else", 1)) lex->tk = LEX_R_ELSE;
406+
else if (jslIsToken("extends", 1)) lex->tk = LEX_R_EXTENDS;
405407
break;
406408
case 'f': if (jslIsToken("false", 1)) lex->tk = LEX_R_FALSE;
407409
else if (jslIsToken("finally", 1)) lex->tk = LEX_R_FINALLY;
@@ -419,7 +421,9 @@ void jslGetNextToken() {
419421
break;
420422
case 'r': if (jslIsToken("return", 1)) lex->tk = LEX_R_RETURN;
421423
break;
422-
case 's': if (jslIsToken("switch", 1)) lex->tk = LEX_R_SWITCH;
424+
case 's': if (jslIsToken("static", 1)) lex->tk = LEX_R_STATIC;
425+
else if (jslIsToken("super", 1)) lex->tk = LEX_R_SUPER;
426+
else if (jslIsToken("switch", 1)) lex->tk = LEX_R_SWITCH;
423427
break;
424428
case 't': if (jslIsToken("this", 1)) lex->tk = LEX_R_THIS;
425429
else if (jslIsToken("throw", 1)) lex->tk = LEX_R_THROW;
@@ -792,6 +796,10 @@ void jslTokenAsString(int token, char *str, size_t len) {
792796
/*LEX_R_TYPEOF : */ "typeof\0"
793797
/*LEX_R_VOID : */ "void\0"
794798
/*LEX_R_DEBUGGER : */ "debugger\0"
799+
/*LEX_R_CLASS : */ "class\0"
800+
/*LEX_R_EXTENDS : */ "extends\0"
801+
/*LEX_R_SUPER : */ "super\0"
802+
/*LEX_R_STATIC : */ "static\0"
795803
;
796804
unsigned int p = 0;
797805
int n = token-_LEX_OPERATOR_START;

src/jslex.h

+5-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,11 @@ _LEX_R_LIST_START,
9494
LEX_R_TYPEOF,
9595
LEX_R_VOID,
9696
LEX_R_DEBUGGER,
97-
_LEX_R_LIST_END = LEX_R_DEBUGGER /* always the last entry */
97+
LEX_R_CLASS,
98+
LEX_R_EXTENDS,
99+
LEX_R_SUPER,
100+
LEX_R_STATIC,
101+
_LEX_R_LIST_END = LEX_R_CLASS /* always the last entry */
98102
} LEX_TYPES;
99103

100104

src/jsparse.c

+140-4
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ NO_INLINE JsVar *jspeFunctionDefinition(bool parseNamedFunction) {
466466
// Parse the actual function block
467467
jspeFunctionDefinitionInternal(funcVar, false);
468468

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

@@ -1191,7 +1191,20 @@ NO_INLINE JsVar *jspeFactorFunctionCall() {
11911191
}
11921192

11931193
JsVar *parent = 0;
1194+
#ifndef SAVE_ON_FLASH
1195+
bool wasSuper = lex->tk==LEX_R_SUPER;
1196+
#endif
11941197
JsVar *a = jspeFactorMember(jspeFactor(), &parent);
1198+
#ifndef SAVE_ON_FLASH
1199+
if (wasSuper) {
1200+
/* if this was 'super.something' then we need
1201+
* to overwrite the parent, because it'll be
1202+
* set to the prototype otherwise.
1203+
*/
1204+
jsvUnLock(parent);
1205+
parent = jsvLockAgainSafe(execInfo.thisVar);
1206+
}
1207+
#endif
11951208

11961209
while ((lex->tk=='(' || (isConstructor && JSP_SHOULD_EXECUTE)) && !jspIsInterrupted()) {
11971210
JsVar *funcName = a;
@@ -1481,6 +1494,73 @@ NO_INLINE JsVar *jspeExpressionOrArrowFunction() {
14811494
return a;
14821495
}
14831496
}
1497+
1498+
/// Parse an ES6 class, expects LEX_R_CLASS already parsed
1499+
NO_INLINE JsVar *jspeClassDefinition(bool parseNamedClass) {
1500+
JsVar *classFunction = 0;
1501+
JsVar *classPrototype = 0;
1502+
JsVar *classInternalName = 0;
1503+
1504+
bool actuallyCreateClass = JSP_SHOULD_EXECUTE;
1505+
if (actuallyCreateClass)
1506+
classFunction = jsvNewWithFlags(JSV_FUNCTION);
1507+
1508+
if (parseNamedClass && lex->tk==LEX_ID) {
1509+
if (classFunction)
1510+
classInternalName = jslGetTokenValueAsVar(lex);
1511+
JSP_ASSERT_MATCH(LEX_ID);
1512+
}
1513+
if (classFunction) {
1514+
JsVar *prototypeName = jsvFindChildFromString(classFunction, JSPARSE_PROTOTYPE_VAR, true);
1515+
jspEnsureIsPrototype(classFunction, prototypeName); // make sure it's an object
1516+
classPrototype = jsvSkipName(prototypeName);
1517+
jsvUnLock(prototypeName);
1518+
}
1519+
if (lex->tk==LEX_R_EXTENDS) {
1520+
JSP_ASSERT_MATCH(LEX_R_EXTENDS);
1521+
JsVar *extendsFrom = actuallyCreateClass ? jsvSkipNameAndUnLock(jspGetNamedVariable(jslGetTokenValueAsString(lex))) : 0;
1522+
JSP_MATCH_WITH_CLEANUP_AND_RETURN(LEX_ID,jsvUnLock4(extendsFrom,classFunction,classInternalName,classPrototype),0);
1523+
if (classPrototype) {
1524+
if (jsvIsFunction(extendsFrom)) {
1525+
jsvObjectSetChild(classPrototype, JSPARSE_INHERITS_VAR, extendsFrom);
1526+
// link in default constructor if ours isn't supplied
1527+
jsvObjectSetChildAndUnLock(classFunction, JSPARSE_FUNCTION_CODE_NAME, jsvNewFromString("if(this.__proto__.__proto__)this.__proto__.__proto__.apply(this,arguments)"));
1528+
} else
1529+
jsExceptionHere(JSET_SYNTAXERROR, "'extends' argument should be a function, got %t", extendsFrom);
1530+
}
1531+
jsvUnLock(extendsFrom);
1532+
}
1533+
JSP_MATCH_WITH_CLEANUP_AND_RETURN('{',jsvUnLock3(classFunction,classInternalName,classPrototype),0);
1534+
1535+
while ((lex->tk==LEX_ID || lex->tk==LEX_R_STATIC) && !jspIsInterrupted()) {
1536+
bool isStatic = lex->tk==LEX_R_STATIC;
1537+
if (isStatic) JSP_ASSERT_MATCH(LEX_R_STATIC);
1538+
1539+
JsVar *funcName = jslGetTokenValueAsVar(lex);
1540+
JSP_MATCH_WITH_CLEANUP_AND_RETURN(LEX_ID,jsvUnLock3(classFunction,classInternalName,classPrototype),0);
1541+
JsVar *method = jspeFunctionDefinition(false);
1542+
if (classFunction && classPrototype) {
1543+
if (jsvIsStringEqual(funcName, "get") || jsvIsStringEqual(funcName, "set")) {
1544+
jsExceptionHere(JSET_SYNTAXERROR, "'get' and 'set' and not supported in Espruino");
1545+
} else if (jsvIsStringEqual(funcName, "constructor")) {
1546+
jswrap_function_replaceWith(classFunction, method);
1547+
} else {
1548+
funcName = jsvMakeIntoVariableName(funcName, 0);
1549+
jsvSetValueOfName(funcName, method);
1550+
jsvAddName(isStatic ? classFunction : classPrototype, funcName);
1551+
}
1552+
}
1553+
jsvUnLock2(method,funcName);
1554+
}
1555+
jsvUnLock(classPrototype);
1556+
// If we had a name, add it to the end (or it gets confused with the constructor arguments)
1557+
if (classInternalName)
1558+
jsvObjectSetChildAndUnLock(classFunction, JSPARSE_FUNCTION_NAME_NAME, classInternalName);
1559+
1560+
JSP_MATCH_WITH_CLEANUP_AND_RETURN('}',jsvUnLock(classFunction),0);
1561+
return classFunction;
1562+
}
1563+
14841564
#endif
14851565

14861566
NO_INLINE JsVar *jspeFactor() {
@@ -1580,6 +1660,49 @@ NO_INLINE JsVar *jspeFactor() {
15801660
if (!jspCheckStackPosition()) return 0;
15811661
JSP_ASSERT_MATCH(LEX_R_FUNCTION);
15821662
return jspeFunctionDefinition(true);
1663+
#ifndef SAVE_ON_FLASH
1664+
} else if (lex->tk==LEX_R_CLASS) {
1665+
if (!jspCheckStackPosition()) return 0;
1666+
JSP_ASSERT_MATCH(LEX_R_CLASS);
1667+
return jspeClassDefinition(true);
1668+
} else if (lex->tk==LEX_R_SUPER) {
1669+
JSP_ASSERT_MATCH(LEX_R_SUPER);
1670+
/* This is kind of nasty, since super appears to do
1671+
three different things.
1672+
1673+
* In the constructor it references the extended class's constructor
1674+
* in a method it references the constructor's prototype.
1675+
* in a static method it references the extended class's constructor (but this is different)
1676+
*/
1677+
1678+
if (jsvIsObject(execInfo.thisVar)) {
1679+
// 'this' is an object - must be calling a normal method
1680+
JsVar *proto1 = jsvObjectGetChild(execInfo.thisVar, JSPARSE_INHERITS_VAR, 0); // if we're in a method, get __proto__ first
1681+
JsVar *proto2 = jsvIsObject(proto1) ? jsvObjectGetChild(proto1, JSPARSE_INHERITS_VAR, 0) : 0; // still in method, get __proto__.__proto__
1682+
jsvUnLock(proto1);
1683+
if (!proto2) {
1684+
jsExceptionHere(JSET_SYNTAXERROR, "Calling 'super' outside of class");
1685+
return 0;
1686+
}
1687+
if (lex->tk=='(') return proto2; // eg. used in a constructor
1688+
// But if we're doing something else - eg '.' or '[' then it needs to reference the prototype
1689+
JsVar *proto3 = jsvIsFunction(proto2) ? jsvObjectGetChild(proto2, JSPARSE_PROTOTYPE_VAR, 0) : 0;
1690+
jsvUnLock(proto2);
1691+
return proto3;
1692+
} else if (jsvIsFunction(execInfo.thisVar)) {
1693+
// 'this' is a function - must be calling a static method
1694+
JsVar *proto1 = jsvObjectGetChild(execInfo.thisVar, JSPARSE_PROTOTYPE_VAR, 0);
1695+
JsVar *proto2 = jsvIsObject(proto1) ? jsvObjectGetChild(proto1, JSPARSE_INHERITS_VAR, 0) : 0;
1696+
jsvUnLock(proto1);
1697+
if (!proto2) {
1698+
jsExceptionHere(JSET_SYNTAXERROR, "Calling 'super' outside of class");
1699+
return 0;
1700+
}
1701+
return proto2;
1702+
}
1703+
jsExceptionHere(JSET_SYNTAXERROR, "Calling 'super' outside of class");
1704+
return 0;
1705+
#endif
15831706
} else if (lex->tk==LEX_R_THIS) {
15841707
JSP_ASSERT_MATCH(LEX_R_THIS);
15851708
return jsvLockAgain( execInfo.thisVar ? execInfo.thisVar : execInfo.root );
@@ -2460,21 +2583,29 @@ NO_INLINE JsVar *jspeStatementThrow() {
24602583
return 0;
24612584
}
24622585

2463-
NO_INLINE JsVar *jspeStatementFunctionDecl() {
2586+
NO_INLINE JsVar *jspeStatementFunctionDecl(bool isClass) {
24642587
JsVar *funcName = 0;
24652588
JsVar *funcVar;
2589+
2590+
#ifndef SAVE_ON_FLASH
2591+
JSP_ASSERT_MATCH(isClass ? LEX_R_CLASS : LEX_R_FUNCTION);
2592+
#else
24662593
JSP_ASSERT_MATCH(LEX_R_FUNCTION);
2594+
#endif
24672595

24682596
bool actuallyCreateFunction = JSP_SHOULD_EXECUTE;
24692597
if (actuallyCreateFunction) {
24702598
funcName = jsvMakeIntoVariableName(jslGetTokenValueAsVar(lex), 0);
24712599
if (!funcName) { // out of memory
2472-
jspSetError(false);
24732600
return 0;
24742601
}
24752602
}
24762603
JSP_MATCH_WITH_CLEANUP_AND_RETURN(LEX_ID, jsvUnLock(funcName), 0);
2604+
#ifndef SAVE_ON_FLASH
2605+
funcVar = isClass ? jspeClassDefinition(false) : jspeFunctionDefinition(false);
2606+
#else
24772607
funcVar = jspeFunctionDefinition(false);
2608+
#endif
24782609
if (actuallyCreateFunction) {
24792610
// find a function with the same name (or make one)
24802611
// OPT: can Find* use just a JsVar that is a 'name'?
@@ -2520,6 +2651,7 @@ NO_INLINE JsVar *jspeStatement() {
25202651
lex->tk==LEX_R_DELETE ||
25212652
lex->tk==LEX_R_TYPEOF ||
25222653
lex->tk==LEX_R_VOID ||
2654+
lex->tk==LEX_R_SUPER ||
25232655
lex->tk==LEX_PLUSPLUS ||
25242656
lex->tk==LEX_MINUSMINUS ||
25252657
lex->tk=='!' ||
@@ -2557,7 +2689,11 @@ NO_INLINE JsVar *jspeStatement() {
25572689
} else if (lex->tk==LEX_R_THROW) {
25582690
return jspeStatementThrow();
25592691
} else if (lex->tk==LEX_R_FUNCTION) {
2560-
return jspeStatementFunctionDecl();
2692+
return jspeStatementFunctionDecl(false/* function */);
2693+
#ifndef SAVE_ON_FLASH
2694+
} else if (lex->tk==LEX_R_CLASS) {
2695+
return jspeStatementFunctionDecl(true/* class */);
2696+
#endif
25612697
} else if (lex->tk==LEX_R_CONTINUE) {
25622698
JSP_ASSERT_MATCH(LEX_R_CONTINUE);
25632699
if (JSP_SHOULD_EXECUTE) {

src/jsvar.c

+7
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,13 @@ void jsvUnLock3(JsVar *var1, JsVar *var2, JsVar *var3) {
689689
jsvUnLock(var2);
690690
jsvUnLock(var3);
691691
}
692+
/// Unlock 4 variables in one go
693+
void jsvUnLock4(JsVar *var1, JsVar *var2, JsVar *var3, JsVar *var4) {
694+
jsvUnLock(var1);
695+
jsvUnLock(var2);
696+
jsvUnLock(var3);
697+
jsvUnLock(var4);
698+
}
692699

693700
/// Unlock an array of variables
694701
NO_INLINE void jsvUnLockMany(unsigned int count, JsVar **vars) {

src/jsvar.h

+2
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,8 @@ ALWAYS_INLINE void jsvUnLock(JsVar *var);
351351
void jsvUnLock2(JsVar *var1, JsVar *var2);
352352
/// Unlock 3 variables in one go
353353
void jsvUnLock3(JsVar *var1, JsVar *var2, JsVar *var3);
354+
/// Unlock 4 variables in one go
355+
void jsvUnLock4(JsVar *var1, JsVar *var2, JsVar *var3, JsVar *var4);
354356

355357
/// Unlock an array of variables
356358
NO_INLINE void jsvUnLockMany(unsigned int count, JsVar **vars);

src/jswrap_object.c

+9-4
Original file line numberDiff line numberDiff line change
@@ -837,7 +837,8 @@ void jswrap_object_removeAllListeners_cstr(JsVar *parent, const char *event) {
837837
["newFunc","JsVar","The new function to replace this function with"]
838838
]
839839
}
840-
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.
840+
This replaces the function with the one in the argument - while keeping the old function's scope.
841+
This allows inner functions to be edited, and is used when edit() is called on an inner function.
841842
*/
842843
void jswrap_function_replaceWith(JsVar *oldFunc, JsVar *newFunc) {
843844
if (!jsvIsFunction(newFunc)) {
@@ -859,8 +860,9 @@ void jswrap_function_replaceWith(JsVar *oldFunc, JsVar *newFunc) {
859860
oldFunc->flags = (oldFunc->flags&~JSV_VARTYPEMASK) |JSV_FUNCTION;
860861
}
861862

862-
// Grab scope - the one thing we want to keep
863+
// Grab scope and prototype - the things we want to keep
863864
JsVar *scope = jsvFindChildFromString(oldFunc, JSPARSE_FUNCTION_SCOPE_NAME, false);
865+
JsVar *prototype = jsvFindChildFromString(oldFunc, JSPARSE_PROTOTYPE_VAR, false);
864866
// so now remove all existing entries
865867
jsvRemoveAllChildren(oldFunc);
866868
// now re-add scope
@@ -872,7 +874,8 @@ void jswrap_function_replaceWith(JsVar *oldFunc, JsVar *newFunc) {
872874
while (jsvObjectIteratorHasValue(&it)) {
873875
JsVar *el = jsvObjectIteratorGetKey(&it);
874876
jsvObjectIteratorNext(&it);
875-
if (!jsvIsStringEqual(el, JSPARSE_FUNCTION_SCOPE_NAME)) {
877+
if (!jsvIsStringEqual(el, JSPARSE_FUNCTION_SCOPE_NAME) &&
878+
!jsvIsStringEqual(el, JSPARSE_PROTOTYPE_VAR)) {
876879
JsVar *copy = jsvCopy(el, true);
877880
if (copy) {
878881
jsvAddName(oldFunc, copy);
@@ -882,7 +885,9 @@ void jswrap_function_replaceWith(JsVar *oldFunc, JsVar *newFunc) {
882885
jsvUnLock(el);
883886
}
884887
jsvObjectIteratorFree(&it);
885-
888+
// re-add prototype (it needs to come after other hidden vars)
889+
if (prototype) jsvAddName(oldFunc, prototype);
890+
jsvUnLock(prototype);
886891
}
887892

888893
/*JSON{

tests/test_class.js

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// roughly based on https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes
2+
// and https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/super
3+
class Rectangle {
4+
constructor(height, width) {
5+
this.height = height;
6+
this.width = width;
7+
}
8+
9+
// Method
10+
calcArea() {
11+
return this.height * this.width;
12+
}
13+
14+
static isACircle() {
15+
return false;
16+
}
17+
}
18+
19+
var p = new Rectangle(4,3);
20+
var ra = p.width==3 && p.height==4 && p.calcArea()==12 && !Rectangle.isACircle();
21+
22+
// --------------------------------------------
23+
24+
class Cat {
25+
constructor(name) {
26+
this.name = name;
27+
}
28+
speak() {
29+
return this.name + ' makes a noise.';
30+
}
31+
static isDog() {
32+
return false;
33+
}
34+
}
35+
36+
class Lion extends Cat {
37+
speak() {
38+
return super.speak()+this.name + ' roars.';
39+
}
40+
static isReallyADog() {
41+
return super.isDog();
42+
}
43+
}
44+
45+
class Lion2 extends Cat {
46+
constructor(name) {
47+
super(name);
48+
}
49+
speak() {
50+
return super.speak()+this.name + ' roars.';
51+
}
52+
}
53+
54+
var c = new Cat("Tiddles");
55+
var l = new Lion("Alan");
56+
var l2 = new Lion("Nigel");
57+
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;
58+
59+
// --------------------------------------------
60+
61+
result = ra && rb;
62+

0 commit comments

Comments
 (0)