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
30 changes: 28 additions & 2 deletions run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
lua_path="$script_dir/?.lua;$script_dir/?/init.lua;$script_dir/src/?.lua;$script_dir/src/?/init.lua;$script_dir/vendor/?.lua;$LUA_PATH"

# Parse command line arguments to determine which modules to run
default_modules=("bit16" "bit32" "bit64")
all_modules=("bit16" "bit32" "bit64")
default_modules=("bit16" "bit32" "bit64" "probe" "lpack")
all_modules=("bit16" "bit32" "bit64" "probe" "lpack")
modules_to_run=("$@")

# Validate modules if specified
Expand Down Expand Up @@ -131,6 +131,32 @@ run_selftest "16-bit operations" "bit16" "bitn.bit16"
run_selftest "32-bit operations" "bit32" "bitn.bit32"
run_selftest "64-bit operations" "bit64" "bitn.bit64"

# The probe must bind a real 5.3-dialect string.pack, or the byte helpers silently
# lose their fast path; only the 5.3+ legs can prove this.
run_test "string.pack binds when it is the 5.3 dialect" "probe" "
local _compat = require('bitn._compat')
if rawget(string, 'pack') and _compat.string_pack == nil then
print('string.pack is present but the probe declined it')
os.exit(1)
end
print(_compat.string_pack and 'string.pack bound' or 'no string.pack on this interpreter')
"

# Control4's LuaJIT has string.pack/unpack as lpack, a different dialect: every
# selftest again with that shape installed first, and the 5.3 fast path must stay unbound.
run_test "All operations with lpack-shaped string.pack" "lpack" "
dofile('$script_dir/test/lpack_stub.lua')
if require('bitn._compat').string_pack ~= nil then
print('lpack-shaped string.pack was bound as the 5.3 dialect')
os.exit(1)
end
for _, name in ipairs({ 'bitn.bit16', 'bitn.bit32', 'bitn.bit64' }) do
if not require(name).selftest() then
os.exit(1)
end
end
"

passed_count=${#passed_modules[@]}
failed_count=${#failed_modules[@]}
total_count=$((passed_count + failed_count))
Expand Down
25 changes: 25 additions & 0 deletions src/bitn/_compat.lua
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,31 @@ _compat.to_unsigned = to_unsigned
-- Constants
local MASK32 = 0xFFFFFFFF

-- string.pack/unpack are bound only if they answer in the 5.3 dialect. Control4's
-- LuaJIT ships lpack under the same names: no size suffixes, and unpack takes
-- (data, fmt, pos) and returns the position first, so existence proves nothing.
local function probe_string_pack()
local pack, unpack = rawget(string, "pack"), rawget(string, "unpack")
if not (pack and unpack) then
return nil, nil
end
local packed_ok, packed = pcall(pack, "<I4", 0x04030201)
if not packed_ok or packed ~= "\1\2\3\4" then
return nil, nil
end
packed_ok, packed = pcall(pack, ">I4", 0x04030201)
if not packed_ok or packed ~= "\4\3\2\1" then
return nil, nil
end
local unpacked_ok, value, next_pos = pcall(unpack, "<I4", "\0\1\2\3\4", 2)
if not unpacked_ok or value ~= 0x04030201 or next_pos ~= 6 then
return nil, nil
end
return pack, unpack
end

_compat.string_pack, _compat.string_unpack = probe_string_pack()

--------------------------------------------------------------------------------
-- Implementation 1: Native operators (Lua 5.3+)
--------------------------------------------------------------------------------
Expand Down
4 changes: 2 additions & 2 deletions src/bitn/bit32.lua
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,8 @@ end

local string_char = string.char
local string_byte = string.byte
local string_pack = rawget(string, "pack")
local string_unpack = rawget(string, "unpack")
local string_pack = _compat.string_pack
local string_unpack = _compat.string_unpack
-- % is the whole operation on the pure Lua backend; skip the call.
local fast_band = _compat.has_native_ops or _compat.has_bit_lib

Expand Down
4 changes: 2 additions & 2 deletions src/bitn/bit64.lua
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ local bit32_rshift = bit32.rshift
local impl_name = _compat.impl_name
local math_floor = math.floor
local string_char = string.char
local string_pack = rawget(string, "pack")
local string_unpack = rawget(string, "unpack")
local string_pack = _compat.string_pack
local string_unpack = _compat.string_unpack

-- Private metatable for Int64 type identification
local Int64Meta = { __name = "Int64" }
Expand Down
83 changes: 83 additions & 0 deletions test/lpack_stub.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
--- @diagnostic disable: duplicate-set-field
-- Installs string.pack/string.unpack in the shape Control4's LuaJIT ships (lpack),
-- as measured on a controller: single-letter codes with no size suffix, '<' and
-- '=' little-endian, '>' big-endian, a NUL ends the format, a digit or unknown
-- code raises, and unpack(data, fmt, pos) returns the next position before the
-- values, with nil for a value the data cannot supply. Loaded ahead of bitn so a
-- byte helper that assumes the 5.3 dialect fails here rather than on a controller.
local SIZE = { b = 1, B = 1, c = 1, h = 2, H = 2, i = 4, I = 4, l = 4, L = 4 }

local function pack_int(value, size, big_endian)
value = value % 2 ^ (8 * size)
local out = {}
for _ = 1, size do
out[#out + 1] = string.char(value % 256)
value = math.floor(value / 256)
end
if big_endian then
local reversed = {}
for i = size, 1, -1 do
reversed[#reversed + 1] = out[i]
end
out = reversed
end
return table.concat(out)
end

local function unpack_int(data, pos, size, big_endian)
if #data < pos + size - 1 then
return pos, nil
end
local value = 0
for i = 0, size - 1 do
local weight = big_endian and (size - 1 - i) or i
value = value + string.byte(data, pos + i) * 2 ^ (8 * weight)
end
return pos + size, value
end

local function codes(fmt)
local out = {}
for c in fmt:gmatch(".") do
if c == "\0" then
break
end
out[#out + 1] = c
end
return out
end

function string.pack(fmt, ...)
local args, i, out, big_endian = { ... }, 0, {}, false
for _, c in ipairs(codes(fmt)) do
if c == "<" or c == "=" then
big_endian = false
elseif c == ">" then
big_endian = true
elseif SIZE[c] then
i = i + 1
out[#out + 1] = pack_int(args[i], SIZE[c], big_endian)
else
error("lpack stub: unsupported code '" .. c .. "'")
end
end
return table.concat(out)
end

function string.unpack(data, fmt, pos)
pos = pos or 1
local out, n, big_endian = {}, 0, false
for _, c in ipairs(codes(fmt)) do
if c == "<" or c == "=" then
big_endian = false
elseif c == ">" then
big_endian = true
elseif SIZE[c] then
n = n + 1
pos, out[n] = unpack_int(data, pos, SIZE[c], big_endian)
else
error("lpack stub: unsupported code '" .. c .. "'")
end
end
return pos, (table.unpack or unpack)(out, 1, n)
end
Loading