This is the source code for parsing and evaluating general conditional expressions.
The parser is based on the Comparse library.
P = require('comparse')First, we define two scanners for separators, mandatory and optional, that permit any combination of whitespace characters.
# Mandatory separator
sep = P.space.skipMany 1
# Optional separator
optSep = P.space.skipMany()We also we add a convenience extension to the parser to simplify
expression of elements enclosed by a common pattern, such as being
between same separators.
P::within = (p) -> @between p, pIn a given conditional expression, you can have numbers, variables, quoted strings, and functions (with or without arguments).
A number can begin with a negative character and contain any sequence
of digits, with at most one decimal dot in between. A decimal number
must have at least one number before/after the ., such as
0.1. The parser will reject .1 and 0. as a valid number.
number = P.char('-').option('').bind (neg) ->
P.digit.concat(1).bind (head) ->
(P.char('.').bind (dot) ->
P.digit.concat(1).bind (tail) ->
P.unit Number (neg + head + dot + tail)
).orElse P.unit Number (neg + head)A variable must begin with a letter or _ character and be followed
by sequence of alphanumeric characters.
variable = (P.letter.orElse P.char '_').bind (fst) ->
P.alphanum.concat().bind (rest) ->
P.unit fst + restA single-quoted string is matched as-is. Need to support double-quoted strings...
quote = P.noneOf("'").concat().between P.char("'"),P.char("'")An argument to a method can contain variable, quoted string, or a number. Nesting of expressions and methods within a method argument is currently not supported.
argument = P.choice(variable, quote, number).between optSep, optSepA method is a function declaration which will produce a result to be
used during expression evaluation. It must be a variable identifier
followed immediately by an ( and closed with ). It can have at
most one argument expression, or left blank.
method = variable.bind (name) ->
P.char('(').bind ->
argument.option().bind (arg) ->
P.char(')').bind ->
P.unit [ name, arg ]A map object of available operator primitives and the associated
evaluation function.
operators =
'*': (x, y=1) -> x * y
'/': (x, y=1) -> x / y
'%': (x, y=1) -> x % y
'+': (x, y=0) -> x + y
'-': (x, y=0) -> x - y
'<': (x, y) -> x < y
'<=': (x, y) -> x <= y
'>': (x, y) -> x > y
'>=': (x, y) -> x >= y
'=': (x, y) -> x == y
'==': (x, y) -> x == y
'div': (x, y=1) -> x / y
'mod': (x, y=1) -> x % y
'or': (x, y) -> Boolean (x or y)
'||': (x, y) -> Boolean (x or y)
'and': (x, y) -> Boolean (x and y)
'&&': (x, y) -> Boolean (x and y)
'!': (x) -> not x
'not': (x) -> not xA helper routine to return a matching operator function from the
operators map object. Each of the scanned operator keywords returns
a function as the immediate result of parsing.
opf = (op) -> P.unit operators[op]Computational expressions perform expected behaviors depending on the types of entity (number, string, etc.) being evaluated.
multiply = P.char('*').within(optSep).bind opf
divide = (P.char('/').within(optSep).orElse P.string('div').within(sep)).bind opf
modulo = (P.char('%').within(optSep).orElse P.string('mod').within(sep)).bind opf
plus = P.char('+').within(optSep).bind opf
minus = P.char('-').within(optSep).bind opfComparison expressions perform equality checks and evaluates to a Boolean result.
lt = P.char('<').within(optSep).bind opf
lte = P.string('<=').within(optSep).bind opf
gt = P.char('>').within(optSep).bind opf
gte = P.string('>=').within(optSep).bind opf
equal = (P.string('==').within(optSep).orElse P.char('=').within(optSep)).bind opfConditional expressions evalutes to a Boolean result.
negate = (P.char('!').orElse P.string('not').between optSep, sep).bind opf
union = (P.char('|').concat(2).within(optSep).orElse P.string('or').within(sep)).bind opf
combine = (P.char('&').concat(2).within(optSep).orElse P.string('and').within(sep)).bind opfThe expression parsing result produces a composition of nested functions that are bound with operands (which themselves can be generated functions).
The magic happens when you execute the function generated by
xparse. When you execute the generated function, you can optionally
pass in a resolver function which can be used to supply values for
variable and method entities in a given expression.
Please refer to the main package README for examples.
evaluate = (x, y, resolver) ->
x = if typeof x is 'function' then x(resolver) else x
y = if typeof y is 'function' then y(resolver) else y
this?(x,y) ? resolver?(x,y)
wrap = (func, x, y=null) ->
kind = switch
when func? then "#{x} #{func.name} #{y}"
when x? and y? then "#{x}(#{y})"
when x? then "#{x}"
f = evaluate.bind func, x, y
f[k] = v for k, v of arguments
Object.defineProperty f, 'toString', value: -> kind
return P.unit fHere we add an extension to the parser to simplify expression of recursive precedence handling. See below Grammer section for usage.
P::step = (before, after) ->
before().bind (x) =>
next = @bind (op) ->
after().bind (y) -> wrap op, x, y
next.option xHighest precedence to lowest precedence:
-
union (logical OR)
-
combine (logical AND)
-
equal
-
lt, lte, gt, gte
-
plus, minus
-
multiply, divide, modulo
-
negate, grouping
expr = -> union.step(term1, expr) term1 = -> combine.step(term2, term1) term2 = -> equal.step(term3, term2) term3 = -> P.choice(lt, lte, gt, gte).step(term4, term3) term4 = -> P.choice(plus, minus).step(term5, term4) term5 = -> P.choice(multiply, divide, modulo).step(factor, term5)
Describing the top-level factor is a bit more complicated. We handle
the negate and grouping case, while scanning for various entities.
factor = ->
P.choice(
negate.bind (op) -> factor().bind (f) -> wrap op, f
P.char('(').bind ->
optSep.bind -> expr().bind (e) -> optSep.bind ->
P.char(')').bind -> P.unit e
method.bind (kv=[]) -> wrap null, kv[0], kv[1]
variable.bind (k) -> wrap null, k
number
quote
)The public API of this module consists of a single parsing function as
the default export.
It's called with text to parse. The result of parse is an
executable function that can then be executed to perform the
evaluation of the expression.
factory = (f) -> p = f(); p.parse.bind p
xparse = factory -> expr().within(optSep)
exports = module.exports = xparse['default'] = xparseHere we expose the various expression parsing rules as a new
ExpressionParser class for making other parsers.
exports.Parser =
class ExpressionParser extends P
@create: factory
@wrap: wrap
# entities
@number: number
@variable: variable
@quote: quote
@argument: argument
@method: method
# operators
@operators: operators
@multiply: multiply
@divide: divide
@modulo: modulo
@plus: plus
@minus: minus
@lt: lt
@lte: lte
@gt: gt
@gte: gte
@equal: equal
@negate: negate
@union: union
@combine: combine