Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/compile/c_gen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<double>(int_value) : float_value;
return FormatCDoubleLiteral(d);
return FormatCDoubleLiteral(ConstNumberExpToDouble(kind, int_value, float_value, exp));
}
}

Expand Down
18 changes: 17 additions & 1 deletion src/compile/c_runtime_header.h
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
25 changes: 25 additions & 0 deletions src/compile/compile_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<double>(int_value) : float_value;
if (d == 0.0 && ConstNumberExpHasFloatSyntax(node)) {
int minuses = 0;
SyntaxTreeInterfacePtr cur = node;
for (;;) {
const auto un = std::dynamic_pointer_cast<SyntaxTreeExp>(cur);
if (!un || un->GetExpKind() != ExpKind::kUnop) {
break;
}
const auto op = std::dynamic_pointer_cast<SyntaxTreeUnop>(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) {
Expand Down
6 changes: 2 additions & 4 deletions src/interp/codegen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<double>(int_value) : float_value;
return interp_rt::Float(d);
return interp_rt::Float(ConstNumberExpToDouble(kind, int_value, float_value, exp));
}
return interp_rt::Nil();
}
Expand Down Expand Up @@ -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<double>(int_value) : float_value;
LoadConstTo(r, interp_rt::Float(d));
LoadConstTo(r, interp_rt::Float(ConstNumberExpToDouble(kind, int_value, float_value, exp)));
}
return r;
}
Expand Down
5 changes: 2 additions & 3 deletions src/interp/runtime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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<int>(buf.size());
Expand Down
3 changes: 2 additions & 1 deletion src/native/basic/native_basic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
8 changes: 3 additions & 5 deletions src/native/log/native_log.cpp
Original file line number Diff line number Diff line change
@@ -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 <string>
#include <string_view>
Expand All @@ -27,12 +28,9 @@ static std::string FormatArgs(State *s, CVar *args, int n) {
case static_cast<int>(VarType::Int):
result += std::to_string(arg.data_.i);
break;
case static_cast<int>(VarType::Float): {
char buf[64];
std::snprintf(buf, sizeof(buf), "%.17g", arg.data_.f);
result += buf;
case static_cast<int>(VarType::Float):
result += FormatLuaFloat(arg.data_.f);
break;
}
case static_cast<int>(VarType::String):
case static_cast<int>(VarType::StringId):
result += inter::FakeluaToNativeString(s, arg);
Expand Down
23 changes: 12 additions & 11 deletions src/native/string/native_string.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>(VarType::Bool) || curr_arg.type_ == static_cast<int>(VarType::Table)) {
// Lua 5.4:%s 等价于对参数调 tostring(luaL_tolstring):
// string/number/bool/nil 都合法;table 没有 __tostring 元方法才报错
if (curr_arg.type_ == static_cast<int>(VarType::Table)) {
ThrowFakeluaException("bad argument to 'format' (string expected)");
}
std::string sval;
if (curr_arg.type_ == static_cast<int>(VarType::String) || curr_arg.type_ == static_cast<int>(VarType::StringId)) {
sval = std::string(KeyToStringView(curr_arg));
} else if (curr_arg.type_ == static_cast<int>(VarType::Int)) {
sval = std::to_string(curr_arg.data_.i);
} else if (curr_arg.type_ == static_cast<int>(VarType::Float)) {
sval = std::to_string(curr_arg.data_.f);
} else {
sval = AsVar(curr_arg).ToString(/*has_quote=*/false, /*has_postfix=*/false);
}
Expand Down Expand Up @@ -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<int64_t>(slen) + 1;
if (n >= 4) {
CVar a3 = inter::GetNativeArg(state, args, n, 3);
if (a3.type_ == static_cast<int>(VarType::Bool) || a3.type_ == static_cast<int>(VarType::Table)) {
ThrowFakeluaException("bad argument #4 to 'string.gsub' (number expected)");
if (a3.type_ != static_cast<int>(VarType::Nil)) {
if (a3.type_ == static_cast<int>(VarType::Bool) || a3.type_ == static_cast<int>(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 不合法
Expand All @@ -1431,7 +1432,7 @@ void RegisterStringLibraryApi(State *s) {
size_t prev_end = std::numeric_limits<size_t>::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;
Expand Down
3 changes: 2 additions & 1 deletion src/native/table/native_table.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -821,7 +822,7 @@ void RegisterTableLibraryApi(State *s) {
parts.push_back(owned.back());
total += owned.back().size();
} else if (item.type_ == static_cast<int>(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<int>(VarType::String) || item.type_ == static_cast<int>(VarType::StringId)) {
Expand Down
30 changes: 30 additions & 0 deletions src/util/number_util.h
Original file line number Diff line number Diff line change
@@ -1,11 +1,41 @@
#pragma once

#include <cmath>
#include <cstdio>
#include <cstring>
#include <limits>
#include <optional>
#include <string>

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<size_t>(len); ++i) {
if (buf[i] < '0' || buf[i] > '9') {
break;
}
}
if (i == static_cast<size_t>(len) && static_cast<size_t>(len) + 2 < sizeof(buf)) {
buf[len++] = '.';
buf[len++] = '0';
}
return std::string(buf, static_cast<size_t>(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
Expand Down
3 changes: 2 additions & 1 deletion src/var/var.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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:
Expand Down
23 changes: 23 additions & 0 deletions test/lua/basic/test_basic_tostring_cases.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 0 additions & 5 deletions test/lua/string/test_format_s_bad_arg.lua

This file was deleted.

23 changes: 23 additions & 0 deletions test/lua/string/test_format_s_types.lua
Original file line number Diff line number Diff line change
@@ -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
29 changes: 29 additions & 0 deletions test/lua/string/test_gsub_negative_n.lua
Original file line number Diff line number Diff line change
@@ -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
24 changes: 19 additions & 5 deletions test/test_string.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Loading
Loading