Skip to content

Commit

Permalink
Add bitwise operators after bits..
Browse files Browse the repository at this point in the history
Fixes #1919

Signed-off-by: Michael "Gilli" Gilliland <mjg.py3@gmail.com>
  • Loading branch information
mjgpy3 authored and patrick-east committed Mar 2, 2020
1 parent 863f052 commit ff3766d
Show file tree
Hide file tree
Showing 5 changed files with 334 additions and 4 deletions.
71 changes: 69 additions & 2 deletions ast/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ var DefaultBuiltins = [...]*Builtin{
Abs,
Rem,

// Bitwise Arithmetic
BitsOr,
BitsAnd,
BitsNegate,
BitsXOr,
BitsShiftLeft,
BitsShiftRight,

// Binary
And,
Or,
Expand Down Expand Up @@ -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{
Expand Down
11 changes: 11 additions & 0 deletions docs/content/policy-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
| --- | --- |
| <span class="opa-keep-it-together">``z := bits.or(x, y)``</span> | ``z`` is the bitwise or of integers ``x`` and ``y`` |
| <span class="opa-keep-it-together">``z := bits.and(x, y)``</span> | ``z`` is the bitwise and of integers ``x`` and ``y`` |
| <span class="opa-keep-it-together">``z := bits.negate(x)``</span> | ``z`` is the bitwise negation (flip) of integer ``x`` |
| <span class="opa-keep-it-together">``z := bits.xor(x, y)``</span> | ``z`` is the bitwise exclusive-or of integers ``x`` and ``y`` |
| <span class="opa-keep-it-together">``z := bits.lsh(x, s)``</span> | ``z`` is the bitshift of integer ``x`` by ``s`` bits to the left |
| <span class="opa-keep-it-together">``z := bits.rsh(x, s)``</span> | ``z`` is the bitshift of integer ``x`` by ``s`` bits to the right |

### Conversions

| Built-in | Description |
Expand Down
88 changes: 88 additions & 0 deletions topdown/bits.go
Original file line number Diff line number Diff line change
@@ -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))
}
148 changes: 148 additions & 0 deletions topdown/bits_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
20 changes: 18 additions & 2 deletions topdown/builtins/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit ff3766d

Please sign in to comment.