adder-script
Version:
Python like language to execute untrusted codes in browsers and Node.js.
460 lines (381 loc) • 14.7 kB
JavaScript
"use strict";
/**
* Represent a value in program's memory. Everything inside the running environment is stored inside variables.
*
* Author: Ronen Ness.
* Since: 2016
*/
// include jsface for classes
var jsface = require("./../dependencies/jsface"),
Class = jsface.Class,
extend = jsface.extend;
// include errors
var Errors = require("./../errors");
// include language defs
var LanguageDefs = require("./../language/defs");
// require basic utils
var Utils = require("./../utils");
// set of simple variable types
var simpleTypes = Utils.toSet(["string", "number", "boolean", "none"]);
// Variable class
var Variable = Class({
// variable constructor
// @param context - context of program currently executed.
// @param value - variable value - must be raw, native type. NOT an expression.
constructor: function(context, value)
{
// store context and arguments
this._context = context;
// set type
// special case for null and undefined
if (value === null || value === undefined)
{
value = null; // <-- in case we got "undefined" convert it to null.
this.type = "none";
this.isSimple = true;
this._estimatedSize = 1;
}
// if its an object set type
else
{
// function
if (value.isFunction) {
this.type = "function";
this.isSimple = false;
this._estimatedSize = 4;
}
// NaN
else if (typeof value === "number" && isNaN(value))
{
this.type = "NaN";
this.isSimple = false;
this._estimatedSize = 4;
}
// if object and have type
else if (value.type)
{
this.type = value.type;
this.isSimple = false;
this._estimatedSize = 4;
}
// any other object
else {
// set type
this.type = typeof value;
// special case - if string - remove quotes
if (this.type === "string") {
// make sure don't exceed max string length
var maxLen = this._context._interpreter._flags.maxStringLen;
if (maxLen && value.length > maxLen) {
throw new Errors.ExceedMemoryLimit("String exceeded length limit of " + maxLen + "!");
}
// set estimated size
this._estimatedSize = value.length;
}
// for any other simple type estimated size is 1 byte
else {
this._estimatedSize = 1;
}
// its a simple type
this.isSimple = true;
}
}
// add API based on variable type
this.api = APIs[this.type];
// set scope data
this._scopeData = {};
// store value
this._value = value;
},
// set scope-related data of this variable
setScopeData: function(scope, name, readonly) {
this._scopeData = {
scope: scope,
name: name,
readonly: readonly,
};
},
// some static functions
$static: {
// return if given value is a variable instance
isVariable: function(val) {
return val && val.constructor === Variable;
},
// check if two variables are equal
equal: function(a, b) {
// if same object return true
if (a === b) {
return true;
}
// first get types and check if different types
var typeA = a.type;
var typeB = b.type;
if (typeA !== typeB) {return false;}
// get values
a = a._value;
b = b._value;
// compare based on type
switch (typeA) {
// special case - NaN (remember we checked types before, so we don't need to check if both are NaN).
// why we need this? because in JS if you do NaN === NaN the result is false, and I want to fix that.
case "NaN":
return true;
// if list compare lists
case "list":
// if different length return false
if (a.len() !== b.len()) {return false;}
// iterate over items and compare recursively
for (var i = 0; i < a._list.length; ++i) {
if (!Variable.equal(a._list[i], b._list[i])) {
return false;
}
}
// if got here it means they are equal
return true;
// if dict compare as dicts
case "dict":
// get keys
var ak = a.keys(); var bk = b.keys();
// different length? not equal
if (ak.len() !== bk.len()) {return false;}
// iterate over keys and compare recursively
for (var i = 0; i < ak._list.length; ++i) {
var key = ak._list[i]._value;
if (!Variable.equal(a._dict[key], b._dict[key])) {
return false;
}
}
// if got here it means they are equal
return true;
// if set compare sets
case "set":
// if different length return false
if (a.len() !== b.len()) {return false;}
// convert both sets to lists
a = a.to_list(); b = b.to_list();
// iterate over items and compare recursively
for (var i = 0; i < a._list.length; ++i) {
if (!Variable.equal(a._list[i], b._list[i])) {
return false;
}
}
// if got here it means they are equal
return true;
// for default types just compare
default:
return a === b;
};
},
// check if two variables are the same (like 'is' in python)
is: function(a, b) {
// if same object return true
if (a === b) {
return true;
}
// first get types and check if different types
var typeA = a.type;
var typeB = b.type;
if (typeA !== typeB) {return false;}
// get values
a = a._value;
b = b._value;
// special case - NaN (remember we checked types before, so we don't need to check if both are NaN).
// why we need this? because in JS if you do NaN === NaN the result is false, and I want to fix that.
if (typeA === "NaN") {return true;}
// if its an object, check if the same instance
if (a && a.api)
{
return a === b;
}
// for native types return comparison
return a === b;
},
// make sure given value is a variable (or a list of variables, if array is given).
// note: this can either handle a single value or an array. if array, it will convert all items inside to variables.
// @param forceCreateNew - if true, and val is already a variable, clone it to a new variable.
makeVariables: function(context, val, forceCreateNew) {
// if array convert all items into variable
if (val instanceof Array) {
// length is 0? stop here!
if (val.length === 0) {return val;}
// convert to variables and return
for (var i = 0; i < val.length; ++i) {
val[i] = Variable.makeVariables(context, val[i], forceCreateNew);
}
// return value
return val;
}
// if already variable stop here..
if (val && val.constructor === Variable)
{
if (forceCreateNew) {
return new Variable(context, val._value);
}
return val;
}
// convert to variable
return new Variable(context, val);
},
// similar to makeVariables but slightly different in a way that it will convert arrays into Adder lists, Sets
// into adder Sets, etc..
makeAdderObjects: function(context, val, forceCreateNew) {
// if already a variable stop here..
if (val && val.constructor === Variable)
{
// unless forced to create a new var, in which case create a copy
if (forceCreateNew) {
return new Variable(context, val._value);
}
return val;
}
// if array convert all items inside into adder objects
else if (val instanceof Array)
{
for (var i = 0; i < val.length; ++i) {
val[i] = Variable.makeAdderObjects(context, val[i], forceCreateNew);
}
// create and return the new list
val = new _List(context, val);
}
// if a Set
else if (val && val.constructor === Set)
{
// create and return the new Set
val = new _Set(context, Utils.toArray(val));
}
// else if a dictionary convert to a dict
else if (val instanceof Object && !val.__isAdderObject)
{
// convert to variables and return
for (var key in val) {
val[key] = Variable.makeAdderObjects(context, val[key], forceCreateNew);
}
// create and return the new dictionary
val = new _Dict(context, val);
}
// now convert to a variable and return
return new Variable(context, val);
},
},
// get variable value.
getValue: function()
{
return this._value;
},
// get variable type
getType: function()
{
return this.type;
},
// delete this variable
deleteSelf: function()
{
// if no scope
if (!this._scopeData.scope) {
throw new Errors.RuntimeError("Cannot delete object '" + this.toString() + "'!");
}
// remove self from context and reset scope data
this._context.remove(this._scopeData.name);
this._scopeData = {};
},
// implement the 'in' operator, eg if val is inside this
has: function(val)
{
// first check if have value
if (this._value === null || this._value === undefined) {
return false;
}
// now check if this var have 'has()' implemented internally. if so, use it.
if (this._value.has) {
return this._value.has(val);
}
// make sure value to check is not a variable
if (val && val.getValue) {
val = val._value;
}
// if has() is not supported, fallback to simple string indexOf
return String(this._value).indexOf(val) !== -1;
},
// convert variable to a native javascript object
toNativeJs: function()
{
// get value
var val = this._value;
// if its null etc.
if (!val) {
return val;
}
// if value is an object with its own to toNativeJs function, call it.
if (val.toNativeJs) {
return val.toNativeJs();
}
// special case for dictionaries
if (val.isFunction) {
return val.func || val.__imp;
}
// if a simple value return the value itself
return val;
},
// convert to repr
toRepr: function()
{
// get value
var val = this._value;
// return string based on type
switch (this.type)
{
case "string":
return '"' + val + '"';
case "number":
return String(val);
case "none":
return LanguageDefs.keywords['null'];
case "boolean":
return LanguageDefs.keywords[String(val)];
case "function":
return val.toRepr();
default:
// check if value got a string function of its own
if (val && val.toRepr) {
return val.toRepr();
}
// if not just convert to a string and return
return String(val);
}
},
// convert to string
toString: function()
{
// return string based on type
switch (this.type)
{
case "string":
return this._value;
case "number":
return String(this._value);
case "none":
return "";
case "boolean":
return LanguageDefs.keywords[String(this._value)];
case "function":
return this._value.toString();
default:
// check if value got a string function of its own
if (this._value && this._value.toString) {
return this._value.toString();
}
// if not just convert to a string and return
return String(this._value);
}
},
});
// export the Executable class
module.exports = Variable;
// get List, Dict and Set
var _List = require("./list");
var _Set = require("./set");
var _Dict = require("./dict");
// different APIs for different variable types
var APIs = {
'string': require("./string_api"),
};