mirror of
https://github.com/hanzoai/expr.git
synced 2026-09-24 16:58:58 +00:00
522 lines
11 KiB
Go
522 lines
11 KiB
Go
package expr
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
type associativity int
|
|
|
|
const (
|
|
left associativity = iota + 1
|
|
right
|
|
)
|
|
|
|
type info struct {
|
|
precedence int
|
|
associativity associativity
|
|
}
|
|
|
|
var unaryOperators = map[string]info{
|
|
"not": {50, left},
|
|
"!": {50, left},
|
|
"-": {500, left},
|
|
"+": {500, left},
|
|
}
|
|
|
|
var binaryOperators = map[string]info{
|
|
"or": {10, left},
|
|
"||": {10, left},
|
|
"and": {15, left},
|
|
"&&": {15, left},
|
|
"|": {16, left},
|
|
"^": {17, left},
|
|
"&": {18, left},
|
|
"==": {20, left},
|
|
"!=": {20, left},
|
|
"<": {20, left},
|
|
">": {20, left},
|
|
">=": {20, left},
|
|
"<=": {20, left},
|
|
"not in": {20, left},
|
|
"in": {20, left},
|
|
"matches": {20, left},
|
|
"..": {25, left},
|
|
"+": {30, left},
|
|
"-": {30, left},
|
|
"~": {40, left},
|
|
"*": {60, left},
|
|
"/": {60, left},
|
|
"%": {60, left},
|
|
"**": {200, right},
|
|
}
|
|
|
|
type parser struct {
|
|
input string
|
|
tokens []token
|
|
position int
|
|
current token
|
|
options *options
|
|
}
|
|
|
|
type options struct {
|
|
names map[string]struct{}
|
|
funcs map[string]struct{}
|
|
}
|
|
|
|
// OptionFn for configuring parser.
|
|
type OptionFn func(p *options)
|
|
|
|
// Parse parses input into ast.
|
|
func Parse(input string, ops ...OptionFn) (Node, error) {
|
|
tokens, err := lex(input)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
p := &parser{
|
|
input: input,
|
|
tokens: tokens,
|
|
current: tokens[0],
|
|
options: &options{},
|
|
}
|
|
|
|
for _, op := range ops {
|
|
op(p.options)
|
|
}
|
|
|
|
node, err := p.parseExpression(0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !p.isEOF() {
|
|
return nil, p.errorf("unexpected token %v", p.current)
|
|
}
|
|
return node, nil
|
|
}
|
|
|
|
// Names sets list of allowed names.
|
|
func Names(names ...string) OptionFn {
|
|
return func(o *options) {
|
|
set := make(map[string]struct{})
|
|
for _, name := range names {
|
|
set[name] = struct{}{}
|
|
}
|
|
o.names = set
|
|
}
|
|
}
|
|
|
|
// Funcs sets list of allowed function.
|
|
func Funcs(funcs ...string) OptionFn {
|
|
return func(o *options) {
|
|
set := make(map[string]struct{})
|
|
for _, fn := range funcs {
|
|
set[fn] = struct{}{}
|
|
}
|
|
o.funcs = set
|
|
}
|
|
}
|
|
|
|
func (p *parser) errorf(format string, args ...interface{}) error {
|
|
return &syntaxError{
|
|
message: fmt.Sprintf(format, args...),
|
|
input: p.input,
|
|
pos: p.current.pos,
|
|
}
|
|
}
|
|
|
|
func (p *parser) next() error {
|
|
p.position++
|
|
if p.position >= len(p.tokens) {
|
|
return p.errorf("unexpected end of expression")
|
|
}
|
|
p.current = p.tokens[p.position]
|
|
return nil
|
|
}
|
|
|
|
func (p *parser) expect(kind tokenKind, values ...string) error {
|
|
if p.current.is(kind, values...) {
|
|
return p.next()
|
|
}
|
|
return p.errorf("unexpected token %v", p.current)
|
|
}
|
|
|
|
func (p *parser) isEOF() bool {
|
|
return p.current.is(eof)
|
|
}
|
|
|
|
// parse functions
|
|
|
|
func (p *parser) parseExpression(precedence int) (Node, error) {
|
|
node, err := p.parsePrimary()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
token := p.current
|
|
for token.is(operator) {
|
|
if op, ok := binaryOperators[token.value]; ok {
|
|
if op.precedence >= precedence {
|
|
if err = p.next(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var expr Node
|
|
if op.associativity == left {
|
|
expr, err = p.parseExpression(op.precedence + 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
expr, err = p.parseExpression(op.precedence)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
node = binaryNode{operator: token.value, left: node, right: expr}
|
|
token = p.current
|
|
continue
|
|
}
|
|
}
|
|
break
|
|
}
|
|
|
|
if precedence == 0 {
|
|
node, err = p.parseConditionalExpression(node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return node, nil
|
|
}
|
|
|
|
func (p *parser) parsePrimary() (Node, error) {
|
|
token := p.current
|
|
|
|
if token.is(operator) {
|
|
if op, ok := unaryOperators[token.value]; ok {
|
|
if err := p.next(); err != nil {
|
|
return nil, err
|
|
}
|
|
expr, err := p.parseExpression(op.precedence)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return p.parsePostfixExpression(unaryNode{operator: token.value, node: expr})
|
|
}
|
|
}
|
|
|
|
if token.is(punctuation, "(") {
|
|
if err := p.next(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
expr, err := p.parseExpression(0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = p.expect(punctuation, ")")
|
|
if err != nil {
|
|
return nil, p.errorf("an opened parenthesis is not properly closed")
|
|
}
|
|
|
|
return p.parsePostfixExpression(expr)
|
|
}
|
|
|
|
return p.parsePrimaryExpression()
|
|
}
|
|
|
|
func (p *parser) parseConditionalExpression(node Node) (Node, error) {
|
|
var err error
|
|
var expr1, expr2 Node
|
|
for p.current.is(punctuation, "?") {
|
|
if err := p.next(); err != nil {
|
|
return nil, err
|
|
}
|
|
if !p.current.is(punctuation, ":") {
|
|
expr1, err = p.parseExpression(0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := p.expect(punctuation, ":"); err != nil {
|
|
return nil, err
|
|
}
|
|
expr2, err = p.parseExpression(0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
if err := p.next(); err != nil {
|
|
return nil, err
|
|
}
|
|
expr1 = node
|
|
expr2, err = p.parseExpression(0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
node = conditionalNode{node, expr1, expr2}
|
|
}
|
|
return node, nil
|
|
}
|
|
|
|
func (p *parser) parsePrimaryExpression() (Node, error) {
|
|
var err error
|
|
var node Node
|
|
token := p.current
|
|
switch token.kind {
|
|
case name:
|
|
if err := p.next(); err != nil {
|
|
return nil, err
|
|
}
|
|
switch token.value {
|
|
case "true":
|
|
return boolNode{value: true}, nil
|
|
case "false":
|
|
return boolNode{value: false}, nil
|
|
case "nil":
|
|
return nilNode{}, nil
|
|
default:
|
|
if p.current.is(punctuation, "(") {
|
|
if p.options.funcs != nil {
|
|
if _, ok := p.options.funcs[token.value]; !ok {
|
|
return nil, p.errorf("unknown func %v", token.value)
|
|
}
|
|
}
|
|
arguments, err := p.parseArguments()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
node = functionNode{name: token.value, arguments: arguments}
|
|
} else {
|
|
if p.options.names != nil {
|
|
if _, ok := p.options.names[token.value]; !ok {
|
|
return nil, p.errorf("unknown name %v", token.value)
|
|
}
|
|
}
|
|
node = nameNode{name: token.value}
|
|
}
|
|
}
|
|
|
|
case number:
|
|
if err := p.next(); err != nil {
|
|
return nil, err
|
|
}
|
|
number, err := strconv.ParseFloat(token.value, 64)
|
|
if err != nil {
|
|
return nil, p.errorf("%v", err)
|
|
}
|
|
return numberNode{value: number}, nil
|
|
|
|
case text:
|
|
if err := p.next(); err != nil {
|
|
return nil, err
|
|
}
|
|
return textNode{value: token.value}, nil
|
|
|
|
default:
|
|
if token.is(punctuation, "[") {
|
|
node, err = p.parseArrayExpression()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else if token.is(punctuation, "{") {
|
|
node, err = p.parseMapExpression()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
return nil, p.errorf("unexpected token %v", token)
|
|
}
|
|
}
|
|
|
|
return p.parsePostfixExpression(node)
|
|
}
|
|
|
|
func (p *parser) parseArrayExpression() (Node, error) {
|
|
nodes, err := p.parseList("array items", "[", "]")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return arrayNode{nodes}, nil
|
|
}
|
|
|
|
func (p *parser) parseMapExpression() (Node, error) {
|
|
err := p.expect(punctuation, "{")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
nodes := make([]pairNode, 0)
|
|
for !p.current.is(punctuation, "}") {
|
|
if len(nodes) > 0 {
|
|
err = p.expect(punctuation, ",")
|
|
if err != nil {
|
|
return nil, p.errorf("a map value must be followed by a comma")
|
|
}
|
|
}
|
|
|
|
var key Node
|
|
// a map key can be:
|
|
// * a number
|
|
// * a text
|
|
// * a name, which is equivalent to a string
|
|
// * an expression, which must be enclosed in parentheses -- (1 + 2)
|
|
if p.current.is(number) || p.current.is(text) || p.current.is(name) {
|
|
key = identifierNode{p.current.value}
|
|
if err := p.next(); err != nil {
|
|
return nil, err
|
|
}
|
|
} else if p.current.is(punctuation, "(") {
|
|
key, err = p.parseExpression(0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
return nil, p.errorf("a map key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token %v)", p.current)
|
|
}
|
|
|
|
err = p.expect(punctuation, ":")
|
|
if err != nil {
|
|
return nil, p.errorf("a map key must be followed by a colon (:)")
|
|
}
|
|
|
|
node, err := p.parseExpression(0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nodes = append(nodes, pairNode{key, node})
|
|
}
|
|
|
|
err = p.expect(punctuation, "}")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return mapNode{nodes}, nil
|
|
}
|
|
|
|
func (p *parser) parsePostfixExpression(node Node) (Node, error) {
|
|
token := p.current
|
|
for token.is(punctuation) {
|
|
if token.value == "." {
|
|
if err := p.next(); err != nil {
|
|
return nil, err
|
|
}
|
|
token = p.current
|
|
if err := p.next(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if token.kind != name &&
|
|
// Operators like "not" and "matches" are valid method or property names,
|
|
//
|
|
// In other words, besides name token kind, operator kind could also be parsed as a property or method.
|
|
// This is because operators are processed by the lexer prior to names. So "not" in "foo.not()"
|
|
// or "matches" in "foo.matches" will be recognized as an operator first. But in fact, "not"
|
|
// and "matches" in such expressions shall be parsed as method or property names.
|
|
//
|
|
// And this ONLY works if the operator consists of valid characters for a property or method name.
|
|
//
|
|
// Other types, such as text kind and number kind, can't be parsed as property nor method names.
|
|
//
|
|
// As a result, if token is NOT an operator OR token.value is NOT a valid property or method name,
|
|
// an error shall be returned.
|
|
(token.kind != operator || !isValidIdentifier(token.value)) {
|
|
return nil, p.errorf("expected name")
|
|
}
|
|
|
|
property := identifierNode{value: token.value}
|
|
|
|
if p.current.is(punctuation, "(") {
|
|
arguments, err := p.parseArguments()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
node = methodNode{node: node, property: property, arguments: arguments}
|
|
} else {
|
|
node = propertyNode{node: node, property: property}
|
|
}
|
|
|
|
} else if token.value == "[" {
|
|
|
|
if err := p.next(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
arg, err := p.parseExpression(0)
|
|
if err != nil {
|
|
return err, nil
|
|
}
|
|
|
|
node = propertyNode{node: node, property: arg}
|
|
|
|
err = p.expect(punctuation, "]")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
} else {
|
|
break
|
|
}
|
|
|
|
token = p.current
|
|
}
|
|
return node, nil
|
|
}
|
|
|
|
func isValidIdentifier(str string) bool {
|
|
if len(str) == 0 {
|
|
return false
|
|
}
|
|
h, w := utf8.DecodeRuneInString(str)
|
|
if !isAlphabetic(h) {
|
|
return false
|
|
}
|
|
for _, r := range str[w:] {
|
|
if !isAlphaNumeric(r) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (p *parser) parseArguments() ([]Node, error) {
|
|
return p.parseList("arguments", "(", ")")
|
|
}
|
|
|
|
func (p *parser) parseList(what, start, end string) ([]Node, error) {
|
|
err := p.expect(punctuation, start)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
nodes := make([]Node, 0)
|
|
for !p.current.is(punctuation, end) {
|
|
if len(nodes) > 0 {
|
|
err = p.expect(punctuation, ",")
|
|
if err != nil {
|
|
return nil, p.errorf("%v must be separated by a comma", what)
|
|
}
|
|
}
|
|
node, err := p.parseExpression(0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nodes = append(nodes, node)
|
|
}
|
|
|
|
err = p.expect(punctuation, end)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return nodes, nil
|
|
}
|