diff --git a/builtin/builtin.go b/builtin/builtin.go index 8576160a6..d76cc9d06 100644 --- a/builtin/builtin.go +++ b/builtin/builtin.go @@ -972,4 +972,48 @@ var Builtins = []*ast.Function{ return arrayType, nil }, }, + bitFunc("bitand", func(x, y int) (any, error) { + return x & y, nil + }), + bitFunc("bitor", func(x, y int) (any, error) { + return x | y, nil + }), + bitFunc("bitxor", func(x, y int) (any, error) { + return x ^ y, nil + }), + bitFunc("bitnand", func(x, y int) (any, error) { + return x &^ y, nil + }), + bitFunc("bitshl", func(x, y int) (any, error) { + if y < 0 { + return nil, fmt.Errorf("invalid operation: negative shift count %d (type int)", y) + } + return x << y, nil + }), + bitFunc("bitshr", func(x, y int) (any, error) { + if y < 0 { + return nil, fmt.Errorf("invalid operation: negative shift count %d (type int)", y) + } + return x >> y, nil + }), + bitFunc("bitushr", func(x, y int) (any, error) { + if y < 0 { + return nil, fmt.Errorf("invalid operation: negative shift count %d (type int)", y) + } + return int(uint(x) >> y), nil + }), + { + Name: "bitnot", + Func: func(args ...any) (any, error) { + if len(args) != 1 { + return nil, fmt.Errorf("invalid number of arguments for bitnot (expected 1, got %d)", len(args)) + } + x, err := toInt(args[0]) + if err != nil { + return nil, fmt.Errorf("%v to call bitnot", err) + } + return ^x, nil + }, + Types: types(new(func(int) int)), + }, } diff --git a/builtin/builtin_test.go b/builtin/builtin_test.go index 40f6e32ee..a861d0faa 100644 --- a/builtin/builtin_test.go +++ b/builtin/builtin_test.go @@ -199,6 +199,13 @@ func TestBuiltin_errors(t *testing.T) { {`date("error")`, `invalid date`}, {`get()`, `invalid number of arguments (expected 2, got 0)`}, {`get(1, 2)`, `type int does not support indexing`}, + {`bitnot("1")`, "cannot use string as argument (type int) to call bitnot (1:8)"}, + {`bitand("1", 1)`, "cannot use string as argument (type int) to call bitand (1:8)"}, + {`"10" | bitor(1)`, "cannot use string as argument (type int) to call bitor (1:1)"}, + {`bitshr("5", 1)`, "cannot use string as argument (type int) to call bitshr (1:8)"}, + {`bitshr(-5, -2)`, "invalid operation: negative shift count -2 (type int) (1:1)"}, + {`bitshl(1, -1)`, "invalid operation: negative shift count -1 (type int) (1:1)"}, + {`bitushr(-5, -2)`, "invalid operation: negative shift count -2 (type int) (1:1)"}, } for _, test := range errorTests { t.Run(test.input, func(t *testing.T) { @@ -437,3 +444,31 @@ func TestBuiltin_sort(t *testing.T) { }) } } + +func TestBuiltin_bitOpsFunc(t *testing.T) { + tests := []struct { + input string + want int + }{ + {`bitnot(156)`, -157}, + {`bitand(bitnot(156), 255)`, 99}, + {`bitor(987, -123)`, -33}, + {`bitxor(15, 32)`, 47}, + {`bitshl(39, 3)`, 312}, + {`bitshr(5, 1)`, 2}, + {`bitushr(-5, 2)`, 4611686018427387902}, + {`bitnand(35, 9)`, 34}, + } + + for _, test := range tests { + t.Run(test.input, func(t *testing.T) { + program, err := expr.Compile(test.input, expr.Env(nil)) + require.NoError(t, err) + + out, err := expr.Run(program, nil) + fmt.Printf("%v : %v", test.input, out) + require.NoError(t, err) + assert.Equal(t, test.want, out) + }) + } +} diff --git a/builtin/func.go b/builtin/func.go index 9bcd7827b..1c2546a2d 100644 --- a/builtin/func.go +++ b/builtin/func.go @@ -6,6 +6,7 @@ import ( "reflect" "strconv" + "github.com/expr-lang/expr/ast" "github.com/expr-lang/expr/vm/runtime" ) @@ -272,3 +273,24 @@ func Min(args ...any) (any, error) { } return min, nil } + +func bitFunc(name string, fn func(x, y int) (any, error)) *ast.Function { + return &ast.Function{ + Name: name, + Func: func(args ...any) (any, error) { + if len(args) != 2 { + return nil, fmt.Errorf("invalid number of arguments for %s (expected 2, got %d)", name, len(args)) + } + x, err := toInt(args[0]) + if err != nil { + return nil, fmt.Errorf("%v to call %s", err, name) + } + y, err := toInt(args[1]) + if err != nil { + return nil, fmt.Errorf("%v to call %s", err, name) + } + return fn(x, y) + }, + Types: types(new(func(int, int) int)), + } +} diff --git a/builtin/utils.go b/builtin/utils.go index eff8fdcd1..f831e95b6 100644 --- a/builtin/utils.go +++ b/builtin/utils.go @@ -63,3 +63,30 @@ loop: panic(fmt.Sprintf("cannot deref %s", v)) } + +func toInt(val any) (int, error) { + switch v := val.(type) { + case int: + return v, nil + case int8: + return int(v), nil + case int16: + return int(v), nil + case int32: + return int(v), nil + case int64: + return int(v), nil + case uint: + return int(v), nil + case uint8: + return int(v), nil + case uint16: + return int(v), nil + case uint32: + return int(v), nil + case uint64: + return int(v), nil + default: + return 0, fmt.Errorf("cannot use %T as argument (type int)", val) + } +} diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index 083fc688e..c3b460aa0 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -4,12 +4,13 @@ import ( "math" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/expr-lang/expr" "github.com/expr-lang/expr/test/playground" "github.com/expr-lang/expr/vm" "github.com/expr-lang/expr/vm/runtime" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) type B struct { diff --git a/docs/Language-Definition.md b/docs/Language-Definition.md index 46bad9322..5ba643587 100644 --- a/docs/Language-Definition.md +++ b/docs/Language-Definition.md @@ -696,6 +696,74 @@ Converts an array of key-value pairs to a map. fromPairs([["name", "John"], ["age", 30]]) == {"name": "John", "age": 30} ``` +## Bitwise Functions + +### bitand(int, int) + +Returns the values resulting from the bitwise AND operation. + +```expr +bitand(10, 12) == 8 +``` + +### bitor(int, int) + +Returns the values resulting from the bitwise OR operation. + +```expr +bitor(10, 12) == 14 +``` + +### bitxor(int, int) + +Returns the values resulting from the bitwise XOR operation. + +```expr +bitxor(10, 12) == 6 +``` + +### bitnand(int, int) + +Returns the values resulting from the bitwise AND NOT operation. + +```expr +bitnand(10, 12) == 2 +``` + +### bitnot(int) + +Returns the values resulting from the bitwise NOT operation. + +```expr +bitnot(10) == -11 +``` + +## Shift Functions + +### bitshl(int, int) + +Returns the values resulting from the Left Shift operation. + +```expr +bitshl(45, 2) == 180 +``` + +### bitshr(int, int) + +Returns the values resulting from the Right Shift operation. + +```expr +bitshr(45, 2) == 11 +``` + +### bitushr(int, int) + +Returns the values resulting from the unsigned Right Shift operation. + +```expr +bitushr(-5, 2) == 4611686018427387902 +``` + ## Miscellaneous Functions ### len(v) diff --git a/expr_test.go b/expr_test.go index eb77408b1..745441ee6 100644 --- a/expr_test.go +++ b/expr_test.go @@ -1135,6 +1135,38 @@ func TestExpr(t *testing.T) { `.5 in ArrayOfInt`, false, }, + { + `bitnot(10)`, + -11, + }, + { + `bitxor(15, 32)`, + 47, + }, + { + `bitand(90, 34)`, + 2, + }, + { + `bitnand(35, 9)`, + 34, + }, + { + `bitor(10, 5)`, + 15, + }, + { + `bitshr(7, 2)`, + 1, + }, + { + `bitshl(7, 2)`, + 28, + }, + { + `bitushr(-100, 5)`, + 576460752303423484, + }, } for _, tt := range tests { diff --git a/parser/parser_test.go b/parser/parser_test.go index 2af7635ed..c1d7db1e6 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -5,10 +5,11 @@ import ( "strings" "testing" - . "github.com/expr-lang/expr/ast" - "github.com/expr-lang/expr/parser" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + . "github.com/expr-lang/expr/ast" + "github.com/expr-lang/expr/parser" ) func TestParse(t *testing.T) { diff --git a/vm/runtime/generated.go b/vm/runtime/generated.go index 720feb455..a5b92f477 100644 --- a/vm/runtime/generated.go +++ b/vm/runtime/generated.go @@ -3372,4 +3372,4 @@ func Modulo(a, b interface{}) int { } } panic(fmt.Sprintf("invalid operation: %T %% %T", a, b)) -} +} \ No newline at end of file