import * as acorn from "../utils/acorn"; /** * @license * JavaScript Interpreter * * Copyright 2013 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @fileoverview Interpreting JavaScript in JavaScript. * @author fraser@google.com (Neil Fraser) */ 'use strict'; /** * Create a new interpreter. * @param {string|!Object} code Raw JavaScript text or AST. * @param {Function=} opt_initFunc Optional initialization function. Used to * define APIs. When called it is passed the interpreter object and the * global scope object. * @param {Number} Bitburner-specific number used for determining exception line numbers * @constructor */ var Interpreter = function(code, opt_initFunc, lineOffset=0) { this.sourceCode = code; this.sourceCodeLineOffset = lineOffset; if (typeof code === 'string') { code = acorn.parse(code, Interpreter.PARSE_OPTIONS); } this.ast = code; this.initFunc_ = opt_initFunc; this.paused_ = false; this.polyfills_ = []; // Unique identifier for native functions. Used in serialization. this.functionCounter_ = 0; // Map node types to our step function names; a property lookup is faster // than string concatenation with "step" prefix. this.stepFunctions_ = Object.create(null); var stepMatch = /^step([A-Z]\w*)$/; var m; for (var methodName in this) { if ((typeof this[methodName] === 'function') && (m = methodName.match(stepMatch))) { this.stepFunctions_[m[1]] = this[methodName].bind(this); } } // Create and initialize the global scope. this.global = this.createScope(this.ast, null); // Run the polyfills. this.ast = acorn.parse(this.polyfills_.join('\n'), Interpreter.PARSE_OPTIONS); this.polyfills_ = undefined; // Allow polyfill strings to garbage collect. this.stripLocations_(this.ast, undefined, undefined); var state = new Interpreter.State(this.ast, this.global); state.done = false; this.stateStack = [state]; this.run(); this.value = undefined; // Point at the main program. this.ast = code; var state = new Interpreter.State(this.ast, this.global); state.done = false; this.stateStack.length = 0; this.stateStack[0] = state; // Get a handle on Acorn's node_t object. It's tricky to access. this.nodeConstructor = state.node.constructor; // Preserve publicly properties from being pruned/renamed by JS compilers. // Add others as needed. this['stateStack'] = this.stateStack; }; /** * @const {!Object} Configuration used for all Acorn parsing. */ Interpreter.PARSE_OPTIONS = { ecmaVersion: 5, locations: true }; /** * Property descriptor of readonly properties. */ Interpreter.READONLY_DESCRIPTOR = { configurable: true, enumerable: true, writable: false }; /** * Property descriptor of non-enumerable properties. */ Interpreter.NONENUMERABLE_DESCRIPTOR = { configurable: true, enumerable: false, writable: true }; /** * Property descriptor of readonly, non-enumerable properties. */ Interpreter.READONLY_NONENUMERABLE_DESCRIPTOR = { configurable: true, enumerable: false, writable: false }; /** * Property descriptor of variables. */ Interpreter.VARIABLE_DESCRIPTOR = { configurable: false, enumerable: true, writable: true }; /** * Unique symbol for indicating that a step has encountered an error, has * added it to the stack, and will be thrown within the user's program. * When STEP_ERROR is thrown in the JS-Interpreter, the error can be ignored. */ Interpreter.STEP_ERROR = {}; /** * Unique symbol for indicating that a reference is a variable on the scope, * not an object property. */ Interpreter.SCOPE_REFERENCE = {}; /** * Unique symbol for indicating, when used as the value of the value * parameter in calls to setProperty and friends, that the value * should be taken from the property descriptor instead. */ Interpreter.VALUE_IN_DESCRIPTOR = {}; /** * For cycle detection in array to string and error conversion; * see spec bug github.com/tc39/ecma262/issues/289 * Since this is for atomic actions only, it can be a class property. */ Interpreter.toStringCycles_ = []; /** * Determine error/exception line number in Bitburner source code * @param {Object} AST Node that causes Error/Exception */ Interpreter.prototype.getErrorLineNumber = function(node) { var code = this.sourceCode; if (node == null || node.start == null) {return NaN;} try { code = code.substring(0, node.start); return (code.match(/\n/g) || []).length + 1 - this.sourceCodeLineOffset; } catch(e) { return NaN; } } /** * Generate the appropriate line number error message for Bitburner * @param {Number} lineNumber */ Interpreter.prototype.getErrorLineNumberMessage = function(lineNumber) { if (isNaN(lineNumber)) { return " (Unknown line number)"; } else if (lineNumber <= 0) { return " (Error occurred in an imported function)"; } else { return " (Line Number " + lineNumber + ". This line number is probably incorrect " + "if your script is importing any functions. This is being worked on)"; } } /** * Add more code to the interpreter. * @param {string|!Object} code Raw JavaScript text or AST. */ Interpreter.prototype.appendCode = function(code) { var state = this.stateStack[0]; if (!state || state.node['type'] !== 'Program') { throw Error('Expecting original AST to start with a Program node.'); } if (typeof code === 'string') { code = acorn.parse(code, Interpreter.PARSE_OPTIONS); } if (!code || code['type'] !== 'Program') { throw Error('Expecting new AST to start with a Program node.'); } this.populateScope_(code, state.scope); // Append the new program to the old one. for (var i = 0, node; (node = code['body'][i]); i++) { state.node['body'].push(node); } state.done = false; }; /** * Execute one step of the interpreter. * @return {boolean} True if a step was executed, false if no more instructions. */ Interpreter.prototype.step = function() { var stack = this.stateStack; var state = stack[stack.length - 1]; if (!state) { return false; } var node = state.node, type = node['type']; if (type === 'Program' && state.done) { return false; } else if (this.paused_) { return true; } try { var nextState = this.stepFunctions_[type](stack, state, node); } catch (e) { // Eat any step errors. They have been thrown on the stack. if (e !== Interpreter.STEP_ERROR) { // Uh oh. This is a real error in the JS-Interpreter. Rethrow. throw e; } } if (nextState) { stack.push(nextState); } if (!node['end']) { // This is polyfill code. Keep executing until we arrive at user code. return this.step(); } return true; }; /** * Execute the interpreter to program completion. Vulnerable to infinite loops. * @return {boolean} True if a execution is asynchronously blocked, * false if no more instructions. */ Interpreter.prototype.run = function() { while (!this.paused_ && this.step()) {} return this.paused_; }; /** * Initialize the global scope with buitin properties and functions. * @param {!Interpreter.Object} scope Global scope. */ Interpreter.prototype.initGlobalScope = function(scope) { // Initialize uneditable global properties. this.setProperty(scope, 'NaN', NaN, Interpreter.READONLY_DESCRIPTOR); this.setProperty(scope, 'Infinity', Infinity, Interpreter.READONLY_DESCRIPTOR); this.setProperty(scope, 'undefined', undefined, Interpreter.READONLY_DESCRIPTOR); this.setProperty(scope, 'window', scope, Interpreter.READONLY_DESCRIPTOR); this.setProperty(scope, 'this', scope, Interpreter.READONLY_DESCRIPTOR); this.setProperty(scope, 'self', scope); // Editable. // Create the objects which will become Object.prototype and // Function.prototype, which are needed to bootstrap everything else. this.OBJECT_PROTO = new Interpreter.Object(null); this.FUNCTION_PROTO = new Interpreter.Object(this.OBJECT_PROTO); // Initialize global objects. this.initFunction(scope); this.initObject(scope); // Unable to set scope's parent prior (OBJECT did not exist). // Note that in a browser this would be 'Window', whereas in Node.js it would // be 'Object'. This interpreter is closer to Node in that it has no DOM. scope.proto = this.OBJECT_PROTO; this.setProperty(scope, 'constructor', this.OBJECT, Interpreter.NONENUMERABLE_DESCRIPTOR); this.initArray(scope); this.initString(scope); this.initBoolean(scope); this.initNumber(scope); this.initDate(scope); this.initRegExp(scope); this.initError(scope); this.initMath(scope); this.initJSON(scope); // Initialize global functions. var thisInterpreter = this; var func = this.createNativeFunction( function(x) {throw EvalError("Can't happen");}, false); func.eval = true; this.setProperty(scope, 'eval', func); this.setProperty(scope, 'parseInt', this.createNativeFunction(parseInt, false)); this.setProperty(scope, 'parseFloat', this.createNativeFunction(parseFloat, false)); this.setProperty(scope, 'isNaN', this.createNativeFunction(isNaN, false)); this.setProperty(scope, 'isFinite', this.createNativeFunction(isFinite, false)); var strFunctions = [ [escape, 'escape'], [unescape, 'unescape'], [decodeURI, 'decodeURI'], [decodeURIComponent, 'decodeURIComponent'], [encodeURI, 'encodeURI'], [encodeURIComponent, 'encodeURIComponent'] ]; for (var i = 0; i < strFunctions.length; i++) { var wrapper = (function(nativeFunc) { return function(str) { try { return nativeFunc(str); } catch (e) { // decodeURI('%xy') will throw an error. Catch and rethrow. thisInterpreter.throwException(thisInterpreter.URI_ERROR, e.message); } }; })(strFunctions[i][0]); this.setProperty(scope, strFunctions[i][1], this.createNativeFunction(wrapper, false), Interpreter.NONENUMERABLE_DESCRIPTOR); } // Preserve publicly properties from being pruned/renamed by JS compilers. // Add others as needed. this['OBJECT'] = this.OBJECT; this['OBJECT_PROTO'] = this.OBJECT_PROTO; this['FUNCTION'] = this.FUNCTION; this['FUNCTION_PROTO'] = this.FUNCTION_PROTO; this['ARRAY'] = this.ARRAY; this['ARRAY_PROTO'] = this.ARRAY_PROTO; this['REGEXP'] = this.REGEXP; this['REGEXP_PROTO'] = this.REGEXP_PROTO; this['DATE'] = this.DATE; this['DATE_PROTO'] = this.DATE_PROTO; // The following properties are obsolete. Do not use. this['UNDEFINED'] = undefined; this['NULL'] = null; this['NAN'] = NaN; this['TRUE'] = true; this['FALSE'] = false; this['STRING_EMPTY'] = ''; this['NUMBER_ZERO'] = 0; this['NUMBER_ONE'] = 1; // Run any user-provided initialization. if (this.initFunc_) { this.initFunc_(this, scope); } }; /** * Initialize the Function class. * @param {!Interpreter.Object} scope Global scope. */ Interpreter.prototype.initFunction = function(scope) { var thisInterpreter = this; var wrapper; var identifierRegexp = /^[A-Za-z_$][\w$]*$/; // Function constructor. wrapper = function(var_args) { if (thisInterpreter.calledWithNew()) { // Called as new Function(). var newFunc = this; } else { // Called as Function(). var newFunc = thisInterpreter.createObjectProto(thisInterpreter.FUNCTION_PROTO); } if (arguments.length) { var code = String(arguments[arguments.length - 1]); } else { var code = ''; } var argsStr = Array.prototype.slice.call(arguments, 0, -1).join(',').trim(); if (argsStr) { var args = argsStr.split(/\s*,\s*/); for (var i = 0; i < args.length; i++) { var name = args[i]; if (!identifierRegexp.test(name)) { thisInterpreter.throwException(thisInterpreter.SYNTAX_ERROR, 'Invalid function argument: ' + name); } } argsStr = args.join(', '); } // Interestingly, the scope for constructed functions is the global scope, // even if they were constructed in some other scope. newFunc.parentScope = thisInterpreter.global; // Acorn needs to parse code in the context of a function or else 'return' // statements will be syntax errors. try { var ast = acorn.parse('(function(' + argsStr + ') {' + code + '})', Interpreter.PARSE_OPTIONS); } catch (e) { // Acorn threw a SyntaxError. Rethrow as a trappable error. thisInterpreter.throwException(thisInterpreter.SYNTAX_ERROR, 'Invalid code: ' + e.message); } if (ast['body'].length !== 1) { // Function('a', 'return a + 6;}; {alert(1);'); thisInterpreter.throwException(thisInterpreter.SYNTAX_ERROR, 'Invalid code in function body.'); } newFunc.node = ast['body'][0]['expression']; thisInterpreter.setProperty(newFunc, 'length', newFunc.node['length'], Interpreter.READONLY_DESCRIPTOR); return newFunc; }; wrapper.id = this.functionCounter_++; this.FUNCTION = this.createObjectProto(this.FUNCTION_PROTO); this.setProperty(scope, 'Function', this.FUNCTION); // Manually setup type and prototype because createObj doesn't recognize // this object as a function (this.FUNCTION did not exist). this.setProperty(this.FUNCTION, 'prototype', this.FUNCTION_PROTO); this.FUNCTION.nativeFunc = wrapper; // Configure Function.prototype. this.setProperty(this.FUNCTION_PROTO, 'constructor', this.FUNCTION, Interpreter.NONENUMERABLE_DESCRIPTOR); this.FUNCTION_PROTO.nativeFunc = function() {}; this.FUNCTION_PROTO.nativeFunc.id = this.functionCounter_++; this.setProperty(this.FUNCTION_PROTO, 'length', 0, Interpreter.READONLY_DESCRIPTOR); var boxThis = function(value) { // In non-strict mode 'this' must be an object. if ((!value || !value.isObject) && !thisInterpreter.getScope().strict) { if (value === undefined || value === null) { // 'Undefined' and 'null' are changed to global object. value = thisInterpreter.global; } else { // Primitives must be boxed in non-strict mode. var box = thisInterpreter.createObjectProto( thisInterpreter.getPrototype(value)); box.data = value; value = box; } } return value; }; wrapper = function(thisArg, args) { var state = thisInterpreter.stateStack[thisInterpreter.stateStack.length - 1]; // Rewrite the current 'CallExpression' to apply a different function. state.func_ = this; // Assign the 'this' object. state.funcThis_ = boxThis(thisArg); // Bind any provided arguments. state.arguments_ = []; if (args !== null && args !== undefined) { if (args.isObject) { state.arguments_ = thisInterpreter.arrayPseudoToNative(args); } else { thisInterpreter.throwException(thisInterpreter.TYPE_ERROR, 'CreateListFromArrayLike called on non-object'); } } state.doneExec_ = false; }; this.setNativeFunctionPrototype(this.FUNCTION, 'apply', wrapper); wrapper = function(thisArg /*, var_args */) { var state = thisInterpreter.stateStack[thisInterpreter.stateStack.length - 1]; // Rewrite the current 'CallExpression' to call a different function. state.func_ = this; // Assign the 'this' object. state.funcThis_ = boxThis(thisArg); // Bind any provided arguments. state.arguments_ = []; for (var i = 1; i < arguments.length; i++) { state.arguments_.push(arguments[i]); } state.doneExec_ = false; }; this.setNativeFunctionPrototype(this.FUNCTION, 'call', wrapper); this.polyfills_.push( // Polyfill copied from: // developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_objects/Function/bind "Object.defineProperty(Function.prototype, 'bind',", "{configurable: true, writable: true, value:", "function(oThis) {", "if (typeof this !== 'function') {", "throw TypeError('What is trying to be bound is not callable');", "}", "var aArgs = Array.prototype.slice.call(arguments, 1),", "fToBind = this,", "fNOP = function() {},", "fBound = function() {", "return fToBind.apply(this instanceof fNOP", "? this", ": oThis,", "aArgs.concat(Array.prototype.slice.call(arguments)));", "};", "if (this.prototype) {", "fNOP.prototype = this.prototype;", "}", "fBound.prototype = new fNOP();", "return fBound;", "}", "});", ""); // Function has no parent to inherit from, so it needs its own mandatory // toString and valueOf functions. wrapper = function() { return this.toString(); }; this.setNativeFunctionPrototype(this.FUNCTION, 'toString', wrapper); this.setProperty(this.FUNCTION, 'toString', this.createNativeFunction(wrapper, false), Interpreter.NONENUMERABLE_DESCRIPTOR); wrapper = function() { return this.valueOf(); }; this.setNativeFunctionPrototype(this.FUNCTION, 'valueOf', wrapper); this.setProperty(this.FUNCTION, 'valueOf', this.createNativeFunction(wrapper, false), Interpreter.NONENUMERABLE_DESCRIPTOR); }; /** * Initialize the Object class. * @param {!Interpreter.Object} scope Global scope. */ Interpreter.prototype.initObject = function(scope) { var thisInterpreter = this; var wrapper; // Object constructor. wrapper = function(value) { if (value === undefined || value === null) { // Create a new object. if (thisInterpreter.calledWithNew()) { // Called as new Object(). return this; } else { // Called as Object(). return thisInterpreter.createObjectProto(thisInterpreter.OBJECT_PROTO); } } if (!value.isObject) { // Wrap the value as an object. var box = thisInterpreter.createObjectProto( thisInterpreter.getPrototype(value)); box.data = value; return box; } // Return the provided object. return value; }; this.OBJECT = this.createNativeFunction(wrapper, true); // Throw away the created prototype and use the root prototype. this.setProperty(this.OBJECT, 'prototype', this.OBJECT_PROTO); this.setProperty(this.OBJECT_PROTO, 'constructor', this.OBJECT, Interpreter.NONENUMERABLE_DESCRIPTOR); this.setProperty(scope, 'Object', this.OBJECT); /** * Checks if the provided value is null or undefined. * If so, then throw an error in the call stack. * @param {Interpreter.Value} value Value to check. */ var throwIfNullUndefined = function(value) { if (value === undefined || value === null) { thisInterpreter.throwException(thisInterpreter.TYPE_ERROR, "Cannot convert '" + value + "' to object"); } }; // Static methods on Object. wrapper = function(obj) { throwIfNullUndefined(obj); var props = obj.isObject ? obj.properties : obj; return thisInterpreter.arrayNativeToPseudo( Object.getOwnPropertyNames(props)); }; this.setProperty(this.OBJECT, 'getOwnPropertyNames', this.createNativeFunction(wrapper, false), Interpreter.NONENUMERABLE_DESCRIPTOR); wrapper = function(obj) { throwIfNullUndefined(obj); if (obj.isObject) { obj = obj.properties; } return thisInterpreter.arrayNativeToPseudo(Object.keys(obj)); }; this.setProperty(this.OBJECT, 'keys', this.createNativeFunction(wrapper, false), Interpreter.NONENUMERABLE_DESCRIPTOR); wrapper = function(proto) { // Support for the second argument is the responsibility of a polyfill. if (proto === null) { return thisInterpreter.createObjectProto(null); } if (proto === undefined || !proto.isObject) { thisInterpreter.throwException(thisInterpreter.TYPE_ERROR, 'Object prototype may only be an Object or null'); } return thisInterpreter.createObjectProto(proto); }; this.setProperty(this.OBJECT, 'create', this.createNativeFunction(wrapper, false), Interpreter.NONENUMERABLE_DESCRIPTOR); // Add a polyfill to handle create's second argument. this.polyfills_.push( "(function() {", "var create_ = Object.create;", "Object.create = function(proto, props) {", "var obj = create_(proto);", "props && Object.defineProperties(obj, props);", "return obj;", "};", "})();", ""); wrapper = function(obj, prop, descriptor) { prop = String(prop); if (!obj || !obj.isObject) { thisInterpreter.throwException(thisInterpreter.TYPE_ERROR, 'Object.defineProperty called on non-object'); } if (!descriptor || !descriptor.isObject) { thisInterpreter.throwException(thisInterpreter.TYPE_ERROR, 'Property description must be an object'); } if (!obj.properties[prop] && obj.preventExtensions) { thisInterpreter.throwException(thisInterpreter.TYPE_ERROR, "Can't define property '" + prop + "', object is not extensible"); } // The polyfill guarantees no inheritance and no getter functions. // Therefore the descriptor properties map is the native object needed. thisInterpreter.setProperty(obj, prop, Interpreter.VALUE_IN_DESCRIPTOR, descriptor.properties); return obj; }; this.setProperty(this.OBJECT, 'defineProperty', this.createNativeFunction(wrapper, false), Interpreter.NONENUMERABLE_DESCRIPTOR); this.polyfills_.push( // Flatten the descriptor to remove any inheritance or getter functions. "(function() {", "var defineProperty_ = Object.defineProperty;", "Object.defineProperty = function(obj, prop, d1) {", "var d2 = {};", "if ('configurable' in d1) d2.configurable = d1.configurable;", "if ('enumerable' in d1) d2.enumerable = d1.enumerable;", "if ('writable' in d1) d2.writable = d1.writable;", "if ('value' in d1) d2.value = d1.value;", "if ('get' in d1) d2.get = d1.get;", "if ('set' in d1) d2.set = d1.set;", "return defineProperty_(obj, prop, d2);", "};", "})();", "Object.defineProperty(Object, 'defineProperties',", "{configurable: true, writable: true, value:", "function(obj, props) {", "var keys = Object.keys(props);", "for (var i = 0; i < keys.length; i++) {", "Object.defineProperty(obj, keys[i], props[keys[i]]);", "}", "return obj;", "}", "});", ""); wrapper = function(obj, prop) { if (!obj || !obj.isObject) { thisInterpreter.throwException(thisInterpreter.TYPE_ERROR, 'Object.getOwnPropertyDescriptor called on non-object'); } prop = String(prop); if (!(prop in obj.properties)) { return undefined; } var descriptor = Object.getOwnPropertyDescriptor(obj.properties, prop); var getter = obj.getter[prop]; var setter = obj.setter[prop]; if (getter || setter) { descriptor.get = getter; descriptor.set = setter; delete descriptor.value; delete descriptor.writable; } // Preserve value, but remove it for the nativeToPseudo call. var value = descriptor.value; var hasValue = 'value' in descriptor; delete descriptor.value; var pseudoDescriptor = thisInterpreter.nativeToPseudo(descriptor); if (hasValue) { thisInterpreter.setProperty(pseudoDescriptor, 'value', value); } return pseudoDescriptor; }; this.setProperty(this.OBJECT, 'getOwnPropertyDescriptor', this.createNativeFunction(wrapper, false), Interpreter.NONENUMERABLE_DESCRIPTOR); wrapper = function(obj) { throwIfNullUndefined(obj); return thisInterpreter.getPrototype(obj); }; this.setProperty(this.OBJECT, 'getPrototypeOf', this.createNativeFunction(wrapper, false), Interpreter.NONENUMERABLE_DESCRIPTOR); wrapper = function(obj) { return Boolean(obj) && !obj.preventExtensions; }; this.setProperty(this.OBJECT, 'isExtensible', this.createNativeFunction(wrapper, false), Interpreter.NONENUMERABLE_DESCRIPTOR); wrapper = function(obj) { if (obj && obj.isObject) { obj.preventExtensions = true; } return obj; }; this.setProperty(this.OBJECT, 'preventExtensions', this.createNativeFunction(wrapper, false), Interpreter.NONENUMERABLE_DESCRIPTOR); // Instance methods on Object. this.setNativeFunctionPrototype(this.OBJECT, 'toString', Interpreter.Object.prototype.toString); this.setNativeFunctionPrototype(this.OBJECT, 'toLocaleString', Interpreter.Object.prototype.toString); this.setNativeFunctionPrototype(this.OBJECT, 'valueOf', Interpreter.Object.prototype.valueOf); wrapper = function(prop) { throwIfNullUndefined(this); if (!this.isObject) { return this.hasOwnProperty(prop); } return String(prop) in this.properties; }; this.setNativeFunctionPrototype(this.OBJECT, 'hasOwnProperty', wrapper); wrapper = function(prop) { throwIfNullUndefined(this); if (!this.isObject) { return this.propertyIsEnumerable(prop); } return Object.prototype.propertyIsEnumerable.call(this.properties, prop); }; this.setNativeFunctionPrototype(this.OBJECT, 'propertyIsEnumerable', wrapper); wrapper = function(obj) { while (true) { // Note, circular loops shouldn't be possible. obj = thisInterpreter.getPrototype(obj); if (!obj) { // No parent; reached the top. return false; } if (obj === this) { return true; } } }; this.setNativeFunctionPrototype(this.OBJECT, 'isPrototypeOf', wrapper); }; /** * Initialize the Array class. * @param {!Interpreter.Object} scope Global scope. */ Interpreter.prototype.initArray = function(scope) { var thisInterpreter = this; var wrapper; // Array constructor. wrapper = function(var_args) { if (thisInterpreter.calledWithNew()) { // Called as new Array(). var newArray = this; } else { // Called as Array(). var newArray = thisInterpreter.createObjectProto(thisInterpreter.ARRAY_PROTO); } var first = arguments[0]; if (arguments.length === 1 && typeof first === 'number') { if (isNaN(Interpreter.legalArrayLength(first))) { thisInterpreter.throwException(thisInterpreter.RANGE_ERROR, 'Invalid array length'); } newArray.properties.length = first; } else { for (var i = 0; i < arguments.length; i++) { newArray.properties[i] = arguments[i]; } newArray.properties.length = i; } return newArray; }; this.ARRAY = this.createNativeFunction(wrapper, true); this.ARRAY_PROTO = this.ARRAY.properties['prototype']; this.setProperty(scope, 'Array', this.ARRAY); // Static methods on Array. wrapper = function(obj) { return obj && obj.class === 'Array'; }; this.setProperty(this.ARRAY, 'isArray', this.createNativeFunction(wrapper, false), Interpreter.NONENUMERABLE_DESCRIPTOR); // Instance methods on Array. wrapper = function() { return Array.prototype.pop.call(this.properties); }; this.setNativeFunctionPrototype(this.ARRAY, 'pop', wrapper); wrapper = function(var_args) { return Array.prototype.push.apply(this.properties, arguments); }; this.setNativeFunctionPrototype(this.ARRAY, 'push', wrapper); wrapper = function() { return Array.prototype.shift.call(this.properties); }; this.setNativeFunctionPrototype(this.ARRAY, 'shift', wrapper); wrapper = function(var_args) { return Array.prototype.unshift.apply(this.properties, arguments); }; this.setNativeFunctionPrototype(this.ARRAY, 'unshift', wrapper); wrapper = function() { Array.prototype.reverse.call(this.properties); return this; }; this.setNativeFunctionPrototype(this.ARRAY, 'reverse', wrapper); wrapper = function(index, howmany /*, var_args*/) { var list = Array.prototype.splice.apply(this.properties, arguments); return thisInterpreter.arrayNativeToPseudo(list); }; this.setNativeFunctionPrototype(this.ARRAY, 'splice', wrapper); wrapper = function(opt_begin, opt_end) { var list = Array.prototype.slice.call(this.properties, opt_begin, opt_end); return thisInterpreter.arrayNativeToPseudo(list); }; this.setNativeFunctionPrototype(this.ARRAY, 'slice', wrapper); wrapper = function(opt_separator) { return Array.prototype.join.call(this.properties, opt_separator); }; this.setNativeFunctionPrototype(this.ARRAY, 'join', wrapper); wrapper = function(var_args) { var list = []; var length = 0; // Start by copying the current array. var iLength = thisInterpreter.getProperty(this, 'length'); for (var i = 0; i < iLength; i++) { if (thisInterpreter.hasProperty(this, i)) { var element = thisInterpreter.getProperty(this, i); list[length] = element; } length++; } // Loop through all arguments and copy them in. for (var i = 0; i < arguments.length; i++) { var value = arguments[i]; if (thisInterpreter.isa(value, thisInterpreter.ARRAY)) { var jLength = thisInterpreter.getProperty(value, 'length'); for (var j = 0; j < jLength; j++) { if (thisInterpreter.hasProperty(value, j)) { list[length] = thisInterpreter.getProperty(value, j); } length++; } } else { list[length] = value; } } return thisInterpreter.arrayNativeToPseudo(list); }; this.setNativeFunctionPrototype(this.ARRAY, 'concat', wrapper); wrapper = function(searchElement, opt_fromIndex) { return Array.prototype.indexOf.apply(this.properties, arguments); }; this.setNativeFunctionPrototype(this.ARRAY, 'indexOf', wrapper); wrapper = function(searchElement, opt_fromIndex) { return Array.prototype.lastIndexOf.apply(this.properties, arguments); }; this.setNativeFunctionPrototype(this.ARRAY, 'lastIndexOf', wrapper); wrapper = function() { Array.prototype.sort.call(this.properties); return this; }; this.setNativeFunctionPrototype(this.ARRAY, 'sort', wrapper); this.polyfills_.push( // Polyfill copied from: // developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/every "Object.defineProperty(Array.prototype, 'every',", "{configurable: true, writable: true, value:", "function(callbackfn, thisArg) {", "if (!this || typeof callbackfn !== 'function') throw TypeError();", "var T, k;", "var O = Object(this);", "var len = O.length >>> 0;", "if (arguments.length > 1) T = thisArg;", "k = 0;", "while (k < len) {", "if (k in O && !callbackfn.call(T, O[k], k, O)) return false;", "k++;", "}", "return true;", "}", "});", // Polyfill copied from: // developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/filter "Object.defineProperty(Array.prototype, 'filter',", "{configurable: true, writable: true, value:", "function(fun/*, thisArg*/) {", "if (this === void 0 || this === null || typeof fun !== 'function') throw TypeError();", "var t = Object(this);", "var len = t.length >>> 0;", "var res = [];", "var thisArg = arguments.length >= 2 ? arguments[1] : void 0;", "for (var i = 0; i < len; i++) {", "if (i in t) {", "var val = t[i];", "if (fun.call(thisArg, val, i, t)) res.push(val);", "}", "}", "return res;", "}", "});", // Polyfill copied from: // https://tc39.github.io/ecma262/#sec-array.prototype.find // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find "if (!Array.prototype.find) {", "Object.defineProperty(Array.prototype, 'find', {", "value: function(predicate) {", "if (this == null) {", "throw new TypeError('\"this\" is null or not defined');", "}", "var o = Object(this);", "var len = o.length >>> 0;", "if (typeof predicate !== 'function') {", "throw new TypeError('predicate must be a function');", "}", "var thisArg = arguments[1];", "var k = 0;", "while (k < len) {", "var kValue = o[k];", "if (predicate.call(thisArg, kValue, k, o)) {", "return kValue;", "}", "k++;", "}", "return undefined;", "},", "configurable: true,", "writable: true", "});", "}", // Poly fill copied from: // https://tc39.github.io/ecma262/#sec-array.prototype.findIndex // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex "if (!Array.prototype.findIndex) {", "Object.defineProperty(Array.prototype, 'findIndex', {", "value: function(predicate) {", "if (this == null) {", "throw new TypeError('\"this\" is null or not defined');", "}", "var o = Object(this);", "var len = o.length >>> 0;", "if (typeof predicate !== 'function') {", "throw new TypeError('predicate must be a function');", "}", "var thisArg = arguments[1];", "var k = 0;", "while (k < len) {", "var kValue = o[k];", "if (predicate.call(thisArg, kValue, k, o)) {", "return k;", "}", "k++;", "}", "return -1;", "},", "configurable: true,", "writable: true", "});", "}", // Polyfill copied from: // developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach "Object.defineProperty(Array.prototype, 'forEach',", "{configurable: true, writable: true, value:", "function(callback, thisArg) {", "if (!this || typeof callback !== 'function') throw TypeError();", "var T, k;", "var O = Object(this);", "var len = O.length >>> 0;", "if (arguments.length > 1) T = thisArg;", "k = 0;", "while (k < len) {", "if (k in O) callback.call(T, O[k], k, O);", "k++;", "}", "}", "});", // Polyfill copied from: // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes#Polyfill "Object.defineProperty(Array.prototype, 'includes', {", "value: function(searchElement, fromIndex) {", "if (this == null) {", "throw new TypeError('\"this\" is null or not defined');", "}", "// 1. Let O be ? ToObject(this value).", "var o = Object(this);", "// 2. Let len be ? ToLength(? Get(O, \"length\")).", "var len = o.length >>> 0;", "// 3. If len is 0, return false.", "if (len === 0) {", "return false;", "}", "// 4. Let n be ? ToInteger(fromIndex).", "// (If fromIndex is undefined, this step produces the value 0.)", "var n = fromIndex | 0;", "// 5. If n ≥ 0, then", "// a. Let k be n.", "// 6. Else n < 0,", "// a. Let k be len + n.", "// b. If k < 0, let k be 0.", "var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);", "function sameValueZero(x, y) {", "return x === y || (typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y));", "}", "// 7. Repeat, while k < len", "while (k < len) {", "// a. Let elementK be the result of ? Get(O, ! ToString(k)).", "// b. If SameValueZero(searchElement, elementK) is true, return true.", "if (sameValueZero(o[k], searchElement)) {", "return true;", "}", "// c. Increase k by 1. ", "k++;", "}", "// 8. Return false", "return false;", "}", "});", // Polyfill copied from: // developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/map "Object.defineProperty(Array.prototype, 'map',", "{configurable: true, writable: true, value:", "function(callback, thisArg) {", "if (!this || typeof callback !== 'function') new TypeError;", "var T, A, k;", "var O = Object(this);", "var len = O.length >>> 0;", "if (arguments.length > 1) T = thisArg;", "A = new Array(len);", "k = 0;", "while (k < len) {", "if (k in O) A[k] = callback.call(T, O[k], k, O);", "k++;", "}", "return A;", "}", "});", // Polyfill copied from: // developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce "Object.defineProperty(Array.prototype, 'reduce',", "{configurable: true, writable: true, value:", "function(callback /*, initialValue*/) {", "if (!this || typeof callback !== 'function') throw TypeError();", "var t = Object(this), len = t.length >>> 0, k = 0, value;", "if (arguments.length === 2) {", "value = arguments[1];", "} else {", "while (k < len && !(k in t)) k++;", "if (k >= len) {", "throw TypeError('Reduce of empty array with no initial value');", "}", "value = t[k++];", "}", "for (; k < len; k++) {", "if (k in t) value = callback(value, t[k], k, t);", "}", "return value;", "}", "});", // Polyfill copied from: // developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/ReduceRight "Object.defineProperty(Array.prototype, 'reduceRight',", "{configurable: true, writable: true, value:", "function(callback /*, initialValue*/) {", "if (null === this || 'undefined' === typeof this || 'function' !== typeof callback) throw TypeError();", "var t = Object(this), len = t.length >>> 0, k = len - 1, value;", "if (arguments.length >= 2) {", "value = arguments[1];", "} else {", "while (k >= 0 && !(k in t)) k--;", "if (k < 0) {", "throw TypeError('Reduce of empty array with no initial value');", "}", "value = t[k--];", "}", "for (; k >= 0; k--) {", "if (k in t) value = callback(value, t[k], k, t);", "}", "return value;", "}", "});", // Polyfill copied from: // developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/some "Object.defineProperty(Array.prototype, 'some',", "{configurable: true, writable: true, value:", "function(fun/*, thisArg*/) {", "if (!this || typeof fun !== 'function') throw TypeError();", "var t = Object(this);", "var len = t.length >>> 0;", "var thisArg = arguments.length >= 2 ? arguments[1] : void 0;", "for (var i = 0; i < len; i++) {", "if (i in t && fun.call(thisArg, t[i], i, t)) {", "return true;", "}", "}", "return false;", "}", "});", "(function() {", "var sort_ = Array.prototype.sort;", "Array.prototype.sort = function(opt_comp) {", // Fast native sort. "if (typeof opt_comp !== 'function') {", "return sort_.call(this);", "}", // Slow bubble sort. "for (var i = 0; i < this.length; i++) {", "var changes = 0;", "for (var j = 0; j < this.length - i - 1; j++) {", "if (opt_comp(this[j], this[j + 1]) > 0) {", "var swap = this[j];", "this[j] = this[j + 1];", "this[j + 1] = swap;", "changes++;", "}", "}", "if (!changes) break;", "}", "return this;", "};", "})();", "Object.defineProperty(Array.prototype, 'toLocaleString',", "{configurable: true, writable: true, value:", "function() {", "var out = [];", "for (var i = 0; i < this.length; i++) {", "out[i] = (this[i] === null || this[i] === undefined) ? '' : this[i].toLocaleString();", "}", "return out.join(',');", "}", "});", ""); }; /** * Initialize the String class. * @param {!Interpreter.Object} scope Global scope. */ Interpreter.prototype.initString = function(scope) { var thisInterpreter = this; var wrapper; // String constructor. wrapper = function(value) { value = String(value); if (thisInterpreter.calledWithNew()) { // Called as new String(). this.data = value; return this; } else { // Called as String(). return value; } }; this.STRING = this.createNativeFunction(wrapper, true); this.setProperty(scope, 'String', this.STRING); // Static methods on String. this.setProperty(this.STRING, 'fromCharCode', this.createNativeFunction(String.fromCharCode, false), Interpreter.NONENUMERABLE_DESCRIPTOR); // Instance methods on String. // Methods with exclusively primitive arguments. var functions = ['charAt', 'charCodeAt', 'concat', 'indexOf', 'lastIndexOf', 'slice', 'substr', 'substring', 'toLocaleLowerCase', 'toLocaleUpperCase', 'toLowerCase', 'toUpperCase', 'trim']; for (var i = 0; i < functions.length; i++) { this.setNativeFunctionPrototype(this.STRING, functions[i], String.prototype[functions[i]]); } wrapper = function(compareString, locales, options) { locales = locales ? thisInterpreter.pseudoToNative(locales) : undefined; options = options ? thisInterpreter.pseudoToNative(options) : undefined; return String(this).localeCompare(compareString, locales, options); }; this.setNativeFunctionPrototype(this.STRING, 'localeCompare', wrapper); wrapper = function(separator, limit) { if (thisInterpreter.isa(separator, thisInterpreter.REGEXP)) { separator = separator.data; } var jsList = String(this).split(separator, limit); return thisInterpreter.arrayNativeToPseudo(jsList); }; this.setNativeFunctionPrototype(this.STRING, 'split', wrapper); wrapper = function(regexp) { if (thisInterpreter.isa(regexp, thisInterpreter.REGEXP)) { regexp = regexp.data; } var m = String(this).match(regexp); return m && thisInterpreter.arrayNativeToPseudo(m); }; this.setNativeFunctionPrototype(this.STRING, 'match', wrapper); wrapper = function(regexp) { if (thisInterpreter.isa(regexp, thisInterpreter.REGEXP)) { regexp = regexp.data; } return String(this).search(regexp); }; this.setNativeFunctionPrototype(this.STRING, 'search', wrapper); wrapper = function(substr, newSubstr) { // Support for function replacements is the responsibility of a polyfill. if (thisInterpreter.isa(substr, thisInterpreter.REGEXP)) { substr = substr.data; } return String(this).replace(substr, newSubstr); }; this.setNativeFunctionPrototype(this.STRING, 'replace', wrapper); // Add a polyfill to handle replace's second argument being a function. this.polyfills_.push( "(function() {", "var replace_ = String.prototype.replace;", "String.prototype.replace = function(substr, newSubstr) {", "if (typeof newSubstr !== 'function') {", // string.replace(string|regexp, string) "return replace_.call(this, substr, newSubstr);", "}", "var str = this;", "if (substr instanceof RegExp) {", // string.replace(regexp, function) "var subs = [];", "var m = substr.exec(str);", "while (m) {", "m.push(m.index, str);", "var inject = newSubstr.apply(null, m);", "subs.push([m.index, m[0].length, inject]);", "m = substr.global ? substr.exec(str) : null;", "}", "for (var i = subs.length - 1; i >= 0; i--) {", "str = str.substring(0, subs[i][0]) + subs[i][2] + " + "str.substring(subs[i][0] + subs[i][1]);", "}", "} else {", // string.replace(string, function) "var i = str.indexOf(substr);", "if (i !== -1) {", "var inject = newSubstr(str.substr(i, substr.length), i, str);", "str = str.substring(0, i) + inject + " + "str.substring(i + substr.length);", "}", "}", "return str;", "};", "})();", // Polyfill copied from: // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith "if (!String.prototype.endsWith) {", "String.prototype.endsWith = function(search, this_len) {", "if (this_len === undefined || this_len > this.length) {", "this_len = this.length;", "}", "return this.substring(this_len - search.length, this_len) === search;", "};", "}", //Polyfill copied from: // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes "if (!String.prototype.includes) {", "String.prototype.includes = function(search, start) {", "'use strict';", "if (typeof start !== 'number') {", "start = 0;", "}", " ", "if (start + search.length > this.length) {", "return false;", "} else {", "return this.indexOf(search, start) !== -1;", "}", "};", "}", // Polyfill copied from: // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith "if (!String.prototype.startsWith) {", "String.prototype.startsWith = function(search, pos) {", "return this.substr(!pos || pos < 0 ? 0 : +pos, search.length) === search;", "};", "}", ""); }; /** * Initialize the Boolean class. * @param {!Interpreter.Object} scope Global scope. */ Interpreter.prototype.initBoolean = function(scope) { var thisInterpreter = this; var wrapper; // Boolean constructor. wrapper = function(value) { value = Boolean(value); if (thisInterpreter.calledWithNew()) { // Called as new Boolean(). this.data = value; return this; } else { // Called as Boolean(). return value; } }; this.BOOLEAN = this.createNativeFunction(wrapper, true); this.setProperty(scope, 'Boolean', this.BOOLEAN); }; /** * Initialize the Number class. * @param {!Interpreter.Object} scope Global scope. */ Interpreter.prototype.initNumber = function(scope) { var thisInterpreter = this; var wrapper; // Number constructor. wrapper = function(value) { value = Number(value); if (thisInterpreter.calledWithNew()) { // Called as new Number(). this.data = value; return this; } else { // Called as Number(). return value; } }; this.NUMBER = this.createNativeFunction(wrapper, true); this.setProperty(scope, 'Number', this.NUMBER); var numConsts = ['MAX_VALUE', 'MIN_VALUE', 'NaN', 'NEGATIVE_INFINITY', 'POSITIVE_INFINITY']; for (var i = 0; i < numConsts.length; i++) { this.setProperty(this.NUMBER, numConsts[i], Number[numConsts[i]], Interpreter.READONLY_NONENUMERABLE_DESCRIPTOR); } // Instance methods on Number. wrapper = function(fractionDigits) { try { return Number(this).toExponential(fractionDigits); } catch (e) { // Throws if fractionDigits isn't within 0-20. thisInterpreter.throwException(thisInterpreter.ERROR, e.message); } }; this.setNativeFunctionPrototype(this.NUMBER, 'toExponential', wrapper); wrapper = function(digits) { try { return Number(this).toFixed(digits); } catch (e) { // Throws if digits isn't within 0-20. thisInterpreter.throwException(thisInterpreter.ERROR, e.message); } }; this.setNativeFunctionPrototype(this.NUMBER, 'toFixed', wrapper); wrapper = function(precision) { try { return Number(this).toPrecision(precision); } catch (e) { // Throws if precision isn't within range (depends on implementation). thisInterpreter.throwException(thisInterpreter.ERROR, e.message); } }; this.setNativeFunctionPrototype(this.NUMBER, 'toPrecision', wrapper); wrapper = function(radix) { try { return Number(this).toString(radix); } catch (e) { // Throws if radix isn't within 2-36. thisInterpreter.throwException(thisInterpreter.ERROR, e.message); } }; this.setNativeFunctionPrototype(this.NUMBER, 'toString', wrapper); wrapper = function(locales, options) { locales = locales ? thisInterpreter.pseudoToNative(locales) : undefined; options = options ? thisInterpreter.pseudoToNative(options) : undefined; return Number(this).toLocaleString(locales, options); }; this.setNativeFunctionPrototype(this.NUMBER, 'toLocaleString', wrapper); }; /** * Initialize the Date class. * @param {!Interpreter.Object} scope Global scope. */ Interpreter.prototype.initDate = function(scope) { var thisInterpreter = this; var wrapper; // Date constructor. wrapper = function(value, var_args) { if (!thisInterpreter.calledWithNew()) { // Called as Date(). // Calling Date() as a function returns a string, no arguments are heeded. return Date(); } // Called as new Date(). var args = [null].concat(Array.from(arguments)); this.data = new (Function.prototype.bind.apply(Date, args)); return this; }; this.DATE = this.createNativeFunction(wrapper, true); this.DATE_PROTO = this.DATE.properties['prototype']; this.setProperty(scope, 'Date', this.DATE); // Static methods on Date. this.setProperty(this.DATE, 'now', this.createNativeFunction(Date.now, false), Interpreter.NONENUMERABLE_DESCRIPTOR); this.setProperty(this.DATE, 'parse', this.createNativeFunction(Date.parse, false), Interpreter.NONENUMERABLE_DESCRIPTOR); this.setProperty(this.DATE, 'UTC', this.createNativeFunction(Date.UTC, false), Interpreter.NONENUMERABLE_DESCRIPTOR); // Instance methods on Date. var functions = ['getDate', 'getDay', 'getFullYear', 'getHours', 'getMilliseconds', 'getMinutes', 'getMonth', 'getSeconds', 'getTime', 'getTimezoneOffset', 'getUTCDate', 'getUTCDay', 'getUTCFullYear', 'getUTCHours', 'getUTCMilliseconds', 'getUTCMinutes', 'getUTCMonth', 'getUTCSeconds', 'getYear', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds', 'setMinutes', 'setMonth', 'setSeconds', 'setTime', 'setUTCDate', 'setUTCFullYear', 'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds', 'setYear', 'toDateString', 'toISOString', 'toJSON', 'toGMTString', 'toLocaleDateString', 'toLocaleString', 'toLocaleTimeString', 'toTimeString', 'toUTCString']; for (var i = 0; i < functions.length; i++) { wrapper = (function(nativeFunc) { return function(var_args) { var args = []; for (var i = 0; i < arguments.length; i++) { args[i] = thisInterpreter.pseudoToNative(arguments[i]); } return this.data[nativeFunc].apply(this.data, args); }; })(functions[i]); this.setNativeFunctionPrototype(this.DATE, functions[i], wrapper); } }; /** * Initialize Regular Expression object. * @param {!Interpreter.Object} scope Global scope. */ Interpreter.prototype.initRegExp = function(scope) { var thisInterpreter = this; var wrapper; // RegExp constructor. wrapper = function(pattern, flags) { if (thisInterpreter.calledWithNew()) { // Called as new RegExp(). var rgx = this; } else { // Called as RegExp(). var rgx = thisInterpreter.createObjectProto(thisInterpreter.REGEXP_PROTO); } pattern = pattern ? pattern.toString() : ''; flags = flags ? flags.toString() : ''; thisInterpreter.populateRegExp(rgx, new RegExp(pattern, flags)); return rgx; }; this.REGEXP = this.createNativeFunction(wrapper, true); this.REGEXP_PROTO = this.REGEXP.properties['prototype']; this.setProperty(scope, 'RegExp', this.REGEXP); this.setProperty(this.REGEXP.properties['prototype'], 'global', undefined, Interpreter.READONLY_NONENUMERABLE_DESCRIPTOR); this.setProperty(this.REGEXP.properties['prototype'], 'ignoreCase', undefined, Interpreter.READONLY_NONENUMERABLE_DESCRIPTOR); this.setProperty(this.REGEXP.properties['prototype'], 'multiline', undefined, Interpreter.READONLY_NONENUMERABLE_DESCRIPTOR); this.setProperty(this.REGEXP.properties['prototype'], 'source', '(?:)', Interpreter.READONLY_NONENUMERABLE_DESCRIPTOR); wrapper = function(str) { return this.data.test(str); }; this.setNativeFunctionPrototype(this.REGEXP, 'test', wrapper); wrapper = function(str) { str = str.toString(); // Get lastIndex from wrapped regex, since this is settable. this.data.lastIndex = Number(thisInterpreter.getProperty(this, 'lastIndex')); var match = this.data.exec(str); thisInterpreter.setProperty(this, 'lastIndex', this.data.lastIndex); if (match) { var result = thisInterpreter.createObjectProto(thisInterpreter.ARRAY_PROTO); for (var i = 0; i < match.length; i++) { thisInterpreter.setProperty(result, i, match[i]); } // match has additional properties. thisInterpreter.setProperty(result, 'index', match.index); thisInterpreter.setProperty(result, 'input', match.input); return result; } return null; }; this.setNativeFunctionPrototype(this.REGEXP, 'exec', wrapper); }; /** * Initialize the Error class. * @param {!Interpreter.Object} scope Global scope. */ Interpreter.prototype.initError = function(scope) { var thisInterpreter = this; // Error constructor. this.ERROR = this.createNativeFunction(function(opt_message) { if (thisInterpreter.calledWithNew()) { // Called as new Error(). var newError = this; } else { // Called as Error(). var newError = thisInterpreter.createObject(thisInterpreter.ERROR); } if (opt_message) { thisInterpreter.setProperty(newError, 'message', String(opt_message), Interpreter.NONENUMERABLE_DESCRIPTOR); } return newError; }, true); this.setProperty(scope, 'Error', this.ERROR); this.setProperty(this.ERROR.properties['prototype'], 'message', '', Interpreter.NONENUMERABLE_DESCRIPTOR); this.setProperty(this.ERROR.properties['prototype'], 'name', 'Error', Interpreter.NONENUMERABLE_DESCRIPTOR); var createErrorSubclass = function(name) { var constructor = thisInterpreter.createNativeFunction( function(opt_message) { if (thisInterpreter.calledWithNew()) { // Called as new XyzError(). var newError = this; } else { // Called as XyzError(). var newError = thisInterpreter.createObject(constructor); } if (opt_message) { thisInterpreter.setProperty(newError, 'message', String(opt_message), Interpreter.NONENUMERABLE_DESCRIPTOR); } return newError; }, true); thisInterpreter.setProperty(constructor, 'prototype', thisInterpreter.createObject(thisInterpreter.ERROR)); thisInterpreter.setProperty(constructor.properties['prototype'], 'name', name, Interpreter.NONENUMERABLE_DESCRIPTOR); thisInterpreter.setProperty(scope, name, constructor); return constructor; }; this.EVAL_ERROR = createErrorSubclass('EvalError'); this.RANGE_ERROR = createErrorSubclass('RangeError'); this.REFERENCE_ERROR = createErrorSubclass('ReferenceError'); this.SYNTAX_ERROR = createErrorSubclass('SyntaxError'); this.TYPE_ERROR = createErrorSubclass('TypeError'); this.URI_ERROR = createErrorSubclass('URIError'); }; /** * Initialize Math object. * @param {!Interpreter.Object} scope Global scope. */ Interpreter.prototype.initMath = function(scope) { var thisInterpreter = this; var myMath = this.createObjectProto(this.OBJECT_PROTO); this.setProperty(scope, 'Math', myMath); var mathConsts = ['E', 'LN2', 'LN10', 'LOG2E', 'LOG10E', 'PI', 'SQRT1_2', 'SQRT2']; for (var i = 0; i < mathConsts.length; i++) { this.setProperty(myMath, mathConsts[i], Math[mathConsts[i]], Interpreter.READONLY_NONENUMERABLE_DESCRIPTOR); } var numFunctions = ['abs', 'acos', 'asin', 'atan', 'atan2', 'ceil', 'cos', 'exp', 'floor', 'log', 'max', 'min', 'pow', 'random', 'round', 'sin', 'sqrt', 'tan']; for (var i = 0; i < numFunctions.length; i++) { this.setProperty(myMath, numFunctions[i], this.createNativeFunction(Math[numFunctions[i]], false), Interpreter.NONENUMERABLE_DESCRIPTOR); } }; /** * Initialize JSON object. * @param {!Interpreter.Object} scope Global scope. */ Interpreter.prototype.initJSON = function(scope) { var thisInterpreter = this; var myJSON = thisInterpreter.createObjectProto(this.OBJECT_PROTO); this.setProperty(scope, 'JSON', myJSON); var wrapper = function(text) { try { var nativeObj = JSON.parse(text.toString()); } catch (e) { thisInterpreter.throwException(thisInterpreter.SYNTAX_ERROR, e.message); } return thisInterpreter.nativeToPseudo(nativeObj); }; this.setProperty(myJSON, 'parse', this.createNativeFunction(wrapper, false)); wrapper = function(value) { var nativeObj = thisInterpreter.pseudoToNative(value); try { var str = JSON.stringify(nativeObj); } catch (e) { thisInterpreter.throwException(thisInterpreter.TYPE_ERROR, e.message); } return str; }; this.setProperty(myJSON, 'stringify', this.createNativeFunction(wrapper, false)); }; /** * Is an object of a certain class? * @param {Interpreter.Value} child Object to check. * @param {Interpreter.Object} constructor Constructor of object. * @return {boolean} True if object is the class or inherits from it. * False otherwise. */ Interpreter.prototype.isa = function(child, constructor) { if (child === null || child === undefined || !constructor) { return false; } var proto = constructor.properties['prototype']; if (child === proto) { return true; } // The first step up the prototype chain is harder since the child might be // a primitive value. Subsequent steps can just follow the .proto property. child = this.getPrototype(child); while (child) { if (child === proto) { return true; } child = child.proto; } return false; }; /** * Is a value a legal integer for an array length? * @param {Interpreter.Value} x Value to check. * @return {number} Zero, or a positive integer if the value can be * converted to such. NaN otherwise. */ Interpreter.legalArrayLength = function(x) { var n = x >>> 0; // Array length must be between 0 and 2^32-1 (inclusive). return (n === Number(x)) ? n : NaN; }; /** * Is a value a legal integer for an array index? * @param {Interpreter.Value} x Value to check. * @return {number} Zero, or a positive integer if the value can be * converted to such. NaN otherwise. */ Interpreter.legalArrayIndex = function(x) { var n = x >>> 0; // Array index cannot be 2^32-1, otherwise length would be 2^32. // 0xffffffff is 2^32-1. return (String(n) === String(x) && n !== 0xffffffff) ? n : NaN; }; /** * Typedef for JS values. * @typedef {!Interpreter.Object|boolean|number|string|undefined|null} */ Interpreter.Value; /** * Class for an object. * @param {Interpreter.Object} proto Prototype object or null. * @constructor */ Interpreter.Object = function(proto) { this.getter = Object.create(null); this.setter = Object.create(null); this.properties = Object.create(null); this.proto = proto; }; /** @type {Interpreter.Object} */ Interpreter.Object.prototype.proto = null; /** @type {boolean} */ Interpreter.Object.prototype.isObject = true; /** @type {string} */ Interpreter.Object.prototype.class = 'Object'; /** @type {Date|RegExp|boolean|number|string|undefined|null} */ Interpreter.Object.prototype.data = null; /** * Convert this object into a string. * @return {string} String value. * @override */ Interpreter.Object.prototype.toString = function() { if (this.class === 'Array') { // Array var cycles = Interpreter.toStringCycles_; cycles.push(this); try { var strs = []; for (var i = 0; i < this.properties.length; i++) { var value = this.properties[i]; strs[i] = (value && value.isObject && cycles.indexOf(value) !== -1) ? '...' : value; } } finally { cycles.pop(); } return strs.join(','); } if (this.class === 'Error') { var cycles = Interpreter.toStringCycles_; if (cycles.indexOf(this) !== -1) { return '[object Error]'; } var name, message; // Bug: Does not support getters and setters for name or message. var obj = this; do { if ('name' in obj.properties) { name = obj.properties['name']; break; } } while ((obj = obj.proto)); var obj = this; do { if ('message' in obj.properties) { message = obj.properties['message']; break; } } while ((obj = obj.proto)); cycles.push(this); try { name = name && name.toString(); message = message && message.toString(); } finally { cycles.pop(); } return message ? name + ': ' + message : String(name); } // RegExp, Date, and boxed primitives. if (this.data !== null) { return String(this.data); } return '[object ' + this.class + ']'; }; /** * Return the object's value. * @return {Interpreter.Value} Value. * @override */ Interpreter.Object.prototype.valueOf = function() { if (this.data === undefined || this.data === null || this.data instanceof RegExp) { return this; // An Object. } if (this.data instanceof Date) { return this.data.valueOf(); // Milliseconds. } return /** @type {(boolean|number|string)} */ (this.data); // Boxed primitive. }; /** * Create a new data object based on a constructor's prototype. * @param {Interpreter.Object} constructor Parent constructor function, * or null if scope object. * @return {!Interpreter.Object} New data object. */ Interpreter.prototype.createObject = function(constructor) { return this.createObjectProto(constructor && constructor.properties['prototype']); }; /** * Create a new data object based on a prototype. * @param {Interpreter.Object} proto Prototype object. * @return {!Interpreter.Object} New data object. */ Interpreter.prototype.createObjectProto = function(proto) { if (typeof proto !== 'object') { throw Error('Non object prototype'); } var obj = new Interpreter.Object(proto); // Functions have prototype objects. if (this.isa(obj, this.FUNCTION)) { this.setProperty(obj, 'prototype', this.createObjectProto(this.OBJECT_PROTO || null)); obj.class = 'Function'; } // Arrays have length. if (this.isa(obj, this.ARRAY)) { this.setProperty(obj, 'length', 0, {configurable: false, enumerable: false, writable: true}); obj.class = 'Array'; } if (this.isa(obj, this.ERROR)) { obj.class = 'Error'; } return obj; }; /** * Initialize a pseudo regular expression object based on a native regular * expression object. * @param {!Interpreter.Object} pseudoRegexp The existing object to set. * @param {!RegExp} nativeRegexp The native regular expression. */ Interpreter.prototype.populateRegExp = function(pseudoRegexp, nativeRegexp) { pseudoRegexp.data = nativeRegexp; // lastIndex is settable, all others are read-only attributes this.setProperty(pseudoRegexp, 'lastIndex', nativeRegexp.lastIndex, Interpreter.NONENUMERABLE_DESCRIPTOR); this.setProperty(pseudoRegexp, 'source', nativeRegexp.source, Interpreter.READONLY_NONENUMERABLE_DESCRIPTOR); this.setProperty(pseudoRegexp, 'global', nativeRegexp.global, Interpreter.READONLY_NONENUMERABLE_DESCRIPTOR); this.setProperty(pseudoRegexp, 'ignoreCase', nativeRegexp.ignoreCase, Interpreter.READONLY_NONENUMERABLE_DESCRIPTOR); this.setProperty(pseudoRegexp, 'multiline', nativeRegexp.multiline, Interpreter.READONLY_NONENUMERABLE_DESCRIPTOR); }; /** * Create a new function. * @param {!Object} node AST node defining the function. * @param {!Object} scope Parent scope. * @return {!Interpreter.Object} New function. */ Interpreter.prototype.createFunction = function(node, scope) { var func = this.createObjectProto(this.FUNCTION_PROTO); func.parentScope = scope; func.node = node; this.setProperty(func, 'length', func.node['params'].length, Interpreter.READONLY_DESCRIPTOR); return func; }; /** * Create a new native function. * @param {!Function} nativeFunc JavaScript function. * @param {boolean=} opt_constructor If true, the function's * prototype will have its constructor property set to the function. * If false, the function cannot be called as a constructor (e.g. escape). * Defaults to undefined. * @return {!Interpreter.Object} New function. */ Interpreter.prototype.createNativeFunction = function(nativeFunc, opt_constructor) { var func = this.createObjectProto(this.FUNCTION_PROTO); func.nativeFunc = nativeFunc; nativeFunc.id = this.functionCounter_++; this.setProperty(func, 'length', nativeFunc.length, Interpreter.READONLY_DESCRIPTOR); if (opt_constructor) { this.setProperty(func.properties['prototype'], 'constructor', func, Interpreter.NONENUMERABLE_DESCRIPTOR); } else if (opt_constructor === false) { func.illegalConstructor = true; this.setProperty(func, 'prototype', undefined); } return func; }; /** * Create a new native asynchronous function. * @param {!Function} asyncFunc JavaScript function. * @return {!Interpreter.Object} New function. */ Interpreter.prototype.createAsyncFunction = function(asyncFunc) { var func = this.createObjectProto(this.FUNCTION_PROTO); func.asyncFunc = asyncFunc; asyncFunc.id = this.functionCounter_++; this.setProperty(func, 'length', asyncFunc.length, Interpreter.READONLY_DESCRIPTOR); return func; }; /** * Converts from a native JS object or value to a JS interpreter object. * Can handle JSON-style values, does NOT handle cycles. * @param {*} nativeObj The native JS object to be converted. * @return {Interpreter.Value} The equivalent JS interpreter object. */ Interpreter.prototype.nativeToPseudo = function(nativeObj) { if ((typeof nativeObj !== 'object' && typeof nativeObj !== 'function') || nativeObj === null) { return nativeObj; } if (nativeObj instanceof RegExp) { var pseudoRegexp = this.createObjectProto(this.REGEXP_PROTO); this.populateRegExp(pseudoRegexp, nativeObj); return pseudoRegexp; } if (nativeObj instanceof Date) { var pseudoDate = this.createObjectProto(this.DATE_PROTO); pseudoDate.data = nativeObj; return pseudoDate; } if (nativeObj instanceof Function) { var interpreter = this; var wrapper = function() { return interpreter.nativeToPseudo( nativeObj.apply(interpreter, Array.prototype.slice.call(arguments) .map(function(i) { return interpreter.pseudoToNative(i); }) ) ); }; return this.createNativeFunction(wrapper, undefined); } var pseudoObj; if (Array.isArray(nativeObj)) { // Array. pseudoObj = this.createObjectProto(this.ARRAY_PROTO); for (var i = 0; i < nativeObj.length; i++) { if (i in nativeObj) { this.setProperty(pseudoObj, i, this.nativeToPseudo(nativeObj[i])); } } } else { // Object. pseudoObj = this.createObjectProto(this.OBJECT_PROTO); for (var key in nativeObj) { this.setProperty(pseudoObj, key, this.nativeToPseudo(nativeObj[key])); } } return pseudoObj; }; /** * Converts from a JS interpreter object to native JS object. * Can handle JSON-style values, plus cycles. * @param {Interpreter.Value} pseudoObj The JS interpreter object to be * converted. * @param {Object=} opt_cycles Cycle detection (used in recursive calls). * @return {*} The equivalent native JS object or value. */ Interpreter.prototype.pseudoToNative = function(pseudoObj, opt_cycles) { if ((typeof pseudoObj !== 'object' && typeof pseudoObj !== 'function') || pseudoObj === null) { return pseudoObj; } if (this.isa(pseudoObj, this.REGEXP)) { // Regular expression. return pseudoObj.data; } if (this.isa(pseudoObj, this.DATE)) { // Date. return pseudoObj.data; } var cycles = opt_cycles || { pseudo: [], native: [] }; var i = cycles.pseudo.indexOf(pseudoObj); if (i !== -1) { return cycles.native[i]; } cycles.pseudo.push(pseudoObj); var nativeObj; if (this.isa(pseudoObj, this.ARRAY)) { // Array. nativeObj = []; cycles.native.push(nativeObj); var length = this.getProperty(pseudoObj, 'length'); for (var i = 0; i < length; i++) { if (this.hasProperty(pseudoObj, i)) { nativeObj[i] = this.pseudoToNative(this.getProperty(pseudoObj, i), cycles); } } } else { // Object. nativeObj = {}; cycles.native.push(nativeObj); var val; for (var key in pseudoObj.properties) { val = pseudoObj.properties[key]; nativeObj[key] = this.pseudoToNative(val, cycles); } } cycles.pseudo.pop(); cycles.native.pop(); return nativeObj; }; /** * Converts from a native JS array to a JS interpreter array. * Does handle non-numeric properties (like str.match's index prop). * Does NOT recurse into the array's contents. * @param {!Array} nativeArray The JS array to be converted. * @return {!Interpreter.Object} The equivalent JS interpreter array. */ Interpreter.prototype.arrayNativeToPseudo = function(nativeArray) { var pseudoArray = this.createObjectProto(this.ARRAY_PROTO); var props = Object.getOwnPropertyNames(nativeArray); for (var i = 0; i < props.length; i++) { this.setProperty(pseudoArray, props[i], nativeArray[props[i]]); } return pseudoArray; }; /** * Converts from a JS interpreter array to native JS array. * Does handle non-numeric properties (like str.match's index prop). * Does NOT recurse into the array's contents. * @param {!Interpreter.Object} pseudoArray The JS interpreter array, * or JS interpreter object pretending to be an array. * @return {!Array} The equivalent native JS array. */ Interpreter.prototype.arrayPseudoToNative = function(pseudoArray) { var nativeArray = []; for (var key in pseudoArray.properties) { nativeArray[key] = this.getProperty(pseudoArray, key); } // pseudoArray might be an object pretending to be an array. In this case // it's possible that length is non-existent, invalid, or smaller than the // largest defined numeric property. Set length explicitly here. nativeArray.length = Interpreter.legalArrayLength( this.getProperty(pseudoArray, 'length')) || 0; return nativeArray; }; /** * Look up the prototype for this value. * @param {Interpreter.Value} value Data object. * @return {Interpreter.Object} Prototype object, null if none. */ Interpreter.prototype.getPrototype = function(value) { switch (typeof value) { case 'number': return this.NUMBER.properties['prototype']; case 'boolean': return this.BOOLEAN.properties['prototype']; case 'string': return this.STRING.properties['prototype']; } if (value) { return value.proto; } return null; }; /** * Fetch a property value from a data object. * @param {Interpreter.Value} obj Data object. * @param {Interpreter.Value} name Name of property. * @param {Acorn AST Node} node Node that triggered this function. Used by Bitburner for getting error line numbers * @return {Interpreter.Value} Property value (may be undefined). */ Interpreter.prototype.getProperty = function(obj, name, node) { name = String(name); if (obj === undefined || obj === null) { let lineNum; if (node != null && node.loc != null && node.loc.start != null) { lineNum = node.loc.start.line; } this.throwException(this.TYPE_ERROR, "Cannot read property '" + name + "' of " + obj, lineNum); } if (name === 'length') { // Special cases for magic length property. if (this.isa(obj, this.STRING)) { return String(obj).length; } } else if (name.charCodeAt(0) < 0x40) { // Might have numbers in there? // Special cases for string array indexing if (this.isa(obj, this.STRING)) { var n = Interpreter.legalArrayIndex(name); if (!isNaN(n) && n < String(obj).length) { return String(obj)[n]; } } } do { if (obj.properties && name in obj.properties) { var getter = obj.getter[name]; if (getter) { // Flag this function as being a getter and thus needing immediate // execution (rather than being the value of the property). getter.isGetter = true; return getter; } return obj.properties[name]; } } while ((obj = this.getPrototype(obj))); return undefined; }; /** * Does the named property exist on a data object. * @param {Interpreter.Value} obj Data object. * @param {Interpreter.Value} name Name of property. * @return {boolean} True if property exists. */ Interpreter.prototype.hasProperty = function(obj, name) { if (!obj.isObject) { throw TypeError('Primitive data type has no properties'); } name = String(name); if (name === 'length' && this.isa(obj, this.STRING)) { return true; } if (this.isa(obj, this.STRING)) { var n = Interpreter.legalArrayIndex(name); if (!isNaN(n) && n < String(obj).length) { return true; } } do { if (obj.properties && name in obj.properties) { return true; } } while ((obj = this.getPrototype(obj))); return false; }; /** * Set a property value on a data object. * @param {!Interpreter.Object} obj Data object. * @param {Interpreter.Value} name Name of property. * @param {Interpreter.Value} value New property value. * Use Interpreter.VALUE_IN_DESCRIPTOR if value is handled by * descriptor instead. * @param {Object=} opt_descriptor Optional descriptor object. * @return {!Interpreter.Object|undefined} Returns a setter function if one * needs to be called, otherwise undefined. */ Interpreter.prototype.setProperty = function(obj, name, value, opt_descriptor) { name = String(name); if (obj === undefined || obj === null) { this.throwException(this.TYPE_ERROR, "Cannot set property '" + name + "' of " + obj); } if (opt_descriptor && ('get' in opt_descriptor || 'set' in opt_descriptor) && ('value' in opt_descriptor || 'writable' in opt_descriptor)) { this.throwException(this.TYPE_ERROR, 'Invalid property descriptor. ' + 'Cannot both specify accessors and a value or writable attribute'); } var strict = !this.stateStack || this.getScope().strict; if (!obj.isObject) { if (strict) { this.throwException(this.TYPE_ERROR, "Can't create property '" + name + "' on '" + obj + "'"); } return; } if (this.isa(obj, this.STRING)) { var n = Interpreter.legalArrayIndex(name); if (name === 'length' || (!isNaN(n) && n < String(obj).length)) { // Can't set length or letters on String objects. if (strict) { this.throwException(this.TYPE_ERROR, "Cannot assign to read only " + "property '" + name + "' of String '" + obj.data + "'"); } return; } } if (obj.class === 'Array') { // Arrays have a magic length variable that is bound to the elements. var length = obj.properties.length; var i; if (name === 'length') { // Delete elements if length is smaller. if (opt_descriptor) { if (!('value' in opt_descriptor)) { return; } value = opt_descriptor.value; } value = Interpreter.legalArrayLength(value); if (isNaN(value)) { this.throwException(this.RANGE_ERROR, 'Invalid array length'); } if (value < length) { for (i in obj.properties) { i = Interpreter.legalArrayIndex(i); if (!isNaN(i) && value <= i) { delete obj.properties[i]; } } } } else if (!isNaN(i = Interpreter.legalArrayIndex(name))) { // Increase length if this index is larger. obj.properties.length = Math.max(length, i + 1); } } if (obj.preventExtensions && !(name in obj.properties)) { if (strict) { this.throwException(this.TYPE_ERROR, "Can't add property '" + name + "', object is not extensible"); } return; } if (opt_descriptor) { // Define the property. if ('get' in opt_descriptor) { if (opt_descriptor.get) { obj.getter[name] = opt_descriptor.get; } else { delete obj.getter[name]; } } if ('set' in opt_descriptor) { if (opt_descriptor.set) { obj.setter[name] = opt_descriptor.set; } else { delete obj.setter[name]; } } var descriptor = {}; if ('configurable' in opt_descriptor) { descriptor.configurable = opt_descriptor.configurable; } if ('enumerable' in opt_descriptor) { descriptor.enumerable = opt_descriptor.enumerable; } if ('writable' in opt_descriptor) { descriptor.writable = opt_descriptor.writable; delete obj.getter[name]; delete obj.setter[name]; } if ('value' in opt_descriptor) { descriptor.value = opt_descriptor.value; delete obj.getter[name]; delete obj.setter[name]; } else if (value !== Interpreter.VALUE_IN_DESCRIPTOR) { descriptor.value = value; delete obj.getter[name]; delete obj.setter[name]; } try { Object.defineProperty(obj.properties, name, descriptor); } catch (e) { this.throwException(this.TYPE_ERROR, 'Cannot redefine property: ' + name); } } else { // Set the property. if (value === Interpreter.VALUE_IN_DESCRIPTOR) { throw ReferenceError('Value not specified.'); } // Determine the parent (possibly self) where the property is defined. var defObj = obj; while (!(name in defObj.properties)) { defObj = this.getPrototype(defObj); if (!defObj) { // This is a new property. defObj = obj; break; } } if (defObj.setter && defObj.setter[name]) { return defObj.setter[name]; } if (defObj.getter && defObj.getter[name]) { if (strict) { this.throwException(this.TYPE_ERROR, "Cannot set property '" + name + "' of object '" + obj + "' which only has a getter"); } } else { // No setter, simple assignment. try { obj.properties[name] = value; } catch (e) { if (strict) { this.throwException(this.TYPE_ERROR, "Cannot assign to read only " + "property '" + name + "' of object '" + obj + "'"); } } } } }; /** * Convenience method for adding a native function as a non-enumerable property * onto an object's prototype. * @param {!Interpreter.Object} obj Data object. * @param {Interpreter.Value} name Name of property. * @param {!Function} wrapper Function object. */ Interpreter.prototype.setNativeFunctionPrototype = function(obj, name, wrapper) { this.setProperty(obj.properties['prototype'], name, this.createNativeFunction(wrapper, false), Interpreter.NONENUMERABLE_DESCRIPTOR); }; /** * Returns the current scope from the stateStack. * @return {!Interpreter.Object} Current scope dictionary. */ Interpreter.prototype.getScope = function() { var scope = this.stateStack[this.stateStack.length - 1].scope; if (!scope) { throw Error('No scope found.'); } return scope; }; /** * Create a new scope dictionary. * @param {!Object} node AST node defining the scope container * (e.g. a function). * @param {Interpreter.Object} parentScope Scope to link to. * @return {!Interpreter.Object} New scope. */ Interpreter.prototype.createScope = function(node, parentScope) { var scope = this.createObjectProto(null); scope.parentScope = parentScope; if (!parentScope) { this.initGlobalScope(scope); } this.populateScope_(node, scope); // Determine if this scope starts with 'use strict'. scope.strict = false; if (parentScope && parentScope.strict) { scope.strict = true; } else { var firstNode = node['body'] && node['body'][0]; if (firstNode && firstNode.expression && firstNode.expression['type'] === 'Literal' && firstNode.expression.value === 'use strict') { scope.strict = true; } } return scope; }; /** * Create a new special scope dictionary. Similar to createScope(), but * doesn't assume that the scope is for a function body. * This is used for 'catch' clauses and 'with' statements. * @param {!Interpreter.Object} parentScope Scope to link to. * @param {Interpreter.Object=} opt_scope Optional object to transform into * scope. * @return {!Interpreter.Object} New scope. */ Interpreter.prototype.createSpecialScope = function(parentScope, opt_scope) { if (!parentScope) { throw Error('parentScope required'); } var scope = opt_scope || this.createObjectProto(null); scope.parentScope = parentScope; scope.strict = parentScope.strict; return scope; }; /** * Retrieves a value from the scope chain. * @param {string} name Name of variable. * @param {Acorn AST Node} node Node that triggered this function. Used by Bitburner for getting error line number * @return {Interpreter.Value} Any value. * May be flagged as being a getter and thus needing immediate execution * (rather than being the value of the property). */ Interpreter.prototype.getValueFromScope = function(name, node) { var scope = this.getScope(); while (scope && scope !== this.global) { if (name in scope.properties) { return scope.properties[name]; } scope = scope.parentScope; } // The root scope is also an object which has inherited properties and // could also have getters. if (scope === this.global && this.hasProperty(scope, name)) { return this.getProperty(scope, name); } // Typeof operator is unique: it can safely look at non-defined variables. var prevNode = this.stateStack[this.stateStack.length - 1].node; if (prevNode['type'] === 'UnaryExpression' && prevNode['operator'] === 'typeof') { return undefined; } var lineNum; if (node != null && node.loc != null && node.loc.start != null) { lineNum = node.loc.start.line; } this.throwException(this.REFERENCE_ERROR, name + ' is not defined', lineNum); }; /** * Sets a value to the current scope. * @param {string} name Name of variable. * @param {Interpreter.Value} value Value. * @return {!Interpreter.Object|undefined} Returns a setter function if one * needs to be called, otherwise undefined. */ Interpreter.prototype.setValueToScope = function(name, value) { var scope = this.getScope(); var strict = scope.strict; while (scope && scope !== this.global) { if (name in scope.properties) { scope.properties[name] = value; return undefined; } scope = scope.parentScope; } // The root scope is also an object which has readonly properties and // could also have setters. if (scope === this.global && (!strict || this.hasProperty(scope, name))) { return this.setProperty(scope, name, value); } this.throwException(this.REFERENCE_ERROR, name + ' is not defined'); }; /** * Create a new scope for the given node. * @param {!Object} node AST node (program or function). * @param {!Interpreter.Object} scope Scope dictionary to populate. * @private */ Interpreter.prototype.populateScope_ = function(node, scope) { if (node['type'] === 'VariableDeclaration') { for (var i = 0; i < node['declarations'].length; i++) { this.setProperty(scope, node['declarations'][i]['id']['name'], undefined, Interpreter.VARIABLE_DESCRIPTOR); } } else if (node['type'] === 'FunctionDeclaration') { this.setProperty(scope, node['id']['name'], this.createFunction(node, scope), Interpreter.VARIABLE_DESCRIPTOR); return; // Do not recurse into function. } else if (node['type'] === 'FunctionExpression') { return; // Do not recurse into function. } else if (node['type'] === 'ExpressionStatement') { return; // Expressions can't contain variable/function declarations. } var nodeClass = node['constructor']; for (var name in node) { var prop = node[name]; if (prop && typeof prop === 'object') { if (Array.isArray(prop)) { for (var i = 0; i < prop.length; i++) { if (prop[i] && prop[i].constructor === nodeClass) { this.populateScope_(prop[i], scope); } } } else { if (prop.constructor === nodeClass) { this.populateScope_(prop, scope); } } } } }; /** * Remove start and end values from AST, or set start and end values to a * constant value. Used to remove highlighting from polyfills and to set * highlighting in an eval to cover the entire eval expression. * @param {!Object} node AST node. * @param {number=} start Starting character of all nodes, or undefined. * @param {number=} end Ending character of all nodes, or undefined. * @private */ Interpreter.prototype.stripLocations_ = function(node, start, end) { if (start) { node['start'] = start; } else { delete node['start']; } if (end) { node['end'] = end; } else { delete node['end']; } for (var name in node) { if (node.hasOwnProperty(name)) { var prop = node[name]; if (prop && typeof prop === 'object') { this.stripLocations_(prop, start, end); } } } }; /** * Is the current state directly being called with as a construction with 'new'. * @return {boolean} True if 'new foo()', false if 'foo()'. */ Interpreter.prototype.calledWithNew = function() { return this.stateStack[this.stateStack.length - 1].isConstructor; }; /** * Gets a value from the scope chain or from an object property. * @param {!Array} ref Name of variable or object/propname tuple. * @param {Acorn AST Node} node Node that triggered this function. Used by Bitburner for getting error line number * @return {Interpreter.Value} Any value. * May be flagged as being a getter and thus needing immediate execution * (rather than being the value of the property). */ Interpreter.prototype.getValue = function(ref, node) { if (ref[0] === Interpreter.SCOPE_REFERENCE) { // A null/varname variable lookup. return this.getValueFromScope(ref[1], node); } else { // An obj/prop components tuple (foo.bar). return this.getProperty(ref[0], ref[1], node); } }; /** * Sets a value to the scope chain or to an object property. * @param {!Array} ref Name of variable or object/propname tuple. * @param {Interpreter.Value} value Value. * @return {!Interpreter.Object|undefined} Returns a setter function if one * needs to be called, otherwise undefined. */ Interpreter.prototype.setValue = function(ref, value) { if (ref[0] === Interpreter.SCOPE_REFERENCE) { // A null/varname variable lookup. return this.setValueToScope(ref[1], value); } else { // An obj/prop components tuple (foo.bar). return this.setProperty(ref[0], ref[1], value); } }; /** * Completion Value Types. * @enum {number} */ Interpreter.Completion = { NORMAL: 0, BREAK: 1, CONTINUE: 2, RETURN: 3, THROW: 4 }; /** * Throw an exception in the interpreter that can be handled by an * interpreter try/catch statement. If unhandled, a real exception will * be thrown. Can be called with either an error class and a message, or * with an actual object to be thrown. * @param {!Interpreter.Object} errorClass Type of error (if message is * provided) or the value to throw (if no message). * @param {string=} opt_message Message being thrown. */ Interpreter.prototype.throwException = function(errorClass, opt_message, lineNumber) { if (opt_message === undefined) { var error = errorClass; // This is a value to throw, not an error class. } else { var error = this.createObject(errorClass); this.setProperty(error, 'message', opt_message, Interpreter.NONENUMERABLE_DESCRIPTOR); } var lineNumErrorMsg; if (lineNumber != null) { lineNumErrorMsg = this.getErrorLineNumberMessage(lineNumber); } this.unwind(Interpreter.Completion.THROW, error, undefined, lineNumErrorMsg); // Abort anything related to the current step. throw Interpreter.STEP_ERROR; }; /** * Unwind the stack to the innermost relevant enclosing TryStatement, * For/ForIn/WhileStatement or Call/NewExpression. If this results in * the stack being completely unwound the thread will be terminated * and the appropriate error being thrown. * @param {Interpreter.Completion} type Completion type. * @param {Interpreter.Value=} value Value computed, returned or thrown. * @param {string=} label Target label for break or return. */ Interpreter.prototype.unwind = function(type, value, label, lineNumberMsg="") { if (type === Interpreter.Completion.NORMAL) { throw TypeError('Should not unwind for NORMAL completions'); } for (var stack = this.stateStack; stack.length > 0; stack.pop()) { var state = stack[stack.length - 1]; switch (state.node['type']) { case 'TryStatement': state.cv = {type: type, value: value, label: label}; return; case 'CallExpression': case 'NewExpression': if (type === Interpreter.Completion.RETURN) { state.value = value; return; } else if (type !== Interpreter.Completion.THROW) { throw Error('Unsynatctic break/continue not rejected by Acorn'); } } if (type === Interpreter.Completion.BREAK) { if (label ? (state.labels && state.labels.indexOf(label) !== -1) : (state.isLoop || state.isSwitch)) { stack.pop(); return; } } else if (type === Interpreter.Completion.CONTINUE) { if (label ? (state.labels && state.labels.indexOf(label) !== -1) : state.isLoop) { return; } } } // Unhandled completion. Throw a real error. var realError; if (this.isa(value, this.ERROR)) { var errorTable = { 'EvalError': EvalError, 'RangeError': RangeError, 'ReferenceError': ReferenceError, 'SyntaxError': SyntaxError, 'TypeError': TypeError, 'URIError': URIError }; var name = this.getProperty(value, 'name').toString(); var message = this.getProperty(value, 'message').valueOf(); var type = errorTable[name] || Error; realError = type(message + lineNumberMsg); } else { realError = String(value) + lineNumberMsg; } throw realError; }; /** * Create a call to a getter function. * @param {!Interpreter.Object} func Function to execute. * @param {!Interpreter.Object|!Array} left * Name of variable or object/propname tuple. * @private */ Interpreter.prototype.createGetter_ = function(func, left) { // Normally 'this' will be specified as the object component (o.x). // Sometimes 'this' is explicitly provided (o). var funcThis = Array.isArray(left) ? left[0] : left; var node = new this.nodeConstructor(); node['type'] = 'CallExpression'; var state = new Interpreter.State(node, this.stateStack[this.stateStack.length - 1].scope); state.doneCallee_ = true; state.funcThis_ = funcThis; state.func_ = func; state.doneArgs_ = true; state.arguments_ = []; return state; }; /** * Create a call to a setter function. * @param {!Interpreter.Object} func Function to execute. * @param {!Interpreter.Object|!Array} left * Name of variable or object/propname tuple. * @param {Interpreter.Value} value Value to set. * @private */ Interpreter.prototype.createSetter_ = function(func, left, value) { // Normally 'this' will be specified as the object component (o.x). // Sometimes 'this' is implicitly the global object (x). var funcThis = Array.isArray(left) ? left[0] : this.global; var node = new this.nodeConstructor(); node['type'] = 'CallExpression'; var state = new Interpreter.State(node, this.stateStack[this.stateStack.length - 1].scope); state.doneCallee_ = true; state.funcThis_ = funcThis; state.func_ = func; state.doneArgs_ = true; state.arguments_ = [value]; return state; }; /** * Class for a state. * @param {!Object} node AST node for the state. * @param {!Interpreter.Object} scope Scope object for the state. * @constructor */ Interpreter.State = function(node, scope) { this.node = node; this.scope = scope; }; /////////////////////////////////////////////////////////////////////////////// // Functions to handle each node type. /////////////////////////////////////////////////////////////////////////////// Interpreter.prototype['stepArrayExpression'] = function(stack, state, node) { var elements = node['elements']; var n = state.n_ || 0; if (!state.array_) { state.array_ = this.createObjectProto(this.ARRAY_PROTO); state.array_.properties.length = elements.length; } else { this.setProperty(state.array_, n, state.value); n++; } while (n < elements.length) { // Skip missing elements - they're not defined, not undefined. if (elements[n]) { state.n_ = n; return new Interpreter.State(elements[n], state.scope); } n++; } stack.pop(); stack[stack.length - 1].value = state.array_; }; Interpreter.prototype['stepAssignmentExpression'] = function(stack, state, node) { if (!state.doneLeft_) { state.doneLeft_ = true; var nextState = new Interpreter.State(node['left'], state.scope); nextState.components = true; return nextState; } if (!state.doneRight_) { if (!state.leftReference_) { state.leftReference_ = state.value; } if (state.doneGetter_) { state.leftValue_ = state.value; } if (!state.doneGetter_ && node['operator'] !== '=') { var leftValue = this.getValue(state.leftReference_, node); state.leftValue_ = leftValue; if (leftValue && typeof leftValue === 'object' && leftValue.isGetter) { // Clear the getter flag and call the getter function. leftValue.isGetter = false; state.doneGetter_ = true; var func = /** @type {!Interpreter.Object} */ (leftValue); return this.createGetter_(func, state.leftReference_); } } state.doneRight_ = true; return new Interpreter.State(node['right'], state.scope); } if (state.doneSetter_) { // Return if setter function. // Setter method on property has completed. // Ignore its return value, and use the original set value instead. stack.pop(); stack[stack.length - 1].value = state.setterValue_; return; } var value = state.leftValue_; var rightValue = state.value; switch (node['operator']) { case '=': value = rightValue; break; case '+=': value += rightValue; break; case '-=': value -= rightValue; break; case '*=': value *= rightValue; break; case '/=': value /= rightValue; break; case '%=': value %= rightValue; break; case '<<=': value <<= rightValue; break; case '>>=': value >>= rightValue; break; case '>>>=': value >>>= rightValue; break; case '&=': value &= rightValue; break; case '^=': value ^= rightValue; break; case '|=': value |= rightValue; break; default: throw SyntaxError('Unknown assignment expression: ' + node['operator']); } var setter = this.setValue(state.leftReference_, value); if (setter) { state.doneSetter_ = true; state.setterValue_ = value; return this.createSetter_(setter, state.leftReference_, value); } // Return if no setter function. stack.pop(); stack[stack.length - 1].value = value; }; Interpreter.prototype['stepBinaryExpression'] = function(stack, state, node) { if (!state.doneLeft_) { state.doneLeft_ = true; return new Interpreter.State(node['left'], state.scope); } if (!state.doneRight_) { state.doneRight_ = true; state.leftValue_ = state.value; return new Interpreter.State(node['right'], state.scope); } stack.pop(); var leftValue = state.leftValue_; var rightValue = state.value; var value; switch (node['operator']) { case '==': value = leftValue == rightValue; break; case '!=': value = leftValue != rightValue; break; case '===': value = leftValue === rightValue; break; case '!==': value = leftValue !== rightValue; break; case '>': value = leftValue > rightValue; break; case '>=': value = leftValue >= rightValue; break; case '<': value = leftValue < rightValue; break; case '<=': value = leftValue <= rightValue; break; case '+': value = leftValue + rightValue; break; case '-': value = leftValue - rightValue; break; case '*': value = leftValue * rightValue; break; case '/': value = leftValue / rightValue; break; case '%': value = leftValue % rightValue; break; case '&': value = leftValue & rightValue; break; case '|': value = leftValue | rightValue; break; case '^': value = leftValue ^ rightValue; break; case '<<': value = leftValue << rightValue; break; case '>>': value = leftValue >> rightValue; break; case '>>>': value = leftValue >>> rightValue; break; case 'in': if (!rightValue || !rightValue.isObject) { let lineNum = this.getErrorLineNumber(node); this.throwException(this.TYPE_ERROR, "'in' expects an object, not '" + rightValue + "'", lineNum); } value = this.hasProperty(rightValue, leftValue); break; case 'instanceof': if (!this.isa(rightValue, this.FUNCTION)) { let lineNum = this.getErrorLineNumber(node); this.throwException(this.TYPE_ERROR, 'Right-hand side of instanceof is not an object', lineNum); } value = leftValue.isObject ? this.isa(leftValue, rightValue) : false; break; default: throw SyntaxError('Unknown binary operator: ' + node['operator']); } stack[stack.length - 1].value = value; }; Interpreter.prototype['stepBlockStatement'] = function(stack, state, node) { var n = state.n_ || 0; var expression = node['body'][n]; if (expression) { state.n_ = n + 1; return new Interpreter.State(expression, state.scope); } stack.pop(); }; Interpreter.prototype['stepBreakStatement'] = function(stack, state, node) { var label = node['label'] && node['label']['name']; this.unwind(Interpreter.Completion.BREAK, undefined, label); }; Interpreter.prototype['stepCallExpression'] = function(stack, state, node) { if (!state.doneCallee_) { state.doneCallee_ = 1; // Components needed to determine value of 'this'. var nextState = new Interpreter.State(node['callee'], state.scope); nextState.components = true; return nextState; } if (state.doneCallee_ === 1) { // Determine value of the function. state.doneCallee_ = 2; var func = state.value; if (Array.isArray(func)) { state.func_ = this.getValue(func, node); if (func[0] === Interpreter.SCOPE_REFERENCE) { // (Globally or locally) named function. Is it named 'eval'? state.directEval_ = (func[1] === 'eval'); } else { // Method function, 'this' is object (ignored if invoked as 'new'). state.funcThis_ = func[0]; } func = state.func_; if (func && typeof func === 'object' && func.isGetter) { // Clear the getter flag and call the getter function. func.isGetter = false; state.doneCallee_ = 1; return this.createGetter_(/** @type {!Interpreter.Object} */ (func), state.value); } } else { // Already evaluated function: (function(){...})(); state.func_ = func; } state.arguments_ = []; state.n_ = 0; } var func = state.func_; if (!state.doneArgs_) { if (state.n_ !== 0) { state.arguments_.push(state.value); } if (node['arguments'][state.n_]) { return new Interpreter.State(node['arguments'][state.n_++], state.scope); } // Determine value of 'this' in function. if (node['type'] === 'NewExpression') { if (func.illegalConstructor) { // Illegal: new escape(); let lineNum = this.getErrorLineNumber(node); this.throwException(this.TYPE_ERROR, func + ' is not a constructor', lineNum); } // Constructor, 'this' is new object. var proto = func.properties['prototype']; if (typeof proto !== 'object' || proto === null) { // Non-object prototypes default to Object.prototype. proto = this.OBJECT_PROTO; } state.funcThis_ = this.createObjectProto(proto); state.isConstructor = true; } else if (state.funcThis_ === undefined) { // Global function, 'this' is global object (or 'undefined' if strict). state.funcThis_ = state.scope.strict ? undefined : this.global; } state.doneArgs_ = true; } if (!state.doneExec_) { state.doneExec_ = true; if (!func || !func.isObject) { let lineNum = this.getErrorLineNumber(node); this.throwException(this.TYPE_ERROR, func + ' is not a function', lineNum); } var funcNode = func.node; if (funcNode) { var scope = this.createScope(funcNode['body'], func.parentScope); // Add all arguments. for (var i = 0; i < funcNode['params'].length; i++) { var paramName = funcNode['params'][i]['name']; var paramValue = state.arguments_.length > i ? state.arguments_[i] : undefined; this.setProperty(scope, paramName, paramValue); } // Build arguments variable. var argsList = this.createObjectProto(this.ARRAY_PROTO); for (var i = 0; i < state.arguments_.length; i++) { this.setProperty(argsList, i, state.arguments_[i]); } this.setProperty(scope, 'arguments', argsList); // Add the function's name (var x = function foo(){};) var name = funcNode['id'] && funcNode['id']['name']; if (name) { this.setProperty(scope, name, func); } this.setProperty(scope, 'this', state.funcThis_, Interpreter.READONLY_DESCRIPTOR); state.value = undefined; // Default value if no explicit return. return new Interpreter.State(funcNode['body'], scope); } else if (func.eval) { var code = state.arguments_[0]; if (typeof code !== 'string') { // JS does not parse String objects: // eval(new String('1 + 1')) -> '1 + 1' state.value = code; } else { try { var ast = acorn.parse(code.toString(), Interpreter.PARSE_OPTIONS); } catch (e) { // Acorn threw a SyntaxError. Rethrow as a trappable error. let lineNum = this.getErrorLineNumber(node); this.throwException(this.SYNTAX_ERROR, 'Invalid code: ' + e.message, lineNum); } var evalNode = new this.nodeConstructor(); evalNode['type'] = 'EvalProgram_'; evalNode['body'] = ast['body']; this.stripLocations_(evalNode, node['start'], node['end']); // Create new scope and update it with definitions in eval(). var scope = state.directEval_ ? state.scope : this.global; if (scope.strict) { // Strict mode get its own scope in eval. scope = this.createScope(ast, scope); } else { // Non-strict mode pollutes the current scope. this.populateScope_(ast, scope); } this.value = undefined; // Default value if no code. return new Interpreter.State(evalNode, scope); } } else if (func.nativeFunc) { state.value = func.nativeFunc.apply(state.funcThis_, state.arguments_); } else if (func.asyncFunc) { var thisInterpreter = this; var callback = function(value) { state.value = value; thisInterpreter.paused_ = false; }; var argsWithCallback = state.arguments_.concat(callback); this.paused_ = true; func.asyncFunc.apply(state.funcThis_, argsWithCallback); return; } else { /* A child of a function is a function but is not callable. For example: var F = function() {}; F.prototype = escape; var f = new F(); f(); */ let lineNum = this.getErrorLineNumber(node); this.throwException(this.TYPE_ERROR, func.class + ' is not a function', lineNum); } } else { // Execution complete. Put the return value on the stack. stack.pop(); if (state.isConstructor && typeof state.value !== 'object') { stack[stack.length - 1].value = state.funcThis_; } else { stack[stack.length - 1].value = state.value; } } }; Interpreter.prototype['stepCatchClause'] = function(stack, state, node) { if (!state.done_) { state.done_ = true; // Create an empty scope. var scope = this.createSpecialScope(state.scope); // Add the argument. this.setProperty(scope, node['param']['name'], state.throwValue); // Execute catch clause. return new Interpreter.State(node['body'], scope); } else { stack.pop(); } }; Interpreter.prototype['stepConditionalExpression'] = function(stack, state, node) { var mode = state.mode_ || 0; if (mode === 0) { state.mode_ = 1; return new Interpreter.State(node['test'], state.scope); } if (mode === 1) { state.mode_ = 2; var value = Boolean(state.value); if (value && node['consequent']) { // Execute 'if' block. return new Interpreter.State(node['consequent'], state.scope); } else if (!value && node['alternate']) { // Execute 'else' block. return new Interpreter.State(node['alternate'], state.scope); } // eval('1;if(false){2}') -> undefined this.value = undefined; } stack.pop(); if (node['type'] === 'ConditionalExpression') { stack[stack.length - 1].value = state.value; } }; Interpreter.prototype['stepContinueStatement'] = function(stack, state, node) { var label = node['label'] && node['label']['name']; this.unwind(Interpreter.Completion.CONTINUE, undefined, label); }; Interpreter.prototype['stepDebuggerStatement'] = function(stack, state, node) { // Do nothing. May be overridden by developers. stack.pop(); }; Interpreter.prototype['stepDoWhileStatement'] = function(stack, state, node) { if (node['type'] === 'DoWhileStatement' && state.test_ === undefined) { // First iteration of do/while executes without checking test. state.value = true; state.test_ = true; } if (!state.test_) { state.test_ = true; return new Interpreter.State(node['test'], state.scope); } if (!state.value) { // Done, exit loop. stack.pop(); } else if (node['body']) { // Execute the body. state.test_ = false; state.isLoop = true; return new Interpreter.State(node['body'], state.scope); } }; Interpreter.prototype['stepEmptyStatement'] = function(stack, state, node) { stack.pop(); }; Interpreter.prototype['stepEvalProgram_'] = function(stack, state, node) { var n = state.n_ || 0; var expression = node['body'][n]; if (expression) { state.n_ = n + 1; return new Interpreter.State(expression, state.scope); } stack.pop(); stack[stack.length - 1].value = this.value; }; Interpreter.prototype['stepExpressionStatement'] = function(stack, state, node) { if (!state.done_) { state.done_ = true; return new Interpreter.State(node['expression'], state.scope); } stack.pop(); // Save this value to interpreter.value for use as a return value if // this code is inside an eval function. this.value = state.value; }; Interpreter.prototype['stepForInStatement'] = function(stack, state, node) { // First, initialize a variable if exists. Only do so once, ever. if (!state.doneInit_) { state.doneInit_ = true; if (node['left']['declarations'] && node['left']['declarations'][0]['init']) { if (state.scope.strict) { let lineNum = this.getErrorLineNumber(node); this.throwException(this.SYNTAX_ERROR, 'for-in loop variable declaration may not have an initializer.', lineNum); } // Variable initialization: for (var x = 4 in y) return new Interpreter.State(node['left'], state.scope); } } // Second, look up the object. Only do so once, ever. if (!state.doneObject_) { state.doneObject_ = true; if (!state.variable_) { state.variable_ = state.value; } return new Interpreter.State(node['right'], state.scope); } if (!state.isLoop) { // First iteration. state.isLoop = true; state.object_ = state.value; state.visited_ = Object.create(null); } // Third, find the property name for this iteration. if (state.name_ === undefined) { gotPropName: while (true) { if (state.object_ && state.object_.isObject) { if (!state.props_) { state.props_ = Object.getOwnPropertyNames(state.object_.properties); } while (true) { var prop = state.props_.shift(); if (prop === undefined) { break; // Reached end of this object's properties. } if (!Object.prototype.hasOwnProperty.call(state.object_.properties, prop)) { continue; // Property has been deleted in the loop. } if (state.visited_[prop]) { continue; // Already seen this property on a child. } state.visited_[prop] = true; if (!Object.prototype.propertyIsEnumerable.call( state.object_.properties, prop)) { continue; // Skip non-enumerable property. } state.name_ = prop; break gotPropName; } } else if (state.object_ !== null && state.object_ !== undefined) { // Primitive value (other than null or undefined). if (!state.props_) { state.props_ = Object.getOwnPropertyNames(state.object_); } while (true) { var prop = state.props_.shift(); if (prop === undefined) { break; // Reached end of this value's properties. } state.visited_[prop] = true; if (!Object.prototype.propertyIsEnumerable.call( state.object_, prop)) { continue; // Skip non-enumerable property. } state.name_ = prop; break gotPropName; } } state.object_ = this.getPrototype(state.object_); state.props_ = null; if (state.object_ === null) { // Done, exit loop. stack.pop(); return; } } } // Fourth, find the variable if (!state.doneVariable_) { state.doneVariable_ = true; var left = node['left']; if (left['type'] === 'VariableDeclaration') { // Inline variable declaration: for (var x in y) state.variable_ = [Interpreter.SCOPE_REFERENCE, left['declarations'][0]['id']['name']]; } else { // Arbitrary left side: for (foo().bar in y) state.variable_ = null; var nextState = new Interpreter.State(left, state.scope); nextState.components = true; return nextState; } } if (!state.variable_) { state.variable_ = state.value; } // Fifth, set the variable. if (!state.doneSetter_) { state.doneSetter_ = true; var value = state.name_; var setter = this.setValue(state.variable_, value); if (setter) { return this.createSetter_(setter, state.variable_, value); } } // Next step will be step three. state.name_ = undefined; // Reevaluate the variable since it could be a setter on the global object. state.doneVariable_ = false; state.doneSetter_ = false; // Sixth and finally, execute the body if there was one. this. if (node['body']) { return new Interpreter.State(node['body'], state.scope); } }; Interpreter.prototype['stepForStatement'] = function(stack, state, node) { var mode = state.mode_ || 0; if (mode === 0) { state.mode_ = 1; if (node['init']) { return new Interpreter.State(node['init'], state.scope); } } else if (mode === 1) { state.mode_ = 2; if (node['test']) { return new Interpreter.State(node['test'], state.scope); } } else if (mode === 2) { state.mode_ = 3; if (node['test'] && !state.value) { // Done, exit loop. stack.pop(); } else { // Execute the body. state.isLoop = true; return new Interpreter.State(node['body'], state.scope); } } else if (mode === 3) { state.mode_ = 1; if (node['update']) { return new Interpreter.State(node['update'], state.scope); } } }; Interpreter.prototype['stepFunctionDeclaration'] = function(stack, state, node) { // This was found and handled when the scope was populated. stack.pop(); }; Interpreter.prototype['stepFunctionExpression'] = function(stack, state, node) { stack.pop(); stack[stack.length - 1].value = this.createFunction(node, state.scope); }; Interpreter.prototype['stepIdentifier'] = function(stack, state, node) { stack.pop(); if (state.components) { stack[stack.length - 1].value = [Interpreter.SCOPE_REFERENCE, node['name']]; return; } var value = this.getValueFromScope(node['name'], node); // An identifier could be a getter if it's a property on the global object. if (value && typeof value === 'object' && value.isGetter) { // Clear the getter flag and call the getter function. value.isGetter = false; var scope = state.scope; while (!this.hasProperty(scope, node['name'])) { scope = scope.parentScope; } var func = /** @type {!Interpreter.Object} */ (value); return this.createGetter_(func, this.global); } stack[stack.length - 1].value = value; }; Interpreter.prototype['stepIfStatement'] = Interpreter.prototype['stepConditionalExpression']; Interpreter.prototype['stepLabeledStatement'] = function(stack, state, node) { // No need to hit this node again on the way back up the stack. stack.pop(); // Note that a statement might have multiple labels. var labels = state.labels || []; labels.push(node['label']['name']); var nextState = new Interpreter.State(node['body'], state.scope); nextState.labels = labels; return nextState; }; Interpreter.prototype['stepLiteral'] = function(stack, state, node) { stack.pop(); var value = node['value']; if (value instanceof RegExp) { var pseudoRegexp = this.createObjectProto(this.REGEXP_PROTO); this.populateRegExp(pseudoRegexp, value); value = pseudoRegexp; } stack[stack.length - 1].value = value; }; Interpreter.prototype['stepLogicalExpression'] = function(stack, state, node) { if (node['operator'] !== '&&' && node['operator'] !== '||') { throw SyntaxError('Unknown logical operator: ' + node['operator']); } if (!state.doneLeft_) { state.doneLeft_ = true; return new Interpreter.State(node['left'], state.scope); } if (!state.doneRight_) { if ((node['operator'] === '&&' && !state.value) || (node['operator'] === '||' && state.value)) { // Shortcut evaluation. stack.pop(); stack[stack.length - 1].value = state.value; } else { state.doneRight_ = true; return new Interpreter.State(node['right'], state.scope); } } else { stack.pop(); stack[stack.length - 1].value = state.value; } }; Interpreter.prototype['stepMemberExpression'] = function(stack, state, node) { if (!state.doneObject_) { state.doneObject_ = true; return new Interpreter.State(node['object'], state.scope); } var propName; if (!node['computed']) { state.object_ = state.value; // obj.foo -- Just access 'foo' directly. propName = node['property']['name']; } else if (!state.doneProperty_) { state.object_ = state.value; // obj[foo] -- Compute value of 'foo'. state.doneProperty_ = true; return new Interpreter.State(node['property'], state.scope); } else { propName = state.value; } stack.pop(); if (state.components) { stack[stack.length - 1].value = [state.object_, propName]; } else { var value = this.getProperty(state.object_, propName); if (value && typeof value === 'object' && value.isGetter) { // Clear the getter flag and call the getter function. value.isGetter = false; var func = /** @type {!Interpreter.Object} */ (value); return this.createGetter_(func, state.object_); } stack[stack.length - 1].value = value; } }; Interpreter.prototype['stepNewExpression'] = Interpreter.prototype['stepCallExpression']; Interpreter.prototype['stepObjectExpression'] = function(stack, state, node) { var n = state.n_ || 0; var property = node['properties'][n]; if (!state.object_) { // First execution. state.object_ = this.createObjectProto(this.OBJECT_PROTO); state.properties_ = Object.create(null); } else { // Determine property name. var key = property['key']; if (key['type'] === 'Identifier') { var propName = key['name']; } else if (key['type'] === 'Literal') { var propName = key['value']; } else { throw SyntaxError('Unknown object structure: ' + key['type']); } // Set the property computed in the previous execution. if (!state.properties_[propName]) { // Create temp object to collect value, getter, and/or setter. state.properties_[propName] = {}; } state.properties_[propName][property['kind']] = state.value; state.n_ = ++n; property = node['properties'][n]; } if (property) { return new Interpreter.State(property['value'], state.scope); } for (var key in state.properties_) { var kinds = state.properties_[key]; if ('get' in kinds || 'set' in kinds) { // Set a property with a getter or setter. var descriptor = { configurable: true, enumerable: true, get: kinds['get'], set: kinds['set'] }; this.setProperty(state.object_, key, null, descriptor); } else { // Set a normal property with a value. this.setProperty(state.object_, key, kinds['init']); } } stack.pop(); stack[stack.length - 1].value = state.object_; }; Interpreter.prototype['stepProgram'] = function(stack, state, node) { var expression = node['body'].shift(); if (expression) { state.done = false; return new Interpreter.State(expression, state.scope); } state.done = true; // Don't pop the stateStack. // Leave the root scope on the tree in case the program is appended to. }; Interpreter.prototype['stepReturnStatement'] = function(stack, state, node) { if (node['argument'] && !state.done_) { state.done_ = true; return new Interpreter.State(node['argument'], state.scope); } this.unwind(Interpreter.Completion.RETURN, state.value, undefined); }; Interpreter.prototype['stepSequenceExpression'] = function(stack, state, node) { var n = state.n_ || 0; var expression = node['expressions'][n]; if (expression) { state.n_ = n + 1; return new Interpreter.State(expression, state.scope); } stack.pop(); stack[stack.length - 1].value = state.value; }; Interpreter.prototype['stepSwitchStatement'] = function(stack, state, node) { if (!state.test_) { state.test_ = 1; return new Interpreter.State(node['discriminant'], state.scope); } if (state.test_ === 1) { state.test_ = 2; // Preserve switch value between case tests. state.switchValue_ = state.value; state.defaultCase_ = -1; } while (true) { var index = state.index_ || 0; var switchCase = node['cases'][index]; if (!state.matched_ && switchCase && !switchCase['test']) { // Test on the default case is null. // Bypass (but store) the default case, and get back to it later. state.defaultCase_ = index; state.index_ = index + 1; continue; } if (!switchCase && !state.matched_ && state.defaultCase_ !== -1) { // Ran through all cases, no match. Jump to the default. state.matched_ = true; state.index_ = state.defaultCase_; continue; } if (switchCase) { if (!state.matched_ && !state.tested_ && switchCase['test']) { state.tested_ = true; return new Interpreter.State(switchCase['test'], state.scope); } if (state.matched_ || state.value === state.switchValue_) { state.matched_ = true; var n = state.n_ || 0; if (switchCase['consequent'][n]) { state.isSwitch = true; state.n_ = n + 1; return new Interpreter.State(switchCase['consequent'][n], state.scope); } } // Move on to next case. state.tested_ = false; state.n_ = 0; state.index_ = index + 1; } else { stack.pop(); return; } } }; Interpreter.prototype['stepThisExpression'] = function(stack, state, node) { stack.pop(); stack[stack.length - 1].value = this.getValueFromScope('this', node); }; Interpreter.prototype['stepThrowStatement'] = function(stack, state, node) { if (!state.done_) { state.done_ = true; return new Interpreter.State(node['argument'], state.scope); } else { this.throwException(state.value); } }; Interpreter.prototype['stepTryStatement'] = function(stack, state, node) { if (!state.doneBlock_) { state.doneBlock_ = true; return new Interpreter.State(node['block'], state.scope); } if (state.cv && state.cv.type === Interpreter.Completion.THROW && !state.doneHandler_ && node['handler']) { state.doneHandler_ = true; var nextState = new Interpreter.State(node['handler'], state.scope); nextState.throwValue = state.cv.value; state.cv = undefined; // This error has been handled, don't rethrow. return nextState; } if (!state.doneFinalizer_ && node['finalizer']) { state.doneFinalizer_ = true; return new Interpreter.State(node['finalizer'], state.scope); } stack.pop(); if (state.cv) { // There was no catch handler, or the catch/finally threw an error. // Throw the error up to a higher try. this.unwind(state.cv.type, state.cv.value, state.cv.label); } }; Interpreter.prototype['stepUnaryExpression'] = function(stack, state, node) { if (!state.done_) { state.done_ = true; var nextState = new Interpreter.State(node['argument'], state.scope); nextState.components = node['operator'] === 'delete'; return nextState; } stack.pop(); var value = state.value; if (node['operator'] === '-') { value = -value; } else if (node['operator'] === '+') { value = +value; } else if (node['operator'] === '!') { value = !value; } else if (node['operator'] === '~') { value = ~value; } else if (node['operator'] === 'delete') { var result = true; // If value is not an array, then it is a primitive, or some other value. // If so, skip the delete and return true. if (Array.isArray(value)) { var obj = value[0]; if (obj === Interpreter.SCOPE_REFERENCE) { // 'delete foo;' is the same as 'delete window.foo'. obj = state.scope; } var name = String(value[1]); try { delete obj.properties[name]; } catch (e) { if (state.scope.strict) { this.throwException(this.TYPE_ERROR, "Cannot delete property '" + name + "' of '" + obj + "'"); } else { result = false; } } } value = result; } else if (node['operator'] === 'typeof') { value = (value && value.class === 'Function') ? 'function' : typeof value; } else if (node['operator'] === 'void') { value = undefined; } else { throw SyntaxError('Unknown unary operator: ' + node['operator']); } stack[stack.length - 1].value = value; }; Interpreter.prototype['stepUpdateExpression'] = function(stack, state, node) { if (!state.doneLeft_) { state.doneLeft_ = true; var nextState = new Interpreter.State(node['argument'], state.scope); nextState.components = true; return nextState; } if (!state.leftSide_) { state.leftSide_ = state.value; } if (state.doneGetter_) { state.leftValue_ = state.value; } if (!state.doneGetter_) { var leftValue = this.getValue(state.leftSide_, node); state.leftValue_ = leftValue; if (leftValue && typeof leftValue === 'object' && leftValue.isGetter) { // Clear the getter flag and call the getter function. leftValue.isGetter = false; state.doneGetter_ = true; var func = /** @type {!Interpreter.Object} */ (leftValue); return this.createGetter_(func, state.leftSide_); } } if (state.doneSetter_) { // Return if setter function. // Setter method on property has completed. // Ignore its return value, and use the original set value instead. stack.pop(); stack[stack.length - 1].value = state.setterValue_; return; } var leftValue = Number(state.leftValue_); var changeValue; if (node['operator'] === '++') { changeValue = leftValue + 1; } else if (node['operator'] === '--') { changeValue = leftValue - 1; } else { throw SyntaxError('Unknown update expression: ' + node['operator']); } var returnValue = node['prefix'] ? changeValue : leftValue; var setter = this.setValue(state.leftSide_, changeValue); if (setter) { state.doneSetter_ = true; state.setterValue_ = returnValue; return this.createSetter_(setter, state.leftSide_, changeValue); } // Return if no setter function. stack.pop(); stack[stack.length - 1].value = returnValue; }; Interpreter.prototype['stepVariableDeclaration'] = function(stack, state, node) { var declarations = node['declarations']; var n = state.n_ || 0; var declarationNode = declarations[n]; if (state.init_ && declarationNode) { // This setValue call never needs to deal with calling a setter function. // Note that this is setting the init value, not defining the variable. // Variable definition is done when scope is populated. this.setValueToScope(declarationNode['id']['name'], state.value); state.init_ = false; declarationNode = declarations[++n]; } while (declarationNode) { // Skip any declarations that are not initialized. They have already // been defined as undefined in populateScope_. if (declarationNode['init']) { state.n_ = n; state.init_ = true; return new Interpreter.State(declarationNode['init'], state.scope); } declarationNode = declarations[++n]; } stack.pop(); }; Interpreter.prototype['stepWithStatement'] = function(stack, state, node) { if (!state.doneObject_) { state.doneObject_ = true; return new Interpreter.State(node['object'], state.scope); } else if (!state.doneBody_) { state.doneBody_ = true; var scope = this.createSpecialScope(state.scope, state.value); return new Interpreter.State(node['body'], scope); } else { stack.pop(); } }; Interpreter.prototype['stepWhileStatement'] = Interpreter.prototype['stepDoWhileStatement']; // Preserve top-level API functions from being pruned/renamed by JS compilers. // Add others as needed. // The global object ('window' in a browser, 'global' in node.js) is 'this'. //this['Interpreter'] = Interpreter; Interpreter.prototype['step'] = Interpreter.prototype.step; Interpreter.prototype['run'] = Interpreter.prototype.run; Interpreter.prototype['appendCode'] = Interpreter.prototype.appendCode; Interpreter.prototype['createObject'] = Interpreter.prototype.createObject; Interpreter.prototype['createObjectProto'] = Interpreter.prototype.createObjectProto; Interpreter.prototype['createAsyncFunction'] = Interpreter.prototype.createAsyncFunction; Interpreter.prototype['createNativeFunction'] = Interpreter.prototype.createNativeFunction; Interpreter.prototype['getProperty'] = Interpreter.prototype.getProperty; Interpreter.prototype['setProperty'] = Interpreter.prototype.setProperty; Interpreter.prototype['nativeToPseudo'] = Interpreter.prototype.nativeToPseudo; Interpreter.prototype['pseudoToNative'] = Interpreter.prototype.pseudoToNative; // Obsolete. Do not use. Interpreter.prototype['createPrimitive'] = function(x) {return x;}; export {Interpreter};