Created
July 12, 2021 07:01
-
-
Save userhooke/800f83c900c506469f193a1863f1480f to your computer and use it in GitHub Desktop.
simple lexer, parser and evaluator to run calculations
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
const assert = require('assert').strict; | |
const readline = require('readline'); | |
const TOKEN_TYPES = { | |
INT: 'INT', | |
PLUS: 'PLUS', | |
MINUS: 'MINUS', | |
MUL: 'MUL', | |
DIV: 'DIV', | |
LPAREN: 'LPAREN', | |
RPAREN: 'RPAREN', | |
EOF: 'EOF' | |
} | |
const pipe = (...funcs) => value => | |
funcs.reduce((value, func) => func(value), value); | |
const pop = array => array.shift(); | |
const isInt = char => !isNaN(parseInt(char)); | |
const isPlus = char => char === '+'; | |
const isMinus = char => char === '-'; | |
const isMul = char => char === '*'; | |
const isDiv = char => char === '/'; | |
const isLParen = char => char === '(' | |
const isRParen = char => char === ')' | |
const isWhitespace = char => /\s/g.test(char); | |
const isTypeInt = token => token.type === TOKEN_TYPES.INT; | |
const isTypePlus = token => token.type === TOKEN_TYPES.PLUS; | |
const isTypeMinus = token => token.type === TOKEN_TYPES.MINUS; | |
const isTypeMul = token => token.type === TOKEN_TYPES.MUL; | |
const isTypeDiv = token => token.type === TOKEN_TYPES.DIV; | |
const isTypeLParen = token => token.type === TOKEN_TYPES.LPAREN; | |
const isTypeRParen = token => token.type === TOKEN_TYPES.RPAREN; | |
const isTypeEOF = token => token.type === TOKEN_TYPES.EOF; | |
const createToken = (type, value) => { | |
if (!TOKEN_TYPES[type]) { | |
throw new Error(`${type} type is not valid`) | |
} else { | |
return { | |
type, | |
value | |
} | |
} | |
} | |
/** | |
* takes an input and converts it into a stream of tokens | |
*/ | |
const lexer = input => { | |
let cursor = 0; | |
const tokens = []; | |
while(cursor < input.length) { | |
let symbol = input[cursor] | |
if (isInt(symbol)) { | |
while(isInt(input[cursor+1])) { | |
symbol += input[cursor+1]; | |
cursor++; | |
} | |
tokens.push(createToken(TOKEN_TYPES.INT, parseInt(symbol))); | |
cursor++; | |
} else if (isWhitespace(symbol)) { | |
cursor++; | |
} else if (isPlus(symbol)) { | |
tokens.push(createToken(TOKEN_TYPES.PLUS, '+')) | |
cursor++; | |
} else if (isMinus(symbol)) { | |
tokens.push(createToken(TOKEN_TYPES.MINUS, '-')) | |
cursor++; | |
} else if (isMul(symbol)) { | |
tokens.push(createToken(TOKEN_TYPES.MUL, '*')) | |
cursor++; | |
} else if (isDiv(symbol)) { | |
tokens.push(createToken(TOKEN_TYPES.DIV, '/')) | |
cursor++; | |
} else if (isLParen(symbol)) { | |
tokens.push(createToken(TOKEN_TYPES.LPAREN, '(')) | |
cursor++; | |
} else if (isRParen(symbol)) { | |
tokens.push(createToken(TOKEN_TYPES.RPAREN, ')')) | |
cursor++; | |
} else { | |
throw new Error(`"${symbol}" is illegal character`) | |
} | |
} | |
tokens.push(createToken('EOF', 'EOF')) | |
return tokens; | |
} | |
/* | |
expr : term ((PLUS | MINUS) term)* | |
term : factor ((MUL | DIV) factor)* | |
factor : INTEGER | LPAREN expr RPAREN | |
*/ | |
const factor = tokens => { | |
const token = pop(tokens) | |
if (isTypeInt(token)) { | |
return token.value | |
} else if (isTypeLParen(token)) { | |
return expr(tokens) | |
} | |
} | |
const term = tokens => { | |
let result = factor(tokens) | |
let cursor = 0; | |
while (tokens[cursor] && (isTypeMul(tokens[cursor]) || isTypeDiv(tokens[cursor]))) { | |
const token = pop(tokens) | |
if (isTypeMul(token)) { | |
result = result * factor(tokens) | |
} else if (isTypeDiv(token)) { | |
result = result / factor(tokens) | |
} | |
cursor++ | |
} | |
return result | |
} | |
const expr = tokens => { | |
let result = term(tokens) | |
let cursor = 0; | |
while (tokens[cursor] && (isTypePlus(tokens[cursor]) || isTypeMinus(tokens[cursor]))) { | |
const token = pop(tokens) | |
if (isTypePlus(token)) { | |
result = result + term(tokens) | |
} else if (isTypeMinus(token)) { | |
result = result - term(tokens) | |
} | |
cursor++; | |
} | |
return result | |
} | |
const calc = pipe( | |
lexer, | |
expr | |
) | |
assert.strictEqual(calc('7 + 3 * (10 / (12 / (3 + 1) - 1)) / (2 + 3) - 5 - 3 + (8)'), 10) | |
assert.strictEqual(calc("7 + 3 * (10 / (12 / (3 + 1) - 1))"), 22) | |
assert.strictEqual(calc('2 + 2 * 2'), 6) | |
assert.strictEqual(calc('2 + 2 - 2'), 4) | |
assert.strictEqual(calc('7 + (((3 + 2)))'), 12) | |
const rl = readline.createInterface({ | |
input: process.stdin, | |
output: process.stdout | |
}); | |
const recursiveAsyncReadLine = () => { | |
rl.question('calc> ', expr => { | |
const output = calc(expr); | |
console.log(output) | |
recursiveAsyncReadLine() | |
}) | |
} | |
recursiveAsyncReadLine() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment