Created
March 16, 2015 21:01
-
-
Save 1995eaton/d04b12e689616e339dd0 to your computer and use it in GitHub Desktop.
Shunting Yard parser
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var readline = require('readline'); | |
LOG = console.log.bind(console); | |
var NUMBER = /([0-9]+\.[0-9]*|\.[0-9]+|[0-9]+)(e[-+]?[0-9]+)?/; | |
function Operator(associativity, precidence) { | |
this.asso = associativity; | |
this.prec = precidence; | |
} | |
var Fns = { | |
rad: function(n) { | |
return n * Math.PI / 180; | |
}, | |
deg: function(n) { | |
return n * 180 / Math.PI; | |
}, | |
ln: Math.log | |
}; | |
Object.getOwnPropertyNames(Math).forEach(function(e) { | |
if (typeof Math[e] === 'function') { | |
Fns[e] = Math[e]; | |
} | |
}); | |
Fns.log = function(n, base) { | |
if (typeof base === 'undefined') | |
return Math.log(n); | |
return Math.log(n) / Math.log(base); | |
}; | |
var Constants = { | |
PI: Math.PI, | |
pi: Math.PI, | |
E: Math.E, | |
e: Math.E, | |
PHI: 1.618033988749895, | |
phi: 1.618033988749895, | |
Infinity: Infinity, | |
inf: Infinity, | |
}; | |
var op = { | |
'&&': new Operator('Left', 6), | |
'||': new Operator('Left', 5), | |
'+': new Operator('Left', 13), | |
'-': new Operator('Left', 13), | |
'**': new Operator('Left', 15), | |
'*': new Operator('Left', 14), | |
'/': new Operator('Left', 14), | |
'%': new Operator('Left', 14), | |
'&': new Operator('Left', 9), | |
'^': new Operator('Left', 8), | |
'|': new Operator('Left', 7), | |
'!=': new Operator('Left', 11), | |
'!': new Operator('Right', 15), | |
'~': new Operator('Right', 15), | |
'#': new Operator('Right', 15), // UNARY_MINUS | |
'@': new Operator('Right', 15), // UNARY_PLUS | |
'<<': new Operator('Left', 12), | |
'>>': new Operator('Left', 12), | |
'<=': new Operator('Left', 11), | |
'<': new Operator('Left', 11), | |
'>=': new Operator('Left', 11), | |
'>': new Operator('Left', 11), | |
'==': new Operator('Left', 11), | |
}; | |
var parse = function(tokens) { | |
var ops = [], | |
out = []; | |
for (var i = 0, t; i < tokens.length; i++) { | |
if (/[0-9.]+/.test(t = tokens[i])) { | |
out.push(t); | |
continue; | |
} | |
switch (t) { | |
case '(': | |
ops.push('('); | |
break; | |
case ')': | |
while (ops.length && (t = ops.pop()) !== '(') { | |
out.push(t); | |
} | |
break; | |
default: | |
if (!op.hasOwnProperty(t)) { | |
if (Constants.hasOwnProperty(t)) { | |
out.push(Constants[t]); | |
break; | |
} | |
} | |
if (op[t].asso === 'Left') { | |
while (ops.length && ops[ops.length - 1] !== '(' && | |
op[t].prec <= op[ops[ops.length - 1]].prec) { | |
out.push(ops.pop()); | |
} | |
} else { | |
while (ops.length && ops[ops.length - 1] !== '(' && | |
op[t].prec < op[ops[ops.length - 1]].prec) { | |
out.push(ops.pop()); | |
} | |
} | |
ops.push(t); | |
} | |
} | |
return out.concat(ops.reverse()); | |
}; | |
function splitParameters(params) { | |
params = params.split(''); | |
var d = 0; | |
var r = []; | |
var s = ''; | |
for (var i = 0; i < params.length; i++) { | |
var c = params[i]; | |
if (c === '(') | |
d++; | |
else if (c === ')') | |
d--; | |
else if (c === ',' && !d) { | |
r.push(s); | |
s = ''; | |
continue; | |
} | |
s += c; | |
} | |
r.push(s); | |
return r; | |
} | |
function parseFunctionCall(call) { | |
var name = call.replace(/\(.*/, ''); | |
var params = splitParameters(call.slice(name.length + 1, -1)); | |
params = params.map(function(e) { | |
e = tokenize(e); | |
return evaluate(parse(e)); | |
}); | |
return Fns[name].apply(null, params); | |
} | |
function replaceKeywords(input) { | |
return input.replace(/\btrue\b/g, '!0') | |
.replace(/\bfalse\b/g, '!1') | |
.replace(/\bNaN\b/g, '(0/0)'); | |
} | |
function tokenize(input) { | |
var result = []; | |
input = replaceKeywords(input); | |
for (var constant in Constants) { | |
input = input.replace(new RegExp('(^|[^a-zA-Z.0-9])' + constant + | |
'($|[^a-zA-Z.0-9])', 'g'), '$1' + Constants[constant] + '$2'); | |
} | |
while (/[a-zA-Z]+\(/.test(input)) { | |
var start = input.search(/[a-zA-Z]+\(/); | |
var paren = start; | |
while (input.charAt(paren) !== '(') { | |
paren += 1; | |
} | |
var end = paren; | |
var depth = 0; | |
do { | |
if (input.charAt(end) === '(') { | |
depth += 1; | |
} else if (input.charAt(end) === ')') { | |
depth -= 1; | |
} | |
if (end >= input.length) | |
throw Error('Unclosed parentheses'); | |
end += 1; | |
} while (depth); | |
var call = parseFunctionCall(input.slice(start, end)); | |
input = input.slice(0, start) + call + input.slice(end); | |
input = replaceKeywords(input); | |
} | |
while (input.length) { | |
input = input.replace(/^\s*/, ''); | |
var found = false; | |
if (new RegExp('^' + NUMBER.source).test(input)) { | |
input = input.replace(NUMBER, function(e) { | |
result.push(+e); | |
return ''; | |
}); | |
continue; | |
} | |
var e; | |
for (e in op) { | |
if (input.indexOf(e) === 0) { | |
if ((e === '-' || e === '+') && | |
!NUMBER.test(result[result.length - 1]) && | |
result[result.length - 1] !== ')' && | |
!Constants.hasOwnProperty(result[result.length - 1])) | |
{ | |
// UNARY_MINUS/PLUS | |
result.push(e === '-' ? '#' : '@'); | |
} else { | |
result.push(e); | |
} | |
input = input.slice(e.length); | |
found = true; | |
break; | |
} | |
} | |
for (e in Constants) { | |
if (input.indexOf(e) === 0) { | |
result.push(Constants[e]); | |
input = input.slice(e.length); | |
found = true; | |
break; | |
} | |
} | |
if (!found) { | |
result.push(input.charAt(0)); | |
input = input.slice(1); | |
} | |
} | |
return result; | |
} | |
function evaluate(stack) { | |
var result = []; | |
while (stack.length) { | |
var token = stack.shift(); | |
if (NUMBER.test(token) || Constants.hasOwnProperty(token)) { | |
result.push(+token); | |
continue; | |
} | |
var b = result.pop(); | |
switch (token) { | |
case '!': result.push(!b); continue; | |
case '~': result.push(~b); continue; | |
case '#': result.push(-b); continue; | |
case '@': result.push(+b); continue; | |
} | |
var e, a = result.pop(); | |
if (a === void 0) { | |
throw Error('Unexpected end of line'); | |
} | |
switch (token) { | |
case '+': e = a + b; break; | |
case '-': e = a - b; break; | |
case '/': e = a / b; break; | |
case '*': e = a * b; break; | |
case '%': e = a % b; break; | |
case '^': e = a ^ b; break; | |
case '&': e = a & b; break; | |
case '|': e = a | b; break; | |
case '<': e = a < b; break; | |
case '>': e = a > b; break; | |
case '<=': e = a <= b; break; | |
case '>=': e = a >= b; break; | |
case '<<': e = a << b; break; | |
case '>>': e = a >> b; break; | |
case '&&': e = a && b; break; | |
case '||': e = a || b; break; | |
case '==': e = a === b; break; | |
case '!=': e = a !== b; break; | |
case '**': e = Math.pow(a, b); break; | |
} | |
result.push(e); | |
} | |
if (result.length !== 1) { | |
throw Error('Unexpected token'); | |
} | |
return result.shift(); | |
} | |
(function() { | |
var printLine = function(line) { | |
LOG(line.toString().replace(/Infinity/g, 'inf')); | |
}; | |
var rl = readline.createInterface({ | |
input: process.stdin, | |
output: process.stdout | |
}); | |
(function promptLine() { | |
rl.question(': ', function(e) { | |
try { | |
if (/^[\sa-zA-Z]+=[^=]/.test(e)) { | |
var name = e.replace(/=.*/, '').trim(); | |
Constants[name] = | |
evaluate(parse(tokenize(e.replace(/^[^=]+=/, '')))); | |
printLine(Constants[name]); | |
} else { | |
var t = tokenize(e); | |
var p = parse(t); | |
var r = evaluate(p); | |
printLine(r); | |
} | |
} catch (ex) { | |
LOG('ERROR'); | |
} | |
promptLine(); | |
}); | |
})(); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment