Skip to content

Commit 604f5b8

Browse files
committed
Add bit32.s32() and bit32.smul() for signed 32-bit int math
1 parent f10710d commit 604f5b8

File tree

4 files changed

+102
-0
lines changed

4 files changed

+102
-0
lines changed

VM/src/lbitlib.cpp

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,43 @@ static int b_swap(lua_State* L)
252252
return 1;
253253
}
254254

255+
// ServerLua: convert double to signed 32-bit via fmod (no UB for any input)
256+
static int32_t double_to_s32(double n)
257+
{
258+
// NaN/Inf: i386 fistp returns INT32_MIN ("integer indefinite")
259+
if (!isfinite(n))
260+
return INT32_MIN;
261+
double truncated;
262+
modf(n, &truncated);
263+
// fmod handles arbitrarily large values; result is in (-2^32, 2^32)
264+
constexpr double kUint32Mod = (double)UINT32_MAX + 1.0;
265+
constexpr double kInt32Upper = -(double)INT32_MIN;
266+
double result = fmod(truncated, kUint32Mod);
267+
// Wrap into signed 32-bit range [-2^31, 2^31)
268+
if (result >= kInt32Upper)
269+
result -= kUint32Mod;
270+
else if (result < -kInt32Upper)
271+
result += kUint32Mod;
272+
return (int32_t)result;
273+
}
274+
275+
// ServerLua: normalize to signed 32-bit range using pure floating-point math (no UB)
276+
static int b_s32(lua_State* L)
277+
{
278+
lua_pushnumber(L, (double)double_to_s32(luaL_checknumber(L, 1)));
279+
return 1;
280+
}
281+
282+
// ServerLua: signed 32-bit multiply without float64 precision loss
283+
static int b_smul(lua_State* L)
284+
{
285+
int32_t a = double_to_s32(luaL_checknumber(L, 1));
286+
int32_t b = double_to_s32(luaL_checknumber(L, 2));
287+
int32_t result = (int32_t)((int64_t)a * (int64_t)b);
288+
lua_pushnumber(L, (double)result);
289+
return 1;
290+
}
291+
255292
static const luaL_Reg bitlib[] = {
256293
{"arshift", b_arshift},
257294
{"band", b_and},
@@ -265,6 +302,8 @@ static const luaL_Reg bitlib[] = {
265302
{"replace", b_replace},
266303
{"rrotate", b_rrot},
267304
{"rshift", b_rshift},
305+
{"s32", b_s32},
306+
{"smul", b_smul},
268307
{"countlz", b_countlz},
269308
{"countrz", b_countrz},
270309
{"byteswap", b_swap},

tests/SLConformance.test.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,11 @@ TEST_CASE("Integer bitwise operations")
730730
runConformance("integer_bitwise.lua");
731731
}
732732

733+
TEST_CASE("bit32.s32")
734+
{
735+
runConformance("bit32_s32.lua");
736+
}
737+
733738
// ServerLua: interrupt state for lljson yield tests
734739
static bool jsonInterruptEnabled = false;
735740
static int jsonYieldCount = 0;

tests/conformance/bit32_s32.lua

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
-- basic identity cases
2+
assert(bit32.s32(0) == 0)
3+
assert(bit32.s32(1) == 1)
4+
assert(bit32.s32(-1) == -1)
5+
6+
-- positive values within signed range are unchanged
7+
assert(bit32.s32(0x7FFFFFFF) == 2147483647)
8+
9+
-- unsigned values above INT32_MAX wrap to negative
10+
assert(bit32.s32(0x80000000) == -2147483648)
11+
assert(bit32.s32(0xFFFFFFFF) == -1)
12+
assert(bit32.s32(0xFFFFFFFE) == -2)
13+
14+
-- composing with bit32 operations (the primary use case)
15+
assert(bit32.s32(bit32.bnot(0)) == -1)
16+
assert(bit32.s32(bit32.bor(0x80000000, 1)) == -2147483647)
17+
18+
-- truncation toward zero
19+
assert(bit32.s32(2.7) == 2)
20+
assert(bit32.s32(-2.7) == -2)
21+
assert(bit32.s32(0.9) == 0)
22+
assert(bit32.s32(-0.9) == 0)
23+
24+
-- modular wrapping for values beyond uint32 range
25+
assert(bit32.s32(0x100000000) == 0)
26+
assert(bit32.s32(0x100000001) == 1)
27+
assert(bit32.s32(0x1FFFFFFFF) == -1)
28+
29+
-- large values still wrap correctly (no UB through fmod path)
30+
assert(bit32.s32(1e15) == -1530494976)
31+
assert(bit32.s32(-1e15) == 1530494976)
32+
33+
-- NaN and Inf produce INT32_MIN (matches i386 "integer indefinite")
34+
assert(bit32.s32(0/0) == -2147483648)
35+
assert(bit32.s32(1/0) == -2147483648)
36+
assert(bit32.s32(-1/0) == -2147483648)
37+
38+
-- result is always a plain number, never an LSL integer
39+
assert(type(bit32.s32(42)) == "number")
40+
41+
-- smul: basic multiplication
42+
assert(bit32.smul(3, 4) == 12)
43+
assert(bit32.smul(-3, 4) == -12)
44+
assert(bit32.smul(-3, -4) == 12)
45+
46+
-- smul: overflow wrapping
47+
assert(bit32.smul(0x7FFFFFFF, 2) == -2)
48+
assert(bit32.smul(0x80000000, -1) == -2147483648)
49+
50+
-- smul: the case that bit32.s32(a*b) gets wrong due to float64 precision loss
51+
assert(bit32.smul(0x10000, 0x10000) == 0)
52+
53+
-- smul: result is always a plain number
54+
assert(type(bit32.smul(2, 3)) == "number")
55+
56+
return "OK"

tests/conformance/types.luau

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ local ignore =
1212
"_G.dangerouslyexecuterequiredmodule",
1313
"_G.table.append",
1414
"_G.table.extend",
15+
"_G.bit32.s32",
16+
"_G.bit32.smul",
1517

1618
-- what follows is a set of mismatches that hopefully eventually will go down to 0
1719
"_G.require", -- need to move to Roblox type defs

0 commit comments

Comments
 (0)