mirror of
https://github.com/Nighthawk42/roll20-character-sheets.git
synced 2026-08-30 11:42:29 +00:00
997 lines
28 KiB
JavaScript
997 lines
28 KiB
JavaScript
'use strict';
|
|
import _ from 'underscore';
|
|
import {PFLog, PFConsole} from './PFLog';
|
|
import TAS from './TheAaronSheet.js';
|
|
|
|
var rollOperator;
|
|
|
|
function parseExpression (s, until) {
|
|
var untilCb = (typeof until === "function" ? until : function (tok) {
|
|
return (tok == until);
|
|
}),
|
|
// constants
|
|
ARG_COUNTS = {
|
|
'abs': 1,
|
|
'ceil': 1,
|
|
'floor': 1,
|
|
'round': 1,
|
|
'max': [1],
|
|
'min': [1]
|
|
},
|
|
BINARY_PRECEDENCE = {
|
|
'?': 1,
|
|
':': 2,
|
|
'||': 3,
|
|
'&&': 4,
|
|
'|': 5,
|
|
'^': 6,
|
|
'&': 7,
|
|
'=': 8,
|
|
'==': 8,
|
|
'!=': 8,
|
|
'>=': 9,
|
|
'>': 9,
|
|
'<': 9,
|
|
'<=': 9,
|
|
'<<': 10,
|
|
'>>': 10,
|
|
'+': 11,
|
|
'-': 11,
|
|
'*': 12,
|
|
'/': 12,
|
|
'%': 12,
|
|
'**': 14,
|
|
't': 98,
|
|
'd': 99
|
|
},
|
|
UNARY_PRECEDENCE = {
|
|
'!': 13,
|
|
'~': 13,
|
|
'-': 13
|
|
},
|
|
CLOSERS = {
|
|
'(': ")",
|
|
'{': "}"
|
|
},
|
|
// local variables
|
|
operators = [{
|
|
'precedence': 0
|
|
}],
|
|
operands = [],
|
|
tableExp,
|
|
m,
|
|
err,
|
|
operand;
|
|
// helper functions
|
|
function getToken(s) {
|
|
var m;
|
|
if (!s) {
|
|
return s;
|
|
}
|
|
|
|
function retVal(tokType, matchObj) {
|
|
return {
|
|
'type': tokType,
|
|
'text': matchObj[0],
|
|
'match': matchObj
|
|
};
|
|
}
|
|
m = s.match(/^\s+/);
|
|
if (m) {
|
|
return retVal("whitespace", m);
|
|
}
|
|
m = s.match(/^(abs|ceil|floor|round|max|min)[(]/);
|
|
if (m) {
|
|
return retVal("function", m);
|
|
}
|
|
m = s.match(/^[({]/);
|
|
if (m) {
|
|
return retVal("opengroup", m);
|
|
}
|
|
m = s.match(/^[)}]/);
|
|
if (m) {
|
|
return retVal("closegroup", m);
|
|
}
|
|
m = s.match(/^((\d+(\.\d+)?)|(\.\d+))/);
|
|
if (m) {
|
|
return retVal("number", m);
|
|
}
|
|
m = s.match(/^['"]/);
|
|
if (m) {
|
|
return retVal("quote", m);
|
|
}
|
|
m = s.match(/^((\|\|)|(&&)|(==)|(!=)|(>=)|(<=)|(<<)|(>>)|(\*\*)|[?:|\^&=><%!~])/);
|
|
if (m) {
|
|
return retVal("extoperator", m);
|
|
}
|
|
m = s.match(/^[\-+*\/td]/);
|
|
if (m) {
|
|
return retVal("baseoperator", m);
|
|
}
|
|
m = s.match(/^\[([^\]]+)\]/);
|
|
if (m) {
|
|
return retVal("label", m);
|
|
}
|
|
m = s.match(/^\$\{([^'"\($\}][^}]*)\}/);
|
|
if (m) {
|
|
return retVal("variable", m);
|
|
}
|
|
m = s.match(/^\$\{/);
|
|
if (m) {
|
|
return retVal("openvariable", m);
|
|
}
|
|
return {
|
|
'type': "raw",
|
|
'text': s.charAt(0)
|
|
};
|
|
}
|
|
|
|
function popToken(state) {
|
|
state.tok = getToken(state.s);
|
|
if (state.tok) {
|
|
state.s = state.s.substring(state.tok.text.length);
|
|
}
|
|
return state;
|
|
}
|
|
|
|
function popString(state, delim) {
|
|
var i = -1,
|
|
j = i,
|
|
retval;
|
|
// find first index of delim not preceded by an odd number of backslashes
|
|
while (((i - j) & 1) === 0) {
|
|
i = state.s.indexOf(delim, i + 1);
|
|
if (i < 0) {
|
|
return;
|
|
}
|
|
j = i - 1;
|
|
while ((j >= 0) && (state.s.charAt(j) === '\\')) {
|
|
j--;
|
|
}
|
|
}
|
|
// unescape string to be returned
|
|
function replaceEscapes(s) {
|
|
return s.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, "\t").replace(/\\/g, "");
|
|
}
|
|
retval = state.s.substring(0, i).split("\\\\").map(replaceEscapes).join("\\");
|
|
// point state delim, then pop off the delimiter token
|
|
state.s = state.s.substring(i);
|
|
popToken(state);
|
|
return retval;
|
|
}
|
|
|
|
function popOperator() {
|
|
var op = operators.pop(),
|
|
right = operands.pop(),
|
|
left,
|
|
cond,
|
|
datatype;
|
|
if (op.unary) {
|
|
operands.push({
|
|
'type': (op.type === "baseoperator" ? "unop" : "unopex"),
|
|
'datatype': right.datatype,
|
|
'operator': op.text,
|
|
'operand': right
|
|
});
|
|
return;
|
|
}
|
|
left = operands.pop();
|
|
if (op.text !== ":") {
|
|
if (op.text === "d" || op.text === "t") {
|
|
datatype = "number";
|
|
} else if (left.datatype === right.datatype) {
|
|
datatype = left.datatype;
|
|
} else if ((left.datatype === "string") || (right.datatype === "string")) {
|
|
datatype = "string";
|
|
}
|
|
operands.push({
|
|
'type': (op.type === "baseoperator" ? "binop" : "binopex"),
|
|
'datatype': datatype,
|
|
'operator': op.text,
|
|
'left': left,
|
|
'right': right,
|
|
'mods': op.mods,
|
|
'label': op.label
|
|
});
|
|
return;
|
|
}
|
|
op = operators.pop();
|
|
if (op.text !== "?") {
|
|
return "Error: Expected ? but got " + op.text;
|
|
}
|
|
cond = operands.pop();
|
|
operands.push({
|
|
'type': "cond",
|
|
'cond': cond,
|
|
'left': left,
|
|
'right': right,
|
|
'datatype': (left.datatype === right.datatype ? left.datatype : undefined)
|
|
});
|
|
}
|
|
|
|
function pushOperator(op) {
|
|
var err;
|
|
op.precedence = (op.unary ? UNARY_PRECEDENCE[op.text] : BINARY_PRECEDENCE[op.text]) || 0;
|
|
while (operators[operators.length - 1].precedence >= op.precedence) {
|
|
err = popOperator();
|
|
if (err) {
|
|
return err;
|
|
}
|
|
}
|
|
operators.push(op);
|
|
}
|
|
|
|
function argListUntil(tok) {
|
|
return (tok === ',') || (tok === ')');
|
|
}
|
|
|
|
function parseHelper() {
|
|
var err,
|
|
func,
|
|
argCounts,
|
|
minArgs,
|
|
maxArgs,
|
|
str,
|
|
args,
|
|
argTree,
|
|
opener,
|
|
closer,
|
|
operand,
|
|
varExp;
|
|
popToken(s);
|
|
if (!s.tok) {
|
|
return "Error: Unrecognized token: " + s.s.split(" ", 1)[0];
|
|
}
|
|
while (s.tok.type === "whitespace") {
|
|
popToken(s);
|
|
if (!s.tok) {
|
|
return "Error: Unrecognized token: " + s.s.split(" ", 1)[0];
|
|
}
|
|
}
|
|
switch (s.tok.type) {
|
|
case "function":
|
|
func = s.tok.match[1];
|
|
argCounts = ARG_COUNTS[func];
|
|
if (argCounts === undefined) {
|
|
return "Error: Unrecognized function: " + func;
|
|
}
|
|
if (Array.isArray(argCounts)) {
|
|
minArgs = argCounts[0];
|
|
maxArgs = argCounts[1];
|
|
} else {
|
|
minArgs = argCounts;
|
|
maxArgs = argCounts;
|
|
}
|
|
args = [];
|
|
while ((s.tok) && (s.tok.text !== ')')) {
|
|
argTree = parseExpression(s, argListUntil);
|
|
if (typeof (argTree) === "string") {
|
|
return argTree;
|
|
} // error
|
|
args.push(argTree);
|
|
if (!s.tok) {
|
|
return "Error: Unterminated function: " + func;
|
|
}
|
|
if (!argListUntil(s.tok.text)) {
|
|
return "Error: Expected ',' or ')' to continue/close '" + func + "(', but got '" + s.tok.text + "'";
|
|
}
|
|
}
|
|
if (minArgs < 0) {
|
|
minArgs = args.length;
|
|
}
|
|
if (isNaN(maxArgs) || maxArgs < 0) {
|
|
maxArgs = args.length;
|
|
}
|
|
if (args.length < minArgs) {
|
|
return "Error: Function '" + func + "' requires at least " + minArgs + " argument(s)";
|
|
}
|
|
if (args.length > maxArgs) {
|
|
return "Error: Function '" + func + "' requires at most " + maxArgs + " argument(s)";
|
|
}
|
|
operands.push({
|
|
'type': "function",
|
|
'datatype': "number",
|
|
'function': func,
|
|
'args': args
|
|
});
|
|
return;
|
|
case "number":
|
|
operands.push({
|
|
'type': "number",
|
|
'datatype': "number",
|
|
'value': parseFloat(s.tok.text)
|
|
});
|
|
return;
|
|
case "variable":
|
|
operands.push({
|
|
'type': "variable",
|
|
'value': s.tok.match[1]
|
|
});
|
|
return;
|
|
case "quote":
|
|
str = popString(s, s.tok.text);
|
|
if (typeof (str) !== "string") {
|
|
return "Error: Unterminated string";
|
|
}
|
|
operands.push({
|
|
'type': "string",
|
|
'datatype': "string",
|
|
'value': str
|
|
});
|
|
return;
|
|
case "opengroup":
|
|
opener = s.tok.text;
|
|
closer = CLOSERS[opener];
|
|
operand = parseExpression(s, closer);
|
|
if (typeof (operand) === "string") {
|
|
return operand;
|
|
} // error
|
|
operands.push(operand);
|
|
if (s.tok.text !== closer) {
|
|
return "Error: Expected '" + closer + "' to close '" + opener + "', but got '" + s.tok.text + "'";
|
|
}
|
|
return;
|
|
case "openvariable":
|
|
varExp = parseExpression(s, "}");
|
|
if (typeof (varExp) === "string") {
|
|
return varExp;
|
|
} // error
|
|
if (s.tok.text !== "}") {
|
|
return "Error: Expected '}' to close '${', but got '" + s.tok.text + "'";
|
|
}
|
|
operands.push({
|
|
'type': "variable",
|
|
'value': varExp
|
|
});
|
|
return;
|
|
case "extoperator":
|
|
case "baseoperator":
|
|
if (!UNARY_PRECEDENCE[s.tok.text]) {
|
|
return "Error: " + s.tok.text + " is not a unary operator";
|
|
}
|
|
s.tok.unary = true;
|
|
err = pushOperator(s.tok);
|
|
if (err) {
|
|
return err;
|
|
}
|
|
return parseHelper();
|
|
}
|
|
return "Error: Unrecognized token: " + s.tok.text + (s.tok.type === "raw" ? s.s.split(" ", 1)[0] : "");
|
|
}
|
|
// if we were given a string, construct a state object
|
|
if (typeof (s) === "string") {
|
|
s = {
|
|
's': s
|
|
};
|
|
}
|
|
// push operators and operands to their respective stacks, building sub-ASTs in the operand stack as needed
|
|
err = parseHelper();
|
|
if (err) {
|
|
return err;
|
|
}
|
|
for (popToken(s) ;
|
|
(s.tok) && (!untilCb(s.tok.text)) && ((until) || (s.tok.type !== "raw")) ; popToken(s)) {
|
|
switch (s.tok.type) {
|
|
case "extoperator":
|
|
case "baseoperator":
|
|
rollOperator = (s.tok.text === "d" ? s.tok : null);
|
|
err = pushOperator(s.tok);
|
|
if (err) {
|
|
return err;
|
|
}
|
|
if ((rollOperator) && (s.s.charAt(0) === 'F')) {
|
|
operands.push({
|
|
'type': "rollspec",
|
|
'value': "F"
|
|
});
|
|
s.s = s.s.substring(1);
|
|
} else if (s.tok.text === "t") {
|
|
if (s.s.charAt(0) !== '[') {
|
|
return "Error: 't' operator requires '[table]' argument";
|
|
}
|
|
m = s.s.match(/^\[([^'"$(\]][^\]]*)\]/);
|
|
if (m) {
|
|
tableExp = m[1];
|
|
s.s = s.s.substring(m[0].length);
|
|
} else {
|
|
s.s = s.s.substring(1);
|
|
tableExp = parseExpression(s, "]");
|
|
if (typeof (tableExp) === "string") {
|
|
return tableExp;
|
|
} // error
|
|
if (s.tok.text !== "]") {
|
|
return "Error: Expected ']' to close 't[', but got '" + s.tok.text + "'";
|
|
}
|
|
}
|
|
operands.push({
|
|
'type': "tablename",
|
|
'value': tableExp
|
|
});
|
|
} else {
|
|
err = parseHelper();
|
|
if (err) {
|
|
return err;
|
|
}
|
|
}
|
|
if (rollOperator) {
|
|
m = s.s.match(/^[acdfhkloprs0-9<=>!]+/);
|
|
if (m) {
|
|
rollOperator.mods = m[0];
|
|
s.s = s.s.substring(m[0].length);
|
|
}
|
|
}
|
|
break;
|
|
case "label":
|
|
if ((operators.length > 0) && (operators[operators.length - 1].text === "d")) {
|
|
// set label on "d" operator instead of operand (e.g. "1d6[foo]" is "(1d6)[foo]", not "1d(6[foo])")
|
|
operators[operators.length - 1].label = s.tok.match[1];
|
|
break;
|
|
}
|
|
operand = operands.pop();
|
|
if (operand) {
|
|
operand.label = s.tok.match[1];
|
|
operands.push(operand);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
// no more input; collapse remaining operators and operands into a single AST
|
|
while (operators.length > 1) {
|
|
err = popOperator();
|
|
if (err) {
|
|
return err;
|
|
}
|
|
}
|
|
return operands.pop();
|
|
}
|
|
export function write (s) {
|
|
TAS.debug("EXEXP:" + s);
|
|
}
|
|
function sendCommand (chunks, asts, evalResults, labels) {
|
|
//infinite loop
|
|
//TAS.debug("at sendCommand");
|
|
//TAS.debug(chunks, asts, evalResults, labels);
|
|
// constants
|
|
var FUNCTION_FUNCTIONS = {
|
|
'abs': Math.abs,
|
|
'ceil': Math.ceil,
|
|
'floor': Math.floor,
|
|
'round': Math.round,
|
|
'max': Math.max,
|
|
'min': Math.min
|
|
},
|
|
BINARY_FUNCTIONS = {
|
|
'||': function (x, y) {
|
|
return x || y;
|
|
},
|
|
'&&': function (x, y) {
|
|
return x && y;
|
|
},
|
|
'|': function (x, y) {
|
|
return x | y;
|
|
},
|
|
'^': function (x, y) {
|
|
return x ^ y;
|
|
},
|
|
'&': function (x, y) {
|
|
return x & y;
|
|
},
|
|
'=': function (x, y) {
|
|
return x == y;
|
|
},
|
|
'==': function (x, y) {
|
|
return x === y;
|
|
},
|
|
'!=': function (x, y) {
|
|
return x != y;
|
|
},
|
|
'>=': function (x, y) {
|
|
return x >= y;
|
|
},
|
|
'>': function (x, y) {
|
|
return x > y;
|
|
},
|
|
'<': function (x, y) {
|
|
return x < y;
|
|
},
|
|
'<=': function (x, y) {
|
|
return x <= y;
|
|
},
|
|
'<<': function (x, y) {
|
|
return x << y;
|
|
},
|
|
'>>': function (x, y) {
|
|
return x >> y;
|
|
},
|
|
'+': function (x, y) {
|
|
return x + y;
|
|
},
|
|
'-': function (x, y) {
|
|
return x - y;
|
|
},
|
|
'*': function (x, y) {
|
|
return x * y;
|
|
},
|
|
'/': function (x, y) {
|
|
return x / y;
|
|
},
|
|
'%': function (x, y) {
|
|
return x % y;
|
|
},
|
|
'**': Math.pow,
|
|
'd': function (x, y) {
|
|
var retval = 0,
|
|
i = 0;
|
|
for (i = 0; i < x; i++) {
|
|
retval += randomInteger(y);
|
|
}
|
|
return retval;
|
|
}
|
|
},
|
|
UNARY_FUNCTIONS = {
|
|
'!': function (x) {
|
|
return !x;
|
|
},
|
|
'~': function (x) {
|
|
return ~x;
|
|
},
|
|
'-': function (x) {
|
|
return -x;
|
|
}
|
|
},
|
|
// local variables
|
|
references = {},
|
|
unevalRefs = [],
|
|
evalReqs = [],
|
|
i = 0,
|
|
t,
|
|
err,
|
|
doSubstitution = false,
|
|
label,
|
|
newUneval = [],
|
|
r,
|
|
retval;
|
|
// helper functions
|
|
function lazyEval(t, labels, references, unevalRefs, evalReqs, force) {
|
|
//alert(' at lazyEval, t: ' + t + ', t.type:'+t.type);
|
|
var x,
|
|
y,
|
|
args = [],
|
|
i = 0,
|
|
forceSubtrees;
|
|
if (t.label) {
|
|
labels[t.label] = t;
|
|
}
|
|
switch (t.type) {
|
|
case "number":
|
|
case "rollspec":
|
|
t.baseValid = true;
|
|
return t;
|
|
case "string":
|
|
return t;
|
|
case "tablename":
|
|
if (typeof (t.value) !== "string") {
|
|
x = lazyEval(t.value, labels, references, unevalRefs, evalReqs, true);
|
|
if (typeof (x) === "string") {
|
|
return x;
|
|
} // error
|
|
if (x.type === "number") {
|
|
// number node; coerce to string
|
|
x.value = String(x.value);
|
|
x.type = "string";
|
|
}
|
|
if (x.type !== "string") {
|
|
// unable to fully evaluate table name
|
|
if (t.baseValid) {
|
|
t.baseValid = false;
|
|
}
|
|
unevalRefs.push(t.value);
|
|
return t;
|
|
}
|
|
// successfully evaluated table name
|
|
t.value = x.value;
|
|
}
|
|
// if we got here, t.value is the name of a rollable table
|
|
t.baseValid = true;
|
|
return t;
|
|
case "function":
|
|
for (i = 0; i < t.args.length; i++) {
|
|
x = lazyEval(t.args[i], labels, references, unevalRefs, evalReqs, true);
|
|
if (typeof (x) === "string") {
|
|
return x;
|
|
} // error
|
|
if (x.type === "string") {
|
|
x.value = parseFloat(x.value);
|
|
x.type = "number";
|
|
}
|
|
if (x.type !== "number") {
|
|
// unable to fully evaluate argument
|
|
if (t.baseValid) {
|
|
t.baseValid = false;
|
|
}
|
|
return t;
|
|
}
|
|
args.push(x.value);
|
|
}
|
|
// successfully evaluated all arguments
|
|
t.type = "number";
|
|
t.datatype = "number";
|
|
t.value = FUNCTION_FUNCTIONS[t["function"]].apply(args, args);
|
|
for (i = 0; i < t.args.length; i++) {
|
|
if (t.args[i].label) {
|
|
labels[t.args[i].label] = t.args[i];
|
|
}
|
|
}
|
|
delete t["function"];
|
|
delete t.args;
|
|
t.baseValid = true;
|
|
return t;
|
|
case "unop":
|
|
case "unopex":
|
|
force = force || (t.type !== "unop");
|
|
x = lazyEval(t.operand, labels, references, unevalRefs, evalReqs, force);
|
|
if (typeof (x) === "string") {
|
|
return x;
|
|
} // error
|
|
if (force) {
|
|
if (x.type !== "number") {
|
|
// unable to fully evaluate operand
|
|
if (t.baseValid) {
|
|
t.baseValid = false;
|
|
}
|
|
return t;
|
|
}
|
|
// successfully evaluated operand
|
|
t.type = "number";
|
|
t.datatype = "number";
|
|
t.value = UNARY_FUNCTIONS[t.operator](x.value);
|
|
delete t.operator;
|
|
if (t.operand.label) {
|
|
labels[t.operand.label] = x;
|
|
}
|
|
delete t.operand;
|
|
t.baseValid = true;
|
|
} else {
|
|
t.baseValid = x.baseValid;
|
|
}
|
|
return t;
|
|
case "binop":
|
|
case "binopex":
|
|
force = force || (t.type !== "binop") || (t.left.datatype === "string") || (t.right.datatype === "string");
|
|
forceSubtrees = force || (t.operator === "d") || (t.operator === "t");
|
|
//TAS.debug('left is: ' + t.left + ', right is:' + t.right);
|
|
x = lazyEval(t.left, labels, references, unevalRefs, evalReqs, forceSubtrees);
|
|
y = lazyEval(t.right, labels, references, unevalRefs, evalReqs, forceSubtrees);
|
|
//TAS.debug(x);
|
|
//TAS.debug(y);
|
|
force = true;
|
|
/*********************didn't work until i commented out, now seems to have no effect ********************************/
|
|
if (typeof x === "string") {
|
|
//TAS.debug(x);
|
|
return x;
|
|
} // error
|
|
if (typeof y === "string") {
|
|
//TAS.debug(y);
|
|
return y;
|
|
} // error
|
|
/****************************************************/
|
|
if (force) {
|
|
if ((x.type !== "number") && (x.type !== "string")) {
|
|
// unable to fully evaluate left operand
|
|
if (t.baseValid) {
|
|
t.baseValid = false;
|
|
}
|
|
return t;
|
|
}
|
|
if ((y.type !== "number") && (y.type !== "string") && (y.type !== "rollspec") && (y.type !== "tablename")) {
|
|
// unable to fully evaluate right operand
|
|
if (t.baseValid) {
|
|
t.baseValid = false;
|
|
}
|
|
return t;
|
|
}
|
|
if ((y.type === "rollspec") && (t.operator !== "d")) {
|
|
return "Rollspec operand is only compatible with 'd' operator";
|
|
}
|
|
if ((t.operator === "t") && (y.type !== "tablename")) {
|
|
return "'t' operator requires tablename operand";
|
|
}
|
|
// successfully evaluated both operands
|
|
if ((t.operator === "t") || ((t.operator === "d") && (t.mods))) {
|
|
// operator is rollable table or is roll with mods; must submit to base system for evaluation
|
|
evalReqs.push(t);
|
|
return t;
|
|
}
|
|
//TAS.debug('about to call binary');
|
|
t.value = BINARY_FUNCTIONS[t.operator](x.value, y.value);
|
|
delete t.operator;
|
|
if (t.left.label) {
|
|
labels[t.left.label] = x;
|
|
}
|
|
delete t.left;
|
|
if (t.right.label) {
|
|
labels[t.right.label] = y;
|
|
}
|
|
delete t.right;
|
|
t.type = (typeof (t.value) === "string" ? "string" : "number");
|
|
t.datatype = t.type;
|
|
t.baseValid = (t.datatype === "number");
|
|
} else if ((x.datatype === "number") && (y.datatype === "number")) {
|
|
t.datatype = "number";
|
|
t.baseValid = true;
|
|
}
|
|
return t;
|
|
case "cond":
|
|
x = lazyEval(t.cond, labels, references, unevalRefs, evalReqs, true);
|
|
if (typeof (x) === "string") {
|
|
return x;
|
|
} // error
|
|
if ((x.type !== "number") && (x.type !== "string")) {
|
|
// unable to fully evaluate condition
|
|
t.baseValid = false;
|
|
return t;
|
|
}
|
|
// successfully evaluated condition; replace t with t.left or t.right as appropriate
|
|
y = (x.value ? t.left : t.right);
|
|
if (t.cond.label) {
|
|
labels[t.cond.label] = x;
|
|
}
|
|
delete t.cond;
|
|
delete t.left;
|
|
delete t.right;
|
|
_.each(y, function (k) {
|
|
t[k] = y[k];
|
|
});
|
|
return lazyEval(t, labels, references, unevalRefs, evalReqs, force);
|
|
case "variable":
|
|
if (typeof (t.value) !== "string") {
|
|
x = lazyEval(t.value, labels, references, unevalRefs, evalReqs, true);
|
|
if (typeof (x) === "string") {
|
|
return x;
|
|
} // error
|
|
if (x.type === "number") {
|
|
// number node; coerce to string
|
|
x.value = String(x.value);
|
|
x.type = "string";
|
|
}
|
|
if (x.type !== "string") {
|
|
// unable to fully evaluate variable name
|
|
if (t.baseValid) {
|
|
t.baseValid = false;
|
|
}
|
|
unevalRefs.push(t.value);
|
|
return t;
|
|
}
|
|
// successfully evaluated variable name
|
|
t.value = x.value;
|
|
}
|
|
// if we got here, t.value is the name of a variable
|
|
if ((labels[t.value]) && ((labels[t.value].type === "string") || (labels[t.value].type === "number"))) {
|
|
// variable exists and has been fully evaluated
|
|
t.type = labels[t.value].type;
|
|
t.datatype = labels[t.value].datatype;
|
|
t.baseValid = labels[t.value].baseValid;
|
|
t.value = labels[t.value].value;
|
|
} else {
|
|
// variable not yet defined or not yet fully evaluated
|
|
if (!references[t.value]) {
|
|
references[t.value] = [];
|
|
}
|
|
references[t.value].push(t);
|
|
if (t.baseValid) {
|
|
t.baseValid = false;
|
|
}
|
|
}
|
|
return t;
|
|
default:
|
|
return "Unknown node type: " + t.type;
|
|
}
|
|
}
|
|
|
|
function hasUnevaluatedLabels(t) {
|
|
var i = 0;
|
|
// base types: fully evaluated
|
|
if ((t.type === "number") || (t.type === "string") || (t.type === "rollspec")) {
|
|
return false;
|
|
}
|
|
// if we got here, node is unevaluated
|
|
if (t.label) {
|
|
return true;
|
|
}
|
|
// node has no label; check children
|
|
switch (t.type) {
|
|
case "function":
|
|
for (i = 0; i < t.args.length; i++) {
|
|
if (hasUnevaluatedLabels(t.args[i])) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
case "tablename":
|
|
case "variable":
|
|
if (typeof (t.value) === "string") {
|
|
return false;
|
|
}
|
|
return hasUnevaluatedLabels(t.value);
|
|
case "unop":
|
|
case "unopex":
|
|
return hasUnevaluatedLabels(t.operand);
|
|
case "cond":
|
|
if (hasUnevaluatedLabels(t.cond)) {
|
|
return true;
|
|
}
|
|
//don't fall through
|
|
if (hasUnevaluatedLabels(t.left)) {
|
|
return true;
|
|
}
|
|
return hasUnevaluatedLabels(t.right);
|
|
case "binop":
|
|
case "binopex":
|
|
if (hasUnevaluatedLabels(t.left)) {
|
|
return true;
|
|
}
|
|
return hasUnevaluatedLabels(t.right);
|
|
}
|
|
}
|
|
|
|
function flattenAST(t) {
|
|
var retval;
|
|
switch (t.type) {
|
|
case "number":
|
|
case "rollspec":
|
|
retval = t.value || 0;
|
|
break;
|
|
case "tablename":
|
|
retval = "[" + t.value + "]";
|
|
break;
|
|
case "unop":
|
|
retval = "(" + t.operator + flattenAST(t.operand) + ")";
|
|
break;
|
|
case "binop":
|
|
retval = "(" + flattenAST(t.left) + t.operator + flattenAST(t.right) + (t.mods || "") + ")";
|
|
if ((t.label) && (t.operator === "d")) {
|
|
retval += "[" + t.label + "]";
|
|
}
|
|
break;
|
|
default:
|
|
return "Unknown node type: " + t.type;
|
|
}
|
|
return retval;
|
|
}
|
|
|
|
function astToCmd(t) {
|
|
if (t.type === "string") {
|
|
return t.value;
|
|
}
|
|
var retval = flattenAST(t);
|
|
return retval;
|
|
}
|
|
|
|
function reportError(err) {
|
|
write("Error: " + err);
|
|
return "";
|
|
}
|
|
//BEGIN
|
|
// substitute in results of base evaluation
|
|
for (i = 0; i < evalResults.length; i++) {
|
|
t = evalResults[i][0];
|
|
delete t.operator;
|
|
delete t.left;
|
|
delete t.right;
|
|
t.type = "number";
|
|
t.datatype = "number";
|
|
t.value = evalResults[i][1];
|
|
t.baseValid = true;
|
|
}
|
|
// traverse ASTs, collapsing as much as possible
|
|
for (i = 0; i < asts.length; i++) {
|
|
if (asts[i].baseValid) {
|
|
continue;
|
|
} // can be handled by base expression evaluator
|
|
if ((asts[i].type === "string") || (asts[i].type === "number")) {
|
|
continue;
|
|
} // tree is fully evaluated
|
|
err = lazyEval(asts[i], labels, references, unevalRefs, evalReqs, false);
|
|
if (typeof (err) === "string") {
|
|
return reportError(err);
|
|
}
|
|
}
|
|
// do variable substitution; repeat until we don't make any more progress
|
|
doSubstitution = true;
|
|
while (doSubstitution) {
|
|
doSubstitution = false;
|
|
// substitute in values for variables for which we already have names
|
|
for (label in references) {
|
|
if (references.hasOwnProperty(label)) {
|
|
if (!labels[label]) {
|
|
return reportError("Variable '" + label + "' not defined");
|
|
}
|
|
if ((labels[label].type !== "string") && (labels[label].type !== "number")) {
|
|
// variable exists but not yet evaluated; try to evaluate
|
|
err = lazyEval(labels[label], labels, references, unevalRefs, evalReqs, true);
|
|
if (typeof (err) === "string") {
|
|
return reportError(err);
|
|
}
|
|
} else if ((labels[label].type === "string") || (labels[label].type === "number")) {
|
|
// variable fully evaluated; substitute it in
|
|
for (i = 0; i < references[label].length; i++) {
|
|
references[label][i].type = labels[label].type;
|
|
references[label][i].datatype = labels[label].datatype;
|
|
references[label][i].value = labels[label].value;
|
|
references[label][i].baseValid = labels[label].baseValid;
|
|
}
|
|
delete references[label];
|
|
doSubstitution = true;
|
|
}
|
|
}
|
|
}
|
|
// try to get names for variables and tables with unevaluated names
|
|
while (unevalRefs.length > 0) {
|
|
r = lazyEval(unevalRefs.shift(), labels, references, unevalRefs, evalReqs, true);
|
|
if (typeof (r) === "string") {
|
|
return reportError(err);
|
|
}
|
|
if ((r.type === "string") || (r.type === "number")) {
|
|
doSubstitution = true;
|
|
} else {
|
|
newUneval.push(r);
|
|
}
|
|
}
|
|
unevalRefs = newUneval;
|
|
}
|
|
// flatten fully evaluated ASTs into strings and splice into chunks
|
|
for (i = asts.length - 1; i >= 0; i--) {
|
|
if ((!asts[i].baseValid) && (asts[i].type !== "number") && (asts[i].type !== "string")) {
|
|
continue;
|
|
}
|
|
if ((unevalRefs.length > 0) & (hasUnevaluatedLabels(asts[i]))) {
|
|
continue;
|
|
}
|
|
chunks.splice(i, 2, (chunks[i] || "") + astToCmd(asts.splice(i, 1)[0]) + (chunks[i + 1] || ""));
|
|
}
|
|
if (evalReqs.length > 0) {
|
|
TAS.error("Cannot evaluate");
|
|
return "";
|
|
}
|
|
if (asts.length > 0) {
|
|
// need to finish evaluating some ASTs; recurse directly
|
|
//TAS.debug("sendCommand (recurse), asts.length=" + asts.length + ", asts[0].baseValid=" + asts[0].baseValid + ", asts[0].type=" + asts[0].type);
|
|
if (!(asts.length === 1 && asts[0].type === "binop")) {
|
|
// HACK! minus (probably) in front; no math needed
|
|
return sendCommand(chunks, asts, [], labels);
|
|
}
|
|
}
|
|
// if we got here, we're done evaluating everything; submit results
|
|
retval = chunks.join("");
|
|
return retval;
|
|
}
|
|
export function handleExpression (msg) {
|
|
//replace spaces. replace "-" in front with "0-", replace "(-" with "(0-"
|
|
//also replace leading + with '', and replace (+ with (0+
|
|
var chunks = [],
|
|
asts = [],
|
|
cmd,
|
|
state,
|
|
ast;
|
|
msg = msg.replace(/\s/g, '').replace(/^-/, '0-').replace(/\(-/g, '(0-').replace(/^\+/, '').replace(/\(\+/g, '(0+');
|
|
cmd = msg;
|
|
state = {
|
|
's': cmd
|
|
};
|
|
//TAS.debug(msg);
|
|
ast = parseExpression(state, null);
|
|
//TAS.debug(ast);
|
|
if (typeof (ast) === "string") {
|
|
write("could not parse" + msg);
|
|
return "";
|
|
}
|
|
asts.push(ast);
|
|
state.s = (state.tok) ? (state.tok.text + state.s) : state.s;
|
|
// ((state.tok || {'text': ""}).text || "") + state.s;
|
|
chunks.push(state.s);
|
|
return sendCommand(chunks, asts, [], {});
|
|
}
|
|
|
|
//PFConsole.log(' ExExp module loaded ');
|
|
//PFLog.modulecount++;
|