package expr
import (
"fmt"
"regexp"
"strings"
"sync"
"unicode"
)
// Value is a dynamically-typed expression value.
type Value struct {
isStr bool
s string
n float64
}
// S constructs a string value.
func S(s string) Value { return Value{isStr: true, s: s} }
// N constructs a numeric value.
func N(n float64) Value { return Value{n: n} }
// String returns the underlying string (numbers are formatted).
func (v Value) String() string {
if v.isStr {
return v.s
}
return fmt.Sprintf("%g", v.n)
}
// Env is the evaluation environment supplying identifier lookups.
type Env interface {
Lookup(name string) (Value, bool)
}
// Eval walks the tree. Unknown identifiers resolve to empty string. A
// regex operator compiles to a package-level cache.
func Eval(n *Node, env Env) bool {
switch n.Kind {
case "and":
return Eval(n.Children[0], env) && Eval(n.Children[1], env)
case "or":
return Eval(n.Children[0], env) || Eval(n.Children[1], env)
case "not":
return !Eval(n.Children[0], env)
case "cmp":
return evalCmp(n, env)
}
return false
}
func evalCmp(n *Node, env Env) bool {
left := resolve(n.Children[0], env)
right := resolve(n.Children[1], env)
switch n.Op {
case "=":
return left.String() == right.String()
case "!=":
return left.String() != right.String()
case "~":
re := compileRegex(right.String())
return re != nil && re.MatchString(left.String())
case "<", "<=", ">", ">=":
return numericCmp(left, right, n.Op)
}
return false
}
func numericCmp(a, b Value, op string) bool {
// Coerce both to float; non-parsable strings become NaN which always
// fails the comparison.
fa, okA := toFloat(a)
fb, okB := toFloat(b)
if !okA || !okB {
return false
}
switch op {
case "<":
return fa < fb
case "<=":
return fa <= fb
case ">":
return fa > fb
case ">=":
return fa >= fb
}
return false
}
func toFloat(v Value) (float64, bool) {
if !v.isStr {
return v.n, true
}
var f float64
_, err := fmt.Sscanf(strings.TrimSpace(v.s), "%f", &f)
return f, err == nil
}
func resolve(n *Node, env Env) Value {
switch n.Kind {
case "ident":
if v, ok := env.Lookup(n.Str); ok {
return v
}
return S("")
case "string":
return S(n.Str)
case "number":
return N(n.Num)
}
return S("")
}
var (
reMu sync.Mutex
reCache = map[string]*regexp.Regexp{}
)
func compileRegex(pat string) *regexp.Regexp {
reMu.Lock()
defer reMu.Unlock()
if re, ok := reCache[pat]; ok {
return re
}
re, err := regexp.Compile(pat)
if err != nil {
return nil
}
reCache[pat] = re
return re
}
// IsPrintableASCII is a shared helper used by both parser.go and eval.go
// when deciding whether a filter literal needs quoting in error output.
func IsPrintableASCII(s string) bool {
for _, r := range s {
if r > unicode.MaxASCII || !unicode.IsPrint(r) {
return false
}
}
return true
}