Skip to content

fix: match Lua 5.4 output semantics for float tostring, print, format %s, gsub n - #26

Open
esrrhs wants to merge 1 commit into
masterfrom
fix/lua-output-semantics-tostring-print-format-gsub
Open

esrrhs wants to merge 1 commit into
masterfrom
fix/lua-output-semantics-tostring-print-format-gsub

Conversation

@esrrhs

@esrrhs esrrhs commented Oct 6, 2026

Copy link
Copy Markdown
Owner

背景

与真实 Lua 5.4 的差分测试发现 4 个输出层语义差异。语义均对照 Lua 5.4.6 官方源码核实(lobject.c::tostringbuff、lstrlib.c 的 str_format/str_gsub)。

修改内容

  • float → string 对齐 Lua 5.4:此前各路径分别用 %.17g / std::format / std::to_string,且从不补 Lua 的 .0 后缀——tostring(1.0) 得到 "1",导致 tonumber(tostring(1.0)) 静默变成 integer。
    新增统一的 FormatLuaFloat(%.14g,结果只含符号和数字时补 .0,等价 Lua 的 strspn 判定),覆盖全部 Lua 值→字符串路径:Var::ToString(tostring/print/native)、interp BinConcat、table.concat、log,以及 JIT C 运行时头里的 FlVarToStr(TCC 按 C89 兼容写法)。json/xml/csv/ini 等序列化格式刻意不改。
  • print 不再给字符串加引号:改用 ToString(false, false),使 print(x) 与 print(tostring(x)) 一致。
  • string.format("%s", x) 对齐 luaL_tolstring:bool/nil/number 均合法并走 tostring(此前 bool 报错、float 固定 6 位小数 2.5→"2.500000");仅 table(无 __tostring)仍报错。
  • gsub(s,p,r,n) 负数 n:此前任何负数都被当作不限次数。Lua 缺省/nil 时默认 srcl+1,循环条件 n < max_s,因此显式负数或 0 表示 0 次替换。

连带修复

新断言暴露出 interp 后端把 -0.0 编成 +0.0(JIT 为 -0.0)。根因:ClassifyConstNumberExp 为表键把 0.0 归一化成 int 0,值语境重建 double 时丢了负号。新增 ConstNumberExpToDouble,按嵌套一元负号层数奇偶还原 ±0.0,三处值构造点(CGen + interp 两处)共用;表键归一化不受影响。修复后三后端 1/-0.0 均为 -inf。

测试

  • 扩展 test_basic_tostring_cases:精确断言 1.0/-0.0/0.1/1e20/inf/nan、字符串拼接、math.type 往返、table.concat
  • 新增 test_format_s_types、test_gsub_negative_n(CallAll 覆盖 TCC/GCC/interp 三后端)
  • 删除断言方向错误的 test_format_s_bad_arg(它错误地期望 bool 必须报错)
  • 更新 var.ToString 断言(12345.0 → "12345.0")
  • 本地全量 1491 个测试全部通过(test_algo/test_closure 需本机 Lua 开发头未编译,由 CI 覆盖;mysql/redis/net/http 需外部服务)

备注

… %s, gsub n

Differential testing against real Lua exposed four output-layer
divergences (semantics verified against Lua 5.4.6 sources:
lobject.c::tostringbuff and lstrlib.c str_format/str_gsub):

- float tostring used %.17g (interp/JIT concat) and std::format/
  std::to_string elsewhere, and never appended the Lua '.0' suffix:
  tostring(1.0) -> "1", so tonumber(tostring(1.0)) silently became an
  integer. Add FormatLuaFloat (%.14g + '.0' when the result contains
  only sign/digits, matching Lua's strspn check) and route every Lua
  value->string path through it: Var::ToString (tostring/print/natives),
  interp BinConcat, table.concat, log, and FlVarToStr in the JIT C
  runtime header (C89-compatible for TCC). Serialization formats
  (json/xml/csv/ini) are intentionally left unchanged.

- print quoted its string arguments (ToString defaults to has_quote=true);
  call ToString(false, false) so print(x) equals print(tostring(x)).

- string.format("%s", x) rejected booleans and formatted floats with
  std::to_string (fixed 6 decimals, 2.5 -> "2.500000"). %s is defined as
  luaL_tolstring: string/number/boolean/nil are valid and formatted via
  tostring; only tables without __tostring still raise. Replace the
  incorrect bool-must-error regression with test_format_s_types.lua.

- gsub(s, p, r, n) treated any negative n as unlimited. Lua defaults the
  missing/nil argument to srcl+1 and loops while n < max_s, so an
  explicit negative or zero performs zero replacements. Use srcl+1 as
  the default, honor explicit nil, and drop the negative escape hatch.

Also fix a backend inconsistency surfaced by the new assertions: the
interp compiled -0.0 as +0.0. ClassifyConstNumberExp normalizes 0.0 to
an integer zero for table keys, but in value context rebuilding the
double dropped the sign. Add ConstNumberExpToDouble, which restores
+-0.0 by the parity of nested unary minuses, shared by all three value
construction sites (CGen and both interp codegen paths); table-key
normalization is unaffected. 1/-0.0 is now -inf on all backends.

Tests: extend test_basic_tostring_cases with exact assertions
(1.0/-0.0/0.1/1e20/inf/nan, concat, math.type round-trip, table.concat),
add test_gsub_negative_n, update the var.ToString assertion
(12345.0 -> "12345.0"); all three JIT backends, 1491 tests pass.
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 93.18182% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.07%. Comparing base (a55a4bf) to head (2a437b9).

Files with missing lines Patch % Lines
src/interp/codegen.cpp 50.00% 1 Missing ⚠️
src/native/string/native_string.cpp 85.71% 1 Missing ⚠️
src/util/number_util.h 92.85% 1 Missing ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@            Coverage Diff             @@
##           master      #26      +/-   ##
==========================================
+ Coverage   88.04%   88.07%   +0.03%     
==========================================
  Files         123      124       +1     
  Lines       25216    25234      +18     
==========================================
+ Hits        22201    22225      +24     
+ Misses       3015     3009       -6     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants