Skip to content

Latest commit

 

History

History
248 lines (190 loc) · 7.93 KB

File metadata and controls

248 lines (190 loc) · 7.93 KB

xparse - flexible expression parsing library for user defined conditionals

This is the source code for parsing and evaluating general conditional expressions.

The parser is based on the Comparse library.

P = require('comparse')

Separators

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, p

Entities

In 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 + rest

A 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, optSep

A 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 ]

Operations

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 x

A 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]

Computations

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 opf

Comparisons

Comparison 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 opf

Conditionals

Conditional 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 opf

Evaluator

The 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 f

Here 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 x

Grammer

Highest 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
  )

Parsing Function

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'] = xparse

Here 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