package vm
import (
"fmt"
"math"
"reflect"
"github.com/grafana/agent/pkg/river/internal/value"
"github.com/grafana/agent/pkg/river/token"
)
func evalBinop(lhs value.Value, op token.Token, rhs value.Value) (value.Value, error) {
switch op {
case token.EQ:
return value.Bool(valuesEqual(lhs, rhs)), nil
case token.NEQ:
return value.Bool(!valuesEqual(lhs, rhs)), nil
}
if !acceptableBinopType(lhs, op) {
return value.Null, value.Error{
Value: lhs,
Inner: fmt.Errorf("should be one of %v for binop %s, got %s", binopAllowedTypes[op], op, lhs.Type()),
}
} else if !acceptableBinopType(rhs, op) {
return value.Null, value.Error{
Value: rhs,
Inner: fmt.Errorf("should be one of %v for binop %s, got %s", binopAllowedTypes[op], op, rhs.Type()),
}
}
if lhs.Type() != rhs.Type() {
return value.Null, value.TypeError{Value: rhs, Expected: lhs.Type()}
}
switch op {
case token.OR:
return value.Bool(lhs.Bool() || rhs.Bool()), nil
case token.AND:
return value.Bool(lhs.Bool() && rhs.Bool()), nil
case token.ADD:
if lhs.Type() == value.TypeString {
return value.String(lhs.Text() + rhs.Text()), nil
}
lhsNum, rhsNum := lhs.Number(), rhs.Number()
switch fitNumberKinds(lhsNum.Kind(), rhsNum.Kind()) {
case value.NumberKindUint:
return value.Uint(lhsNum.Uint() + rhsNum.Uint()), nil
case value.NumberKindInt:
return value.Int(lhsNum.Int() + rhsNum.Int()), nil
case value.NumberKindFloat:
return value.Float(lhsNum.Float() + rhsNum.Float()), nil
}
case token.SUB:
lhsNum, rhsNum := lhs.Number(), rhs.Number()
switch fitNumberKinds(lhsNum.Kind(), rhsNum.Kind()) {
case value.NumberKindUint:
return value.Uint(lhsNum.Uint() - rhsNum.Uint()), nil
case value.NumberKindInt:
return value.Int(lhsNum.Int() - rhsNum.Int()), nil
case value.NumberKindFloat:
return value.Float(lhsNum.Float() - rhsNum.Float()), nil
}
case token.MUL:
lhsNum, rhsNum := lhs.Number(), rhs.Number()
switch fitNumberKinds(lhsNum.Kind(), rhsNum.Kind()) {
case value.NumberKindUint:
return value.Uint(lhsNum.Uint() * rhsNum.Uint()), nil
case value.NumberKindInt:
return value.Int(lhsNum.Int() * rhsNum.Int()), nil
case value.NumberKindFloat:
return value.Float(lhsNum.Float() * rhsNum.Float()), nil
}
case token.DIV:
lhsNum, rhsNum := lhs.Number(), rhs.Number()
switch fitNumberKinds(lhsNum.Kind(), rhsNum.Kind()) {
case value.NumberKindUint:
return value.Uint(lhsNum.Uint() / rhsNum.Uint()), nil
case value.NumberKindInt:
return value.Int(lhsNum.Int() / rhsNum.Int()), nil
case value.NumberKindFloat:
return value.Float(lhsNum.Float() / rhsNum.Float()), nil
}
case token.MOD:
lhsNum, rhsNum := lhs.Number(), rhs.Number()
switch fitNumberKinds(lhsNum.Kind(), rhsNum.Kind()) {
case value.NumberKindUint:
return value.Uint(lhsNum.Uint() % rhsNum.Uint()), nil
case value.NumberKindInt:
return value.Int(lhsNum.Int() % rhsNum.Int()), nil
case value.NumberKindFloat:
return value.Float(math.Mod(lhsNum.Float(), rhsNum.Float())), nil
}
case token.POW:
lhsNum, rhsNum := lhs.Number(), rhs.Number()
switch fitNumberKinds(lhsNum.Kind(), rhsNum.Kind()) {
case value.NumberKindUint:
return value.Uint(intPow(lhsNum.Uint(), rhsNum.Uint())), nil
case value.NumberKindInt:
return value.Int(intPow(lhsNum.Int(), rhsNum.Int())), nil
case value.NumberKindFloat:
return value.Float(math.Pow(lhsNum.Float(), rhsNum.Float())), nil
}
case token.LT:
if lhs.Type() == value.TypeString {
return value.Bool(lhs.Text() < rhs.Text()), nil
}
lhsNum, rhsNum := lhs.Number(), rhs.Number()
switch fitNumberKinds(lhsNum.Kind(), rhsNum.Kind()) {
case value.NumberKindUint:
return value.Bool(lhsNum.Uint() < rhsNum.Uint()), nil
case value.NumberKindInt:
return value.Bool(lhsNum.Int() < rhsNum.Int()), nil
case value.NumberKindFloat:
return value.Bool(lhsNum.Float() < rhsNum.Float()), nil
}
case token.GT:
if lhs.Type() == value.TypeString {
return value.Bool(lhs.Text() > rhs.Text()), nil
}
lhsNum, rhsNum := lhs.Number(), rhs.Number()
switch fitNumberKinds(lhsNum.Kind(), rhsNum.Kind()) {
case value.NumberKindUint:
return value.Bool(lhsNum.Uint() > rhsNum.Uint()), nil
case value.NumberKindInt:
return value.Bool(lhsNum.Int() > rhsNum.Int()), nil
case value.NumberKindFloat:
return value.Bool(lhsNum.Float() > rhsNum.Float()), nil
}
case token.LTE:
if lhs.Type() == value.TypeString {
return value.Bool(lhs.Text() <= rhs.Text()), nil
}
lhsNum, rhsNum := lhs.Number(), rhs.Number()
switch fitNumberKinds(lhsNum.Kind(), rhsNum.Kind()) {
case value.NumberKindUint:
return value.Bool(lhsNum.Uint() <= rhsNum.Uint()), nil
case value.NumberKindInt:
return value.Bool(lhsNum.Int() <= rhsNum.Int()), nil
case value.NumberKindFloat:
return value.Bool(lhsNum.Float() <= rhsNum.Float()), nil
}
case token.GTE:
if lhs.Type() == value.TypeString {
return value.Bool(lhs.Text() >= rhs.Text()), nil
}
lhsNum, rhsNum := lhs.Number(), rhs.Number()
switch fitNumberKinds(lhsNum.Kind(), rhsNum.Kind()) {
case value.NumberKindUint:
return value.Bool(lhsNum.Uint() >= rhsNum.Uint()), nil
case value.NumberKindInt:
return value.Bool(lhsNum.Int() >= rhsNum.Int()), nil
case value.NumberKindFloat:
return value.Bool(lhsNum.Float() >= rhsNum.Float()), nil
}
}
panic("river/vm: unreachable")
}
func valuesEqual(lhs value.Value, rhs value.Value) bool {
if lhs.Type() != rhs.Type() {
return false
}
switch lhs.Type() {
case value.TypeNull:
return true
case value.TypeNumber:
lhsNum, rhsNum := lhs.Number(), rhs.Number()
switch fitNumberKinds(lhsNum.Kind(), rhsNum.Kind()) {
case value.NumberKindUint:
return lhsNum.Uint() == rhsNum.Uint()
case value.NumberKindInt:
return lhsNum.Int() == rhsNum.Int()
case value.NumberKindFloat:
return lhsNum.Float() == rhsNum.Float()
}
case value.TypeString:
return lhs.Text() == rhs.Text()
case value.TypeBool:
return lhs.Bool() == rhs.Bool()
case value.TypeArray:
if lhs.Len() != rhs.Len() {
return false
}
for i := 0; i < lhs.Len(); i++ {
if !valuesEqual(lhs.Index(i), rhs.Index(i)) {
return false
}
}
return true
case value.TypeObject:
if lhs.Len() != rhs.Len() {
return false
}
for _, key := range lhs.Keys() {
lhsElement, _ := lhs.Key(key)
rhsElement, inRHS := rhs.Key(key)
if !inRHS {
return false
}
if !valuesEqual(lhsElement, rhsElement) {
return false
}
}
return true
case value.TypeFunction:
return false
case value.TypeCapsule:
return reflect.DeepEqual(lhs.Interface(), rhs.Interface())
}
panic("river/vm: unreachable")
}
var binopAllowedTypes = map[token.Token][]value.Type{
token.OR: {value.TypeBool},
token.AND: {value.TypeBool},
token.ADD: {value.TypeNumber, value.TypeString},
token.SUB: {value.TypeNumber},
token.MUL: {value.TypeNumber},
token.DIV: {value.TypeNumber},
token.MOD: {value.TypeNumber},
token.POW: {value.TypeNumber},
token.LT: {value.TypeNumber, value.TypeString},
token.GT: {value.TypeNumber, value.TypeString},
token.LTE: {value.TypeNumber, value.TypeString},
token.GTE: {value.TypeNumber, value.TypeString},
}
func acceptableBinopType(val value.Value, op token.Token) bool {
allowed, ok := binopAllowedTypes[op]
if !ok {
panic("river/vm: unexpected binop type")
}
actualType := val.Type()
for _, allowType := range allowed {
if allowType == actualType {
return true
}
}
return false
}
func fitNumberKinds(a, b value.NumberKind) value.NumberKind {
aPrec, bPrec := numberKindPrec[a], numberKindPrec[b]
if aPrec > bPrec {
return a
}
return b
}
var numberKindPrec = map[value.NumberKind]int{
value.NumberKindUint: 0,
value.NumberKindInt: 1,
value.NumberKindFloat: 2,
}
func intPow[Number int64 | uint64](n, m Number) Number {
if m == 0 {
return 1
}
result := n
for i := Number(2); i <= m; i++ {
result *= n
}
return result
}