A builtin ‘integer’ type to represent 64 bit integer numbers.
Current methods of representing 64-bit integers in Luau with default environment are done with high-low pairs or by splitting them among vectors which leads to poor ergonomics and may not be sufficient for cases where performance is important.
In the case of a high-low pair implementation, each individual number takes 16 bytes (or 24) and needs to be be handled together which can be confusing.
In the case of vector implementations, ergonomics are improved by storing the entire integer in one value however you have the same issues of implementation complexity and lack of integration with the type system.
In both cases, performance is lacking compared to what could be provided by a native implementation.
As Luau grows, the restriction to doubles will be an increasing pain point for any use case which involves a 64-bit integer. System support for 64-bit integers is ubiquitous and their use is widespread.
Besides the above solutions in userland, runtimes are also able to define their own userdata for 64-bit integers. However, this requires each runtime to reimplement 64-bit integer support themselves, and carries with it the potential of significant differences in their semantics. Such an outcome would harm the ecosystem as a whole, requiring libraries to find a common API across all of their supported runtimes, or otherwise program around differences in their APIs.
Further, representing 64-bit numbers as heap-allocated userdata is sufficient for correctness, but also adds a layer of indirection for something that could fit within the existing value size. In domains where the use of these numbers is more common, the performance characteristics of these varying implementation techniques could be problematic or even prohibitive.
Userdata remain the right mechanism for complex or embedder-specific numeric types, but 64-bit integers have a sufficiently large problem domain to justify their inclusion as a core value type, and bring significant benefits for performance and interoperability in the overall Luau ecosystem.
This will be implemented a with new type called integer.
An additional character may be specified at the end of numeric literals i which will signify an 64 bit integer literal.
64 bit integer literals will support separators, hex, and binary values:
local a = 123i
local b = 1_000i
local c = 0xABABi
local d = 0b1000_1000i
Integer literal numbers have to be exactly representable, overflow is a parsing error.
Operations performing a string formatting of an integer should format the number as signed by default.
Integer values will have a built-in equality comparison, but will not have any other operators or metamethods defined.
Functions for creating and manipulating this type will exist in a new library called ‘integer`.
function integer.create(n: number): integer?
Converts a number to an integer.
Returns nil If the double number cannot be represented as an integer exactly, i.e. it has a fractional part, is out of range or is NaN.
This behavior is chosen to closely match math.tointeger from Lua 5.4 and can have a relatively efficient implementation.
function integer.fromstring(str: string, base: number?): integer?
Converts a string representation of a number into an integer.
Number inside the string has to be an integer.
String is allowed to have leading and trailing spaces and number can be preceded by a sign.
If base is not specified, number can have a 0x or 0X prefix to be converted in base 16, otherwise base 10 is used.
When base is specified, it has to be in range between 2 and 36 inclusive.
In base 10 and base 16, number is allowed to have a 0x or 0X prefix.
Returns nil if the string doesn’t contain a number.
This behavior is chosen to match tonumber, but excludes floating-point numbers.
function integer.tostring(n: integer): string
Converts an integer to a string representation.
function integer.tonumber(n: integer): number
Converts an integer to a double.
If the value cannot be represented as a double exactly, round to nearest, tie to even rounding mode is used.
function integer.neg(a: integer): integer
Negates the value. Overflow wraps around according to rules of two-complement arithmetic.
function integer.add(a: integer, b: integer): integer
Adds a to b.
Overflow wraps around according to rules of two-complement arithmetic.
function integer.sub(a: integer, b: integer): integer
Subtracts b from a.
Overflow wraps around according to rules of two-complement arithmetic.
function integer.mul(a: integer, b: integer): integer
Multiplies a and b.
Overflow wraps around according to rules of two-complement arithmetic.
function integer.div(a: integer, b: integer): integer
Performes signed truncated division of a by b.
If b is 0, throws a division by zero error.
If a is -2^63 and b is -1, throws an overflow error.
function integer.rem(a: integer, b: integer): integer
Computes remainder of the signed truncated division of a by b.
If b is 0, throws a division by zero error.
If a is -2^63 and b is -1, result is 0.
function integer.idiv(a: integer, b: integer): integer
Performes signed floored division of a by b.
If b is 0, throws a division by zero error.
If a is -2^63 and b is -1, throws an overflow error.
function integer.mod(a: integer, b: integer): integer
Performes signed floored modulus division of a by b.
If b is 0, throws a division by zero error.
If a is -2^63 and b is -1, result is 0.
function integer.udiv(a: integer, b: integer): integer
Performs unsigned division of a by b.
If b is 0, throws an error.
function integer.urem(a: integer, b: integer): integer
Computes remainder of the unsigned division of a by b.
If b is 0, throws an error.
function integer.min(a: integer, b: integer): integer
Returns the smallest of two integer numbers.
function integer.max(a: integer, b: integer): integer
Returns the largest of two integer numbers.
function integer.clamp(a: integer, min: integer, max: integer): integer
Returns a if the number is in [min, max] range; otherwise, returns min when a < min, and max otherwise.
The function errors if min > max, consistent with math.clamp.
function integer.band(a: integer, b: integer): integer
Performs a bitwise and of a and b.
function integer.bor(a: integer, b: integer): integer
Performs a bitwise or of a and b.
function integer.bnot(a: integer): integer
Returns a bitwise negation of the input number.
function integer.bxor(a: integer, b: integer): integer
Performs a bitwise xor (exclusive or) of a and b.
function integer.lt(a: integer, b: integer): boolean
Compares signed less than (<) comparison of a and b.
function integer.le(a: integer, b: integer): boolean
Compares signed less than or equal (<=) comparison of a and b.
function integer.ult(a: integer, b: integer): boolean
Compares unsigned less than (<) comparison of a and b.
function integer.ule(a: integer, b: integer): boolean
Compares unsigned less than or equal (<=) comparison of a and b.
function integer.lshift(n: integer, i: integer): integer
Shifts n to the left by i bits (if i is negative, a right shift is performed instead).
When i is outside of [-63..63] range, returns 0.
function integer.rshift(n: integer, i: integer): integer
Shifts n to the right by i bits (if i is negative, a left shift is performed instead).
When i is outside of [-63..63] range, returns 0.
function integer.arshift(n: integer, i: integer): integer
Shifts n by i bits to the right (if i is negative, a left shift is performed instead).
The most significant bit of n is propagated during the shift.
When i is larger than 63, returns an integer with all bits set to the sign bit of n.
When i is smaller than -63, 0 is returned.
function integer.lrotate(n: integer, i: integer): integer
Rotates n to the left by i bits (if i is negative, a right rotate is performed instead); the bits that are shifted past the bit width are shifted back from the right.
i is interpreted modulo 64.
function integer.rrotate(n: integer, i: integer): integer
Rotates n to the right by i bits (if i is negative, a left rotate is performed instead); the bits that are shifted past the bit width are shifted back from the left.
i is interpreted modulo 64.
function integer.extract(n: integer, f: integer, w: integer?): integer
Extracts bits of n at position f with a width of w.
w defaults to 1, so a two-argument version of extract returns the bit value at position f.
Bits are indexed starting at 0.
f cannot be negative.
w has to be positive (> 0).
f + w must be less than or equal to 64.
function integer.replace(n: integer, r: integer, f: integer, w: integer?): integer
Replaces bits of n at position f and width w with w least significant bits of r.
w defaults to 1, so a three-argument version of replace changes one bit at position f to r and returns the result.
Bits are indexed starting at 0.
f cannot be negative.
w has to be positive (> 0).
f + w must be less than or equal to 64.
function integer.btest(a: integer, b: integer): boolean
Perform a bitwise and a and b and returns true iff the result is not 0.
function integer.countrz(n: integer): integer
Returns the number of consecutive zero bits starting from the right-most (least significant) bit.
Returns 64 if n is zero.
function integer.countlz(n: integer): integer
Returns the number of consecutive zero bits starting from the left-most (most significant) bit.
Returns 64 if n is zero.
function integer.bswap(n: integer): integer
Returns n with the order of the bytes swapped.
Functions to read and write integer values are added.
Functions use integer in the name as opposed to i64 to signify the difference of the result/argument type from that of number.
function buffer.readinteger(b: buffer, offset: number): integer
Reads an integer out of a buffer at the given offset.
function buffer.writeinteger(b: buffer, offset: number, value: integer): ()
Writes an integer to a buffer at the given offset.
For parity with Lua 5.3+, two additional constants are added to the math library.
math.maxinteger
Integer value representing 2^63-1 (9_223_372_036_854_775_807i)
math.mininteger
Integer value representing -2^63 (-9_223_372_036_854_775_808i)
string.format function is updated to support integer arguments.
‘d’, ‘i’ and ‘*’ format specifiers will format integer as a signed 64 bit integer number.
‘o’, ‘u’, ‘x’ and ‘X’ format specifiers will format integer as an unsigned 64 bit integer number.
int64_t lua_tointeger64(lua_State *L, int idx, int* isinteger)
Returns a value of an integer at idx or 0 on failure.
If isinteger is not a null pointer, writes 1 if the value at idx was an integer value and 0 on failure.
void lua_pushinteger64(lua_State *L, int64_t n)
Pushes a integer value on top of the stack.
int lua_isinteger64(lua_State *L, int idx)
Determines if a value at idx is an integer.
This function is implemented as a C macro.
int64_t luaL_checkinteger64(lua_State *L, int narg)
Returns a value of an integer at narg or throws an error on failure.
int64_t luaL_optinteger64(lua_State *L, int narg, int64_t def)
Returns a value of an integer at narg or def if there is no value.
Throws an error if there is a value but it is not an integer.
luaopen_integer(lua_State *L)
Registers the integer library.
Included in luaL_openlibs.
This increases the complexity of the VM as it may need to implement separate paths for mathematical operations.
Do nothing, the workarounds exist and are in use but this will restrict the areas Luau can be used without implementation burden or reliance on an externally maintained implementation of 64-bit integers.