Skip to content

fix(stringlib): implement string.format with Lua 5.4 conversion rules - #80

Merged
wolfy-j merged 1 commit into
mainfrom
fix/string-format-conversions
Oct 28, 2026
Merged

wolfy-j merged 1 commit into
mainfrom
fix/string-format-conversions

Conversation

@wolfy-j

@wolfy-j wolfy-j commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

Problem

string.format passed raw Lua values to Go's fmt.Sprintf, so no conversion ever converted its argument:

Lua Before Lua 5.4
string.format("%.1f", 0) %!f(lua.LInteger=0) 0.0
string.format("%d", 3.0) %!d(lua.LNumber=3) 3
string.format("%x", -1) -1 ffffffffffffffff
string.format("%f", 1/0) +Inf inf
string.format("%q", "a\nb") Go quoting "a\ newline b"
string.format("%d") %!d(MISSING) error: no value
string.format("%y", 1) %!y(...) error: invalid conversion

Any Lua code that formats integer-valued numbers with %f hit this. JSON-decoded numbers and literal 0 are LInteger.

Fix

stringlib_format.go ports lstrlib's str_format:

  • Each specification is checked as checkformat checks it: the flags each conversion allows, and width and precision of at most two digits.
  • Arguments are converted like the reference implementation: luaL_checkinteger for c d i u o x X (unsigned for u o x X), luaL_checknumber for a A e E f F g G, tostring (with __tostring) for s, and addliteral for q.
  • Integers, hex floats (%a), inf/nan and string widths are formatted directly, byte-based as in C. e f g use Go's fmt with the precision C defaults to, which matches C for every flag.
  • Error messages match Lua 5.4.

Tests

stringlib_format_test.go has 94 cases. Every expected value comes from the Lua 5.4 reference interpreter. They cover conversions, flags, widths, precision, inf/nan, %a rounding, %q escapes, and every error path.

I also compared 2,895 random flag/width/precision/conversion/value combinations against lua5.4. They all match except %s of floats. %s is tostring, and tostring spells floats differently from Lua 5.4 (3.0 → 3, 1/0 → +Inf, 2^53). That is a separate defect in LNumber.String and is not changed here.

go test . passes.

string.format handed raw Lua values to Go's fmt.Sprintf, so a conversion
never converted its argument: string.format("%.1f", 0) produced
"%!f(lua.LInteger=0)", %d of 3.0 printed a float, %x of -1 printed "-1",
%q used Go quoting, inf/nan printed Go spellings, %s widths counted runes,
missing arguments printed %!v(MISSING) and invalid specifiers passed through.

Each specification is now validated as lstrlib's checkformat does (per
conversion flags, two-digit width and precision) and its argument is
converted the way the reference implementation converts it:
luaL_checkinteger for c/d/i/u/o/x/X, luaL_checknumber for a/e/f/g, tostring
for s, and addliteral for q. Errors match Lua 5.4: no value, number has no
integer representation, invalid conversion, specifier '%q' cannot have
modifiers, value has no literal form.

Expectations come from the Lua 5.4 reference interpreter; a randomized
comparison of 2,895 specifier/value pairs against lua5.4 matches except %s
of floats, where %s is tostring and tostring's float spelling differs.
@wolfy-j
wolfy-j merged commit 8d1d201 into main Oct 28, 2026
12 checks passed
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.

1 participant