527 lines
17 KiB
C++
527 lines
17 KiB
C++
/*
|
|
* TinyJS
|
|
*
|
|
* A single-file Javascript-alike engine
|
|
*
|
|
* Authored By Gordon Williams <gw@pur3.co.uk>
|
|
*
|
|
* Copyright (C) 2009 Pur3 Ltd
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
* this software and associated documentation files (the "Software"), to deal in
|
|
* the Software without restriction, including without limitation the rights to
|
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
* of the Software, and to permit persons to whom the Software is furnished to do
|
|
* so, subject to the following conditions:
|
|
|
|
* The above copyright notice and this permission notice shall be included in all
|
|
* copies or substantial portions of the Software.
|
|
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
* SOFTWARE.
|
|
*/
|
|
|
|
#pragma once
|
|
|
|
// If defined, this keeps a note of all calls and where from in memory. This is slower, but good for
|
|
// debugging
|
|
#define TINYJS_CALL_STACK
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
#include <functional>
|
|
#include <variant>
|
|
#include <limits>
|
|
#include <any>
|
|
|
|
#ifndef TRACE
|
|
#define TRACE printf
|
|
#endif // TRACE
|
|
|
|
//------------------------------------------------------------------------
|
|
namespace TJS {
|
|
|
|
constexpr int TINYJS_LOOP_MAX_ITERATIONS = 8192;
|
|
|
|
enum class LexType : int
|
|
{
|
|
Eof = 0,
|
|
ID = 256,
|
|
INT,
|
|
FLOAT,
|
|
STR,
|
|
|
|
EQUAL,
|
|
TYPEEQUAL,
|
|
NEQUAL,
|
|
NTYPEEQUAL,
|
|
LEQUAL,
|
|
LSHIFT,
|
|
LSHIFTEQUAL,
|
|
GEQUAL,
|
|
RSHIFT,
|
|
RSHIFTUNSIGNED,
|
|
RSHIFTEQUAL,
|
|
PLUSEQUAL,
|
|
MINUSEQUAL,
|
|
PLUSPLUS,
|
|
MINUSMINUS,
|
|
ANDEQUAL,
|
|
ANDAND,
|
|
OREQUAL,
|
|
OROR,
|
|
XOREQUAL,
|
|
|
|
// reserved words
|
|
R_LIST_START,
|
|
R_IF = R_LIST_START,
|
|
R_ELSE,
|
|
R_DO,
|
|
R_WHILE,
|
|
R_FOR,
|
|
R_BREAK,
|
|
R_CONTINUE,
|
|
R_FUNCTION,
|
|
R_RETURN,
|
|
R_VAR,
|
|
R_TRUE,
|
|
R_FALSE,
|
|
R_NULL,
|
|
R_UNDEFINED,
|
|
R_NEW,
|
|
|
|
LIST_END /* always the last entry */
|
|
};
|
|
inline constexpr int asInteger (LexType t) { return static_cast<int> (t); }
|
|
inline constexpr LexType asLexType (int t)
|
|
{
|
|
if (t < asInteger (LexType::ID) || t > (asInteger (LexType::LIST_END)))
|
|
return LexType::Eof;
|
|
return static_cast<LexType> (t);
|
|
}
|
|
|
|
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,
|
|
|
|
};
|
|
|
|
static constexpr auto TINYJS_RETURN_VAR = "return";
|
|
static constexpr auto TINYJS_PROTOTYPE_CLASS = "prototype";
|
|
static constexpr auto TINYJS_TEMP_NAME = "";
|
|
static constexpr auto TINYJS_BLANK_DATA = "";
|
|
|
|
//------------------------------------------------------------------------
|
|
// Custom memory allocator
|
|
using AllocatorFunc = std::function<void*(size_t)>;
|
|
using DeallocatorFunc = std::function<void (void*, size_t)>;
|
|
extern AllocatorFunc allocator;
|
|
extern DeallocatorFunc deallocator;
|
|
|
|
void setCustomAllocator (AllocatorFunc&& allocator, DeallocatorFunc&& deallocator);
|
|
|
|
//------------------------------------------------------------------------
|
|
template<typename T>
|
|
struct Allocator
|
|
{
|
|
using value_type = T;
|
|
using propagate_on_container_move_assignment = std::true_type;
|
|
|
|
Allocator () = default;
|
|
|
|
template<class U>
|
|
constexpr Allocator (const Allocator<U>&) noexcept
|
|
{
|
|
}
|
|
|
|
[[nodiscard]] T* allocate (std::size_t n)
|
|
{
|
|
return static_cast<T*> (allocator (n * sizeof (T)));
|
|
}
|
|
void deallocate (T* p, std::size_t n) noexcept { deallocator (p, n); }
|
|
|
|
bool operator== (const Allocator& other) const { return &other == this; }
|
|
bool operator!= (const Allocator& other) const { return &other != this; }
|
|
};
|
|
|
|
using string = std::basic_string<char, std::char_traits<char>, Allocator<char>>;
|
|
using ostringstream = std::basic_ostringstream<char, std::char_traits<char>, Allocator<char>>;
|
|
|
|
/** convert the given string into a quoted string suitable for javascript */
|
|
string getJSString (std::string_view str);
|
|
/** convert the given string to an 64 bit integer supporting hex and octal written numbers */
|
|
int64_t stringToInteger (std::string_view str);
|
|
|
|
class CScriptException
|
|
{
|
|
public:
|
|
string text;
|
|
CScriptException (const string& exceptionText);
|
|
CScriptException (string&& exceptionText);
|
|
CScriptException (const CScriptException&) = default;
|
|
CScriptException (CScriptException&&) = default;
|
|
~CScriptException () noexcept;
|
|
|
|
static void* operator new (std::size_t count);
|
|
static void operator delete (void* ptr, std::size_t size);
|
|
};
|
|
|
|
class CScriptLex
|
|
{
|
|
public:
|
|
CScriptLex (std::string_view input);
|
|
~CScriptLex (void);
|
|
|
|
/** Get the string representation of the given token */
|
|
static string getTokenStr (int token);
|
|
|
|
/** Lexical match wotsit */
|
|
void match (int expected_tk);
|
|
void match (LexType expected_tk);
|
|
/** Reset this lex so we can start again */
|
|
void reset ();
|
|
|
|
int getToken () const { return token; }
|
|
size_t getTokenStart () const { return tokenStart; }
|
|
size_t getTokenEnd () const { return tokenEnd; }
|
|
const string& getTokenString () const { return tkStr; }
|
|
|
|
/** Return a sub-string from the given position up until right now */
|
|
string getSubString (size_t pos) const;
|
|
/** Return a sub-lexer from the given position up until right now */
|
|
CScriptLex* getSubLex (size_t lastPosition) const;
|
|
/** Return a string representing the position in lines and columns of the character pos given */
|
|
string getPosition (size_t pos = std::numeric_limits<size_t>::max ()) const;
|
|
|
|
static void* operator new (std::size_t count);
|
|
static void operator delete (void* ptr, std::size_t size);
|
|
|
|
private:
|
|
void getNextCh ();
|
|
/** Get the text token from our text string */
|
|
void getNextToken ();
|
|
|
|
/** The type of the token that we have */
|
|
int token;
|
|
/** Position in the data at the beginning of the token we have here */
|
|
size_t tokenStart;
|
|
/** Position in the data at the last character of the token we have here */
|
|
size_t tokenEnd;
|
|
/** Position in the data at the last character of the last token */
|
|
size_t tokenLastEnd;
|
|
/** Data contained in the token we have here */
|
|
string tkStr;
|
|
|
|
char currCh, nextCh;
|
|
|
|
/** Data string to get tokens from */
|
|
const char* data;
|
|
/** Start and end position in data string */
|
|
size_t dataEnd;
|
|
/** Position in data (we CAN go past the end of the string here) */
|
|
size_t dataPos;
|
|
|
|
std::vector<size_t, Allocator<size_t>> newLinePositions;
|
|
};
|
|
|
|
class CScriptVar;
|
|
|
|
using JSCallback = std::function<void (CScriptVar* var)>;
|
|
|
|
class CScriptVarLink
|
|
{
|
|
public:
|
|
CScriptVarLink (CScriptVar* var, const string& name = TINYJS_TEMP_NAME, bool own = false);
|
|
/** Copy constructor */
|
|
CScriptVarLink (const CScriptVarLink& link);
|
|
~CScriptVarLink ();
|
|
/** Replace the Variable pointed to */
|
|
void replaceWith (CScriptVar* newVar);
|
|
/** Replace the Variable pointed to (just dereferences) */
|
|
void replaceWith (CScriptVarLink* newVar);
|
|
/** Get the name as an integer (for arrays) */
|
|
int getIntName () const;
|
|
/** Set the name as an integer (for arrays) */
|
|
void setIntName (int n);
|
|
|
|
const string& getName () const { return name; }
|
|
|
|
void setNextSibling (CScriptVarLink* s) { nextSibling = s; }
|
|
void setPrevSibling (CScriptVarLink* s) { prevSibling = s; }
|
|
CScriptVarLink* getNextSibling () const { return nextSibling; }
|
|
CScriptVarLink* getPrevSibling () const { return prevSibling; }
|
|
|
|
void setVar (CScriptVar* v);
|
|
CScriptVar* getVar () const { return var; }
|
|
|
|
bool owned () const { return isOwned; }
|
|
|
|
static void* operator new (std::size_t count);
|
|
static void operator delete (void* ptr, std::size_t size);
|
|
|
|
private:
|
|
string name;
|
|
CScriptVarLink* nextSibling {nullptr};
|
|
CScriptVarLink* prevSibling {nullptr};
|
|
CScriptVar* var {nullptr};
|
|
bool isOwned {false};
|
|
};
|
|
|
|
struct IScriptVarLifeTimeObserver
|
|
{
|
|
virtual ~IScriptVarLifeTimeObserver () noexcept = default;
|
|
|
|
virtual void onDestroy (CScriptVar* var) = 0;
|
|
};
|
|
|
|
/** Variable class (containing a doubly-linked list of children) */
|
|
class CScriptVar
|
|
{
|
|
public:
|
|
/** Create undefined */
|
|
CScriptVar ();
|
|
/** User defined */
|
|
CScriptVar (const string& varData, int varFlags);
|
|
/** Create a string */
|
|
CScriptVar (std::string_view str);
|
|
/** Create a double */
|
|
CScriptVar (double varData);
|
|
/** Create an integer */
|
|
CScriptVar (int64_t val);
|
|
/** Create an integer */
|
|
CScriptVar (bool val);
|
|
virtual ~CScriptVar (void);
|
|
|
|
/** If this is a function, get the result value (for use by native functions) */
|
|
CScriptVar* getReturnVar ();
|
|
/** Set the result value. Use this when setting complex return data as it avoids a deepCopy() */
|
|
void setReturnVar (CScriptVar* var);
|
|
/** If this is a function, get the parameter with the given name (for use by native functions)
|
|
*/
|
|
CScriptVar* getParameter (std::string_view name);
|
|
|
|
/** Tries to find a child with the given name, may return 0 */
|
|
CScriptVarLink* findChild (std::string_view childName);
|
|
/** Tries to find a child with the given name, or will create it with the given flags */
|
|
CScriptVarLink* findChildOrCreate (std::string_view childName,
|
|
int varFlags = SCRIPTVAR_UNDEFINED);
|
|
/** Tries to find a child with the given path (separated by dots) */
|
|
CScriptVarLink* findChildOrCreateByPath (const string& path);
|
|
/** add a child if not already exist */
|
|
CScriptVarLink* addChild (std::string_view childName, CScriptVar* child = NULL);
|
|
/** add a child overwriting any with the same name */
|
|
CScriptVarLink* addChildNoDup (std::string_view childName, CScriptVar* child = NULL);
|
|
/** remove the child */
|
|
void removeChild (CScriptVar* child);
|
|
/** Remove a specific link (this is faster than finding via a child) */
|
|
void removeLink (CScriptVarLink* link);
|
|
void removeAllChildren ();
|
|
/** The the value at an array index */
|
|
CScriptVar* getArrayIndex (int idx);
|
|
/** Set the value at an array index */
|
|
void setArrayIndex (int idx, CScriptVar* value);
|
|
/** If this is an array, return the number of items in it (else 0) */
|
|
int getArrayLength ();
|
|
/** Get the number of children */
|
|
int getChildren ();
|
|
|
|
int64_t getInt ();
|
|
bool getBool () { return getInt () != 0; }
|
|
double getDouble ();
|
|
const string& getString ();
|
|
/** get Data as a parsable javascript string */
|
|
string getParsableString ();
|
|
void setInt (int64_t num);
|
|
void setDouble (double val);
|
|
void setString (std::string_view str);
|
|
void setUndefined ();
|
|
void setArray ();
|
|
bool equals (CScriptVar* v);
|
|
|
|
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; }
|
|
/** Is this *not* an array/object/etc */
|
|
bool isBasic () { return firstChild == 0; }
|
|
|
|
/** do a maths op with another script variable */
|
|
CScriptVar* mathsOp (CScriptVar* b, int op);
|
|
/** copy the value from the value given */
|
|
void copyValue (CScriptVar* val);
|
|
/** deep copy this node and return the result */
|
|
CScriptVar* deepCopy ();
|
|
|
|
/** Dump out the contents of this using trace */
|
|
void trace (string indentStr = "", const string& name = "");
|
|
/** For debugging - just dump a string version of the flags */
|
|
string getFlagsAsString ();
|
|
/** Write out all the JS code needed to recreate this script variable to the stream (as JSON) */
|
|
void getJSON (std::ostream& destination, const string linePrefix = "");
|
|
/** Set the callback for native functions */
|
|
void setCallback (const JSCallback& callback);
|
|
/** Moves in the callback for native functions */
|
|
void setCallback (JSCallback&& callback);
|
|
|
|
void callCallback (CScriptVar* var);
|
|
|
|
void setFunctionScript (std::string_view str);
|
|
|
|
/// For memory management/garbage collection
|
|
/** Add reference to this variable */
|
|
CScriptVar* addRef ();
|
|
/** Remove a reference, and delete this variable if required */
|
|
void release ();
|
|
/** Get the number of references to this script variable */
|
|
int getRefs ();
|
|
|
|
void setLifeTimeObserver (IScriptVarLifeTimeObserver* obs) { lifeTimeObserver = obs; }
|
|
|
|
CScriptVarLink* getFirstChild () const { return firstChild; }
|
|
CScriptVarLink* getLastChild () const { return lastChild; }
|
|
|
|
void setCustomData (std::any&& cd) { customData = std::move (cd); }
|
|
const std::any& getCustomData () const { return customData; }
|
|
|
|
static void* operator new (std::size_t count);
|
|
static void operator delete (void* ptr, std::size_t size);
|
|
|
|
protected:
|
|
CScriptVarLink* firstChild {nullptr};
|
|
CScriptVarLink* lastChild {nullptr};
|
|
|
|
/** The number of references held to this - used for garbage collection */
|
|
int refs {0};
|
|
/** the flags determine the type of the variable - int/double/string/etc */
|
|
int flags {0};
|
|
std::variant<string, int64_t, double, JSCallback> variant;
|
|
std::any customData;
|
|
string dataStr;
|
|
|
|
/** Copy the basic data and flags from the variable given, with no
|
|
* children. Should be used internally only - by copyValue and deepCopy */
|
|
void copySimpleData (CScriptVar* val);
|
|
|
|
private:
|
|
IScriptVarLifeTimeObserver* lifeTimeObserver {nullptr};
|
|
};
|
|
|
|
inline CScriptVar* owning (CScriptVar* v) { return v->addRef (); }
|
|
|
|
class CTinyJS
|
|
{
|
|
public:
|
|
CTinyJS ();
|
|
~CTinyJS ();
|
|
|
|
void execute (const 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 release the result as it goes out of scope. If you want to
|
|
* keep it, you must use addRef() and release() */
|
|
CScriptVarLink evaluateComplex (std::string_view code);
|
|
/** Evaluate the given code and return a string. If nothing to return, will return
|
|
* 'undefined' */
|
|
string evaluate (std::string_view 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 (std::string_view funcDesc, const JSCallback& ptr);
|
|
|
|
/** Get the given variable specified by a path (var1.var2.etc), or return 0 */
|
|
CScriptVar* getScriptVariable (const string& path) const;
|
|
/** Get the value of the given variable, or return 0 */
|
|
const string* getVariable (const string& path) const;
|
|
/** set the value of the given variable, return trur if it exists and gets set */
|
|
bool setVariable (const string& path, const string& varData);
|
|
|
|
/** Send all variables to stdout */
|
|
void trace ();
|
|
|
|
CScriptVar* getRoot () const { return root; }
|
|
|
|
static void* operator new (std::size_t count);
|
|
static void operator delete (void* ptr, std::size_t size);
|
|
|
|
private:
|
|
/** root of symbol table */
|
|
CScriptVar* root {nullptr};
|
|
/** current lexer */
|
|
CScriptLex* lexer {nullptr};
|
|
/** stack of scopes when parsing */
|
|
std::vector<CScriptVar*> scopes;
|
|
#ifdef TINYJS_CALL_STACK
|
|
/** Names of places called so we can show when erroring */
|
|
std::vector<string> call_stack;
|
|
#endif
|
|
|
|
/** Built in string class */
|
|
CScriptVar* stringClass {nullptr};
|
|
/** Built in object class */
|
|
CScriptVar* objectClass {nullptr};
|
|
/** Built in array class */
|
|
CScriptVar* arrayClass {nullptr};
|
|
|
|
// parsing - in order of precedence
|
|
CScriptVarLink* functionCall (bool& execute, CScriptVarLink* function, CScriptVar* parent);
|
|
CScriptVarLink* factor (bool& execute);
|
|
CScriptVarLink* unary (bool& execute);
|
|
CScriptVarLink* term (bool& execute);
|
|
CScriptVarLink* expression (bool& execute);
|
|
CScriptVarLink* shift (bool& execute);
|
|
CScriptVarLink* condition (bool& execute);
|
|
CScriptVarLink* logic (bool& execute);
|
|
CScriptVarLink* ternary (bool& execute);
|
|
CScriptVarLink* base (bool& execute);
|
|
void block (bool& execute);
|
|
void statement (bool& execute);
|
|
// parsing utility functions
|
|
CScriptVarLink* parseFunctionDefinition ();
|
|
void parseFunctionArguments (CScriptVar* funcVar) const;
|
|
|
|
/** Finds a child, looking recursively up the scopes */
|
|
CScriptVarLink* findInScopes (const string& childName) const;
|
|
/** Look up in any parent classes of the given object */
|
|
CScriptVarLink* findInParentClasses (CScriptVar* object, const string& name) const;
|
|
};
|
|
|
|
//------------------------------------------------------------------------
|
|
} // TJS
|