Skip to content

[fork-ffi] prelude: showNumberImpl — showNumberImpl delegates the core formatting to Lua's tostring, which… #100

@Unisay

Description

@Unisay

Package: purescript-lua-prelude
File: src/Data/Show.lua
Function: showNumberImpl
Class: show-escaping Severity: medium

showNumberImpl delegates the core formatting to Lua's tostring, which uses the default %.14g format. PureScript's Show Number must reproduce JavaScript's Number.prototype.toString (the upstream JS FFI is var str = n.toString(); return isNaN(str + ".0") ? str : str + ".0";). Lua's %.14g diverges from JS in two independent ways. (1) Precision: it prints at most 14 significant digits, so show (1.0/3.0) yields "0.33333333333333" where PureScript/JS yields "0.3333333333333333" (16-17 sig digits, shortest round-trip). The Lua result does not even round-trip back to the same Double. (2) Number formatting: Lua switches to scientific notation far earlier than JS and zero-pads exponents to >=2 digits with an explicit sign. Concretely show (1.0e-5) gives Lua "1e-05" but JS "0.00001"; show (1.0e-7) gives Lua "1e-07" but JS "1e-7". The trailing-".0" logic itself is correct (it appends .0 only when there is no '.'/'e'/'E', matching the JS isNaN trick), but it operates on an already-wrong base string. The divergence is latent: the test suite's testNumberShow foreign import has no .lua implementation (only Utils.js exists, and it only defines throwErr), so no eval golden currently exercises Number show formatting under Lua 5.1.

Current (Lua):

showNumberImpl = (function(n)
  if n ~= n then return "NaN" end
  if n == math.huge then return "Infinity" end
  if n == -math.huge then return "-Infinity" end
  local str = tostring(n)
  if str:find("[.eE]") then return str end
  return str .. ".0"
end)

Expected: PureScript/JS show: 1.0/3.0 -> "0.3333333333333333"; 1.0e-5 -> "0.00001"; 1.0e-7 -> "1e-7"; 1.0e21 -> "1e+21"; 100.0 -> "100.0"; 0.1 -> "0.1". Lua tostring currently gives: 1.0/3.0 -> "0.33333333333333"; 1.0e-5 -> "1e-05"; 1.0e-7 -> "1e-07". NaN/Infinity/-Infinity and the .0 suffix logic are already correct.

Proposed fix:

Replace the bare `tostring(n)` with a base string produced by a shortest-round-trip search and then normalize the result to JS conventions, instead of relying on %.14g. Step 1 (precision): build str by trying string.format("%."..p.."g", n) for p = 1..17 and taking the first whose tonumber() equals n. Step 2 (formatting): %g alone is not sufficient because the shortest round-trip can pick scientific form for plain integers (e.g. 100 -> "1e+02") and zero-pads exponents; post-process to match JS: when the decimal exponent is in JS's plain-decimal range (roughly -6 <= exp < 21) emit fixed/decimal notation, otherwise emit `mantissa e+/-EXP` with the exponent's leading zeros stripped (keep the sign, so e-7 not e-07 and e+21 not e+021). Keep the existing NaN/Infinity/-Infinity guards and the trailing-".0" rule (append ".0" only when the final string contains no '.', 'e', or 'E'). Fully emulating Number.prototype.toString in Lua 5.1 is non-trivial; at minimum the shortest-round-trip search fixes the precision loss, and the exponent-normalization fixes the e-07/e-7 mismatch.

Found by the FFI audit; reproduced under Lua 5.1.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions