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
master
pur3mail 15 years ago
parent f9e358d3b8
commit ade7a91672

@ -3,18 +3,21 @@ CFLAGS=-c -g -Wall
LDFLAGS= LDFLAGS=
SOURCES= \ SOURCES= \
Script.cpp \
TinyJS.cpp \ TinyJS.cpp \
TinyJS_Functions.cpp TinyJS_Functions.cpp
OBJECTS=$(SOURCES:.cpp=.o) OBJECTS=$(SOURCES:.cpp=.o)
EXECUTABLE=Script
#all: $(SOURCES) $(EXECUTABLE) all: run_tests Script
all: $(EXECUTABLE)
$(EXECUTABLE): $(OBJECTS) run_tests: run_tests.o $(OBJECTS)
$(CC) $(LDFLAGS) $(OBJECTS) -o $@ $(CC) $(LDFLAGS) run_tests.o $(OBJECTS) -o $@
Script: Script.o $(OBJECTS)
$(CC) $(LDFLAGS) Script.o $(OBJECTS) -o $@
.cpp.o: .cpp.o:
$(CC) $(CFLAGS) $< -o $@ $(CC) $(CFLAGS) $< -o $@
clean:
rm run_tests Script run_tests.o Script.o $(OBJECTS)

@ -54,10 +54,14 @@ int main(int argc, char **argv)
s.addNative("function print(text)", &js_print); s.addNative("function print(text)", &js_print);
s.addNative("function dump()", &js_dump); s.addNative("function dump()", &js_dump);
/* Execute out bit of code - we could call 'evaluate' here if /* Execute out bit of code - we could call 'evaluate' here if
we wanted something returned */ we wanted something returned */
s.execute("var lets_quit = 0; function quit() { lets_quit = 1; }"); try {
s.execute("print(\"Interactive mode... Type quit(); to exit, or print(...); to print something, or dump() to dump the symbol table!\");"); 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") { while (s.evaluate("lets_quit") == "0") {
char buffer[2048]; char buffer[2048];
fgets ( buffer, sizeof(buffer), stdin ); fgets ( buffer, sizeof(buffer), stdin );

@ -32,6 +32,18 @@
Added nil Added nil
Added rough JSON parsing Added rough JSON parsing
Improved example app 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" #include "TinyJS.h"
@ -138,7 +150,9 @@ CScriptLex::~CScriptLex(void)
void CScriptLex::reset() { void CScriptLex::reset() {
dataPos = dataStart; dataPos = dataStart;
tokenPosition = 0; tokenStart = 0;
tokenEnd = 0;
tokenLastEnd = 0;
tk = 0; tk = 0;
tkStr = ""; tkStr = "";
getNextCh(); getNextCh();
@ -150,7 +164,7 @@ void CScriptLex::match(int expected_tk) {
if (tk!=expected_tk) { if (tk!=expected_tk) {
ostringstream errorString; ostringstream errorString;
errorString << "Got " << getTokenStr(tk) << " expected " << getTokenStr(expected_tk) errorString << "Got " << getTokenStr(tk) << " expected " << getTokenStr(expected_tk)
<< " at " << getPosition(tokenPosition) << "in '" << data << "'"; << " at " << getPosition(tokenStart) << "in '" << data << "'";
throw new CScriptException(errorString.str()); throw new CScriptException(errorString.str());
} }
getNextToken(); getNextToken();
@ -199,9 +213,10 @@ string CScriptLex::getTokenStr(int token) {
void CScriptLex::getNextCh() { void CScriptLex::getNextCh() {
currCh = nextCh; currCh = nextCh;
if (dataPos < dataEnd) if (dataPos < dataEnd)
nextCh = data[dataPos++]; nextCh = data[dataPos];
else else
nextCh = 0; nextCh = 0;
dataPos++;
} }
void CScriptLex::getNextToken() { void CScriptLex::getNextToken() {
@ -224,7 +239,7 @@ void CScriptLex::getNextToken() {
return; return;
} }
// record beginning of this token // record beginning of this token
tokenPosition = dataPos-2; tokenStart = dataPos-2;
// tokens // tokens
if (isAlpha(currCh)) { // IDs if (isAlpha(currCh)) { // IDs
while (isAlpha(currCh) || isNumeric(currCh)) { while (isAlpha(currCh) || isNumeric(currCh)) {
@ -242,9 +257,7 @@ void CScriptLex::getNextToken() {
else if (tkStr=="true") tk = LEX_R_TRUE; else if (tkStr=="true") tk = LEX_R_TRUE;
else if (tkStr=="false") tk = LEX_R_FALSE; else if (tkStr=="false") tk = LEX_R_FALSE;
else if (tkStr=="nil") tk = LEX_R_NIL; else if (tkStr=="nil") tk = LEX_R_NIL;
return; } else if (isNumeric(currCh)) { // Numbers
}
if (isNumeric(currCh)) { // Numbers
tk = LEX_INT; tk = LEX_INT;
while (isNumeric(currCh)) { while (isNumeric(currCh)) {
tkStr += currCh; tkStr += currCh;
@ -259,9 +272,7 @@ void CScriptLex::getNextToken() {
getNextCh(); getNextCh();
} }
} }
return; } else if (currCh=='"') {
}
if (currCh=='"') {
getNextCh(); getNextCh();
while (currCh && currCh!='"') { while (currCh && currCh!='"') {
if (currCh == '\\') { if (currCh == '\\') {
@ -279,52 +290,56 @@ void CScriptLex::getNextToken() {
} }
getNextCh(); getNextCh();
tk = LEX_STR; tk = LEX_STR;
return; } else {
} // single chars
// single chars tk = currCh;
tk = currCh; if (currCh) getNextCh();
getNextCh(); if (tk=='=' && currCh=='=') {
if (tk=='=' && currCh=='=') { tk = LEX_EQUAL;
tk = LEX_EQUAL; getNextCh();
getNextCh(); } else if (tk=='!' && currCh=='=') {
} else if (tk=='!' && currCh=='=') { tk = LEX_NEQUAL;
tk = LEX_NEQUAL; getNextCh();
getNextCh(); } else if (tk=='<' && currCh=='=') {
} else if (tk=='<' && currCh=='=') { tk = LEX_LEQUAL;
tk = LEX_LEQUAL; getNextCh();
getNextCh(); } else if (tk=='>' && currCh=='=') {
} else if (tk=='>' && currCh=='=') { tk = LEX_GEQUAL;
tk = LEX_GEQUAL; getNextCh();
getNextCh(); } else if (tk=='+' && currCh=='=') {
} else if (tk=='+' && currCh=='=') { tk = LEX_PLUSEQUAL;
tk = LEX_PLUSEQUAL; getNextCh();
getNextCh(); } else if (tk=='-' && currCh=='=') {
} else if (tk=='-' && currCh=='=') { tk = LEX_MINUSEQUAL;
tk = LEX_MINUSEQUAL; getNextCh();
getNextCh(); } else if (tk=='+' && currCh=='+') {
} else if (tk=='+' && currCh=='+') { tk = LEX_PLUSPLUS;
tk = LEX_PLUSPLUS; getNextCh();
getNextCh(); } else if (tk=='-' && currCh=='-') {
} else if (tk=='-' && currCh=='-') { tk = LEX_MINUSMINUS;
tk = LEX_MINUSMINUS; getNextCh();
getNextCh(); } else if (tk=='&' && currCh=='&') {
} else if (tk=='&' && currCh=='&') { tk = LEX_ANDAND;
tk = LEX_ANDAND; getNextCh();
getNextCh(); } else if (tk=='|' && currCh=='|') {
} else if (tk=='|' && currCh=='|') { tk = LEX_OROR;
tk = LEX_OROR; getNextCh();
getNextCh(); }
} }
/* This isn't quite right yet */
tokenLastEnd = tokenEnd;
tokenEnd = dataPos-3;
} }
string CScriptLex::getSubString(int lastPosition) { 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 /* save a memory alloc by using our data array to create the
substring */ substring */
char old = data[tokenPosition]; char old = data[lastCharIdx];
data[tokenPosition] = 0; data[lastCharIdx] = 0;
std::string value = &data[lastPosition]; std::string value = &data[lastPosition];
data[tokenPosition] = old; data[lastCharIdx] = old;
return value; return value;
} else { } else {
return std::string(&data[lastPosition]); return std::string(&data[lastPosition]);
@ -333,8 +348,9 @@ string CScriptLex::getSubString(int lastPosition) {
CScriptLex *CScriptLex::getSubLex(int lastPosition) { CScriptLex *CScriptLex::getSubLex(int lastPosition) {
if (dataPos < dataEnd) int lastCharIdx = tokenLastEnd+1;
return new CScriptLex( this, lastPosition, tokenPosition); if (lastCharIdx < dataEnd)
return new CScriptLex( this, lastPosition, lastCharIdx);
else else
return new CScriptLex( this, lastPosition, dataEnd ); return new CScriptLex( this, lastPosition, dataEnd );
} }
@ -409,7 +425,7 @@ CScriptVar *CScriptVar::findChild(const string &childName) {
} }
CScriptVar *CScriptVar::findChildOrCreate(const string &childName) { CScriptVar *CScriptVar::findChildOrCreate(const string &childName) {
CScriptVar *v = findChild(childName); CScriptVar *v = findChild(childName);
if (!v) { if (!v) {
v = new CScriptVar(childName); v = new CScriptVar(childName);
addChild(v); addChild(v);
@ -417,6 +433,15 @@ CScriptVar *CScriptVar::findChildOrCreate(const string &childName) {
return v; 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 *CScriptVar::findRecursive(const string &childName) {
CScriptVar *v = findChild(childName); CScriptVar *v = findChild(childName);
if (!v && parent) if (!v && parent)
@ -438,7 +463,7 @@ void CScriptVar::addChild(CScriptVar *child) {
} }
void CScriptVar::addNamedChild(const std::string &childName, CScriptVar *child) { void CScriptVar::addNamedChild(const std::string &childName, CScriptVar *child) {
if (!child) { if (!child) {
// not given anything - just add empty child // not given anything - just add empty child
addChild(new CScriptVar(childName)); addChild(new CScriptVar(childName));
} else if (child->parent) { } else if (child->parent) {
@ -446,7 +471,7 @@ void CScriptVar::addNamedChild(const std::string &childName, CScriptVar *child)
CScriptVar *c = new CScriptVar(childName); CScriptVar *c = new CScriptVar(childName);
c->copyValue(child); c->copyValue(child);
addChild(c); addChild(c);
} else { } else {
// no owners - just change name and add it // no owners - just change name and add it
child->name = childName; child->name = childName;
addChild(child); addChild(child);
@ -518,20 +543,20 @@ const string &CScriptVar::getString() {
void CScriptVar::setInt(int val) { void CScriptVar::setInt(int val) {
char buf[256]; char buf[256];
sprintf(buf, "%d", val); sprintf(buf, "%d", val);
flags = (flags&~SCRIPTVAR_VARTYPEMASK) | SCRIPTVAR_INTEGER;
data = buf; data = buf;
flags = SCRIPTVAR_NUMERIC | SCRIPTVAR_INTEGER;
} }
void CScriptVar::setDouble(double val) { void CScriptVar::setDouble(double val) {
char buf[256]; char buf[256];
sprintf(buf, "%lf", val); sprintf(buf, "%lf", val);
flags = (flags&~SCRIPTVAR_VARTYPEMASK) | SCRIPTVAR_DOUBLE;
data = buf; data = buf;
flags = SCRIPTVAR_NUMERIC;
} }
void CScriptVar::setString(const string &str) { void CScriptVar::setString(const string &str) {
// name sure it's not still a number or integer // name sure it's not still a number or integer
flags &= ~SCRIPTVAR_VARTYPEMASK; flags = (flags&~SCRIPTVAR_VARTYPEMASK) | SCRIPTVAR_STRING;
data = str; data = str;
} }
@ -611,6 +636,7 @@ CScriptVar *CScriptVar::mathsOp(CScriptVar *b, int op) {
void CScriptVar::copyValue(CScriptVar *val) { void CScriptVar::copyValue(CScriptVar *val) {
if (val) { if (val) {
// we *don't* copy the name
data = val->data; data = val->data;
flags = (flags & ~SCRIPTVAR_VARTYPEMASK) | (val->flags & SCRIPTVAR_VARTYPEMASK); flags = (flags & ~SCRIPTVAR_VARTYPEMASK) | (val->flags & SCRIPTVAR_VARTYPEMASK);
// copy children // copy children
@ -637,7 +663,11 @@ CScriptVar *CScriptVar::deepCopy() {
} }
void CScriptVar::trace(string indentStr) { 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+" "; string indent = indentStr+" ";
CScriptVar *child = firstChild; CScriptVar *child = firstChild;
while (child) { 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() { string CScriptVar::getParsableString() {
// Numbers can just be put in directly
if (isNumeric()) if (isNumeric())
return getString(); 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) { void CScriptVar::getJSON(ostringstream &destination) {
@ -659,9 +721,9 @@ void CScriptVar::getJSON(ostringstream &destination) {
CScriptVar *child = firstChild; CScriptVar *child = firstChild;
while (child) { while (child) {
destination << getJSString(child->name) << " : "; destination << getJSString(child->name) << " : ";
child->getJSON(destination); child->getJSON(destination);
child = child->nextSibling; child = child->nextSibling;
if (child) if (child)
destination << ",\n"; destination << ",\n";
else else
destination << "\n"; destination << "\n";
@ -704,7 +766,7 @@ void CTinyJS::execute(const string &code) {
while (l->tk) statement(execute); while (l->tk) statement(execute);
} catch (CScriptException *e) { } catch (CScriptException *e) {
ostringstream msg; ostringstream msg;
msg << "Error " << e->text << " at " << l->getPosition(l->tokenPosition); msg << "Error " << e->text << " at " << l->getPosition(l->tokenLastEnd);
delete l; delete l;
l = oldLex; l = oldLex;
throw new CScriptException(msg.str()); throw new CScriptException(msg.str());
@ -723,7 +785,7 @@ string CTinyJS::evaluate(const string &code) {
v = base(execute); v = base(execute);
} catch (CScriptException *e) { } catch (CScriptException *e) {
ostringstream msg; ostringstream msg;
msg << "Error " << e->text << " at " << l->getPosition(l->tokenPosition); msg << "Error " << e->text << " at " << l->getPosition(l->tokenLastEnd);
delete l; delete l;
l = oldLex; l = oldLex;
throw new CScriptException(msg.str()); throw new CScriptException(msg.str());
@ -737,6 +799,17 @@ string CTinyJS::evaluate(const string &code) {
return result; 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) { void CTinyJS::addNative(const string &funcDesc, JSCallback ptr) {
CScriptLex *oldLex = l; CScriptLex *oldLex = l;
l = new CScriptLex(funcDesc); 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); CScriptVar *funcVar = new CScriptVar(l->tkStr, "", SCRIPTVAR_FUNCTION | SCRIPTVAR_NATIVE);
funcVar->setCallback(ptr); funcVar->setCallback(ptr);
l->match(LEX_ID); l->match(LEX_ID);
l->match('('); parseFunctionArguments(funcVar);
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(')');
delete l; delete l;
l = oldLex; l = oldLex;
root->addChild(funcVar); 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) { CScriptVar *CTinyJS::factor(bool &execute) {
if (l->tk=='(') { if (l->tk=='(') {
l->match('('); l->match('(');
@ -780,12 +864,13 @@ CScriptVar *CTinyJS::factor(bool &execute) {
if (l->tk==LEX_ID) { if (l->tk==LEX_ID) {
CScriptVar *a = symbol_base->findRecursive(l->tkStr); CScriptVar *a = symbol_base->findRecursive(l->tkStr);
if (!a) { if (!a) {
/* Variable doesn't exist! */
if (execute) { if (execute) {
ostringstream msg; ostringstream msg;
msg << "Unknown ID '" << l->tkStr << "'"; msg << "Unknown ID '" << l->tkStr << "'";
throw new CScriptException(msg.str()); throw new CScriptException(msg.str());
} }
a = new CScriptVar(l->tkStr, ""); a = new CScriptVar(l->tkStr);
} }
l->match(LEX_ID); l->match(LEX_ID);
while (l->tk=='(' || l->tk=='.' || l->tk=='[') { while (l->tk=='(' || l->tk=='.' || l->tk=='[') {
@ -866,13 +951,13 @@ CScriptVar *CTinyJS::factor(bool &execute) {
a = child; a = child;
} else ASSERT(0); } else ASSERT(0);
// return value?
} }
return a; return a;
} }
if (l->tk==LEX_INT || l->tk==LEX_FLOAT) { 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); l->match(l->tk);
return a; return a;
} }
@ -887,7 +972,9 @@ CScriptVar *CTinyJS::factor(bool &execute) {
l->match('{'); l->match('{');
while (l->tk != '}') { while (l->tk != '}') {
string id = l->tkStr; 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(':'); l->match(':');
CScriptVar *a = base(execute); CScriptVar *a = base(execute);
contents->addNamedChild(id, a); contents->addNamedChild(id, a);
@ -903,10 +990,10 @@ CScriptVar *CTinyJS::factor(bool &execute) {
/* JSON-style array */ /* JSON-style array */
l->match('['); l->match('[');
int idx = 0; int idx = 0;
while (l->tk != ']') { while (l->tk != ']') {
char idx_str[16]; // big enough for 2^32 char idx_str[16]; // big enough for 2^32
sprintf(idx_str,"%d",idx); sprintf(idx_str,"%d",idx);
CScriptVar *a = base(execute); CScriptVar *a = base(execute);
contents->addNamedChild(idx_str, a); contents->addNamedChild(idx_str, a);
// no need to clean here, as it will definitely be used // no need to clean here, as it will definitely be used
@ -916,6 +1003,13 @@ CScriptVar *CTinyJS::factor(bool &execute) {
l->match(']'); l->match(']');
return contents; 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); l->match(LEX_EOF);
return new CScriptVar(); return new CScriptVar();
} }
@ -923,7 +1017,7 @@ CScriptVar *CTinyJS::factor(bool &execute) {
CScriptVar *CTinyJS::unary(bool &execute) { CScriptVar *CTinyJS::unary(bool &execute) {
CScriptVar *a; CScriptVar *a;
if (l->tk=='!') { if (l->tk=='!') {
l->match('!'); // negation l->match('!'); // binary not
a = factor(execute); a = factor(execute);
if (execute) { if (execute) {
CScriptVar zero(0); CScriptVar zero(0);
@ -1085,40 +1179,55 @@ CScriptVar *CTinyJS::base(bool &execute) {
void CTinyJS::block(bool &execute) { void CTinyJS::block(bool &execute) {
// TODO: fast skip of blocks // TODO: fast skip of blocks
l->match('{'); l->match('{');
while (l->tk && l->tk!='}') if (execute) {
while (l->tk && l->tk!='}')
statement(execute); 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) { 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); block(execute);
} else if (l->tk==';') {
/* Empty statement - to allow things like ;;; */
l->match(';');
} else if (l->tk==LEX_R_VAR) { } 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); l->match(LEX_R_VAR);
CScriptVar *a = 0; CScriptVar *a = 0;
if (execute) { if (execute)
a = symbol_base->findChild(l->tkStr); a = symbol_base->findChildOrCreate(l->tkStr);
if (!a) {
a = new CScriptVar(l->tkStr, "");
symbol_base->addChild(a);
}
}
l->match(LEX_ID); l->match(LEX_ID);
// now do stuff defined with dots // now do stuff defined with dots
while (l->tk == '.') { while (l->tk == '.') {
l->match('.'); l->match('.');
if (execute) { if (execute) {
CScriptVar *lastA = a; CScriptVar *lastA = a;
a = lastA->findChild(l->tkStr); a = lastA->findChildOrCreate(l->tkStr);
if (!a) {
a = new CScriptVar(l->tkStr, "");
lastA->addChild(a);
}
} }
l->match(LEX_ID); l->match(LEX_ID);
} }
//. sort out initialiser // sort out initialiser
if (l->tk == '=') { if (l->tk == '=') {
l->match('='); l->match('=');
CScriptVar *var = base(execute); CScriptVar *var = base(execute);
@ -1127,9 +1236,6 @@ void CTinyJS::statement(bool &execute) {
CLEAN(var); CLEAN(var);
} }
l->match(';'); l->match(';');
} else if (l->tk==LEX_ID) {
CLEAN(base(execute));
l->match(';');
} else if (l->tk==LEX_R_IF) { } else if (l->tk==LEX_R_IF) {
l->match(LEX_R_IF); l->match(LEX_R_IF);
l->match('('); l->match('(');
@ -1148,14 +1254,14 @@ void CTinyJS::statement(bool &execute) {
// there's definitely some opportunity for optimisation here // there's definitely some opportunity for optimisation here
l->match(LEX_R_WHILE); l->match(LEX_R_WHILE);
l->match('('); l->match('(');
int whileCondStart = l->tokenPosition; int whileCondStart = l->tokenStart;
bool noexecute = false; bool noexecute = false;
CScriptVar *cond = base(execute); CScriptVar *cond = base(execute);
bool loopCond = execute && cond->getBool(); bool loopCond = execute && cond->getBool();
CLEAN(cond); CLEAN(cond);
CScriptLex *whileCond = l->getSubLex(whileCondStart); CScriptLex *whileCond = l->getSubLex(whileCondStart);
l->match(')'); l->match(')');
int whileBodyStart = l->tokenPosition; int whileBodyStart = l->tokenStart;
statement(loopCond ? execute : noexecute); statement(loopCond ? execute : noexecute);
CScriptLex *whileBody = l->getSubLex(whileBodyStart); CScriptLex *whileBody = l->getSubLex(whileBodyStart);
CScriptLex *oldLex = l; CScriptLex *oldLex = l;
@ -1178,7 +1284,7 @@ void CTinyJS::statement(bool &execute) {
if (loopCount<=0) { if (loopCount<=0) {
root->trace(); 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"); throw new CScriptException("LOOP_ERROR");
} }
} else if (l->tk==LEX_R_FOR) { } else if (l->tk==LEX_R_FOR) {
@ -1186,18 +1292,18 @@ void CTinyJS::statement(bool &execute) {
l->match('('); l->match('(');
statement(execute); // initialisation statement(execute); // initialisation
//l->match(';'); //l->match(';');
int forCondStart = l->tokenPosition; int forCondStart = l->tokenStart;
bool noexecute = false; bool noexecute = false;
CScriptVar *cond = base(execute); // condition CScriptVar *cond = base(execute); // condition
bool loopCond = execute && cond->getBool(); bool loopCond = execute && cond->getBool();
CLEAN(cond); CLEAN(cond);
CScriptLex *forCond = l->getSubLex(forCondStart); CScriptLex *forCond = l->getSubLex(forCondStart);
l->match(';'); l->match(';');
int forIterStart = l->tokenPosition; int forIterStart = l->tokenStart;
base(noexecute); // iterator base(noexecute); // iterator
CScriptLex *forIter = l->getSubLex(forIterStart); CScriptLex *forIter = l->getSubLex(forIterStart);
l->match(')'); l->match(')');
int forBodyStart = l->tokenPosition; int forBodyStart = l->tokenStart;
statement(loopCond ? execute : noexecute); statement(loopCond ? execute : noexecute);
CScriptLex *forBody = l->getSubLex(forBodyStart); CScriptLex *forBody = l->getSubLex(forBodyStart);
CScriptLex *oldLex = l; CScriptLex *oldLex = l;
@ -1230,7 +1336,7 @@ void CTinyJS::statement(bool &execute) {
delete forBody; delete forBody;
if (loopCount<=0) { if (loopCount<=0) {
root->trace(); 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"); throw new CScriptException("LOOP_ERROR");
} }
} else if (l->tk==LEX_R_RETURN) { } else if (l->tk==LEX_R_RETURN) {
@ -1247,25 +1353,14 @@ void CTinyJS::statement(bool &execute) {
CLEAN(result); CLEAN(result);
l->match(';'); l->match(';');
} else if (l->tk==LEX_R_FUNCTION) { } else if (l->tk==LEX_R_FUNCTION) {
// actually parse a function... CScriptVar *funcVar = parseFunctionDefinition();
l->match(LEX_R_FUNCTION); if (execute) {
string funcName = l->tkStr; if (funcVar->name == TINYJS_TEMP_NAME)
bool noexecute = false; TRACE("Functions defined at statement-level are meant to have a name");
CScriptVar *funcVar = new CScriptVar(funcName, "", SCRIPTVAR_FUNCTION); else
symbol_base->addChildNoDup(funcVar); symbol_base->addChildNoDup(funcVar);
l->match(LEX_ID); } else
// add all parameters CLEAN(funcVar);
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));
} else l->match(LEX_EOF); } else l->match(LEX_EOF);
} }

@ -65,12 +65,20 @@ enum LEX_TYPES {
}; };
enum SCRIPTVAR_FLAGS { enum SCRIPTVAR_FLAGS {
SCRIPTVAR_FUNCTION = 1, SCRIPTVAR_UNDEFINED = 0,
SCRIPTVAR_NATIVE = 2, SCRIPTVAR_FUNCTION = 1,
SCRIPTVAR_NUMERIC = 4, SCRIPTVAR_PARAMETER = 2,
SCRIPTVAR_INTEGER = 8, // eg. not floating point SCRIPTVAR_NATIVE = 4,
SCRIPTVAR_VARTYPEMASK = 12, SCRIPTVAR_DOUBLE = 8, // floating point double
SCRIPTVAR_PARAMETER = 16 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" #define TINYJS_RETURN_VAR "return"
@ -95,7 +103,9 @@ public:
char currCh, nextCh; char currCh, nextCh;
int tk; ///< The type of the token that we have 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 std::string tkStr; ///< Data contained in the token we have here
void match(int expected_tk); ///< Lexical match wotsit void match(int expected_tk); ///< Lexical match wotsit
@ -115,7 +125,7 @@ protected:
int dataStart, dataEnd; ///< Start and end position in data string int dataStart, dataEnd; ///< Start and end position in data string
bool dataOwned; ///< Do we own this 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 getNextCh();
void getNextToken(); ///< Get the text token from our text string void getNextToken(); ///< Get the text token from our text string
@ -130,13 +140,14 @@ class CScriptVar
{ {
public: public:
CScriptVar(void); 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(double varData);
CScriptVar(int val); CScriptVar(int val);
~CScriptVar(void); ~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 *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 CScriptVar *findRecursive(const std::string &childName); ///< Finds a child, looking recursively up the tree
void addChild(CScriptVar *child); void addChild(CScriptVar *child);
void addNamedChild(const std::string &childName, CScriptVar *child); ///< add the named child. if it is already owned, copy it. 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(); void removeAllChildren();
CScriptVar *getRoot(); ///< Get the absolute root of the tree CScriptVar *getRoot(); ///< Get the absolute root of the tree
const std::string &getName(); const std::string &getName();
int getInt(); int getInt();
bool getBool() { return getInt() != 0; } bool getBool() { return getInt() != 0; }
double getDouble(); double getDouble();
@ -156,9 +167,10 @@ public:
void setString(const std::string &str); void setString(const std::string &str);
void setVoid(); void setVoid();
bool isInt() { return (flags&SCRIPTVAR_NUMERIC)!=0 && (flags&SCRIPTVAR_INTEGER)!=0; } bool isInt() { return (flags&SCRIPTVAR_INTEGER)!=0; }
bool isDouble() { return (flags&SCRIPTVAR_NUMERIC)!=0 && (flags&SCRIPTVAR_INTEGER)==0; } bool isDouble() { return (flags&SCRIPTVAR_DOUBLE)==0; }
bool isNumeric() { return (flags&SCRIPTVAR_NUMERIC)!=0; } bool isString() { return (flags&SCRIPTVAR_STRING)!=0; }
bool isNumeric() { return (flags&SCRIPTVAR_NUMERICMASK)!=0; }
bool isFunction() { return (flags&SCRIPTVAR_FUNCTION)!=0; } bool isFunction() { return (flags&SCRIPTVAR_FUNCTION)!=0; }
bool isParameter() { return (flags&SCRIPTVAR_PARAMETER)!=0; } bool isParameter() { return (flags&SCRIPTVAR_PARAMETER)!=0; }
bool isNative() { return (flags&SCRIPTVAR_NATIVE)!=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 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 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); void setCallback(JSCallback callback);
@ -226,6 +239,9 @@ private:
CScriptVar *base(bool &execute); CScriptVar *base(bool &execute);
void block(bool &execute); void block(bool &execute);
void statement(bool &execute); void statement(bool &execute);
// parsing utility functions
CScriptVar *parseFunctionDefinition();
void parseFunctionArguments(CScriptVar *funcVar);
}; };
#endif #endif

@ -0,0 +1,118 @@
/*
* TinyJS
*
* A single-file Javascript-alike engine
*
* Authored By Gordon Williams <gw@pur3.co.uk>
*
* 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 <assert.h>
#include <sys/stat.h>
#include <string>
#include <sstream>
#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;
}

@ -0,0 +1,2 @@
// simply testing we can return the correct value
result = 1;

@ -0,0 +1,3 @@
// comparison
var a = 42;
result = a==42;

@ -0,0 +1,5 @@
// simple for loop
var a = 0;
var i;
for (i=1;i<10;i++) a = a + i;
result = a==45;

@ -0,0 +1,4 @@
// simple if
var a = 42;
if (a < 43)
result = 1;

@ -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;

@ -0,0 +1,3 @@
// simple function
function add(x,y) { return x+y; }
result = add(3,6)==9;

@ -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;

@ -0,0 +1,5 @@
// functions in variables
var bob;
bob.add = function(x,y) { return x+y; };
result = bob.add(3,6)==9;

@ -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;

@ -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;

@ -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;

@ -0,0 +1,6 @@
// if .. else
var a = 42;
if (a != 42)
result = 0;
else
result = 1;

@ -0,0 +1,7 @@
// if .. else with blocks
var a = 42;
if (a != 42) {
result = 0;
} else {
result = 1;
}
Loading…
Cancel
Save