diff --git a/ast/builtins.go b/ast/builtins.go index e3f47ac3d9..1ab7dc0a93 100644 --- a/ast/builtins.go +++ b/ast/builtins.go @@ -50,6 +50,14 @@ var DefaultBuiltins = [...]*Builtin{ Abs, Rem, + // Bitwise Arithmetic + BitsOr, + BitsAnd, + BitsNegate, + BitsXOr, + BitsShiftLeft, + BitsShiftRight, + // Binary And, Or, @@ -383,10 +391,69 @@ var Rem = &Builtin{ } /** - * Binary + * Bitwise */ -// TODO(tsandall): update binary operators to support integers. +// BitsOr returns the bitwise "or" of two integers. +var BitsOr = &Builtin{ + Name: "bits.or", + Decl: types.NewFunction( + types.Args(types.N, types.N), + types.N, + ), +} + +// BitsAnd returns the bitwise "and" of two integers. +var BitsAnd = &Builtin{ + Name: "bits.and", + Decl: types.NewFunction( + types.Args(types.N, types.N), + types.N, + ), +} + +// BitsNegate returns the bitwise "negation" of an integer (i.e. flips each +// bit). +var BitsNegate = &Builtin{ + Name: "bits.negate", + Decl: types.NewFunction( + types.Args(types.N), + types.N, + ), +} + +// BitsXOr returns the bitwise "exclusive-or" of two integers. +var BitsXOr = &Builtin{ + Name: "bits.xor", + Decl: types.NewFunction( + types.Args(types.N, types.N), + types.N, + ), +} + +// BitsShiftLeft returns a new integer with its bits shifted some value to the +// left. +var BitsShiftLeft = &Builtin{ + Name: "bits.lsh", + Decl: types.NewFunction( + types.Args(types.N, types.N), + types.N, + ), +} + +// BitsShiftRight returns a new integer with its bits shifted some value to the +// right. +var BitsShiftRight = &Builtin{ + Name: "bits.rsh", + Decl: types.NewFunction( + types.Args(types.N, types.N), + types.N, + ), +} + +/** + * Sets + */ // And performs an intersection operation on sets. var And = &Builtin{ diff --git a/docs/content/policy-reference.md b/docs/content/policy-reference.md index 21a9092202..a3cf6ad954 100644 --- a/docs/content/policy-reference.md +++ b/docs/content/policy-reference.md @@ -161,6 +161,17 @@ The following table shows examples of how ``glob.match`` works: | ``output := glob.match("{cat,bat,[fr]at}", [], "rat")`` | ``true`` | A glob with pattern-alternatives matchers. | | ``output := glob.match("{cat,bat,[fr]at}", [], "at")`` | ``false`` | A glob with pattern-alternatives matchers. | +### Bitwise + +| Built-in | Description | +| --- | --- | +| ``z := bits.or(x, y)`` | ``z`` is the bitwise or of integers ``x`` and ``y`` | +| ``z := bits.and(x, y)`` | ``z`` is the bitwise and of integers ``x`` and ``y`` | +| ``z := bits.negate(x)`` | ``z`` is the bitwise negation (flip) of integer ``x`` | +| ``z := bits.xor(x, y)`` | ``z`` is the bitwise exclusive-or of integers ``x`` and ``y`` | +| ``z := bits.lsh(x, s)`` | ``z`` is the bitshift of integer ``x`` by ``s`` bits to the left | +| ``z := bits.rsh(x, s)`` | ``z`` is the bitshift of integer ``x`` by ``s`` bits to the right | + ### Conversions | Built-in | Description | diff --git a/topdown/bits.go b/topdown/bits.go new file mode 100644 index 0000000000..7a63c0df1e --- /dev/null +++ b/topdown/bits.go @@ -0,0 +1,88 @@ +// Copyright 2020 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package topdown + +import ( + "math/big" + + "github.com/open-policy-agent/opa/ast" + "github.com/open-policy-agent/opa/topdown/builtins" +) + +type bitsArity1 func(a *big.Int) (*big.Int, error) +type bitsArity2 func(a, b *big.Int) (*big.Int, error) + +func bitsOr(a, b *big.Int) (*big.Int, error) { + return new(big.Int).Or(a, b), nil +} + +func bitsAnd(a, b *big.Int) (*big.Int, error) { + return new(big.Int).And(a, b), nil +} + +func bitsNegate(a *big.Int) (*big.Int, error) { + return new(big.Int).Not(a), nil +} + +func bitsXOr(a, b *big.Int) (*big.Int, error) { + return new(big.Int).Xor(a, b), nil +} + +func bitsShiftLeft(a, b *big.Int) (*big.Int, error) { + if b.Sign() == -1 { + return nil, builtins.NewOperandErr(2, "must be an unsigned integer number but got a negative integer") + } + shift := uint(b.Uint64()) + return new(big.Int).Lsh(a, shift), nil +} + +func bitsShiftRight(a, b *big.Int) (*big.Int, error) { + if b.Sign() == -1 { + return nil, builtins.NewOperandErr(2, "must be an unsigned integer number but got a negative integer") + } + shift := uint(b.Uint64()) + return new(big.Int).Rsh(a, shift), nil +} + +func builtinBitsArity1(fn bitsArity1) BuiltinFunc { + return func(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { + i, err := builtins.BigIntOperand(operands[0].Value, 1) + if err != nil { + return err + } + iOut, err := fn(i) + if err != nil { + return err + } + return iter(ast.NewTerm(builtins.IntToNumber(iOut))) + } +} + +func builtinBitsArity2(fn bitsArity2) BuiltinFunc { + return func(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { + i1, err := builtins.BigIntOperand(operands[0].Value, 1) + if err != nil { + return err + } + i2, err := builtins.BigIntOperand(operands[1].Value, 2) + if err != nil { + return err + } + iOut, err := fn(i1, i2) + if err != nil { + return err + } + return iter(ast.NewTerm(builtins.IntToNumber(iOut))) + } +} + +func init() { + RegisterBuiltinFunc(ast.BitsOr.Name, builtinBitsArity2(bitsOr)) + RegisterBuiltinFunc(ast.BitsAnd.Name, builtinBitsArity2(bitsAnd)) + RegisterBuiltinFunc(ast.BitsNegate.Name, builtinBitsArity1(bitsNegate)) + RegisterBuiltinFunc(ast.BitsXOr.Name, builtinBitsArity2(bitsXOr)) + RegisterBuiltinFunc(ast.BitsShiftLeft.Name, builtinBitsArity2(bitsShiftLeft)) + RegisterBuiltinFunc(ast.BitsShiftRight.Name, builtinBitsArity2(bitsShiftRight)) +} diff --git a/topdown/bits_test.go b/topdown/bits_test.go new file mode 100644 index 0000000000..809a7e2976 --- /dev/null +++ b/topdown/bits_test.go @@ -0,0 +1,148 @@ +// Copyright 2020 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package topdown + +import ( + "fmt" + "math" + "testing" + + "github.com/open-policy-agent/opa/ast" +) + +func TestBuiltinBitsOr(t *testing.T) { + tests := []struct { + note string + rules []string + expected interface{} + }{ + {"basic bitwise-or", []string{`p[x] { x := bits.or(7, 9) }`}, `[15]`}, + {"or with zero is value", []string{`p[x] { x := bits.or(50, 0) }`}, `[50]`}, + {"lhs (float) error", []string{`p = x { x := bits.or(7.2, 42) }`}, &Error{Code: TypeErr, Message: "bits.or: operand 1 must be integer number but got floating-point number"}}, + { + "rhs (wrong type-type) error", + []string{`p = x { x := bits.or(7, "hi") }`}, + ast.Errors{ast.NewError(ast.TypeErr, nil, "bits.or: invalid argument(s)")}, + }, + } + + for _, tc := range tests { + runTopDownTestCase(t, map[string]interface{}{}, tc.note, tc.rules, tc.expected) + } +} + +func TestBuiltinBitsAnd(t *testing.T) { + tests := []struct { + note string + rules []string + expected interface{} + }{ + {"basic bitwise-and", []string{`p[x] { x := bits.and(7, 9) }`}, `[1]`}, + {"and with zero is and", []string{`p[x] { x := bits.and(50, 0) }`}, `[0]`}, + {"lhs (float) error", []string{`p = x { x := bits.and(7.2, 42) }`}, &Error{Code: TypeErr, Message: "bits.and: operand 1 must be integer number but got floating-point number"}}, + { + "rhs (wrong type-type) error", + []string{`p = x { x := bits.and(7, "hi") }`}, + ast.Errors{ast.NewError(ast.TypeErr, nil, "bits.and: invalid argument(s)")}, + }, + } + + for _, tc := range tests { + runTopDownTestCase(t, map[string]interface{}{}, tc.note, tc.rules, tc.expected) + } +} + +func TestBuiltinBitsNegate(t *testing.T) { + tests := []struct { + note string + rules []string + expected interface{} + }{ + {"basic bitwise-negate", []string{`p[x] { x := bits.negate(42) }`}, `[-43]`}, + {"float error", []string{`p = x { x := bits.negate(7.2) }`}, &Error{Code: TypeErr, Message: "bits.negate: operand 1 must be integer number but got floating-point number"}}, + { + "type error", + []string{`p = x { x := bits.negate("hi") }`}, + ast.Errors{ast.NewError(ast.TypeErr, nil, "bits.negate: invalid argument(s)")}, + }, + } + + for _, tc := range tests { + runTopDownTestCase(t, map[string]interface{}{}, tc.note, tc.rules, tc.expected) + } +} + +func TestBuiltinBitsXOr(t *testing.T) { + tests := []struct { + note string + rules []string + expected interface{} + }{ + {"basic bitwise-xor", []string{`p[x] { x := bits.xor(42, 3) }`}, `[41]`}, + {"xor same is 0", []string{`p[x] { x := bits.xor(42, 42) }`}, `[0]`}, + {"lhs (float) error", []string{`p = x { x := bits.xor(7.2, 42) }`}, &Error{Code: TypeErr, Message: "bits.xor: operand 1 must be integer number but got floating-point number"}}, + { + "rhs (wrong type-type) error", + []string{`p = x { x := bits.xor(7, "hi") }`}, + ast.Errors{ast.NewError(ast.TypeErr, nil, "bits.xor: invalid argument(s)")}, + }, + } + + for _, tc := range tests { + runTopDownTestCase(t, map[string]interface{}{}, tc.note, tc.rules, tc.expected) + } +} + +func TestBuiltinBitsShiftLeft(t *testing.T) { + tests := []struct { + note string + rules []string + expected interface{} + }{ + {"basic shift-left", []string{`p[x] { x := bits.lsh(1, 3) }`}, `[8]`}, + {"lhs (float) error", []string{`p = x { x := bits.lsh(7.2, 42) }`}, &Error{Code: TypeErr, Message: "bits.lsh: operand 1 must be integer number but got floating-point number"}}, + { + "rhs (wrong type-type) error", + []string{`p = x { x := bits.lsh(7, "hi") }`}, + ast.Errors{ast.NewError(ast.TypeErr, nil, "bits.lsh: invalid argument(s)")}, + }, + {"rhs must be unsigned", []string{`p = x { x := bits.lsh(7, -1) }`}, &Error{Code: TypeErr, Message: "bits.lsh: operand 2 must be an unsigned integer number but got a negative integer"}}, + { + "shift of max int32 doesn't overflow", + []string{fmt.Sprintf(`p = x { x := bits.lsh(%d, 1) }`, math.MaxInt32)}, + `4294967294`, + }, + { + "shift of max int64 doesn't overflow, but it's lossy do to conversion to exponent type (see discussion in #2160)", + []string{fmt.Sprintf(`p = x { x := bits.lsh(%d, 1) }`, math.MaxInt64)}, + `18446744074000000000`, + }, + } + + for _, tc := range tests { + runTopDownTestCase(t, map[string]interface{}{}, tc.note, tc.rules, tc.expected) + } +} + +func TestBuiltinBitsShiftRight(t *testing.T) { + tests := []struct { + note string + rules []string + expected interface{} + }{ + {"basic shift-right", []string{`p[x] { x := bits.rsh(8, 3) }`}, `[1]`}, + {"lhs (float) error", []string{`p = x { x := bits.rsh(7.2, 42) }`}, &Error{Code: TypeErr, Message: "bits.rsh: operand 1 must be integer number but got floating-point number"}}, + { + "rhs (wrong type-type) error", + []string{`p = x { x := bits.rsh(7, "hi") }`}, + ast.Errors{ast.NewError(ast.TypeErr, nil, "bits.rsh: invalid argument(s)")}, + }, + {"rhs must be unsigned", []string{`p = x { x := bits.rsh(7, -1) }`}, &Error{Code: TypeErr, Message: "bits.rsh: operand 2 must be an unsigned integer number but got a negative integer"}}, + } + + for _, tc := range tests { + runTopDownTestCase(t, map[string]interface{}{}, tc.note, tc.rules, tc.expected) + } +} diff --git a/topdown/builtins/builtins.go b/topdown/builtins/builtins.go index 415fcf7130..861167f398 100644 --- a/topdown/builtins/builtins.go +++ b/topdown/builtins/builtins.go @@ -92,6 +92,21 @@ func IntOperand(x ast.Value, pos int) (int, error) { return i, nil } +// BigIntOperand converts x to a big int. If the cast fails, a descriptive error +// is returned. +func BigIntOperand(x ast.Value, pos int) (*big.Int, error) { + n, err := NumberOperand(x, 1) + if err != nil { + return nil, NewOperandTypeErr(pos, x, "integer") + } + bi, err := NumberToInt(n) + if err != nil { + return nil, NewOperandErr(pos, "must be integer number but got floating-point number") + } + + return bi, nil +} + // NumberOperand converts x to a number. If the cast fails, a descriptive error is // returned. func NumberOperand(x ast.Value, pos int) (ast.Number, error) { @@ -159,8 +174,9 @@ func FloatToNumber(f *big.Float) ast.Number { // NumberToInt converts n to a big int. // If n cannot be converted to an big int, an error is returned. func NumberToInt(n ast.Number) (*big.Int, error) { - r, ok := new(big.Int).SetString(string(n), 10) - if !ok { + f := NumberToFloat(n) + r, accuracy := f.Int(nil) + if accuracy != big.Exact { return nil, fmt.Errorf("illegal value") } return r, nil