Skip to content

Instantly share code, notes, and snippets.

@patrickroberts
Last active May 9, 2022 15:58
Show Gist options
  • Save patrickroberts/35366b0ffb824e533a3bfb8fb28cad31 to your computer and use it in GitHub Desktop.
Save patrickroberts/35366b0ffb824e533a3bfb8fb28cad31 to your computer and use it in GitHub Desktop.
Math expression compiler using PEG.js parser generator to build expression tree
/*
* parser.pegjs -> parser.js
* https://pegjs.org/online
* Use results cache, Optimize parsing speed
*/
// import parser generator for grammar defined in parser.pegjs
const { parse } = require('./parser')
// accepts mapping to convert parameter list into named parameters based on position
const args = (expression, map) => (...args) => expression(map(...args))
// convert string expression into function that accepts parameter list
const compile = (string, map = () => {}) => args(parse(string), map)
// export parser and compiler
module.exports = { parse, compile }
/*
* Example Usage
*/
// import compiler defined in compiler.js
const { compile } = require('./compiler')
// use this mapping for named parameters
// we must also define the identifier for `sqrt`
const map = (a, b, c) => ({ a, b, c, sqrt: Math.sqrt })
// compile these expressions
const positive = compile('[-b+sqrt(b^2-4a c)]/(2a)', map)
const negative = compile('[-b-sqrt(b^2-4a c)]/(2a)', map)
// output solutions as tuple
const solver = (a, b, c) => [positive, negative].map(fn => fn(a, b, c))
// x^2 -x -2 = 0
console.log(solver(1, -1, -2))
// [2, -1]
// factories to initialize value expressions
const constant = (v) => () => v
const variable = (label) => (o) => o[label]
// factory to initialize operator expressions
const operation = (fn) => (...args) => {
// if all arguments of this expression are constant expressions
if (args.every(expression => expression.length === 0)) {
// return constant expression based on precomputed value
return constant(fn(...args.map(expression => expression())))
}
// return variable expression that accepts named parameters
return (o) => fn(...args.map(expression => expression(o)))
}
// initialize operator expressions
const [
add, subtract,
multiply, divide,
mod, power,
negate, invoke
] = [
(a, b) => a + b, (a, b) => a - b,
(a, b) => a * b, (a, b) => a / b,
(a, b) => a % b, (a, b) => a ** b,
(a) => -a, (fn, ...a) => fn(...a)
].map(operation)
// export expression factories and all expressions
module.exports = { operation, constant, variable, add, subtract, multiply, divide, mod, power, negate, invoke }
{
// import all expressions defined in expressions.js
const { constant, variable, add, subtract, multiply, divide, mod, power, negate, invoke } = require('./expressions')
}
Expression 'expression'
= _ expression:TranslateExpression _
{ return expression }
TranslateExpression
= left:ScaleExpression _ operator:TranslateOperator _ right:NextTranslateExpression
{ return right(left, operator) }
/ left:ScaleExpression _ operator:TranslateOperator _ right:ScaleExpression
{ return operator(left, right) }
/ ScaleExpression
NextTranslateExpression
= middle:ScaleExpression _ operator:TranslateOperator _ right:NextTranslateExpression
{ return (left, op) => right(op(left, middle), operator) }
/ middle:ScaleExpression _ operator:TranslateOperator _ right:ScaleExpression
{ return (left, op) => operator(op(left, middle), right) }
TranslateOperator 'operator'
= '+' { return add }
/ '-' { return subtract }
ScaleExpression
= left:NegateExpression _ operator:ScaleOperator _ right:NextScaleExpression
{ return right(left, operator) }
/ left:NegateExpression _ operator:ScaleOperator _ right:NegateExpression
{ return operator(left, right) }
/ left:NegateExpression _ right:NextScaleAfterImplicitExpression
{ return right(left, multiply) }
/ left:NegateExpression _ right:PowerExpression
{ return multiply(left, right) }
/ NegateExpression
NextScaleExpression
= middle:NegateExpression _ operator:ScaleOperator _ right:NextScaleExpression
{ return (left, op) => right(op(left, middle), operator) }
/ middle:NegateExpression _ operator:ScaleOperator _ right:NegateExpression
{ return (left, op) => operator(op(left, middle), right) }
/ middle:NegateExpression _ right:NextScaleAfterImplicitExpression
{ return (left, op) => right(op(left, middle), multiply) }
/ middle:NegateExpression _ right:PowerExpression
{ return (left, op) => multiply(op(left, middle), right) }
NextScaleAfterImplicitExpression
= middle:PowerExpression _ operator:ScaleOperator _ right:NextScaleExpression
{ return (left, op) => right(op(left, middle), operator) }
/ middle:PowerExpression _ operator:ScaleOperator _ right:NegateExpression
{ return (left, op) => operator(op(left, middle), right) }
/ middle:PowerExpression _ right:NextScaleAfterImplicitExpression
{ return (left, op) => right(op(left, middle), multiply) }
/ middle:PowerExpression _ right:PowerExpression
{ return (left, op) => multiply(op(left, middle), right) }
ScaleOperator 'operator'
= '*' { return multiply }
/ '/' { return divide }
/ '%' { return mod }
NegateExpression
= '-' _ expression:NegateExpression
{ return negate(expression) }
/ PowerExpression
PowerExpression
= left:GroupExpression _ PowerOperator _ right:NegateExpression
{ return power(left, right) }
/ GroupExpression
PowerOperator 'operator'
= '^'
/ '**'
GroupExpression 'group'
= '(' expression:Expression ')'
{ return expression }
/ '[' expression:Expression ']'
{ return expression }
/ '{' expression:Expression '}'
{ return expression }
/ Function
Function 'call'
= id:Identifier _ '(' parameters:Parameters ')'
{ return invoke(id, ...parameters) }
/ Value
Parameters 'parameters'
= parameter:Expression ',' rest:Parameters
{ return [parameter].concat(rest) }
/ expression:Expression
{ return [expression] }
/ _
{ return [] }
Value
= Identifier
/ Number
Identifier 'identifier'
= [A-Za-z][A-Za-z0-9]*
{ return variable(text()) }
Number 'number'
= Float ([Ee] [+-]? Integer)?
{ return constant(Number(text())) }
Float
= Integer '.'? Integer?
Integer
= [0-9]+
_ 'whitespace'
= [ \t\r\n]*
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment