Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ go get github.com/vadv/gopher-lua-libs
* [xmlpath](/xmlpath) [gopkg.in/xmlpath.v2](https://gopkg.in/xmlpath.v2) port
* [yaml](/yaml) [gopkg.in/yaml.v2](https://gopkg.in/yaml.v2) port
* [zabbix](/zabbix) zabbix bot
* [bit](/bit) bitwise operations


## Usage
Expand Down
16 changes: 16 additions & 0 deletions bit/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# bit [![GoDoc](https://godoc.org/github.com/vadv/gopher-lua-libs/bit?bit.svg)](https://godoc.org/github.com/vadv/gopher-lua-libs/bit)

## Usage

```lua
local bit = require("bit")

local result, _ = bit.band(1, 0)
print(result)
-- Output: 0

local result, _ = bit.lshift(10, 5)
print(result)
-- Output: 320
```

85 changes: 85 additions & 0 deletions bit/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Package bit implements Go bitwise operations functionality for Lua.
package bit

import (
"fmt"
"math"

lua "github.com/yuin/gopher-lua"
)

type op uint

const (
and op = iota
or
not
xor
ls
rs
)

// Bitwise returns a Lua function used for bitwise operations.
func Bitwise(kind op) lua.LGFunction {
return func(l *lua.LState) int {
if kind > rs {
l.RaiseError("unsupported operation type")
return 0
}
val1, val2, err := prepareParams(l)
if err != nil {
l.Push(lua.LNil)
l.Push(lua.LString(err.Error()))
return 2
}
var ret uint32
switch kind {
case and:
ret = val1 & val2
case or:
ret = val1 | val2
case xor:
ret = val1 ^ val2
case ls:
ret = val1 << val2
case rs:
ret = val1 >> val2
}
l.Push(lua.LNumber(ret))
return 1
}
}

// Not implements bitwise not.
func Not(l *lua.LState) int {
val, err := intToU32(l.CheckInt(1))
if err != nil {
l.Push(lua.LNil)
l.Push(lua.LString(err.Error()))
return 2
}
l.Push(lua.LNumber(^val))
return 1
}

func prepareParams(l *lua.LState) (val1, val2 uint32, err error) {
val1, err = intToU32(l.CheckInt(1))
if err != nil {
return 0, 0, err
}
val2, err = intToU32(l.CheckInt(2))
if err != nil {
return 0, 0, err
}
return
}

func intToU32(i int) (uint32, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Why why not 64 bit?

Copy link
Contributor Author

@dborovcanin dborovcanin Jun 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mainly because numbers in Lua 5.1 are 64-bit floating-point, and Go uint64 overflows them so Go 64-bit uint cannot be presented properly in Lua 5.1 floating-point.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense; it's a little disappointing that it can't use the full size… I wonder if we should suffix methods with 32 to indicate that, but I won't press that and we can consider this resolved

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can add a suffix if you insist. I was considering that, but I didn't do it because it's impossible to have <operation>u64 and u8 or u16 can be handled with the existing version. Maybe we can add a suffix in the future if we support different versions (possibly even u64), but that is unlikely to happen.

if i < 0 {
return 0, fmt.Errorf("cannot convert negative int %d to uint32", i)
}
if i > math.MaxUint32 {
return 0, fmt.Errorf("int %d overflows uint32", i)
}
return uint32(i), nil
}
13 changes: 13 additions & 0 deletions bit/api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package bit

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/vadv/gopher-lua-libs/tests"
)

func TestApi(t *testing.T) {
preload := tests.SeveralPreloadFuncs(Preload)
assert.NotZero(t, tests.RunLuaTestFile(t, preload, "./test/test_api.lua"))
}
28 changes: 28 additions & 0 deletions bit/loader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package bit

import lua "github.com/yuin/gopher-lua"

// Preload adds bit to the given Lua state's package.preload table. After it
// has been preloaded, it can be loaded using require:
//
// local bit = require("bit")
func Preload(l *lua.LState) {
l.PreloadModule("bit", Loader)
}

// Loader is the module loader function.
func Loader(L *lua.LState) int {
t := L.NewTable()
L.SetFuncs(t, api)
L.Push(t)
return 1
}

var api = map[string]lua.LGFunction{
"band": Bitwise(and),
"bor": Bitwise(or),
"bxor": Bitwise(xor),
"lshift": Bitwise(ls),
"rshift": Bitwise(rs),
"bnot": Not,
}
202 changes: 202 additions & 0 deletions bit/test/test_api.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
local bit = require 'bit'
local assert = require 'assert'

function TestAnd(t)
local tests = {
{
input1 = -3,
input2 = 23,
expected = nil,
err = "cannot convert negative int -3 to uint32",
},
{
input1 = 4294967296,
input2 = 23,
expected = nil,
err = "int 4294967296 overflows uint32",
},
{
input1 = 1,
input2 = 0,
expected = 0,
},
{
input1 = 111,
input2 = 222,
expected = 78,
}
}
Comment on lines +5 to +28
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: I like this style - I was thinking of doing it - Github copilot did the positional thing for me so I left it, but associative arrays are more descriptive!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I completely missed your PR on my branch, so I did it this way, similarly to the tests you linked in the previous comments (there's a bit of repetition, but I hope that's acceptable).

for _, tt in ipairs(tests) do
t:Run(tostring(tt.input1) .. " and " .. tostring(tt.input2), function(t)
local got, err = bit.band(tt.input1, tt.input2)
assert:Equal(t, tt.expected, got)
assert:Equal(t, tt.err, err)
end)
end
end

function TestOr(t)
local tests = {
{
input1 = 5,
input2 = -423,
expected = nil,
err = "cannot convert negative int -423 to uint32",
},
{
input1 = 123,
input2 = 4294967296,
expected = nil,
err = "int 4294967296 overflows uint32",
},
{
input1 = 1,
input2 = 0,
expected = 1,
},
{
input1 = 111,
input2 = 222,
expected = 255,
}
}
for _, tt in ipairs(tests) do
t:Run(tostring(tt.input1) .. " or " .. tostring(tt.input2), function(t)
local got, err = bit.bor(tt.input1, tt.input2)
assert:Equal(t, tt.expected, got)
assert:Equal(t, tt.err, err)
end)
end
end

function TestXor(t)
local tests = {
{
input1 = -1,
input2 = -46,
expected = nil,
err = "cannot convert negative int -1 to uint32",
},
{
input1 = 4294967300,
input2 = 46,
expected = nil,
err = "int 4294967300 overflows uint32",
},
{
input1 = 1,
input2 = 0,
expected = 1,
},
{
input1 = 111,
input2 = 222,
expected = 177,
}
}
for _, tt in ipairs(tests) do
t:Run(tostring(tt.input1) .. " xor " .. tostring(tt.input2), function(t)
local got, err = bit.bxor(tt.input1, tt.input2)
assert:Equal(t, tt.expected, got)
assert:Equal(t, tt.err, err)
end)
end
end

function TestLShift(t)
local tests = {
{
input1 = 0,
input2 = -10,
expected = nil,
err = "cannot convert negative int -10 to uint32",
},
{
input1 = 4294967297,
input2 = 4294967298,
expected = nil,
err = "int 4294967297 overflows uint32",
},
{
input1 = 123456,
input2 = 8,
expected = 31604736,
},
{
input1 = 0XFF,
input2 = 8,
expected = 65280,
}
}
for _, tt in ipairs(tests) do
t:Run(tostring(tt.input1) .. " << " .. tostring(tt.input2), function(t)
local got, err = bit.lshift(tt.input1, tt.input2)
assert:Equal(t, tt.expected, got)
assert:Equal(t, tt.err, err)
end)
end
end

function TestRShift(t)
local tests = {
{
input1 = -10,
input2 = 0,
expected = nil,
err = "cannot convert negative int -10 to uint32",
},
{
input1 = 4294967296,
input2 = -3,
expected = nil,
err = "int 4294967296 overflows uint32",
},
{
input1 = 123456,
input2 = 8,
expected = 482,
},
{
input1 = 0XFF,
input2 = 1,
expected = 0x7F,
}
}
for _, tt in ipairs(tests) do
t:Run(tostring(tt.input1) .. " >> " .. tostring(tt.input2), function(t)
local got, err = bit.rshift(tt.input1, tt.input2)
assert:Equal(t, tt.expected, got)
assert:Equal(t, tt.err, err)
end)
end
end

function TestNot(t)
local tests = {
{
input = -3,
expected = nil,
err = "cannot convert negative int -3 to uint32",
},
{
input = 4294967297,
expected = nil,
err = "int 4294967297 overflows uint32",
},
{
input = 65536,
expected = 4294901759,
},
{
input = 4294901759,
expected = 65536,
}
}
for _, tt in ipairs(tests) do
t:Run("not " .. tostring(tt.input), function(t)
local got, err = bit.bnot(tt.input)
assert:Equal(t, tt.expected, got)
assert:Equal(t, tt.err, err)
end)
end
end
2 changes: 2 additions & 0 deletions plugin/preload.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"github.com/vadv/gopher-lua-libs/argparse"
"github.com/vadv/gopher-lua-libs/aws/cloudwatch"
"github.com/vadv/gopher-lua-libs/base64"
"github.com/vadv/gopher-lua-libs/bit"
"github.com/vadv/gopher-lua-libs/cert_util"
"github.com/vadv/gopher-lua-libs/chef"
"github.com/vadv/gopher-lua-libs/cmd"
Expand Down Expand Up @@ -74,4 +75,5 @@ func PreloadAll(L *lua.LState) {
xmlpath.Preload(L)
yaml.Preload(L)
zabbix.Preload(L)
bit.Preload(L)
}