/* Contains the entire implementation (parser and evaluator) of * the Netscript language. It needs to be all in one file because the scripts * are evaluated using Web Workers, which runs code from a single file */ /* Evaluator * Evaluates the Abstract Syntax Tree for Netscript * generated by the Parser class */ function evaluate(exp, workerScript) { var env = workerScript.env; switch (exp.type) { case "num": case "str": case "bool": return exp.value; case "var": return env.get(exp.value); //Can currently only assign to "var"s case "assign": if (exp.left.type != "var") throw new Error("Cannot assign to " + JSON.stringify(exp.left)); var p = new Promise(function(resolve, reject) { setTimeout(function() { var res = evaluate(exp.right, workerScript); resolve(res); }, 3000) }); return p.then(function(expRight) { return env.set(exp.left.value, expRight); }); case "binary": var pLeft = new Promise(function(resolve, reject) { setTimeout(function() { var resLeft = evaluate(exp.left, workerScript); resolve(resLeft); }, 3000); }); var pRight = pLeft.then(function(expLeft) { setTimeout(function() { var resRight = evaluate(exp.right, workerScript); resolve([expLeft, resRight]); }, 3000); }); return pRight.then(function(args) { return apply_op(exp.operator, args[0], args[1]); }); //TODO case "if": var numConds = exp.cond.length; var numThens = exp.then.length; if (numConds == 0 || numThens == 0 || numConds != numThens) { throw new Error ("Number of conds and thens in if structure don't match (or there are none)"); } for (var i = 0; i < numConds; i++) { var cond = evaluate(exp.cond[i], workerScript); if (cond) return evaluate(exp.then[i], workerScript); } //Evaluate else if it exists, snce none of the conditionals //were true return exp.else ? evaluate(exp.else, workerScript) : false; case "for": var pInit = new Promise(function(resolve, reject) { setTimeout(function() { var resInit = evaluate(exp.init, workerScript); resolve(resInit); }, 3000); }); pInit.then(function(expInit) { setTimeout(function() { var resCond = evaluate(exp.cond, workerScript); console.log("Evaluated the conditional"); resolve(resCond); }, 3000); }) .then(function(expCond) { while (expCond) { var pCode = new Promise(function(resolve, reject) { setTimeout(function() { var resCode = evaluate(exp.code, workerScript); resolve(resCode); }, 3000); }); var pPostloop = pCode.then(function(expCode) { setTimeout(function() { var resPostloop = evaluate(exp.postloop, workerScript); resolve(resPostloop); }, 3000); }); var pCond = pPostloop.then(function(expPostloop) { setTimeout(function() { var resCond = evaluate(exp.cond, workerScript); resolve(resCond); }, 3000); }); pCond.then(function(resCond) { expCond = resCond; //Do i need resolve here? }); } }); //TODO I don't think I need to return anything..but I might be wrong break; case "while": var pCond = new Promise(function(resolve, reject) { setTimeout(function() { var resCond = evaluate(exp.cond, workerScript); resolve(resCond); }, 3000); }); pCond.then(function(expCond) { while (expCond) { var pCode = new Promise(function(resolve, reject) { setTimeout(function() { var resCode = evaluate(exp.code, workerScript); resolve(resCode); }, 3000); }); pCode.then(function(expCode) { expCond = evaluate(exp.cond, workerScript); //Do i need resolve here? }); } }); //TODO I don't think I need to return anything..but I might be wrong break; case "prog": var val = false; exp.prog.forEach(function(exp){ var pExp = new Promise(function(resolve, reject) { setTimeout(function() { var resExp = evaluate(exp, workerScript); resolve(resExp); }, 3000); }); pExp.then(function(resExp) { val = resExp; }); }); return val; /* Currently supported function calls: * hack() * sleep(N) - sleep N seconds * print(x) - Prints a variable or constant * */ case "call": //Define only valid function calls here, like hack() and stuff //var func = evaluate(exp.func, env); //return func.apply(null, exp.args.map(function(arg){ // return evaluate(arg, env); //})); if (exp.func.value == "hack") { console.log("Execute hack()"); } else if (exp.func.value == "sleep") { console.log("Execute sleep()"); } else if (exp.func.value == "print") { post(evaluate(exp.args[0], workerScript).toString()); } break; default: throw new Error("I don't know how to evaluate " + exp.type); } } function apply_op(op, a, b) { function num(x) { if (typeof x != "number") throw new Error("Expected number but got " + x); return x; } function div(x) { if (num(x) == 0) throw new Error("Divide by zero"); return x; } switch (op) { case "+": return num(a) + num(b); case "-": return num(a) - num(b); case "*": return num(a) * num(b); case "/": return num(a) / div(b); case "%": return num(a) % div(b); case "&&": return a !== false && b; case "||": return a !== false ? a : b; case "<": return num(a) < num(b); case ">": return num(a) > num(b); case "<=": return num(a) <= num(b); case ">=": return num(a) >= num(b); case "==": return a === b; case "!=": return a !== b; } throw new Error("Can't apply operator " + op); } /* Environment * NetScript program environment */ function Environment(parent) { this.vars = Object.create(parent ? parent.vars : null); this.parent = parent; } Environment.prototype = { //Create a "subscope", which is a new new "sub-environment" //The subscope is linked to this through its parent variable extend: function() { return new Environment(this); }, //Finds the scope where the variable with the given name is defined lookup: function(name) { var scope = this; while (scope) { if (Object.prototype.hasOwnProperty.call(scope.vars, name)) return scope; scope = scope.parent; } }, //Get the current value of a variable get: function(name) { if (name in this.vars) return this.vars[name]; throw new Error("Undefined variable " + name); }, //Sets the value of a variable in any scope set: function(name, value) { var scope = this.lookup(name); // let's not allow defining globals from a nested environment // // If scope is null (aka existing variable with name could not be found) // and this is NOT the global scope, throw error if (!scope && this.parent) throw new Error("Undefined variable " + name); return (scope || this).vars[name] = value; }, //Creates (or overwrites) a variable in the current scope def: function(name, value) { return this.vars[name] = value; } }; /* Parser * Creates Abstract Syntax Tree Nodes * Operates on a stream of tokens from the Tokenizer */ var FALSE = {type: "bool", value: false}; function Parser(input) { var PRECEDENCE = { "=": 1, "||": 2, "&&": 3, "<": 7, ">": 7, "<=": 7, ">=": 7, "==": 7, "!=": 7, "+": 10, "-": 10, "*": 20, "/": 20, "%": 20, }; return parse_toplevel(); //Returns true if the next token is a punc type with value ch function is_punc(ch) { var tok = input.peek(); return tok && tok.type == "punc" && (!ch || tok.value == ch) && tok; } //Returns true if the next token is the kw keyword function is_kw(kw) { var tok = input.peek(); return tok && tok.type == "kw" && (!kw || tok.value == kw) && tok; } //Returns true if the next token is an op type with the given op value function is_op(op) { var tok = input.peek(); return tok && tok.type == "op" && (!op || tok.value == op) && tok; } //Checks that the next character is the given punctuation character and throws //an error if it's not. If it is, skips over it in the input function checkPuncAndSkip(ch) { if (is_punc(ch)) input.next(); else input.croak("Expecting punctuation: \"" + ch + "\""); } //Checks that the next character is the given keyword and throws an error //if its not. If it is, skips over it in the input function checkKeywordAndSkip(kw) { if (is_kw(kw)) input.next(); else input.croak("Expecting keyword: \"" + kw + "\""); } //Checks that the next character is the given operator and throws an error //if its not. If it is, skips over it in the input function checkOpAndSkip(op) { if (is_op(op)) input.next(); else input.croak("Expecting operator: \"" + op + "\""); } function unexpected() { input.croak("Unexpected token: " + JSON.stringify(input.peek())); } function maybe_binary(left, my_prec) { var tok = is_op(); if (tok) { var his_prec = PRECEDENCE[tok.value]; if (his_prec > my_prec) { input.next(); return maybe_binary({ type : tok.value == "=" ? "assign" : "binary", operator : tok.value, left : left, right : maybe_binary(parse_atom(), his_prec) }, my_prec); } } return left; } function delimited(start, stop, separator, parser) { var a = [], first = true; checkPuncAndSkip(start); while (!input.eof()) { if (is_punc(stop)) break; if (first) first = false; else checkPuncAndSkip(separator); if (is_punc(stop)) break; a.push(parser()); } checkPuncAndSkip(stop); return a; } function parse_call(func) { return { type: "call", func: func, args: delimited("(", ")", ",", parse_expression), }; } function parse_varname() { var name = input.next(); if (name.type != "var") input.croak("Expecting variable name"); return name.value; } /* type: "if", * cond: [ {"type": "var", "value": "cond1"}, {"type": "var", "value": "cond2"}...] * then: [ {"type": "var", "value": "then1"}, {"type": "var", "value": "then2"}...] * else: {"type": "var", "value": "foo"} */ function parse_if() { console.log("Parsing if token"); checkKeywordAndSkip("if"); //Conditional var cond = parse_expression(); //Body var then = parse_expression(); var ret = { type: "if", cond: [], then: [], }; ret.cond.push(cond); ret.then.push(then); // Parse all elif branches while (is_kw("elif")) { input.next(); var cond = parse_expression(); var then = parse_expression(); ret.cond.push(cond); ret.then.push(then); } // Parse else branch, if it exists if (is_kw("else")) { input.next(); ret.else = parse_expression(); } return ret; } /* for (init, cond, postloop) {code;} * * type: "for", * init: assign node, * cond: var node, * postloop: assign node * code: prog node */ function parse_for() { console.log("Parsing for token"); checkKeywordAndSkip("for"); splitExpressions = delimited("(", ")", ";", parse_expression); console.log("Parsing code in for loop"); code = parse_expression(); if (splitExpressions.length != 3) { throw new Error("for statement has incorrect number of arugments"); } //TODO Check type of the init, cond, and postloop nodes return { type: "for", init: splitExpressions[0], cond: splitExpressions[1], postloop: splitExpressions[2], code: code } } /* while (cond) {} * * type: "while", * cond: var node * code: prog node */ function parse_while() { console.log("Parsing while token"); checkKeywordAndSkip("while"); var cond = parse_expression(); var code = parse_expression(); return { type: "while", cond: cond, code: code } } function parse_bool() { return { type : "bool", value : input.next().value == "true" }; } function maybe_call(expr) { expr = expr(); return is_punc("(") ? parse_call(expr) : expr; } function parse_atom() { return maybe_call(function(){ if (is_punc("(")) { input.next(); var exp = parse_expression(); checkPuncAndSkip(")"); return exp; } if (is_punc("{")) return parse_prog(); if (is_kw("if")) return parse_if(); if (is_kw("for")) return parse_for(); if (is_kw("while")) return parse_while(); //Note, let for loops be function calls (call node types) if (is_kw("true") || is_kw("false")) return parse_bool(); var tok = input.next(); if (tok.type == "var" || tok.type == "num" || tok.type == "str") return tok; unexpected(); }); } function parse_toplevel() { var prog = []; while (!input.eof()) { prog.push(parse_expression()); if (!input.eof()) checkPuncAndSkip(";"); } //Return the top level Abstract Syntax Tree, where the top node is a "prog" node return { type: "prog", prog: prog }; } function parse_prog() { console.log("Parsing prog token"); var prog = delimited("{", "}", ";", parse_expression); if (prog.length == 0) return FALSE; if (prog.length == 1) return prog[0]; return { type: "prog", prog: prog }; } function parse_expression() { return maybe_call(function(){ return maybe_binary(parse_atom(), 0); }); } } /* Tokenizer * Acts on top of the InputStream class. Takes in a character input stream and and parses it into tokens. * Tokens can be accessed with peek() and next(). * * Token types: * {type: "punc", value: "(" } // punctuation: parens, comma, semicolon etc. * {type: "num", value: 5 } // numbers (including floats) * {type: "str", value: "Hello World!" } // strings * {type: "kw", value: "for/if/" } // keywords, see defs below * {type: "var", value: "a" } // identifiers/variables * {type: "op", value: "!=" } // operator characters * {type: "bool", value: "true" } // Booleans * */ function Tokenizer(input) { var current = null; var keywords = " if elif else true false while for "; return { next : next, peek : peek, eof : eof, croak : input.croak } function is_keyword(x) { return keywords.indexOf(" " + x + " ") >= 0; } function is_digit(ch) { return /[0-9]/i.test(ch); } //An identifier can start with any letter or an underscore function is_id_start(ch) { return /[a-z_]/i.test(ch); } function is_id(ch) { return is_id_start(ch) || "?!-<>=0123456789".indexOf(ch) >= 0; } function is_op_char(ch) { return "+-*/%=&|<>!".indexOf(ch) >= 0; } function is_punc(ch) { return ",;(){}[]".indexOf(ch) >= 0; } function is_whitespace(ch) { return " \t\n".indexOf(ch) >= 0; } function read_while(predicate) { var str = ""; while (!input.eof() && predicate(input.peek())) str += input.next(); return str; } function read_number() { var has_dot = false; //Reads the number from the input. Checks for only a single decimal point var number = read_while(function(ch){ if (ch == ".") { if (has_dot) return false; has_dot = true; return true; } return is_digit(ch); }); return { type: "num", value: parseFloat(number) }; } //This function also checks the identifier against a list of known keywords (defined at the top) //and will return a kw object rather than identifier if it is one function read_ident() { //Identifier must start with a letter or underscore..and can contain anything from ?!-<>=0123456789 var id = read_while(is_id); return { type : is_keyword(id) ? "kw" : "var", value : id }; } function read_escaped(end) { var escaped = false, str = ""; input.next(); //Skip the quotation mark while (!input.eof()) { var ch = input.next(); if (escaped) { str += ch; escaped = false; } else if (ch == "\\") { escaped = true; } else if (ch == end) { break; } else { str += ch; } } return str; } function read_string(ch) { if (ch == '"') { return { type: "str", value: read_escaped('"') }; } else if (ch == '\'') { return { type: "str", value: read_escaped('\'') }; } } //Only supports single-line comments right now function skip_comment() { read_while(function(ch){ return ch != "\n" }); input.next(); } //Gets the next token function read_next() { //Skip over whitespace read_while(is_whitespace); if (input.eof()) return null; //Peek the next character and decide what to do based on what that //next character is var ch = input.peek(); if (ch == "//") { skip_comment(); return read_next(); } if (ch == '"' || ch == '\'') return read_string(ch); if (is_digit(ch)) return read_number(); if (is_id_start(ch)) return read_ident(); if (is_punc(ch)) return { type : "punc", value : input.next() } if (is_op_char(ch)) return { type : "op", value : read_while(is_op_char) } } function peek() { //Returns current token, unless its null in which case it grabs the next one //and returns it return current || (current = read_next()); } function next() { //The token might have been peaked already, in which case read_next() was already //called so just return current var tok = current; current = null; return tok || read_next(); } function eof() { return peek() == null; } } /* InputStream class. Creates a "stream object" that provides operations to read * from a string. */ function InputStream(input) { var pos = 0, line = 1, col = 0; return { next : next, peek : peek, eof : eof, croak : croak, }; function next() { var ch = input.charAt(pos++); if (ch == "\n") line++, col = 0; else col++; return ch; } function peek() { return input.charAt(pos); } function eof() { return peek() == ""; } function croak(msg) { throw new Error(msg + " (" + line + ":" + col + ")"); } } /* Object that defines the performance/statistics of the script, * such as how much money it has stolen from every Foreign Server * and how much exp it has gained for the player. * * Every NetscriptWorker has its own ScriptStats object that is passed * back to the main thread regularly. The main thread then uses the information * from the object to change the game state. Then, the worker resets the Script Stats * object and continues. */ function ScriptStats() { ECorpMoneyHacked = 0; ECorpTimesHacked = 0; MegaCorpMoneyHacked = 0; MegaCorpTimesHacked = 0; BachmanAndAssociatesMoneyHacked = 0; BachmanAndAssociatesTimesHacked = 0; NWOMoneyHacked = 0; NWOTimesHacked = 0; ClarkeIncorporatedMoneyHacked = 0; ClarkeIncorporatedTimesHacked = 0; OmniTekIncorporatedMoneyHacked = 0; OmniTekIncorporatedTimesHacked = 0; FourSigmaMoneyHacked = 0; FourSigmaTimesHacked = 0; KuaiGongInternationalMoneyHacked = 0; KuaiGongInternationalTimesHacked = 0; FulcrumTechnologiesMoneyHacked = 0; FulcrumTechnologiesTimesHacked = 0; FulcrumSecretTechnologiesMoneyHacked = 0; FulcrumSecretTechnologiesTimesHacked = 0; StormTechnologiesMoneyHacked = 0; StormTechnologiesTimesHacked = 0; DefCommMoneyHacked = 0; DefCommTimesHacked = 0; InfoCommMoneyHacked = 0; InfoCommTimesHacked = 0; HeliosLabsMoneyHacked = 0; HeliosLabsTimesHacked = 0; VitaLifeMoneyHacked = 0; VitaLifeTimesHacked = 0; IcarusMicrosystemsMoneyHacked = 0; IcarusMicrosystemsTimesHacked = 0; UniversalEnergyMoneyHacked = 0; UniversalEnergyTimesHacked = 0; TitanLabsMoneyHacked = 0; TitanLabsTimesHacked = 0; MicrodyneTechnologiesMoneyHacked = 0; MicrodyneTechnologiesTimesHacked = 0; TaiYangDigitalMoneyHacked = 0; TaiYangDigitalTimesHacked = 0; GalacticCybersystemsMoneyHacked = 0; GalacticCybersystemsTimesHacked = 0; AeroCorpMoneyHacked = 0; AeroCorpTimesHacked = 0; OmniaCybersystemsMoneyHacked = 0; OmniaCybersystemsTimesHacked = 0; ZBDefenseMoneyHacked = 0; ZBDefenseTimesHacked = 0; AppliedEnergeticsMoneyHacked = 0; AppliedEnergeticsTimesHacked = 0; SolarisSpaceSystemsMoneyHacked = 0; SolarisSpaceSystemsTimesHacked = 0; DeltaOneMoneyHacked = 0; DeltaOneTimesHacked = 0; GlobalPharmaceuticalsMoneyHacked = 0; GlobalPharmaceuticalsTimesHacked = 0; NovaMedicalMoneyHacked = 0; NovaMedicalTimesHacked = 0; ZeusMedicalMoneyHacked = 0; ZeusMedicalTimesHacked = 0; UnitaLifeGroupMoneyHacked = 0; UnitaLifeGroupTimesHacked = 0; LexoCorpMoneyHacked = 0; LexoCorpTimesHacked = 0; RhoConstructionMoneyHacked = 0; RhoConstructionTimesHacked = 0; AlphaEnterprisesMoneyHacked = 0; AlphaEnterprisesTimesHacked = 0; AevumPoliceMoneyHacked = 0; AevumPoliceTimesHacked = 0; RothmanUniversityMoneyHacked = 0; RothmanUniversityTimesHacked = 0; ZBInstituteOfTechnologyMoneyHacked = 0; ZBInstituteOfTechnologyTimesHacked = 0; SummitUniversityMoneyHacked = 0; SummitUniversityTimesHacked = 0; SysCoreSecuritiesMoneyHacked = 0; SysCoreSecuritiesTimesHacked = 0; CatalystVenturesMoneyHacked = 0; CatalystVenturesTimesHacked = 0; TheHubMoneyHacked = 0; TheHubTimesHacked = 0; CompuTekMoneyHacked = 0; CompuTekTimesHacked = 0; NetLinkTechnologiesMoneyHacked = 0; NetLinkTechnologiesTimesHacked = 0; JohnsonOrthopedicsMoneyHacked = 0; JohnsonOrthopedicsTimesHacked = 0; FoodNStuffMoneyHacked = 0; FoodNStuffTimesHacked = 0; SigmaCosmeticsMoneyHacked = 0; SigmaCosmeticsTimesHacked = 0; JoesGunsMoneyHacked = 0; JoesGunsTimesHacked = 0; Zer0NightclubMoneyHacked = 0; Zer0NightclubTimesHacked = 0; NectarNightclubMoneyHacked = 0; NectarNightclubTimesHacked = 0; NeoNightclubMoneyHacked = 0; NeoNightclubTimesHacked = 0; SilverHelixMoneyHacked = 0; SilverHelixTimesHacked = 0; HongFangTeaHouseMoneyHacked = 0; HongFangTeaHouseTimesHacked = 0; HaraKiriSushiBarMoneyHacked = 0; HaraKiriSushiBarTimesHacked = 0; PhantasyMoneyHacked = 0; PhantasyTimesHacked = 0; MaxHardwareMoneyHacked = 0; MaxHardwareTimesHacked = 0; OmegaSoftwareMoneyHacked = 0; OmegaSoftwareTimesHacked = 0; CrushFitnessGymMoneyHacked = 0; CrushFitnessGymTimesHacked = 0; IronGymMoneyHacked = 0; IronGymTimesHacked = 0; MilleniumFitnessGymMoneyHacked = 0; MilleniumFitnessGymTimesHacked = 0; PowerhouseGymMoneyHacked = 0; PowerhouseGymTimesHacked = 0; SnapFitnessGymMoneyHacked = 0; SnapFitnessGymTimesHacked = 0; } ScriptStats.prototype.reset = function() { for (var key in this) { if (this.hasOwnProperty(key)) { this[key] = 0; } } } /* Actual Worker Code */ function WorkerScript() { this.name = ""; this.running = false; this.code = ""; this.env = new Environment(); this.timeout = null; } var scriptStats = new ScriptStats(); var workerForeignServers = null; var workerPlayer = null; var workerScripts = []; self.addEventListener('message', msgRecvHandler); function msgRecvHandler(e) { /* The first element of the data array from main thread specifies * what kind of data it is: * Status Update * Start Script * Stop Script */ if (e.data.type == "Status Update") { console.log("Status update received in Script Web Worker"); ForeignServersScript = JSON.parse(e.data.buf1); PlayerScript = JSON.parse(e.data.buf2); } else if (e.data.type == "Start Script") { var s = new WorkerScript(); s.name = e.data.buf1; s.code = e.data.buf2; workerScripts.push(s); } else if (e.data.type == "Stop Script") { var scriptName = e.data.buf1; for (var i = 0; i < workerScripts.length; i++) { if (workerScripts[i].name == scriptName) { //Stop the script from running and then remove it from workerScripts clearTimeout(workerScripts[i].timeout); workerScripts.splice(i, 1); } } } var ast = Parser(Tokenizer(InputStream(code))); var globalEnv = new Environment(); evaluate(ast, globalEnv); } //Loop through workerScripts and run every script that is not currently running function runScriptsLoop() { console.log("runScriptsLoop() iteration"); for (var i = 0; i < workerScripts.length; i++) { if (workerScripts[i].running == false) { var ast = Parser(Tokenizer(InputStream(workerScripts[i].code))); evaluate(ast, workerScripts[i]); workerScripts[i].running = true; } } setTimeout(runScriptsLoop, 10000); } runScriptsLoop();