diff --git a/CLAUDE.md b/CLAUDE.md index 81f3cbc..5fac01a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -140,7 +140,7 @@ All three modules share a core API: `band`, `bor`, `bxor`, `bnot`, `lshift`, The surface is not uniform beyond that, and the differences bite callers: - `mask` is **bit16 and bit32 only**. `bit64` has none. -- `to_unsigned` is **bit32 only**. +- `to_unsigned` and `to_signed` are **bit32 only**. - `bit64` alone adds the Int64 constructors and accessors below, plus the compat aliases `xor`, `shr`, `lsl`, `asr`. diff --git a/src/bitn/_compat.lua b/src/bitn/_compat.lua index cdbf6a4..bbc655a 100644 --- a/src/bitn/_compat.lua +++ b/src/bitn/_compat.lua @@ -289,53 +289,82 @@ function _compat.impl_name() return "pure Lua" end -function _compat.band(a, b) - local r = 0 - local bit_val = 1 - for _ = 0, 31 do - if (a % 2 == 1) and (b % 2 == 1) then - r = r + bit_val - end - a = math_floor(a / 2) - b = math_floor(b / 2) - bit_val = bit_val * 2 - if a == 0 and b == 0 then - break +-- 4-bit truth tables, indexed [a * 16 + b + 1], so each op is at most 8 steps. +local AND4, OR4, XOR4 = {}, {}, {} +for a = 0, 15 do + for b = 0, 15 do + local x, y, r_and, r_or, r_xor, bit_val = a, b, 0, 0, 0, 1 + for _ = 1, 4 do + local xb, yb = x % 2, y % 2 + if xb == 1 and yb == 1 then + r_and = r_and + bit_val + end + if xb == 1 or yb == 1 then + r_or = r_or + bit_val + end + if xb ~= yb then + r_xor = r_xor + bit_val + end + x, y, bit_val = (x - xb) / 2, (y - yb) / 2, bit_val * 2 end + local i = a * 16 + b + 1 + AND4[i], OR4[i], XOR4[i] = r_and, r_or, r_xor + end +end + +-- band(a, 2^k - 1) is a % 2^k. Built by doubling so the values are integers on 5.3+. +local LOW_MASK = {} +do + local m = 1 + for _ = 1, 32 do + m = m * 2 + LOW_MASK[m - 1] = m + end +end + +function _compat.band(a, b) + a, b = math_floor(a) % 0x100000000, math_floor(b) % 0x100000000 + local m = LOW_MASK[b] or LOW_MASK[a] + if m then + return (LOW_MASK[b] and a or b) % m + end + local r, scale = 0, 1 + while a > 0 and b > 0 do + local na, nb = a % 16, b % 16 + r = r + AND4[na * 16 + nb + 1] * scale + a, b, scale = (a - na) / 16, (b - nb) / 16, scale * 16 end return r end function _compat.bor(a, b) - local r = 0 - local bit_val = 1 - for _ = 0, 31 do - if (a % 2 == 1) or (b % 2 == 1) then - r = r + bit_val - end - a = math_floor(a / 2) - b = math_floor(b / 2) - bit_val = bit_val * 2 - if a == 0 and b == 0 then - break - end + a, b = math_floor(a) % 0x100000000, math_floor(b) % 0x100000000 + if a == 0 then + return b + elseif b == 0 then + return a + end + local r, scale = 0, 1 + while a > 0 or b > 0 do + local na, nb = a % 16, b % 16 + r = r + OR4[na * 16 + nb + 1] * scale + a, b, scale = (a - na) / 16, (b - nb) / 16, scale * 16 end return r end function _compat.bxor(a, b) - local r = 0 - local bit_val = 1 - for _ = 0, 31 do - if (a % 2) ~= (b % 2) then - r = r + bit_val - end - a = math_floor(a / 2) - b = math_floor(b / 2) - bit_val = bit_val * 2 - if a == 0 and b == 0 then - break - end + a, b = math_floor(a) % 0x100000000, math_floor(b) % 0x100000000 + if a == 0 then + return b + elseif b == 0 then + return a + end + local r, scale = 0, 1 + while a > 0 or b > 0 do + local na, nb = a % 16, b % 16 + r = r + XOR4[na * 16 + nb + 1] * scale + a, b, scale = (a - na) / 16, (b - nb) / 16, scale * 16 end return r end diff --git a/src/bitn/bit16.lua b/src/bitn/bit16.lua index ebb7f47..4dc44eb 100644 --- a/src/bitn/bit16.lua +++ b/src/bitn/bit16.lua @@ -171,7 +171,12 @@ end --- @return integer n 16-bit unsigned integer function bit16.be_bytes_to_u16(str, offset) offset = offset or 1 - assert(#str >= offset + 1, "Insufficient bytes for u16") + if offset ~= offset or offset < 1 then + error("Offset must be at least 1") + end + if #str < offset + 1 then + error("Insufficient bytes for u16") + end local b1, b2 = string_byte(str, offset, offset + 1) return b1 * 256 + b2 end @@ -182,7 +187,12 @@ end --- @return integer n 16-bit unsigned integer function bit16.le_bytes_to_u16(str, offset) offset = offset or 1 - assert(#str >= offset + 1, "Insufficient bytes for u16") + if offset ~= offset or offset < 1 then + error("Offset must be at least 1") + end + if #str < offset + 1 then + error("Insufficient bytes for u16") + end local b1, b2 = string_byte(str, offset, offset + 1) return b1 + b2 * 256 end @@ -398,6 +408,26 @@ function bit16.selftest() end end + local decoder_errors = { + { "le_bytes_to_u16 rejects offset 0", bit16.le_bytes_to_u16, "\1\2\3", 0, "Offset must be at least 1" }, + { "be_bytes_to_u16 rejects offset 0", bit16.be_bytes_to_u16, "\1\2\3", 0, "Offset must be at least 1" }, + { "le_bytes_to_u16 rejects a NaN offset", bit16.le_bytes_to_u16, "\1\2\3", 0 / 0, "Offset must be at least 1" }, + { "be_bytes_to_u16 rejects a NaN offset", bit16.be_bytes_to_u16, "\1\2\3", 0 / 0, "Offset must be at least 1" }, + { "le_bytes_to_u16 rejects a short buffer", bit16.le_bytes_to_u16, "\1", 1, "Insufficient bytes for u16" }, + { "be_bytes_to_u16 rejects a short buffer", bit16.be_bytes_to_u16, "\1\2", 2, "Insufficient bytes for u16" }, + } + for _, test in ipairs(decoder_errors) do + local test_name, fn, input, offset, message = test[1], test[2], test[3], test[4], test[5] + total = total + 1 + local raised, err_text = pcall(fn, input, offset) + if not raised and type(err_text) == "string" and string.find(err_text, message, 1, true) then + print(" PASS: " .. test_name) + passed = passed + 1 + else + print(" FAIL: " .. test_name) + end + end + print(string.format("\n16-bit operations: %d/%d tests passed\n", passed, total)) return passed == total end diff --git a/src/bitn/bit32.lua b/src/bitn/bit32.lua index ffa8469..ed7f89a 100644 --- a/src/bitn/bit32.lua +++ b/src/bitn/bit32.lua @@ -45,6 +45,17 @@ function bit32.to_unsigned(n) return compat_to_unsigned(n) end +--- Convert unsigned 32-bit value to signed. +--- The inverse of `to_unsigned`: values of 2^31 and above wrap to negative. +--- @param n number Unsigned 32-bit value (already-signed values pass through) +--- @return integer result Signed 32-bit value (-2^31 to 2^31 - 1) +function bit32.to_signed(n) + if n >= 0x80000000 then + return n - 0x100000000 + end + return n +end + --- Ensure value fits in 32-bit unsigned integer. --- @param n number Input value --- @return integer result 32-bit unsigned integer (0 to 0xFFFFFFFF) @@ -212,12 +223,19 @@ end local string_char = string.char local string_byte = string.byte +local string_pack = rawget(string, "pack") +local string_unpack = rawget(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 --- Convert 32-bit unsigned integer to 4 bytes (big-endian). --- @param n integer 32-bit unsigned integer --- @return string bytes 4-byte string in big-endian order function bit32.u32_to_be_bytes(n) - n = compat_band(n, MASK32) + if string_pack then + return string_pack(">I4", n % 0x100000000) + end + n = fast_band and compat_band(n, MASK32) or n % 0x100000000 return string_char( math_floor(n / 16777216) % 256, math_floor(n / 65536) % 256, @@ -230,7 +248,10 @@ end --- @param n integer 32-bit unsigned integer --- @return string bytes 4-byte string in little-endian order function bit32.u32_to_le_bytes(n) - n = compat_band(n, MASK32) + if string_pack then + return string_pack("= offset + 3, "Insufficient bytes for u32") + if offset ~= offset or offset < 1 then + error("Offset must be at least 1") + end + if #str < offset + 3 then + error("Insufficient bytes for u32") + end + if string_unpack then + return (string_unpack(">I4", str, offset)) + end local b1, b2, b3, b4 = string_byte(str, offset, offset + 3) return b1 * 16777216 + b2 * 65536 + b3 * 256 + b4 end @@ -256,7 +285,15 @@ end --- @return integer n 32-bit unsigned integer function bit32.le_bytes_to_u32(str, offset) offset = offset or 1 - assert(#str >= offset + 3, "Insufficient bytes for u32") + if offset ~= offset or offset < 1 then + error("Offset must be at least 1") + end + if #str < offset + 3 then + error("Insufficient bytes for u32") + end + if string_unpack then + return (string_unpack("I4I4", high, low) + end + return string_char( + math_floor(high / 16777216) % 256, + math_floor(high / 65536) % 256, + math_floor(high / 256) % 256, + math_floor(high % 256), + math_floor(low / 16777216) % 256, + math_floor(low / 65536) % 256, + math_floor(low / 256) % 256, + math_floor(low % 256) + ) end --- Convert 64-bit value to 8 bytes (little-endian). --- @param x Int64HighLow 64-bit value {high, low} --- @return string bytes 8-byte string in little-endian order function bit64.u64_to_le_bytes(x) - return bit32_u32_to_le_bytes(x[2]) .. bit32_u32_to_le_bytes(x[1]) + local high, low = x[1] % 0x100000000, x[2] % 0x100000000 + if string_pack then + return string_pack("= offset + 7, "Insufficient bytes for u64") + if offset ~= offset or offset < 1 then + error("Offset must be at least 1") + end + if #str < offset + 7 then + error("Insufficient bytes for u64") + end + if string_unpack then + local high, low = string_unpack(">I4I4", str, offset) + return setmetatable({ high, low }, Int64Meta) + end local high = bit32_be_bytes_to_u32(str, offset) local low = bit32_be_bytes_to_u32(str, offset + 4) return bit64.new(high, low) @@ -299,7 +336,16 @@ end --- @return Int64HighLow value {high, low} 64-bit value function bit64.le_bytes_to_u64(str, offset) offset = offset or 1 - assert(#str >= offset + 7, "Insufficient bytes for u64") + if offset ~= offset or offset < 1 then + error("Offset must be at least 1") + end + if #str < offset + 7 then + error("Insufficient bytes for u64") + end + if string_unpack then + local low, high = string_unpack(" 0 then + print(" PASS: from_number(-0.0) gives a +0 high word") + passed = passed + 1 + else + print(" FAIL: from_number(-0.0) gives a +0 high word") + end + + local decoder_errors = { + { "le_bytes_to_u64 rejects offset 0", bit64.le_bytes_to_u64, "\1\2\3\4\5\6\7\8\9", 0, "Offset must be at least 1" }, + { "be_bytes_to_u64 rejects offset 0", bit64.be_bytes_to_u64, "\1\2\3\4\5\6\7\8\9", 0, "Offset must be at least 1" }, + { + "le_bytes_to_u64 rejects a NaN offset", + bit64.le_bytes_to_u64, + "\1\2\3\4\5\6\7\8\9", + 0 / 0, + "Offset must be at least 1", + }, + { + "be_bytes_to_u64 rejects a NaN offset", + bit64.be_bytes_to_u64, + "\1\2\3\4\5\6\7\8\9", + 0 / 0, + "Offset must be at least 1", + }, + { + "le_bytes_to_u64 rejects a short buffer", + bit64.le_bytes_to_u64, + "\1\2\3\4\5\6\7", + 1, + "Insufficient bytes for u64", + }, + { + "be_bytes_to_u64 rejects a short buffer", + bit64.be_bytes_to_u64, + "\1\2\3\4\5\6\7\8", + 2, + "Insufficient bytes for u64", + }, + } + for _, test in ipairs(decoder_errors) do + local test_name, fn, input, offset, message = test[1], test[2], test[3], test[4], test[5] + total = total + 1 + local raised, err_text = pcall(fn, input, offset) + if not raised and type(err_text) == "string" and string.find(err_text, message, 1, true) then + print(" PASS: " .. test_name) + passed = passed + 1 + else + print(" FAIL: " .. test_name) + end + end + print(string.format("\n64-bit operations: %d/%d tests passed\n", passed, total)) return passed == total end