Space Lua: Align arithmetic model with standard Lua (#1611)
* Align Space Lua arithmetic model with standard Lua * Add missing __unm and __bnot metamethods - add new basic tests - add helper for negative tests * Add comments for ASCII numeric constants Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>
This commit is contained in:
@@ -1,91 +1,549 @@
|
||||
local function assert_eq(actual, expected, msg)
|
||||
local function assert_eq(actual, expected, message)
|
||||
if actual ~= expected then
|
||||
error('assert_eq failed: ' .. msg)
|
||||
error('Assertion failed: ' .. message)
|
||||
end
|
||||
end
|
||||
|
||||
-- Integer-only (0 and -0 as integers)
|
||||
-- Expectation: -0 is integer 0, so 1/-0 == +Inf, -1/-0 == +Inf
|
||||
assert_eq( 1/ 0 == 1/ 0, true, 'int: 1/ 0 == 1/ 0')
|
||||
assert_eq( 1/ 0 == -1/ 0, false, 'int: 1/ 0 == -1/ 0')
|
||||
assert_eq( 1/ 0 == 1/-0, true, 'int: 1/ 0 == 1/-0')
|
||||
assert_eq( 1/ 0 == -1/-0, false, 'int: 1/ 0 == -1/-0')
|
||||
local function assert_throws(msg_substr, fn)
|
||||
local ok, err = pcall(fn)
|
||||
|
||||
assert_eq( -1/ 0 == 1/ 0, false, 'int: -1/ 0 == 1/ 0')
|
||||
assert_eq( -1/ 0 == -1/ 0, true, 'int: -1/ 0 == -1/ 0')
|
||||
assert_eq( -1/ 0 == 1/-0, false, 'int: -1/ 0 == 1/-0')
|
||||
assert_eq( -1/ 0 == -1/-0, true, 'int: -1/ 0 == -1/-0')
|
||||
if ok then
|
||||
error('Assertion failed: expected error containing "'
|
||||
.. msg_substr .. '"')
|
||||
end
|
||||
|
||||
assert_eq( 1/-0 == 1/ 0, true, 'int: 1/-0 == 1/ 0')
|
||||
assert_eq( 1/-0 == -1/ 0, false, 'int: 1/-0 == -1/ 0')
|
||||
assert_eq( 1/-0 == 1/-0, true, 'int: 1/-0 == 1/-0')
|
||||
assert_eq( 1/-0 == -1/-0, false, 'int: 1/-0 == -1/-0')
|
||||
if type(err) ~= 'string' then
|
||||
err = tostring(err)
|
||||
end
|
||||
|
||||
assert_eq( -1/-0 == 1/ 0, false, 'int: -1/-0 == 1/ 0')
|
||||
assert_eq( -1/-0 == -1/ 0, true, 'int: -1/-0 == -1/0 ')
|
||||
assert_eq( -1/-0 == 1/-0, false, 'int: -1/-0 == 1/-0')
|
||||
assert_eq( -1/-0 == -1/-0, true, 'int: -1/-0 == -1/-0')
|
||||
if not string.find(err, msg_substr, 1, true) then
|
||||
error('Assertion failed: expected error message to contain "'
|
||||
.. msg_substr .. '", got: "' .. err .. '"')
|
||||
end
|
||||
end
|
||||
|
||||
-- Float-only (0.0 and -0.0 as floats)
|
||||
-- Expectation: -0.0 is float negative zero, so 1/-0.0 == -Inf, -1/-0.0 == +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, false, 'float: 1/ 0.0 == -1/ 0.0')
|
||||
assert_eq( 1/ 0.0 == 1/-0.0, false, 'float: 1/ 0.0 == 1/-0.0')
|
||||
assert_eq( 1/ 0.0 == -1/-0.0, true, 'float: 1/ 0.0 == -1/-0.0')
|
||||
-- 1. Integer vs float zero divisors
|
||||
|
||||
assert_eq( -1/ 0.0 == 1/ 0.0, false, 'float: -1/ 0.0 == 1/ 0.0')
|
||||
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')
|
||||
assert_eq( -1/ 0.0 == -1/-0.0, false, 'float: -1/ 0.0 == -1/-0.0')
|
||||
-- 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')
|
||||
|
||||
assert_eq( 1/-0.0 == 1/ 0.0, false, 'float: 1/-0.0 == 1/ 0.0')
|
||||
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')
|
||||
assert_eq( 1/-0.0 == -1/-0.0, false, 'float: 1/-0.0 == -1/-0.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')
|
||||
|
||||
assert_eq( -1/-0.0 == 1/ 0.0, true, 'float: -1/-0.0 == 1/ 0.0')
|
||||
assert_eq( -1/-0.0 == -1/ 0.0, false, 'float: -1/-0.0 == -1/ 0.0')
|
||||
assert_eq( -1/-0.0 == 1/-0.0, false, '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')
|
||||
|
||||
-- Integer-Float (left int 0/-0; right float 0.0/-0.0)
|
||||
assert_eq( 1/ 0 == 1/ 0.0, true, 'int-float: 1/0 == 1/ 0.0')
|
||||
assert_eq( 1/ 0 == -1/ 0.0, false, 'int-float: 1/0 == -1/ 0.0')
|
||||
assert_eq( 1/ 0 == 1/-0.0, false, 'int-float: 1/0 == 1/-0.0')
|
||||
assert_eq( 1/ 0 == -1/-0.0, true, 'int-float: 1/0 == -1/-0.0')
|
||||
-- 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)')
|
||||
|
||||
assert_eq( -1/ 0 == 1/ 0.0, false, 'int-float: -1/0 == 1/ 0.0')
|
||||
assert_eq( -1/ 0 == -1/ 0.0, true, 'int-float: -1/0 == -1/ 0.0')
|
||||
assert_eq( -1/ 0 == 1/-0.0, true, 'int-float: -1/0 == 1/-0.0')
|
||||
assert_eq( -1/ 0 == -1/-0.0, false, 'int-float: -1/0 == -1/-0.0')
|
||||
-- 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')
|
||||
|
||||
assert_eq( 1/-0 == 1/ 0.0, true, 'int-float: 1/-0 == 1/ 0.0')
|
||||
assert_eq( 1/-0 == -1/ 0.0, false, 'int-float: 1/-0 == -1/ 0.0')
|
||||
assert_eq( 1/-0 == 1/-0.0, false, 'int-float: 1/-0 == 1/-0.0')
|
||||
assert_eq( 1/-0 == -1/-0.0, true, 'int-float: 1/-0 == -1/-0.0')
|
||||
-- 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)')
|
||||
|
||||
assert_eq( -1/-0 == 1/ 0.0, false, 'int-float: -1/-0 == 1/ 0.0')
|
||||
assert_eq( -1/-0 == -1/ 0.0, true, 'int-float: -1/-0 == -1/ 0.0')
|
||||
assert_eq( -1/-0 == 1/-0.0, true, 'int-float: -1/-0 == 1/-0.0')
|
||||
assert_eq( -1/-0 == -1/-0.0, false, 'int-float: -1/-0 == -1/-0.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)')
|
||||
|
||||
-- Float-Integer (left float 0.0/-0.0; right int 0/-0)
|
||||
assert_eq( 1/ 0.0 == 1/ 0, true, 'float-int: 1/0.0 == 1/ 0')
|
||||
assert_eq( 1/ 0.0 == -1/ 0, false, 'float-int: 1/0.0 == -1/ 0')
|
||||
assert_eq( 1/ 0.0 == 1/-0, true, 'float-int: 1/0.0 == 1/-0')
|
||||
assert_eq( 1/ 0.0 == -1/-0, false, 'float-int: 1/0.0 == -1/-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')
|
||||
|
||||
assert_eq( -1/ 0.0 == 1/ 0, false, 'float-int: -1/0.0 == 1/ 0')
|
||||
assert_eq( -1/ 0.0 == -1/ 0, true, 'float-int: -1/0.0 == -1/ 0')
|
||||
assert_eq( -1/ 0.0 == 1/-0, false, 'float-int: -1/0.0 == 1/-0')
|
||||
assert_eq( -1/ 0.0 == -1/-0, true, 'float-int: -1/0.0 == -1/-0')
|
||||
-- 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)')
|
||||
|
||||
assert_eq( 1/-0.0 == 1/ 0, false, 'float-int: 1/-0.0 == 1/ 0')
|
||||
assert_eq( 1/-0.0 == -1/ 0, true, 'float-int: 1/-0.0 == -1/ 0')
|
||||
assert_eq( 1/-0.0 == 1/-0, false, 'float-int: 1/-0.0 == 1/-0')
|
||||
assert_eq( 1/-0.0 == -1/-0, true, 'float-int: 1/-0.0 == -1/-0')
|
||||
-- 6. Variables
|
||||
local zi, zf, zfn = 0, 0.0, -0.0
|
||||
|
||||
assert_eq( -1/-0.0 == 1/ 0, true, 'float-int: -1/-0.0 == 1/ 0')
|
||||
assert_eq( -1/-0.0 == -1/ 0, false, 'float-int: -1/-0.0 == -1/ 0')
|
||||
assert_eq( -1/-0.0 == 1/-0, true, 'float-int: -1/-0.0 == 1/-0')
|
||||
assert_eq( -1/-0.0 == -1/-0, false, 'float-int: -1/-0.0 == -1/-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)')
|
||||
|
||||
-- 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)')
|
||||
|
||||
-- 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')
|
||||
assert_throws('attempt to perform arithmetic on a non-number',
|
||||
function()
|
||||
return 'x1'+1
|
||||
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
|
||||
assert_throws('modulo by zero',
|
||||
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
|
||||
assert_throws('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
|
||||
assert_throws('has no integer representation',
|
||||
function()
|
||||
return ~0.5
|
||||
end
|
||||
)
|
||||
|
||||
assert_throws('attempt to perform arithmetic on a non-number',
|
||||
function()
|
||||
return 'a'+1
|
||||
end
|
||||
)
|
||||
|
||||
assert_throws('attempt to perform arithmetic on a non-number',
|
||||
function()
|
||||
return -{}
|
||||
end
|
||||
)
|
||||
|
||||
-- 19.1. Bitwise on non-integers should error
|
||||
assert_throws('has no integer representation',
|
||||
function()
|
||||
return 1.5&1
|
||||
end
|
||||
)
|
||||
|
||||
assert_throws('attempt to perform arithmetic on a non-number',
|
||||
function()
|
||||
return '3'|1
|
||||
end
|
||||
)
|
||||
|
||||
assert_throws('has no integer representation',
|
||||
function()
|
||||
return 1~1.2
|
||||
end
|
||||
)
|
||||
|
||||
assert_throws('has no integer representation',
|
||||
function()
|
||||
return 1<<0.1
|
||||
end
|
||||
)
|
||||
|
||||
-- 19.2. Relational type error
|
||||
assert_throws('attempt to compare number with string',
|
||||
function()
|
||||
return 1<'1'
|
||||
end
|
||||
)
|
||||
|
||||
-- 19.3. Additional negative tests
|
||||
assert_throws('attempt to perform arithmetic on a non-number',
|
||||
function()
|
||||
return 1+{}
|
||||
end
|
||||
)
|
||||
|
||||
assert_throws('attempt to perform arithmetic on a non-number',
|
||||
function()
|
||||
return -'x'
|
||||
end
|
||||
)
|
||||
|
||||
assert_throws('attempt to perform arithmetic on a non-number',
|
||||
function()
|
||||
return ~'1'
|
||||
end
|
||||
)
|
||||
|
||||
assert_throws('attempt to compare string with number',
|
||||
function()
|
||||
return '1'<1
|
||||
end
|
||||
)
|
||||
|
||||
assert_throws('attempt to compare number with object',
|
||||
function()
|
||||
return 1<{}
|
||||
end
|
||||
)
|
||||
|
||||
+479
-139
@@ -1,4 +1,10 @@
|
||||
import type { ASTCtx, LuaExpression, LuaLValue, LuaStatement } from "./ast.ts";
|
||||
import type {
|
||||
ASTCtx,
|
||||
LuaExpression,
|
||||
LuaLValue,
|
||||
LuaStatement,
|
||||
NumericType,
|
||||
} from "./ast.ts";
|
||||
import { evalPromiseValues } from "./util.ts";
|
||||
import {
|
||||
type ILuaFunction,
|
||||
@@ -30,18 +36,126 @@ import {
|
||||
ArrayQueryCollection,
|
||||
type LuaCollectionQuery,
|
||||
} from "./query_collection.ts";
|
||||
import { luaToNumber } from "./tonumber.ts";
|
||||
import {
|
||||
boxZero,
|
||||
coerceNumeric,
|
||||
coerceNumericPair,
|
||||
type OpHints,
|
||||
} from "./numeric.ts";
|
||||
|
||||
// Wrapper for arithmetical operators
|
||||
function luaCoerceToNumber(val: unknown): number {
|
||||
if (typeof val === "number") return val;
|
||||
if (typeof val === "string") {
|
||||
const n = luaToNumber(val);
|
||||
if (n !== null) {
|
||||
return n;
|
||||
}
|
||||
function luaFloorDiv(
|
||||
a: unknown,
|
||||
b: unknown,
|
||||
ctx: ASTCtx,
|
||||
sf: LuaStackFrame,
|
||||
hints?: OpHints,
|
||||
): number {
|
||||
const { ax, bx, bothInt } = coerceNumericPair(a, b, hints);
|
||||
if (bothInt && bx === 0) {
|
||||
throw new LuaRuntimeError(
|
||||
`attempt to divide by zero`,
|
||||
sf.withCtx(ctx),
|
||||
);
|
||||
}
|
||||
throw new Error(`attempt to perform arithmetic on a non-number`);
|
||||
|
||||
const q = Math.floor(ax / bx);
|
||||
if (q === 0) {
|
||||
if (bothInt) {
|
||||
return boxZero("int");
|
||||
}
|
||||
return Object.is(q, -0) ? -0 : boxZero("float");
|
||||
}
|
||||
return q;
|
||||
}
|
||||
|
||||
function luaMod(
|
||||
a: unknown,
|
||||
b: unknown,
|
||||
ctx: ASTCtx,
|
||||
sf: LuaStackFrame,
|
||||
hints?: OpHints,
|
||||
): number {
|
||||
const { ax, bx, bothInt } = coerceNumericPair(a, b, hints);
|
||||
if (bothInt && bx === 0) {
|
||||
throw new LuaRuntimeError(
|
||||
`attempt to perform modulo by zero`,
|
||||
sf.withCtx(ctx),
|
||||
);
|
||||
}
|
||||
|
||||
const q = Math.floor(ax / bx);
|
||||
const r = ax - q * bx;
|
||||
if (r === 0) {
|
||||
if (Object.is(ax, -0)) {
|
||||
return -0;
|
||||
}
|
||||
return boxZero(bothInt ? "int" : "float");
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
function luaLess(
|
||||
a: any,
|
||||
b: any,
|
||||
ctx: ASTCtx,
|
||||
sf: LuaStackFrame,
|
||||
): boolean {
|
||||
const { ta, tb, av, bv } = luaRelOperands(a, b);
|
||||
|
||||
if (ta === "number" && tb === "number") {
|
||||
return av < bv;
|
||||
}
|
||||
|
||||
if (ta === "string" && tb === "string") {
|
||||
return av < bv;
|
||||
}
|
||||
|
||||
throw new LuaRuntimeError(
|
||||
`attempt to compare ${ta} with ${tb}`,
|
||||
sf.withCtx(ctx),
|
||||
);
|
||||
}
|
||||
|
||||
function luaLessEqual(
|
||||
a: any,
|
||||
b: any,
|
||||
ctx: ASTCtx,
|
||||
sf: LuaStackFrame,
|
||||
): boolean {
|
||||
const { ta, tb, av, bv } = luaRelOperands(a, b);
|
||||
|
||||
if (ta === "number" && tb === "number") {
|
||||
return av <= bv;
|
||||
}
|
||||
|
||||
if (ta === "string" && tb === "string") {
|
||||
return av <= bv;
|
||||
}
|
||||
|
||||
throw new LuaRuntimeError(
|
||||
`attempt to compare ${ta} with ${tb}`,
|
||||
sf.withCtx(ctx),
|
||||
);
|
||||
}
|
||||
|
||||
function luaUnaryMinus(
|
||||
v: any,
|
||||
): number {
|
||||
const { n, zeroKind } = coerceNumeric(v);
|
||||
|
||||
if (n === 0) {
|
||||
if (Object.is(n, -0)) {
|
||||
return boxZero("float");
|
||||
}
|
||||
if (zeroKind === "int") {
|
||||
return boxZero("int");
|
||||
}
|
||||
if (zeroKind === "float") {
|
||||
return -0;
|
||||
}
|
||||
return -0;
|
||||
}
|
||||
return -n;
|
||||
}
|
||||
|
||||
async function handleTableFieldSync(
|
||||
@@ -77,6 +191,23 @@ async function handleTableFieldSync(
|
||||
}
|
||||
}
|
||||
|
||||
// Unwrap parentheses and unary +/- around a numeric literal
|
||||
function astNumberKind(e: LuaExpression | undefined): NumericType | undefined {
|
||||
if (!e) {
|
||||
return undefined;
|
||||
}
|
||||
while (e.type === "Parenthesized") {
|
||||
e = e.expression;
|
||||
}
|
||||
if (e.type === "Unary" && (e.operator === "-" || e.operator === "+")) {
|
||||
return astNumberKind(e.argument);
|
||||
}
|
||||
if (e.type === "Number") {
|
||||
return e.numericType === "int" ? "int" : "float";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function evalExpression(
|
||||
e: LuaExpression,
|
||||
env: LuaEnv,
|
||||
@@ -84,122 +215,119 @@ export function evalExpression(
|
||||
): Promise<LuaValue> | LuaValue {
|
||||
try {
|
||||
switch (e.type) {
|
||||
case "String":
|
||||
case "String": {
|
||||
return e.value;
|
||||
case "Number":
|
||||
}
|
||||
case "Number": {
|
||||
return (e.value === 0 && !Object.is(e.value, -0))
|
||||
? boxZero(e.numericType === "int" ? "int" : "float")
|
||||
: e.value;
|
||||
}
|
||||
case "Boolean": {
|
||||
return e.value;
|
||||
case "Boolean":
|
||||
return e.value;
|
||||
case "Nil":
|
||||
}
|
||||
case "Nil": {
|
||||
return null;
|
||||
}
|
||||
case "Binary": {
|
||||
if (e.operator === "or") {
|
||||
// Special case: eagerly evaluate left before even attempting right
|
||||
const left = evalExpression(e.left, env, sf);
|
||||
if (left instanceof Promise) {
|
||||
return left.then((left) => {
|
||||
if (luaTruthy(left)) {
|
||||
return left;
|
||||
}
|
||||
return evalExpression(e.right, env, sf);
|
||||
});
|
||||
} else if (luaTruthy(left)) {
|
||||
return left;
|
||||
} else {
|
||||
return evalExpression(e.right, env, sf);
|
||||
}
|
||||
return evalLogical("or", e.left, e.right, env, sf);
|
||||
} else if (e.operator === "and") {
|
||||
// Special case: eagerly evaluate left before even attempting right
|
||||
const left = evalExpression(e.left, env, sf);
|
||||
if (left instanceof Promise) {
|
||||
return left.then((left) => {
|
||||
if (!luaTruthy(left)) {
|
||||
return left;
|
||||
}
|
||||
return evalExpression(e.right, env, sf);
|
||||
});
|
||||
} else if (!luaTruthy(left)) {
|
||||
return left;
|
||||
} else {
|
||||
return evalExpression(e.right, env, sf);
|
||||
}
|
||||
}
|
||||
const values = evalPromiseValues([
|
||||
evalExpression(e.left, env, sf),
|
||||
evalExpression(e.right, env, sf),
|
||||
]);
|
||||
if (values instanceof Promise) {
|
||||
return values.then(([left, right]) =>
|
||||
luaOp(
|
||||
e.operator,
|
||||
singleResult(left),
|
||||
singleResult(right),
|
||||
e.ctx,
|
||||
sf,
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return luaOp(
|
||||
e.operator,
|
||||
singleResult(values[0]),
|
||||
singleResult(values[1]),
|
||||
e.ctx,
|
||||
sf,
|
||||
);
|
||||
return evalLogical("and", e.left, e.right, env, sf);
|
||||
}
|
||||
// Enforce left-to-right evaluation
|
||||
const hints: OpHints = opHintsFromBinary(e);
|
||||
return evalBinaryWithLR(
|
||||
e.operator,
|
||||
e.left,
|
||||
e.right,
|
||||
e.ctx,
|
||||
env,
|
||||
sf,
|
||||
hints,
|
||||
);
|
||||
}
|
||||
case "Unary": {
|
||||
const value = evalExpression(e.argument, env, sf);
|
||||
if (value instanceof Promise) {
|
||||
return value.then((value) => {
|
||||
switch (e.operator) {
|
||||
case "-":
|
||||
if (
|
||||
e.argument.type === "Number" &&
|
||||
e.argument.numericType === "int"
|
||||
) {
|
||||
if (e.argument.value === 0) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return -luaCoerceToNumber(singleResult(value));
|
||||
case "+":
|
||||
return +singleResult(value);
|
||||
case "not":
|
||||
return !singleResult(value);
|
||||
case "~":
|
||||
return ~exactInt(singleResult(value), e.ctx, sf);
|
||||
case "#":
|
||||
return luaLen(singleResult(value));
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown unary operator ${e.operator}`,
|
||||
case "-": {
|
||||
const arg = singleResult(value);
|
||||
return unaryWithMeta(
|
||||
arg,
|
||||
"__unm",
|
||||
e.ctx,
|
||||
sf,
|
||||
() => luaUnaryMinus(arg),
|
||||
);
|
||||
}
|
||||
case "+": {
|
||||
return +singleResult(value);
|
||||
}
|
||||
case "not": {
|
||||
return !singleResult(value);
|
||||
}
|
||||
case "~": {
|
||||
const arg = singleResult(value);
|
||||
return unaryWithMeta(
|
||||
arg,
|
||||
"__bnot",
|
||||
e.ctx,
|
||||
sf,
|
||||
() => ~exactInt(arg, e.ctx, sf),
|
||||
);
|
||||
}
|
||||
case "#": {
|
||||
return luaLen(singleResult(value));
|
||||
}
|
||||
default: {
|
||||
throw new LuaRuntimeError(
|
||||
`Unknown unary operator ${e.operator}`,
|
||||
sf.withCtx(e.ctx),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
switch (e.operator) {
|
||||
case "-":
|
||||
if (
|
||||
e.argument.type === "Number" && e.argument.numericType === "int"
|
||||
) {
|
||||
if (e.argument.value === 0) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return -luaCoerceToNumber(singleResult(value));
|
||||
case "+":
|
||||
return +singleResult(value);
|
||||
case "not":
|
||||
return !singleResult(value);
|
||||
case "~":
|
||||
return ~exactInt(singleResult(value), e.ctx, sf);
|
||||
case "#":
|
||||
return luaLen(singleResult(value));
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown unary operator ${e.operator}`,
|
||||
case "-": {
|
||||
const arg = singleResult(value);
|
||||
return unaryWithMeta(
|
||||
arg,
|
||||
"__unm",
|
||||
e.ctx,
|
||||
sf,
|
||||
() => luaUnaryMinus(arg),
|
||||
);
|
||||
}
|
||||
case "+": {
|
||||
return +singleResult(value);
|
||||
}
|
||||
case "not": {
|
||||
return !singleResult(value);
|
||||
}
|
||||
case "~": {
|
||||
const arg = singleResult(value);
|
||||
return unaryWithMeta(
|
||||
arg,
|
||||
"__bnot",
|
||||
e.ctx,
|
||||
sf,
|
||||
() => ~exactInt(arg, e.ctx, sf),
|
||||
);
|
||||
}
|
||||
case "#": {
|
||||
return luaLen(singleResult(value));
|
||||
}
|
||||
default: {
|
||||
throw new LuaRuntimeError(
|
||||
`Unknown unary operator ${e.operator}`,
|
||||
sf.withCtx(e.ctx),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -207,8 +335,9 @@ export function evalExpression(
|
||||
case "Variable":
|
||||
case "FunctionCall":
|
||||
case "TableAccess":
|
||||
case "PropertyAccess":
|
||||
case "PropertyAccess": {
|
||||
return evalPrefixExpression(e, env, sf);
|
||||
}
|
||||
case "TableConstructor": {
|
||||
return Promise.resolve().then(async () => {
|
||||
const table = new LuaTable();
|
||||
@@ -227,7 +356,10 @@ export function evalExpression(
|
||||
// console.log("Query", e);
|
||||
const findFromClause = e.clauses.find((c) => c.type === "From");
|
||||
if (!findFromClause) {
|
||||
throw new LuaRuntimeError("No from clause found", sf.withCtx(e.ctx));
|
||||
throw new LuaRuntimeError(
|
||||
"No from clause found",
|
||||
sf.withCtx(e.ctx),
|
||||
);
|
||||
}
|
||||
const objectVariable = findFromClause.name;
|
||||
const objectExpression = findFromClause.expression;
|
||||
@@ -297,7 +429,10 @@ export function evalExpression(
|
||||
);
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown expression type ${e.type}`);
|
||||
throw new LuaRuntimeError(
|
||||
`Unknown expression type ${e.type}`,
|
||||
sf.withCtx(e.ctx),
|
||||
);
|
||||
}
|
||||
} catch (err: any) {
|
||||
// Repackage any non Lua-specific exceptions with some position information
|
||||
@@ -323,8 +458,9 @@ function evalPrefixExpression(
|
||||
return value;
|
||||
}
|
||||
}
|
||||
case "Parenthesized":
|
||||
case "Parenthesized": {
|
||||
return evalExpression(e.expression, env, sf);
|
||||
}
|
||||
// <<expr>>[<<expr>>]
|
||||
case "TableAccess": {
|
||||
const values = evalPromiseValues([
|
||||
@@ -398,7 +534,10 @@ function evalPrefixExpression(
|
||||
}
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown prefix expression type ${e.type}`);
|
||||
throw new LuaRuntimeError(
|
||||
`Unknown prefix expression type ${e.type}`,
|
||||
sf.withCtx(e.ctx),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,6 +560,138 @@ function evalMetamethod(
|
||||
}
|
||||
}
|
||||
|
||||
// Unary metamethod lookup and call
|
||||
function evalUnaryMetamethod(
|
||||
value: any,
|
||||
metaMethod: "__unm" | "__bnot",
|
||||
ctx: ASTCtx,
|
||||
sf: LuaStackFrame,
|
||||
): LuaValue | Promise<LuaValue> | undefined {
|
||||
const mt = getMetatable(value, sf);
|
||||
if (mt?.has(metaMethod)) {
|
||||
const fn = mt.get(metaMethod);
|
||||
return luaCall(fn, [value], ctx, sf);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Unary metamethod handling (with fallback)
|
||||
function unaryWithMeta(
|
||||
arg: any,
|
||||
meta: "__unm" | "__bnot",
|
||||
ctx: ASTCtx,
|
||||
sf: LuaStackFrame,
|
||||
fallback: () => any,
|
||||
): any {
|
||||
const mm = evalUnaryMetamethod(arg, meta, ctx, sf);
|
||||
|
||||
if (mm !== undefined) {
|
||||
return mm instanceof Promise ? mm.then(singleResult) : singleResult(mm);
|
||||
}
|
||||
return fallback();
|
||||
}
|
||||
|
||||
// Logical short-circuit evaluation
|
||||
function evalLogical(
|
||||
op: "and" | "or",
|
||||
leftExpr: LuaExpression,
|
||||
rightExpr: LuaExpression,
|
||||
env: LuaEnv,
|
||||
sf: LuaStackFrame,
|
||||
): any {
|
||||
const left = evalExpression(leftExpr, env, sf);
|
||||
|
||||
const decide = (lv: any) => {
|
||||
if (op === "or") {
|
||||
if (luaTruthy(lv)) {
|
||||
return lv;
|
||||
}
|
||||
return evalExpression(rightExpr, env, sf);
|
||||
} else {
|
||||
if (!luaTruthy(lv)) {
|
||||
return lv;
|
||||
}
|
||||
return evalExpression(rightExpr, env, sf);
|
||||
}
|
||||
};
|
||||
|
||||
if (left instanceof Promise) {
|
||||
return left.then(decide);
|
||||
} else {
|
||||
return decide(left);
|
||||
}
|
||||
}
|
||||
|
||||
function opHintsFromBinary(
|
||||
e: Extract<LuaExpression, { type: "Binary" }>,
|
||||
): OpHints {
|
||||
return {
|
||||
leftKind: astNumberKind(e.left),
|
||||
rightKind: astNumberKind(e.right),
|
||||
};
|
||||
}
|
||||
|
||||
function evalBinaryWithLR(
|
||||
op: string,
|
||||
leftExpr: LuaExpression,
|
||||
rightExpr: LuaExpression,
|
||||
ctx: ASTCtx,
|
||||
env: LuaEnv,
|
||||
sf: LuaStackFrame,
|
||||
hints?: OpHints,
|
||||
): any {
|
||||
const leftVal = evalExpression(leftExpr, env, sf);
|
||||
|
||||
const evalRightAndOp = (lv: any) => {
|
||||
const rightVal = evalExpression(rightExpr, env, sf);
|
||||
if (rightVal instanceof Promise) {
|
||||
return rightVal.then((rv) =>
|
||||
luaOp(
|
||||
op,
|
||||
singleResult(lv),
|
||||
singleResult(rv),
|
||||
ctx,
|
||||
sf,
|
||||
hints,
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return luaOp(
|
||||
op,
|
||||
singleResult(leftVal instanceof Promise ? lv : leftVal),
|
||||
singleResult(rightVal),
|
||||
ctx,
|
||||
sf,
|
||||
hints,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (leftVal instanceof Promise) {
|
||||
return leftVal.then(evalRightAndOp);
|
||||
} else {
|
||||
return evalRightAndOp(leftVal);
|
||||
}
|
||||
}
|
||||
|
||||
// Relational comparison "prelude"
|
||||
function luaRelOperands(
|
||||
a: any,
|
||||
b: any,
|
||||
): {
|
||||
ta: string;
|
||||
tb: string;
|
||||
av: any;
|
||||
bv: any;
|
||||
} {
|
||||
const ta = (a instanceof Number) ? "number" : typeof a;
|
||||
const tb = (b instanceof Number) ? "number" : typeof b;
|
||||
const av = (a instanceof Number) ? Number(a) : a;
|
||||
const bv = (b instanceof Number) ? Number(b) : b;
|
||||
|
||||
return { ta, tb, av, bv };
|
||||
}
|
||||
|
||||
export function getMetatable(
|
||||
value: LuaValue,
|
||||
sf?: LuaStackFrame,
|
||||
@@ -461,37 +732,77 @@ const operatorsMetaMethods: Record<string, {
|
||||
b: LuaValue,
|
||||
ctx: ASTCtx,
|
||||
sf: LuaStackFrame,
|
||||
hints?: OpHints,
|
||||
) => LuaValue;
|
||||
}> = {
|
||||
"+": {
|
||||
metaMethod: "__add",
|
||||
nativeImplementation: (a, b) => luaCoerceToNumber(a) + luaCoerceToNumber(b),
|
||||
nativeImplementation: (a, b, _ctx, _sf, hints) => {
|
||||
const { ax, bx, bothInt } = coerceNumericPair(a, b, hints);
|
||||
const r = ax + bx;
|
||||
|
||||
if (r === 0) {
|
||||
if (Object.is(r, -0)) {
|
||||
return bothInt ? boxZero("int") : -0;
|
||||
}
|
||||
return boxZero(bothInt ? "int" : "float");
|
||||
}
|
||||
return r;
|
||||
},
|
||||
},
|
||||
"-": {
|
||||
metaMethod: "__sub",
|
||||
nativeImplementation: (a, b) => luaCoerceToNumber(a) - luaCoerceToNumber(b),
|
||||
nativeImplementation: (a, b, _ctx, _sf, hints) => {
|
||||
const { ax, bx, bothInt } = coerceNumericPair(a, b, hints);
|
||||
const r = ax - bx;
|
||||
|
||||
if (r === 0) {
|
||||
if (Object.is(r, -0)) {
|
||||
return bothInt ? boxZero("int") : -0;
|
||||
}
|
||||
return boxZero(bothInt ? "int" : "float");
|
||||
}
|
||||
return r;
|
||||
},
|
||||
},
|
||||
"*": {
|
||||
metaMethod: "__mul",
|
||||
nativeImplementation: (a, b) => luaCoerceToNumber(a) * luaCoerceToNumber(b),
|
||||
nativeImplementation: (a, b, _ctx, _sf, hints) => {
|
||||
const { ax, bx, bothInt } = coerceNumericPair(a, b, hints);
|
||||
const r = ax * bx;
|
||||
|
||||
if (r === 0) {
|
||||
if (Object.is(r, -0)) {
|
||||
return bothInt ? boxZero("int") : -0;
|
||||
}
|
||||
return boxZero(bothInt ? "int" : "float");
|
||||
}
|
||||
return r;
|
||||
},
|
||||
},
|
||||
"/": {
|
||||
metaMethod: "__div",
|
||||
nativeImplementation: (a, b) => luaCoerceToNumber(a) / luaCoerceToNumber(b),
|
||||
nativeImplementation: (a, b, _ctx, _sf, hints) => {
|
||||
const { ax, bx } = coerceNumericPair(a, b, hints);
|
||||
return ax / bx;
|
||||
},
|
||||
},
|
||||
"//": {
|
||||
metaMethod: "__idiv",
|
||||
nativeImplementation: (a, b) =>
|
||||
Math.floor(luaCoerceToNumber(a) / luaCoerceToNumber(b)),
|
||||
nativeImplementation: (a, b, ctx, sf, hints) =>
|
||||
luaFloorDiv(a, b, ctx, sf, hints),
|
||||
},
|
||||
"%": {
|
||||
metaMethod: "__mod",
|
||||
nativeImplementation: (a, b) => luaCoerceToNumber(a) % luaCoerceToNumber(b),
|
||||
nativeImplementation: (a, b, ctx, sf, hints) =>
|
||||
luaMod(a, b, ctx, sf, hints),
|
||||
},
|
||||
"^": {
|
||||
metaMethod: "__pow",
|
||||
nativeImplementation: (a, b) =>
|
||||
luaCoerceToNumber(a) ** luaCoerceToNumber(b),
|
||||
nativeImplementation: (a, b, _ctx, _sf, hints) => {
|
||||
const { ax, bx } = coerceNumericPair(a, b, hints);
|
||||
return ax ** bx;
|
||||
},
|
||||
},
|
||||
"&": {
|
||||
metaMethod: "__band",
|
||||
@@ -523,6 +834,7 @@ const operatorsMetaMethods: Record<string, {
|
||||
nativeImplementation: (a, b) => {
|
||||
const aString = luaToString(a);
|
||||
const bString = luaToString(b);
|
||||
|
||||
if (aString instanceof Promise || bString instanceof Promise) {
|
||||
return Promise.all([aString, bString]).then(([a, b]) => a + b);
|
||||
} else {
|
||||
@@ -542,15 +854,20 @@ const operatorsMetaMethods: Record<string, {
|
||||
metaMethod: "__ne",
|
||||
nativeImplementation: (a, b) => !luaEquals(a, b),
|
||||
},
|
||||
"<": { metaMethod: "__lt", nativeImplementation: (a, b) => a < b },
|
||||
"<=": { metaMethod: "__le", nativeImplementation: (a, b) => a <= b },
|
||||
">": { nativeImplementation: (a, b, ctx, sf) => !luaOp("<=", a, b, ctx, sf) },
|
||||
">=": { nativeImplementation: (a, b, ctx, sf) => !luaOp("<", a, b, ctx, sf) },
|
||||
"and": {
|
||||
metaMethod: "__and",
|
||||
nativeImplementation: (a, b) => a && b,
|
||||
"<": {
|
||||
metaMethod: "__lt",
|
||||
nativeImplementation: (a, b, ctx, sf) => luaLess(a, b, ctx, sf),
|
||||
},
|
||||
"<=": {
|
||||
metaMethod: "__le",
|
||||
nativeImplementation: (a, b, ctx, sf) => luaLessEqual(a, b, ctx, sf),
|
||||
},
|
||||
">": {
|
||||
nativeImplementation: (a, b, ctx, sf) => !luaOp("<=", a, b, ctx, sf),
|
||||
},
|
||||
">=": {
|
||||
nativeImplementation: (a, b, ctx, sf) => !luaOp("<", a, b, ctx, sf),
|
||||
},
|
||||
"or": { metaMethod: "__or", nativeImplementation: (a, b) => a || b },
|
||||
};
|
||||
|
||||
function luaOp(
|
||||
@@ -559,6 +876,7 @@ function luaOp(
|
||||
right: any,
|
||||
ctx: ASTCtx,
|
||||
sf: LuaStackFrame,
|
||||
hints?: OpHints,
|
||||
): any {
|
||||
const handler = operatorsMetaMethods[op];
|
||||
if (!handler) {
|
||||
@@ -572,7 +890,7 @@ function luaOp(
|
||||
}
|
||||
}
|
||||
|
||||
return handler.nativeImplementation(left, right, ctx, sf);
|
||||
return handler.nativeImplementation(left, right, ctx, sf, hints);
|
||||
}
|
||||
|
||||
function evalExpressions(
|
||||
@@ -633,11 +951,16 @@ export async function evalStatement(
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "Semicolon":
|
||||
case "Semicolon": {
|
||||
break;
|
||||
}
|
||||
case "Label":
|
||||
case "Goto":
|
||||
throw new Error("Labels and gotos are not supported");
|
||||
case "Goto": {
|
||||
throw new LuaRuntimeError(
|
||||
"Labels and gotos are not supported",
|
||||
sf.withCtx(s.ctx),
|
||||
);
|
||||
}
|
||||
case "Block": {
|
||||
const newEnv = new LuaEnv(env);
|
||||
for (const statement of s.statements) {
|
||||
@@ -695,8 +1018,9 @@ export async function evalStatement(
|
||||
} while (!luaTruthy(await evalExpression(s.condition, env, sf)));
|
||||
break;
|
||||
}
|
||||
case "Break":
|
||||
case "Break": {
|
||||
throw new LuaBreak();
|
||||
}
|
||||
case "FunctionCallStatement": {
|
||||
await evalExpression(s.call, env, sf);
|
||||
return;
|
||||
@@ -828,8 +1152,12 @@ function evalLValue(
|
||||
sf: LuaStackFrame,
|
||||
): LuaLValueContainer | Promise<LuaLValueContainer> {
|
||||
switch (lval.type) {
|
||||
case "Variable":
|
||||
return { env, key: lval.name };
|
||||
case "Variable": {
|
||||
return {
|
||||
env,
|
||||
key: lval.name,
|
||||
};
|
||||
}
|
||||
case "TableAccess": {
|
||||
const objValue = evalExpression(
|
||||
lval.object,
|
||||
@@ -879,16 +1207,28 @@ function evalLValue(
|
||||
}
|
||||
|
||||
function exactInt(
|
||||
num: number,
|
||||
num: any,
|
||||
ctx: ASTCtx,
|
||||
sf: LuaStackFrame,
|
||||
): number {
|
||||
// See conversion from float to integer https://www.lua.org/manual/5.4/manual.html#3.4.3
|
||||
if (!Number.isInteger(num)) {
|
||||
let n: number;
|
||||
if (typeof num === "number") {
|
||||
n = num;
|
||||
} else if (num instanceof Number) {
|
||||
n = Number(num);
|
||||
} else {
|
||||
throw new LuaRuntimeError(
|
||||
`Number ${num} has no integer representation (consider math.floor or math.ceil)`,
|
||||
`attempt to perform arithmetic on a non-number`,
|
||||
sf.withCtx(ctx),
|
||||
);
|
||||
}
|
||||
return num;
|
||||
|
||||
if (!Number.isInteger(n)) {
|
||||
throw new LuaRuntimeError(
|
||||
`Number ${n} has no integer representation (consider math.floor or math.ceil)`,
|
||||
sf.withCtx(ctx),
|
||||
);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import type { NumericType } from "./ast.ts";
|
||||
import { luaToNumberDetailed } from "./tonumber.ts";
|
||||
|
||||
export const ZeroBoxKind = Symbol("ZeroBox");
|
||||
|
||||
export function isBoxedZero(x: any): boolean {
|
||||
return x instanceof Number && Number(x) === 0 && !Object.is(Number(x), -0);
|
||||
}
|
||||
|
||||
export function getZeroBoxKind(x: any): NumericType | undefined {
|
||||
return isBoxedZero(x)
|
||||
? (x as any)[ZeroBoxKind] as NumericType | undefined
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function boxZero(kind: NumericType): number {
|
||||
const z = new Number(0);
|
||||
(z as any)[ZeroBoxKind] = kind;
|
||||
return z as unknown as number;
|
||||
}
|
||||
|
||||
export type OpHints = {
|
||||
leftKind?: NumericType;
|
||||
rightKind?: NumericType;
|
||||
};
|
||||
|
||||
export function coerceNumeric(
|
||||
val: unknown,
|
||||
): {
|
||||
n: number;
|
||||
isInt: boolean;
|
||||
zeroKind?: NumericType;
|
||||
} {
|
||||
if (isBoxedZero(val)) {
|
||||
return { n: 0, isInt: true, zeroKind: getZeroBoxKind(val)! };
|
||||
}
|
||||
|
||||
if (typeof val === "number") {
|
||||
const n = val;
|
||||
if (n === 0) {
|
||||
if (Object.is(n, -0)) {
|
||||
return {
|
||||
n,
|
||||
isInt: true,
|
||||
zeroKind: "float",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
n,
|
||||
isInt: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
n,
|
||||
isInt: Number.isInteger(n),
|
||||
};
|
||||
}
|
||||
|
||||
if (val instanceof Number) {
|
||||
const n = Number(val);
|
||||
if (n === 0 && !Object.is(n, -0)) {
|
||||
return {
|
||||
n,
|
||||
isInt: true,
|
||||
zeroKind: getZeroBoxKind(val),
|
||||
};
|
||||
}
|
||||
return {
|
||||
n,
|
||||
isInt: Number.isInteger(n),
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof val === "string") {
|
||||
const det = luaToNumberDetailed(val);
|
||||
if (!det) {
|
||||
throw new Error(`attempt to perform arithmetic on a non-number`);
|
||||
}
|
||||
|
||||
const n = det.value;
|
||||
const isInt = det.numericType === "int";
|
||||
if (n === 0) {
|
||||
if (Object.is(n, -0)) {
|
||||
return {
|
||||
n,
|
||||
isInt,
|
||||
zeroKind: "float",
|
||||
};
|
||||
}
|
||||
return {
|
||||
n,
|
||||
isInt,
|
||||
zeroKind: det.numericType,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
n,
|
||||
isInt,
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`attempt to perform arithmetic on a non-number`);
|
||||
}
|
||||
|
||||
export function coerceNumericPair(
|
||||
a: unknown,
|
||||
b: unknown,
|
||||
hints?: OpHints,
|
||||
): {
|
||||
ax: number;
|
||||
bx: number;
|
||||
bothInt: boolean;
|
||||
aZeroKind?: NumericType;
|
||||
bZeroKind?: NumericType;
|
||||
} {
|
||||
const A = coerceNumeric(a);
|
||||
const B = coerceNumeric(b);
|
||||
|
||||
const aIsInt = hints?.leftKind
|
||||
? hints.leftKind === "int"
|
||||
: (A.zeroKind ? A.zeroKind === "int" : A.isInt);
|
||||
|
||||
const bIsInt = hints?.rightKind
|
||||
? hints.rightKind === "int"
|
||||
: (B.zeroKind ? B.zeroKind === "int" : B.isInt);
|
||||
|
||||
return {
|
||||
ax: A.n,
|
||||
bx: B.n,
|
||||
bothInt: aIsInt && bIsInt,
|
||||
aZeroKind: A.zeroKind,
|
||||
bZeroKind: B.zeroKind,
|
||||
};
|
||||
}
|
||||
+37
-12
@@ -296,6 +296,14 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
|
||||
this.metatable = null;
|
||||
}
|
||||
|
||||
private static isIntegerKey(key: any): boolean {
|
||||
const k = key instanceof Number ? Number(key) : key;
|
||||
return typeof k === "number" && Number.isInteger(k) && k >= 1;
|
||||
}
|
||||
private static toIndex(key: any): number {
|
||||
return (key instanceof Number ? Number(key) : key) - 1;
|
||||
}
|
||||
|
||||
get length(): number {
|
||||
return this.arrayPart.length;
|
||||
}
|
||||
@@ -324,8 +332,8 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
|
||||
has(key: LuaValue) {
|
||||
if (typeof key === "string") {
|
||||
return this.stringKeys[key] !== undefined;
|
||||
} else if (Number.isInteger(key) && key >= 1) {
|
||||
return this.arrayPart[key - 1] !== undefined;
|
||||
} else if (LuaTable.isIntegerKey(key)) {
|
||||
return this.arrayPart[LuaTable.toIndex(key)] !== undefined;
|
||||
} else if (this.otherKeys) {
|
||||
return this.otherKeys.has(key);
|
||||
}
|
||||
@@ -345,8 +353,8 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
|
||||
} else {
|
||||
this.stringKeys[key] = value;
|
||||
}
|
||||
} else if (Number.isInteger(key) && key >= 1) {
|
||||
this.arrayPart[key - 1] = value;
|
||||
} else if (LuaTable.isIntegerKey(key)) {
|
||||
this.arrayPart[LuaTable.toIndex(key)] = value;
|
||||
} else {
|
||||
if (!this.otherKeys) {
|
||||
this.otherKeys = new Map();
|
||||
@@ -385,8 +393,8 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
|
||||
rawGet(key: LuaValue): LuaValue | null {
|
||||
if (typeof key === "string") {
|
||||
return this.stringKeys[key];
|
||||
} else if (Number.isInteger(key) && key >= 1) {
|
||||
return this.arrayPart[key - 1];
|
||||
} else if (LuaTable.isIntegerKey(key)) {
|
||||
return this.arrayPart[LuaTable.toIndex(key)];
|
||||
} else if (this.otherKeys) {
|
||||
return this.otherKeys.get(key);
|
||||
}
|
||||
@@ -513,7 +521,14 @@ export function luaIndexValue(
|
||||
}
|
||||
}
|
||||
// If not, perhaps let's assume this is a plain JavaScript object and we just index into it
|
||||
const objValue = value[key];
|
||||
const idxKey = key instanceof Number ? Number(key) : key;
|
||||
if (
|
||||
typeof value === "object" && Array.isArray(value) &&
|
||||
typeof idxKey === "number"
|
||||
) {
|
||||
return (value as any)[idxKey - 1];
|
||||
}
|
||||
const objValue = (value as any)[idxKey];
|
||||
if (objValue === undefined || objValue === null) {
|
||||
return null;
|
||||
} else {
|
||||
@@ -539,7 +554,8 @@ export async function luaSet(
|
||||
if (obj instanceof LuaTable || obj instanceof LuaEnv) {
|
||||
await obj.set(key, value, sf);
|
||||
} else {
|
||||
obj[key] = value;
|
||||
const k = key instanceof Number ? Number(key) : key;
|
||||
obj[k] = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -563,11 +579,13 @@ export function luaGet(
|
||||
|
||||
if (obj instanceof LuaTable || obj instanceof LuaEnv) {
|
||||
return obj.get(key, sf);
|
||||
} else if (typeof key === "number") {
|
||||
return obj[key - 1];
|
||||
} else if (typeof key === "number" || key instanceof Number) {
|
||||
const idx = key instanceof Number ? Number(key) : key;
|
||||
return obj[idx - 1];
|
||||
} else {
|
||||
// Native JS object
|
||||
const val = obj[key];
|
||||
const k = key instanceof Number ? Number(key) : key;
|
||||
const val = obj[k];
|
||||
if (typeof val === "function") {
|
||||
// Automatically bind the function to the object
|
||||
return val.bind(obj);
|
||||
@@ -649,6 +667,11 @@ export function luaCall(
|
||||
}
|
||||
|
||||
export function luaEquals(a: any, b: any): boolean {
|
||||
const an = a instanceof Number ? Number(a) : a;
|
||||
const bn = b instanceof Number ? Number(b) : b;
|
||||
if ((typeof an === "number") && (typeof bn === "number")) {
|
||||
return an === bn;
|
||||
}
|
||||
return a === b;
|
||||
}
|
||||
|
||||
@@ -670,7 +693,7 @@ export function luaTypeOf(val: any): LuaType | Promise<LuaType> {
|
||||
return val.then((v) => luaTypeOf(v));
|
||||
} else if (typeof val === "boolean") {
|
||||
return "boolean";
|
||||
} else if (typeof val === "number") {
|
||||
} else if (typeof val === "number" || val instanceof Number) {
|
||||
return "number";
|
||||
} else if (typeof val === "string") {
|
||||
return "string";
|
||||
@@ -903,6 +926,8 @@ export function luaValueToJS(value: any, sf: LuaStackFrame): any {
|
||||
return value.call(sf, ...jsArgs);
|
||||
}
|
||||
};
|
||||
} else if (value instanceof Number) {
|
||||
return Number(value);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -12,12 +12,13 @@
|
||||
* - hex float requires `p` or `P` exponent
|
||||
* - decimal float allows '.' and `e` or `E` exponent
|
||||
*/
|
||||
import type { NumericType } from "./ast.ts";
|
||||
|
||||
function skipSpace(s: string, i: number): number {
|
||||
const n = s.length;
|
||||
while (i < n) {
|
||||
const c = s.charCodeAt(i);
|
||||
if (c === 32 || (c >= 9 && c <= 13)) {
|
||||
if (c === 32 || (c >= 9 && c <= 13)) { // SP, HT, LF, VT, FF, CR
|
||||
i++;
|
||||
} else {
|
||||
break;
|
||||
@@ -27,21 +28,21 @@ function skipSpace(s: string, i: number): number {
|
||||
}
|
||||
|
||||
function charToDigitBase(c: number, base: number): number {
|
||||
if (c >= 48 && c <= 57) {
|
||||
if (c >= 48 && c <= 57) { // '0'..'9'
|
||||
const v = c - 48;
|
||||
if (v < base) {
|
||||
return v;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
} else if (c >= 65 && c <= 90) {
|
||||
} else if (c >= 65 && c <= 90) { // 'A'..'Z'
|
||||
const v = 10 + (c - 65);
|
||||
if (v < base) {
|
||||
return v;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
} else if (c >= 97 && c <= 122) {
|
||||
} else if (c >= 97 && c <= 122) { // 'a'..'z'
|
||||
const v = 10 + (c - 97);
|
||||
if (v < base) {
|
||||
return v;
|
||||
@@ -65,10 +66,10 @@ function parseIntWithBase(
|
||||
}
|
||||
|
||||
let sign = 1;
|
||||
if (s.charCodeAt(i) === 45) {
|
||||
if (s.charCodeAt(i) === 45) { // '-'
|
||||
sign = -1;
|
||||
i++;
|
||||
} else if (s.charCodeAt(i) === 43) {
|
||||
} else if (s.charCodeAt(i) === 43) { // '+'
|
||||
i++;
|
||||
}
|
||||
|
||||
@@ -81,9 +82,9 @@ function parseIntWithBase(
|
||||
|
||||
while (i < n) {
|
||||
const c = s.charCodeAt(i);
|
||||
const isAlnum = (c >= 48 && c <= 57) ||
|
||||
(c >= 65 && c <= 90) ||
|
||||
(c >= 97 && c <= 122);
|
||||
const isAlnum = (c >= 48 && c <= 57) || // '0'..'9'
|
||||
(c >= 65 && c <= 90) || // 'A'..'Z'
|
||||
(c >= 97 && c <= 122); // 'a'..'z'
|
||||
|
||||
if (!isAlnum) {
|
||||
break;
|
||||
@@ -135,18 +136,18 @@ function parseInt(s: string): { ok: boolean; value: number } {
|
||||
let any = false;
|
||||
|
||||
// hex?
|
||||
if (s.charCodeAt(i) === 48 && i + 1 < n) {
|
||||
if (s.charCodeAt(i) === 48 && i + 1 < n) { // '0'
|
||||
const x = s.charCodeAt(i + 1);
|
||||
if (x === 120 || x === 88) {
|
||||
if (x === 120 || x === 88) { // 'x' or 'X'
|
||||
i += 2;
|
||||
while (i < n) {
|
||||
const c = s.charCodeAt(i);
|
||||
let d = -1;
|
||||
if (c >= 48 && c <= 57) {
|
||||
d = c - 48;
|
||||
} else if (c >= 65 && c <= 70) {
|
||||
if (c >= 48 && c <= 57) { // '0'..'9'
|
||||
d = c - 48; // '0'
|
||||
} else if (c >= 65 && c <= 70) { // 'A'..'F'
|
||||
d = 10 + (c - 65);
|
||||
} else if (c >= 97 && c <= 102) {
|
||||
} else if (c >= 97 && c <= 102) { // 'a'..'f'
|
||||
d = 10 + (c - 97);
|
||||
} else {
|
||||
d = -1;
|
||||
@@ -170,7 +171,7 @@ function parseInt(s: string): { ok: boolean; value: number } {
|
||||
// decimal integer
|
||||
while (i < n) {
|
||||
const c = s.charCodeAt(i);
|
||||
if (c < 48 || c > 57) {
|
||||
if (c < 48 || c > 57) { // not '0'..'9'
|
||||
break;
|
||||
}
|
||||
acc = acc * 10 + (c - 48);
|
||||
@@ -196,10 +197,10 @@ function parseDecFloat(s: string): { ok: boolean; value: number } {
|
||||
|
||||
let sign = 1;
|
||||
const c0 = s.charCodeAt(i);
|
||||
if (c0 === 45) {
|
||||
if (c0 === 45) { // '-'
|
||||
sign = -1;
|
||||
i++;
|
||||
} else if (c0 === 43) {
|
||||
} else if (c0 === 43) { // '+'
|
||||
i++;
|
||||
}
|
||||
|
||||
@@ -210,7 +211,7 @@ function parseDecFloat(s: string): { ok: boolean; value: number } {
|
||||
// integer part
|
||||
while (i < n) {
|
||||
const c = s.charCodeAt(i);
|
||||
if (c < 48 || c > 57) {
|
||||
if (c < 48 || c > 57) { // not '0'..'9'
|
||||
break;
|
||||
}
|
||||
val = val * 10 + (c - 48);
|
||||
@@ -220,12 +221,12 @@ function parseDecFloat(s: string): { ok: boolean; value: number } {
|
||||
|
||||
// fractional part
|
||||
if (i < n) {
|
||||
if (s.charCodeAt(i) === 46) {
|
||||
if (s.charCodeAt(i) === 46) { // '.'
|
||||
i++;
|
||||
let scale = 1;
|
||||
while (i < n) {
|
||||
const c = s.charCodeAt(i);
|
||||
if (c < 48 || c > 57) {
|
||||
if (c < 48 || c > 57) { // not '0'..'9'
|
||||
break;
|
||||
}
|
||||
scale *= 0.1;
|
||||
@@ -241,7 +242,7 @@ function parseDecFloat(s: string): { ok: boolean; value: number } {
|
||||
let hasExp = false;
|
||||
if (i < n) {
|
||||
const ec = s.charCodeAt(i);
|
||||
if (ec === 101 || ec === 69) {
|
||||
if (ec === 101 || ec === 69) { // 'e' or 'E'
|
||||
hasExp = true;
|
||||
i++;
|
||||
if (i >= n) {
|
||||
@@ -250,10 +251,10 @@ function parseDecFloat(s: string): { ok: boolean; value: number } {
|
||||
let expSign = 1;
|
||||
if (i < n) {
|
||||
const es = s.charCodeAt(i);
|
||||
if (es === 45) {
|
||||
if (es === 45) { // '-'
|
||||
expSign = -1;
|
||||
i++;
|
||||
} else if (es === 43) {
|
||||
} else if (es === 43) { // '+'
|
||||
i++;
|
||||
}
|
||||
}
|
||||
@@ -263,7 +264,7 @@ function parseDecFloat(s: string): { ok: boolean; value: number } {
|
||||
let anyExp = false;
|
||||
while (i < n) {
|
||||
const c = s.charCodeAt(i);
|
||||
if (c < 48 || c > 57) {
|
||||
if (c < 48 || c > 57) { // not '0'..'9'
|
||||
break;
|
||||
}
|
||||
exp = exp * 10 + (c - 48);
|
||||
@@ -300,18 +301,18 @@ function parseHexFloat(s: string): { ok: boolean; value: number } {
|
||||
|
||||
let sign = 1;
|
||||
const c0 = s.charCodeAt(i);
|
||||
if (c0 === 45) {
|
||||
if (c0 === 45) { // '-'
|
||||
sign = -1;
|
||||
i++;
|
||||
} else if (c0 === 43) {
|
||||
} else if (c0 === 43) { // '+'
|
||||
i++;
|
||||
}
|
||||
|
||||
if (!(i + 1 < n && s.charCodeAt(i) === 48)) {
|
||||
if (!(i + 1 < n && s.charCodeAt(i) === 48)) { // '0'
|
||||
return { ok: false, value: 0 };
|
||||
}
|
||||
const x = s.charCodeAt(i + 1);
|
||||
if (!(x === 120 || x === 88)) {
|
||||
if (!(x === 120 || x === 88)) { // 'x' or 'X'
|
||||
return { ok: false, value: 0 };
|
||||
}
|
||||
i += 2;
|
||||
@@ -325,11 +326,11 @@ function parseHexFloat(s: string): { ok: boolean; value: number } {
|
||||
while (i < n) {
|
||||
const c = s.charCodeAt(i);
|
||||
let d = -1;
|
||||
if (c >= 48 && c <= 57) {
|
||||
if (c >= 48 && c <= 57) { // '0'..'9'
|
||||
d = c - 48;
|
||||
} else if (c >= 65 && c <= 70) {
|
||||
} else if (c >= 65 && c <= 70) { // 'A'..'F'
|
||||
d = 10 + (c - 65);
|
||||
} else if (c >= 97 && c <= 102) {
|
||||
} else if (c >= 97 && c <= 102) { // 'a'..'f'
|
||||
d = 10 + (c - 97);
|
||||
} else {
|
||||
d = -1;
|
||||
@@ -344,16 +345,16 @@ function parseHexFloat(s: string): { ok: boolean; value: number } {
|
||||
|
||||
// optional fractional part
|
||||
if (i < n) {
|
||||
if (s.charCodeAt(i) === 46) {
|
||||
if (s.charCodeAt(i) === 46) { // '.'
|
||||
i++;
|
||||
while (i < n) {
|
||||
const c = s.charCodeAt(i);
|
||||
let d = -1;
|
||||
if (c >= 48 && c <= 57) {
|
||||
if (c >= 48 && c <= 57) { // '0'..'9'
|
||||
d = c - 48;
|
||||
} else if (c >= 65 && c <= 70) {
|
||||
} else if (c >= 65 && c <= 70) { // 'A'..'F'
|
||||
d = 10 + (c - 65);
|
||||
} else if (c >= 97 && c <= 102) {
|
||||
} else if (c >= 97 && c <= 102) { // 'a'..'f'
|
||||
d = 10 + (c - 97);
|
||||
} else {
|
||||
d = -1;
|
||||
@@ -374,7 +375,7 @@ function parseHexFloat(s: string): { ok: boolean; value: number } {
|
||||
return { ok: false, value: 0 };
|
||||
}
|
||||
const ec = s.charCodeAt(i);
|
||||
if (!(ec === 112 || ec === 80)) {
|
||||
if (!(ec === 112 || ec === 80)) { // 'p' or 'P'
|
||||
return { ok: false, value: 0 };
|
||||
}
|
||||
i++;
|
||||
@@ -382,10 +383,10 @@ function parseHexFloat(s: string): { ok: boolean; value: number } {
|
||||
let expSign = 1;
|
||||
if (i < n) {
|
||||
const sc = s.charCodeAt(i);
|
||||
if (sc === 45) {
|
||||
if (sc === 45) { // '-'
|
||||
expSign = -1;
|
||||
i++;
|
||||
} else if (sc === 43) {
|
||||
} else if (sc === 43) { // '+'
|
||||
i++;
|
||||
}
|
||||
}
|
||||
@@ -397,7 +398,7 @@ function parseHexFloat(s: string): { ok: boolean; value: number } {
|
||||
let exp = 0;
|
||||
while (i < n) {
|
||||
const c = s.charCodeAt(i);
|
||||
if (c < 48 || c > 57) {
|
||||
if (c < 48 || c > 57) { // not '0'..'9'
|
||||
break;
|
||||
}
|
||||
exp = exp * 10 + (c - 48);
|
||||
@@ -418,15 +419,12 @@ function parseHexFloat(s: string): { ok: boolean; value: number } {
|
||||
return { ok: true, value: result };
|
||||
}
|
||||
|
||||
export function luaToNumber(s: string, base?: number): number | null {
|
||||
if (typeof s === "number") {
|
||||
// No conversion required
|
||||
return s;
|
||||
}
|
||||
if (typeof s !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Detailed string conversion that returns numeric type, returns null
|
||||
// on failure, integer -0 is canonicalized to +0
|
||||
export function luaToNumberDetailed(
|
||||
s: string,
|
||||
base?: number,
|
||||
): { value: number; numericType: NumericType } | null {
|
||||
if (base !== undefined) {
|
||||
if (!(typeof base === "number" && base >= 2 && base <= 36)) {
|
||||
return null;
|
||||
@@ -434,7 +432,8 @@ export function luaToNumber(s: string, base?: number): number | null {
|
||||
|
||||
const parsed = parseIntWithBase(s, base);
|
||||
if (parsed.ok) {
|
||||
return parsed.value;
|
||||
const v = parsed.value;
|
||||
return { value: v === 0 ? 0 : v, numericType: "int" };
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@@ -443,23 +442,39 @@ export function luaToNumber(s: string, base?: number): number | null {
|
||||
{
|
||||
const parsed = parseInt(s);
|
||||
if (parsed.ok) {
|
||||
return parsed.value;
|
||||
const v = parsed.value;
|
||||
return { value: v === 0 ? 0 : v, numericType: "int" };
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const parsed = parseHexFloat(s);
|
||||
if (parsed.ok) {
|
||||
return parsed.value;
|
||||
return { value: parsed.value, numericType: "float" };
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const parsed = parseDecFloat(s);
|
||||
if (parsed.ok) {
|
||||
return parsed.value;
|
||||
return { value: parsed.value, numericType: "float" };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Space Lua `tonumber` returning only the numeric value or null
|
||||
export function luaToNumber(s: unknown, base?: number): number | null {
|
||||
if (typeof s === "number") {
|
||||
return s;
|
||||
}
|
||||
if (s instanceof Number) {
|
||||
return Number(s);
|
||||
}
|
||||
if (typeof s !== "string") {
|
||||
return null;
|
||||
}
|
||||
const detailed = luaToNumberDetailed(s, base);
|
||||
return detailed ? detailed.value : null;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export * as mq from "./syscalls/mq.ts";
|
||||
export * as datastore from "./syscalls/datastore.ts";
|
||||
export * as jsonschema from "./syscalls/jsonschema.ts";
|
||||
export * as lua from "./syscalls/lua.ts";
|
||||
export * as config from "./syscalls/config.ts"
|
||||
export * as config from "./syscalls/config.ts";
|
||||
|
||||
// Not technically syscalls, but we want to export them for convenience
|
||||
export * as index from "./syscalls/index.ts";
|
||||
|
||||
@@ -11,11 +11,11 @@ export async function deletePage() {
|
||||
// Query for last
|
||||
const recentlyOpenedPages = await editor.getRecentlyOpenedPages();
|
||||
const allPages = await space.listPages();
|
||||
const existingPageNames = new Set(allPages.map(p => p.name));
|
||||
const existingPageNames = new Set(allPages.map((p) => p.name));
|
||||
|
||||
// Find the first recently opened page that still exists and is not the current page
|
||||
const firstRecentlyOpenedPage = recentlyOpenedPages.find(
|
||||
(page) => page.name !== pageName && existingPageNames.has(page.name)
|
||||
(page) => page.name !== pageName && existingPageNames.has(page.name),
|
||||
);
|
||||
await space.deletePage(pageName);
|
||||
console.log("Navigating to previous page");
|
||||
|
||||
Reference in New Issue
Block a user