diff --git a/src/compile/c_gen.cpp b/src/compile/c_gen.cpp index 56fe719..3df1480 100644 --- a/src/compile/c_gen.cpp +++ b/src/compile/c_gen.cpp @@ -3073,8 +3073,7 @@ std::string CGen::CompileNumericExp(const SyntaxTreeInterfacePtr &exp) { if (ConstNumberExpIsIntValue(kind, exp)) { return std::to_string(int_value); } - const double d = (kind == TableKeyKind::kInt) ? static_cast(int_value) : float_value; - return FormatCDoubleLiteral(d); + return FormatCDoubleLiteral(ConstNumberExpToDouble(kind, int_value, float_value, exp)); } } diff --git a/src/compile/c_runtime_header.h b/src/compile/c_runtime_header.h index d263477..a4ea669 100644 --- a/src/compile/c_runtime_header.h +++ b/src/compile/c_runtime_header.h @@ -1257,7 +1257,23 @@ static inline int FlVarToStr(CVar v, char *buf, int buf_size) { case VAR_NIL: memcpy(buf, "nil", 3); buf[3] = '\0'; return 3; case VAR_BOOL: if (v.data_.b) { memcpy(buf, "true", 4); buf[4] = '\0'; return 4; } else { memcpy(buf, "false", 5); buf[5] = '\0'; return 5; } case VAR_INT: return snprintf(buf, buf_size, "%lld", (long long)v.data_.i); - case VAR_FLOAT: return snprintf(buf, buf_size, "%.17g", v.data_.f); + case VAR_FLOAT: { + /* Lua 5.4 tostringbuff: %.14g,结果只含数字和符号时补 .0 */ + int flen = snprintf(buf, buf_size, "%.14g", v.data_.f); + int fi = 0; + if (flen > 0 && flen < buf_size) { + if (buf[fi] == '-') fi++; + for (; fi < flen; fi++) { + if (buf[fi] < '0' || buf[fi] > '9') break; + } + if (fi == flen && flen + 2 < buf_size) { + buf[flen] = '.'; + buf[flen + 1] = '0'; + flen += 2; + } + } + return flen; + } case VAR_STRING: case VAR_STRINGID: FakeluaThrowError(_S, "FlVarToStr: string type should be handled by caller"); return 0; case VAR_TABLE: { int __n = snprintf(buf, buf_size - 1, "table(0x%llx", (unsigned long long)(uintptr_t)v.data_.t); if (__n > 0 && __n < buf_size - 1) { buf[__n] = ')'; buf[__n + 1] = '\0'; return __n + 1; } return __n; } diff --git a/src/compile/compile_common.h b/src/compile/compile_common.h index cf21dcb..ee26209 100644 --- a/src/compile/compile_common.h +++ b/src/compile/compile_common.h @@ -427,6 +427,31 @@ inline bool ConstNumberExpIsIntValue(TableKeyKind kind, const SyntaxTreeInterfac return kind == TableKeyKind::kInt && !ConstNumberExpHasFloatSyntax(node); } +// 值语境(区别于表键归一化)下,把 ClassifyConstNumberExp 的结果还原成 double。 +// 0.0 会被表键归一化成 int 零,但 Lua 值语义里 0.0 是 float;一元负号作用于 +// 浮点零还必须保留负零:奇数次负号 -> -0.0,偶数次 -> +0.0。 +inline double ConstNumberExpToDouble(TableKeyKind kind, int64_t int_value, double float_value, const SyntaxTreeInterfacePtr &node) { + double d = (kind == TableKeyKind::kInt) ? static_cast(int_value) : float_value; + if (d == 0.0 && ConstNumberExpHasFloatSyntax(node)) { + int minuses = 0; + SyntaxTreeInterfacePtr cur = node; + for (;;) { + const auto un = std::dynamic_pointer_cast(cur); + if (!un || un->GetExpKind() != ExpKind::kUnop) { + break; + } + const auto op = std::dynamic_pointer_cast(un->Op()); + if (!op || op->GetOpKind() != UnOpKind::kMinus) { + break; + } + ++minuses; + cur = un->Right(); + } + d = (minuses % 2 != 0) ? -0.0 : 0.0; + } + return d; +} + inline std::string FormatCDoubleLiteral(double d) { std::string s = std::format("{:.17g}", d); if (s.find('.') == std::string::npos && s.find('e') == std::string::npos && s.find('E') == std::string::npos) { diff --git a/src/interp/codegen.cpp b/src/interp/codegen.cpp index 9e5356a..00cae67 100644 --- a/src/interp/codegen.cpp +++ b/src/interp/codegen.cpp @@ -218,8 +218,7 @@ CVar InterpCodegen::LiteralFromExp(const SyntaxTreeInterfacePtr &exp) { if (ConstNumberExpIsIntValue(kind, exp)) { return interp_rt::Int(int_value); } - const double d = (kind == TableKeyKind::kInt) ? static_cast(int_value) : float_value; - return interp_rt::Float(d); + return interp_rt::Float(ConstNumberExpToDouble(kind, int_value, float_value, exp)); } return interp_rt::Nil(); } @@ -1332,8 +1331,7 @@ int InterpCodegen::CompileExp(const SyntaxTreeInterfacePtr &exp, bool preserve_m if (ConstNumberExpIsIntValue(kind, exp)) { LoadConstTo(r, interp_rt::Int(int_value)); } else { - const double d = (kind == TableKeyKind::kInt) ? static_cast(int_value) : float_value; - LoadConstTo(r, interp_rt::Float(d)); + LoadConstTo(r, interp_rt::Float(ConstNumberExpToDouble(kind, int_value, float_value, exp))); } return r; } diff --git a/src/interp/runtime.cpp b/src/interp/runtime.cpp index 10b3de9..e149399 100644 --- a/src/interp/runtime.cpp +++ b/src/interp/runtime.cpp @@ -4,6 +4,7 @@ #include "state/const_string.h" #include "state/state.h" #include "util/exception.h" +#include "util/number_util.h" #include "var/var.h" #include "var/var_closure.h" #include "var/var_multi.h" @@ -42,9 +43,7 @@ CVar BinConcat(State *s, const CVar &a, const CVar &b) { } if (v.type_ == kInt) buf = std::to_string(v.data_.i); else { - char tmp[256]; - std::snprintf(tmp, sizeof(tmp), "%.17g", v.data_.f); - buf = tmp; + buf = FormatLuaFloat(v.data_.f); } p = buf.c_str(); len = static_cast(buf.size()); diff --git a/src/native/basic/native_basic.cpp b/src/native/basic/native_basic.cpp index 277b59f..d0ff511 100644 --- a/src/native/basic/native_basic.cpp +++ b/src/native/basic/native_basic.cpp @@ -197,7 +197,8 @@ void RegisterBasicLibraryApi(State *s) { for (int i = 0; i < n; ++i) { if (i > 0) std::printf("\t"); CVar arg = inter::GetNativeArg(state, args, n, i); - std::string str = AsVar(arg).ToString(); + // Lua 的 print 对每个参数走 tostring,字符串不带引号 + std::string str = AsVar(arg).ToString(/*has_quote=*/false, /*has_postfix=*/false); std::printf("%s", str.c_str()); } std::printf("\n"); diff --git a/src/native/log/native_log.cpp b/src/native/log/native_log.cpp index 7cd2012..71f5897 100644 --- a/src/native/log/native_log.cpp +++ b/src/native/log/native_log.cpp @@ -1,6 +1,7 @@ #include "native/log/native_log.h" #include "native/native_common.h" #include "util/logging.h" +#include "util/number_util.h" #include #include @@ -27,12 +28,9 @@ static std::string FormatArgs(State *s, CVar *args, int n) { case static_cast(VarType::Int): result += std::to_string(arg.data_.i); break; - case static_cast(VarType::Float): { - char buf[64]; - std::snprintf(buf, sizeof(buf), "%.17g", arg.data_.f); - result += buf; + case static_cast(VarType::Float): + result += FormatLuaFloat(arg.data_.f); break; - } case static_cast(VarType::String): case static_cast(VarType::StringId): result += inter::FakeluaToNativeString(s, arg); diff --git a/src/native/string/native_string.cpp b/src/native/string/native_string.cpp index 80400e6..783bdd4 100644 --- a/src/native/string/native_string.cpp +++ b/src/native/string/native_string.cpp @@ -1144,17 +1144,14 @@ void RegisterStringLibraryApi(State *s) { } res.push_back('"'); } else if (spec == 's') { - // 标准 Lua:%s 的参数必须是 string/number,Bool/Table 不合法 - if (curr_arg.type_ == static_cast(VarType::Bool) || curr_arg.type_ == static_cast(VarType::Table)) { + // Lua 5.4:%s 等价于对参数调 tostring(luaL_tolstring): + // string/number/bool/nil 都合法;table 没有 __tostring 元方法才报错 + if (curr_arg.type_ == static_cast(VarType::Table)) { ThrowFakeluaException("bad argument to 'format' (string expected)"); } std::string sval; if (curr_arg.type_ == static_cast(VarType::String) || curr_arg.type_ == static_cast(VarType::StringId)) { sval = std::string(KeyToStringView(curr_arg)); - } else if (curr_arg.type_ == static_cast(VarType::Int)) { - sval = std::to_string(curr_arg.data_.i); - } else if (curr_arg.type_ == static_cast(VarType::Float)) { - sval = std::to_string(curr_arg.data_.f); } else { sval = AsVar(curr_arg).ToString(/*has_quote=*/false, /*has_postfix=*/false); } @@ -1403,13 +1400,17 @@ void RegisterStringLibraryApi(State *s) { std::string_view pat_view = GetStringArgView(a1, temp1); const size_t slen = sv.size(); - int64_t max_replace = -1; + // Lua 5.4:第四参缺省/nil 时默认 srcl+1(即不限次数);显式传入的值 + // 原样使用,负数或 0 意味着一次都不替换(while (n < max_s))。 + int64_t max_replace = static_cast(slen) + 1; if (n >= 4) { CVar a3 = inter::GetNativeArg(state, args, n, 3); - if (a3.type_ == static_cast(VarType::Bool) || a3.type_ == static_cast(VarType::Table)) { - ThrowFakeluaException("bad argument #4 to 'string.gsub' (number expected)"); + if (a3.type_ != static_cast(VarType::Nil)) { + if (a3.type_ == static_cast(VarType::Bool) || a3.type_ == static_cast(VarType::Table)) { + ThrowFakeluaException("bad argument #4 to 'string.gsub' (number expected)"); + } + max_replace = inter::CVarToInteger(a3, max_replace); } - max_replace = inter::CVarToInteger(a3, -1); } // 标准 Lua:gsub 的替换参数必须是 string/function/table,Bool 不合法 @@ -1431,7 +1432,7 @@ void RegisterStringLibraryApi(State *s) { size_t prev_end = std::numeric_limits::max();// 上一次产出匹配的尾后位置 int64_t count = 0; - while ((max_replace < 0 || count < max_replace) && src <= slen) { + while (count < max_replace && src <= slen) { // 从 src 起逐位尝试;与 gmatch 相同的零宽规则:起点处的零宽匹配若紧跟上 // 一次产出匹配的尾后位置,则跳过该位继续找(避免相邻空匹配)。 lua_pattern::MatchResult m; diff --git a/src/native/table/native_table.cpp b/src/native/table/native_table.cpp index d9caccc..775ba80 100644 --- a/src/native/table/native_table.cpp +++ b/src/native/table/native_table.cpp @@ -5,6 +5,7 @@ #include "native/string/native_string.h" #include "state/state.h" #include "util/exception.h" +#include "util/number_util.h" #include "var/var.h" #include "var/var_closure.h" #include "var/var_string.h" @@ -821,7 +822,7 @@ void RegisterTableLibraryApi(State *s) { parts.push_back(owned.back()); total += owned.back().size(); } else if (item.type_ == static_cast(VarType::Float)) { - owned.push_back(std::format("{}", item.data_.f)); + owned.push_back(FormatLuaFloat(item.data_.f)); parts.push_back(owned.back()); total += owned.back().size(); } else if (item.type_ == static_cast(VarType::String) || item.type_ == static_cast(VarType::StringId)) { diff --git a/src/util/number_util.h b/src/util/number_util.h index c05c216..65d0a24 100644 --- a/src/util/number_util.h +++ b/src/util/number_util.h @@ -1,11 +1,41 @@ #pragma once #include +#include +#include #include #include +#include namespace fakelua { +// Lua 5.4 number-to-string rule (lobject.c::tostringbuff): +// format with "%.14g"; if the result looks like an integer (only sign and +// digits), append ".0" so that floats never print in integer shape. +// Keeps float/integer distinguishable across tostring -> tonumber round-trips. +inline std::string FormatLuaFloat(double value) { + char buf[64]; + int len = std::snprintf(buf, sizeof(buf), "%.14g", value); + if (len <= 0) { + return {}; + } + // Equivalent to Lua's buff[strspn(buff, "-0123456789")] == '\0' + size_t i = 0; + if (buf[i] == '-') { + ++i; + } + for (; i < static_cast(len); ++i) { + if (buf[i] < '0' || buf[i] > '9') { + break; + } + } + if (i == static_cast(len) && static_cast(len) + 2 < sizeof(buf)) { + buf[len++] = '.'; + buf[len++] = '0'; + } + return std::string(buf, static_cast(len)); +} + // Try to convert a double to int64_t if it's a finite integer value within int64_t range. // Returns nullopt if the conversion is not possible. // The upper bound must be checked as "< 2^63" rather than "<= INT64_MAX" because diff --git a/src/var/var.cpp b/src/var/var.cpp index 8d609a6..147fca8 100644 --- a/src/var/var.cpp +++ b/src/var/var.cpp @@ -3,6 +3,7 @@ #include "state/const_string.h" #include "state/state.h" #include "util/common.h" +#include "util/number_util.h" #include "var_multi.h" #include "var_string.h" @@ -56,7 +57,7 @@ std::string Var::ToString(bool has_quote, bool has_postfix) const { ret = std::to_string(data_.i); break; case VarType::Float: { - ret = std::format("{}", data_.f); + ret = FormatLuaFloat(data_.f); break; } case VarType::String: diff --git a/test/lua/basic/test_basic_tostring_cases.lua b/test/lua/basic/test_basic_tostring_cases.lua index 87babcf..2bcc0ce 100644 --- a/test/lua/basic/test_basic_tostring_cases.lua +++ b/test/lua/basic/test_basic_tostring_cases.lua @@ -83,5 +83,28 @@ function test_basic_tostring_edge() local round4 = tonumber(tostring(orig4)) if round4 ~= orig4 then return 54 end + -- ------------------------------------------------------------------------- + -- Lua 5.4 float formatting: %.14g, integer-valued floats keep a trailing .0 + -- ------------------------------------------------------------------------- + if tostring(1.0) ~= "1.0" then return 60 end + if tostring(100.0) ~= "100.0" then return 61 end + if tostring(-0.0) ~= "-0.0" then return 62 end + if tostring(0.1) ~= "0.1" then return 63 end + if tostring(1e20) ~= "1e+20" then return 64 end + if tostring(1 / 0) ~= "inf" then return 65 end + -- sign of NaN is platform-dependent in Lua too + local nan_str = tostring(0 / 0) + if nan_str ~= "nan" and nan_str ~= "-nan" then return 66 end + + -- String concatenation uses the same formatting + if ("x" .. 1.0) ~= "x1.0" then return 67 end + if (1.0 .. "") ~= "1.0" then return 68 end + + -- Round-trip must preserve the float subtype + if math.type(tonumber(tostring(1.0))) ~= "float" then return 69 end + + -- table.concat numbers follows the same rule + if table.concat({ 1.0, 2.5, 3 }, ",") ~= "1.0,2.5,3" then return 70 end + return 5000 end diff --git a/test/lua/string/test_format_s_bad_arg.lua b/test/lua/string/test_format_s_bad_arg.lua deleted file mode 100644 index 3c8e748..0000000 --- a/test/lua/string/test_format_s_bad_arg.lua +++ /dev/null @@ -1,5 +0,0 @@ -function test_format_s_bad_arg() - -- string.format "%s" format requires a string/number argument; Bool is invalid per standard Lua - string.format("%s", true) - return 0 -end diff --git a/test/lua/string/test_format_s_types.lua b/test/lua/string/test_format_s_types.lua new file mode 100644 index 0000000..14afa6e --- /dev/null +++ b/test/lua/string/test_format_s_types.lua @@ -0,0 +1,23 @@ +-- Lua 5.4: string.format("%s", x) 等价于对 x 调用 tostring(luaL_tolstring): +-- string/number/bool/nil 均合法,只有 table(无 __tostring 元方法)报错。 +function test_format_s_types() + if string.format("%s", true) ~= "true" then return 1 end + if string.format("%s", false) ~= "false" then return 2 end + if string.format("%s", nil) ~= "nil" then return 3 end + if string.format("%s", 42) ~= "42" then return 4 end + -- float 走 Lua 数字格式,而不是 C 的 6 位小数 + if string.format("%s", 2.5) ~= "2.5" then return 5 end + if string.format("%s", 1.0) ~= "1.0" then return 6 end + if string.format("%s", 1e20) ~= "1e+20" then return 7 end + if string.format("[%s]", 2.5) ~= "[2.5]" then return 8 end + -- 宽度/对齐修饰仍作用于 tostring 的结果 + if string.format("%-6s|", "ab") ~= "ab |" then return 9 end + -- string 原样返回 + if string.format("%s", "hi") ~= "hi" then return 10 end + + -- table 没有 __tostring,仍然报错 + local ok = pcall(function() return string.format("%s", { 1, 2 }) end) + if ok then return 11 end + + return 5000 +end diff --git a/test/lua/string/test_gsub_negative_n.lua b/test/lua/string/test_gsub_negative_n.lua new file mode 100644 index 0000000..6742c6b --- /dev/null +++ b/test/lua/string/test_gsub_negative_n.lua @@ -0,0 +1,29 @@ +-- Lua 5.4: gsub 第四参缺省/nil 时为 srcl+1(不限次数);显式传入的负数或 0 +-- 意味着一次都不替换(while (n < max_s)),而不是"不限次数"。 +function test_gsub_negative_n() + local r1, c1 = ("aaa"):gsub("a", "X", -1) + if r1 ~= "aaa" or c1 ~= 0 then return 1 end + + local r2, c2 = ("hello"):gsub("l", "L", -2) + if r2 ~= "hello" or c2 ~= 0 then return 2 end + + local r3, c3 = ("aaa"):gsub("a", "X", 0) + if r3 ~= "aaa" or c3 ~= 0 then return 3 end + + local r4, c4 = ("aaa"):gsub("a", "X", 2) + if r4 ~= "XXa" or c4 ~= 2 then return 4 end + + -- 缺省:全部替换 + local r5, c5 = ("aaa"):gsub("a", "X") + if r5 ~= "XXX" or c5 ~= 3 then return 5 end + + -- 显式 nil 与缺省等价 + local r6, c6 = ("aaa"):gsub("a", "X", nil) + if r6 ~= "XXX" or c6 ~= 3 then return 6 end + + -- 大于匹配总数的值等同于全部替换 + local r7, c7 = ("aaa"):gsub("a", "X", 100) + if r7 ~= "XXX" or c7 ~= 3 then return 7 end + + return 5000 +end diff --git a/test/test_string.cpp b/test/test_string.cpp index 505e902..4a39352 100644 --- a/test/test_string.cpp +++ b/test/test_string.cpp @@ -430,16 +430,30 @@ TEST(test_string, test_gsub_bad_func_return_bool) { FakeluaDeleteState(s); } -TEST(test_string, test_format_s_bad_arg) { +TEST(test_string, test_format_s_types) { State *s = FakeluaNewState(); ASSERT_NE(s, nullptr); CompileConfig config; - CompileFile(s, "./string/test_format_s_bad_arg.lua", config); + CompileFile(s, "./string/test_format_s_types.lua", config); - // TCC 是 C 编译器,不支持 C++ 异常传播,只测试 GCC 后端 - double res = 0; - CallThrow(s, "test_format_s_bad_arg", res); + int64_t res = 0; + CallAll(s, "test_format_s_types", res); + EXPECT_EQ(res, 5000); + + FakeluaDeleteState(s); +} + +TEST(test_string, test_gsub_negative_n) { + State *s = FakeluaNewState(); + ASSERT_NE(s, nullptr); + CompileConfig config; + + CompileFile(s, "./string/test_gsub_negative_n.lua", config); + + int64_t res = 0; + CallAll(s, "test_gsub_negative_n", res); + EXPECT_EQ(res, 5000); FakeluaDeleteState(s); } diff --git a/test/test_var.cpp b/test/test_var.cpp index 730934f..bedaaba 100644 --- a/test/test_var.cpp +++ b/test/test_var.cpp @@ -120,7 +120,7 @@ TEST(var, ToString) { ASSERT_EQ(v.ToString(), "12345"); v.SetFloat(12345.0); - ASSERT_EQ(v.ToString(), "12345"); + ASSERT_EQ(v.ToString(), "12345.0"); v.SetFloat(12345.1); ASSERT_EQ(v.ToString(), "12345.1");