Skip to content

Format.lua FFI references unbound variable number instead of its n argument #1

@Unisay

Description

@Unisay

Summary

The Lua FFI in src/Data/Number/Format.lua references an unbound variable number in every string.format call, while the actual Number argument is bound under a different parameter name (n). All three native formatting functions (toPrecisionNative, toFixedNative, toExponentialNative) are therefore broken: at runtime number resolves to the global number (which is nil), so string.format receives nil as its value and errors instead of formatting the supplied number.

Offending code

src/Data/Number/Format.lua:

return {
  toPrecisionNative = (function(d) return function(n) return string.format("%." .. tostring(d) .. "f", number) end end),
  toFixedNative = (function(d) return function(n) return string.format("%." .. tostring(d) .. "d", number) end end),
  toExponentialNative = (function(d) return function(n) return string.format("%." .. tostring(d) .. "e", number) end end),
  toString = (function(num) return tostring(num) end)
}

The PureScript declarations (src/Data/Number/Format.purs) are curried as Int -> Number -> String:

foreign import toPrecisionNative ::   Int -> Number -> String
foreign import toFixedNative ::       Int -> Number -> String
foreign import toExponentialNative :: Int -> Number -> String

So the outer parameter d is the precision/digits Int, and the inner parameter n is the Number to be formatted. The format string correctly interpolates d, but the value passed to string.format should be n, not the non-existent number.

The reference JS FFI (src/Data/Number/Format.js) confirms the intended shape — outer arg is the digit count, inner arg is the number being formatted:

function wrap(method) {
  return function(d) {
    return function(num) {
      return method.apply(num, [d]);
    };
  };
}
export const toPrecisionNative = wrap(Number.prototype.toPrecision);
export const toFixedNative = wrap(Number.prototype.toFixed);
export const toExponentialNative = wrap(Number.prototype.toExponential);

Root cause

Hand-written Lua FFI typo: the inner closure parameter is named n, but the body references number. Since number is never bound in any enclosing scope, Lua reads it as a global, which is nil. The n parameter is consequently never used. This is a pure copy/rename mistake in the Lua port — the function bodies were likely adapted from a version that named the parameter number and the rename to n was applied to the signature only, not the body.

Impact

Every toStringWith call (via precision, fixed, or exponential) routes through these functions, so number formatting is entirely non-functional in the Lua backend:

> string.format("%.6f", nil)
stdin:1: bad argument #2 to 'format' (number expected, got nil)

toString is unaffected (it correctly uses num).

How it was found

luacheck --quiet --std min src/ on the freshly cloned master:

src/Data/Number/Format.lua:2:52: unused argument n
src/Data/Number/Format.lua:2:104: accessing undefined variable number
src/Data/Number/Format.lua:3:48: unused argument n
src/Data/Number/Format.lua:3:100: accessing undefined variable number
src/Data/Number/Format.lua:4:54: unused argument n
src/Data/Number/Format.lua:4:106: accessing undefined variable number

Fix

Replace number with n in all three string.format calls so each function uses its actual bound argument.

Related (out of scope here)

toFixedNative uses the %d (integer) conversion specifier rather than %f. JavaScript's Number.prototype.toFixed(d) produces a fixed-point decimal with d fractional digits, so %d is semantically wrong for a floating-point value (and on Lua 5.3+ string.format("%.3d", 1.5) raises "number has no integer representation"). This is a separate correctness defect from the unbound-variable bug reported here; flagging it so it is not lost, but the fix in this issue is intentionally scoped to the undefined-variable / unused-argument defect only.

Metadata

Metadata

Assignees

No one assigned

    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