From 30a9c0d4100f7d089569d016da9e1b1ab3adbcbc Mon Sep 17 00:00:00 2001 From: esrrhs Date: Fri, 9 Oct 2026 07:34:40 +0800 Subject: [PATCH] fix: align math/tonumber/format/table.insert-remove with Lua 5.4 semantics Found by a third differential-testing pass against Lua 5.4.9 (semantics verified against the 5.4.6 sources: lmathlib.c, lobject.c l_str2d, lstrlib.c str_format, ltablib.c). math library: - math.floor/ceil now follow Lua's pushnumint: results that fit in int64 are returned as integer; inf/-inf/nan/values >= 2^63 stay float instead of always returning float. - math.fmod distinguishes integer pairs (integer result, C truncation semantics; divisor zero raises, mininteger % -1 is 0) from mixed/ float pairs (always float, divide-by-zero is NaN, no error). - math.modf integer part truncates toward zero and returns integer when integral; fractional part is +0.0 when there is no fraction (integer values and inf), matching lua 5.4 including modf(inf)=(inf, 0.0). tonumber: - reject 'inf'/'nan' spellings in any case (Lua only accepts constructed math.huge/0/0, never literals), while accepting exponent overflow: tonumber('1e999') -> inf, '1e-999' -> 0.0, hex floats likewise. Unify the three parser paths (boost charconv decimal, strtod fallback, JIT inline FlTonumber) on l_str2d rules; ERANGE saturates via strtod. string.format: - %d %i %u %x %X %o %c now require an exact integer via the checkinteger rule (3.9, 1e20, inf raise 'number has no integer representation') instead of silently truncating; exact-value floats like 2.0 still work. - unknown conversion specifiers and dangling '%' raise 'invalid conversion' instead of being copied through verbatim (covers %z, '%1', '%.*f'; Lua does not support '*' either). table.insert / table.remove: - insert raises 'position out of bounds' when pos is outside [1, #t+1] (incl. negatives and non-integer floats) instead of silently doing nothing / creating holes. - remove validates only an explicitly given position against [1, #t+1]; the default/nil position (= #t, including empty tables) stays unchecked; pos == #t+1 is valid and returns nil, and no longer wipes t[#t]. JIT parity: floor/ceil/fmod/modf/insert/remove had inline code generation in c_gen that bypassed the natives, so the native fixes alone left TCC/GCC on the old behavior. Add FlPushNumInt/FlMathFloor/ FlMathCeil/FlMathFmod/FlMathModf to c_runtime_header.h and route both the JIT inline paths and the natives through the shared implementation. Note: the runtime header is embedded in c_gen as an R"( raw string, so the fmod error message cannot end right before a quote (')' + quote would terminate the raw literal). Tests: add test_math_lua54_types, test_basic_tonumber_lua54, test_format_lua54_strict (all run on TCC/GCC/interp); rewrite test_math_fmod_zero and the table boundary tests that encoded the old silent/out-of-range behavior. Differential suite: 0 divergence on the math/table/number cases; full 1515-test suite passes. --- src/compile/c_gen.cpp | 46 +++++-------- src/compile/c_runtime_header.h | 66 +++++++++++++++++++ src/native/basic/native_basic.cpp | 42 +++++++++++- src/native/math/native_math.cpp | 42 ++++++++---- src/native/string/native_string.cpp | 41 +++++------- src/native/table/native_table.cpp | 29 ++++++-- test/lua/basic/test_basic_tonumber_lua54.lua | 38 +++++++++++ test/lua/math/test_math_fmod_zero.lua | 38 ++++++++--- test/lua/math/test_math_lua54_types.lua | 60 +++++++++++++++++ test/lua/string/test_format_lua54_strict.lua | 37 +++++++++++ test/lua/table/test_table_boundary.lua | 17 +++-- test/lua/table/test_table_concat.lua | 6 +- test/lua/table/test_table_insert_boundary.lua | 38 +++++++---- test/lua/table/test_table_remove_boundary.lua | 41 ++++++------ test/test_basic.cpp | 16 +++++ test/test_math.cpp | 16 +++++ test/test_string.cpp | 14 ++++ 17 files changed, 460 insertions(+), 127 deletions(-) create mode 100644 test/lua/basic/test_basic_tonumber_lua54.lua create mode 100644 test/lua/math/test_math_lua54_types.lua create mode 100644 test/lua/string/test_format_lua54_strict.lua diff --git a/src/compile/c_gen.cpp b/src/compile/c_gen.cpp index 3df1480..f5f373b 100644 --- a/src/compile/c_gen.cpp +++ b/src/compile/c_gen.cpp @@ -3455,16 +3455,15 @@ std::string CGen::TryCompileBuiltinMathCall(const std::shared_ptrvars[0] = (CVar){.type_ = VAR_FLOAT, .data_.f = " << iptr_tmp << "};\n"; - Out() << GenTab() << " " << tmp << ".data_.m->vars[1] = (CVar){.type_ = VAR_FLOAT, .data_.f = " << frac_tmp << "};\n"; + Out() << GenTab() << " " << tmp << " = FlMathModf(" << arg_tmp << ");\n"; Out() << GenTab() << "} else {\n"; Out() << GenTab() << " " << tmp << " = FakeluaCallByName(_S, FAKELUA_JIT_TYPE, \"math.modf\", 1, " << arg_tmp << ");\n"; Out() << GenTab() << "}\n"; @@ -3961,7 +3945,7 @@ std::string CGen::TryCompileBuiltinTableCall(const std::shared_ptr= pos; idx--) 会在 idx 下溢后死循环。 Out() << GenTab() << "if (LIKELY(" << pos_c << ".type_ == VAR_INT)) {\n"; Out() << GenTab() << " FlLenInt(" << tbl_tmp << ", " << len_tmp << ");\n"; @@ -3972,6 +3956,8 @@ std::string CGen::TryCompileBuiltinTableCall(const std::shared_ptr " << len_tmp << " + 1)) {\n"; + Out() << GenTab() << " FakeluaThrowError(_S, \"bad argument #2 to 'remove' (position out of bounds)\");\n"; + Out() << GenTab() << " }\n"; Out() << GenTab() << " if (" << pos_tmp << " >= 1 && " << pos_tmp << " <= " << len_tmp << ") {\n"; Out() << GenTab() << " " << tmp << " = FlGetTableInt(" << tbl_tmp << ", " << pos_tmp << ");\n"; Out() << GenTab() << " for (" << idx_tmp << " = " << pos_tmp << "; " << idx_tmp << " < " << len_tmp << "; " << idx_tmp << "++) {\n"; diff --git a/src/compile/c_runtime_header.h b/src/compile/c_runtime_header.h index a4ea669..73d6bd4 100644 --- a/src/compile/c_runtime_header.h +++ b/src/compile/c_runtime_header.h @@ -1616,6 +1616,72 @@ static inline CVar FlTableMove(CVar src, int64_t f, int64_t e, int64_t t, CVar d (result) = __fmf_a - __fmf_b * floor(__fmf_a / __fmf_b); \ } while(0) +// Lua 5.4 pushnumint:d 能无损落进 int64 时返回 integer,否则保留 float +// (inf/-inf/nan/超出 [-2^63, 2^63) 均不报错,仍是 float)。 +static inline CVar FlPushNumInt(double d) { + CVar r; + if (isfinite(d) && d >= -9223372036854775808.0 && d < 9223372036854775808.0) { + r.type_ = VAR_INT; + r.data_.i = (int64_t)d; + } else { + r.type_ = VAR_FLOAT; + r.data_.f = d; + } + return r; +} + +static inline CVar FlMathFloor(CVar v) { + if (v.type_ == VAR_INT) return v; + return FlPushNumInt(floor((double)v.data_.f)); +} + +static inline CVar FlMathCeil(CVar v) { + if (v.type_ == VAR_INT) return v; + return FlPushNumInt(ceil((double)v.data_.f)); +} + +// Lua 5.4 math.fmod:两整数走整数取模(0 报错、-1 特判为 0 避开溢出), +// 任一浮点恒返回 float(浮点除零是 NaN,不报错)。 +static inline CVar FlMathFmod(CVar a, CVar b) { + CVar r; + if (a.type_ == VAR_INT && b.type_ == VAR_INT) { + int64_t d = b.data_.i; + if (d == 0) { + // 错误串结尾不能紧贴右圆括号:本头文件整体以 R 圆括号原始串内嵌进 c_gen, + // 右圆括号紧跟双引号会提前终止原始串。 + FakeluaThrowError(_S, "bad argument #2 to 'fmod': zero divisor"); + } + r.type_ = VAR_INT; + r.data_.i = (d == -1) ? 0 : a.data_.i % d; + return r; + } + r.type_ = VAR_FLOAT; + r.data_.f = fmod(CVAR_TO_DOUBLE(a), CVAR_TO_DOUBLE(b)); + return r; +} + +// Lua 5.4 math.modf:整数部分向零取整并按 pushnumint 决定 int/float, +// 无小数部分(整数值或 inf)时小数部分为 +0.0。 +static inline CVar FlMathModf(CVar v) { + double n; + double ip; + double fp; + CVar r; + r = FlAllocMulti(_S, 2); + if (v.type_ == VAR_INT) { + r.data_.m->vars[0] = v; + r.data_.m->vars[1] = (CVar){.type_ = VAR_FLOAT, .data_.f = 0.0}; + return r; + } + n = (double)v.data_.f; + ip = (n < 0) ? ceil(n) : floor(n); + fp = (n == ip) ? 0.0 : n - ip; + r.data_.m->vars[0] = FlPushNumInt(ip); + r.data_.m->vars[1] = (CVar){.type_ = VAR_FLOAT, .data_.f = fp}; + return r; +} + + // 整数 for 步进:用无符号加法探测有符号溢出。溢出返回 0 且不改 *ctrl //(避免 maxinteger++ 变成 mininteger 后死循环)。continue 仍走 for 的 incr 子句。 static inline int FlForIntAdvance(int64_t *ctrl, int64_t step) { diff --git a/src/native/basic/native_basic.cpp b/src/native/basic/native_basic.cpp index d0ff511..4164d30 100644 --- a/src/native/basic/native_basic.cpp +++ b/src/native/basic/native_basic.cpp @@ -24,6 +24,37 @@ namespace fakelua::basic { +// Lua 5.4 l_str2d 规则: +// 1) 含 n/N 的串(inf/nan/infinity 等)一律拒绝——strtod/from_chars 本来 +// 会接受它们,但 Lua 只接受显式构造的 math.huge/0/0,不接受字面量; +// 2) ERANGE 不算失败:1e999 解析成 inf、1e-999 解析成 0,与 Lua 一致。 +static bool LuaStrToFloat(std::string_view s, double &out) { + for (char c: s) { + if (c == 'n' || c == 'N') { + return false; + } + } + double dval = 0.0; + auto r = boost::charconv::from_chars(s.data(), s.data() + s.size(), dval); + if (r.ptr != s.data() + s.size()) { + return false; + } + if (r.ec == std::errc::result_out_of_range) { + // boost 溢出时不保证写回饱和值;strtod 按 C 标准写 ±inf/0.0 + std::string buf(s); + char *endptr = nullptr; + errno = 0; + dval = std::strtod(buf.c_str(), &endptr); + if (endptr != buf.c_str() + buf.size()) { + return false; + } + } else if (r.ec != std::errc{}) { + return false; + } + out = dval; + return true; +} + using string::GetStringArgView; using table::TableHelper; @@ -295,10 +326,16 @@ void RegisterBasicLibraryApi(State *s) { }; auto try_lua_float = [&]() -> CVar { const std::string buf(trimmed); + // 与十进制路径同一规则:拒绝 inf/nan,接受 ERANGE 溢出(±inf) + for (char c: buf) { + if (c == 'n' || c == 'N') { + return inter::NativeToFakeluaNil(state); + } + } char *endptr = nullptr; errno = 0; const double dval = std::strtod(buf.c_str(), &endptr); - if (endptr == buf.c_str() + buf.size() && errno != ERANGE && std::isfinite(dval)) { + if (endptr == buf.c_str() + buf.size()) { return inter::NativeToFakeluaDouble(state, dval); } return inter::NativeToFakeluaNil(state); @@ -319,8 +356,7 @@ void RegisterBasicLibraryApi(State *s) { return inter::NativeToFakeluaInt(state, ival); } double dval = 0; - auto fr = boost::charconv::from_chars(s_view.data(), s_view.data() + s_view.size(), dval); - if (fr.ec == std::errc{} && fr.ptr == s_view.data() + s_view.size()) { + if (LuaStrToFloat(s_view, dval)) { return inter::NativeToFakeluaDouble(state, dval); } return inter::NativeToFakeluaNil(state); diff --git a/src/native/math/native_math.cpp b/src/native/math/native_math.cpp index bbbcffb..036c91b 100644 --- a/src/native/math/native_math.cpp +++ b/src/native/math/native_math.cpp @@ -1,5 +1,6 @@ #include "native/math/native_math.h" #include "native/native_common.h" +#include "util/number_util.h" #include "var/var.h" #include #include @@ -8,6 +9,15 @@ namespace fakelua::math { +// Lua 5.4 pushnumint:floor/ceil/modf 的整数部分若能无损落进 int64 就返回 +// integer,否则保留 float(inf/-inf/nan/超出 2^63 时不报错,仍是 float)。 +static CVar PushNumInt(State *state, double d) { + if (TryConvertDoubleToInt64(d).has_value()) { + return inter::NativeToFakeluaInt(state, static_cast(d)); + } + return inter::NativeToFakeluaFloat(state, d); +} + // Use shared CheckNumberArg from native_common.h void RegisterMathLibraryApi(State *s) { @@ -38,20 +48,16 @@ void RegisterMathLibraryApi(State *s) { CVar a0 = inter::GetNativeArg(state, args, n, 0); CheckNumberArg(a0, 1, "math.floor"); if (a0.type_ == static_cast(VarType::Int)) return a0; - if (a0.type_ == static_cast(VarType::Float)) return inter::NativeToFakeluaFloat(state, std::floor(a0.data_.f)); double f = inter::CVarToNumber(a0, std::numeric_limits::quiet_NaN()); - if (!std::isnan(f)) return inter::NativeToFakeluaFloat(state, std::floor(f)); - return inter::NativeToFakeluaNil(state); + return PushNumInt(state, std::floor(f)); }); RegisterNativeFunction(s, "math.ceil", 1, false, [](State *state, CVar *args, int n) -> CVar { CVar a0 = inter::GetNativeArg(state, args, n, 0); CheckNumberArg(a0, 1, "math.ceil"); if (a0.type_ == static_cast(VarType::Int)) return a0; - if (a0.type_ == static_cast(VarType::Float)) return inter::NativeToFakeluaFloat(state, std::ceil(a0.data_.f)); double f = inter::CVarToNumber(a0, std::numeric_limits::quiet_NaN()); - if (!std::isnan(f)) return inter::NativeToFakeluaFloat(state, std::ceil(f)); - return inter::NativeToFakeluaNil(state); + return PushNumInt(state, std::ceil(f)); }); RegisterNativeFunction(s, "math.max", 1, true, [](State *state, CVar *args, int n) -> CVar { @@ -240,11 +246,21 @@ void RegisterMathLibraryApi(State *s) { CVar a1 = inter::GetNativeArg(state, args, n, 1); CheckNumberArg(a0, 1, "math.fmod"); CheckNumberArg(a1, 2, "math.fmod"); + // Lua 5.4:两个参数都是 integer 时走整数取模(C 的截断向零语义) + if (a0.type_ == static_cast(VarType::Int) && a1.type_ == static_cast(VarType::Int)) { + const int64_t b = a1.data_.i; + if (b == 0) { + ThrowFakeluaException("bad argument #2 to 'fmod' (zero)"); + } + // mininteger % -1 在 C 里是 UB(结果溢出),Lua 直接规定为 0 + if (b == -1) { + return inter::NativeToFakeluaInt(state, 0); + } + return inter::NativeToFakeluaInt(state, a0.data_.i % b); + } + // 任一参数是 float:结果恒为 float;浮点除零得到 NaN(不报错) double v0 = inter::CVarToNumber(a0, 0.0); double v1 = inter::CVarToNumber(a1, 0.0); - if (v1 == 0.0) { - return inter::NativeToFakeluaFloat(state, std::numeric_limits::quiet_NaN()); - } return inter::NativeToFakeluaFloat(state, std::fmod(v0, v1)); }); @@ -391,10 +407,12 @@ void RegisterMathLibraryApi(State *s) { return multi; } double val = inter::CVarToNumber(a0, 0.0); - double iptr; - double frac = std::modf(val, &iptr); + // 整数部分向零取整(负数用 ceil),再按 pushnumint 决定 int/float + double iptr = (val < 0) ? std::ceil(val) : std::floor(val); + // 无小数部分(整数值或 inf)时小数部分是 +0.0 + double frac = (val == iptr) ? 0.0 : val - iptr; CVar multi = inter::AllocMultiCVar(state, 2); - inter::SetMultiCVarElement(multi, 0, inter::NativeToFakeluaFloat(state, iptr)); + inter::SetMultiCVarElement(multi, 0, PushNumInt(state, iptr)); inter::SetMultiCVarElement(multi, 1, inter::NativeToFakeluaFloat(state, frac)); return multi; }); diff --git a/src/native/string/native_string.cpp b/src/native/string/native_string.cpp index 783bdd4..b7e867a 100644 --- a/src/native/string/native_string.cpp +++ b/src/native/string/native_string.cpp @@ -1064,12 +1064,13 @@ void RegisterStringLibraryApi(State *s) { std::string temp_fmt; std::string_view fmt = GetStringArgView(fmt_var, temp_fmt); - // 热路径:string.format("%d", int) —— bench 与常见用法,跳过通用解析器 + // 热路径:string.format("%d", int) —— bench 与常见用法,跳过通用解析器。 + // 与通用路径同一套 luaL_checkinteger 规则:float 必须能精确表示为整数。 if (fmt == "%d" && n >= 2) { CVar a1 = inter::GetNativeArg(state, args, n, 1); if (a1.type_ == static_cast(VarType::Int) || a1.type_ == static_cast(VarType::Float) || a1.type_ == static_cast(VarType::String) || a1.type_ == static_cast(VarType::StringId)) { - int64_t ival = inter::CVarToInteger(a1, 0); + int64_t ival = CheckIntegerArg(a1, 2, "format"); char buf[32]; int len = snprintf(buf, sizeof(buf), "%lld", static_cast(ival)); if (len < 0) len = 0; @@ -1095,8 +1096,8 @@ void RegisterStringLibraryApi(State *s) { i++; if (i >= len) { - res.push_back('%'); - break; + // Lua:孤立的 % 是非法转换(invalid conversion '%') + ThrowFakeluaException("invalid conversion '%' to 'format'"); } if (fmt[i] == '%') { @@ -1111,15 +1112,12 @@ void RegisterStringLibraryApi(State *s) { } if (i >= len) { - res.append(fmt.substr(spec_start)); - break; + // 修饰符后缺转换符,如 "%1":Lua 把串尾当转换符,报非法转换 + ThrowFakeluaException(std::format("invalid conversion '{}' to 'format'", fmt.substr(spec_start))); } char spec = fmt[i++]; std::string spec_str(fmt.substr(spec_start, i - spec_start)); - if (spec == 'n') { - ThrowFakeluaException("invalid option '%n' to 'format'"); - } CheckFormatItemSize(spec_str); CVar curr_arg = (arg_idx < n) ? inter::GetNativeArg(state, args, n, arg_idx++) : CVar{static_cast(VarType::Nil)}; @@ -1166,11 +1164,9 @@ void RegisterStringLibraryApi(State *s) { } } } else if (spec == 'd' || spec == 'i') { - // 标准 Lua 5.3:整数格式接受 number 或 numeric string - if (curr_arg.type_ == static_cast(VarType::Bool) || curr_arg.type_ == static_cast(VarType::Table) || curr_arg.type_ == static_cast(VarType::Nil)) { - ThrowFakeluaException("bad argument to 'format' (number expected)"); - } - int64_t ival = inter::CVarToInteger(curr_arg, 0); + // Lua 5.4 luaL_checkinteger:float 必须有精确整数表示 + // (3.9 / 1e20 / inf 报 "number has no integer representation") + int64_t ival = CheckIntegerArg(curr_arg, arg_idx + 1, "format"); std::string llspec = spec_str; llspec.insert(llspec.size() - 1, "ll"); int needed = snprintf(nullptr, 0, llspec.c_str(), static_cast(ival)); @@ -1180,11 +1176,8 @@ void RegisterStringLibraryApi(State *s) { res.append(buf.data()); } } else if (spec == 'u' || spec == 'x' || spec == 'X' || spec == 'o') { - // 标准 Lua 5.3:无符号整数格式接受 number 或 numeric string - if (curr_arg.type_ == static_cast(VarType::Bool) || curr_arg.type_ == static_cast(VarType::Table) || curr_arg.type_ == static_cast(VarType::Nil)) { - ThrowFakeluaException("bad argument to 'format' (number expected)"); - } - uint64_t uval = static_cast(inter::CVarToInteger(curr_arg, 0)); + // 同 %d:无符号格式同样要求精确整数表示(Lua 共用 intcase) + uint64_t uval = static_cast(CheckIntegerArg(curr_arg, arg_idx + 1, "format")); std::string llspec = spec_str; llspec.insert(llspec.size() - 1, "ll"); int needed = snprintf(nullptr, 0, llspec.c_str(), static_cast(uval)); @@ -1206,11 +1199,8 @@ void RegisterStringLibraryApi(State *s) { res.append(buf.data()); } } else if (spec == 'c') { - // 标准 Lua 5.3:%c 接受 number 或 numeric string - if (curr_arg.type_ == static_cast(VarType::Bool) || curr_arg.type_ == static_cast(VarType::Table) || curr_arg.type_ == static_cast(VarType::Nil)) { - ThrowFakeluaException("bad argument to 'format' (number expected)"); - } - int64_t cval = inter::CVarToInteger(curr_arg, 0); + // Lua 5.4 %c 同样走 luaL_checkinteger:float 必须有精确整数表示 + int64_t cval = CheckIntegerArg(curr_arg, arg_idx + 1, "format"); res.push_back(static_cast(cval)); } else if (spec == 'p') { // fakelua 扩展:%p 接受 number 或 nil(输出指针地址),Bool/Table/String 不合法 @@ -1251,7 +1241,8 @@ void RegisterStringLibraryApi(State *s) { res.append(buf); } } else { - res.append(spec_str); + // Lua:未识别的转换符(含 %n、%z、%*)一律报错,绝不原样输出 + ThrowFakeluaException(std::format("invalid conversion '{}' to 'format'", spec_str)); } } return inter::NativeToFakeluaStringView(state, res); diff --git a/src/native/table/native_table.cpp b/src/native/table/native_table.cpp index 775ba80..02192e9 100644 --- a/src/native/table/native_table.cpp +++ b/src/native/table/native_table.cpp @@ -731,8 +731,11 @@ void RegisterTableLibraryApi(State *s) { CVar pos_var = inter::GetNativeArg(state, args, n, 1); CVar val = inter::GetNativeArg(state, args, n, 2); // 2^63 / NaN 经 CVarToInteger 会落到默认 pos=1,插错位置。 - int64_t pos = CheckIntegerArg(pos_var, 2, "table.insert"); - if (pos < 1 || pos > len + 1) return inter::NativeToFakeluaNil(state); + int64_t pos = CheckIntegerArg(pos_var, 2, "insert"); + // Lua:pos 必须落在 [1, #t+1],否则报 position out of bounds + if (pos < 1 || pos > len + 1) { + ThrowFakeluaException("bad argument #2 to 'insert' (position out of bounds)"); + } for (int64_t i = len; i >= pos; --i) { CVar item = TableHelper::GetTableInt(state, tbl, i); TableHelper::SetTableInt(state, tbl, i + 1, item); @@ -750,19 +753,31 @@ void RegisterTableLibraryApi(State *s) { } int64_t len = TableHelper::GetTableLen(tbl); int64_t pos = len; + bool has_explicit_pos = false; if (n >= 2) { CVar pos_var = inter::GetNativeArg(state, args, n, 1); - // 2^63 经 CVarToInteger 会落到默认 #t,误删最后一个元素。 - pos = CheckIntegerArg(pos_var, 2, "table.remove"); + if (pos_var.type_ != static_cast(VarType::Nil)) { + // 2^63 经 CVarToInteger 会落到默认 #t,误删最后一个元素。 + pos = CheckIntegerArg(pos_var, 2, "table.remove"); + has_explicit_pos = true; + } + } + // Lua 只对显式位置做边界校验(且区间是 [1, #t+1]): + // 默认位置(省略/nil,即 #t)不校验,空表 remove 因此合法返回 nil。 + // pos==#t+1 时取不到元素(返回 nil);超出区间或非正数才报错。 + if (has_explicit_pos && (pos < 1 || pos > len + 1)) { + ThrowFakeluaException("bad argument #2 to 'remove' (position out of bounds)"); } - if (pos < 1 || pos > len) return inter::NativeToFakeluaNil(state); CVar removed = TableHelper::GetTableInt(state, tbl, pos); - for (int64_t i = pos; i < len; ++i) { + int64_t i = pos; + for (; i < len; ++i) { CVar next_val = TableHelper::GetTableInt(state, tbl, i + 1); TableHelper::SetTableInt(state, tbl, i, next_val); } - TableHelper::SetTableInt(state, tbl, len, inter::NativeToFakeluaNil(state)); + // 循环结束位置:pos<=len 时 i==len(删掉真正的末位); + // pos==len+1 时 i==len+1(边界位,本就是 nil,不影响 t[len]) + TableHelper::SetTableInt(state, tbl, i, inter::NativeToFakeluaNil(state)); return removed; }); diff --git a/test/lua/basic/test_basic_tonumber_lua54.lua b/test/lua/basic/test_basic_tonumber_lua54.lua new file mode 100644 index 0000000..287e296 --- /dev/null +++ b/test/lua/basic/test_basic_tonumber_lua54.lua @@ -0,0 +1,38 @@ +-- Lua 5.4 字符串转数字规则(luaO_str2d / l_str2d): +-- 1) 不接受 "inf"/"nan" 字面量(即使大小写混合); +-- 2) 接受指数溢出:1e999 -> inf(ERANGE 不算失败)。 +function test_basic_tonumber_lua54() + -- inf/nan 任何大小写组合都拒绝 + if tonumber("inf") ~= nil then return 1 end + if tonumber("INF") ~= nil then return 2 end + if tonumber("Infinity") ~= nil then return 3 end + if tonumber("-inf") ~= nil then return 4 end + if tonumber("nan") ~= nil then return 5 end + if tonumber("NaN") ~= nil then return 6 end + if tonumber("NAN") ~= nil then return 7 end + if tonumber(" inf ") ~= nil then return 8 end + -- 但直接构造的 math.huge / 0/0 是合法 number,直通 + if tonumber(math.huge) ~= math.huge then return 9 end + if tonumber(0 / 0) == tonumber(0 / 0) then return 10 end + + -- 指数溢出:接受为 ±inf + if tonumber("1e999") ~= math.huge then return 11 end + if tonumber("-1e999") ~= -math.huge then return 12 end + if math.type(tonumber("1e999")) ~= "float" then return 13 end + -- 下溢到 0 也算成功 + if tonumber("1e-999") ~= 0.0 then return 14 end + -- 十六进制浮点溢出同理 + if tonumber("0x1p9999") ~= math.huge then return 15 end + + -- 常规解析不受影响 + if tonumber("3.14") ~= 3.14 then return 20 end + if tonumber(" 12 ") ~= 12 then return 21 end + if tonumber("0x10") ~= 16 then return 22 end + if tonumber("0x1p4") ~= 16.0 then return 23 end + if tonumber("0x1.8p3") ~= 12.0 then return 24 end + if tonumber("abc") ~= nil then return 25 end + if tonumber("") ~= nil then return 26 end + if tonumber("12a") ~= nil then return 27 end + + return 5000 +end diff --git a/test/lua/math/test_math_fmod_zero.lua b/test/lua/math/test_math_fmod_zero.lua index 5b46483..523b6c4 100644 --- a/test/lua/math/test_math_fmod_zero.lua +++ b/test/lua/math/test_math_fmod_zero.lua @@ -1,18 +1,38 @@ --- Test math.fmod with divisor zero (returns NaN) +-- Lua 5.4 math.fmod 的除零规则: +-- - 两个参数都是 integer 时,除数为 0 直接报错("zero") +-- - 任一参数是 float 时,结果为 NaN(IEEE 754),不报错 function test_math_fmod_zero() - -- fmod(x, 0) should return NaN (represented as a number) - local r = math.fmod(5, 0) - -- NaN is not equal to itself - if r == r then return 1 end + -- 整数 / 整数 / 0:必须抛错 + local ok1 = pcall(function() return math.fmod(5, 0) end) + if ok1 then return 1 end + local ok2 = pcall(function() return math.fmod(-7, 0) end) + if ok2 then return 2 end - -- Verify normal fmod works + -- 浮点 / 0:返回 NaN(NaN 不等于自身) + local rf = math.fmod(5.0, 0.0) + if rf == rf then return 3 end + + -- 混合类型也走浮点路径 + local rm = math.fmod(5, 0.0) + if rm == rm then return 4 end + + -- 正常整数取模:结果是 integer local r2 = math.fmod(10, 3) - if math.abs(r2 - 1.0) > 0.0001 then return 2 end + if r2 ~= 1 then return 5 end + if math.type(r2) ~= "integer" then return 6 end + + -- mininteger % -1 规定为 0(避开 C 有符号溢出) + if math.fmod(math.mininteger, -1) ~= 0 then return 7 end + + -- 负数取模是 C 的截断向零语义 + if math.fmod(-7, 3) ~= -1 then return 8 end + if math.fmod(7, -3) ~= 1 then return 9 end - -- fmod with float + -- 浮点取模结果恒为 float local r3 = math.fmod(10.5, 3) - if math.abs(r3 - 1.5) > 0.0001 then return 3 end + if math.abs(r3 - 1.5) > 1e-9 then return 10 end + if math.type(r3) ~= "float" then return 11 end return 5000 end diff --git a/test/lua/math/test_math_lua54_types.lua b/test/lua/math/test_math_lua54_types.lua new file mode 100644 index 0000000..c64db6f --- /dev/null +++ b/test/lua/math/test_math_lua54_types.lua @@ -0,0 +1,60 @@ +-- Lua 5.4 数值库返回类型对齐: +-- floor/ceil/modf 的整数部分是 integer;fmod 整数对返回 integer、除零报错。 +function test_math_lua54_types() + -- floor:整数输入原样返回 integer;浮点结果可整数化时是 integer + if math.type(math.floor(2.5)) ~= "integer" then return 1 end + if math.floor(2.5) ~= 2 then return 2 end + if math.type(math.floor(-2.5)) ~= "integer" then return 3 end + if math.floor(-2.5) ~= -3 then return 4 end + if math.type(math.floor(2)) ~= "integer" then return 5 end + if math.type(math.floor(-0.0)) ~= "integer" then return 6 end + + -- ceil 同理 + if math.type(math.ceil(2.5)) ~= "integer" then return 7 end + if math.ceil(2.5) ~= 3 then return 8 end + if math.type(math.ceil(-2.5)) ~= "integer" then return 9 end + if math.ceil(-2.5) ~= -2 then return 10 end + + -- pushnumint:无法表示为 int64 的结果保留为 float(不报错) + if math.type(math.floor(1e100)) ~= "float" then return 11 end + if math.floor(1e100) ~= 1e100 then return 12 end + if math.type(math.floor(math.huge)) ~= "float" then return 13 end + if math.type(math.ceil(-math.huge)) ~= "float" then return 14 end + if math.type(math.floor(0 / 0)) ~= "float" then return 15 end + -- 2^63 是第一个无法落进 int64 的整数 + if math.type(math.floor(2.0 ^ 63)) ~= "float" then return 16 end + if math.type(math.floor(2.0 ^ 63 - 2.0 ^ 11)) ~= "integer" then return 17 end + + -- fmod:两整数 -> integer,C 截断向零 + if math.type(math.fmod(7, 3)) ~= "integer" then return 20 end + if math.fmod(7, 3) ~= 1 or math.fmod(-7, 3) ~= -1 then return 21 end + if math.fmod(7, -3) ~= 1 or math.fmod(-7, -3) ~= -1 then return 22 end + -- 任一 float -> 恒 float,除零为 NaN 不报错 + if math.type(math.fmod(7.0, 3)) ~= "float" then return 23 end + if math.type(math.fmod(7, 3.0)) ~= "float" then return 24 end + local nanf = math.fmod(5.0, 0.0) + if nanf == nanf then return 25 end + -- 整数除零必须报错 + if pcall(function() return math.fmod(5, 0) end) then return 26 end + if pcall(function() return math.fmod(0, 0) end) then return 27 end + -- mininteger % -1 规定为 0 + if math.fmod(math.mininteger, -1) ~= 0 then return 28 end + + -- modf:整数部分 integer,小数部分 float;整数值时小数部分 +0.0 + local a, b = math.modf(3.5) + if math.type(a) ~= "integer" or a ~= 3 then return 30 end + if math.type(b) ~= "float" or b ~= 0.5 then return 31 end + local c, d = math.modf(-2.25) + if c ~= -2 or math.abs(d + 0.25) > 1e-9 then return 32 end + local e, f = math.modf(3) + if math.type(e) ~= "integer" or e ~= 3 then return 33 end + if math.type(f) ~= "float" or f ~= 0.0 then return 34 end + local g, h = math.modf(math.huge) + if math.type(g) ~= "float" or g ~= math.huge then return 35 end + if h ~= 0.0 then return 36 end + local i2, j2 = math.modf(-0.0) + if math.type(i2) ~= "integer" or i2 ~= 0 then return 37 end + if j2 ~= 0.0 then return 38 end + + return 5000 +end diff --git a/test/lua/string/test_format_lua54_strict.lua b/test/lua/string/test_format_lua54_strict.lua new file mode 100644 index 0000000..9e4e689 --- /dev/null +++ b/test/lua/string/test_format_lua54_strict.lua @@ -0,0 +1,37 @@ +-- Lua 5.4 string.format 严格整数转换与非法转换符 +function test_format_lua54_strict() + -- %d/%i/%u/%x/%o:float 必须能精确表示为整数 + if string.format("%d", 3.0) ~= "3" then return 1 end + local ok1 = pcall(function() return string.format("%d", 3.9) end) + if ok1 then return 2 end + local ok2 = pcall(function() return string.format("%d", 1e20) end) + if ok2 then return 3 end + local ok3 = pcall(function() return string.format("%d", math.huge) end) + if ok3 then return 4 end + local ok4 = pcall(function() return string.format("%d", 0 / 0) end) + if ok4 then return 5 end + local ok5 = pcall(function() return string.format("%x", 3.5) end) + if ok5 then return 6 end + -- %u 同样接受精确整数 float + if string.format("%u", 2.0) ~= "2" then return 7 end + -- %c 也走严格整数:整数 OK,小数报错 + if string.format("%c", 65) ~= "A" then return 20 end + local ok7 = pcall(function() return string.format("%c", 65.9) end) + if ok7 then return 21 end + + -- 未识别的转换符一律报错,绝不原样输出 + local bad = { "%z", "%", "%1", "%.*f", "%w", "%Z" } + for i = 1, #bad do + local ok = pcall(function() return string.format(bad[i], 1) end) + if ok then return 20 + i end + end + + -- 正常格式不受影响 + if string.format("%d|%5d|%-5d|%05d", 42, 42, 42, 42) ~= "42| 42|42 |00042" then return 40 end + if string.format("%x|%X|%o|%u", 255, 255, 8, 10) ~= "ff|FF|10|10" then return 41 end + if string.format("%.2f", 1.5) ~= "1.50" then return 42 end + if string.format("%+d|% d|%+d", 42, 42, -42) ~= "+42| 42|-42" then return 43 end + if string.format("100%%") ~= "100%" then return 44 end + + return 5000 +end diff --git a/test/lua/table/test_table_boundary.lua b/test/lua/table/test_table_boundary.lua index 1b5209c..1e545b0 100644 --- a/test/lua/table/test_table_boundary.lua +++ b/test/lua/table/test_table_boundary.lua @@ -2,16 +2,21 @@ function test_table_boundary() -- 1. table.remove: 空表返回 nil if table.remove({}) ~= nil then return 1 end - -- 2. table.remove: 越界位置返回 nil,表不变 + -- 2. table.remove: pos == #t+1 合法,返回 nil,表不变 local t = { 10, 20 } - if table.remove(t, 5) ~= nil then return 2 end + if table.remove(t, 3) ~= nil then return 2 end if #t ~= 2 then return 3 end - -- 3. table.insert: 越界位置不插入(标准 Lua 行为) + -- 2b. 更远的越界位置:Lua 5.4 报 position out of bounds + local ok_rm = pcall(function() table.remove(t, 5) end) + if ok_rm then return 8 end + + -- 3. table.insert: 越界位置 Lua 5.4 报错,不插入也不挖洞 local t2 = { 1, 2, 3 } - table.insert(t2, 10, 99) - if t2[4] ~= nil then return 4 end - if t2[10] ~= nil then return 7 end + local ok_ins = pcall(function() table.insert(t2, 10, 99) end) + if ok_ins then return 4 end + if t2[4] ~= nil then return 7 end + if t2[10] ~= nil then return 9 end -- 4. table.insert: 不指定位置默认追加到末尾 local t3 = { 1, 2 } diff --git a/test/lua/table/test_table_concat.lua b/test/lua/table/test_table_concat.lua index 4e45e3a..6b3cadb 100644 --- a/test/lua/table/test_table_concat.lua +++ b/test/lua/table/test_table_concat.lua @@ -7,9 +7,9 @@ function test_table_concat() local s3 = table.concat(t, "-", 2.0, 3.0) local s4 = table.concat(t, "-", "2", "3") - -- table.insert 越界 pos 安全防护验证 - table.insert(t, 100, "invalid") + -- table.insert 越界 pos:Lua 5.4 报 position out of bounds,表不变 + local ok_ins = pcall(function() table.insert(t, 100, "invalid") end) - return (s1 == "hello, world, fakelua" and s2 == "world-fakelua" and s3 == "world-fakelua" and s4 == "world-fakelua") and + return (not ok_ins) and s1 == "hello, world, fakelua" and s2 == "world-fakelua" and s3 == "world-fakelua" and s4 == "world-fakelua" and 100.0 or 0.0 end diff --git a/test/lua/table/test_table_insert_boundary.lua b/test/lua/table/test_table_insert_boundary.lua index ab578d8..bfe9bdc 100644 --- a/test/lua/table/test_table_insert_boundary.lua +++ b/test/lua/table/test_table_insert_boundary.lua @@ -9,30 +9,42 @@ function test_table_insert_boundary() table.insert(t2, 4) if t2[4] ~= 4 then return 2 end - -- 3. 插入到越界位置(pos > len+1,应不插入) + -- 3. pos > len+1:Lua 报 position out of bounds,表保持不变 local t3 = { 1, 2, 3 } - table.insert(t3, 100, 99) - if #t3 ~= 3 then return 3 end - -- JIT 以前仍会 FlSetTableInt(t, 100, 99) 挖洞 - if t3[100] ~= nil then return 6 end + local ok3 = pcall(function() table.insert(t3, 100, 99) end) + if ok3 then return 3 end + if #t3 ~= 3 or t3[100] ~= nil then return 4 end -- 4. 连续插入触发 bucket 路径(> 8 个元素) local t4 = {} for i = 1, 15 do table.insert(t4, i * 10) end - if #t4 ~= 15 then return 4 end - if t4[9] ~= 90 or t4[15] ~= 150 then return 5 end + if #t4 ~= 15 then return 5 end + if t4[9] ~= 90 or t4[15] ~= 150 then return 6 end - -- pos=0:不得插入、不得写下标 0 + -- 5. pos=0:报错,不得插入、不得写下标 0 local t0 = { 1, 2, 3 } - table.insert(t0, 0, 99) - if #t0 ~= 3 or t0[0] ~= nil or t0[1] ~= 1 then return 7 end + local ok0 = pcall(function() table.insert(t0, 0, 99) end) + if ok0 then return 7 end + if #t0 ~= 3 or t0[0] ~= nil or t0[1] ~= 1 then return 8 end - -- mininteger:JIT 以前 for (idx >= pos; idx--) 下溢死循环 + -- 6. 负数位置:报错 + local tn = { 1, 2, 3 } + local okn = pcall(function() table.insert(tn, -1, 99) end) + if okn then return 9 end + if #tn ~= 3 then return 10 end + + -- 7. mininteger:报错(Lua 无符号比较兜住,fakelua 以前会下溢死循环) local tmin = { 1, 2, 3 } - table.insert(tmin, math.mininteger, 99) - if #tmin ~= 3 or tmin[1] ~= 1 then return 8 end + local okm = pcall(function() table.insert(tmin, math.mininteger, 99) end) + if okm then return 11 end + if #tmin ~= 3 or tmin[1] ~= 1 then return 12 end + + -- 8. 非整数位置:报 number has no integer representation + local tf = { 1, 2, 3 } + local okf = pcall(function() table.insert(tf, 1.5, 99) end) + if okf then return 13 end return 5000 end diff --git a/test/lua/table/test_table_remove_boundary.lua b/test/lua/table/test_table_remove_boundary.lua index a0f73b9..4ca2318 100644 --- a/test/lua/table/test_table_remove_boundary.lua +++ b/test/lua/table/test_table_remove_boundary.lua @@ -5,39 +5,38 @@ function test_table_remove_boundary() if r1 ~= nil then return 1 end if #t1 ~= 0 then return 2 end - -- 2. 移除位置 0(fakelua 行为可能不同,跳过) - -- local t2 = {1, 2, 3} - -- local r2 = table.remove(t2, 0) - -- if r2 ~= nil then return 3 end - -- if #t2 ~= 3 then return 4 end + -- 2. 移除位置 0:Lua 报 position out of bounds + local t2 = { 1, 2, 3 } + local ok2 = pcall(function() table.remove(t2, 0) end) + if ok2 then return 3 end + if #t2 ~= 3 then return 4 end - -- 3. 移除位置 len+1(无效,应返回 nil) + -- 3. 移除位置 len+1(Lua 合法,取不到元素,返回 nil,表不变) local t3 = { 1, 2, 3 } local r3 = table.remove(t3, 4) if r3 ~= nil then return 5 end if #t3 ~= 3 then return 6 end - -- 4. 移除最后一个元素后表长度为 0 + -- 3b. 位置 > len+1:报错 + local t3b = { 1, 2, 3 } + local ok3b = pcall(function() table.remove(t3b, 5) end) + if ok3b then return 7 end + + -- 3c. 负数位置:报错 + local t3c = { 1, 2, 3 } + local ok3c = pcall(function() table.remove(t3c, -1) end) + if ok3c then return 8 end + + -- 4. 移除最后一个元素 local t4 = { 42 } local r4 = table.remove(t4) - if r4 ~= 42 then return 7 end - -- fakelua 的 # 运算符对空表可能返回非 0,跳过 - -- if #t4 ~= 0 then return 8 end + if r4 ~= 42 then return 9 end -- 5. 移除中间元素,后续元素前移 local t5 = { 10, 20, 30, 40 } local r5 = table.remove(t5, 2) - if r5 ~= 20 then return 9 end - if t5[1] ~= 10 or t5[2] ~= 30 or t5[3] ~= 40 then return 10 end - -- fakelua 的 # 运算符对非连续索引可能返回非预期值,跳过 - -- if #t5 ~= 3 then return 11 end - - -- 6. 连续移除所有元素(fakelua 行为可能不同,简化) - -- local t6 = {1, 2, 3} - -- table.remove(t6) - -- table.remove(t6) - -- table.remove(t6) - -- if #t6 ~= 0 then return 12 end + if r5 ~= 20 then return 10 end + if t5[1] ~= 10 or t5[2] ~= 30 or t5[3] ~= 40 then return 11 end return 5000 end diff --git a/test/test_basic.cpp b/test/test_basic.cpp index 6261e32..dcc631b 100644 --- a/test/test_basic.cpp +++ b/test/test_basic.cpp @@ -371,6 +371,22 @@ TEST(test_basic, test_basic_tostring_edge) { FakeluaDeleteState(s); } +TEST(test_basic, test_basic_tonumber_lua54) { + State *s = FakeluaNewState(); + ASSERT_NE(s, nullptr); + CompileConfig config; + + for (auto jit_type: AllJitTypes()) { + SCOPED_TRACE(::testing::Message() << "jit=" << JitTypeName(jit_type)); + CompileFile(s, "./basic/test_basic_tonumber_lua54.lua", config); + int64_t res = 0; + Call(s, jit_type, "test_basic_tonumber_lua54", res); + EXPECT_EQ(res, 5000); + } + + FakeluaDeleteState(s); +} + TEST(test_basic, test_basic_boundary_error) { State *s = FakeluaNewState(); ASSERT_NE(s, nullptr); diff --git a/test/test_math.cpp b/test/test_math.cpp index 758d021..080a84c 100644 --- a/test/test_math.cpp +++ b/test/test_math.cpp @@ -478,6 +478,22 @@ TEST(test_math, test_math_fmod_zero) { FakeluaDeleteState(s); } +TEST(test_math, test_math_lua54_types) { + State *s = FakeluaNewState(); + ASSERT_NE(s, nullptr); + CompileConfig config; + + for (auto jit_type: AllJitTypes()) { + SCOPED_TRACE(::testing::Message() << "jit=" << JitTypeName(jit_type)); + CompileFile(s, "./math/test_math_lua54_types.lua", config); + int64_t res = 0; + Call(s, jit_type, "test_math_lua54_types", res); + EXPECT_EQ(res, 5000); + } + + FakeluaDeleteState(s); +} + TEST(test_math, test_math_boundary_nan_inf) { State *s = FakeluaNewState(); ASSERT_NE(s, nullptr); diff --git a/test/test_string.cpp b/test/test_string.cpp index 4a39352..0977171 100644 --- a/test/test_string.cpp +++ b/test/test_string.cpp @@ -458,6 +458,20 @@ TEST(test_string, test_gsub_negative_n) { FakeluaDeleteState(s); } +TEST(test_string, test_format_lua54_strict) { + State *s = FakeluaNewState(); + ASSERT_NE(s, nullptr); + CompileConfig config; + + CompileFile(s, "./string/test_format_lua54_strict.lua", config); + + int64_t res = 0; + CallAll(s, "test_format_lua54_strict", res); + EXPECT_EQ(res, 5000); + + FakeluaDeleteState(s); +} + TEST(test_string, test_format_s_bad_arg_table) { State *s = FakeluaNewState(); ASSERT_NE(s, nullptr);