From edc1c42c2ecff519fd2c60a7cf28544419ddc7ee Mon Sep 17 00:00:00 2001 From: pur3mail Date: Fri, 3 Sep 2010 06:05:21 +0000 Subject: [PATCH] Version 0.22 : First part of ardi's changes: sprintf -> sprintf_s extra tokens parsed array memory leak fixed Fixed memory leak in evaluateComplex Fixed memory leak in FOR loops Fixed memory leak for unary minus --- Script.cpp | 27 +- TinyJS.cpp | 3821 +++++++++++++++++++++++++------------------------ TinyJS.h | 642 +++++---- run_tests.cpp | 10 +- 4 files changed, 2298 insertions(+), 2202 deletions(-) diff --git a/Script.cpp b/Script.cpp index a1ad79d..f702a30 100755 --- a/Script.cpp +++ b/Script.cpp @@ -27,10 +27,11 @@ * This is a simple program showing how to use TinyJS */ -#include -#include #include "TinyJS.h" #include "TinyJS_Functions.h" +#include +#include + //const char *code = "var a = 5; if (a==5) a=4; else a=3;"; //const char *code = "{ var a = 4; var b = 1; while (a>0) { b = b * 2; a = a - 1; } var c = 5; }"; @@ -49,29 +50,35 @@ void js_dump(CScriptVar *v, void *userdata) { int main(int argc, char **argv) { - CTinyJS js; + CTinyJS *js = new CTinyJS(); /* add the functions from TinyJS_Functions.cpp */ - registerFunctions(&js); + registerFunctions(js); /* Add a native function */ - js.addNative("function print(text)", &js_print, 0); - js.addNative("function dump()", &js_dump, &js); + js->addNative("function print(text)", &js_print, 0); + js->addNative("function dump()", &js_dump, js); /* Execute out bit of code - we could call 'evaluate' here if we wanted something returned */ try { - js.execute("var lets_quit = 0; function quit() { lets_quit = 1; }"); - js.execute("print(\"Interactive mode... Type quit(); to exit, or print(...); to print something, or dump() to dump the symbol table!\");"); + js->execute("var lets_quit = 0; function quit() { lets_quit = 1; }"); + js->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 (js.evaluate("lets_quit") == "0") { + while (js->evaluate("lets_quit") == "0") { char buffer[2048]; fgets ( buffer, sizeof(buffer), stdin ); try { - js.execute(buffer); + js->execute(buffer); } catch (CScriptException *e) { printf("ERROR: %s\n", e->text.c_str()); } } + delete js; +#ifdef _WIN32 +#ifdef _DEBUG + _CrtDumpMemoryLeaks(); +#endif +#endif return 0; } diff --git a/TinyJS.cpp b/TinyJS.cpp index f129777..5b8205f 100755 --- a/TinyJS.cpp +++ b/TinyJS.cpp @@ -1,1879 +1,1942 @@ -/* - * 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. - */ - -/* Version 0.1 : (gw) First published on Google Code - Version 0.11 : Making sure the 'root' variable never changes - 'symbol_base' added for the current base of the sybmbol table - Version 0.12 : Added findChildOrCreate, changed string passing to use references - Fixed broken string encoding in getJSString() - Removed getInitCode and added getJSON instead - 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 - Version 0.14 : Added parsing of more number types - Added parsing of string defined with ' - Changed nil to null as per spec, added 'undefined' - Now set variables with the correct scope, and treat unknown - as 'undefined' rather than failing - Added proper (I hope) handling of null and undefined - Added === check - Version 0.15 : Fix for possible memory leaks - Version 0.16 : Removal of un-needed findRecursive calls - symbol_base removed and replaced with 'scopes' stack - Added reference counting a proper tree structure - (Allowing pass by reference) - Allowed JSON output to output IDs, not strings - Added get/set for array indices - Changed Callbacks to include user data pointer - Added some support for objects - Added more Java-esque builtin functions - Version 0.17 : Now we don't deepCopy the parent object of the class - Added JSON.stringify and eval() - Nicer JSON indenting - Fixed function output in JSON - Added evaluateComplex - Fixed some reentrancy issues with evaluate/execute - Version 0.18 : Fixed some issues with code being executed when it shouldn't - Version 0.19 : Added array.length - Changed '__parent' to 'prototype' to bring it more in line with javascript - Version 0.20 : Added '%' operator - Version 0.21 : Added array type - String.length() no more - now String.length - Added extra constructors to reduce confusion - Fixed checks against undefined - - NOTE: This doesn't support constructors for objects - Recursive loops of data such as a.foo = a; fail to be garbage collected - 'length' cannot be set - There is no ternary operator implemented yet - The postfix increment operator returns the current value, not the previous as it should. - */ - -#include "TinyJS.h" -#include - -#define ASSERT(X) assert(X) -/* Frees the given link IF it isn't owned by anything else */ -#define CLEAN(x) { CScriptVarLink *__v = x; if (__v && !__v->owned) { delete __v; } } -/* Create a LINK to point to VAR and free the old link. - * BUT this is more clever - it tries to keep the old link if it's not owned to save allocations */ -#define CREATE_LINK(LINK, VAR) { if (!LINK || LINK->owned) LINK = new CScriptVarLink(VAR); else LINK->replaceWith(VAR); } - -#include -#include -#include -#include -#include - -using namespace std; - -#ifdef __GNUC__ -#define vsprintf_s vsnprintf -#define sprintf_s snprintf -#endif - -// ----------------------------------------------------------------------------------- Memory Debug - -//#define DEBUG_MEMORY 1 - -#if DEBUG_MEMORY - -vector allocatedVars; -vector allocatedLinks; - -void mark_allocated(CScriptVar *v) { - allocatedVars.push_back(v); -} - -void mark_deallocated(CScriptVar *v) { - for (size_t i=0;igetRefs()); - allocatedVars[i]->trace(" "); - } - for (size_t i=0;iname.c_str(), allocatedLinks[i]->var->getRefs()); - allocatedLinks[i]->var->trace(" "); - } - allocatedVars.clear(); - allocatedLinks.clear(); -} - -#endif - -// ----------------------------------------------------------------------------------- Utils -bool isWhitespace(char ch) { - return (ch==' ') || (ch=='\t') || (ch=='\n') || (ch=='\r'); -} - -bool isNumeric(char ch) { - return (ch>='0') && (ch<='9'); -} -bool isNumber(const string &str) { - for (size_t i=0;i='0') && (ch<='9')) || - ((ch>='a') && (ch<='f')) || - ((ch>='A') && (ch<='F')); -} -bool isAlpha(char ch) { - return ((ch>='a') && (ch<='z')) || ((ch>='A') && (ch<='Z')) || ch=='_'; -} - -bool isIDString(const char *s) { - if (!isAlpha(*s)) - return false; - while (*s) { - if (!(isAlpha(*s) || isNumeric(*s))) - return false; - s++; - } - return true; -} - -void replace(string &str, char textFrom, const char *textTo) { - int sLen = strlen(textTo); - size_t p = str.find(textFrom); - while (p != string::npos) { - str = str.substr(0, p) + textTo + str.substr(p+1); - p = str.find(textFrom, p+sLen); - } -} - -/// convert the given string into a quoted string suitable for javascript -std::string getJSString(const std::string &str) { - std::string nStr = str; - for (size_t i=0;idata; - dataOwned = false; - dataStart = startChar; - dataEnd = endChar; - reset(); -} - -CScriptLex::~CScriptLex(void) -{ - if (dataOwned) - free((void*)data); -} - -void CScriptLex::reset() { - dataPos = dataStart; - tokenStart = 0; - tokenEnd = 0; - tokenLastEnd = 0; - tk = 0; - tkStr = ""; - getNextCh(); - getNextCh(); - getNextToken(); -} - -void CScriptLex::match(int expected_tk) { - if (tk!=expected_tk) { - ostringstream errorString; - errorString << "Got " << getTokenStr(tk) << " expected " << getTokenStr(expected_tk) - << " at " << getPosition(tokenStart) << " in '" << data << "'"; - throw new CScriptException(errorString.str()); - } - getNextToken(); -} - -string CScriptLex::getTokenStr(int token) { - if (token>32 && token<128) { - char buf[4] = "' '"; - buf[1] = token; - return buf; - } - switch (token) { - case LEX_EOF : return "EOF"; - case LEX_ID : return "ID"; - case LEX_INT : return "INT"; - case LEX_FLOAT : return "FLOAT"; - case LEX_STR : return "STRING"; - case LEX_EQUAL : return "=="; - case LEX_TYPEEQUAL : return "==="; - case LEX_NEQUAL : return "!="; - case LEX_NTYPEEQUAL : return "!=="; - case LEX_LEQUAL : return "<="; - case LEX_GEQUAL : return ">="; - case LEX_PLUSEQUAL : return "+="; - case LEX_MINUSEQUAL : return "-="; - case LEX_PLUSPLUS : return "++"; - case LEX_MINUSMINUS : return "--"; - case LEX_ANDAND : return "&&"; - case LEX_OROR : return "||"; - // reserved words - case LEX_R_IF : return "if"; - case LEX_R_ELSE : return "else"; - case LEX_R_WHILE : return "while"; - case LEX_R_FOR : return "for"; - case LEX_R_FUNCTION : return "function"; - case LEX_R_RETURN : return "return"; - case LEX_R_VAR : return "var"; - case LEX_R_TRUE : return "true"; - case LEX_R_FALSE : return "false"; - case LEX_R_NULL : return "null"; - case LEX_R_UNDEFINED : return "undefined"; - case LEX_R_NEW : return "new"; - } - - ostringstream msg; - msg << "?[" << token << "]"; - return msg.str(); -} - -void CScriptLex::getNextCh() { - currCh = nextCh; - if (dataPos < dataEnd) - nextCh = data[dataPos]; - else - nextCh = 0; - dataPos++; -} - -void CScriptLex::getNextToken() { - tk = LEX_EOF; - tkStr.clear(); - while (currCh && isWhitespace(currCh)) getNextCh(); - // newline comments - if (currCh=='/' && nextCh=='/') { - while (currCh && currCh!='\n') getNextCh(); - getNextCh(); - getNextToken(); - return; - } - // block comments - if (currCh=='/' && nextCh=='*') { - while (currCh && (currCh!='*' || nextCh!='/')) getNextCh(); - getNextCh(); - getNextCh(); - getNextToken(); - return; - } - // record beginning of this token - tokenStart = dataPos-2; - // tokens - if (isAlpha(currCh)) { // IDs - while (isAlpha(currCh) || isNumeric(currCh)) { - tkStr += currCh; - getNextCh(); - } - tk = LEX_ID; - if (tkStr=="if") tk = LEX_R_IF; - else if (tkStr=="else") tk = LEX_R_ELSE; - else if (tkStr=="while") tk = LEX_R_WHILE; - else if (tkStr=="for") tk = LEX_R_FOR; - else if (tkStr=="function") tk = LEX_R_FUNCTION; - else if (tkStr=="return") tk = LEX_R_RETURN; - else if (tkStr=="var") tk = LEX_R_VAR; - else if (tkStr=="true") tk = LEX_R_TRUE; - else if (tkStr=="false") tk = LEX_R_FALSE; - else if (tkStr=="null") tk = LEX_R_NULL; - else if (tkStr=="undefined") tk = LEX_R_UNDEFINED; - else if (tkStr=="new") tk = LEX_R_NEW; - } else if (isNumeric(currCh)) { // Numbers - bool isHex = false; - if (currCh=='0') { tkStr += currCh; getNextCh(); } - if (currCh=='x') { - isHex = true; - tkStr += currCh; getNextCh(); - } - tk = LEX_INT; - while (isNumeric(currCh) || (isHex && isHexadecimal(currCh))) { - tkStr += currCh; - getNextCh(); - } - if (!isHex && currCh=='.') { - tk = LEX_FLOAT; - tkStr += '.'; - getNextCh(); - while (isNumeric(currCh)) { - tkStr += currCh; - getNextCh(); - } - } - // do fancy e-style floating point - if (!isHex && currCh=='e') { - tk = LEX_FLOAT; - tkStr += currCh; getNextCh(); - if (currCh=='-') { tkStr += currCh; getNextCh(); } - while (isNumeric(currCh)) { - tkStr += currCh; getNextCh(); - } - } - } else if (currCh=='"') { - // strings... - getNextCh(); - while (currCh && currCh!='"') { - if (currCh == '\\') { - getNextCh(); - switch (currCh) { - case 'n' : tkStr += '\n'; break; - case '"' : tkStr += '"'; break; - case '\\' : tkStr += '\\'; break; - default: tkStr += currCh; - } - } else { - tkStr += currCh; - } - getNextCh(); - } - getNextCh(); - tk = LEX_STR; - } else if (currCh=='\'') { - // strings again... - getNextCh(); - while (currCh && currCh!='\'') { - if (currCh == '\\') { - getNextCh(); - switch (currCh) { - case 'n' : tkStr += '\n'; break; - case '\'' : tkStr += '\''; break; - case '\\' : tkStr += '\\'; break; - default: tkStr += currCh; - } - } else { - tkStr += currCh; - } - getNextCh(); - } - getNextCh(); - tk = LEX_STR; - } else { - // single chars - tk = currCh; - if (currCh) getNextCh(); - if (tk=='=' && currCh=='=') { // == - tk = LEX_EQUAL; - getNextCh(); - if (currCh=='=') { // === - tk = LEX_TYPEEQUAL; - getNextCh(); - } - } else if (tk=='!' && currCh=='=') { // != - tk = LEX_NEQUAL; - getNextCh(); - if (currCh=='=') { // !== - tk = LEX_NTYPEEQUAL; - 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) { - int lastCharIdx = tokenLastEnd+1; - if (lastCharIdx < dataEnd) { - /* save a memory alloc by using our data array to create the - substring */ - char old = data[lastCharIdx]; - data[lastCharIdx] = 0; - std::string value = &data[lastPosition]; - data[lastCharIdx] = old; - return value; - } else { - return std::string(&data[lastPosition]); - } -} - - -CScriptLex *CScriptLex::getSubLex(int lastPosition) { - int lastCharIdx = tokenLastEnd+1; - if (lastCharIdx < dataEnd) - return new CScriptLex( this, lastPosition, lastCharIdx); - else - return new CScriptLex( this, lastPosition, dataEnd ); -} - -string CScriptLex::getPosition(int pos) { - int line = 1,col = 1; - for (int i=0;iname = name; - this->nextSibling = 0; - this->prevSibling = 0; - this->var = var->ref(); - this->owned = false; -} - -CScriptVarLink::~CScriptVarLink() { -#if DEBUG_MEMORY - mark_deallocated(this); -#endif - var->unref(); -} - -CScriptVarLink::CScriptVarLink(const CScriptVarLink &link) { - // Copy constructor -#if DEBUG_MEMORY - mark_allocated(this); -#endif - this->name = link.name; - this->nextSibling = 0; - this->prevSibling = 0; - this->var = link.var->ref(); - this->owned = false; -} - -void CScriptVarLink::replaceWith(CScriptVar *newVar) { - CScriptVar *oldVar = var; - var = newVar->ref(); - oldVar->unref(); -} - -void CScriptVarLink::replaceWith(CScriptVarLink *newVar) { - if (newVar) - replaceWith(newVar->var); - else - replaceWith(new CScriptVar()); -} - -// ----------------------------------------------------------------------------------- CSCRIPTVAR - -CScriptVar::CScriptVar() { - refs = 0; -#if DEBUG_MEMORY - mark_allocated(this); -#endif - init(); - flags = SCRIPTVAR_UNDEFINED; - data = TINYJS_BLANK_DATA; -} - -CScriptVar::CScriptVar(const string &str) { - refs = 0; -#if DEBUG_MEMORY - mark_allocated(this); -#endif - init(); - flags = SCRIPTVAR_STRING; - data = str; -} - - -CScriptVar::CScriptVar(const string &varData, int varFlags) { - refs = 0; -#if DEBUG_MEMORY - mark_allocated(this); -#endif - init(); - flags = varFlags; - data = varData; -} - -CScriptVar::CScriptVar(double val) { - refs = 0; -#if DEBUG_MEMORY - mark_allocated(this); -#endif - init(); - setDouble(val); -} - -CScriptVar::CScriptVar(int val) { - refs = 0; -#if DEBUG_MEMORY - mark_allocated(this); -#endif - init(); - setInt(val); -} - -CScriptVar::~CScriptVar(void) { -#if DEBUG_MEMORY - mark_deallocated(this); -#endif - removeAllChildren(); -} - -void CScriptVar::init() { - firstChild = 0; - lastChild = 0; - flags = 0; - jsCallback = 0; - jsCallbackUserData = 0; -} - -CScriptVar *CScriptVar::getReturnVar() { - return getParameter(TINYJS_RETURN_VAR); -} - -void CScriptVar::setReturnVar(CScriptVar *var) { - findChildOrCreate(TINYJS_RETURN_VAR)->replaceWith(var); -} - - -CScriptVar *CScriptVar::getParameter(const std::string &name) { - return findChildOrCreate(name)->var; -} - -CScriptVarLink *CScriptVar::findChild(const string &childName) { - CScriptVarLink *v = firstChild; - while (v) { - if (v->name.compare(childName)==0) - return v; - v = v->nextSibling; - } - return 0; -} - -CScriptVarLink *CScriptVar::findChildOrCreate(const string &childName, int varFlags) { - CScriptVarLink *l = findChild(childName); - if (l) return l; - - return addChild(childName, new CScriptVar(TINYJS_BLANK_DATA, varFlags)); -} - -CScriptVarLink *CScriptVar::findChildOrCreateByPath(const std::string &path) { - size_t p = path.find('.'); - if (p == string::npos) - return findChildOrCreate(path); - - return findChildOrCreate(path.substr(0,p), SCRIPTVAR_OBJECT)->var-> - findChildOrCreateByPath(path.substr(p+1)); -} - -CScriptVarLink *CScriptVar::addChild(const std::string &childName, CScriptVar *child) { - if (isUndefined()) { - flags = SCRIPTVAR_OBJECT; - } - // if no child supplied, create one - if (!child) - child = new CScriptVar(); - - CScriptVarLink *link = new CScriptVarLink(child, childName); - link->owned = true; - if (lastChild) { - lastChild->nextSibling = link; - link->prevSibling = lastChild; - lastChild = link; - } else { - firstChild = link; - lastChild = link; - } - return link; -} - -CScriptVarLink *CScriptVar::addChildNoDup(const std::string &childName, CScriptVar *child) { - // if no child supplied, create one - if (!child) - child = new CScriptVar(); - - CScriptVarLink *v = findChild(childName); - if (v) { - v->replaceWith(child); - } else { - v = addChild(childName, child); - } - - return v; -} - -void CScriptVar::removeChild(CScriptVar *child) { - CScriptVarLink *link = firstChild; - while (link) { - if (link->var == child) - break; - link = link->nextSibling; - } - ASSERT(link); - removeLink(link); -} - -void CScriptVar::removeLink(CScriptVarLink *link) { - if (!link) return; - if (link->nextSibling) - link->nextSibling->prevSibling = link->prevSibling; - if (link->prevSibling) - link->prevSibling->nextSibling = link->nextSibling; - if (lastChild == link) - lastChild = link->prevSibling; - if (firstChild == link) - firstChild = link->nextSibling; - delete link; -} - -void CScriptVar::removeAllChildren() { - CScriptVarLink *c = firstChild; - while (c) { - CScriptVarLink *t = c->nextSibling; - delete c; - c = t; - } - firstChild = 0; - lastChild = 0; -} - -CScriptVar *CScriptVar::getArrayIndex(int idx) { - char sIdx[64]; - sprintf(sIdx, "%d", idx); - CScriptVarLink *link = findChild(sIdx); - if (link) return link->var; - else return new CScriptVar(TINYJS_BLANK_DATA, SCRIPTVAR_NULL); // undefined -} - -void CScriptVar::setArrayIndex(int idx, CScriptVar *value) { - char sIdx[64]; - sprintf(sIdx, "%d", idx); - CScriptVarLink *link = findChild(sIdx); - - if (link) { - if (value->isUndefined()) - removeLink(link); - else - link->replaceWith(value); - } else { - if (!value->isUndefined()) - addChild(sIdx, value); - } -} - -int CScriptVar::getArrayLength() { - int highest = -1; - if (!isArray()) return 0; - - CScriptVarLink *link = firstChild; - while (link) { - if (isNumber(link->name)) { - int val = atoi(link->name.c_str()); - if (val > highest) highest = val; - } - link = link->nextSibling; - } - return highest+1; -} - -int CScriptVar::getChildren() { - int n = 0; - CScriptVarLink *link = firstChild; - while (link) { - n++; - link = link->nextSibling; - } - return n; -} - -int CScriptVar::getInt() { - /* strtol understands about hex and octal */ - if (isInt()) return strtol(data.c_str(),0,0); - if (isNull()) return 0; - if (isUndefined()) return 0; - if (isDouble()) return (int)getDouble(); - return 0; -} - -double CScriptVar::getDouble() { - if (isDouble()) return strtod(data.c_str(),0); - if (isInt()) return getInt(); - if (isNull()) return 0; - if (isUndefined()) return 0; - return 0; /* or NaN? */ -} - -const string &CScriptVar::getString() { - /* Because we can't return a string that is generated on demand. - * I should really just use char* :) */ - static string s_null = "null"; - static string s_undefined = "undefined"; - if (isNull()) return s_null; - if (isUndefined()) return s_undefined; - return data; -} - -void CScriptVar::setInt(int val) { - char buf[256]; - sprintf(buf, "%d", val); - flags = (flags&~SCRIPTVAR_VARTYPEMASK) | SCRIPTVAR_INTEGER; - data = buf; -} - -void CScriptVar::setDouble(double val) { - char buf[256]; - sprintf(buf, "%lf", val); - flags = (flags&~SCRIPTVAR_VARTYPEMASK) | SCRIPTVAR_DOUBLE; - data = buf; -} - -void CScriptVar::setString(const string &str) { - // name sure it's not still a number or integer - flags = (flags&~SCRIPTVAR_VARTYPEMASK) | SCRIPTVAR_STRING; - data = str; -} - -void CScriptVar::setUndefined() { - // name sure it's not still a number or integer - flags = (flags&~SCRIPTVAR_VARTYPEMASK) | SCRIPTVAR_UNDEFINED; - data = TINYJS_BLANK_DATA; - removeAllChildren(); -} - -CScriptVar *CScriptVar::mathsOp(CScriptVar *b, int op) { - CScriptVar *a = this; - // TODO Equality checks on classes/structures - // Type equality check - if (op == LEX_TYPEEQUAL || op == LEX_NTYPEEQUAL) { - bool eql = ((a->flags & SCRIPTVAR_VARTYPEMASK) == - (b->flags & SCRIPTVAR_VARTYPEMASK)) && - a->data == b->data; - if (op == LEX_TYPEEQUAL) - return new CScriptVar(eql); - else - return new CScriptVar(!eql); - } - // do maths... - if (a->isUndefined() && b->isUndefined()) { - if (op == LEX_EQUAL) return new CScriptVar(true); - else if (op == LEX_NEQUAL) return new CScriptVar(false); - else return new CScriptVar(); // undefined - } else if ((a->isNumeric() || a->isUndefined()) && - (b->isNumeric() || b->isUndefined())) { - if (!a->isDouble() && !b->isDouble()) { - // use ints - int da = a->getInt(); - int db = b->getInt(); - switch (op) { - case '+': return new CScriptVar(da+db); - case '-': return new CScriptVar(da-db); - case '*': return new CScriptVar(da*db); - case '/': return new CScriptVar(da/db); - case '&': return new CScriptVar(da&db); - case '|': return new CScriptVar(da|db); - case '^': return new CScriptVar(da^db); - case '%': return new CScriptVar(da%db); - case LEX_EQUAL: return new CScriptVar(da==db); - case LEX_NEQUAL: return new CScriptVar(da!=db); - case '<': return new CScriptVar(da': return new CScriptVar(da>db); - case LEX_GEQUAL: return new CScriptVar(da>=db); - default: throw new CScriptException("This operation not supported on the int datatype"); - } - } else { - // use doubles - double da = a->getDouble(); - double db = b->getDouble(); - switch (op) { - case '+': return new CScriptVar(da+db); - case '-': return new CScriptVar(da-db); - case '*': return new CScriptVar(da*db); - case '/': return new CScriptVar(da/db); - case LEX_EQUAL: return new CScriptVar(da==db); - case LEX_NEQUAL: return new CScriptVar(da!=db); - case '<': return new CScriptVar(da': return new CScriptVar(da>db); - case LEX_GEQUAL: return new CScriptVar(da>=db); - default: throw new CScriptException("This operation not supported on the double datatype"); - } - } - } else { - string da = a->getString(); - string db = b->getString(); - // use strings - switch (op) { - case '+': return new CScriptVar(da+db, SCRIPTVAR_STRING); - case LEX_EQUAL: return new CScriptVar(da==db); - case LEX_NEQUAL: return new CScriptVar(da!=db); - case '<': return new CScriptVar(da': return new CScriptVar(da>db); - case LEX_GEQUAL: return new CScriptVar(da>=db); - default: throw new CScriptException("This operation not supported on the string datatype"); - } - } - ASSERT(0); - return 0; -} - -void CScriptVar::copyValue(CScriptVar *val) { - if (val) { - // we *don't* copy the name - data = val->data; - flags = (flags & ~SCRIPTVAR_VARTYPEMASK) | (val->flags & SCRIPTVAR_VARTYPEMASK); - // remove all current children - removeAllChildren(); - // copy children of 'val' - CScriptVarLink *child = val->firstChild; - while (child) { - CScriptVar *copied; - // don't copy the 'parent' object... - if (child->name != TINYJS_PROTOTYPE_CLASS) - copied = child->var->deepCopy(); - else - copied = child->var; - - addChild(child->name, copied); - - child = child->nextSibling; - } - } else { - setUndefined(); - } -} - -CScriptVar *CScriptVar::deepCopy() { - CScriptVar *newVar = new CScriptVar(data, flags); - // copy children - CScriptVarLink *child = firstChild; - while (child) { - CScriptVar *copied; - // don't copy the 'parent' object... - if (child->name != TINYJS_PROTOTYPE_CLASS) - copied = child->var->deepCopy(); - else - copied = child->var; - - newVar->addChild(child->name, copied); - child = child->nextSibling; - } - return newVar; -} - -void CScriptVar::trace(string indentStr, const string &name) { - TRACE("%s'%s' = '%s' %s\n", - indentStr.c_str(), - name.c_str(), - data.c_str(), - getFlagsAsString().c_str()); - string indent = indentStr+" "; - CScriptVarLink *link = firstChild; - while (link) { - link->var->trace(indent, link->name); - link = link->nextSibling; - } -} - -string CScriptVar::getFlagsAsString() { - string flagstr = ""; - if (flags&SCRIPTVAR_FUNCTION) flagstr = flagstr + "FUNCTION "; - if (flags&SCRIPTVAR_OBJECT) flagstr = flagstr + "OBJECT "; - if (flags&SCRIPTVAR_ARRAY) flagstr = flagstr + "ARRAY "; - 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(); - if (isFunction()) { - ostringstream funcStr; - funcStr << "function ("; - // get list of parameters - CScriptVarLink *link = firstChild; - while (link) { - funcStr << link->name; - if (link->nextSibling) funcStr << ","; - link = link->nextSibling; - } - // add function body - funcStr << ") " << getString(); - return funcStr.str(); - } - // if it is a string then we quote it - if (isString()) - return getJSString(getString()); - if (isNull()) - return "null"; - return "undefined"; -} - -void CScriptVar::getJSON(ostringstream &destination, const string linePrefix) { - if (isObject()) { - string indentedLinePrefix = linePrefix+" "; - // children - handle with bracketed list - destination << "{ \n"; - CScriptVarLink *link = firstChild; - while (link) { - destination << indentedLinePrefix; - if (isAlphaNum(link->name)) - destination << link->name; - else - destination << getJSString(link->name); - destination << " : "; - link->var->getJSON(destination, indentedLinePrefix); - link = link->nextSibling; - if (link) { - destination << ",\n"; - } - } - destination << "\n" << linePrefix << "}"; - } else if (isArray()) { - string indentedLinePrefix = linePrefix+" "; - destination << "[\n"; - int len = getArrayLength(); - if (len>10000) len=10000; // we don't want to get stuck here! - - for (int i=0;igetJSON(destination, indentedLinePrefix); - if (iref(); - // Add built-in classes - stringClass = (new CScriptVar(TINYJS_BLANK_DATA, SCRIPTVAR_OBJECT))->ref(); - arrayClass = (new CScriptVar(TINYJS_BLANK_DATA, SCRIPTVAR_OBJECT))->ref(); - objectClass = (new CScriptVar(TINYJS_BLANK_DATA, SCRIPTVAR_OBJECT))->ref(); - root->addChild("String", stringClass); - root->addChild("Array", arrayClass); - root->addChild("Object", objectClass); -} - -CTinyJS::~CTinyJS() { - ASSERT(!l); - scopes.clear(); - stringClass->unref(); - objectClass->unref(); - root->unref(); - -#if DEBUG_MEMORY - show_allocated(); -#endif -} - -void CTinyJS::trace() { - root->trace(); -} - -void CTinyJS::execute(const string &code) { - CScriptLex *oldLex = l; - vector oldScopes = scopes; - l = new CScriptLex(code); - scopes.clear(); - scopes.push_back(root); - try { - bool execute = true; - while (l->tk) statement(execute); - } catch (CScriptException *e) { - ostringstream msg; - msg << "Error " << e->text << " at " << l->getPosition(l->tokenLastEnd); - delete l; - l = oldLex; - throw new CScriptException(msg.str()); - } - delete l; - l = oldLex; - scopes = oldScopes; -} - -CScriptVarLink CTinyJS::evaluateComplex(const string &code) { - CScriptLex *oldLex = l; - vector oldScopes = scopes; - - l = new CScriptLex(code); - scopes.clear(); - scopes.push_back(root); - CScriptVarLink *v = 0; - try { - bool execute = true; - v = base(execute); - } catch (CScriptException *e) { - ostringstream msg; - msg << "Error " << e->text << " at " << l->getPosition(l->tokenLastEnd); - delete l; - l = oldLex; - throw new CScriptException(msg.str()); - } - delete l; - l = oldLex; - scopes = oldScopes; - - if (v) return *v; - // return undefined... - return CScriptVarLink(new CScriptVar()); -} - -string CTinyJS::evaluate(const string &code) { - return evaluateComplex(code).var->getString(); -} - -void CTinyJS::parseFunctionArguments(CScriptVar *funcVar) { - l->match('('); - while (l->tk!=')') { - funcVar->addChildNoDup(l->tkStr); - l->match(LEX_ID); - if (l->tk!=')') l->match(','); - } - l->match(')'); -} - -void CTinyJS::addNative(const string &funcDesc, JSCallback ptr, void *userdata) { - CScriptLex *oldLex = l; - l = new CScriptLex(funcDesc); - - CScriptVar *base = root; - - l->match(LEX_R_FUNCTION); - string funcName = l->tkStr; - l->match(LEX_ID); - /* Check for dots, we might want to do something like function String.substring ... */ - while (l->tk == '.') { - l->match('.'); - CScriptVarLink *link = base->findChild(funcName); - // if it doesn't exist, make an object class - if (!link) link = base->addChild(funcName, new CScriptVar(TINYJS_BLANK_DATA, SCRIPTVAR_OBJECT)); - base = link->var; - funcName = l->tkStr; - l->match(LEX_ID); - } - - CScriptVar *funcVar = new CScriptVar(TINYJS_BLANK_DATA, SCRIPTVAR_FUNCTION | SCRIPTVAR_NATIVE); - funcVar->setCallback(ptr, userdata); - parseFunctionArguments(funcVar); - delete l; - l = oldLex; - - base->addChild(funcName, funcVar); -} - -CScriptVarLink *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); - } - CScriptVarLink *funcVar = new CScriptVarLink(new CScriptVar(TINYJS_BLANK_DATA, SCRIPTVAR_FUNCTION), funcName); - parseFunctionArguments(funcVar->var); - int funcBegin = l->tokenStart; - bool noexecute = false; - block(noexecute); - funcVar->var->data = l->getSubString(funcBegin); - return funcVar; -} - -CScriptVarLink *CTinyJS::factor(bool &execute) { - if (l->tk=='(') { - l->match('('); - CScriptVarLink *a = base(execute); - l->match(')'); - return a; - } - if (l->tk==LEX_R_TRUE) { - l->match(LEX_R_TRUE); - return new CScriptVarLink(new CScriptVar(1)); - } - if (l->tk==LEX_R_FALSE) { - l->match(LEX_R_FALSE); - return new CScriptVarLink(new CScriptVar(0)); - } - if (l->tk==LEX_R_NULL) { - l->match(LEX_R_NULL); - return new CScriptVarLink(new CScriptVar(TINYJS_BLANK_DATA,SCRIPTVAR_NULL)); - } - if (l->tk==LEX_R_UNDEFINED) { - l->match(LEX_R_UNDEFINED); - return new CScriptVarLink(new CScriptVar(TINYJS_BLANK_DATA,SCRIPTVAR_UNDEFINED)); - } - if (l->tk==LEX_ID) { - CScriptVarLink *a = execute ? findInScopes(l->tkStr) : new CScriptVarLink(new CScriptVar()); - /* The parent if we're executing a method call */ - CScriptVar *parent = 0; - - if (execute && !a) { - /* Variable doesn't exist! JavaScript says we should create it - * (we won't add it here. This is done in the assignment operator)*/ - a = new CScriptVarLink(new CScriptVar(), l->tkStr); - } - l->match(LEX_ID); - while (l->tk=='(' || l->tk=='.' || l->tk=='[') { - if (l->tk=='(') { // ------------------------------------- Function Call - if (execute) { - if (!a->var->isFunction()) { - string errorMsg = "Expecting '"; - errorMsg = errorMsg + a->name + "' to be a function"; - throw new CScriptException(errorMsg.c_str()); - } - l->match('('); - // create a new symbol table entry for execution of this function - CScriptVar *functionRoot = new CScriptVar(TINYJS_BLANK_DATA, SCRIPTVAR_FUNCTION); - if (parent) - functionRoot->addChildNoDup("this", parent); - // grab in all parameters - CScriptVarLink *v = a->var->firstChild; - while (v) { - CScriptVarLink *value = base(execute); - if (execute) { - if (value->var->isBasic()) { - // pass by value - functionRoot->addChild(v->name, value->var->deepCopy()); - } else { - // pass by reference - functionRoot->addChild(v->name, value->var); - } - } - CLEAN(value); - if (l->tk!=')') l->match(','); - v = v->nextSibling; - } - l->match(')'); - // setup a return variable - CScriptVarLink *returnVar = NULL; - // execute function! - // add the function's execute space to the symbol table so we can recurse - CScriptVarLink *returnVarLink = functionRoot->addChild(TINYJS_RETURN_VAR); - scopes.push_back(functionRoot); - - if (a->var->isNative()) { - ASSERT(a->var->jsCallback); - a->var->jsCallback(functionRoot, a->var->jsCallbackUserData); - } else { - /* we just want to execute the block, but something could - * have messed up and left us with the wrong ScriptLex, so - * we want to be careful here... */ - CScriptException *exception = 0; - CScriptLex *oldLex = l; - CScriptLex *newLex = new CScriptLex(a->var->getString()); - l = newLex; - try { - block(execute); - // because return will probably have called this, and set execute to false - execute = true; - } catch (CScriptException *e) { - exception = e; - } - delete newLex; - l = oldLex; - - if (exception) - throw exception; - } - - scopes.pop_back(); - /* get the real return var before we remove it from our function */ - returnVar = new CScriptVarLink(returnVarLink->var); - functionRoot->removeLink(returnVarLink); - delete functionRoot; - if (returnVar) - a = returnVar; - else - a = new CScriptVarLink(new CScriptVar()); - } else { - // function, but not executing - just parse args and be done - l->match('('); - while (l->tk != ')') { - CScriptVarLink *value = base(execute); - CLEAN(value); - if (l->tk!=')') l->match(','); - } - l->match(')'); - if (l->tk == '{') { - block(execute); - } - } - } else if (l->tk == '.') { // ------------------------------------- Record Access - l->match('.'); - if (execute) { - const string &name = l->tkStr; - CScriptVarLink *child = a->var->findChild(name); - if (!child) child = findInParentClasses(a->var, name); - if (!child) { - /* if we haven't found this defined yet, use the built-in - 'length' properly */ - if (a->var->isArray() && name == "length") { - int l = a->var->getArrayLength(); - child = new CScriptVarLink(new CScriptVar(l)); - } else if (a->var->isString() && name == "length") { - int l = a->var->getString().size(); - child = new CScriptVarLink(new CScriptVar(l)); - } else { - child = a->var->addChild(name); - } - } - parent = a->var; - a = child; - } - l->match(LEX_ID); - } else if (l->tk == '[') { // ------------------------------------- Array Access - l->match('['); - CScriptVarLink *index = expression(execute); - l->match(']'); - if (execute) { - CScriptVarLink *child = a->var->findChildOrCreate(index->var->getString()); - parent = a->var; - a = child; - } - CLEAN(index); - } else ASSERT(0); - } - return a; - } - if (l->tk==LEX_INT || l->tk==LEX_FLOAT) { - CScriptVar *a = new CScriptVar(l->tkStr, - ((l->tk==LEX_INT)?SCRIPTVAR_INTEGER:SCRIPTVAR_DOUBLE)); - l->match(l->tk); - return new CScriptVarLink(a); - } - if (l->tk==LEX_STR) { - CScriptVar *a = new CScriptVar(l->tkStr, SCRIPTVAR_STRING); - l->match(LEX_STR); - return new CScriptVarLink(a); - } - if (l->tk=='{') { - CScriptVar *contents = new CScriptVar(TINYJS_BLANK_DATA, SCRIPTVAR_OBJECT); - /* JSON-style object definition */ - l->match('{'); - while (l->tk != '}') { - string id = l->tkStr; - // 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(':'); - if (execute) { - CScriptVarLink *a = base(execute); - contents->addChild(id, a->var); - CLEAN(a); - } - // no need to clean here, as it will definitely be used - if (l->tk != '}') l->match(','); - } - - l->match('}'); - return new CScriptVarLink(contents); - } - if (l->tk=='[') { - CScriptVar *contents = new CScriptVar(TINYJS_BLANK_DATA, SCRIPTVAR_ARRAY); - /* JSON-style array */ - l->match('['); - int idx = 0; - while (l->tk != ']') { - if (execute) { - char idx_str[16]; // big enough for 2^32 - sprintf(idx_str,"%d",idx); - - CScriptVarLink *a = base(execute); - contents->addChild(idx_str, a->var); - CLEAN(a); - } - // no need to clean here, as it will definitely be used - if (l->tk != ']') l->match(','); - idx++; - } - l->match(']'); - return new CScriptVarLink(contents); - } - if (l->tk==LEX_R_FUNCTION) { - CScriptVarLink *funcVar = parseFunctionDefinition(); - if (funcVar->name != TINYJS_TEMP_NAME) - TRACE("Functions not defined at statement-level are not meant to have a name"); - return funcVar; - } - if (l->tk==LEX_R_NEW) { - // new -> create a new object - l->match(LEX_R_NEW); - const string &className = l->tkStr; - if (execute) { - CScriptVarLink *objClass = findInScopes(className); - if (!objClass) { - TRACE("%s is not a valid class name", className.c_str()); - return new CScriptVarLink(new CScriptVar()); - } - l->match(LEX_ID); - CScriptVar *obj = new CScriptVar(TINYJS_BLANK_DATA, SCRIPTVAR_OBJECT); - obj->addChild(TINYJS_PROTOTYPE_CLASS, objClass->var); - if (l->tk == '(') { - l->match('('); - l->match(')'); - } - // TODO: Object constructors - return new CScriptVarLink(obj); - } else { - l->match(LEX_ID); - if (l->tk == '(') { - l->match('('); - l->match(')'); - } - } - } - // Nothing we can do here... just hope it's the end... - l->match(LEX_EOF); - return 0; -} - -CScriptVarLink *CTinyJS::unary(bool &execute) { - CScriptVarLink *a; - if (l->tk=='!') { - l->match('!'); // binary not - a = factor(execute); - if (execute) { - CScriptVar zero(0); - CScriptVar *res = a->var->mathsOp(&zero, LEX_EQUAL); - CREATE_LINK(a, res); - } - } else - a = factor(execute); - return a; -} - -CScriptVarLink *CTinyJS::term(bool &execute) { - CScriptVarLink *a = unary(execute); - while (l->tk=='*' || l->tk=='/' || l->tk=='%') { - int op = l->tk; - l->match(l->tk); - CScriptVarLink *b = unary(execute); - if (execute) { - CScriptVar *res = a->var->mathsOp(b->var, op); - CREATE_LINK(a, res); - } - CLEAN(b); - } - return a; -} - -CScriptVarLink *CTinyJS::expression(bool &execute) { - bool negate = false; - if (l->tk=='-') { - l->match('-'); - negate = true; - } - CScriptVarLink *a = term(execute); - if (negate) { - CScriptVar *zero = new CScriptVar(0); - CScriptVar *res = zero->mathsOp(a->var, '-'); - CREATE_LINK(a, res); - } - - while (l->tk=='+' || l->tk=='-' || - l->tk==LEX_PLUSPLUS || l->tk==LEX_MINUSMINUS) { - int op = l->tk; - l->match(l->tk); - if (op==LEX_PLUSPLUS || op==LEX_MINUSMINUS) { - if (execute) { - CScriptVar one(1); - CScriptVar *res = a->var->mathsOp(&one, op==LEX_PLUSPLUS ? '+' : '-'); - // in-place add/subtract - a->replaceWith(res); - } - } else { - CScriptVarLink *b = term(execute); - if (execute) { - // not in-place, so just replace - CScriptVar *res = a->var->mathsOp(b->var, op); - CREATE_LINK(a, res); - } - CLEAN(b); - } - } - return a; -} - -CScriptVarLink *CTinyJS::condition(bool &execute) { - CScriptVarLink *a = expression(execute); - CScriptVarLink *b; - while (l->tk==LEX_EQUAL || l->tk==LEX_NEQUAL || - l->tk==LEX_TYPEEQUAL || l->tk==LEX_NTYPEEQUAL || - l->tk==LEX_LEQUAL || l->tk==LEX_GEQUAL || - l->tk=='<' || l->tk=='>') { - int op = l->tk; - l->match(l->tk); - b = expression(execute); - if (execute) { - CScriptVar *res = a->var->mathsOp(b->var, op); - CREATE_LINK(a,res); - } - CLEAN(b); - } - return a; -} - -CScriptVarLink *CTinyJS::logic(bool &execute) { - CScriptVarLink *a = condition(execute); - CScriptVarLink *b; - while (l->tk=='&' || l->tk=='|' || l->tk=='^' || l->tk==LEX_ANDAND || l->tk==LEX_OROR) { - bool noexecute = false; - int op = l->tk; - l->match(l->tk); - bool shortCircuit = false; - bool boolean = false; - // if we have short-circuit ops, then if we know the outcome - // we don't bother to execute the other op. Even if not - // we need to tell mathsOp it's an & or | - if (op==LEX_ANDAND) { - op = '&'; - shortCircuit = !a->var->getBool(); - boolean = true; - } else if (op==LEX_OROR) { - op = '|'; - shortCircuit = a->var->getBool(); - boolean = true; - } - b = condition(shortCircuit ? noexecute : execute); - if (execute && !shortCircuit) { - if (boolean) { - CScriptVar *newa = new CScriptVar(a->var->getBool()); - CScriptVar *newb = new CScriptVar(b->var->getBool()); - CREATE_LINK(a, newa); - CREATE_LINK(b, newb); - } - CScriptVar *res = a->var->mathsOp(b->var, op); - CREATE_LINK(a, res); - } - CLEAN(b); - } - return a; -} - -CScriptVarLink *CTinyJS::base(bool &execute) { - CScriptVarLink *lhs = logic(execute); - if (l->tk=='=' || l->tk==LEX_PLUSEQUAL || l->tk==LEX_MINUSEQUAL) { - /* If we're assigning to this and we don't have a parent, - * add it to the symbol table root as per JavaScript. */ - if (execute && !lhs->owned) { - if (lhs->name.length()>0) { - CScriptVarLink *realLhs = root->addChildNoDup(lhs->name, lhs->var); - CLEAN(lhs); - lhs = realLhs; - } else - TRACE("Trying to assign to an un-named type\n"); - } - - int op = l->tk; - l->match(l->tk); - CScriptVarLink *rhs = base(execute); - if (execute) { - if (op=='=') { - lhs->replaceWith(rhs); - } else if (op==LEX_PLUSEQUAL) { - CScriptVar *res = lhs->var->mathsOp(rhs->var, '+'); - lhs->replaceWith(res); - } else if (op==LEX_MINUSEQUAL) { - CScriptVar *res = lhs->var->mathsOp(rhs->var, '-'); - lhs->replaceWith(res); - } else ASSERT(0); - } - CLEAN(rhs); - } - return lhs; -} - -void CTinyJS::block(bool &execute) { - // TODO: fast skip of blocks - l->match('{'); - if (execute) { - while (l->tk && l->tk!='}') - statement(execute); - 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==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. 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); - CScriptVarLink *a = 0; - if (execute) - a = scopes.back()->findChildOrCreate(l->tkStr); - l->match(LEX_ID); - // now do stuff defined with dots - while (l->tk == '.') { - l->match('.'); - if (execute) { - CScriptVarLink *lastA = a; - a = lastA->var->findChildOrCreate(l->tkStr); - } - l->match(LEX_ID); - } - // sort out initialiser - if (l->tk == '=') { - l->match('='); - CScriptVarLink *var = base(execute); - if (execute) - a->replaceWith(var); - CLEAN(var); - } - l->match(';'); - } else if (l->tk==LEX_R_IF) { - l->match(LEX_R_IF); - l->match('('); - CScriptVarLink *var = base(execute); - l->match(')'); - bool cond = execute && var->var->getBool(); - CLEAN(var); - bool noexecute = false; // because we need to be abl;e to write to it - statement(cond ? execute : noexecute); - if (l->tk==LEX_R_ELSE) { - l->match(LEX_R_ELSE); - statement(cond ? noexecute : execute); - } - } else if (l->tk==LEX_R_WHILE) { - // We do repetition by pulling out the string representing our statement - // there's definitely some opportunity for optimisation here - l->match(LEX_R_WHILE); - l->match('('); - int whileCondStart = l->tokenStart; - bool noexecute = false; - CScriptVarLink *cond = base(execute); - bool loopCond = execute && cond->var->getBool(); - CLEAN(cond); - CScriptLex *whileCond = l->getSubLex(whileCondStart); - l->match(')'); - int whileBodyStart = l->tokenStart; - statement(loopCond ? execute : noexecute); - CScriptLex *whileBody = l->getSubLex(whileBodyStart); - CScriptLex *oldLex = l; - int loopCount = TINYJS_LOOP_MAX_ITERATIONS; - while (loopCond && loopCount-->0) { - whileCond->reset(); - l = whileCond; - cond = base(execute); - loopCond = execute && cond->var->getBool(); - CLEAN(cond); - if (loopCond) { - whileBody->reset(); - l = whileBody; - statement(execute); - } - } - l = oldLex; - delete whileCond; - delete whileBody; - - if (loopCount<=0) { - root->trace(); - 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) { - l->match(LEX_R_FOR); - l->match('('); - statement(execute); // initialisation - //l->match(';'); - int forCondStart = l->tokenStart; - bool noexecute = false; - CScriptVarLink *cond = base(execute); // condition - bool loopCond = execute && cond->var->getBool(); - CLEAN(cond); - CScriptLex *forCond = l->getSubLex(forCondStart); - l->match(';'); - int forIterStart = l->tokenStart; - base(noexecute); // iterator - CScriptLex *forIter = l->getSubLex(forIterStart); - l->match(')'); - int forBodyStart = l->tokenStart; - statement(loopCond ? execute : noexecute); - CScriptLex *forBody = l->getSubLex(forBodyStart); - CScriptLex *oldLex = l; - if (loopCond) { - forIter->reset(); - l = forIter; - base(execute); - } - int loopCount = TINYJS_LOOP_MAX_ITERATIONS; - while (execute && loopCond && loopCount-->0) { - forCond->reset(); - l = forCond; - cond = base(execute); - loopCond = cond->var->getBool(); - CLEAN(cond); - if (execute && loopCond) { - forBody->reset(); - l = forBody; - statement(execute); - } - if (execute && loopCond) { - forIter->reset(); - l = forIter; - base(execute); - } - } - l = oldLex; - delete forCond; - delete forIter; - delete forBody; - if (loopCount<=0) { - root->trace(); - 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) { - l->match(LEX_R_RETURN); - CScriptVarLink *result = 0; - if (l->tk != ';') - result = base(execute); - if (execute) { - CScriptVarLink *resultVar = scopes.back()->findChild(TINYJS_RETURN_VAR); - if (resultVar) - resultVar->replaceWith(result); - else - TRACE("RETURN statement, but not in a function.\n"); - execute = false; - } - CLEAN(result); - l->match(';'); - } else if (l->tk==LEX_R_FUNCTION) { - CScriptVarLink *funcVar = parseFunctionDefinition(); - if (execute) { - if (funcVar->name == TINYJS_TEMP_NAME) - TRACE("Functions defined at statement-level are meant to have a name\n"); - else - scopes.back()->addChildNoDup(funcVar->name, funcVar->var); - } - CLEAN(funcVar); - } else l->match(LEX_EOF); -} - -/// Get the value of the given variable, or return 0 -const string *CTinyJS::getVariable(const string &path) { - // traverse path - size_t prevIdx = 0; - size_t thisIdx = path.find('.'); - if (thisIdx == string::npos) thisIdx = path.length(); - CScriptVar *var = root; - while (var && prevIdxfindChild(el); - var = varl?varl->var:0; - prevIdx = thisIdx+1; - thisIdx = path.find('.', prevIdx); - if (thisIdx == string::npos) thisIdx = path.length(); - } - // return result - if (var) - return &var->getString(); - else - return 0; -} - -/// Finds a child, looking recursively up the scopes -CScriptVarLink *CTinyJS::findInScopes(const std::string &childName) { - for (int s=scopes.size()-1;s>=0;s--) { - CScriptVarLink *v = scopes[s]->findChild(childName); - if (v) return v; - } - return NULL; - -} - -/// Look up in any parent classes of the given object -CScriptVarLink *CTinyJS::findInParentClasses(CScriptVar *object, const std::string &name) { - // Look for links to actual parent classes - CScriptVarLink *parentClass = object->findChild(TINYJS_PROTOTYPE_CLASS); - while (parentClass) { - CScriptVarLink *implementation = parentClass->var->findChild(name); - if (implementation) return implementation; - parentClass = parentClass->var->findChild(TINYJS_PROTOTYPE_CLASS); - } - // else fake it for strings and finally objects - if (object->isString()) { - CScriptVarLink *implementation = stringClass->findChild(name); - if (implementation) return implementation; - } - if (object->isArray()) { - CScriptVarLink *implementation = arrayClass->findChild(name); - if (implementation) return implementation; - } - CScriptVarLink *implementation = objectClass->findChild(name); - if (implementation) return implementation; - - return 0; -} +/* + * 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. + */ + +/* Version 0.1 : (gw) First published on Google Code + Version 0.11 : Making sure the 'root' variable never changes + 'symbol_base' added for the current base of the sybmbol table + Version 0.12 : Added findChildOrCreate, changed string passing to use references + Fixed broken string encoding in getJSString() + Removed getInitCode and added getJSON instead + 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 + Version 0.14 : Added parsing of more number types + Added parsing of string defined with ' + Changed nil to null as per spec, added 'undefined' + Now set variables with the correct scope, and treat unknown + as 'undefined' rather than failing + Added proper (I hope) handling of null and undefined + Added === check + Version 0.15 : Fix for possible memory leaks + Version 0.16 : Removal of un-needed findRecursive calls + symbol_base removed and replaced with 'scopes' stack + Added reference counting a proper tree structure + (Allowing pass by reference) + Allowed JSON output to output IDs, not strings + Added get/set for array indices + Changed Callbacks to include user data pointer + Added some support for objects + Added more Java-esque builtin functions + Version 0.17 : Now we don't deepCopy the parent object of the class + Added JSON.stringify and eval() + Nicer JSON indenting + Fixed function output in JSON + Added evaluateComplex + Fixed some reentrancy issues with evaluate/execute + Version 0.18 : Fixed some issues with code being executed when it shouldn't + Version 0.19 : Added array.length + Changed '__parent' to 'prototype' to bring it more in line with javascript + Version 0.20 : Added '%' operator + Version 0.21 : Added array type + String.length() no more - now String.length + Added extra constructors to reduce confusion + Fixed checks against undefined + Version 0.22 : First part of ardi's changes: + sprintf -> sprintf_s + extra tokens parsed + array memory leak fixed + Fixed memory leak in evaluateComplex + Fixed memory leak in FOR loops + Fixed memory leak for unary minus + + NOTE: This doesn't support constructors for objects + Recursive loops of data such as a.foo = a; fail to be garbage collected + 'length' cannot be set + There is no ternary operator implemented yet + The postfix increment operator returns the current value, not the previous as it should. + + TODO: + Utility va-args style function in TinyJS for executing a function directly + + */ + +#include "TinyJS.h" +#include + +#define ASSERT(X) assert(X) +/* Frees the given link IF it isn't owned by anything else */ +#define CLEAN(x) { CScriptVarLink *__v = x; if (__v && !__v->owned) { delete __v; } } +/* Create a LINK to point to VAR and free the old link. + * BUT this is more clever - it tries to keep the old link if it's not owned to save allocations */ +#define CREATE_LINK(LINK, VAR) { if (!LINK || LINK->owned) LINK = new CScriptVarLink(VAR); else LINK->replaceWith(VAR); } + +#include +#include +#include +#include +#include + +using namespace std; + +#ifdef _WIN32 +#ifdef _DEBUG + #ifndef DBG_NEW + #define DBG_NEW new ( _NORMAL_BLOCK , __FILE__ , __LINE__ ) + #define new DBG_NEW + #endif +#endif +#endif + +#ifdef __GNUC__ +#define vsprintf_s vsnprintf +#define sprintf_s snprintf +#define _strdup strdup +#endif + +// ----------------------------------------------------------------------------------- Memory Debug + +#define DEBUG_MEMORY 0 + +#if DEBUG_MEMORY + +vector allocatedVars; +vector allocatedLinks; + +void mark_allocated(CScriptVar *v) { + allocatedVars.push_back(v); +} + +void mark_deallocated(CScriptVar *v) { + for (size_t i=0;igetRefs()); + allocatedVars[i]->trace(" "); + } + for (size_t i=0;iname.c_str(), allocatedLinks[i]->var->getRefs()); + allocatedLinks[i]->var->trace(" "); + } + allocatedVars.clear(); + allocatedLinks.clear(); +} +#endif + +// ----------------------------------------------------------------------------------- Utils +bool isWhitespace(char ch) { + return (ch==' ') || (ch=='\t') || (ch=='\n') || (ch=='\r'); +} + +bool isNumeric(char ch) { + return (ch>='0') && (ch<='9'); +} +bool isNumber(const string &str) { + for (size_t i=0;i='0') && (ch<='9')) || + ((ch>='a') && (ch<='f')) || + ((ch>='A') && (ch<='F')); +} +bool isAlpha(char ch) { + return ((ch>='a') && (ch<='z')) || ((ch>='A') && (ch<='Z')) || ch=='_'; +} + +bool isIDString(const char *s) { + if (!isAlpha(*s)) + return false; + while (*s) { + if (!(isAlpha(*s) || isNumeric(*s))) + return false; + s++; + } + return true; +} + +void replace(string &str, char textFrom, const char *textTo) { + int sLen = strlen(textTo); + size_t p = str.find(textFrom); + while (p != string::npos) { + str = str.substr(0, p) + textTo + str.substr(p+1); + p = str.find(textFrom, p+sLen); + } +} + +/// convert the given string into a quoted string suitable for javascript +std::string getJSString(const std::string &str) { + std::string nStr = str; + for (size_t i=0;idata; + dataOwned = false; + dataStart = startChar; + dataEnd = endChar; + reset(); +} + +CScriptLex::~CScriptLex(void) +{ + if (dataOwned) + free((void*)data); +} + +void CScriptLex::reset() { + dataPos = dataStart; + tokenStart = 0; + tokenEnd = 0; + tokenLastEnd = 0; + tk = 0; + tkStr = ""; + getNextCh(); + getNextCh(); + getNextToken(); +} + +void CScriptLex::match(int expected_tk) { + if (tk!=expected_tk) { + ostringstream errorString; + errorString << "Got " << getTokenStr(tk) << " expected " << getTokenStr(expected_tk) + << " at " << getPosition(tokenStart) << " in '" << data << "'"; + throw new CScriptException(errorString.str()); + } + getNextToken(); +} + +string CScriptLex::getTokenStr(int token) { + if (token>32 && token<128) { + char buf[4] = "' '"; + buf[1] = (char)token; + return buf; + } + switch (token) { + case LEX_EOF : return "EOF"; + case LEX_ID : return "ID"; + case LEX_INT : return "INT"; + case LEX_FLOAT : return "FLOAT"; + case LEX_STR : return "STRING"; + case LEX_EQUAL : return "=="; + case LEX_TYPEEQUAL : return "==="; + case LEX_NEQUAL : return "!="; + case LEX_NTYPEEQUAL : return "!=="; + case LEX_LEQUAL : return "<="; + case LEX_LSHIFT : return "<<"; + case LEX_LSHIFTEQUAL : return "<<="; + case LEX_GEQUAL : return ">="; + case LEX_RSHIFT : return ">>"; + case LEX_RSHIFTEQUAL : return ">>="; + case LEX_PLUSEQUAL : return "+="; + case LEX_MINUSEQUAL : return "-="; + case LEX_PLUSPLUS : return "++"; + case LEX_MINUSMINUS : return "--"; + case LEX_ANDEQUAL : return "&="; + case LEX_ANDAND : return "&&"; + case LEX_OREQUAL : return "|="; + case LEX_OROR : return "||"; + case LEX_XOREQUAL : return "^="; + // reserved words + case LEX_R_IF : return "if"; + case LEX_R_ELSE : return "else"; + case LEX_R_DO : return "do"; + case LEX_R_WHILE : return "while"; + case LEX_R_FOR : return "for"; + case LEX_R_BREAK : return "break"; + case LEX_R_CONTINUE : return "continue"; + case LEX_R_FUNCTION : return "function"; + case LEX_R_RETURN : return "return"; + case LEX_R_VAR : return "var"; + case LEX_R_TRUE : return "true"; + case LEX_R_FALSE : return "false"; + case LEX_R_NULL : return "null"; + case LEX_R_UNDEFINED : return "undefined"; + case LEX_R_NEW : return "new"; + } + + ostringstream msg; + msg << "?[" << token << "]"; + return msg.str(); +} + +void CScriptLex::getNextCh() { + currCh = nextCh; + if (dataPos < dataEnd) + nextCh = data[dataPos]; + else + nextCh = 0; + dataPos++; +} + +void CScriptLex::getNextToken() { + tk = LEX_EOF; + tkStr.clear(); + while (currCh && isWhitespace(currCh)) getNextCh(); + // newline comments + if (currCh=='/' && nextCh=='/') { + while (currCh && currCh!='\n') getNextCh(); + getNextCh(); + getNextToken(); + return; + } + // block comments + if (currCh=='/' && nextCh=='*') { + while (currCh && (currCh!='*' || nextCh!='/')) getNextCh(); + getNextCh(); + getNextCh(); + getNextToken(); + return; + } + // record beginning of this token + tokenStart = dataPos-2; + // tokens + if (isAlpha(currCh)) { // IDs + while (isAlpha(currCh) || isNumeric(currCh)) { + tkStr += currCh; + getNextCh(); + } + tk = LEX_ID; + if (tkStr=="if") tk = LEX_R_IF; + else if (tkStr=="else") tk = LEX_R_ELSE; + else if (tkStr=="do") tk = LEX_R_DO; + else if (tkStr=="while") tk = LEX_R_WHILE; + else if (tkStr=="for") tk = LEX_R_FOR; + else if (tkStr=="break") tk = LEX_R_BREAK; + else if (tkStr=="continue") tk = LEX_R_CONTINUE; + else if (tkStr=="function") tk = LEX_R_FUNCTION; + else if (tkStr=="return") tk = LEX_R_RETURN; + else if (tkStr=="var") tk = LEX_R_VAR; + else if (tkStr=="true") tk = LEX_R_TRUE; + else if (tkStr=="false") tk = LEX_R_FALSE; + else if (tkStr=="null") tk = LEX_R_NULL; + else if (tkStr=="undefined") tk = LEX_R_UNDEFINED; + else if (tkStr=="new") tk = LEX_R_NEW; + } else if (isNumeric(currCh)) { // Numbers + bool isHex = false; + if (currCh=='0') { tkStr += currCh; getNextCh(); } + if (currCh=='x') { + isHex = true; + tkStr += currCh; getNextCh(); + } + tk = LEX_INT; + while (isNumeric(currCh) || (isHex && isHexadecimal(currCh))) { + tkStr += currCh; + getNextCh(); + } + if (!isHex && currCh=='.') { + tk = LEX_FLOAT; + tkStr += '.'; + getNextCh(); + while (isNumeric(currCh)) { + tkStr += currCh; + getNextCh(); + } + } + // do fancy e-style floating point + if (!isHex && currCh=='e') { + tk = LEX_FLOAT; + tkStr += currCh; getNextCh(); + if (currCh=='-') { tkStr += currCh; getNextCh(); } + while (isNumeric(currCh)) { + tkStr += currCh; getNextCh(); + } + } + } else if (currCh=='"') { + // strings... + getNextCh(); + while (currCh && currCh!='"') { + if (currCh == '\\') { + getNextCh(); + switch (currCh) { + case 'n' : tkStr += '\n'; break; + case '"' : tkStr += '"'; break; + case '\\' : tkStr += '\\'; break; + default: tkStr += currCh; + } + } else { + tkStr += currCh; + } + getNextCh(); + } + getNextCh(); + tk = LEX_STR; + } else if (currCh=='\'') { + // strings again... + getNextCh(); + while (currCh && currCh!='\'') { + if (currCh == '\\') { + getNextCh(); + switch (currCh) { + case 'n' : tkStr += '\n'; break; + case '\'' : tkStr += '\''; break; + case '\\' : tkStr += '\\'; break; + default: tkStr += currCh; + } + } else { + tkStr += currCh; + } + getNextCh(); + } + getNextCh(); + tk = LEX_STR; + } else { + // single chars + tk = currCh; + if (currCh) getNextCh(); + if (tk=='=' && currCh=='=') { // == + tk = LEX_EQUAL; + getNextCh(); + if (currCh=='=') { // === + tk = LEX_TYPEEQUAL; + getNextCh(); + } + } else if (tk=='!' && currCh=='=') { // != + tk = LEX_NEQUAL; + getNextCh(); + if (currCh=='=') { // !== + tk = LEX_NTYPEEQUAL; + getNextCh(); + } + } else if (tk=='<' && currCh=='=') { + tk = LEX_LEQUAL; + getNextCh(); + } else if (tk=='<' && currCh=='<') { + tk = LEX_LSHIFT; + getNextCh(); + if (currCh=='=') { // <<= + tk = LEX_LSHIFTEQUAL; + getNextCh(); + } + } else if (tk=='>' && currCh=='=') { + tk = LEX_GEQUAL; + getNextCh(); + } else if (tk=='>' && currCh=='>') { + tk = LEX_RSHIFT; + getNextCh(); + if (currCh=='=') { // <<= + tk = LEX_RSHIFTEQUAL; + 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_ANDEQUAL; + getNextCh(); + } else if (tk=='&' && currCh=='&') { + tk = LEX_ANDAND; + getNextCh(); + } else if (tk=='|' && currCh=='=') { + tk = LEX_OREQUAL; + getNextCh(); + } else if (tk=='|' && currCh=='|') { + tk = LEX_OROR; + getNextCh(); + } else if (tk=='^' && currCh=='=') { + tk = LEX_XOREQUAL; + getNextCh(); + } + } + /* This isn't quite right yet */ + tokenLastEnd = tokenEnd; + tokenEnd = dataPos-3; +} + +string CScriptLex::getSubString(int lastPosition) { + int lastCharIdx = tokenLastEnd+1; + if (lastCharIdx < dataEnd) { + /* save a memory alloc by using our data array to create the + substring */ + char old = data[lastCharIdx]; + data[lastCharIdx] = 0; + std::string value = &data[lastPosition]; + data[lastCharIdx] = old; + return value; + } else { + return std::string(&data[lastPosition]); + } +} + + +CScriptLex *CScriptLex::getSubLex(int lastPosition) { + int lastCharIdx = tokenLastEnd+1; + if (lastCharIdx < dataEnd) + return new CScriptLex( this, lastPosition, lastCharIdx); + else + return new CScriptLex( this, lastPosition, dataEnd ); +} + +string CScriptLex::getPosition(int pos) { + if (pos<0) pos=tokenLastEnd; + int line = 1,col = 1; + for (int i=0;iname = name; + this->nextSibling = 0; + this->prevSibling = 0; + this->var = var->ref(); + this->owned = false; +} + +CScriptVarLink::CScriptVarLink(const CScriptVarLink &link) { + // Copy constructor +#if DEBUG_MEMORY + mark_allocated(this); +#endif + this->name = link.name; + this->nextSibling = 0; + this->prevSibling = 0; + this->var = link.var->ref(); + this->owned = false; +} + +CScriptVarLink::~CScriptVarLink() { +#if DEBUG_MEMORY + mark_deallocated(this); +#endif + var->unref(); +} + +void CScriptVarLink::replaceWith(CScriptVar *newVar) { + CScriptVar *oldVar = var; + var = newVar->ref(); + oldVar->unref(); +} + +void CScriptVarLink::replaceWith(CScriptVarLink *newVar) { + if (newVar) + replaceWith(newVar->var); + else + replaceWith(new CScriptVar()); +} + +// ----------------------------------------------------------------------------------- CSCRIPTVAR + +CScriptVar::CScriptVar() { + refs = 0; +#if DEBUG_MEMORY + mark_allocated(this); +#endif + init(); + flags = SCRIPTVAR_UNDEFINED; + data = TINYJS_BLANK_DATA; +} + +CScriptVar::CScriptVar(const string &str) { + refs = 0; +#if DEBUG_MEMORY + mark_allocated(this); +#endif + init(); + flags = SCRIPTVAR_STRING; + data = str; +} + + +CScriptVar::CScriptVar(const string &varData, int varFlags) { + refs = 0; +#if DEBUG_MEMORY + mark_allocated(this); +#endif + init(); + flags = varFlags; + data = varData; +} + +CScriptVar::CScriptVar(double val) { + refs = 0; +#if DEBUG_MEMORY + mark_allocated(this); +#endif + init(); + setDouble(val); +} + +CScriptVar::CScriptVar(int val) { + refs = 0; +#if DEBUG_MEMORY + mark_allocated(this); +#endif + init(); + setInt(val); +} + +CScriptVar::~CScriptVar(void) { +#if DEBUG_MEMORY + mark_deallocated(this); +#endif + removeAllChildren(); +} + +void CScriptVar::init() { + firstChild = 0; + lastChild = 0; + flags = 0; + jsCallback = 0; + jsCallbackUserData = 0; +} + +CScriptVar *CScriptVar::getReturnVar() { + return getParameter(TINYJS_RETURN_VAR); +} + +void CScriptVar::setReturnVar(CScriptVar *var) { + findChildOrCreate(TINYJS_RETURN_VAR)->replaceWith(var); +} + + +CScriptVar *CScriptVar::getParameter(const std::string &name) { + return findChildOrCreate(name)->var; +} + +CScriptVarLink *CScriptVar::findChild(const string &childName) { + CScriptVarLink *v = firstChild; + while (v) { + if (v->name.compare(childName)==0) + return v; + v = v->nextSibling; + } + return 0; +} + +CScriptVarLink *CScriptVar::findChildOrCreate(const string &childName, int varFlags) { + CScriptVarLink *l = findChild(childName); + if (l) return l; + + return addChild(childName, new CScriptVar(TINYJS_BLANK_DATA, varFlags)); +} + +CScriptVarLink *CScriptVar::findChildOrCreateByPath(const std::string &path) { + size_t p = path.find('.'); + if (p == string::npos) + return findChildOrCreate(path); + + return findChildOrCreate(path.substr(0,p), SCRIPTVAR_OBJECT)->var-> + findChildOrCreateByPath(path.substr(p+1)); +} + +CScriptVarLink *CScriptVar::addChild(const std::string &childName, CScriptVar *child) { + if (isUndefined()) { + flags = SCRIPTVAR_OBJECT; + } + // if no child supplied, create one + if (!child) + child = new CScriptVar(); + + CScriptVarLink *link = new CScriptVarLink(child, childName); + link->owned = true; + if (lastChild) { + lastChild->nextSibling = link; + link->prevSibling = lastChild; + lastChild = link; + } else { + firstChild = link; + lastChild = link; + } + return link; +} + +CScriptVarLink *CScriptVar::addChildNoDup(const std::string &childName, CScriptVar *child) { + // if no child supplied, create one + if (!child) + child = new CScriptVar(); + + CScriptVarLink *v = findChild(childName); + if (v) { + v->replaceWith(child); + } else { + v = addChild(childName, child); + } + + return v; +} + +void CScriptVar::removeChild(CScriptVar *child) { + CScriptVarLink *link = firstChild; + while (link) { + if (link->var == child) + break; + link = link->nextSibling; + } + ASSERT(link); + removeLink(link); +} + +void CScriptVar::removeLink(CScriptVarLink *link) { + if (!link) return; + if (link->nextSibling) + link->nextSibling->prevSibling = link->prevSibling; + if (link->prevSibling) + link->prevSibling->nextSibling = link->nextSibling; + if (lastChild == link) + lastChild = link->prevSibling; + if (firstChild == link) + firstChild = link->nextSibling; + delete link; +} + +void CScriptVar::removeAllChildren() { + CScriptVarLink *c = firstChild; + while (c) { + CScriptVarLink *t = c->nextSibling; + delete c; + c = t; + } + firstChild = 0; + lastChild = 0; +} + +CScriptVar *CScriptVar::getArrayIndex(int idx) { + char sIdx[64]; + sprintf_s(sIdx, sizeof(sIdx), "%d", idx); + CScriptVarLink *link = findChild(sIdx); + if (link) return link->var; + else return new CScriptVar(TINYJS_BLANK_DATA, SCRIPTVAR_NULL); // undefined +} + +void CScriptVar::setArrayIndex(int idx, CScriptVar *value) { + char sIdx[64]; + sprintf_s(sIdx, sizeof(sIdx), "%d", idx); + CScriptVarLink *link = findChild(sIdx); + + if (link) { + if (value->isUndefined()) + removeLink(link); + else + link->replaceWith(value); + } else { + if (!value->isUndefined()) + addChild(sIdx, value); + } +} + +int CScriptVar::getArrayLength() { + int highest = -1; + if (!isArray()) return 0; + + CScriptVarLink *link = firstChild; + while (link) { + if (isNumber(link->name)) { + int val = atoi(link->name.c_str()); + if (val > highest) highest = val; + } + link = link->nextSibling; + } + return highest+1; +} + +int CScriptVar::getChildren() { + int n = 0; + CScriptVarLink *link = firstChild; + while (link) { + n++; + link = link->nextSibling; + } + return n; +} + +int CScriptVar::getInt() { + /* strtol understands about hex and octal */ + if (isInt()) return strtol(data.c_str(),0,0); + if (isNull()) return 0; + if (isUndefined()) return 0; + if (isDouble()) return (int)getDouble(); + return 0; +} + +double CScriptVar::getDouble() { + if (isDouble()) return strtod(data.c_str(),0); + if (isInt()) return getInt(); + if (isNull()) return 0; + if (isUndefined()) return 0; + return 0; /* or NaN? */ +} + +const string &CScriptVar::getString() { + /* Because we can't return a string that is generated on demand. + * I should really just use char* :) */ + static string s_null = "null"; + static string s_undefined = "undefined"; + if (isNull()) return s_null; + if (isUndefined()) return s_undefined; + return data; +} + +void CScriptVar::setInt(int val) { + char buf[256]; + sprintf_s(buf, sizeof(buf), "%d", val); + flags = (flags&~SCRIPTVAR_VARTYPEMASK) | SCRIPTVAR_INTEGER; + data = buf; +} + +void CScriptVar::setDouble(double val) { + char buf[256]; + sprintf_s(buf, sizeof(buf), "%lf", val); + flags = (flags&~SCRIPTVAR_VARTYPEMASK) | SCRIPTVAR_DOUBLE; + data = buf; +} + +void CScriptVar::setString(const string &str) { + // name sure it's not still a number or integer + flags = (flags&~SCRIPTVAR_VARTYPEMASK) | SCRIPTVAR_STRING; + data = str; +} + +void CScriptVar::setUndefined() { + // name sure it's not still a number or integer + flags = (flags&~SCRIPTVAR_VARTYPEMASK) | SCRIPTVAR_UNDEFINED; + data = TINYJS_BLANK_DATA; + removeAllChildren(); +} + +CScriptVar *CScriptVar::mathsOp(CScriptVar *b, int op) { + CScriptVar *a = this; + // TODO Equality checks on classes/structures + // Type equality check + if (op == LEX_TYPEEQUAL || op == LEX_NTYPEEQUAL) { + bool eql = ((a->flags & SCRIPTVAR_VARTYPEMASK) == + (b->flags & SCRIPTVAR_VARTYPEMASK)) && + a->data == b->data; + if (op == LEX_TYPEEQUAL) + return new CScriptVar(eql); + else + return new CScriptVar(!eql); + } + // do maths... + if (a->isUndefined() && b->isUndefined()) { + if (op == LEX_EQUAL) return new CScriptVar(true); + else if (op == LEX_NEQUAL) return new CScriptVar(false); + else return new CScriptVar(); // undefined + } else if ((a->isNumeric() || a->isUndefined()) && + (b->isNumeric() || b->isUndefined())) { + if (!a->isDouble() && !b->isDouble()) { + // use ints + int da = a->getInt(); + int db = b->getInt(); + switch (op) { + case '+': return new CScriptVar(da+db); + case '-': return new CScriptVar(da-db); + case '*': return new CScriptVar(da*db); + case '/': return new CScriptVar(da/db); + case '&': return new CScriptVar(da&db); + case '|': return new CScriptVar(da|db); + case '^': return new CScriptVar(da^db); + case '%': return new CScriptVar(da%db); + case LEX_EQUAL: return new CScriptVar(da==db); + case LEX_NEQUAL: return new CScriptVar(da!=db); + case '<': return new CScriptVar(da': return new CScriptVar(da>db); + case LEX_GEQUAL: return new CScriptVar(da>=db); + default: throw new CScriptException("This operation not supported on the int datatype"); + } + } else { + // use doubles + double da = a->getDouble(); + double db = b->getDouble(); + switch (op) { + case '+': return new CScriptVar(da+db); + case '-': return new CScriptVar(da-db); + case '*': return new CScriptVar(da*db); + case '/': return new CScriptVar(da/db); + case LEX_EQUAL: return new CScriptVar(da==db); + case LEX_NEQUAL: return new CScriptVar(da!=db); + case '<': return new CScriptVar(da': return new CScriptVar(da>db); + case LEX_GEQUAL: return new CScriptVar(da>=db); + default: throw new CScriptException("This operation not supported on the double datatype"); + } + } + } else { + string da = a->getString(); + string db = b->getString(); + // use strings + switch (op) { + case '+': return new CScriptVar(da+db, SCRIPTVAR_STRING); + case LEX_EQUAL: return new CScriptVar(da==db); + case LEX_NEQUAL: return new CScriptVar(da!=db); + case '<': return new CScriptVar(da': return new CScriptVar(da>db); + case LEX_GEQUAL: return new CScriptVar(da>=db); + default: throw new CScriptException("This operation not supported on the string datatype"); + } + } + ASSERT(0); + return 0; +} + +void CScriptVar::copyValue(CScriptVar *val) { + if (val) { + // we *don't* copy the name + data = val->data; + flags = (flags & ~SCRIPTVAR_VARTYPEMASK) | (val->flags & SCRIPTVAR_VARTYPEMASK); + // remove all current children + removeAllChildren(); + // copy children of 'val' + CScriptVarLink *child = val->firstChild; + while (child) { + CScriptVar *copied; + // don't copy the 'parent' object... + if (child->name != TINYJS_PROTOTYPE_CLASS) + copied = child->var->deepCopy(); + else + copied = child->var; + + addChild(child->name, copied); + + child = child->nextSibling; + } + } else { + setUndefined(); + } +} + +CScriptVar *CScriptVar::deepCopy() { + CScriptVar *newVar = new CScriptVar(data, flags); + // copy children + CScriptVarLink *child = firstChild; + while (child) { + CScriptVar *copied; + // don't copy the 'parent' object... + if (child->name != TINYJS_PROTOTYPE_CLASS) + copied = child->var->deepCopy(); + else + copied = child->var; + + newVar->addChild(child->name, copied); + child = child->nextSibling; + } + return newVar; +} + +void CScriptVar::trace(string indentStr, const string &name) { + TRACE("%s'%s' = '%s' %s\n", + indentStr.c_str(), + name.c_str(), + data.c_str(), + getFlagsAsString().c_str()); + string indent = indentStr+" "; + CScriptVarLink *link = firstChild; + while (link) { + link->var->trace(indent, link->name); + link = link->nextSibling; + } +} + +string CScriptVar::getFlagsAsString() { + string flagstr = ""; + if (flags&SCRIPTVAR_FUNCTION) flagstr = flagstr + "FUNCTION "; + if (flags&SCRIPTVAR_OBJECT) flagstr = flagstr + "OBJECT "; + if (flags&SCRIPTVAR_ARRAY) flagstr = flagstr + "ARRAY "; + 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(); + if (isFunction()) { + ostringstream funcStr; + funcStr << "function ("; + // get list of parameters + CScriptVarLink *link = firstChild; + while (link) { + funcStr << link->name; + if (link->nextSibling) funcStr << ","; + link = link->nextSibling; + } + // add function body + funcStr << ") " << getString(); + return funcStr.str(); + } + // if it is a string then we quote it + if (isString()) + return getJSString(getString()); + if (isNull()) + return "null"; + return "undefined"; +} + +void CScriptVar::getJSON(ostringstream &destination, const string linePrefix) { + if (isObject()) { + string indentedLinePrefix = linePrefix+" "; + // children - handle with bracketed list + destination << "{ \n"; + CScriptVarLink *link = firstChild; + while (link) { + destination << indentedLinePrefix; + if (isAlphaNum(link->name)) + destination << link->name; + else + destination << getJSString(link->name); + destination << " : "; + link->var->getJSON(destination, indentedLinePrefix); + link = link->nextSibling; + if (link) { + destination << ",\n"; + } + } + destination << "\n" << linePrefix << "}"; + } else if (isArray()) { + string indentedLinePrefix = linePrefix+" "; + destination << "[\n"; + int len = getArrayLength(); + if (len>10000) len=10000; // we don't want to get stuck here! + + for (int i=0;igetJSON(destination, indentedLinePrefix); + if (iref(); + // Add built-in classes + stringClass = (new CScriptVar(TINYJS_BLANK_DATA, SCRIPTVAR_OBJECT))->ref(); + arrayClass = (new CScriptVar(TINYJS_BLANK_DATA, SCRIPTVAR_OBJECT))->ref(); + objectClass = (new CScriptVar(TINYJS_BLANK_DATA, SCRIPTVAR_OBJECT))->ref(); + root->addChild("String", stringClass); + root->addChild("Array", arrayClass); + root->addChild("Object", objectClass); +} + +CTinyJS::~CTinyJS() { + ASSERT(!l); + scopes.clear(); + stringClass->unref(); + arrayClass->unref(); + objectClass->unref(); + root->unref(); + +#if DEBUG_MEMORY + show_allocated(); +#endif +} + +void CTinyJS::trace() { + root->trace(); +} + +void CTinyJS::execute(const string &code) { + CScriptLex *oldLex = l; + vector oldScopes = scopes; + l = new CScriptLex(code); + scopes.clear(); + scopes.push_back(root); + try { + bool execute = true; + while (l->tk) statement(execute); + } catch (CScriptException *e) { + ostringstream msg; + msg << "Error " << e->text << " at " << l->getPosition(); + delete l; + l = oldLex; + throw new CScriptException(msg.str()); + } + delete l; + l = oldLex; + scopes = oldScopes; +} + +CScriptVarLink CTinyJS::evaluateComplex(const string &code) { + CScriptLex *oldLex = l; + vector oldScopes = scopes; + + l = new CScriptLex(code); + scopes.clear(); + scopes.push_back(root); + CScriptVarLink *v = 0; + try { + bool execute = true; + v = base(execute); + } catch (CScriptException *e) { + ostringstream msg; + msg << "Error " << e->text << " at " << l->getPosition(); + delete l; + l = oldLex; + throw new CScriptException(msg.str()); + } + delete l; + l = oldLex; + scopes = oldScopes; + + if (v) { + CScriptVarLink r = *v; + CLEAN(v); + return r; + } + // return undefined... + return CScriptVarLink(new CScriptVar()); +} + +string CTinyJS::evaluate(const string &code) { + return evaluateComplex(code).var->getString(); +} + +void CTinyJS::parseFunctionArguments(CScriptVar *funcVar) { + l->match('('); + while (l->tk!=')') { + funcVar->addChildNoDup(l->tkStr); + l->match(LEX_ID); + if (l->tk!=')') l->match(','); + } + l->match(')'); +} + +void CTinyJS::addNative(const string &funcDesc, JSCallback ptr, void *userdata) { + CScriptLex *oldLex = l; + l = new CScriptLex(funcDesc); + + CScriptVar *base = root; + + l->match(LEX_R_FUNCTION); + string funcName = l->tkStr; + l->match(LEX_ID); + /* Check for dots, we might want to do something like function String.substring ... */ + while (l->tk == '.') { + l->match('.'); + CScriptVarLink *link = base->findChild(funcName); + // if it doesn't exist, make an object class + if (!link) link = base->addChild(funcName, new CScriptVar(TINYJS_BLANK_DATA, SCRIPTVAR_OBJECT)); + base = link->var; + funcName = l->tkStr; + l->match(LEX_ID); + } + + CScriptVar *funcVar = new CScriptVar(TINYJS_BLANK_DATA, SCRIPTVAR_FUNCTION | SCRIPTVAR_NATIVE); + funcVar->setCallback(ptr, userdata); + parseFunctionArguments(funcVar); + delete l; + l = oldLex; + + base->addChild(funcName, funcVar); +} + +CScriptVarLink *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); + } + CScriptVarLink *funcVar = new CScriptVarLink(new CScriptVar(TINYJS_BLANK_DATA, SCRIPTVAR_FUNCTION), funcName); + parseFunctionArguments(funcVar->var); + int funcBegin = l->tokenStart; + bool noexecute = false; + block(noexecute); + funcVar->var->data = l->getSubString(funcBegin); + return funcVar; +} + +CScriptVarLink *CTinyJS::factor(bool &execute) { + if (l->tk=='(') { + l->match('('); + CScriptVarLink *a = base(execute); + l->match(')'); + return a; + } + if (l->tk==LEX_R_TRUE) { + l->match(LEX_R_TRUE); + return new CScriptVarLink(new CScriptVar(1)); + } + if (l->tk==LEX_R_FALSE) { + l->match(LEX_R_FALSE); + return new CScriptVarLink(new CScriptVar(0)); + } + if (l->tk==LEX_R_NULL) { + l->match(LEX_R_NULL); + return new CScriptVarLink(new CScriptVar(TINYJS_BLANK_DATA,SCRIPTVAR_NULL)); + } + if (l->tk==LEX_R_UNDEFINED) { + l->match(LEX_R_UNDEFINED); + return new CScriptVarLink(new CScriptVar(TINYJS_BLANK_DATA,SCRIPTVAR_UNDEFINED)); + } + if (l->tk==LEX_ID) { + CScriptVarLink *a = execute ? findInScopes(l->tkStr) : new CScriptVarLink(new CScriptVar()); + //printf("0x%08X for %s at %s\n", (unsigned int)a, l->tkStr.c_str(), l->getPosition().c_str()); + /* The parent if we're executing a method call */ + CScriptVar *parent = 0; + + if (execute && !a) { + /* Variable doesn't exist! JavaScript says we should create it + * (we won't add it here. This is done in the assignment operator)*/ + a = new CScriptVarLink(new CScriptVar(), l->tkStr); + } + l->match(LEX_ID); + while (l->tk=='(' || l->tk=='.' || l->tk=='[') { + if (l->tk=='(') { // ------------------------------------- Function Call + if (execute) { + if (!a->var->isFunction()) { + string errorMsg = "Expecting '"; + errorMsg = errorMsg + a->name + "' to be a function"; + throw new CScriptException(errorMsg.c_str()); + } + l->match('('); + // create a new symbol table entry for execution of this function + CScriptVar *functionRoot = new CScriptVar(TINYJS_BLANK_DATA, SCRIPTVAR_FUNCTION); + if (parent) + functionRoot->addChildNoDup("this", parent); + // grab in all parameters + CScriptVarLink *v = a->var->firstChild; + while (v) { + CScriptVarLink *value = base(execute); + if (execute) { + if (value->var->isBasic()) { + // pass by value + functionRoot->addChild(v->name, value->var->deepCopy()); + } else { + // pass by reference + functionRoot->addChild(v->name, value->var); + } + } + CLEAN(value); + if (l->tk!=')') l->match(','); + v = v->nextSibling; + } + l->match(')'); + // setup a return variable + CScriptVarLink *returnVar = NULL; + // execute function! + // add the function's execute space to the symbol table so we can recurse + CScriptVarLink *returnVarLink = functionRoot->addChild(TINYJS_RETURN_VAR); + scopes.push_back(functionRoot); + + if (a->var->isNative()) { + ASSERT(a->var->jsCallback); + a->var->jsCallback(functionRoot, a->var->jsCallbackUserData); + } else { + /* we just want to execute the block, but something could + * have messed up and left us with the wrong ScriptLex, so + * we want to be careful here... */ + CScriptException *exception = 0; + CScriptLex *oldLex = l; + CScriptLex *newLex = new CScriptLex(a->var->getString()); + l = newLex; + try { + block(execute); + // because return will probably have called this, and set execute to false + execute = true; + } catch (CScriptException *e) { + exception = e; + } + delete newLex; + l = oldLex; + + if (exception) + throw exception; + } + + scopes.pop_back(); + /* get the real return var before we remove it from our function */ + returnVar = new CScriptVarLink(returnVarLink->var); + functionRoot->removeLink(returnVarLink); + delete functionRoot; + if (returnVar) + a = returnVar; + else + a = new CScriptVarLink(new CScriptVar()); + } else { + // function, but not executing - just parse args and be done + l->match('('); + while (l->tk != ')') { + CScriptVarLink *value = base(execute); + CLEAN(value); + if (l->tk!=')') l->match(','); + } + l->match(')'); + if (l->tk == '{') { + block(execute); + } + } + } else if (l->tk == '.') { // ------------------------------------- Record Access + l->match('.'); + if (execute) { + const string &name = l->tkStr; + CScriptVarLink *child = a->var->findChild(name); + if (!child) child = findInParentClasses(a->var, name); + if (!child) { + /* if we haven't found this defined yet, use the built-in + 'length' properly */ + if (a->var->isArray() && name == "length") { + int l = a->var->getArrayLength(); + child = new CScriptVarLink(new CScriptVar(l)); + } else if (a->var->isString() && name == "length") { + int l = a->var->getString().size(); + child = new CScriptVarLink(new CScriptVar(l)); + } else { + child = a->var->addChild(name); + } + } + parent = a->var; + a = child; + } + l->match(LEX_ID); + } else if (l->tk == '[') { // ------------------------------------- Array Access + l->match('['); + CScriptVarLink *index = expression(execute); + l->match(']'); + if (execute) { + CScriptVarLink *child = a->var->findChildOrCreate(index->var->getString()); + parent = a->var; + a = child; + } + CLEAN(index); + } else ASSERT(0); + } + return a; + } + if (l->tk==LEX_INT || l->tk==LEX_FLOAT) { + CScriptVar *a = new CScriptVar(l->tkStr, + ((l->tk==LEX_INT)?SCRIPTVAR_INTEGER:SCRIPTVAR_DOUBLE)); + l->match(l->tk); + return new CScriptVarLink(a); + } + if (l->tk==LEX_STR) { + CScriptVar *a = new CScriptVar(l->tkStr, SCRIPTVAR_STRING); + l->match(LEX_STR); + return new CScriptVarLink(a); + } + if (l->tk=='{') { + CScriptVar *contents = new CScriptVar(TINYJS_BLANK_DATA, SCRIPTVAR_OBJECT); + /* JSON-style object definition */ + l->match('{'); + while (l->tk != '}') { + string id = l->tkStr; + // 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(':'); + if (execute) { + CScriptVarLink *a = base(execute); + contents->addChild(id, a->var); + CLEAN(a); + } + // no need to clean here, as it will definitely be used + if (l->tk != '}') l->match(','); + } + + l->match('}'); + return new CScriptVarLink(contents); + } + if (l->tk=='[') { + CScriptVar *contents = new CScriptVar(TINYJS_BLANK_DATA, SCRIPTVAR_ARRAY); + /* JSON-style array */ + l->match('['); + int idx = 0; + while (l->tk != ']') { + if (execute) { + char idx_str[16]; // big enough for 2^32 + sprintf_s(idx_str, sizeof(idx_str), "%d",idx); + + CScriptVarLink *a = base(execute); + contents->addChild(idx_str, a->var); + CLEAN(a); + } + // no need to clean here, as it will definitely be used + if (l->tk != ']') l->match(','); + idx++; + } + l->match(']'); + return new CScriptVarLink(contents); + } + if (l->tk==LEX_R_FUNCTION) { + CScriptVarLink *funcVar = parseFunctionDefinition(); + if (funcVar->name != TINYJS_TEMP_NAME) + TRACE("Functions not defined at statement-level are not meant to have a name"); + return funcVar; + } + if (l->tk==LEX_R_NEW) { + // new -> create a new object + l->match(LEX_R_NEW); + const string &className = l->tkStr; + if (execute) { + CScriptVarLink *objClass = findInScopes(className); + if (!objClass) { + TRACE("%s is not a valid class name", className.c_str()); + return new CScriptVarLink(new CScriptVar()); + } + l->match(LEX_ID); + CScriptVar *obj = new CScriptVar(TINYJS_BLANK_DATA, SCRIPTVAR_OBJECT); + obj->addChild(TINYJS_PROTOTYPE_CLASS, objClass->var); + if (l->tk == '(') { + l->match('('); + l->match(')'); + } + // TODO: Object constructors + return new CScriptVarLink(obj); + } else { + l->match(LEX_ID); + if (l->tk == '(') { + l->match('('); + l->match(')'); + } + } + } + // Nothing we can do here... just hope it's the end... + l->match(LEX_EOF); + return 0; +} + +CScriptVarLink *CTinyJS::unary(bool &execute) { + CScriptVarLink *a; + if (l->tk=='!') { + l->match('!'); // binary not + a = factor(execute); + if (execute) { + CScriptVar zero(0); + CScriptVar *res = a->var->mathsOp(&zero, LEX_EQUAL); + CREATE_LINK(a, res); + } + } else + a = factor(execute); + return a; +} + +CScriptVarLink *CTinyJS::term(bool &execute) { + CScriptVarLink *a = unary(execute); + while (l->tk=='*' || l->tk=='/' || l->tk=='%') { + int op = l->tk; + l->match(l->tk); + CScriptVarLink *b = unary(execute); + if (execute) { + CScriptVar *res = a->var->mathsOp(b->var, op); + CREATE_LINK(a, res); + } + CLEAN(b); + } + return a; +} + +CScriptVarLink *CTinyJS::expression(bool &execute) { + bool negate = false; + if (l->tk=='-') { + l->match('-'); + negate = true; + } + CScriptVarLink *a = term(execute); + if (negate) { + CScriptVar zero(0); + CScriptVar *res = zero.mathsOp(a->var, '-'); + CREATE_LINK(a, res); + } + + while (l->tk=='+' || l->tk=='-' || + l->tk==LEX_PLUSPLUS || l->tk==LEX_MINUSMINUS) { + int op = l->tk; + l->match(l->tk); + if (op==LEX_PLUSPLUS || op==LEX_MINUSMINUS) { + if (execute) { + CScriptVar one(1); + CScriptVar *res = a->var->mathsOp(&one, op==LEX_PLUSPLUS ? '+' : '-'); + // in-place add/subtract + a->replaceWith(res); + } + } else { + CScriptVarLink *b = term(execute); + if (execute) { + // not in-place, so just replace + CScriptVar *res = a->var->mathsOp(b->var, op); + CREATE_LINK(a, res); + } + CLEAN(b); + } + } + return a; +} + +CScriptVarLink *CTinyJS::condition(bool &execute) { + CScriptVarLink *a = expression(execute); + CScriptVarLink *b; + while (l->tk==LEX_EQUAL || l->tk==LEX_NEQUAL || + l->tk==LEX_TYPEEQUAL || l->tk==LEX_NTYPEEQUAL || + l->tk==LEX_LEQUAL || l->tk==LEX_GEQUAL || + l->tk=='<' || l->tk=='>') { + int op = l->tk; + l->match(l->tk); + b = expression(execute); + if (execute) { + CScriptVar *res = a->var->mathsOp(b->var, op); + CREATE_LINK(a,res); + } + CLEAN(b); + } + return a; +} + +CScriptVarLink *CTinyJS::logic(bool &execute) { + CScriptVarLink *a = condition(execute); + CScriptVarLink *b; + while (l->tk=='&' || l->tk=='|' || l->tk=='^' || l->tk==LEX_ANDAND || l->tk==LEX_OROR) { + bool noexecute = false; + int op = l->tk; + l->match(l->tk); + bool shortCircuit = false; + bool boolean = false; + // if we have short-circuit ops, then if we know the outcome + // we don't bother to execute the other op. Even if not + // we need to tell mathsOp it's an & or | + if (op==LEX_ANDAND) { + op = '&'; + shortCircuit = !a->var->getBool(); + boolean = true; + } else if (op==LEX_OROR) { + op = '|'; + shortCircuit = a->var->getBool(); + boolean = true; + } + b = condition(shortCircuit ? noexecute : execute); + if (execute && !shortCircuit) { + if (boolean) { + CScriptVar *newa = new CScriptVar(a->var->getBool()); + CScriptVar *newb = new CScriptVar(b->var->getBool()); + CREATE_LINK(a, newa); + CREATE_LINK(b, newb); + } + CScriptVar *res = a->var->mathsOp(b->var, op); + CREATE_LINK(a, res); + } + CLEAN(b); + } + return a; +} + +CScriptVarLink *CTinyJS::base(bool &execute) { + CScriptVarLink *lhs = logic(execute); + if (l->tk=='=' || l->tk==LEX_PLUSEQUAL || l->tk==LEX_MINUSEQUAL) { + /* If we're assigning to this and we don't have a parent, + * add it to the symbol table root as per JavaScript. */ + if (execute && !lhs->owned) { + if (lhs->name.length()>0) { + CScriptVarLink *realLhs = root->addChildNoDup(lhs->name, lhs->var); + CLEAN(lhs); + lhs = realLhs; + } else + TRACE("Trying to assign to an un-named type\n"); + } + + int op = l->tk; + l->match(l->tk); + CScriptVarLink *rhs = base(execute); + if (execute) { + if (op=='=') { + lhs->replaceWith(rhs); + } else if (op==LEX_PLUSEQUAL) { + CScriptVar *res = lhs->var->mathsOp(rhs->var, '+'); + lhs->replaceWith(res); + } else if (op==LEX_MINUSEQUAL) { + CScriptVar *res = lhs->var->mathsOp(rhs->var, '-'); + lhs->replaceWith(res); + } else ASSERT(0); + } + CLEAN(rhs); + } + return lhs; +} + +void CTinyJS::block(bool &execute) { + l->match('{'); + if (execute) { + while (l->tk && l->tk!='}') + statement(execute); + l->match('}'); + } else { + // fast skip of blocks + 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==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. 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); + CScriptVarLink *a = 0; + if (execute) + a = scopes.back()->findChildOrCreate(l->tkStr); + l->match(LEX_ID); + // now do stuff defined with dots + while (l->tk == '.') { + l->match('.'); + if (execute) { + CScriptVarLink *lastA = a; + a = lastA->var->findChildOrCreate(l->tkStr); + } + l->match(LEX_ID); + } + // sort out initialiser + if (l->tk == '=') { + l->match('='); + CScriptVarLink *var = base(execute); + if (execute) + a->replaceWith(var); + CLEAN(var); + } + l->match(';'); + } else if (l->tk==LEX_R_IF) { + l->match(LEX_R_IF); + l->match('('); + CScriptVarLink *var = base(execute); + l->match(')'); + bool cond = execute && var->var->getBool(); + CLEAN(var); + bool noexecute = false; // because we need to be abl;e to write to it + statement(cond ? execute : noexecute); + if (l->tk==LEX_R_ELSE) { + l->match(LEX_R_ELSE); + statement(cond ? noexecute : execute); + } + } else if (l->tk==LEX_R_WHILE) { + // We do repetition by pulling out the string representing our statement + // there's definitely some opportunity for optimisation here + l->match(LEX_R_WHILE); + l->match('('); + int whileCondStart = l->tokenStart; + bool noexecute = false; + CScriptVarLink *cond = base(execute); + bool loopCond = execute && cond->var->getBool(); + CLEAN(cond); + CScriptLex *whileCond = l->getSubLex(whileCondStart); + l->match(')'); + int whileBodyStart = l->tokenStart; + statement(loopCond ? execute : noexecute); + CScriptLex *whileBody = l->getSubLex(whileBodyStart); + CScriptLex *oldLex = l; + int loopCount = TINYJS_LOOP_MAX_ITERATIONS; + while (loopCond && loopCount-->0) { + whileCond->reset(); + l = whileCond; + cond = base(execute); + loopCond = execute && cond->var->getBool(); + CLEAN(cond); + if (loopCond) { + whileBody->reset(); + l = whileBody; + statement(execute); + } + } + l = oldLex; + delete whileCond; + delete whileBody; + + if (loopCount<=0) { + root->trace(); + TRACE("WHILE Loop exceeded %d iterations at %s\n", TINYJS_LOOP_MAX_ITERATIONS, l->getPosition().c_str()); + throw new CScriptException("LOOP_ERROR"); + } + } else if (l->tk==LEX_R_FOR) { + l->match(LEX_R_FOR); + l->match('('); + statement(execute); // initialisation + //l->match(';'); + int forCondStart = l->tokenStart; + bool noexecute = false; + CScriptVarLink *cond = base(execute); // condition + bool loopCond = execute && cond->var->getBool(); + CLEAN(cond); + CScriptLex *forCond = l->getSubLex(forCondStart); + l->match(';'); + int forIterStart = l->tokenStart; + CLEAN(base(noexecute)); // iterator + CScriptLex *forIter = l->getSubLex(forIterStart); + l->match(')'); + int forBodyStart = l->tokenStart; + statement(loopCond ? execute : noexecute); + CScriptLex *forBody = l->getSubLex(forBodyStart); + CScriptLex *oldLex = l; + if (loopCond) { + forIter->reset(); + l = forIter; + CLEAN(base(execute)); + } + int loopCount = TINYJS_LOOP_MAX_ITERATIONS; + while (execute && loopCond && loopCount-->0) { + forCond->reset(); + l = forCond; + cond = base(execute); + loopCond = cond->var->getBool(); + CLEAN(cond); + if (execute && loopCond) { + forBody->reset(); + l = forBody; + statement(execute); + } + if (execute && loopCond) { + forIter->reset(); + l = forIter; + CLEAN(base(execute)); + } + } + l = oldLex; + delete forCond; + delete forIter; + delete forBody; + if (loopCount<=0) { + root->trace(); + TRACE("FOR Loop exceeded %d iterations at %s\n", TINYJS_LOOP_MAX_ITERATIONS, l->getPosition().c_str()); + throw new CScriptException("LOOP_ERROR"); + } + } else if (l->tk==LEX_R_RETURN) { + l->match(LEX_R_RETURN); + CScriptVarLink *result = 0; + if (l->tk != ';') + result = base(execute); + if (execute) { + CScriptVarLink *resultVar = scopes.back()->findChild(TINYJS_RETURN_VAR); + if (resultVar) + resultVar->replaceWith(result); + else + TRACE("RETURN statement, but not in a function.\n"); + execute = false; + } + CLEAN(result); + l->match(';'); + } else if (l->tk==LEX_R_FUNCTION) { + CScriptVarLink *funcVar = parseFunctionDefinition(); + if (execute) { + if (funcVar->name == TINYJS_TEMP_NAME) + TRACE("Functions defined at statement-level are meant to have a name\n"); + else + scopes.back()->addChildNoDup(funcVar->name, funcVar->var); + } + CLEAN(funcVar); + } else l->match(LEX_EOF); +} + +/// Get the value of the given variable, or return 0 +const string *CTinyJS::getVariable(const string &path) { + // traverse path + size_t prevIdx = 0; + size_t thisIdx = path.find('.'); + if (thisIdx == string::npos) thisIdx = path.length(); + CScriptVar *var = root; + while (var && prevIdxfindChild(el); + var = varl?varl->var:0; + prevIdx = thisIdx+1; + thisIdx = path.find('.', prevIdx); + if (thisIdx == string::npos) thisIdx = path.length(); + } + // return result + if (var) + return &var->getString(); + else + return 0; +} + +/// Finds a child, looking recursively up the scopes +CScriptVarLink *CTinyJS::findInScopes(const std::string &childName) { + for (int s=scopes.size()-1;s>=0;s--) { + CScriptVarLink *v = scopes[s]->findChild(childName); + if (v) return v; + } + return NULL; + +} + +/// Look up in any parent classes of the given object +CScriptVarLink *CTinyJS::findInParentClasses(CScriptVar *object, const std::string &name) { + // Look for links to actual parent classes + CScriptVarLink *parentClass = object->findChild(TINYJS_PROTOTYPE_CLASS); + while (parentClass) { + CScriptVarLink *implementation = parentClass->var->findChild(name); + if (implementation) return implementation; + parentClass = parentClass->var->findChild(TINYJS_PROTOTYPE_CLASS); + } + // else fake it for strings and finally objects + if (object->isString()) { + CScriptVarLink *implementation = stringClass->findChild(name); + if (implementation) return implementation; + } + if (object->isArray()) { + CScriptVarLink *implementation = arrayClass->findChild(name); + if (implementation) return implementation; + } + CScriptVarLink *implementation = objectClass->findChild(name); + if (implementation) return implementation; + + return 0; +} diff --git a/TinyJS.h b/TinyJS.h index e84ab21..c21ec80 100755 --- a/TinyJS.h +++ b/TinyJS.h @@ -1,312 +1,332 @@ -/* - * 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. - */ - -#ifndef TINYJS_H -#define TINYJS_H - -#include -#include - -#ifndef TRACE -#define TRACE printf -#endif // TRACE - -const int TINYJS_LOOP_MAX_ITERATIONS = 8192; - -enum LEX_TYPES { - LEX_EOF = 0, - LEX_ID = 256, - LEX_INT, - LEX_FLOAT, - LEX_STR, - - LEX_EQUAL, - LEX_TYPEEQUAL, - LEX_NEQUAL, - LEX_NTYPEEQUAL, - LEX_LEQUAL, - LEX_GEQUAL, - LEX_PLUSEQUAL, - LEX_MINUSEQUAL, - LEX_PLUSPLUS, - LEX_MINUSMINUS, - LEX_ANDAND, - LEX_OROR, - // reserved words - LEX_R_IF, - LEX_R_ELSE, - LEX_R_WHILE, - LEX_R_FOR, - LEX_R_FUNCTION, - LEX_R_RETURN, - LEX_R_VAR, - LEX_R_TRUE, - LEX_R_FALSE, - LEX_R_NULL, - LEX_R_UNDEFINED, - LEX_R_NEW, -}; - -enum SCRIPTVAR_FLAGS { - SCRIPTVAR_UNDEFINED = 0, - SCRIPTVAR_FUNCTION = 1, - SCRIPTVAR_OBJECT = 2, - SCRIPTVAR_ARRAY = 4, - SCRIPTVAR_DOUBLE = 8, // floating point double - SCRIPTVAR_INTEGER = 16, // integer number - SCRIPTVAR_STRING = 32, // string - SCRIPTVAR_NULL = 64, // it seems null is its own data type - - SCRIPTVAR_NATIVE = 128, // to specify this is a native function - SCRIPTVAR_NUMERICMASK = SCRIPTVAR_NULL | - SCRIPTVAR_DOUBLE | - SCRIPTVAR_INTEGER, - SCRIPTVAR_VARTYPEMASK = SCRIPTVAR_DOUBLE | - SCRIPTVAR_INTEGER | - SCRIPTVAR_STRING | - SCRIPTVAR_FUNCTION | - SCRIPTVAR_OBJECT | - SCRIPTVAR_ARRAY | - SCRIPTVAR_NULL, - -}; - -#define TINYJS_RETURN_VAR "return" -#define TINYJS_PROTOTYPE_CLASS "prototype" -#define TINYJS_TEMP_NAME "" -#define TINYJS_BLANK_DATA "" - -/// convert the given string into a quoted string suitable for javascript -std::string getJSString(const std::string &str); - -class CScriptException { -public: - std::string text; - CScriptException(const std::string &exceptionText); -}; - -class CScriptLex -{ -public: - CScriptLex(const std::string &input); - CScriptLex(CScriptLex *owner, int startChar, int endChar); - ~CScriptLex(void); - - char currCh, nextCh; - int tk; ///< The type of the token that we have - 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 - std::string getTokenStr(int token); ///< Get the string representation of the given token - void reset(); ///< Reset this lex so we can start again - - std::string getSubString(int pos); ///< Return a sub-string from the given position up until right now - CScriptLex *getSubLex(int lastPosition); ///< Return a sub-lexer from the given position up until right now - - std::string getPosition(int pos); ///< Return a string representing the position in lines and columns of the character pos given - -protected: - /* When we go into a loop, we use getSubLex to get a lexer for just the sub-part of the - relevant string. This doesn't re-allocate and copy the string, but instead copies - the data pointer and sets dataOwned to false, and dataStart/dataEnd to the relevant things. */ - char *data; ///< Data string to get tokens from - int dataStart, dataEnd; ///< Start and end position in data string - bool dataOwned; ///< Do we own this data string? - - 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 -}; - -class CScriptVar; - -typedef void (*JSCallback)(CScriptVar *var, void *userdata); - -class CScriptVarLink -{ -public: - std::string name; - CScriptVarLink *nextSibling; - CScriptVarLink *prevSibling; - CScriptVar *var; - bool owned; - - CScriptVarLink(CScriptVar *var, const std::string &name = TINYJS_TEMP_NAME); - CScriptVarLink(const CScriptVarLink &link); ///< Copy constructor - ~CScriptVarLink(); - void replaceWith(CScriptVar *newVar); ///< Replace the Variable pointed to - void replaceWith(CScriptVarLink *newVar); ///< Replace the Variable pointed to (just dereferences) -}; - -/// Variable class (containing a doubly-linked list of children) -class CScriptVar -{ -public: - CScriptVar(); ///< Create undefined - CScriptVar(const std::string &varData, int varFlags); ///< User defined - CScriptVar(const std::string &str); ///< Create a string - CScriptVar(double varData); - CScriptVar(int val); - ~CScriptVar(void); - - CScriptVar *getReturnVar(); ///< If this is a function, get the result value (for use by native functions) - void setReturnVar(CScriptVar *var); ///< Set the result value. Use this when setting complex return data as it avoids a deepCopy() - CScriptVar *getParameter(const std::string &name); ///< If this is a function, get the parameter with the given name (for use by native functions) - - CScriptVarLink *findChild(const std::string &childName); ///< Tries to find a child with the given name, may return 0 - CScriptVarLink *findChildOrCreate(const std::string &childName, int varFlags=SCRIPTVAR_UNDEFINED); ///< Tries to find a child with the given name, or will create it with the given flags - CScriptVarLink *findChildOrCreateByPath(const std::string &path); ///< Tries to find a child with the given path (separated by dots) - CScriptVarLink *addChild(const std::string &childName, CScriptVar *child=NULL); - CScriptVarLink *addChildNoDup(const std::string &childName, CScriptVar *child=NULL); ///< add a child overwriting any with the same name - void removeChild(CScriptVar *child); - void removeLink(CScriptVarLink *link); ///< Remove a specific link (this is faster than finding via a child) - void removeAllChildren(); - CScriptVar *getArrayIndex(int idx); ///< The the value at an array index - void setArrayIndex(int idx, CScriptVar *value); ///< Set the value at an array index - int getArrayLength(); ///< If this is an array, return the number of items in it (else 0) - int getChildren(); ///< Get the number of children - - int getInt(); - bool getBool() { return getInt() != 0; } - double getDouble(); - const std::string &getString(); - std::string getParsableString(); ///< get Data as a parsable javascript string - void setInt(int num); - void setDouble(double val); - void setString(const std::string &str); - void setUndefined(); - - 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 isObject() { return (flags&SCRIPTVAR_OBJECT)!=0; } - bool isArray() { return (flags&SCRIPTVAR_ARRAY)!=0; } - bool isNative() { return (flags&SCRIPTVAR_NATIVE)!=0; } - bool isUndefined() { return (flags & SCRIPTVAR_VARTYPEMASK) == SCRIPTVAR_UNDEFINED; } - bool isNull() { return (flags & SCRIPTVAR_NULL)!=0; } - bool isBasic() { return firstChild==0; } ///< Is this *not* an array/object/etc - - CScriptVar *mathsOp(CScriptVar *b, int op); ///< do a maths op with another script variable - void copyValue(CScriptVar *val); ///< copy the value from the value given - CScriptVar *deepCopy(); ///< deep copy this node and return the result - - - void trace(std::string indentStr = "", const std::string &name = ""); ///< Dump out the contents of this using trace - std::string getFlagsAsString(); - void getJSON(std::ostringstream &destination, const std::string linePrefix=""); ///< Write out all the JS code needed to recreate this script variable to the stream (as JSON) - void setCallback(JSCallback callback, void *userdata); - - CScriptVarLink *firstChild; - CScriptVarLink *lastChild; - - /// For memory management/garbage collection - CScriptVar *ref(); ///< Add reference to this variable - void unref(); ///< Remove a reference, and delete this variable if required - int getRefs(); -protected: - int refs; - - std::string data; - int flags; - JSCallback jsCallback; - void *jsCallbackUserData; - - void init(); // initilisation of data members - - friend class CTinyJS; -}; - -class CTinyJS { -public: - CTinyJS(); - ~CTinyJS(); - - void execute(const std::string &code); - /** Evaluate the given code and return a link to a javascript object, - * useful for (dangerous) JSON parsing. If nothing to return, will return - * 'undefined' variable type. CScriptVarLink is returned as this will - * automatically unref the result as it goes out of scope. If you want to - * keep it, you must use ref() and unref() */ - CScriptVarLink evaluateComplex(const std::string &code); - /** Evaluate the given code and return a string. If nothing to return, will return - * 'undefined' */ - std::string evaluate(const std::string &code); - - /// add a native function to be called from TinyJS - /** example: - \code - void scRandInt(CScriptVar *c, void *userdata) { ... } - tinyJS->addNative("function randInt(min, max)", scRandInt, 0); - \endcode - - or - - \code - void scSubstring(CScriptVar *c, void *userdata) { ... } - tinyJS->addNative("function String.substring(lo, hi)", scSubstring, 0); - \endcode - */ - void addNative(const std::string &funcDesc, JSCallback ptr, void *userdata); - - /// Get the value of the given variable, or return 0 - const std::string *getVariable(const std::string &path); - - /// Send all variables to stdout - void trace(); - - CScriptVar *root; /// root of symbol table -private: - CScriptLex *l; /// current lexer - std::vector scopes; /// stack of scopes when parsing - CScriptVar *stringClass; /// Built in string class - CScriptVar *objectClass; /// Built in object class - CScriptVar *arrayClass; /// Built in array class - - // parsing - in order of precedence - CScriptVarLink *factor(bool &execute); - CScriptVarLink *unary(bool &execute); - CScriptVarLink *term(bool &execute); - CScriptVarLink *expression(bool &execute); - CScriptVarLink *condition(bool &execute); - CScriptVarLink *logic(bool &execute); - CScriptVarLink *base(bool &execute); - void block(bool &execute); - void statement(bool &execute); - // parsing utility functions - CScriptVarLink *parseFunctionDefinition(); - void parseFunctionArguments(CScriptVar *funcVar); - - CScriptVarLink *findInScopes(const std::string &childName); ///< Finds a child, looking recursively up the scopes - /// Look up in any parent classes of the given object - CScriptVarLink *findInParentClasses(CScriptVar *object, const std::string &name); -}; - +/* + * 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. + */ + +#ifndef TINYJS_H +#define TINYJS_H + +#ifdef _WIN32 +#ifdef _DEBUG +#define _CRTDBG_MAP_ALLOC +#include +#include #endif +#endif +#include +#include + +#ifndef TRACE +#define TRACE printf +#endif // TRACE + +const int TINYJS_LOOP_MAX_ITERATIONS = 8192; + +enum LEX_TYPES { + LEX_EOF = 0, + LEX_ID = 256, + LEX_INT, + LEX_FLOAT, + LEX_STR, + + LEX_EQUAL, + LEX_TYPEEQUAL, + LEX_NEQUAL, + LEX_NTYPEEQUAL, + LEX_LEQUAL, + LEX_LSHIFT, + LEX_LSHIFTEQUAL, + LEX_GEQUAL, + LEX_RSHIFT, + LEX_RSHIFTEQUAL, + LEX_PLUSEQUAL, + LEX_MINUSEQUAL, + LEX_PLUSPLUS, + LEX_MINUSMINUS, + LEX_ANDEQUAL, + LEX_ANDAND, + LEX_OREQUAL, + LEX_OROR, + LEX_XOREQUAL, + // reserved words +#define LEX_R_LIST_START LEX_R_IF + LEX_R_IF, + LEX_R_ELSE, + LEX_R_DO, + LEX_R_WHILE, + LEX_R_FOR, + LEX_R_BREAK, + LEX_R_CONTINUE, + LEX_R_FUNCTION, + LEX_R_RETURN, + LEX_R_VAR, + LEX_R_TRUE, + LEX_R_FALSE, + LEX_R_NULL, + LEX_R_UNDEFINED, + LEX_R_NEW, + + LEX_R_LIST_END /* always the last entry */ +}; + +enum SCRIPTVAR_FLAGS { + SCRIPTVAR_UNDEFINED = 0, + SCRIPTVAR_FUNCTION = 1, + SCRIPTVAR_OBJECT = 2, + SCRIPTVAR_ARRAY = 4, + SCRIPTVAR_DOUBLE = 8, // floating point double + SCRIPTVAR_INTEGER = 16, // integer number + SCRIPTVAR_STRING = 32, // string + SCRIPTVAR_NULL = 64, // it seems null is its own data type + + SCRIPTVAR_NATIVE = 128, // to specify this is a native function + SCRIPTVAR_NUMERICMASK = SCRIPTVAR_NULL | + SCRIPTVAR_DOUBLE | + SCRIPTVAR_INTEGER, + SCRIPTVAR_VARTYPEMASK = SCRIPTVAR_DOUBLE | + SCRIPTVAR_INTEGER | + SCRIPTVAR_STRING | + SCRIPTVAR_FUNCTION | + SCRIPTVAR_OBJECT | + SCRIPTVAR_ARRAY | + SCRIPTVAR_NULL, + +}; + +#define TINYJS_RETURN_VAR "return" +#define TINYJS_PROTOTYPE_CLASS "prototype" +#define TINYJS_TEMP_NAME "" +#define TINYJS_BLANK_DATA "" + +/// convert the given string into a quoted string suitable for javascript +std::string getJSString(const std::string &str); + +class CScriptException { +public: + std::string text; + CScriptException(const std::string &exceptionText); +}; + +class CScriptLex +{ +public: + CScriptLex(const std::string &input); + CScriptLex(CScriptLex *owner, int startChar, int endChar); + ~CScriptLex(void); + + char currCh, nextCh; + int tk; ///< The type of the token that we have + 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 + static std::string getTokenStr(int token); ///< Get the string representation of the given token + void reset(); ///< Reset this lex so we can start again + + std::string getSubString(int pos); ///< Return a sub-string from the given position up until right now + CScriptLex *getSubLex(int lastPosition); ///< Return a sub-lexer from the given position up until right now + + std::string getPosition(int pos=-1); ///< Return a string representing the position in lines and columns of the character pos given + +protected: + /* When we go into a loop, we use getSubLex to get a lexer for just the sub-part of the + relevant string. This doesn't re-allocate and copy the string, but instead copies + the data pointer and sets dataOwned to false, and dataStart/dataEnd to the relevant things. */ + char *data; ///< Data string to get tokens from + int dataStart, dataEnd; ///< Start and end position in data string + bool dataOwned; ///< Do we own this data string? + + 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 +}; + +class CScriptVar; + +typedef void (*JSCallback)(CScriptVar *var, void *userdata); + +class CScriptVarLink +{ +public: + std::string name; + CScriptVarLink *nextSibling; + CScriptVarLink *prevSibling; + CScriptVar *var; + bool owned; + + CScriptVarLink(CScriptVar *var, const std::string &name = TINYJS_TEMP_NAME); + CScriptVarLink(const CScriptVarLink &link); ///< Copy constructor + ~CScriptVarLink(); + void replaceWith(CScriptVar *newVar); ///< Replace the Variable pointed to + void replaceWith(CScriptVarLink *newVar); ///< Replace the Variable pointed to (just dereferences) +}; + +/// Variable class (containing a doubly-linked list of children) +class CScriptVar +{ +public: + CScriptVar(); ///< Create undefined + CScriptVar(const std::string &varData, int varFlags); ///< User defined + CScriptVar(const std::string &str); ///< Create a string + CScriptVar(double varData); + CScriptVar(int val); + ~CScriptVar(void); + + CScriptVar *getReturnVar(); ///< If this is a function, get the result value (for use by native functions) + void setReturnVar(CScriptVar *var); ///< Set the result value. Use this when setting complex return data as it avoids a deepCopy() + CScriptVar *getParameter(const std::string &name); ///< If this is a function, get the parameter with the given name (for use by native functions) + + CScriptVarLink *findChild(const std::string &childName); ///< Tries to find a child with the given name, may return 0 + CScriptVarLink *findChildOrCreate(const std::string &childName, int varFlags=SCRIPTVAR_UNDEFINED); ///< Tries to find a child with the given name, or will create it with the given flags + CScriptVarLink *findChildOrCreateByPath(const std::string &path); ///< Tries to find a child with the given path (separated by dots) + CScriptVarLink *addChild(const std::string &childName, CScriptVar *child=NULL); + CScriptVarLink *addChildNoDup(const std::string &childName, CScriptVar *child=NULL); ///< add a child overwriting any with the same name + void removeChild(CScriptVar *child); + void removeLink(CScriptVarLink *link); ///< Remove a specific link (this is faster than finding via a child) + void removeAllChildren(); + CScriptVar *getArrayIndex(int idx); ///< The the value at an array index + void setArrayIndex(int idx, CScriptVar *value); ///< Set the value at an array index + int getArrayLength(); ///< If this is an array, return the number of items in it (else 0) + int getChildren(); ///< Get the number of children + + int getInt(); + bool getBool() { return getInt() != 0; } + double getDouble(); + const std::string &getString(); + std::string getParsableString(); ///< get Data as a parsable javascript string + void setInt(int num); + void setDouble(double val); + void setString(const std::string &str); + void setUndefined(); + + 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 isObject() { return (flags&SCRIPTVAR_OBJECT)!=0; } + bool isArray() { return (flags&SCRIPTVAR_ARRAY)!=0; } + bool isNative() { return (flags&SCRIPTVAR_NATIVE)!=0; } + bool isUndefined() { return (flags & SCRIPTVAR_VARTYPEMASK) == SCRIPTVAR_UNDEFINED; } + bool isNull() { return (flags & SCRIPTVAR_NULL)!=0; } + bool isBasic() { return firstChild==0; } ///< Is this *not* an array/object/etc + + CScriptVar *mathsOp(CScriptVar *b, int op); ///< do a maths op with another script variable + void copyValue(CScriptVar *val); ///< copy the value from the value given + CScriptVar *deepCopy(); ///< deep copy this node and return the result + + + void trace(std::string indentStr = "", const std::string &name = ""); ///< Dump out the contents of this using trace + std::string getFlagsAsString(); + void getJSON(std::ostringstream &destination, const std::string linePrefix=""); ///< Write out all the JS code needed to recreate this script variable to the stream (as JSON) + void setCallback(JSCallback callback, void *userdata); + + CScriptVarLink *firstChild; + CScriptVarLink *lastChild; + + /// For memory management/garbage collection + CScriptVar *ref(); ///< Add reference to this variable + void unref(); ///< Remove a reference, and delete this variable if required + int getRefs(); +protected: + int refs; + + std::string data; + int flags; + JSCallback jsCallback; + void *jsCallbackUserData; + + void init(); // initilisation of data members + + friend class CTinyJS; +}; + +class CTinyJS { +public: + CTinyJS(); + ~CTinyJS(); + + void execute(const std::string &code); + /** Evaluate the given code and return a link to a javascript object, + * useful for (dangerous) JSON parsing. If nothing to return, will return + * 'undefined' variable type. CScriptVarLink is returned as this will + * automatically unref the result as it goes out of scope. If you want to + * keep it, you must use ref() and unref() */ + CScriptVarLink evaluateComplex(const std::string &code); + /** Evaluate the given code and return a string. If nothing to return, will return + * 'undefined' */ + std::string evaluate(const std::string &code); + + /// add a native function to be called from TinyJS + /** example: + \code + void scRandInt(CScriptVar *c, void *userdata) { ... } + tinyJS->addNative("function randInt(min, max)", scRandInt, 0); + \endcode + + or + + \code + void scSubstring(CScriptVar *c, void *userdata) { ... } + tinyJS->addNative("function String.substring(lo, hi)", scSubstring, 0); + \endcode + */ + void addNative(const std::string &funcDesc, JSCallback ptr, void *userdata); + + /// Get the value of the given variable, or return 0 + const std::string *getVariable(const std::string &path); + + /// Send all variables to stdout + void trace(); + + CScriptVar *root; /// root of symbol table +private: + CScriptLex *l; /// current lexer + std::vector scopes; /// stack of scopes when parsing + CScriptVar *stringClass; /// Built in string class + CScriptVar *objectClass; /// Built in object class + CScriptVar *arrayClass; /// Built in array class + + // parsing - in order of precedence + CScriptVarLink *factor(bool &execute); + CScriptVarLink *unary(bool &execute); + CScriptVarLink *term(bool &execute); + CScriptVarLink *expression(bool &execute); + CScriptVarLink *condition(bool &execute); + CScriptVarLink *logic(bool &execute); + CScriptVarLink *base(bool &execute); + void block(bool &execute); + void statement(bool &execute); + // parsing utility functions + CScriptVarLink *parseFunctionDefinition(); + void parseFunctionArguments(CScriptVar *funcVar); + + CScriptVarLink *findInScopes(const std::string &childName); ///< Finds a child, looking recursively up the scopes + /// Look up in any parent classes of the given object + CScriptVarLink *findInParentClasses(CScriptVar *object, const std::string &name); +}; + +#endif diff --git a/run_tests.cpp b/run_tests.cpp index d2912f6..e79af69 100644 --- a/run_tests.cpp +++ b/run_tests.cpp @@ -27,13 +27,14 @@ * This is a program to run all the tests in the tests folder... */ +#include "TinyJS.h" +#include "TinyJS_Functions.h" #include #include #include #include #include -#include "TinyJS.h" -#include "TinyJS_Functions.h" + //#define INSANE_MEMORY_DEBUG @@ -267,6 +268,11 @@ int main(int argc, char **argv) printf("Done. %d tests, %d pass, %d fail\n", count, passed, count-passed); #ifdef INSANE_MEMORY_DEBUG memtracing_kill(); +#endif +#ifdef _WIN32 +#ifdef _DEBUG + _CrtDumpMemoryLeaks(); +#endif #endif return 0; }