UNPKG

mcs

Version:

A pre-processor to write Minecraft Functions more efficiently

887 lines (820 loc) 28.2 kB
/* Advanced Parser InputStream reads characters TokenStream is the lexer (converts everything into tokens) Parser tries to create node structures (AST) out of the tokens Parser based on: http://lisperator.net/pltut/ */ // List of all commands that exist in mc var availableCommands = ["advancement", "ban", "blockdata", "clear", "clone", "debug", "defaultgamemode", "deop", "difficulty", "effect", "enchant", "entitydata", "execute", "fill", "function", "gamemode", "gamerule", "give", "help", "kick", "kill", "list", "locate", "me", "op", "pardon", "particle", "playsound", "publish", "recipe", "reload", "replaceitem", "save", "say", "scoreboard", "seed", "setblock", "setidletimeout", "setmaxplayers", "setworldspawn", "spawnpoint", "spreadplayers", "stats", "stop", "stopsound", "summon", "teleport", "tell", "tellraw", "testfor", "testforblock", "testforblocks", "time", "title", "toggledownfall", "tp", "transferserver", "trigger", "weather", "whitelist", "worldborder", "wsserver", "xp"]; // InputStream (Read input character by character) function InputStream(input) { var pos = 0, line = 1, col = 0, lastVal = null, lastWasNewLineVal = true; return { next: next, peek: peek, eof: eof, croak: croak, last: last, lastWasNewLine: lastWasNewLine }; function next() { // Knows whether or not we switched to a new line if (peek() == "\n") lastWasNewLineVal = true; else if ("\r\t ".indexOf(peek()) == -1) lastWasNewLineVal = false; // Get the last character lastVal = peek(); var ch = input.charAt(pos++); if (ch == "\n") line++, col = 0; else col++; return ch; } function last() { return lastVal; } function lastWasNewLine() { return lastWasNewLineVal; } function peek() { return input.charAt(pos); } function eof() { return peek() == ""; } function croak(msg) { var err = msg + ' at (' + line + ':' + col + ')'; console.error(err); throw new Error(err); } } // Lexer (converts everything into tokens) function TokenStream(input) { var current = null; // List of all keywords that are available var keywords = " function macro group if elseif else return execute true false var for foreach in "; var lastVal = null; return { next: next, peek: peek, eof: eof, croak: input.croak, last: last }; function is_keyword(x) { return keywords.indexOf(" " + x + " ") >= 0; } function is_digit(ch) { return /[0-9]/i.test(ch); } function is_id_start(ch) { return /[a-z0-9_\$]/i.test(ch); } function is_id(ch) { return is_id_start(ch); } function is_ivar(ch) { return /\$[a-z0-9-_]/i.test(ch); } function is_op_char(ch) { return "+-*/%^=&|<>!".indexOf(ch) >= 0; } function is_punc(ch) { return ",;(){}[]".indexOf(ch) >= 0; } function is_whitespace(ch) { return " \t\n\r".indexOf(ch) >= 0; } // Read until the given predicate returns false function read_while(predicate) { var str = ""; while (!input.eof() && predicate(input.peek())) str += input.next(); return str; } function try_number() { input.next(); if (is_digit(input.peek())) { var num = read_number(); num.value *= -1; return num; } input.croak("Can't handle character: " + input.peek()); } function read_number() { var has_dot = false; 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) }; } // Read identifiers, can return a keyword, an ivar ($variable), or reg (anything else) function read_ident() { var id = read_while(is_id); var type; if (is_keyword(id)) type = "kw"; else if (is_ivar(id)) type = "ivar" else type = "reg"; return { type: type, value: id }; } function read_escaped(end) { var escaped = false, str = ""; input.next(); 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; } /* Evaluation blocks Inside a string, content inside `` will be parsed as if it was normal syntax. This allows for easier variable/macro integration: "math result: `math(1,2) + 2`" rather than "math result: " + (math(1,2) + 2) (Although the second option is still available if you need it). */ function read_evaled(val) { // Don't do it if it doesn't need evaluation if (val.indexOf("`") >= 0) { var evalBlock = false, final = [], str = ""; var arr = val.split(''); for (var i = 0; i < arr.length; i++) { var ch = arr[i]; // Currently in an eval block if (evalBlock) { if (ch == '`') { evalBlock = false; // Parse the whole thing as if it was a full code block var parsedEval = Parser(TokenStream(InputStream(str))); if (parsedEval.prog.length != 0) { for (var x = 0; x < parsedEval.prog.length; x++) { if (parsedEval.prog[x].type == "comment") { input.croak("Comments are not allowed in evaluation blocks"); } else if (parsedEval.prog[x].type == "function") { input.croak("Functions are not allowed in evaluation blocks"); } else if (parsedEval.prog[x].type == "macro") { input.croak("Creating macros is not allowed in evaluation blocks"); } } final.push(parsedEval); } str = ""; } else { str += ch; if (i == arr.length - 1) { if (str) final.push({ type: "str", value: str }); } } } // Don't evalBlock else { if (ch == '`') { evalBlock = true; if (str) final.push({ type: "str", value: str }); str = ""; } else { str += ch; if (i == arr.length - 1) { if (str) final.push({ type: "str", value: str }); } } } } return final; } else { return val; } } function read_string() { return { type: "str", value: read_evaled(read_escaped('"')) }; } function selector_or_setting() { var lastNewLine = input.lastWasNewLine(); input.next(); if (input.peek() == '!') { if (!lastNewLine) input.croak('Settings with "@!" need to start at the begining of a line'); return read_settings(); } else { return read_selector(); } } function read_selector() { var output = read_while(function(ch) { return (!is_whitespace(ch) && ch != ';') }); return { type: 'selector', value: '@' + output }; } function read_settings() { var output = read_while(function(ch) { return ch != "\n" }); return { type: "setting", value: output.replace('\r', '') }; } function read_relative() { var val = read_while(function(ch) { return (!is_whitespace(ch) && ch != ";"); }); return { type: 'relative', value: val }; } function read_colon() { input.next(); return { type: 'colon' }; } // Read comments that need to be added (#) function read_comment() { if (!input.lastWasNewLine()) input.croak('Comments with "#" need to start at the begining of a line'); var output = read_while(function(ch) { return ch != "\n" }); return { type: "comment", value: output.replace('\r', '') }; } function skip_comment() { read_while(function(ch) { return ch != "\n" }); input.next(); } // Check whether or not the line is a // comment, skip it if so function check_comment() { var output = read_while(function(ch) { return ch == "/" }); if (output == '//') { skip_comment(); } else { input.next(); } } // Read the next character, assign tokens function read_next() { read_while(is_whitespace); if (input.eof()) return null; var ch = input.peek(); if (ch == "#") { return read_comment(); } if (ch == "/") { check_comment(); return read_next(); } if (ch == "@") return selector_or_setting(); if (ch == '"') return read_string(); if (ch == "~") return read_relative(); if (ch == ":") return read_colon(); 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) }; input.croak("Can't handle character: " + ch); } function peek() { return current || (current = read_next()); } function last() { return lastVal; } function next() { lastVal = peek(); var tok = current; current = null; return tok || read_next(); } function eof() { return peek() == null; } } // Parser (actually parses data) var FALSE = { type: "bool", value: false }; function Parser(input) { // Order of math operations, greater means included first var PRECEDENCE = { "=": 1, "||": 2, "&&": 3, "<": 7, ">": 7, "<=": 7, ">=": 7, "==": 7, "!=": 7, "+": 10, "-": 10, "*": 20, "/": 20, "%": 20, "^": 30, }; return parse_toplevel(); function is_punc(ch) { var tok = input.peek(); return tok && tok.type == "punc" && (!ch || tok.value == ch) && tok; } function is_kw(kw) { var tok = input.peek(); return tok && tok.type == "kw" && (!kw || tok.value == kw) && tok; } function is_op(op) { var tok = input.peek(); return tok && tok.type == "op" && (!op || tok.value == op) && tok; } function is_comment() { var tok = input.peek(); return tok && tok.type == "comment"; } function is_reg() { var tok = input.peek(); return tok && tok.type == "reg"; } function skip_punc(ch) { if (is_punc(ch)) input.next(); else input.croak("Expecting punctuation: \"" + ch + "\""); } function skip_comment(ch) { if (is_comment()) parse_comment(); input.next(); } function skip_kw(kw) { if (is_kw(kw)) input.next(); else input.croak("Expecting keyword: \"" + kw + "\""); } function skip_op(op) { if (is_op(op)) input.next(); else input.croak("Expecting operator: \"" + op + "\""); } function skip_comma() { if (is_punc(",")) { input.next(); return { type: "comma" }; } else input.croak("Expecting comma: \"" + JSON.stringify(input.peek()) + "\"") } function unexpected() { input.croak("Unexpected token: " + JSON.stringify(input.peek())); } // Check whether or not to parse this through binary operations 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; } // Parses through anything between start and stop, with separator, using the given parser function delimited(start, stop, separator, parser) { var a = [], first = true; skip_punc(start); while (!input.eof()) { if (is_punc(stop)) break; if (first) first = false; else if (check_last()) skip_punc(separator); if (is_punc(stop)) break; a.push(parser()); } skip_punc(stop); return a; } function parse_call(func) { return { type: "call", func: func, args: delimited("(", ")", ",", parse_expression), }; } // Variable names can't be ivar nor keyword, check that it's a reg function parse_varname() { var name = input.next(); if (name.type != "ivar") input.croak("Expecting variable name"); return name.value; } // Parse if statements, add elseif if there are some, and add else if there is one function parse_if() { skip_kw("if"); var cond = parse_expression(); var then = parse_expression(); var ret = { type: "if", cond: cond, then: then, }; if (is_kw("else")) { input.next(); ret.else = parse_expression(); } return ret; } // Parse a var declaration function parse_var() { skip_kw("var"); return { type: 'var', value: parse_varname() }; } // Parse a for loop function parse_for() { //input.croak("For loops are currently not supported"); skip_kw("for"); var params = delimited("(", ")", ";", parse_expression); var then = parse_expression(); return { type: "for", params: params, then: then, }; } // Parse a foreach loop function parse_foreach() { skip_kw("foreach"); skip_punc("("); skip_kw("var"); var varName = parse_ivar(); skip_kw("in"); var param = parse_expression(); skip_punc(")"); var then = parse_expression(); return { type: "foreach", variable: varName, param: param, then: then }; } /* Parse a function This can be taken in two ways: 1. actual function declaration ( function name { } ) 2. minecraft function command ( function name [if/unless...] ) Therefore, testing if there are reg arguments following it, if so, it's Option 2. */ function parse_function() { // Skip the function keyword input.next(); // Get the name of the function var name = input.next(); // Check if what's afterwards is a call if (input.peek().type == 'colon') { var obj = { type: "command", value: [{ type: "reg", value: "function" }, name ] }; // Loop through it to add all of the arguments while (!input.eof()) { if (input.peek().type == "kw") obj.value.push(input.next()); else obj.value.push(parse_expression()); if (is_punc(';')) break; else if (input.eof()) skip_punc(';'); } return obj; } else { // It's not a call to a function, parse it as a normal function return { type: "function", name: name.value, body: parse_expression() }; } } // Parsing a group (sub namespaces/folders) function parse_group() { input.next(); return { type: "group", name: input.next().value, body: parse_expression() }; } // Parse a macro, basically a function with parameters function parse_macro() { input.next(); return { type: "macro", name: input.next().value, vars: delimited("(", ")", ",", parse_varname), body: parse_expression() }; } // Return statements function parse_return() { input.next(); return { type: "return", value: parse_expression() }; } function parse_execute() { input.next(); var final = { type: 'execute', selector: '', pos: [] }; var tokenCount = 0; while (!input.eof()) { if (tokenCount == 5) { break; } else { var expr = parse_expression(); if (tokenCount == 0) final.selector = expr; else if (tokenCount > 0 && tokenCount < 4) final.pos.push(expr); else if (tokenCount == 4) final.prog = expr; tokenCount++; } } return final; } // Bool just checks if the value is "true" function parse_bool() { return { type: "bool", value: input.next().value == "true" }; } // Parse an ivar and detect whether or not it's asking for an index of an array function parse_ivar() { var ivar = input.next(); if (is_punc("[")) { skip_punc("["); while (!input.eof()) { if (is_punc("]")) break; if (input.peek().type == "kw") unexpected(); ivar.index = parse_expression(); if (is_punc("]")) break; else input.croak('Expecting punctuation: "]"'); } skip_punc("]"); } return ivar; } // Parsing an array declaration [5, "string", false] function parse_array() { return { type: "array", value: delimited("[", "]", ",", function custom_parser() { var i = input.peek(); // Don't allow keywords such as function, var... inside an array declaration // Macro is allowed to allow for variable functions (lambdas) ($fn = macro call () { something; }) if (i.type == "kw" && i.value != "false" && i.value != "true" && i.value != "macro") unexpected(); else return parse_expression(); }) }; } // Parsing a comment as an actual comment function parse_comment() { return { type: "comment", value: input.next().value }; } function parse_setting() { var setting = input.next().value.trim(); var indexSeparator = setting.indexOf(':'); if (indexSeparator == -1) input.croak('Expecting separator: ":"'); return { type: 'setting', name: setting.substring(setting.indexOf('!') + 1, indexSeparator).trim(), value: setting.substring(indexSeparator + 1).trim() }; } // Relatives need to check for variables inside function parse_relative() { // Parse the relative's content var quickInput = TokenStream(InputStream(input.next().value.substring(1))); var final = []; while (!quickInput.eof()) { final.push(quickInput.next()); } return { type: 'relative', value: final }; } // Selectors need to check for variables inside function parse_selector() { var next = input.next(); var final = []; // If the selector has arguments, parse them if (next.value.indexOf("[") > -1) { var valueToParse = next.value.substring(3, next.value.length - 1); var quickInput = TokenStream(InputStream(valueToParse)); while (!quickInput.eof()) { final.push(quickInput.next()); } } return { type: 'selector', prefix: next.value.substring(0, 2), value: final }; } /* Parsing reg is complicated Most of the time, a reg is simply a minecraft command and its arguments, it checks if it's an actual command, and if so, it returns the full command Sometimes, it's the name of a macro or other function calling, if so return the exact token If it's neither of those, then it's unexpected */ var lala; function parse_reg() { // Regs are commands and command arguments var final = { type: "command", value: [] }; if (availableCommands.includes(input.peek().value)) { final.value.push(input.next()); while (!input.eof()) { var next = parse_expression(); final.value.push(next); if (is_punc(';')) break; else if (input.eof()) skip_punc(';'); } return final; } else { return input.next(); //unexpected(); } } // Check whether or not the given token is a JSON or a program function json_or_prog() { skip_punc("{"); var a = {}, first = true; // Check if the next item is a string if (input.peek().type == "str") { a = { type: "json", value: [] }; while (!input.eof()) { if (is_punc("}")) break; if (first) first = false; else if (input.peek().type == "colon") first = true; else a.value.push(skip_comma());; if (is_punc("}")) break; a.value.push(parse_expression()); } skip_punc("}"); return a; } // Regular program else { a = { type: "prog", prog: [] }; while (!input.eof()) { if (is_punc("}")) break; if (first) first = false; else if (check_last()) skip_punc(";"); if (is_punc("}")) break; a.prog.push(parse_expression()); } skip_punc("}"); if (a.prog.length == 0) return FALSE; if (a.prog.length == 1) return a.prog[0]; return a; } } function maybe_call(expr) { expr = expr(); return is_punc("(") ? parse_call(expr) : expr; } // Major parser, checks what the token is an tells it to how to parse it function parse_atom() { return maybe_call(function() { if (is_punc("(")) { input.next(); var exp = parse_expression(); skip_punc(")"); return exp; } if (is_punc("{")) return json_or_prog(); if (is_punc("[")) return parse_array(); if (is_kw("if")) return parse_if(); if (is_kw("var")) return parse_var(); if (is_kw("true") || is_kw("false")) return parse_bool(); if (is_kw("for")) return parse_for(); if (is_kw("foreach")) return parse_foreach(); if (is_kw("function")) return parse_function(); if (is_kw("group")) return parse_group(); if (is_kw("execute")) return parse_execute(); if (is_kw("macro")) return parse_macro(); if (is_kw("return")) return parse_return(); if (is_comment()) return parse_comment(); if (input.peek().type == 'reg') return parse_reg(); if (input.peek().type == 'setting') return parse_setting(); if (input.peek().type == "ivar") return parse_ivar(); if (input.peek().type == 'relative') return parse_relative(); if (input.peek().type == 'selector') return parse_selector(); var tok = input.next(); if (tok.type == 'colon' || tok.type == "num" || tok.type == "str") return tok; unexpected(); }); } // Utility to check whether or not the last one was a comment or a setting (use it to prevent requirement of semicolon) function check_last() { return (!input.last() || (input.last() && input.last().type != "comment" && input.last().type != "setting")); } // Parsing a program/top level function parse_toplevel() { var prog = []; while (!input.eof()) { prog.push(parse_expression()); // Comments are special because they don't require a ; at the end, so we need to check that it's not a comment if (is_comment()) { prog.push(parse_comment()); } else if (!input.eof() && check_last()) skip_punc(";"); } return { type: "prog", prog: prog }; } /* UNUSED but keep for clarity // Parse through a full program function parse_prog() { var prog = delimited("{", "}", ";", parse_expression); if (prog.length == 0) return FALSE; if (prog.length == 1) return prog[0]; return { type: "prog", prog: prog };}*/ // Parse through everything, parse binary and calls just in case function parse_expression() { return maybe_call(function() { return maybe_binary(parse_atom(), 0); }); } }