Skip to content

Instantly share code, notes, and snippets.

@patrickgalbraith
Last active May 17, 2023 01:17
Show Gist options
  • Save patrickgalbraith/0c749a66d2d5a26c86824cad9503ee46 to your computer and use it in GitHub Desktop.
Save patrickgalbraith/0c749a66d2d5a26c86824cad9503ee46 to your computer and use it in GitHub Desktop.

Simple PEG.JS grammer to parse logical expressions like:

test in(1, "2") and (flag = true or test ~ "test" or price >= 100.00 and (total > 100 or total < 200))

Which will output an AST that looks like:

{
   "type": "expression",
   "left": {
      "type": "functionTerm",
      "variable": "test",
      "function": "in",
      "args": [
         1,
         "2"
      ]
   },
   "operator": "and",
   "right": {
      "type": "group",
      "body": {
         "type": "expression",
         "left": {
            "type": "term",
            "variable": "flag",
            "operator": "=",
            "value": true
         },
         "operator": "or",
         "right": {
            "type": "expression",
            "left": {
               "type": "term",
               "variable": "test",
               "operator": "contains",
               "value": "test"
            },
            "operator": "or",
            "right": {
               "type": "expression",
               "left": {
                  "type": "term",
                  "variable": "price",
                  "operator": ">=",
                  "value": 100
               },
               "operator": "and",
               "right": {
                  "type": "group",
                  "body": {
                     "type": "expression",
                     "left": {
                        "type": "term",
                        "variable": "total",
                        "operator": ">",
                        "value": 100
                     },
                     "operator": "or",
                     "right": {
                        "type": "term",
                        "variable": "total",
                        "operator": "<",
                        "value": 200
                     }
                  }
               }
            }
         }
      }
   }
}

It also supports escaping " characters using \.

Grammer

start
  = Expression

Expression
  = left:(Term / FunctionTerm / GroupedExpression) Whitespace op:LogicalOperator Whitespace right:Expression {
      return {
        type: 'expression',
        left: left,
        operator: op,
        right: right
      };
    }
  / Term
  / FunctionTerm
  / GroupedExpression

GroupedExpression
  = "(" Whitespace expr:Expression Whitespace ")" {
      return {
        type: 'group',
        body: expr
      };
    }

Term
  = variable:Variable Whitespace operator:ComparisonOperator Whitespace value:Value {
      return {
        type: 'term',
        variable: variable,
        operator: operator,
        value: value
      };
    }
    
FunctionName
  = "in"i { return 'in'; }
  / "!in"i { return '!in'; }

FunctionTerm
  = variable:Variable Whitespace functionName:FunctionName Whitespace "(" Whitespace head:Value tail:(Whitespace "," Whitespace value:Value { return value; })* Whitespace ")" {
      return {
        type: 'functionTerm',
        variable: variable,
        function: functionName,
        args: [head].concat(tail)
      };
    }

LogicalOperator
  = "and"i { return 'and'; }
  / "or"i { return 'or'; }

ComparisonOperator
  = "=" { return '='; }
  / "!=" { return '!='; }
  / ">=" { return '>='; }
  / "<=" { return '<='; }
  / ">" { return '>'; }
  / "<" { return '<'; }
  / "startswith" { return 'startswith'; }
  / "endswith" { return 'endswith'; }
  / "contains" { return 'contains'; }
  / "!startswith" { return '!startswith'; }
  / "!endswith" { return '!endswith'; }
  / "!contains" { return '!contains'; }
  / "~" { return 'contains'; }
  / "!~" { return '!contains'; }

Variable
  = IdentifierCharacter+ { return text(); }

Value
  = Boolean
  / Number
  / String

Boolean
  = "true" { return true; }
  / "false" { return false; }

Number
  = Float
  / Integer

Integer
  = Digit+ { return parseInt(text(), 10); }

Float
  = left:Digit+ "." right:Digit+ { return parseFloat(text()); }
  
String
  = '"' chars:(EscapedChar / NormalChar)* '"' { return chars.join(''); }

EscapedChar
  = '\\"' { return '"'; }  // Return the actual character, not the escape sequence

NormalChar
  = [^"]  // Any character that is not a quote

Digit
  = [0-9]

IdentifierCharacter
  = [a-zA-Z0-9_$]

Whitespace
  = [ \t\n\r]*
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment