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