* Don't know what's going on with the Deno docker image, disabling the deno.lock file for now * Another try * Space Lua: Align numeric and table semantics with Lua This change improves Space Lua compatibility with standard Lua 5.4, focusing on numeric subtypes and table behavior. The test suite is extended to lock in the expected semantics and should pass under both Space Lua and a Lua interpreter. SUMMARY OF CHANGES ------------------ Tighten Lua compatibility across evaluator and runtime: - correct metamethod dispatch (`__index`, `__newindex`, `__call`, comparison metamethods), - loop limits, - raw metamethod lookups. Rework numeric semantics to preserve Lua **integer** vs **float** behavior: - $0$ vs $0.0$ and $-0.0$, - explicit zero kind representation, and - updates arithmetic/bitwise coercions accordingly. Improve parser correctness by rejecting **unary plus** with aligned Lua errors and better parsing errors reporting. Fix `stdlib` behavior to match that of Lua: - `table` function `concat`, `insert`, `remove`, `sort` and `unpack` gain metamethod awareness and enforce Lua errors, - `ipairs` iteration updated to stop on first nil and honor `__index`, - `tonumber` updated to Luae conversion using `luaToNumberDetailed`, - `math.modf` return corrected, - `math.type` accuracy improvements for float/integer and $-0.0$, - `math.pi` added. Fix numeric subtypes for `/` and `^` operators so `math.type` matches Lua results using tagging as well as unary `-`. Expand test coverage: - new `metamethods_test.lua` for Lua metamethod/operator semantics, - extend arithmetic and length tests for zero-kind propagation and `rawlen` vs `__len` metamethod, - Extend `math` test suite to test proper Lua alignment (`math.type` and more), and - update context error expectations for Lua error messages. RATIONALE --------- Lua differs from JavaScript by having two numeric subtypes: **integer** and **float**. Operators depend on the subtype: `+`, `-`, `*`, `//` and `%` use integer mode when both operands are integers and float mode otherwise. Bitwise operators require integers and `math.type(x)` reports "integer" or "float". JavaScript has one numeric primitive type (`number`) so a plain number value cannot record whether Lua considers a value to be a float when the value has no fractional part (for example $2.0$). Lua also differs from "everything is IEEE 754 double" because the rules are defined in terms of integer and float subtypes. Float operations preserve IEEE 754 behavior including `NaN`, infinities and signed zero ($-0.0$) which affects results like $1/0.0$ versus $1/-0.0$. Integer arithmetic does not preserve $-0$ and collapses it to $0$. Lua integer arithmetic is exact within its integer range while JavaScript `number` cannot exactly represent all integers in that range. Lua numbers are integers or floats. Numeric strings coerce to integer or float based on _lexical_ form. Each arithmetic operator selects the result subtype from the operator rules and operand subtypes. Integer only operators (bitwise and `//` as integer division) require integer representability. Mixed arithmetic promotes to float as needed. Two operators are **always float** typed: division (`/`) and exponentiation (`^`) produce floats even if both operands are integers and even if the numeric value has no fractional part. `math.type` reports that internal subtype. Tables are associative arrays and assigning `nil` removes a key. The length operator `#` uses `__len` metamethod if present otherwise it uses the raw length rule. `rawlen(table)` ignores `__len`. Without `__len` Lua defines `#` as some boundary `N` such that `table[N]` is not `nil` and `table[N+1]` is `nil`. If the table has holes (missing or `nil` entries in the positive integer key sequence) the boundary may be non unique so `#` is stable only for proper sequences without holes. PERFORMANCE NOTES ----------------- Numeric changes add small checks to preserve Lua integer and float subtype semantics and avoid allocations except when the subtype would otherwise be lost. Some table operations may be slower due to stricter Lua 5.4 behavior especially around length and sequence boundary handling which currently requires extra metadata tracking and scans and cannot be avoided without a completely different internal table representation. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Another try * Replace `LuaFloatTag` plain objects with boxed Number for float tagging * Restore pre-merge eval/numeric architecture and fix regressions This commit restores the original branch architecture. On top of the restored foundation, float-typed integer results (e.g. `1.0 + 1.0` = `2.0`, `0.0 // 1.0` = `0.0`) are now correctly tagged via `makeLuaFloat` so that `tostring` and `math.type` report them as floats. The *unary minus* fast path for float literals and the `tonumber` function also preserve float tagging. Performance regressions from the merge are addressed by avoiding `Number` boxing for non-integer floats (`3.14` needs no tag — it is unambiguously float), adding *string key* fast paths in `LuaTable` `has`/`rawGet`/`rawSet` to skip numeric normalization for the dominant case, and inlining a `typeof` check in math standard library functions to avoid function call overhead on plain numbers. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Replace boxed `Number` float tagging with plain tagged objects * Replace `new Number()` boxing with plain tagged float objects for Lua float type tracking. * Integer-valued floats that need type disambiguation are now represented as `{ value: number, isFloat: true }` instead of boxed `Number` objects with a symbol property. * Pre-allocated singletons are used for positive and negative float zeros to avoid allocation entirely in common cases. * Updated all detection, unwrapping, and coercion paths across `numeric.ts`, `runtime.ts`, `eval.ts`, `stdlib.ts`, and `stdlib/` modules to use the new `isTaggedFloat` type guard. * Removed all `instanceof Number` checks. * Deleted the `FloatKind` symbol and eliminated redundant helpers `isLuaFloat`, `isFloatTag`, `getZeroBoxKind` and `toPlainNumber` that became dead code. * Simplified `math.type`, `luaToString`, `luaEquals`, `luaTypeName` and various other key normalization paths. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Add fast paths in `coerceNumericPair` for tagged float operands Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Fix copy/paste typo Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Avoid extra `LuaEnv` allocations in "For" and "ForIn" loops Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * [Space Lua] Reuse loop variable environment in `for` and `for-in` loops Numeric `for` and generic `for-in` loops allocated a fresh `LuaEnv` on every iteration to hold loop variables. But this is only necessary when a closure inside the loop body captures the loop variable. The optimization uses a two-level check computed at parse time. If no function definition exists in the loop's subtree, environment reuse is safe. When a function definition is present a deeper analysis walks the block to determine whether any function body references the loop variable names without them being shadowed by its own parameters. When a closure captures a loop variable the loop fall back to per-iteration allocation. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * [Space Lua] Format numbers using standard Lua rules Standard Lua formats floats via C `sprintf("%.14g")` (14 significant digits, scientific notation when shorter, exponent padded to 2 digits, and a guaranteed `.0` suffix for integer-valued floats). * Replace the old `luaFormatNumber` with JS `toPrecision(14)`-based implementation that reproduces this behavior. * Integrates it so that `${}` expressions in the UI also display numbers correctly. * Fixes tagged floats (`{ value, isFloat }`) were being stripped by `luaValueToJS` or matched as plain objects before reaching the number formatter. This caused `${}` expressions to render raw JS numbers. Examples: ``` - ${tostring(2^63)} - ${2^63} - ${(2^63)} ``` All of the the above examples show correct `9.2233720368548e+18` now. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * [Space Lua] Fix `string.format` for floats and tagged numbers Unwrap tagged floats before `printf`, handle `inf`/`-inf`/`-nan` in `formatDouble`, and fix `%g` producing `0e+00` for zero. Hopefuly it's enough to gain Lua formatting. Tests: ``` - ${string.format("%.14g", 0.0)} - `0` - ${string.format("%.14g", 1.0)} - `1` - ${string.format("%.14g", 1/3)} - `0.33333333333333` - ${string.format("%.14g", math.pi)} - `3.1415926535898` - ${string.format("%.14g", 1e-10)} - `1e-10` - ${string.format("%.14g", 1e18)} - `1e+18` - ${string.format("%.14g", 2^63)} - `9.2233720368548e+18` - ${string.format("%.14g", 2^53)} - `9.007199254741e+15` - ${string.format("%.14g", 1.7976931348623e+308)} - `1.7976931348623e+308` - ${string.format("%.14g", 5e-324)} - `4.9406564584125e-324` - ${string.format("%.14g", 0/0)} - `-nan` - ${string.format("%.14g", 1/0)} - `inf` - ${string.format("%.14g", -1/0)} - `-inf` ``` Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * [Space Lua] Add and integrate new `luaFormat` utility and test suite * Add `luaFormat` string formatting function compatible with Lua, and integrate it across the codebase as a replacement for prior formatting approaches. * Add extensive test suite. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Fix check Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * [Space Lua] Add `%a`/`%A` and `%q` format specifiers to `string.format` Implement hexadecimal floating-point (`%a`/`%A`) and quoted literal (`%q`) specifiers. * Add `%a`/`%A` as IEEE 754 double decomposition with full flag, width and precision support. * Add `%q` as producind valid Lua literals for strings, numbers, booleans and nil. * Use `Math.PI` for `math.pi` to preserve full double precision. * Remove Deno based test suite and replace it with native Lua test suite. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * [Space Lua] Add `%p` format specifier to `string.format` In standard Lua, `%p` formats the internal C heap address of a value, producing output like `0x55a3bc4e2f10`. It works on tables, functions, threads, strings, and userdata (GC-ed objects). For `nil`, booleans, and numbers it returns `(null)`. In Space Lua, there are no *raw memory addresses* since the runtime is JavaScript. Instead, `%p` assigns a *stable sequential integer* to each object via a `WeakMap`, formatted as a 14-digit zero-padded hex value. The key difference is that identifiers are deterministic and sequential rather than random-looking heap addresses: ```lua local t = {} print(string.format("identifier: %p", t)) -- 0x00000000000001 print(string.format("the same: %p", t)) -- 0x00000000000001 print(string.format("another: %p", {}) -- 0x00000000000002 ``` For strings, a regular `Map` is used so identical string content always produces the same identifier. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Revert merge changes to the deno.json Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Replace `interface` with `type` Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Remove `has_math()` relict function test Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Refactor loop to map Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Remove `Deno.remove("deno.lock")` weirdness Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Refactor: Early return undefined in `astNumberKind` instead of assigning Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> --------- Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> Co-authored-by: Zef Hemel <zef@zef.me>
1744 lines
46 KiB
Lua
1744 lines
46 KiB
Lua
local function assert_eq(actual, expected, message)
|
|
if actual ~= expected then
|
|
error('Assertion failed: ' .. message)
|
|
end
|
|
end
|
|
|
|
local function assertThrows(msg_substr, fn)
|
|
local ok, err = pcall(fn)
|
|
|
|
if ok then
|
|
error('Assertion failed: expected error containing "'
|
|
.. msg_substr .. '"')
|
|
end
|
|
|
|
if type(err) ~= 'string' then
|
|
err = tostring(err)
|
|
end
|
|
|
|
if not string.find(err, msg_substr, 1, true) then
|
|
error('Assertion failed: expected error message to contain "'
|
|
.. msg_substr .. '", got: "' .. err .. '"')
|
|
end
|
|
end
|
|
|
|
-- 1. Integer vs float zero divisors
|
|
|
|
-- 1.1. Integer zeros collapse (no -0 for integers)
|
|
assert_eq(1/0 == 1/-0, true, 'int: 1/0 == 1/-0 (+Inf)')
|
|
assert_eq(-1/0 == 1/-0, false, 'int: -1/0 != 1/-0')
|
|
|
|
-- 1.2. Float zeros sign preservation
|
|
assert_eq(1/0.0 == 1/-0.0, false, 'float: +Inf != -Inf')
|
|
assert_eq(1/0.0 == -1/-0.0, true, 'float: 1/0.0 == -1/-0.0')
|
|
assert_eq(-1/0.0 == 1/-0.0, true, 'float: -1/0.0 == 1/-0.0')
|
|
|
|
-- 1.3. Basic division
|
|
assert_eq(5/2, 2.5, 'div: 5/2 == 2.5')
|
|
assert_eq(-5/2, -2.5, 'div: -5/2 == -2.5')
|
|
assert_eq(5/-2, -2.5, 'div: 5/-2 == -2.5')
|
|
assert_eq(-5/-2, 2.5, 'div: -5/-2 == 2.5')
|
|
|
|
-- 2. Unary minus literals and simple expressions
|
|
assert_eq(1/-(0) == 1/0, true, 'unary minus: int literal (+Inf)')
|
|
assert_eq(1/-(0.0) == -1/0.0, true, 'unary minus: float literal (-Inf)')
|
|
assert_eq(1/-(1-1) == 1/0, true, 'unary minus: int expr (+Inf)')
|
|
assert_eq(1/-(1.0-1.0) == -1/0.0, true, 'unary minus: float expr (-Inf)')
|
|
|
|
-- 2.1. Unary minus coercion and precedence with power
|
|
assert_eq(-2^2, -4, 'precedence: -2^2 == -(2^2)')
|
|
assert_eq((-2)^2, 4, 'precedence: (-2)^2 == 4')
|
|
|
|
-- 2.1.1. Unary minus with float-typed exponentiation results
|
|
do
|
|
local function mt(x)
|
|
return math.type(x)
|
|
end
|
|
|
|
assert_eq(mt(-("1.0" ^ 1)), "float", "math.type(-('1.0'^1)) => float")
|
|
assert_eq(mt(-"1.0" ^ 1), "float", "math.type(-'1.0'^1) => float")
|
|
assert_eq(mt(-("2.0" ^ 1)), "float", "math.type(-('2.0'^1)) => float")
|
|
assert_eq(mt(-("2.0" ^ 2)), "float", "math.type(-('2.0'^2)) => float")
|
|
end
|
|
|
|
-- 2.2. Unary minus uses table metadata for dynamic keys
|
|
local dyn_tbl = {}
|
|
local dyn_key = 'zf'
|
|
dyn_tbl[dyn_key] = 0.0
|
|
assert_eq(1/-(dyn_tbl[dyn_key]) == -1/0.0, true, 'unary minus: table dyn key float (-Inf)')
|
|
|
|
-- 2.3. Unary minus uses table metadata for property access
|
|
local prop_tbl = { zf = 0.0, zi = 0 }
|
|
assert_eq(1/-(prop_tbl.zf) == -1/0.0, true, 'unary minus: table prop float (-Inf)')
|
|
assert_eq(1/-(prop_tbl.zi) == 1/0, true, 'unary minus: table prop int (+Inf)')
|
|
|
|
-- 2.4. Unary minus uses env metadata for locals
|
|
local u_zi, u_zf, u_zfn = 0, 0.0, -0.0
|
|
assert_eq(1/-(u_zi) == 1/0, true, 'var: unary minus zi (+Inf)')
|
|
assert_eq(1/-(u_zfn) == 1/0.0, true, 'var: unary minus zfn (+Inf)')
|
|
assert_eq(1/u_zf == 1/0.0, true, 'var: zf (+Inf)')
|
|
|
|
-- 3. Integer operations (must not produce -0)
|
|
assert_eq(1/(1-1) == 1/0, true, 'int: sub (+0)')
|
|
assert_eq(1/(0*-1) == 1/0, true, 'int: mul (+0)')
|
|
assert_eq(1/(0%1) == 1/0, true, 'int: mod (+0)')
|
|
assert_eq(1/(0%-1) == 1/0, true, 'int: mod neg divisor (+0)')
|
|
|
|
-- 4. Float operations (must preserve -0.0)
|
|
assert_eq(1/(0.0*-1.0) == -1/ 0.0, true, 'float: mul (-0.0)')
|
|
assert_eq(1/((-0.0)%1.0) == -1/ 0.0, true, 'float: mod (-0.0)')
|
|
assert_eq(1/((-0.0)%-1.0) == -1/ 0.0, true, 'float: mod neg divisor (-0.0)')
|
|
|
|
-- 4.1. Zero result from float addition prefers +0.0
|
|
assert_eq(1/((-0.0)+0.0) == 1/0.0, true, 'float: (-0.0)+0.0 yields +0.0')
|
|
|
|
-- 4.2. Plain -0.0 handling in arithmetic
|
|
do
|
|
local nz = -0.0
|
|
-- Addition: -0.0 + 0 yields +0.0 (IEEE 754 rule)
|
|
assert_eq(1/(nz + 0), 1/0.0, 'plain -0.0: nz + 0 yields +0.0')
|
|
assert_eq(1/(0 + nz), 1/0.0, 'plain -0.0: 0 + nz yields +0.0')
|
|
|
|
-- Multiplication preserves -0.0
|
|
assert_eq(1/(nz * 1), -1/0.0, 'plain -0.0: nz * 1 yields -0.0')
|
|
assert_eq(1/(1 * nz), -1/0.0, 'plain -0.0: 1 * nz yields -0.0')
|
|
|
|
-- Subtraction: -0.0 - 0 = -0.0
|
|
assert_eq(1/(nz - 0), 1/0.0, 'plain -0.0: nz - 0 yields +0.0')
|
|
|
|
-- Subtraction: -0.0 - 0.0 = -0.0 (float)
|
|
assert_eq(1/(nz - 0.0), -1/0.0, 'plain -0.0: nz - 0.0 yields -0.0')
|
|
|
|
-- Subtraction: 0 - (-0.0) = +0.0
|
|
assert_eq(1/(0 - nz), 1/0.0, 'plain -0.0: 0 - nz yields +0.0')
|
|
end
|
|
|
|
-- 4.3. Expression-generated -0.0
|
|
do
|
|
local r = 0.0 * -1.0
|
|
assert_eq(1/r, -1/0.0, 'expr: 0.0 * -1.0 yields -0.0')
|
|
|
|
local s = -1.0 * 0.0
|
|
assert_eq(1/s, -1/0.0, 'expr: -1.0 * 0.0 yields -0.0')
|
|
|
|
-- Division producing -0.0
|
|
local d = -0.0 / 1.0
|
|
assert_eq(1/d, -1/0.0, 'expr: -0.0 / 1.0 yields -0.0')
|
|
end
|
|
|
|
-- 5. Mixed arithmetic producing zero
|
|
assert_eq(1/(0*-1.0) == -1/0.0, true, 'mixed: mul int*float (-0.0)')
|
|
assert_eq(1/(0.0*-1 ) == -1/0.0, true, 'mixed: mul float*int (-0.0)')
|
|
assert_eq(1/(1.0+(-1)) == 1/0.0, true, 'mixed: add (+0.0)')
|
|
assert_eq(1/(-(1-1.0)) == -1/0.0, true, 'mixed: sub then unary minus (-0.0)')
|
|
|
|
-- 5.1. Dynamic key table metadata affects binary ops
|
|
local dyn_t, dyn_k = {}, 'zf'
|
|
dyn_t[dyn_k] = 0.0
|
|
assert_eq(1/dyn_t[dyn_k] == 1/0.0, true, 'binary: table dyn key float (+Inf)')
|
|
|
|
-- 6. Variables
|
|
local zi, zf, zfn = 0, 0.0, -0.0
|
|
|
|
assert_eq(1/zi == 1/0, true, 'var: zi (+Inf)')
|
|
assert_eq(1/zf == 1/0.0, true, 'var: zf (+Inf)')
|
|
assert_eq(1/zfn == -1/0.0, true, 'var: zfn (-Inf)')
|
|
assert_eq(1/-(zi) == 1/0, true, 'var: unary minus zi (+Inf)')
|
|
assert_eq(1/-(zfn) == 1/0.0, true, 'var: unary minus zfn (+Inf)')
|
|
|
|
-- 6.1. Variables: metadata must flow through reassignment
|
|
local zswap = 0.0
|
|
assert_eq(1/zswap == 1/0.0, true, 'var: zswap float (+Inf)')
|
|
zswap = 0
|
|
assert_eq(1/zswap == 1/0, true, 'var: zswap reassigned int (+Inf)')
|
|
zswap = 0.0
|
|
assert_eq(1/zswap == 1/0.0, true, 'var: zswap reassigned float (+Inf)')
|
|
|
|
-- 7. Functions returning zeros and unary minus
|
|
local function ret_zi()
|
|
return 0
|
|
end
|
|
|
|
local function ret_zf()
|
|
return 0.0
|
|
end
|
|
|
|
local function ret_zfn()
|
|
return -0.0
|
|
end
|
|
|
|
assert_eq(1/ret_zi() == 1/0, true, 'fn: ret_zi (+Inf)')
|
|
assert_eq(1/ret_zf() == 1/0.0, true, 'fn: ret_zf (+Inf)')
|
|
assert_eq(1/ret_zfn() == -1/0.0, true, 'fn: ret_zfn (-Inf)')
|
|
|
|
assert_eq(1/-(ret_zi()) == 1/0, true, 'fn unary minus: ret_zi (+Inf)')
|
|
assert_eq(1/-(ret_zf()) == -1/0.0, true, 'fn unary minus: ret_zf (-Inf)')
|
|
assert_eq(1/-(ret_zfn()) == 1/0.0, true, 'fn unary minus: ret_zfn (+Inf)')
|
|
|
|
-- 8. Tables and arrays
|
|
local t, arr = {zi=zi, zfn=zfn}, {zi, zfn}
|
|
|
|
-- 8.1. Tables
|
|
assert_eq(1/t.zi == 1/0, true, 'table: t.zi (+Inf)')
|
|
assert_eq(1/t.zfn == -1/0.0, true, 'table: t.zfn (-Inf)')
|
|
|
|
-- 8.2. Arrays
|
|
assert_eq(1/arr[1] == 1/0, true, 'array: arr[1]=zi (+Inf)')
|
|
assert_eq(1/arr[2] == -1/0.0, true, 'array: arr[2]=zfn (-Inf)')
|
|
|
|
-- 8.3. Arrays: dynamic numeric index key metadata
|
|
local arr2 = {}
|
|
local idx = 1
|
|
arr2[idx] = 0.0
|
|
assert_eq(1/arr2[idx] == 1/0.0, true, 'array: dyn index float (+Inf)')
|
|
|
|
-- 9. Deeply nested parentheses and expressions
|
|
local xi, xf = 1-1, 1.0-1.0
|
|
local deepi = -((((0+0)-(1-1))+(zi-xi))) -- int path (+0)
|
|
local deepf = -((((0.0+0.0)-(1.0-1.0))+(zf-xf))) -- float path (-0.0)
|
|
|
|
assert_eq(1/deepi == 1/0, true, 'nested: int (+Inf)')
|
|
assert_eq(1/deepf == -1/0.0, true, 'nested: float (-Inf)')
|
|
|
|
-- 10. Floor division near zero
|
|
assert_eq(1/(0//1) == 1/0, true, 'floor div: int (+0)')
|
|
assert_eq(1/(0.0//1.0) == 1/0.0, true, 'floor div: float (+Inf)')
|
|
assert_eq(1/((-0.0)//1.0) == -1/0.0, true, 'floor div: float (-Inf)')
|
|
assert_eq(1/(0//1.0) == 1/0.0, true, 'floor div: mixed (+0.0)')
|
|
|
|
-- 10.1. Modulo/division identity
|
|
local function id_ok(a, b)
|
|
return a == b * (a // b) + a % b
|
|
end
|
|
|
|
assert_eq(id_ok(5, 2), true, 'identity: 5, 2')
|
|
assert_eq(id_ok(-5, 2), true, 'identity: -5, 2')
|
|
assert_eq(id_ok(5, -2), true, 'identity: 5, -2')
|
|
assert_eq(id_ok(-5, -2), true, 'identity: -5, -2')
|
|
|
|
-- 10.2. Floor division signs
|
|
assert_eq(5//-2, -3, 'idiv: 5//-2 == -3')
|
|
assert_eq(-5//2, -3, 'idiv: -5//2 == -3')
|
|
assert_eq(-5//-2, 2, 'idiv: -5//-2 == 2')
|
|
|
|
-- 11. Ordering and NaN
|
|
assert_eq((-0.0) < (0.0), false, 'ordering: -0.0 < 0.0 is false')
|
|
assert_eq((0.0) < (-0.0), false, 'ordering: 0.0 < -0.0 is false')
|
|
assert_eq((-0.0) <= (0.0), true, 'ordering: -0.0 <= 0.0')
|
|
assert_eq((0.0) <= (-0.0), true, 'ordering: 0.0 <= -0.0')
|
|
assert_eq((0/0) == (0/0), false, 'NaN: never equals itself')
|
|
|
|
-- 12. Bitwise operators
|
|
assert_eq((~0) == -1, true, 'bitwise not on int ok')
|
|
|
|
val = pcall(
|
|
function()
|
|
return ~0.0
|
|
end
|
|
)
|
|
assert_eq(val, true, 'bitwise not on float ok')
|
|
|
|
val = pcall(
|
|
function()
|
|
return 0<<1
|
|
end
|
|
)
|
|
assert_eq(val, true, 'shl int ok')
|
|
|
|
val = pcall(
|
|
function()
|
|
return 0.0<<1
|
|
end
|
|
)
|
|
assert_eq(val, true, 'shl float ok')
|
|
|
|
-- 12.1 Bitwise ops results
|
|
assert_eq((5&3) == 1, true, 'bitwise and result')
|
|
assert_eq((5|2) == 7, true, 'bitwise or result')
|
|
assert_eq((5~1) == 4, true, 'bitwise xor result')
|
|
assert_eq((1<<5) == 32, true, 'bitwise shl result')
|
|
assert_eq((32>>5) == 1, true, 'bitwise shr result')
|
|
|
|
-- 12.2 Bitwise with float values
|
|
assert_eq((~(-0.0)) == -1, true, 'bitwise not on -0.0 == -1')
|
|
|
|
-- 13. Evaluation order (left-to-right) for binary ops
|
|
local log, val
|
|
|
|
-- arithmetic + - * /
|
|
local function lhs_num()
|
|
log[#log + 1] = 'L'
|
|
return 1
|
|
end
|
|
|
|
local function rhs_num()
|
|
log[#log + 1] = 'R'
|
|
return 2
|
|
end
|
|
|
|
log = {}
|
|
val = lhs_num()+rhs_num()
|
|
assert_eq(table.concat(log, ''), 'LR', 'order: +')
|
|
|
|
log = {}
|
|
val = lhs_num()-rhs_num()
|
|
assert_eq(table.concat(log, ''), 'LR', 'order: -')
|
|
|
|
log = {}
|
|
val = lhs_num()*rhs_num()
|
|
assert_eq(table.concat(log, ''), 'LR', 'order: *')
|
|
|
|
log = {}
|
|
val = lhs_num()/rhs_num()
|
|
assert_eq(table.concat(log, ''), 'LR', 'order: /')
|
|
|
|
-- floor div and mod
|
|
log = {}
|
|
val = lhs_num()//rhs_num()
|
|
assert_eq(table.concat(log, ''), 'LR', 'order: //')
|
|
|
|
log = {}
|
|
val = lhs_num()%rhs_num()
|
|
assert_eq(table.concat(log, ''), 'LR', 'order: %')
|
|
|
|
-- power and concatenation
|
|
log = {}
|
|
val = lhs_num()^rhs_num()
|
|
assert_eq(table.concat(log, ''), 'LR', 'order: ^')
|
|
|
|
local function lhs_str()
|
|
log[#log + 1] = 'L'
|
|
return 'a'
|
|
end
|
|
|
|
local function rhs_str()
|
|
log[#log + 1] = 'R'
|
|
return 'b'
|
|
end
|
|
|
|
log = {}
|
|
val = lhs_str()..rhs_str()
|
|
assert_eq(table.concat(log, ''), 'LR', 'order: ..')
|
|
|
|
-- relational
|
|
log = {}
|
|
val = (lhs_num() < rhs_num())
|
|
assert_eq(table.concat(log, ''), 'LR', 'order: <')
|
|
|
|
log = {}
|
|
val = (lhs_num() <= rhs_num())
|
|
assert_eq(table.concat(log, ''), 'LR', 'order: <=')
|
|
|
|
log = {}
|
|
val = (lhs_num() > rhs_num())
|
|
assert_eq(table.concat(log, ''), 'LR', 'order: >')
|
|
|
|
log = {}
|
|
val = (lhs_num() >= rhs_num())
|
|
assert_eq(table.concat(log, ''), 'LR', 'order: >=')
|
|
|
|
log = {}
|
|
val = (lhs_num() == rhs_num())
|
|
assert_eq(table.concat(log, ''), 'LR', 'order: ==')
|
|
|
|
log = {}
|
|
val = (lhs_num() ~= rhs_num())
|
|
assert_eq(table.concat(log, ''), 'LR', 'order: ~=')
|
|
|
|
-- bitwise
|
|
log = {}
|
|
val = lhs_num()&rhs_num()
|
|
assert_eq(table.concat(log, ''), 'LR', 'order: &')
|
|
|
|
log = {}
|
|
val = lhs_num()|rhs_num()
|
|
assert_eq(table.concat(log, ''), 'LR', 'order: |')
|
|
|
|
log = {}
|
|
val = lhs_num()~rhs_num()
|
|
assert_eq(table.concat(log, ''), 'LR', 'order: ~ (xor)')
|
|
|
|
log = {}
|
|
val = lhs_num()<<rhs_num()
|
|
assert_eq(table.concat(log, ''), 'LR', 'order: <<')
|
|
|
|
log = {}
|
|
val = lhs_num()>>rhs_num()
|
|
assert_eq(table.concat(log, ''), 'LR', 'order: >>')
|
|
|
|
-- 13.2. Nested expressions and associativity
|
|
local function exp_a()
|
|
log[#log + 1] = 'A'
|
|
return 2
|
|
end
|
|
|
|
local function exp_b()
|
|
log[#log + 1] = 'B'
|
|
return 3
|
|
end
|
|
|
|
local function exp_c()
|
|
log[#log + 1] = 'C'
|
|
return 2
|
|
end
|
|
|
|
log = {}
|
|
val = exp_a() ^ (exp_b() ^ exp_c())
|
|
assert_eq(val, 512, 'nested expression result')
|
|
assert_eq(table.concat(log, ''), 'ABC', 'nested expression associativity')
|
|
|
|
assert_eq(2^3^2, 512, 'power associativity')
|
|
|
|
-- 14. String-to-number coercion around zero
|
|
assert_eq(1/('0') == 1/0.0, true, 'str: ("0") (+Inf)')
|
|
assert_eq(1/('-0') == 1/0.0, true, 'str: ("-0") (+Inf)')
|
|
assert_eq(1/('0.0') == 1/0.0, true, 'str: ("0.0") (+Inf)')
|
|
assert_eq(1/('-0.0') == -1/0.0, true, 'str: ("-0.0") (-Inf)')
|
|
assert_eq(1/-('0') == 1/0.0, true, 'str: unary minus -("0") (+Inf)')
|
|
assert_eq(1/-('-0') == 1/0.0, true, 'str: unary minus -("-0") (+Inf)')
|
|
|
|
-- 14.1. General arithmetic with numeric strings
|
|
assert_eq('1'+2, 3, 'str-num: "1"+2 == 3')
|
|
assert_eq(' -2 ' * '3', -6, 'str-num: " -2 " * "3" == -6')
|
|
assert_eq('0x10' + 1, 17, 'str-num: hex int string + 1 == 17')
|
|
assert_eq('0x1p4' + 0, 16, 'str-num: hex float string + 0 == 16')
|
|
assertThrows("attempt to add a 'string' with a 'number'",
|
|
function()
|
|
return 'x1'+1
|
|
end
|
|
)
|
|
|
|
-- 14.2 Numeric string coercion preserves int/float kind
|
|
do
|
|
local function mt(x)
|
|
return math.type(x)
|
|
end
|
|
|
|
assert_eq(mt("0" + 0), "integer", "string '0' + 0 => integer")
|
|
assert_eq(mt("-0" + 0), "integer", "string '-0' + 0 => integer")
|
|
|
|
assert_eq(mt("0.0" + 0), "float", "string '0.0' + 0 => float")
|
|
assert_eq(mt("-0.0" + 0), "float", "string '-0.0' + 0 => float")
|
|
|
|
assert_eq(mt("0" + 0.0), "float", "string '0' + 0.0 => float")
|
|
assert_eq(mt("0.0" + 0.0), "float", "string '0.0' + 0.0 => float")
|
|
|
|
assert_eq(mt("0" + -0), "integer", "string '0' + -0 => integer")
|
|
assert_eq(mt("0" + -0.0), "float", "string '0' + -0.0 => float")
|
|
|
|
-- Regression coverage: numeric strings for all arithmetic ops and operand order
|
|
assert_eq(mt("0" - 0), "integer", "string '0' - 0 => integer")
|
|
assert_eq(mt("0.0" - 0), "float", "string '0.0' - 0 => float")
|
|
assert_eq(mt("0" * 1), "integer", "string '0' * 1 => integer")
|
|
assert_eq(mt("0.0" * 1), "float", "string '0.0' * 1 => float")
|
|
assert_eq(mt("0" / 1), "float", "string '0' / 1 => float")
|
|
assert_eq(mt("0.0" / 1), "float", "string '0.0' / 1 => float")
|
|
assert_eq(mt("0" // 1), "integer", "string '0' // 1 => integer")
|
|
assert_eq(mt("0.0" // 1), "float", "string '0.0' // 1 => float")
|
|
assert_eq(mt("0" % 1), "integer", "string '0' % 1 => integer")
|
|
assert_eq(mt("0.0" % 1), "float", "string '0.0' % 1 => float")
|
|
assert_eq(mt("2" ^ 1), "float", "string '2' ^ 1 => float")
|
|
assert_eq(mt("2.0" ^ 1), "float", "string '2.0' ^ 1 => float")
|
|
|
|
assert_eq(mt(0 + "0"), "integer", "0 + string '0' => integer")
|
|
assert_eq(mt(0 + "0.0"), "float", "0 + string '0.0' => float")
|
|
|
|
assert_eq(mt(0 - "0"), "integer", "0 - string '0' => integer")
|
|
assert_eq(mt(0 - "0.0"), "float", "0 - string '0.0' => float")
|
|
|
|
assert_eq(mt(1 * "0"), "integer", "1 * string '0' => integer")
|
|
assert_eq(mt(1 * "0.0"), "float", "1 * string '0.0' => float")
|
|
|
|
assert_eq(mt(0 / "1"), "float", "0 / string '1' => float")
|
|
assert_eq(mt(0 / "1.0"), "float", "0 / string '1.0' => float")
|
|
|
|
assert_eq(mt(0 // "1"), "integer", "0 // string '1' => integer")
|
|
assert_eq(mt(0 // "1.0"), "float", "0 // string '1.0' => float")
|
|
|
|
assert_eq(mt(0 % "1"), "integer", "0 % string '1' => integer")
|
|
assert_eq(mt(0 % "1.0"), "float", "0 % string '1.0' => float")
|
|
|
|
assert_eq(mt(2 ^ "1"), "float", "2 ^ string '1' => float")
|
|
assert_eq(mt(2 ^ "1.0"), "float", "2 ^ string '1.0' => float")
|
|
|
|
assert_eq((1 // "2"), 0, "1 // '2' == 0")
|
|
assert_eq((1 // "2.0"), 0.0, "1 // '2.0' == 0.0")
|
|
end
|
|
|
|
-- 15. Recursive function producing int zero (and unary minus)
|
|
local function rec_zero(n)
|
|
if n == 0 then
|
|
return 0
|
|
end
|
|
return -rec_zero(n - 1)
|
|
end
|
|
|
|
assert_eq(1/rec_zero(5) == 1/0, true, 'recursive: rec_zero (+Inf)')
|
|
assert_eq(1/(rec_zero(5)) == 1/0, true, 'recursive: (rec_zero) (+Inf)')
|
|
assert_eq(1/-(rec_zero(5)) == 1/0, true, 'recursive: -(rec_zero) (+Inf)')
|
|
assert_eq(1/-rec_zero(5) == 1/0, true, 'recursive: -(rec_zero) (+Inf)')
|
|
|
|
-- 16. Modulo and integer division by zero
|
|
|
|
-- 16.1. Modulo by zero
|
|
assertThrows("attempt to perform 'n%0'",
|
|
function()
|
|
return 1%0
|
|
end
|
|
)
|
|
|
|
val = pcall(
|
|
function()
|
|
return 1.0%0.0
|
|
end
|
|
)
|
|
assert_eq(val, true, 'float mod by zero ok (NaN)')
|
|
|
|
val = pcall(
|
|
function()
|
|
return 1.0%0
|
|
end
|
|
)
|
|
assert_eq(val, true, 'mixed (float,int) mod by zero ok (NaN)')
|
|
|
|
val = pcall(
|
|
function()
|
|
return 1%0.0
|
|
end
|
|
)
|
|
assert_eq(val, true, 'mixed (int,float) mod by zero ok (NaN)')
|
|
|
|
-- 16.2. Integer division by zero
|
|
assertThrows('divide by zero',
|
|
function()
|
|
return 1//0
|
|
end
|
|
)
|
|
|
|
val = pcall(
|
|
function()
|
|
return 1.0//0.0
|
|
end
|
|
)
|
|
assert_eq(val, true, 'float idiv by zero ok (+Inf/-Inf)')
|
|
|
|
val = pcall(
|
|
function()
|
|
return 1.0//0
|
|
end
|
|
)
|
|
assert_eq(val, true, 'mixed (float,int) idiv by zero ok (+Inf/-Inf)')
|
|
|
|
val = pcall(
|
|
function()
|
|
return 1//0.0
|
|
end
|
|
)
|
|
assert_eq(val, true, 'mixed (int,float) idiv by zero ok (+Inf/-Inf)')
|
|
|
|
-- 16.3. Modulo sign semantics (explicit)
|
|
assert_eq(5 % -2, -1, 'mod: 5 % -2 == -1')
|
|
assert_eq(-5 % 2, 1, 'mod: -5 % 2 == 1')
|
|
assert_eq(-5 % -2, -1, 'mod: -5 % -2 == -1')
|
|
|
|
-- 17. Metamethod precedence: __add should dispatch
|
|
local mt = {
|
|
__add = function(_, _)
|
|
return 'added'
|
|
end
|
|
}
|
|
local tbl = setmetatable({}, mt)
|
|
|
|
assert_eq(tbl + tbl, 'added', '__add dispatched')
|
|
|
|
-- 17.1 Unary metamethod: __unm (negation)
|
|
local mt_unm = {
|
|
__unm = function(_)
|
|
return 'negated'
|
|
end
|
|
}
|
|
local u = setmetatable({}, mt_unm)
|
|
|
|
assert_eq(-u, 'negated', '__unm dispatched')
|
|
|
|
-- 17.2 Unary metamethod: __bnot (bitwise NOT)
|
|
local bnot_calls = 0
|
|
local mt_bnot = {
|
|
__bnot = function(_)
|
|
bnot_calls = bnot_calls + 1
|
|
return 123
|
|
end
|
|
}
|
|
local b = setmetatable({}, mt_bnot)
|
|
|
|
assert_eq(~b, 123, '__bnot dispatched')
|
|
assert_eq(bnot_calls, 1, '__bnot called exactly once')
|
|
|
|
-- 17.3. Unary metamethods multi-return (first return only)
|
|
local mt_unm_mr = {
|
|
__unm = function(_)
|
|
return 7, 8
|
|
end
|
|
}
|
|
local um = setmetatable({}, mt_unm_mr)
|
|
|
|
assert_eq(-um, 7, '__unm uses first return value')
|
|
|
|
local mt_bnot_mr = {
|
|
__bnot = function(_)
|
|
return 9, 10
|
|
end
|
|
}
|
|
local bm = setmetatable({}, mt_bnot_mr)
|
|
|
|
assert_eq(~bm, 9, '__bnot uses first return value')
|
|
|
|
-- 18. Multi-return in arithmetic (first return only)
|
|
local function multi_ret()
|
|
return 0, 1
|
|
end
|
|
|
|
assert_eq(1/(multi_ret()) == 1/0, true, 'multi-ret: 1st used (+Inf)')
|
|
assert_eq(1/-(multi_ret()) == 1/0, true, 'multi-ret: unary minus 1st used (+Inf)')
|
|
|
|
-- 18.1 Exponentiation zero edge cases
|
|
assert_eq(0^0 == 1, true, 'pow: 0^0 == 1')
|
|
assert_eq((-0.0)^0 == 1, true, 'pow: (-0.0)^0 == 1')
|
|
|
|
-- 19. Error tests
|
|
assertThrows('has no integer representation',
|
|
function()
|
|
return ~0.5
|
|
end
|
|
)
|
|
|
|
assertThrows("attempt to add a 'string' with a 'number'",
|
|
function()
|
|
return 'a'+1
|
|
end
|
|
)
|
|
|
|
assertThrows('attempt to perform arithmetic on a table value',
|
|
function()
|
|
return -{}
|
|
end
|
|
)
|
|
|
|
-- 19.1. Bitwise on non-integers should error
|
|
assertThrows('has no integer representation',
|
|
function()
|
|
return 1.5&1
|
|
end
|
|
)
|
|
|
|
assertThrows(
|
|
"attempt to perform bitwise operation on a string value (constant '3')",
|
|
function()
|
|
return '3'|1
|
|
end
|
|
)
|
|
|
|
assertThrows('has no integer representation',
|
|
function()
|
|
return 1~1.2
|
|
end
|
|
)
|
|
|
|
assertThrows('has no integer representation',
|
|
function()
|
|
return 1<<0.1
|
|
end
|
|
)
|
|
|
|
-- 19.2. Relational type error
|
|
assertThrows('attempt to compare number with string',
|
|
function()
|
|
return 1<'1'
|
|
end
|
|
)
|
|
|
|
-- 19.3. Additional negative tests
|
|
assertThrows('attempt to perform arithmetic on a table value',
|
|
function()
|
|
return 1+{}
|
|
end
|
|
)
|
|
|
|
assertThrows("attempt to unm a 'string' with a 'string'",
|
|
function()
|
|
return -'x'
|
|
end
|
|
)
|
|
|
|
assertThrows(
|
|
"attempt to perform bitwise operation on a string value (constant '1')",
|
|
function()
|
|
return ~'1'
|
|
end
|
|
)
|
|
|
|
assertThrows('attempt to compare string with number',
|
|
function()
|
|
return '1'<1
|
|
end
|
|
)
|
|
|
|
assertThrows('attempt to compare number with table',
|
|
function()
|
|
return 1<{}
|
|
end
|
|
)
|
|
|
|
-- 19.4. String arithmetic: exact verb mapping
|
|
assertThrows("attempt to sub a 'string' with a 'number'",
|
|
function()
|
|
return 'x' - 1
|
|
end
|
|
)
|
|
|
|
assertThrows("attempt to mul a 'string' with a 'number'",
|
|
function()
|
|
return 'x' * 2
|
|
end
|
|
)
|
|
|
|
assertThrows("attempt to div a 'string' with a 'number'",
|
|
function()
|
|
return 'x' / 2
|
|
end
|
|
)
|
|
|
|
assertThrows("attempt to idiv a 'string' with a 'number'",
|
|
function()
|
|
return 'x' // 2
|
|
end
|
|
)
|
|
|
|
assertThrows("attempt to mod a 'string' with a 'number'",
|
|
function()
|
|
return 'x' % 2
|
|
end
|
|
)
|
|
|
|
assertThrows("attempt to pow a 'string' with a 'number'",
|
|
function()
|
|
return 'x' ^ 2
|
|
end
|
|
)
|
|
|
|
-- 19.4.1. String arithmetic: type pairing and string-vs-string cases
|
|
assertThrows("attempt to add a 'string' with a 'string'",
|
|
function()
|
|
return 'x' + 'y'
|
|
end
|
|
)
|
|
|
|
assertThrows("attempt to mul a 'number' with a 'string'",
|
|
function()
|
|
return 2 * 'x'
|
|
end
|
|
)
|
|
|
|
-- 19.5. Bitwise: exact type errors
|
|
assertThrows("attempt to perform bitwise operation on a table value",
|
|
function()
|
|
return {} & 1
|
|
end
|
|
)
|
|
|
|
assertThrows("attempt to perform bitwise operation on a nil value",
|
|
function()
|
|
return nil | 1
|
|
end
|
|
)
|
|
|
|
assertThrows("attempt to perform bitwise operation on a boolean value",
|
|
function()
|
|
return true ~ 1
|
|
end
|
|
)
|
|
|
|
-- 19.6. Bitwise shifts: non-integer RHS
|
|
assertThrows("number has no integer representation",
|
|
function()
|
|
return 1 << 0.5
|
|
end
|
|
)
|
|
|
|
assertThrows("number has no integer representation",
|
|
function()
|
|
return 8 >> 0.25
|
|
end
|
|
)
|
|
|
|
-- 19.7. Concatenation: exact messages
|
|
assertThrows("attempt to concatenate a nil value",
|
|
function()
|
|
return nil .. "x"
|
|
end
|
|
)
|
|
|
|
assertThrows("attempt to concatenate a nil value",
|
|
function()
|
|
return "x" .. nil
|
|
end
|
|
)
|
|
|
|
assertThrows("attempt to concatenate a table value",
|
|
function()
|
|
return "x" .. {}
|
|
end
|
|
)
|
|
|
|
-- 19.8. Length operator: exact message
|
|
assertThrows("attempt to get length of a number value",
|
|
function()
|
|
return #1
|
|
end
|
|
)
|
|
|
|
assertThrows("attempt to get length of a nil value",
|
|
function()
|
|
return #nil
|
|
end
|
|
)
|
|
|
|
-- 19.9. Relational mismatches
|
|
assertThrows("attempt to compare number with string",
|
|
function()
|
|
return 1 <= '1'
|
|
end
|
|
)
|
|
|
|
assertThrows("attempt to compare number with string",
|
|
function()
|
|
return '1' >= 1
|
|
end
|
|
)
|
|
|
|
assertThrows("attempt to compare table with number",
|
|
function()
|
|
return 1 > {}
|
|
end
|
|
)
|
|
|
|
assertThrows("attempt to compare table with number",
|
|
function()
|
|
return {} < 1
|
|
end
|
|
)
|
|
|
|
-- 19.10. Explicit int-path division/modulo by zero through expr
|
|
assertThrows("attempt to perform 'n%0'",
|
|
function()
|
|
return 1 % (1-1)
|
|
end
|
|
)
|
|
|
|
assertThrows("attempt to divide by zero",
|
|
function()
|
|
return 1 // (1-1)
|
|
end
|
|
)
|
|
|
|
-- 20. `tostring()` numeric formatting
|
|
|
|
-- 20.1. Integers
|
|
assert_eq(tostring(0), '0', 'tostring: 0')
|
|
assert_eq(tostring(1), '1', 'tostring: 1')
|
|
assert_eq(tostring(-5), '-5', 'tostring: -5')
|
|
assert_eq(tostring(42), '42', 'tostring: 42')
|
|
assert_eq(tostring(-123), '-123', 'tostring: -123')
|
|
|
|
-- 20.2. Float zeros (positive and negative)
|
|
assert_eq(tostring(0.0), '0.0', 'tostring: 0.0')
|
|
assert_eq(tostring(-0.0), '-0.0', 'tostring: -0.0')
|
|
|
|
-- 20.3. Integer-valued floats
|
|
assert_eq(tostring(1.0), '1.0', 'tostring: 1.0')
|
|
assert_eq(tostring(2.0), '2.0', 'tostring: 2.0')
|
|
assert_eq(tostring(-3.0), '-3.0', 'tostring: -3.0')
|
|
assert_eq(tostring(100.0), '100.0', 'tostring: 100.0')
|
|
|
|
-- 20.4. Non-integer floats
|
|
assert_eq(tostring(0.5), '0.5', 'tostring: 0.5')
|
|
assert_eq(tostring(3.14), '3.14', 'tostring: 3.14')
|
|
assert_eq(tostring(-2.5), '-2.5', 'tostring: -2.5')
|
|
|
|
-- 20.5. Special float values
|
|
assert_eq(tostring(1/0.0), 'inf', 'tostring: +inf')
|
|
assert_eq(tostring(-1/0.0), '-inf', 'tostring: -inf')
|
|
assert_eq(tostring(0.0/0.0), '-nan', 'tostring: NaN')
|
|
assert_eq(tostring(1.0%0.0), '-nan', 'tostring: NaN from modulo')
|
|
assert_eq(tostring((-1.0)%0.0), '-nan', 'tostring: NaN from neg modulo')
|
|
|
|
-- 20.6. Results from tonumber() preserve type
|
|
assert_eq(tostring(tonumber('5')), '5', 'tonumber int: "5"')
|
|
assert_eq(tostring(tonumber('5.')), '5.0', 'tonumber float: "5."')
|
|
assert_eq(tostring(tonumber('.5')), '0.5', 'tonumber float: ".5"')
|
|
assert_eq(tostring(tonumber('5.0')), '5.0', 'tonumber float: "5.0"')
|
|
assert_eq(tostring(tonumber('0x10')), '16', 'tonumber hex int: "0x10"')
|
|
assert_eq(tostring(tonumber('0x10.0')), '16.0', 'tonumber hex float: "0x10.0"')
|
|
assert_eq(tostring(tonumber('0x1p4')), '16.0', 'tonumber hex float: "0x1p4"')
|
|
|
|
-- 20.7. Arithmetic results formatting
|
|
assert_eq(tostring(1+1), '2', 'int+int yields int')
|
|
assert_eq(tostring(1.0+1.0), '2.0', 'float+float yields float')
|
|
assert_eq(tostring(1+1.0), '2.0', 'int+float yields float')
|
|
assert_eq(tostring(5/2), '2.5', 'division yields float')
|
|
assert_eq(tostring(4/2), '2.0', 'division exact yields float')
|
|
|
|
-- 21. Numeric for loops: comprehensive type/mode coverage
|
|
|
|
-- 21.1. Integer mode (all params integer-valued and untagged)
|
|
|
|
do
|
|
local types, vals = {}, {}
|
|
for i = 1, 3 do
|
|
table.insert(types, math.type(i))
|
|
table.insert(vals, tostring(i))
|
|
end
|
|
assert_eq(table.concat(types, ','), 'integer,integer,integer', 'for 1,3: types')
|
|
assert_eq(table.concat(vals, ','), '1,2,3', 'for 1,3: values')
|
|
end
|
|
|
|
do
|
|
local types = {}
|
|
for i = 1, 3, 1 do
|
|
table.insert(types, math.type(i))
|
|
end
|
|
assert_eq(types[1], 'integer', 'for 1,3,1: type')
|
|
end
|
|
|
|
do
|
|
local vals = {}
|
|
for i = 3, 1, -1 do
|
|
table.insert(vals, tostring(i))
|
|
end
|
|
assert_eq(table.concat(vals, ','), '3,2,1', 'for 3,1,-1: descending')
|
|
end
|
|
|
|
do
|
|
local found_zero = false
|
|
local zero_str
|
|
for i = -1, 1 do
|
|
if i == 0 then
|
|
found_zero = true
|
|
zero_str = tostring(i)
|
|
end
|
|
end
|
|
assert_eq(found_zero, true, 'for -1,1: crosses zero')
|
|
assert_eq(zero_str, '0', 'for -1,1: zero is int')
|
|
end
|
|
|
|
do
|
|
for i = 1, 3 do
|
|
local zero = i - i
|
|
assert_eq(math.type(zero), 'integer', 'for 1,3: i-i is int')
|
|
assert_eq(tostring(zero), '0', 'for 1,3: i-i formats as int')
|
|
end
|
|
end
|
|
|
|
do
|
|
for i = 2, 4 do
|
|
local expr = i * 2 - i - i
|
|
assert_eq(math.type(expr), 'integer', 'for 2,4: int expr is int')
|
|
assert_eq(expr, 0, 'for 2,4: int expr value')
|
|
end
|
|
end
|
|
|
|
-- 21.2. Float mode (all params float-typed)
|
|
|
|
do
|
|
local types, vals = {}, {}
|
|
for i = 1.0, 3.0, 1.0 do
|
|
table.insert(types, math.type(i))
|
|
table.insert(vals, tostring(i))
|
|
end
|
|
assert_eq(table.concat(types, ','), 'float,float,float', 'for 1.0,3.0,1.0: types')
|
|
assert_eq(table.concat(vals, ','), '1.0,2.0,3.0', 'for 1.0,3.0,1.0: values')
|
|
end
|
|
|
|
do
|
|
local vals = {}
|
|
for i = 0.5, 2.5, 0.5 do
|
|
table.insert(vals, tostring(i))
|
|
end
|
|
assert_eq(table.concat(vals, ','), '0.5,1.0,1.5,2.0,2.5', 'for 0.5,2.5,0.5: values')
|
|
end
|
|
|
|
do
|
|
local vals = {}
|
|
for i = 3.0, 1.0, -1.0 do
|
|
table.insert(vals, tostring(i))
|
|
end
|
|
assert_eq(table.concat(vals, ','), '3.0,2.0,1.0', 'for 3.0,1.0,-1.0: descending')
|
|
end
|
|
|
|
do
|
|
local vals = {}
|
|
for i = 2.5, 0.5, -0.5 do
|
|
table.insert(vals, tostring(i))
|
|
end
|
|
assert_eq(table.concat(vals, ','), '2.5,2.0,1.5,1.0,0.5', 'for 2.5,0.5,-0.5: values')
|
|
end
|
|
|
|
do
|
|
local zero_str
|
|
for i = -1.0, 1.0 do
|
|
if i == 0 then
|
|
zero_str = tostring(i)
|
|
end
|
|
end
|
|
assert_eq(zero_str, '0.0', 'for -1.0,1.0: zero is float')
|
|
end
|
|
|
|
do
|
|
for i = 1.0, 3.0 do
|
|
local zero = i - i
|
|
assert_eq(math.type(zero), 'float', 'for 1.0,3.0: i-i is float')
|
|
assert_eq(tostring(zero), '0.0', 'for 1.0,3.0: i-i formats as float')
|
|
end
|
|
end
|
|
|
|
do
|
|
for i = 2.0, 4.0 do
|
|
local expr = i * 2.0 - i - i
|
|
assert_eq(math.type(expr), 'float', 'for 2.0,4.0: float expr is float')
|
|
assert_eq(tostring(expr), '0.0', 'for 2.0,4.0: float expr formats')
|
|
end
|
|
end
|
|
|
|
-- 21.3. Mixed mode (start type determines loop var type)
|
|
|
|
do
|
|
local types, vals = {}, {}
|
|
for i = 1, 3.5 do
|
|
table.insert(types, math.type(i))
|
|
table.insert(vals, tostring(i))
|
|
end
|
|
assert_eq(types[1], 'integer', 'for 1,3.5: start int yields var int')
|
|
assert_eq(types[2], 'integer', 'for 1,3.5: var stays int')
|
|
assert_eq(table.concat(vals, ','), '1,2,3', 'for 1,3.5: int formatting')
|
|
end
|
|
|
|
do
|
|
local types, vals = {}, {}
|
|
for i = 1.0, 3 do
|
|
table.insert(types, math.type(i))
|
|
table.insert(vals, tostring(i))
|
|
end
|
|
assert_eq(types[1], 'float', 'for 1.0,3: start float yields var float')
|
|
assert_eq(types[2], 'float', 'for 1.0,3: var stays float')
|
|
assert_eq(table.concat(vals, ','), '1.0,2.0,3.0', 'for 1.0,3: float formatting')
|
|
end
|
|
|
|
do
|
|
local types = {}
|
|
for i = 1, 3.0 do
|
|
table.insert(types, math.type(i))
|
|
end
|
|
assert_eq(types[1], 'integer', 'for 1,3.0: end=3.0 but start int yields var int')
|
|
end
|
|
|
|
do
|
|
local types, vals = {}, {}
|
|
for i = 1, 3, 1.0 do
|
|
table.insert(types, math.type(i))
|
|
table.insert(vals, tostring(i))
|
|
end
|
|
assert_eq(types[1], 'float', 'for 1,3,1.0: step float yields var float')
|
|
assert_eq(table.concat(vals, ','), '1.0,2.0,3.0', 'for 1,3,1.0: float formatting')
|
|
end
|
|
|
|
do
|
|
local types, vals = {}, {}
|
|
for i = 1.0, 3.0, 1 do
|
|
table.insert(types, math.type(i))
|
|
table.insert(vals, tostring(i))
|
|
end
|
|
assert_eq(types[1], 'float', 'for 1.0,3.0,1: start float yields var float')
|
|
assert_eq(table.concat(vals, ','), '1.0,2.0,3.0', 'for 1.0,3.0,1: float formatting')
|
|
end
|
|
|
|
do
|
|
local types = {}
|
|
for i = 3.0, 1, -1 do
|
|
table.insert(types, math.type(i))
|
|
end
|
|
assert_eq(types[1], 'float', 'for 3.0,1,-1: start float yields var float')
|
|
end
|
|
|
|
-- Mixed mode: float step, integer start
|
|
do
|
|
local types, vals = {}, {}
|
|
for i = 1, 3, 1.0 do
|
|
table.insert(types, math.type(i))
|
|
table.insert(vals, tostring(i))
|
|
end
|
|
assert_eq(types[1], 'float', 'for 1,3,1.0: step float yields var float')
|
|
assert_eq(table.concat(vals, ','), '1.0,2.0,3.0', 'for 1,3,1.0: float formatting')
|
|
end
|
|
|
|
-- Mixed mode: integer start, negative float step
|
|
do
|
|
local types, vals = {}, {}
|
|
for i = 3, 1, -1.0 do
|
|
table.insert(types, math.type(i))
|
|
table.insert(vals, tostring(i))
|
|
end
|
|
assert_eq(types[1], 'float', 'for 3,1,-1.0: step float yields var float')
|
|
assert_eq(table.concat(vals, ','), '3.0,2.0,1.0', 'for 3,1,-1.0: float formatting')
|
|
end
|
|
|
|
do
|
|
for i = 1, 3.5 do
|
|
local zero = i - i
|
|
assert_eq(math.type(zero), 'integer', 'for 1,3.5: start int yields i-i is int')
|
|
end
|
|
end
|
|
|
|
do
|
|
for i = 1.0, 3 do
|
|
local zero = i - i
|
|
assert_eq(math.type(zero), 'float', 'for 1.0,3: start float yields i-i is float')
|
|
end
|
|
end
|
|
|
|
-- 21.4. Boundary conditions
|
|
|
|
do
|
|
local count = 0
|
|
for i = 5, 5 do
|
|
count = count + 1
|
|
assert_eq(i, 5, 'for 5,5: i=5')
|
|
end
|
|
assert_eq(count, 1, 'for 5,5: one iteration')
|
|
end
|
|
|
|
do
|
|
local count = 0
|
|
for i = 5, 3 do
|
|
count = count + 1
|
|
end
|
|
assert_eq(count, 0, 'for 5,3: no iterations')
|
|
end
|
|
|
|
do
|
|
local count = 0
|
|
for i = 3, 5, -1 do
|
|
count = count + 1
|
|
end
|
|
assert_eq(count, 0, 'for 3,5,-1: no iterations')
|
|
end
|
|
|
|
do
|
|
local last
|
|
for i = 1.0, 3.0 do
|
|
last = i
|
|
end
|
|
assert_eq(tostring(last), '3.0', 'for 1.0,3.0: exactly reaches end')
|
|
end
|
|
|
|
do
|
|
local last
|
|
for i = 1, 10, 3 do
|
|
last = i
|
|
end
|
|
assert_eq(last, 10, 'for 1,10,3: reaches end exactly')
|
|
end
|
|
|
|
do
|
|
local last
|
|
for i = 1, 9, 3 do
|
|
last = i
|
|
end
|
|
assert_eq(last, 7, 'for 1,9,3: stops before end')
|
|
end
|
|
|
|
do
|
|
local vals = {}
|
|
for i = 0.1, 0.3, 0.1 do
|
|
table.insert(vals, i)
|
|
end
|
|
assert_eq(#vals, 2, 'for 0.1,0.3,0.1: small float step iterations')
|
|
end
|
|
|
|
do
|
|
local vals = {}
|
|
-- 0.5 has exact binary representation
|
|
for i = 0.5, 1.5, 0.5 do
|
|
table.insert(vals, i)
|
|
end
|
|
assert_eq(#vals, 3, 'for 0.5,1.5,0.5: small float step')
|
|
end
|
|
|
|
-- 21.5. Edge cases and errors
|
|
|
|
assertThrows('step is zero', function()
|
|
for i = 1, 10, 0 do
|
|
end
|
|
end)
|
|
|
|
assertThrows('step is zero', function()
|
|
for i = 1.0, 10.0, 0.0 do
|
|
end
|
|
end)
|
|
|
|
do
|
|
local count = 0
|
|
for i = 1, 1000000 do
|
|
count = count + 1
|
|
if count > 5 then break end
|
|
end
|
|
assert_eq(count, 6, 'for 1,1000000: large range with break')
|
|
end
|
|
|
|
do
|
|
local sum = 0
|
|
for i = 1, 5, 2 do
|
|
sum = sum + i
|
|
end
|
|
assert_eq(sum, 9, 'for 1,5,2: step>1 sum (1+3+5)')
|
|
end
|
|
|
|
do
|
|
local sum = 0
|
|
for i = 10, 1, -3 do
|
|
sum = sum + i
|
|
end
|
|
assert_eq(sum, 22, 'for 10,1,-3: negative step>1 sum (10+7+4+1)')
|
|
end
|
|
|
|
-- 22. Table numeric key equivalence
|
|
|
|
-- 22.1. Integer-valued floats normalize to integers
|
|
|
|
do
|
|
local t = {}
|
|
t[1] = 'one'
|
|
assert_eq(t[1.0], 'one', 'table: t[1] accessed via t[1.0]')
|
|
assert_eq(t[1], 'one', 'table: t[1] accessed via t[1]')
|
|
end
|
|
|
|
do
|
|
local t = {}
|
|
t[2.0] = 'two'
|
|
assert_eq(t[2], 'two', 'table: t[2.0] accessed via t[2]')
|
|
assert_eq(t[2.0], 'two', 'table: t[2.0] accessed via t[2.0]')
|
|
end
|
|
|
|
do
|
|
local t = {}
|
|
t[1] = 'a'
|
|
t[2.0] = 'b'
|
|
t[3] = 'c'
|
|
assert_eq(t[1.0], 'a', 'table: multi-key t[1.0]')
|
|
assert_eq(t[2], 'b', 'table: multi-key t[2]')
|
|
assert_eq(t[3.0], 'c', 'table: multi-key t[3.0]')
|
|
end
|
|
|
|
do
|
|
local t = {}
|
|
t[5] = 'first'
|
|
t[5.0] = 'second'
|
|
assert_eq(t[5], 'second', 'table: overwrite int with float')
|
|
assert_eq(t[5.0], 'second', 'table: overwrite int with float (access)')
|
|
end
|
|
|
|
do
|
|
local t = {}
|
|
t[100] = 'hundred'
|
|
assert_eq(t[100.0], 'hundred', 'table: large int t[100.0]')
|
|
end
|
|
|
|
-- 22.2. Zero normalization (both -0 and +0 map to same key)
|
|
|
|
do
|
|
local t = {}
|
|
t[0] = 'zero'
|
|
assert_eq(t[-0.0], 'zero', 'table: t[0] accessed via t[-0.0]')
|
|
assert_eq(t[0.0], 'zero', 'table: t[0] accessed via t[0.0]')
|
|
assert_eq(t[0], 'zero', 'table: t[0] accessed via t[0]')
|
|
end
|
|
|
|
do
|
|
local t = {}
|
|
t[-0.0] = 'negzero'
|
|
assert_eq(t[0], 'negzero', 'table: t[-0.0] accessed via t[0]')
|
|
assert_eq(t[0.0], 'negzero', 'table: t[-0.0] accessed via t[0.0]')
|
|
assert_eq(t[-0.0], 'negzero', 'table: t[-0.0] accessed via t[-0.0]')
|
|
end
|
|
|
|
do
|
|
local t = {}
|
|
t[0.0] = 'float_zero'
|
|
assert_eq(t[0], 'float_zero', 'table: t[0.0] accessed via t[0]')
|
|
assert_eq(t[-0.0], 'float_zero', 'table: t[0.0] accessed via t[-0.0]')
|
|
end
|
|
|
|
do
|
|
local t = {}
|
|
t[0] = 'first'
|
|
t[-0.0] = 'second'
|
|
t[0.0] = 'third'
|
|
assert_eq(t[0], 'third', 'table: zero overwrite final')
|
|
assert_eq(t[-0.0], 'third', 'table: zero overwrite via -0.0')
|
|
end
|
|
|
|
-- 22.3. Non-integer floats are distinct keys
|
|
|
|
do
|
|
local t = {}
|
|
t[1] = 'int_one'
|
|
t[1.5] = 'one_point_five'
|
|
assert_eq(t[1], 'int_one', 'table: non-int float t[1]')
|
|
assert_eq(t[1.5], 'one_point_five', 'table: non-int float t[1.5]')
|
|
assert_eq(t[1.0], 'int_one', 'table: non-int float t[1.0] maps to int')
|
|
end
|
|
|
|
do
|
|
local t = {}
|
|
t[0.5] = 'half'
|
|
t[1.5] = 'one_half'
|
|
t[2.5] = 'two_half'
|
|
assert_eq(t[0.5], 'half', 'table: multi non-int t[0.5]')
|
|
assert_eq(t[1.5], 'one_half', 'table: multi non-int t[1.5]')
|
|
assert_eq(t[2.5], 'two_half', 'table: multi non-int t[2.5]')
|
|
end
|
|
|
|
-- 22.4. Mixed integer and float keys
|
|
|
|
do
|
|
local t = {}
|
|
t[0] = 'zero'
|
|
t[1] = 'one'
|
|
t[1.5] = 'one_point_five'
|
|
t[2.0] = 'two'
|
|
t[-0.0] = 'neg_zero'
|
|
|
|
assert_eq(t[0], 'neg_zero', 'table: mixed t[0] (last zero)')
|
|
assert_eq(t[0.0], 'neg_zero', 'table: mixed t[0.0]')
|
|
assert_eq(t[-0.0], 'neg_zero', 'table: mixed t[-0.0]')
|
|
assert_eq(t[1], 'one', 'table: mixed t[1]')
|
|
assert_eq(t[1.0], 'one', 'table: mixed t[1.0]')
|
|
assert_eq(t[1.5], 'one_point_five', 'table: mixed t[1.5]')
|
|
assert_eq(t[2], 'two', 'table: mixed t[2]')
|
|
assert_eq(t[2.0], 'two', 'table: mixed t[2.0]')
|
|
end
|
|
|
|
-- 22.5. Key equivalence with expressions
|
|
|
|
do
|
|
local t = {}
|
|
t[1+1] = 'two'
|
|
assert_eq(t[2.0], 'two', 'table: expr key t[1+1] via t[2.0]')
|
|
assert_eq(t[4/2], 'two', 'table: expr key t[4/2]')
|
|
end
|
|
|
|
do
|
|
local t = {}
|
|
t[1-1] = 'int_zero'
|
|
assert_eq(t[0], 'int_zero', 'table: expr t[1-1] via t[0]')
|
|
assert_eq(t[0.0], 'int_zero', 'table: expr t[1-1] via t[0.0]')
|
|
assert_eq(t[-0.0], 'int_zero', 'table: expr t[1-1] via t[-0.0]')
|
|
end
|
|
|
|
do
|
|
local t = {}
|
|
t[1.0-1.0] = 'float_zero'
|
|
assert_eq(t[0], 'float_zero', 'table: expr t[1.0-1.0] via t[0]')
|
|
assert_eq(t[-0.0], 'float_zero', 'table: expr t[1.0-1.0] via t[-0.0]')
|
|
end
|
|
|
|
-- 22.6. Key equivalence with variables
|
|
|
|
do
|
|
local t = {}
|
|
local i = 1
|
|
local f = 1.0
|
|
t[i] = 'from_int'
|
|
assert_eq(t[f], 'from_int', 'table: var int key via float var')
|
|
end
|
|
|
|
do
|
|
local t = {}
|
|
local i = 1
|
|
local f = 1.0
|
|
t[f] = 'from_float'
|
|
assert_eq(t[i], 'from_float', 'table: var float key via int var')
|
|
end
|
|
|
|
do
|
|
local t = {}
|
|
local zi = 0
|
|
local zf = 0.0
|
|
local zfn = -0.0
|
|
t[zi] = 'int_zero'
|
|
assert_eq(t[zf], 'int_zero', 'table: var zi via zf')
|
|
assert_eq(t[zfn], 'int_zero', 'table: var zi via zfn')
|
|
end
|
|
|
|
-- 22.7. Key equivalence in array part
|
|
|
|
do
|
|
local t = {10, 20, 30}
|
|
assert_eq(t[1.0], 10, 'table: array t[1.0]')
|
|
assert_eq(t[2.0], 20, 'table: array t[2.0]')
|
|
assert_eq(t[3.0], 30, 'table: array t[3.0]')
|
|
end
|
|
|
|
do
|
|
local t = {}
|
|
t[1.0] = 'first'
|
|
t[2.0] = 'second'
|
|
assert_eq(t[1], 'first', 'table: array assign t[1.0] via t[1]')
|
|
assert_eq(t[2], 'second', 'table: array assign t[2.0] via t[2]')
|
|
end
|
|
|
|
-- 22.8. Key counting and iteration
|
|
|
|
do
|
|
local t = {}
|
|
t[1] = 'a'
|
|
t[1.0] = 'b'
|
|
t[2] = 'c'
|
|
t[2.0] = 'd'
|
|
|
|
local count = 0
|
|
for k, v in pairs(t) do
|
|
count = count + 1
|
|
end
|
|
assert_eq(count, 2, 'table: normalized keys count as one')
|
|
end
|
|
|
|
do
|
|
local t = {}
|
|
t[0] = 'a'
|
|
t[0.0] = 'b'
|
|
t[-0.0] = 'c'
|
|
|
|
local count = 0
|
|
for k, v in pairs(t) do
|
|
count = count + 1
|
|
end
|
|
assert_eq(count, 1, 'table: all zeros count as one key')
|
|
end
|
|
|
|
-- 23. Type introspection: type() and math.type()
|
|
|
|
-- 23.1. Basic type() function (returns general Lua types)
|
|
|
|
assert_eq(type(nil), 'nil', 'type(nil)')
|
|
assert_eq(type(true), 'boolean', 'type(true)')
|
|
assert_eq(type(false), 'boolean', 'type(false)')
|
|
assert_eq(type(0), 'number', 'type(0) is number')
|
|
assert_eq(type(1), 'number', 'type(1) is number')
|
|
assert_eq(type(0.0), 'number', 'type(0.0) is number')
|
|
assert_eq(type(1.0), 'number', 'type(1.0) is number')
|
|
assert_eq(type(-0.0), 'number', 'type(-0.0) is number')
|
|
assert_eq(type(1.5), 'number', 'type(1.5) is number')
|
|
assert_eq(type(1/0.0), 'number', 'type(inf) is number')
|
|
assert_eq(type(0.0/0.0), 'number', 'type(nan) is number')
|
|
assert_eq(type('hello'), 'string', 'type(string)')
|
|
assert_eq(type({}), 'table', 'type(table)')
|
|
assert_eq(type(function() end), 'function', 'type(function)')
|
|
|
|
-- 23.2. math.type() function (distinguishes integer vs float)
|
|
|
|
-- Integers
|
|
assert_eq(math.type(0), 'integer', 'math.type(0)')
|
|
assert_eq(math.type(1), 'integer', 'math.type(1)')
|
|
assert_eq(math.type(-1), 'integer', 'math.type(-1)')
|
|
assert_eq(math.type(42), 'integer', 'math.type(42)')
|
|
assert_eq(math.type(-123), 'integer', 'math.type(-123)')
|
|
assert_eq(math.type(1000000), 'integer', 'math.type(1000000)')
|
|
|
|
-- Floats (literals with decimal point)
|
|
assert_eq(math.type(0.0), 'float', 'math.type(0.0)')
|
|
assert_eq(math.type(-0.0), 'float', 'math.type(-0.0)')
|
|
assert_eq(math.type(1.0), 'float', 'math.type(1.0)')
|
|
assert_eq(math.type(2.0), 'float', 'math.type(2.0)')
|
|
assert_eq(math.type(-3.0), 'float', 'math.type(-3.0)')
|
|
assert_eq(math.type(100.0), 'float', 'math.type(100.0)')
|
|
|
|
-- Floats (non-integer values)
|
|
assert_eq(math.type(0.5), 'float', 'math.type(0.5)')
|
|
assert_eq(math.type(1.5), 'float', 'math.type(1.5)')
|
|
assert_eq(math.type(3.14), 'float', 'math.type(3.14)')
|
|
assert_eq(math.type(-2.5), 'float', 'math.type(-2.5)')
|
|
|
|
-- Special float values
|
|
assert_eq(math.type(1/0.0), 'float', 'math.type(inf)')
|
|
assert_eq(math.type(-1/0.0), 'float', 'math.type(-inf)')
|
|
assert_eq(math.type(0.0/0.0), 'float', 'math.type(nan)')
|
|
|
|
-- Non-numbers return nil
|
|
assert_eq(math.type(nil), nil, 'math.type(nil) yields nil')
|
|
assert_eq(math.type(true), nil, 'math.type(boolean) yields nil')
|
|
assert_eq(math.type('123'), nil, 'math.type(string) yields nil')
|
|
assert_eq(math.type({}), nil, 'math.type(table) yields nil')
|
|
assert_eq(math.type(function() end), nil, 'math.type(function) yields nil')
|
|
|
|
-- 23.3. Integer arithmetic preserves integer type
|
|
|
|
do
|
|
local a = 1
|
|
local b = 2
|
|
assert_eq(math.type(a + b), 'integer', 'int + int yields integer')
|
|
assert_eq(math.type(a - b), 'integer', 'int - int yields integer')
|
|
assert_eq(math.type(a * b), 'integer', 'int * int yields integer')
|
|
assert_eq(math.type(a // b), 'integer', 'int // int yields integer')
|
|
assert_eq(math.type(a % b), 'integer', 'int % int yields integer')
|
|
end
|
|
|
|
-- Integer operations producing zero
|
|
do
|
|
assert_eq(math.type(1 - 1), 'integer', '1 - 1 yields integer zero')
|
|
assert_eq(math.type(0 * 5), 'integer', '0 * 5 yields integer zero')
|
|
assert_eq(math.type(0 % 1), 'integer', '0 % 1 yields integer zero')
|
|
assert_eq(math.type(0 // 1), 'integer', '0 // 1 yields integer zero')
|
|
end
|
|
|
|
-- 23.4. Float arithmetic preserves float type
|
|
|
|
do
|
|
local a = 1.0
|
|
local b = 2.0
|
|
assert_eq(math.type(a + b), 'float', 'float + float yields float')
|
|
assert_eq(math.type(a - b), 'float', 'float - float yields float')
|
|
assert_eq(math.type(a * b), 'float', 'float * float yields float')
|
|
assert_eq(math.type(a / b), 'float', 'float / float yields float')
|
|
assert_eq(math.type(a // b), 'float', 'float // float yields float')
|
|
assert_eq(math.type(a % b), 'float', 'float % float yields float')
|
|
end
|
|
|
|
-- Float operations producing zero
|
|
do
|
|
assert_eq(math.type(1.0 - 1.0), 'float', '1.0 - 1.0 yields float zero')
|
|
assert_eq(math.type(0.0 * 5.0), 'float', '0.0 * 5.0 yields float zero')
|
|
assert_eq(math.type(0.0 % 1.0), 'float', '0.0 % 1.0 yields float zero')
|
|
assert_eq(math.type(0.0 // 1.0), 'float', '0.0 // 1.0 yields float zero')
|
|
end
|
|
|
|
-- 23.5. Mixed arithmetic promotes to float
|
|
|
|
do
|
|
assert_eq(math.type(1 + 1.0), 'float', 'int + float yields float')
|
|
assert_eq(math.type(1.0 + 1), 'float', 'float + int yields float')
|
|
assert_eq(math.type(2 * 1.5), 'float', 'int * float yields float')
|
|
assert_eq(math.type(3.0 - 1), 'float', 'float - int yields float')
|
|
assert_eq(math.type(5 // 2.0), 'float', 'int // float yields float')
|
|
assert_eq(math.type(5.0 % 2), 'float', 'float % int yields float')
|
|
end
|
|
|
|
-- Mixed operations producing zero
|
|
do
|
|
assert_eq(math.type(1 - 1.0), 'float', '1 - 1.0 yields float zero')
|
|
assert_eq(math.type(0 * 1.0), 'float', '0 * 1.0 yields float zero')
|
|
assert_eq(math.type(0.0 * 1), 'float', '0.0 * 1 yields float zero')
|
|
end
|
|
|
|
-- 23.6. Division and power always produce floats
|
|
|
|
do
|
|
assert_eq(math.type(4 / 2), 'float', '4 / 2 yields float (2.0)')
|
|
assert_eq(math.type(5 / 2), 'float', '5 / 2 yields float (2.5)')
|
|
assert_eq(math.type(1 / 1), 'float', '1 / 1 yields float (1.0)')
|
|
|
|
assert_eq(math.type(2 ^ 3), 'float', '2 ^ 3 yields float (8.0)')
|
|
assert_eq(math.type(2 ^ 0), 'float', '2 ^ 0 yields float (1.0)')
|
|
assert_eq(math.type(10 ^ 2), 'float', '10 ^ 2 yields float (100.0)')
|
|
end
|
|
|
|
-- Division producing zero
|
|
do
|
|
assert_eq(math.type(0 / 1), 'float', '0 / 1 yields float zero')
|
|
assert_eq(math.type(0.0 / 1.0), 'float', '0.0 / 1.0 yields float zero')
|
|
end
|
|
|
|
-- 23.7. Unary minus preserves type
|
|
|
|
do
|
|
assert_eq(math.type(-5), 'integer', '-5 is integer')
|
|
assert_eq(math.type(-0), 'integer', '-0 is integer')
|
|
|
|
assert_eq(math.type(-5.0), 'float', '-5.0 is float')
|
|
assert_eq(math.type(-0.0), 'float', '-0.0 is float')
|
|
end
|
|
|
|
-- Unary minus on variables
|
|
do
|
|
local i = 5
|
|
local f = 5.0
|
|
assert_eq(math.type(-i), 'integer', 'unary minus int var')
|
|
assert_eq(math.type(-f), 'float', 'unary minus float var')
|
|
end
|
|
|
|
-- Unary minus on expressions
|
|
do
|
|
assert_eq(math.type(-(1 + 1)), 'integer', 'unary minus int expr')
|
|
assert_eq(math.type(-(1.0 + 1.0)), 'float', 'unary minus float expr')
|
|
end
|
|
|
|
-- 23.8. String coercion preserves type (tonumber)
|
|
|
|
do
|
|
assert_eq(math.type(tonumber('5')), 'integer', 'tonumber("5") yields integer')
|
|
assert_eq(math.type(tonumber('5.')), 'float', 'tonumber("5.") yields float')
|
|
assert_eq(math.type(tonumber('.5')), 'float', 'tonumber(".5") yields float')
|
|
assert_eq(math.type(tonumber('5.0')), 'float', 'tonumber("5.0") yields float')
|
|
assert_eq(math.type(tonumber('-0')), 'integer', 'tonumber("-0") yields integer')
|
|
assert_eq(math.type(tonumber('-0.0')), 'float', 'tonumber("-0.0") yields float')
|
|
end
|
|
|
|
-- Hexadecimal literals
|
|
do
|
|
assert_eq(math.type(tonumber('0x10')), 'integer', 'tonumber("0x10") yields integer')
|
|
assert_eq(math.type(tonumber('0x10.0')), 'float', 'tonumber("0x10.0") yields float')
|
|
assert_eq(math.type(tonumber('0x1p4')), 'float', 'tonumber("0x1p4") yields float')
|
|
end
|
|
|
|
-- 23.9. Variables preserve type through assignment
|
|
|
|
do
|
|
local i = 5
|
|
local f = 5.0
|
|
|
|
assert_eq(math.type(i), 'integer', 'int variable')
|
|
assert_eq(math.type(f), 'float', 'float variable')
|
|
|
|
local i2 = i
|
|
local f2 = f
|
|
|
|
assert_eq(math.type(i2), 'integer', 'int variable copy')
|
|
assert_eq(math.type(f2), 'float', 'float variable copy')
|
|
end
|
|
|
|
-- 23.10. Function returns preserve type
|
|
|
|
do
|
|
local function ret_int()
|
|
return 42
|
|
end
|
|
|
|
local function ret_float()
|
|
return 42.0
|
|
end
|
|
|
|
assert_eq(math.type(ret_int()), 'integer', 'function returns integer')
|
|
assert_eq(math.type(ret_float()), 'float', 'function returns float')
|
|
end
|
|
|
|
-- 23.11. Table values preserve type
|
|
|
|
do
|
|
local t = {
|
|
i = 10,
|
|
f = 10.0,
|
|
[1] = 20,
|
|
[2] = 20.0
|
|
}
|
|
|
|
assert_eq(math.type(t.i), 'integer', 'table int value (string key)')
|
|
assert_eq(math.type(t.f), 'float', 'table float value (string key)')
|
|
assert_eq(math.type(t[1]), 'integer', 'table int value (int key)')
|
|
assert_eq(math.type(t[2]), 'float', 'table float value (int key)')
|
|
end
|
|
|
|
-- 23.12. For-loop variable type tracking
|
|
|
|
do
|
|
-- Integer loop
|
|
for i = 1, 3 do
|
|
assert_eq(math.type(i), 'integer', 'for 1,3: var is integer')
|
|
break -- Just test first iteration
|
|
end
|
|
|
|
-- Float loop (start is float)
|
|
for i = 1.0, 3 do
|
|
assert_eq(math.type(i), 'float', 'for 1.0,3: var is float')
|
|
break
|
|
end
|
|
|
|
-- Float loop (step is float)
|
|
for i = 1, 3, 1.0 do
|
|
assert_eq(math.type(i), 'float', 'for 1,3,1.0: var is float')
|
|
break
|
|
end
|
|
end
|
|
|
|
-- 23.13. Bitwise operations require integers (input & output)
|
|
|
|
do
|
|
assert_eq(math.type(5 & 3), 'integer', 'bitwise and yields integer')
|
|
assert_eq(math.type(5 | 2), 'integer', 'bitwise or yields integer')
|
|
assert_eq(math.type(5 ~ 1), 'integer', 'bitwise xor yields integer')
|
|
assert_eq(math.type(1 << 5), 'integer', 'left shift yields integer')
|
|
assert_eq(math.type(32 >> 2), 'integer', 'right shift yields integer')
|
|
assert_eq(math.type(~0), 'integer', 'bitwise not yields integer')
|
|
end
|
|
|
|
-- Bitwise operations convert float operands to integer
|
|
do
|
|
assert_eq(math.type(5.0 & 3), 'integer', 'float & int yields integer')
|
|
assert_eq(math.type(5 | 2.0), 'integer', 'int | float yields integer')
|
|
assert_eq(math.type(5.0 ~ 1.0), 'integer', 'float ~ float yields integer')
|
|
end
|
|
|
|
-- 23.14. Edge cases: zero types
|
|
|
|
do
|
|
-- Positive integer zero
|
|
assert_eq(math.type(0), 'integer', '0 is integer')
|
|
assert_eq(math.type(1 - 1), 'integer', '1 - 1 is integer')
|
|
assert_eq(math.type(0 * 1), 'integer', '0 * 1 is integer')
|
|
|
|
-- Positive float zero
|
|
assert_eq(math.type(0.0), 'float', '0.0 is float')
|
|
assert_eq(math.type(1.0 - 1.0), 'float', '1.0 - 1.0 is float')
|
|
|
|
-- Negative float zero
|
|
assert_eq(math.type(-0.0), 'float', '-0.0 is float')
|
|
assert_eq(math.type(0.0 * -1.0), 'float', '0.0 * -1.0 is float')
|
|
|
|
-- Integer operations never produce -0
|
|
assert_eq(math.type(0 * -1), 'integer', '0 * -1 is integer (not -0)')
|
|
end
|
|
|
|
-- 23.15. Special float values
|
|
|
|
do
|
|
local inf = 1.0 / 0.0
|
|
local neginf = -1.0 / 0.0
|
|
local nan = 0.0 / 0.0
|
|
|
|
assert_eq(type(inf), 'number', 'inf has type number')
|
|
assert_eq(type(neginf), 'number', '-inf has type number')
|
|
assert_eq(type(nan), 'number', 'nan has type number')
|
|
|
|
assert_eq(math.type(inf), 'float', 'inf has math.type float')
|
|
assert_eq(math.type(neginf), 'float', '-inf has math.type float')
|
|
assert_eq(math.type(nan), 'float', 'nan has math.type float')
|
|
end
|
|
|
|
-- 23.16. Type consistency across operations
|
|
|
|
do
|
|
local i = 10
|
|
local sum_i = 0
|
|
|
|
for n = 1, 5 do
|
|
sum_i = sum_i + n
|
|
assert_eq(math.type(sum_i), 'integer', 'integer sum stays integer')
|
|
end
|
|
|
|
local f = 10.0
|
|
local sum_f = 0.0
|
|
|
|
for n = 1.0, 5.0 do
|
|
sum_f = sum_f + n
|
|
assert_eq(math.type(sum_f), 'float', 'float sum stays float')
|
|
end
|
|
end
|