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:
Matouš Jan Fialka
2025-10-16 08:41:47 +02:00
committed by GitHub
parent cb4f392c05
commit 27339c98f6
7 changed files with 1253 additions and 279 deletions
+530 -72
View File
@@ -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
View File
@@ -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;
}
+136
View File
@@ -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
View File
@@ -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;
}
+68 -53
View File
@@ -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;
}
+1 -1
View File
@@ -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";
+2 -2
View File
@@ -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");