diff --git a/Makefile b/Makefile index 16b8db0..4265b6b 100644 --- a/Makefile +++ b/Makefile @@ -3,18 +3,21 @@ CFLAGS=-c -g -Wall LDFLAGS= SOURCES= \ -Script.cpp \ TinyJS.cpp \ TinyJS_Functions.cpp OBJECTS=$(SOURCES:.cpp=.o) -EXECUTABLE=Script -#all: $(SOURCES) $(EXECUTABLE) -all: $(EXECUTABLE) +all: run_tests Script -$(EXECUTABLE): $(OBJECTS) - $(CC) $(LDFLAGS) $(OBJECTS) -o $@ +run_tests: run_tests.o $(OBJECTS) + $(CC) $(LDFLAGS) run_tests.o $(OBJECTS) -o $@ + +Script: Script.o $(OBJECTS) + $(CC) $(LDFLAGS) Script.o $(OBJECTS) -o $@ .cpp.o: $(CC) $(CFLAGS) $< -o $@ + +clean: + rm run_tests Script run_tests.o Script.o $(OBJECTS) diff --git a/Script.cpp b/Script.cpp index 91c8076..34dc74f 100755 --- a/Script.cpp +++ b/Script.cpp @@ -54,10 +54,14 @@ int main(int argc, char **argv) s.addNative("function print(text)", &js_print); s.addNative("function dump()", &js_dump); /* Execute out bit of code - we could call 'evaluate' here if - we wanted something returned */ - s.execute("var lets_quit = 0; function quit() { lets_quit = 1; }"); - s.execute("print(\"Interactive mode... Type quit(); to exit, or print(...); to print something, or dump() to dump the symbol table!\");"); - + we wanted something returned */ + try { + s.execute("var lets_quit = 0; function quit() { lets_quit = 1; }"); + s.execute("print(\"Interactive mode... Type quit(); to exit, or print(...); to print something, or dump() to dump the symbol table!\");"); + } catch (CScriptException *e) { + printf("ERROR: %s\n", e->text.c_str()); + } + while (s.evaluate("lets_quit") == "0") { char buffer[2048]; fgets ( buffer, sizeof(buffer), stdin ); diff --git a/TinyJS.cpp b/TinyJS.cpp index 3d36310..49f376a 100755 --- a/TinyJS.cpp +++ b/TinyJS.cpp @@ -32,6 +32,18 @@ Added nil Added rough JSON parsing Improved example app + Version 0.13 : Added tokenEnd/tokenLastEnd to lexer to avoid parsing whitespace + Ability to define functions without names + Can now do "var mine = function(a,b) { ... };" + Slightly better 'trace' function + Added findChildOrCreateByPath function + Added simple test suite + Added skipping of blocks when not executing + + NOTE: Currently TinyJS passes by VALUE for function, and never copies + references - which is a major difference between this and proper + JavaScript. + It also doesn't support class prototypes, or the 'new' operator */ #include "TinyJS.h" @@ -138,7 +150,9 @@ CScriptLex::~CScriptLex(void) void CScriptLex::reset() { dataPos = dataStart; - tokenPosition = 0; + tokenStart = 0; + tokenEnd = 0; + tokenLastEnd = 0; tk = 0; tkStr = ""; getNextCh(); @@ -150,7 +164,7 @@ void CScriptLex::match(int expected_tk) { if (tk!=expected_tk) { ostringstream errorString; errorString << "Got " << getTokenStr(tk) << " expected " << getTokenStr(expected_tk) - << " at " << getPosition(tokenPosition) << "in '" << data << "'"; + << " at " << getPosition(tokenStart) << "in '" << data << "'"; throw new CScriptException(errorString.str()); } getNextToken(); @@ -199,9 +213,10 @@ string CScriptLex::getTokenStr(int token) { void CScriptLex::getNextCh() { currCh = nextCh; if (dataPos < dataEnd) - nextCh = data[dataPos++]; + nextCh = data[dataPos]; else nextCh = 0; + dataPos++; } void CScriptLex::getNextToken() { @@ -224,7 +239,7 @@ void CScriptLex::getNextToken() { return; } // record beginning of this token - tokenPosition = dataPos-2; + tokenStart = dataPos-2; // tokens if (isAlpha(currCh)) { // IDs while (isAlpha(currCh) || isNumeric(currCh)) { @@ -242,9 +257,7 @@ void CScriptLex::getNextToken() { else if (tkStr=="true") tk = LEX_R_TRUE; else if (tkStr=="false") tk = LEX_R_FALSE; else if (tkStr=="nil") tk = LEX_R_NIL; - return; - } - if (isNumeric(currCh)) { // Numbers + } else if (isNumeric(currCh)) { // Numbers tk = LEX_INT; while (isNumeric(currCh)) { tkStr += currCh; @@ -259,9 +272,7 @@ void CScriptLex::getNextToken() { getNextCh(); } } - return; - } - if (currCh=='"') { + } else if (currCh=='"') { getNextCh(); while (currCh && currCh!='"') { if (currCh == '\\') { @@ -279,52 +290,56 @@ void CScriptLex::getNextToken() { } getNextCh(); tk = LEX_STR; - return; - } - // single chars - tk = currCh; - getNextCh(); - if (tk=='=' && currCh=='=') { - tk = LEX_EQUAL; - getNextCh(); - } else if (tk=='!' && currCh=='=') { - tk = LEX_NEQUAL; - getNextCh(); - } else if (tk=='<' && currCh=='=') { - tk = LEX_LEQUAL; - getNextCh(); - } else if (tk=='>' && currCh=='=') { - tk = LEX_GEQUAL; - getNextCh(); - } else if (tk=='+' && currCh=='=') { - tk = LEX_PLUSEQUAL; - getNextCh(); - } else if (tk=='-' && currCh=='=') { - tk = LEX_MINUSEQUAL; - getNextCh(); - } else if (tk=='+' && currCh=='+') { - tk = LEX_PLUSPLUS; - getNextCh(); - } else if (tk=='-' && currCh=='-') { - tk = LEX_MINUSMINUS; - getNextCh(); - } else if (tk=='&' && currCh=='&') { - tk = LEX_ANDAND; - getNextCh(); - } else if (tk=='|' && currCh=='|') { - tk = LEX_OROR; - getNextCh(); + } else { + // single chars + tk = currCh; + if (currCh) getNextCh(); + if (tk=='=' && currCh=='=') { + tk = LEX_EQUAL; + getNextCh(); + } else if (tk=='!' && currCh=='=') { + tk = LEX_NEQUAL; + getNextCh(); + } else if (tk=='<' && currCh=='=') { + tk = LEX_LEQUAL; + getNextCh(); + } else if (tk=='>' && currCh=='=') { + tk = LEX_GEQUAL; + getNextCh(); + } else if (tk=='+' && currCh=='=') { + tk = LEX_PLUSEQUAL; + getNextCh(); + } else if (tk=='-' && currCh=='=') { + tk = LEX_MINUSEQUAL; + getNextCh(); + } else if (tk=='+' && currCh=='+') { + tk = LEX_PLUSPLUS; + getNextCh(); + } else if (tk=='-' && currCh=='-') { + tk = LEX_MINUSMINUS; + getNextCh(); + } else if (tk=='&' && currCh=='&') { + tk = LEX_ANDAND; + getNextCh(); + } else if (tk=='|' && currCh=='|') { + tk = LEX_OROR; + getNextCh(); + } } + /* This isn't quite right yet */ + tokenLastEnd = tokenEnd; + tokenEnd = dataPos-3; } string CScriptLex::getSubString(int lastPosition) { - if (dataPos < dataEnd) { + int lastCharIdx = tokenLastEnd+1; + if (lastCharIdx < dataEnd) { /* save a memory alloc by using our data array to create the substring */ - char old = data[tokenPosition]; - data[tokenPosition] = 0; + char old = data[lastCharIdx]; + data[lastCharIdx] = 0; std::string value = &data[lastPosition]; - data[tokenPosition] = old; + data[lastCharIdx] = old; return value; } else { return std::string(&data[lastPosition]); @@ -333,8 +348,9 @@ string CScriptLex::getSubString(int lastPosition) { CScriptLex *CScriptLex::getSubLex(int lastPosition) { - if (dataPos < dataEnd) - return new CScriptLex( this, lastPosition, tokenPosition); + int lastCharIdx = tokenLastEnd+1; + if (lastCharIdx < dataEnd) + return new CScriptLex( this, lastPosition, lastCharIdx); else return new CScriptLex( this, lastPosition, dataEnd ); } @@ -409,7 +425,7 @@ CScriptVar *CScriptVar::findChild(const string &childName) { } CScriptVar *CScriptVar::findChildOrCreate(const string &childName) { - CScriptVar *v = findChild(childName); + CScriptVar *v = findChild(childName); if (!v) { v = new CScriptVar(childName); addChild(v); @@ -417,6 +433,15 @@ CScriptVar *CScriptVar::findChildOrCreate(const string &childName) { return v; } +CScriptVar *CScriptVar::findChildOrCreateByPath(const std::string &path) { + size_t p = path.find('.'); + if (p == string::npos) + return findChildOrCreate(path); + + return findChildOrCreate(path.substr(0,p))-> + findChildOrCreateByPath(path.substr(p+1)); +} + CScriptVar *CScriptVar::findRecursive(const string &childName) { CScriptVar *v = findChild(childName); if (!v && parent) @@ -438,7 +463,7 @@ void CScriptVar::addChild(CScriptVar *child) { } void CScriptVar::addNamedChild(const std::string &childName, CScriptVar *child) { - if (!child) { + if (!child) { // not given anything - just add empty child addChild(new CScriptVar(childName)); } else if (child->parent) { @@ -446,7 +471,7 @@ void CScriptVar::addNamedChild(const std::string &childName, CScriptVar *child) CScriptVar *c = new CScriptVar(childName); c->copyValue(child); addChild(c); - } else { + } else { // no owners - just change name and add it child->name = childName; addChild(child); @@ -518,20 +543,20 @@ const string &CScriptVar::getString() { void CScriptVar::setInt(int val) { char buf[256]; sprintf(buf, "%d", val); + flags = (flags&~SCRIPTVAR_VARTYPEMASK) | SCRIPTVAR_INTEGER; data = buf; - flags = SCRIPTVAR_NUMERIC | SCRIPTVAR_INTEGER; } void CScriptVar::setDouble(double val) { char buf[256]; sprintf(buf, "%lf", val); + flags = (flags&~SCRIPTVAR_VARTYPEMASK) | SCRIPTVAR_DOUBLE; data = buf; - flags = SCRIPTVAR_NUMERIC; } void CScriptVar::setString(const string &str) { // name sure it's not still a number or integer - flags &= ~SCRIPTVAR_VARTYPEMASK; + flags = (flags&~SCRIPTVAR_VARTYPEMASK) | SCRIPTVAR_STRING; data = str; } @@ -611,6 +636,7 @@ CScriptVar *CScriptVar::mathsOp(CScriptVar *b, int op) { void CScriptVar::copyValue(CScriptVar *val) { if (val) { + // we *don't* copy the name data = val->data; flags = (flags & ~SCRIPTVAR_VARTYPEMASK) | (val->flags & SCRIPTVAR_VARTYPEMASK); // copy children @@ -637,7 +663,11 @@ CScriptVar *CScriptVar::deepCopy() { } void CScriptVar::trace(string indentStr) { - TRACE("%s'%s' = '%s'(%d)\n", indentStr.c_str(), name.c_str(), data.c_str(), flags); + TRACE("%s'%s' = '%s' %s\n", + indentStr.c_str(), + name.c_str(), + data.c_str(), + getFlagsAsString().c_str()); string indent = indentStr+" "; CScriptVar *child = firstChild; while (child) { @@ -646,10 +676,42 @@ void CScriptVar::trace(string indentStr) { } } +string CScriptVar::getFlagsAsString() { + string flagstr = ""; + if (flags&SCRIPTVAR_FUNCTION) flagstr = flagstr + "FUNCTION "; + if (flags&SCRIPTVAR_PARAMETER) flagstr = flagstr + "PARAMETER "; + if (flags&SCRIPTVAR_NATIVE) flagstr = flagstr + "NATIVE "; + if (flags&SCRIPTVAR_DOUBLE) flagstr = flagstr + "DOUBLE "; + if (flags&SCRIPTVAR_INTEGER) flagstr = flagstr + "INTEGER "; + if (flags&SCRIPTVAR_STRING) flagstr = flagstr + "STRING "; + return flagstr; +} + string CScriptVar::getParsableString() { + // Numbers can just be put in directly if (isNumeric()) return getString(); - return getJSString(getString()); + if (isFunction()) { + ostringstream funcStr; + funcStr << "function ("; + // get list of parameters + CScriptVar *child = firstChild; + bool first = true; + while (child) { + if (child->isParameter()) { + if (!first) { funcStr << ","; first = false; } + funcStr << child->getName(); + } + child = child->nextSibling; + } + // add function body + funcStr << ")" << getString(); + return funcStr.str(); + } + // if it is a string then we quote it + if (isString()) + return getJSString(getString()); + return "nil"; } void CScriptVar::getJSON(ostringstream &destination) { @@ -659,9 +721,9 @@ void CScriptVar::getJSON(ostringstream &destination) { CScriptVar *child = firstChild; while (child) { destination << getJSString(child->name) << " : "; - child->getJSON(destination); + child->getJSON(destination); child = child->nextSibling; - if (child) + if (child) destination << ",\n"; else destination << "\n"; @@ -704,7 +766,7 @@ void CTinyJS::execute(const string &code) { while (l->tk) statement(execute); } catch (CScriptException *e) { ostringstream msg; - msg << "Error " << e->text << " at " << l->getPosition(l->tokenPosition); + msg << "Error " << e->text << " at " << l->getPosition(l->tokenLastEnd); delete l; l = oldLex; throw new CScriptException(msg.str()); @@ -723,7 +785,7 @@ string CTinyJS::evaluate(const string &code) { v = base(execute); } catch (CScriptException *e) { ostringstream msg; - msg << "Error " << e->text << " at " << l->getPosition(l->tokenPosition); + msg << "Error " << e->text << " at " << l->getPosition(l->tokenLastEnd); delete l; l = oldLex; throw new CScriptException(msg.str()); @@ -737,6 +799,17 @@ string CTinyJS::evaluate(const string &code) { return result; } +void CTinyJS::parseFunctionArguments(CScriptVar *funcVar) { + l->match('('); + while (l->tk!=')') { + CScriptVar *funcParam = new CScriptVar(l->tkStr, "", SCRIPTVAR_PARAMETER); + funcVar->addChildNoDup(funcParam); + l->match(LEX_ID); + if (l->tk!=')') l->match(','); + } + l->match(')'); +} + void CTinyJS::addNative(const string &funcDesc, JSCallback ptr) { CScriptLex *oldLex = l; l = new CScriptLex(funcDesc); @@ -744,20 +817,31 @@ void CTinyJS::addNative(const string &funcDesc, JSCallback ptr) { CScriptVar *funcVar = new CScriptVar(l->tkStr, "", SCRIPTVAR_FUNCTION | SCRIPTVAR_NATIVE); funcVar->setCallback(ptr); l->match(LEX_ID); - l->match('('); - while (l->tk!=')') { - CScriptVar *funcParam = new CScriptVar(l->tkStr, "", SCRIPTVAR_PARAMETER); - funcVar->addChildNoDup(funcParam); - l->match(LEX_ID); - if (l->tk!=')') l->match(','); - } - l->match(')'); + parseFunctionArguments(funcVar); delete l; l = oldLex; root->addChild(funcVar); } +CScriptVar *CTinyJS::parseFunctionDefinition() { + // actually parse a function... + l->match(LEX_R_FUNCTION); + string funcName = TINYJS_TEMP_NAME; + /* we can have functions without names */ + if (l->tk==LEX_ID) { + funcName = l->tkStr; + l->match(LEX_ID); + } + CScriptVar *funcVar = new CScriptVar(funcName, "", SCRIPTVAR_FUNCTION); + parseFunctionArguments(funcVar); + int funcBegin = l->tokenStart; + bool noexecute = false; + block(noexecute); + funcVar->data = l->getSubString(funcBegin); + return funcVar; +} + CScriptVar *CTinyJS::factor(bool &execute) { if (l->tk=='(') { l->match('('); @@ -780,12 +864,13 @@ CScriptVar *CTinyJS::factor(bool &execute) { if (l->tk==LEX_ID) { CScriptVar *a = symbol_base->findRecursive(l->tkStr); if (!a) { + /* Variable doesn't exist! */ if (execute) { ostringstream msg; msg << "Unknown ID '" << l->tkStr << "'"; throw new CScriptException(msg.str()); } - a = new CScriptVar(l->tkStr, ""); + a = new CScriptVar(l->tkStr); } l->match(LEX_ID); while (l->tk=='(' || l->tk=='.' || l->tk=='[') { @@ -866,13 +951,13 @@ CScriptVar *CTinyJS::factor(bool &execute) { a = child; } else ASSERT(0); - - // return value? } return a; } if (l->tk==LEX_INT || l->tk==LEX_FLOAT) { - CScriptVar *a = new CScriptVar(TINYJS_TEMP_NAME, l->tkStr, SCRIPTVAR_NUMERIC | ((l->tk==LEX_INT)?SCRIPTVAR_INTEGER:0)); + CScriptVar *a = new CScriptVar( + TINYJS_TEMP_NAME, l->tkStr, + ((l->tk==LEX_INT)?SCRIPTVAR_INTEGER:SCRIPTVAR_DOUBLE)); l->match(l->tk); return a; } @@ -887,7 +972,9 @@ CScriptVar *CTinyJS::factor(bool &execute) { l->match('{'); while (l->tk != '}') { string id = l->tkStr; - l->match(LEX_STR); + // we only allow strings or IDs on the left hand side of an initialisation + if (l->tk==LEX_STR) l->match(LEX_STR); + else l->match(LEX_ID); l->match(':'); CScriptVar *a = base(execute); contents->addNamedChild(id, a); @@ -903,10 +990,10 @@ CScriptVar *CTinyJS::factor(bool &execute) { /* JSON-style array */ l->match('['); int idx = 0; - while (l->tk != ']') { + while (l->tk != ']') { char idx_str[16]; // big enough for 2^32 sprintf(idx_str,"%d",idx); - + CScriptVar *a = base(execute); contents->addNamedChild(idx_str, a); // no need to clean here, as it will definitely be used @@ -916,6 +1003,13 @@ CScriptVar *CTinyJS::factor(bool &execute) { l->match(']'); return contents; } + if (l->tk==LEX_R_FUNCTION) { + CScriptVar *funcVar = parseFunctionDefinition(); + if (funcVar->name != TINYJS_TEMP_NAME) + TRACE("Functions not defined at statement-level are not meant to have a name"); + return funcVar; + } + // Nothing we can do here... just hope it's the end... l->match(LEX_EOF); return new CScriptVar(); } @@ -923,7 +1017,7 @@ CScriptVar *CTinyJS::factor(bool &execute) { CScriptVar *CTinyJS::unary(bool &execute) { CScriptVar *a; if (l->tk=='!') { - l->match('!'); // negation + l->match('!'); // binary not a = factor(execute); if (execute) { CScriptVar zero(0); @@ -1085,40 +1179,55 @@ CScriptVar *CTinyJS::base(bool &execute) { void CTinyJS::block(bool &execute) { // TODO: fast skip of blocks l->match('{'); - while (l->tk && l->tk!='}') + if (execute) { + while (l->tk && l->tk!='}') statement(execute); - l->match('}'); + l->match('}'); + } else { + int brackets = 1; + while (l->tk && brackets) { + if (l->tk == '{') brackets++; + if (l->tk == '}') brackets--; + l->match(l->tk); + } + } + } void CTinyJS::statement(bool &execute) { - if (l->tk=='{') { + if (l->tk==LEX_ID || + l->tk==LEX_INT || + l->tk==LEX_FLOAT || + l->tk==LEX_STR || + l->tk=='-') { + /* Execute a simple statement that only contains basic arithmetic... */ + CLEAN(base(execute)); + l->match(';'); + } else if (l->tk=='{') { + /* A block of code */ block(execute); + } else if (l->tk==';') { + /* Empty statement - to allow things like ;;; */ + l->match(';'); } else if (l->tk==LEX_R_VAR) { - // variable creation + /* variable creation. TODO - we need a better way of parsing the left + * hand side. Maybe just have a flag called can_create_var that we + * set and then we parse as if we're doing a normal equals.*/ l->match(LEX_R_VAR); CScriptVar *a = 0; - if (execute) { - a = symbol_base->findChild(l->tkStr); - if (!a) { - a = new CScriptVar(l->tkStr, ""); - symbol_base->addChild(a); - } - } + if (execute) + a = symbol_base->findChildOrCreate(l->tkStr); l->match(LEX_ID); // now do stuff defined with dots while (l->tk == '.') { l->match('.'); if (execute) { CScriptVar *lastA = a; - a = lastA->findChild(l->tkStr); - if (!a) { - a = new CScriptVar(l->tkStr, ""); - lastA->addChild(a); - } + a = lastA->findChildOrCreate(l->tkStr); } l->match(LEX_ID); } - //. sort out initialiser + // sort out initialiser if (l->tk == '=') { l->match('='); CScriptVar *var = base(execute); @@ -1127,9 +1236,6 @@ void CTinyJS::statement(bool &execute) { CLEAN(var); } l->match(';'); - } else if (l->tk==LEX_ID) { - CLEAN(base(execute)); - l->match(';'); } else if (l->tk==LEX_R_IF) { l->match(LEX_R_IF); l->match('('); @@ -1148,14 +1254,14 @@ void CTinyJS::statement(bool &execute) { // there's definitely some opportunity for optimisation here l->match(LEX_R_WHILE); l->match('('); - int whileCondStart = l->tokenPosition; + int whileCondStart = l->tokenStart; bool noexecute = false; CScriptVar *cond = base(execute); bool loopCond = execute && cond->getBool(); CLEAN(cond); CScriptLex *whileCond = l->getSubLex(whileCondStart); l->match(')'); - int whileBodyStart = l->tokenPosition; + int whileBodyStart = l->tokenStart; statement(loopCond ? execute : noexecute); CScriptLex *whileBody = l->getSubLex(whileBodyStart); CScriptLex *oldLex = l; @@ -1178,7 +1284,7 @@ void CTinyJS::statement(bool &execute) { if (loopCount<=0) { root->trace(); - TRACE("WHILE Loop exceeded %d iterations at %s\n", TINYJS_LOOP_MAX_ITERATIONS, l->getPosition(l->tokenPosition).c_str()); + TRACE("WHILE Loop exceeded %d iterations at %s\n", TINYJS_LOOP_MAX_ITERATIONS, l->getPosition(l->tokenLastEnd).c_str()); throw new CScriptException("LOOP_ERROR"); } } else if (l->tk==LEX_R_FOR) { @@ -1186,18 +1292,18 @@ void CTinyJS::statement(bool &execute) { l->match('('); statement(execute); // initialisation //l->match(';'); - int forCondStart = l->tokenPosition; + int forCondStart = l->tokenStart; bool noexecute = false; CScriptVar *cond = base(execute); // condition bool loopCond = execute && cond->getBool(); CLEAN(cond); CScriptLex *forCond = l->getSubLex(forCondStart); l->match(';'); - int forIterStart = l->tokenPosition; + int forIterStart = l->tokenStart; base(noexecute); // iterator CScriptLex *forIter = l->getSubLex(forIterStart); l->match(')'); - int forBodyStart = l->tokenPosition; + int forBodyStart = l->tokenStart; statement(loopCond ? execute : noexecute); CScriptLex *forBody = l->getSubLex(forBodyStart); CScriptLex *oldLex = l; @@ -1230,7 +1336,7 @@ void CTinyJS::statement(bool &execute) { delete forBody; if (loopCount<=0) { root->trace(); - TRACE("FOR Loop exceeded %d iterations at %s\n", TINYJS_LOOP_MAX_ITERATIONS, l->getPosition(l->tokenPosition).c_str()); + TRACE("FOR Loop exceeded %d iterations at %s\n", TINYJS_LOOP_MAX_ITERATIONS, l->getPosition(l->tokenLastEnd).c_str()); throw new CScriptException("LOOP_ERROR"); } } else if (l->tk==LEX_R_RETURN) { @@ -1247,25 +1353,14 @@ void CTinyJS::statement(bool &execute) { CLEAN(result); l->match(';'); } else if (l->tk==LEX_R_FUNCTION) { - // actually parse a function... - l->match(LEX_R_FUNCTION); - string funcName = l->tkStr; - bool noexecute = false; - CScriptVar *funcVar = new CScriptVar(funcName, "", SCRIPTVAR_FUNCTION); - symbol_base->addChildNoDup(funcVar); - l->match(LEX_ID); - // add all parameters - l->match('('); - while (l->tk == LEX_ID) { - CScriptVar *param = new CScriptVar(l->tkStr, "", SCRIPTVAR_PARAMETER); - funcVar->addChildNoDup(param); - l->match(LEX_ID); - if (l->tk!=')') l->match(','); - } - l->match(')'); - int funcBegin = l->tokenPosition; - block(noexecute); - funcVar->setString(l->getSubString(funcBegin)); + CScriptVar *funcVar = parseFunctionDefinition(); + if (execute) { + if (funcVar->name == TINYJS_TEMP_NAME) + TRACE("Functions defined at statement-level are meant to have a name"); + else + symbol_base->addChildNoDup(funcVar); + } else + CLEAN(funcVar); } else l->match(LEX_EOF); } diff --git a/TinyJS.h b/TinyJS.h index 70907e8..a9d0afe 100755 --- a/TinyJS.h +++ b/TinyJS.h @@ -65,12 +65,20 @@ enum LEX_TYPES { }; enum SCRIPTVAR_FLAGS { - SCRIPTVAR_FUNCTION = 1, - SCRIPTVAR_NATIVE = 2, - SCRIPTVAR_NUMERIC = 4, - SCRIPTVAR_INTEGER = 8, // eg. not floating point - SCRIPTVAR_VARTYPEMASK = 12, - SCRIPTVAR_PARAMETER = 16 + SCRIPTVAR_UNDEFINED = 0, + SCRIPTVAR_FUNCTION = 1, + SCRIPTVAR_PARAMETER = 2, + SCRIPTVAR_NATIVE = 4, + SCRIPTVAR_DOUBLE = 8, // floating point double + SCRIPTVAR_INTEGER = 16, // integer number + SCRIPTVAR_STRING = 32, // string + SCRIPTVAR_NUMERICMASK = SCRIPTVAR_DOUBLE | + SCRIPTVAR_INTEGER, + SCRIPTVAR_VARTYPEMASK = SCRIPTVAR_DOUBLE | + SCRIPTVAR_INTEGER | + SCRIPTVAR_STRING | + SCRIPTVAR_FUNCTION, + }; #define TINYJS_RETURN_VAR "return" @@ -95,7 +103,9 @@ public: char currCh, nextCh; int tk; ///< The type of the token that we have - int tokenPosition; ///< Position in the data at the beginning of the token we have here + int tokenStart; ///< Position in the data at the beginning of the token we have here + int tokenEnd; ///< Position in the data at the last character of the token we have here + int tokenLastEnd; ///< Position in the data at the last character of the last token std::string tkStr; ///< Data contained in the token we have here void match(int expected_tk); ///< Lexical match wotsit @@ -115,7 +125,7 @@ protected: int dataStart, dataEnd; ///< Start and end position in data string bool dataOwned; ///< Do we own this data string? - int dataPos; ///< Position in data + int dataPos; ///< Position in data (we CAN go past the end of the string here) void getNextCh(); void getNextToken(); ///< Get the text token from our text string @@ -130,13 +140,14 @@ class CScriptVar { public: CScriptVar(void); - CScriptVar(std::string varName, std::string varData=TINYJS_BLANK_DATA, int varFlags = 0); + CScriptVar(std::string varName, std::string varData=TINYJS_BLANK_DATA, int varFlags = SCRIPTVAR_UNDEFINED); CScriptVar(double varData); CScriptVar(int val); ~CScriptVar(void); - CScriptVar *findChild(const std::string &childName); ///< Tries to find a child with the given name, may return 0 + CScriptVar *findChild(const std::string &childName); ///< Tries to find a child with the given name, may return 0 CScriptVar *findChildOrCreate(const std::string &childName); ///< Tries to find a child with the given name, or will create it + CScriptVar *findChildOrCreateByPath(const std::string &path); ///< Tries to find a child with the given path (separated by dots) CScriptVar *findRecursive(const std::string &childName); ///< Finds a child, looking recursively up the tree void addChild(CScriptVar *child); void addNamedChild(const std::string &childName, CScriptVar *child); ///< add the named child. if it is already owned, copy it. @@ -145,7 +156,7 @@ public: void removeAllChildren(); CScriptVar *getRoot(); ///< Get the absolute root of the tree - const std::string &getName(); + const std::string &getName(); int getInt(); bool getBool() { return getInt() != 0; } double getDouble(); @@ -156,9 +167,10 @@ public: void setString(const std::string &str); void setVoid(); - bool isInt() { return (flags&SCRIPTVAR_NUMERIC)!=0 && (flags&SCRIPTVAR_INTEGER)!=0; } - bool isDouble() { return (flags&SCRIPTVAR_NUMERIC)!=0 && (flags&SCRIPTVAR_INTEGER)==0; } - bool isNumeric() { return (flags&SCRIPTVAR_NUMERIC)!=0; } + bool isInt() { return (flags&SCRIPTVAR_INTEGER)!=0; } + bool isDouble() { return (flags&SCRIPTVAR_DOUBLE)==0; } + bool isString() { return (flags&SCRIPTVAR_STRING)!=0; } + bool isNumeric() { return (flags&SCRIPTVAR_NUMERICMASK)!=0; } bool isFunction() { return (flags&SCRIPTVAR_FUNCTION)!=0; } bool isParameter() { return (flags&SCRIPTVAR_PARAMETER)!=0; } bool isNative() { return (flags&SCRIPTVAR_NATIVE)!=0; } @@ -169,6 +181,7 @@ public: void trace(std::string indentStr = ""); ///< Dump out the contents of this using trace + std::string getFlagsAsString(); void getJSON(std::ostringstream &destination); ///< Write out all the JS code needed to recreate this script variable to the stream (as JSON) void setCallback(JSCallback callback); @@ -226,6 +239,9 @@ private: CScriptVar *base(bool &execute); void block(bool &execute); void statement(bool &execute); + // parsing utility functions + CScriptVar *parseFunctionDefinition(); + void parseFunctionArguments(CScriptVar *funcVar); }; #endif diff --git a/run_tests.cpp b/run_tests.cpp new file mode 100644 index 0000000..b665138 --- /dev/null +++ b/run_tests.cpp @@ -0,0 +1,118 @@ +/* + * TinyJS + * + * A single-file Javascript-alike engine + * + * Authored By Gordon Williams + * + * Copyright (C) 2009 Pur3 Ltd + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +/* + * This is a program to run all the tests in the tests folder... + */ + +#include +#include +#include +#include + +#include "TinyJS.h" +#include "TinyJS_Functions.h" + +bool run_test(const char *filename) { + printf("TEST %s ", filename); + struct stat results; + if (!stat(filename, &results) == 0) { + printf("Cannot stat file! '%s'\n", filename); + return false; + } + int size = results.st_size; + FILE *file = fopen( filename, "rb" ); + /* if we open as text, the number of bytes read may be > the size we read */ + if( !file ) { + printf("Unable to open file! '%s'\n", filename); + return false; + } + char *buffer = new char[size+1]; + long actualRead = fread(buffer,1,size,file); + buffer[actualRead]=0; + buffer[size]=0; + fclose(file); + + CTinyJS s; + s.root->addChild(new CScriptVar("result","0",SCRIPTVAR_INTEGER)); + try { + s.execute(buffer); + } catch (CScriptException *e) { + printf("ERROR: %s\n", e->text.c_str()); + } + bool pass = s.root->findChildOrCreate("result")->getBool(); + + if (pass) + printf("PASS\n"); + else { + char fn[64]; + sprintf(fn, "%s.fail.js", filename); + FILE *f = fopen(fn, "wt"); + if (f) { + std::ostringstream symbols; + s.root->getJSON(symbols); + fprintf(f, "%s", symbols.str().c_str()); + fclose(f); + } + + printf("FAIL - symbols written to %s\n", fn); + } + + delete[] buffer; + return pass; +} + +int main(int argc, char **argv) +{ + printf("TinyJS test runner\n"); + printf("USAGE:\n"); + printf(" ./run_tests test.js : run just one test\n"); + printf(" ./run_tests : run all tests\n"); + if (argc==2) { + return !run_test(argv[1]); + } + + int test_num = 1; + int count = 0; + int passed = 0; + + while (test_num<1000) { + char fn[32]; + sprintf(fn, "tests/test%03d.js", test_num); + // check if the file exists - if not, assume we're at the end of our tests + FILE *f = fopen(fn,"r"); + if (!f) break; + fclose(f); + + if (run_test(fn)) + passed++; + count++; + test_num++; + } + + printf("Done. %d tests, %d pass, %d fail\n", count, passed, count-passed); + + return 0; +} diff --git a/tests/test001.js b/tests/test001.js new file mode 100644 index 0000000..e0d95dc --- /dev/null +++ b/tests/test001.js @@ -0,0 +1,2 @@ +// simply testing we can return the correct value +result = 1; diff --git a/tests/test002.js b/tests/test002.js new file mode 100644 index 0000000..cebaa85 --- /dev/null +++ b/tests/test002.js @@ -0,0 +1,3 @@ +// comparison +var a = 42; +result = a==42; diff --git a/tests/test003.js b/tests/test003.js new file mode 100644 index 0000000..55126fa --- /dev/null +++ b/tests/test003.js @@ -0,0 +1,5 @@ +// simple for loop +var a = 0; +var i; +for (i=1;i<10;i++) a = a + i; +result = a==45; diff --git a/tests/test004.js b/tests/test004.js new file mode 100644 index 0000000..2f07c8b --- /dev/null +++ b/tests/test004.js @@ -0,0 +1,4 @@ +// simple if +var a = 42; +if (a < 43) + result = 1; diff --git a/tests/test005.js b/tests/test005.js new file mode 100644 index 0000000..f944064 --- /dev/null +++ b/tests/test005.js @@ -0,0 +1,4 @@ +// simple for loop containing initialisation, using += +var a = 0; +for (var i=1;i<10;i++) a += i; +result = a==45; diff --git a/tests/test006.js b/tests/test006.js new file mode 100644 index 0000000..292ad5e --- /dev/null +++ b/tests/test006.js @@ -0,0 +1,3 @@ +// simple function +function add(x,y) { return x+y; } +result = add(3,6)==9; diff --git a/tests/test007.js b/tests/test007.js new file mode 100644 index 0000000..9a43bc6 --- /dev/null +++ b/tests/test007.js @@ -0,0 +1,4 @@ +// simple function scoping test +var a = 7; +function add(x,y) { var a=x+y; return a; } +result = add(3,6)==9 && a==7; diff --git a/tests/test008.js b/tests/test008.js new file mode 100644 index 0000000..8596ee2 --- /dev/null +++ b/tests/test008.js @@ -0,0 +1,5 @@ +// functions in variables +var bob; +bob.add = function(x,y) { return x+y; }; + +result = bob.add(3,6)==9; diff --git a/tests/test009.js b/tests/test009.js new file mode 100644 index 0000000..08bf42e --- /dev/null +++ b/tests/test009.js @@ -0,0 +1,4 @@ +// functions in variables using JSON-style initialisation +var bob = { add : function(x,y) { return x+y; } }; + +result = bob.add(3,6)==9; diff --git a/tests/test010.js b/tests/test010.js new file mode 100644 index 0000000..82e89da --- /dev/null +++ b/tests/test010.js @@ -0,0 +1,4 @@ +// double function calls +function a(x) { return x+2; } +function b(x) { return a(x)+1; } +result = a(3)==5 && b(3)==6; diff --git a/tests/test011.js b/tests/test011.js new file mode 100644 index 0000000..4eaf641 --- /dev/null +++ b/tests/test011.js @@ -0,0 +1,7 @@ +// recursion +function a(x) { + if (x>1) + return x*a(x-1); + return 1; +} +result = a(5)==1*2*3*4*5; diff --git a/tests/test012.js b/tests/test012.js new file mode 100644 index 0000000..2c50344 --- /dev/null +++ b/tests/test012.js @@ -0,0 +1,6 @@ +// if .. else +var a = 42; +if (a != 42) + result = 0; +else + result = 1; diff --git a/tests/test013.js b/tests/test013.js new file mode 100644 index 0000000..a2fb08f --- /dev/null +++ b/tests/test013.js @@ -0,0 +1,7 @@ +// if .. else with blocks +var a = 42; +if (a != 42) { + result = 0; +} else { + result = 1; +}