diff --git a/ChangeLog b/ChangeLog index 6ea297e9e0..a914afcf8f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -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 diff --git a/src/jslex.c b/src/jslex.c index a2626d8bec..919c8670fc 100644 --- a/src/jslex.c +++ b/src/jslex.c @@ -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; @@ -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; @@ -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; @@ -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; diff --git a/src/jslex.h b/src/jslex.h index 4177b23baf..571613ed25 100644 --- a/src/jslex.h +++ b/src/jslex.h @@ -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; diff --git a/src/jsparse.c b/src/jsparse.c index 96b2103f62..3a711a344a 100644 --- a/src/jsparse.c +++ b/src/jsparse.c @@ -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); @@ -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; @@ -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() { @@ -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 ); @@ -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'? @@ -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=='!' || @@ -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) { diff --git a/src/jsvar.c b/src/jsvar.c index 7b33c324de..e946dea725 100644 --- a/src/jsvar.c +++ b/src/jsvar.c @@ -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) { diff --git a/src/jsvar.h b/src/jsvar.h index d201625f38..b6d45ed1e0 100644 --- a/src/jsvar.h +++ b/src/jsvar.h @@ -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); diff --git a/src/jswrap_object.c b/src/jswrap_object.c index cff0c6111a..bee2a778fd 100644 --- a/src/jswrap_object.c +++ b/src/jswrap_object.c @@ -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)) { @@ -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 @@ -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); @@ -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{ diff --git a/tests/test_class.js b/tests/test_class.js new file mode 100644 index 0000000000..c6d59e0a03 --- /dev/null +++ b/tests/test_class.js @@ -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; +