diff --git a/_glua-tests/base.lua b/_glua-tests/base.lua index 2bc64a8b..297a21be 100644 --- a/_glua-tests/base.lua +++ b/_glua-tests/base.lua @@ -46,3 +46,33 @@ local ok, a, b = xpcall(function() return err .. "!", "b" end) assert(not ok and string.find(a, "error!!") and b == nil) + +-- numbers convert to text with the reference Lua 5.1 "%.14g" rendering +assert(tostring(100) == "100") +assert(tostring(0.1) == "0.1") +assert(tostring(0.1 + 0.2) == "0.3") +assert(tostring(1e-10) == "1e-10") +assert(tostring(1e15) == "1e+15") +assert(tostring(2^53) == "9.007199254741e+15") +assert(tostring(123456789012345678) == "1.2345678901235e+17") +assert(tostring(1/0) == "inf") +assert(tostring(-1/0) == "-inf") +local nan = 0/0 +local s1, s2 = tostring(nan), tostring(-nan) +assert(s1 == "nan" or s1 == "-nan") +assert(s2 == "nan" or s2 == "-nan") +assert(s1 ~= s2) + +-- negative zero renders with its sign +local zero = 0.0 +local negzero = -zero +assert(tostring(zero) == "0") +assert(tostring(negzero) == "-0") +assert(tostring(negzero * 1) == "-0") + +-- the same rendering applies to concatenation and table.concat +assert((1e15 .. "") == "1e+15") +assert((2^53 .. "x") == "9.007199254741e+15x") +assert((1.5 .. "") == "1.5") +assert(("" .. negzero) == "-0") +assert(table.concat({1e15, 2^53}, ",") == "1e+15,9.007199254741e+15") diff --git a/_glua-tests/math.lua b/_glua-tests/math.lua index 871d4041..7915c6d1 100644 --- a/_glua-tests/math.lua +++ b/_glua-tests/math.lua @@ -10,3 +10,14 @@ local ok, msg = pcall(function() math.min() end) assert(not ok and string.find(msg, "wrong number of arguments")) + +-- math.huge is the positive infinity, as in reference Lua 5.1 +assert(math.huge == 1/0) +assert(-math.huge == -1/0) +assert(math.huge > 1e308) +assert(tostring(math.huge) == "inf") +assert(tostring(-math.huge) == "-inf") + +-- the fractional part returned by math.modf prints like the reference +local _, fr = math.modf(3.7) +assert(tostring(fr) == "0.7") diff --git a/_glua-tests/strings.lua b/_glua-tests/strings.lua index 273226d9..ab4a25d4 100644 --- a/_glua-tests/strings.lua +++ b/_glua-tests/strings.lua @@ -11,3 +11,57 @@ assert(ret1 == 1) assert(ret2 == 3) assert(ret3 == "aaa") assert(ret4 == 4) + +-- string.format: Lua 5.1 semantics for %s, %c and %q + +-- %s accepts only strings and numbers +assert(string.format("%s", 12) == "12") +assert(string.format("%s|%s", 12, 0.5) == "12|0.5") +assert(string.format("%s", 1e15) == "1e+15") +assert(not pcall(string.format, "%s", nil)) +assert(not pcall(string.format, "%s", true)) +assert(not pcall(string.format, "%s", {})) + +-- %s stops at an embedded NUL byte ... +local withnul = "a" .. string.char(0) .. "b" +assert(#string.format("%s", withnul) == 1) +-- ... unless the string is 100 bytes or longer and no precision is given, +-- in which case it is passed through whole +local long = string.rep("x", 60) .. string.char(0) .. string.rep("y", 60) +assert(#string.format("%s", long) == 121) +assert(#string.format("%.9s", long) == 9) +local plain = string.rep("ab", 60) +assert(string.format("%99s", plain) == plain) +assert(#string.format("%99s", "x") == 99) +assert(string.format("%.4s", plain) == "abab") + +-- %s width and precision count bytes, not characters +local twobyte = string.char(195, 169) -- a two-byte UTF-8 sequence +assert(#string.format("%.1s", twobyte) == 1) +assert(#string.format("%.2s", twobyte) == 2) +assert(string.format("%3s|", twobyte) == " " .. twobyte .. "|") +assert(#string.format("%4s", twobyte .. twobyte) == 4) + +-- %c writes its value as a single byte; byte zero adds nothing +assert(string.format("%c", 72) == "H") +assert(string.format("a%cb", 0) == "ab") +assert(#string.format("%c", 256) == 0) +assert(string.format("%-3c|", 65) == "A |") + +-- %q escapes: '"', '\\' and newline keep their backslash (the newline stays +-- a real newline), '\r' becomes \r, NUL becomes the three-digit \000; +-- %q also accepts numbers and ignores any width +assert(string.format("%q", "a\nb") == '"a\\\nb"') +assert(string.format("%q", "a\rb") == '"a\\rb"') +assert(string.format("%q", "x" .. string.char(0) .. "y") == '"x\\000y"') +assert(string.format("%q", 42) == '"42"') +assert(string.format("%5q", "x") == '"x"') +assert(#string.format("%q", string.rep("x", 150)) == 152) +assert(not pcall(string.format, "%q", {})) + +-- %q output always round-trips through loadstring +for i = 0, 255 do + local orig = "a" .. string.char(i) .. "b" + local f = loadstring("return " .. string.format("%q", orig)) + assert(f ~= nil and f() == orig) +end diff --git a/_lua5.1-tests/strings.lua b/_lua5.1-tests/strings.lua index 9172f563..c607a841 100644 --- a/_lua5.1-tests/strings.lua +++ b/_lua5.1-tests/strings.lua @@ -99,10 +99,10 @@ assert(tostring(false) == "false") print('+') x = '"ílo"\n\\' --- assert(string.format('%q%s', x, x) == '"\\"ílo\\"\\\n\\\\""ílo"\n\\') --- assert(string.format('%q', "\0") == [["\000"]]) ---assert(string.format("\0%c\0%c%x\0", string.byte("á"), string.byte("b"), 140) == --- "\0á\0b8c\0") +assert(string.format('%q%s', x, x) == '"\\"ílo\\"\\\n\\\\""ílo"\n\\') +assert(string.format('%q', "\0") == [["\000"]]) +assert(string.format("\0%c\0%c%x\0", string.byte("á"), string.byte("b"), 140) == + "\0á\0b8c\0") assert(string.format('') == "") assert(string.format("%c",34)..string.format("%c",48)..string.format("%c",90)..string.format("%c",100) == string.format("%c%c%c%c", 34, 48, 90, 100)) diff --git a/alloc.go b/alloc.go index 7a8cd63a..4b994955 100644 --- a/alloc.go +++ b/alloc.go @@ -1,6 +1,7 @@ package lua import ( + "math" "reflect" "unsafe" ) @@ -55,8 +56,10 @@ func newAllocator(size int) *allocator { // The downside of this is that all of the floats on a given block have to become eligible for gc before the block // as a whole can be gc-ed. func (al *allocator) LNumber2I(v LNumber) LValue { - // first check for shared preloaded numbers - if v >= 0 && v < preloadLimit && float64(v) == float64(int64(v)) { + // first check for shared preloaded numbers; negative zero compares equal + // to zero but must not share the preloaded +0, or it loses its sign + // (tostring(-0.0) is "-0" in Lua 5.1) + if v >= 0 && v < preloadLimit && float64(v) == float64(int64(v)) && !math.Signbit(float64(v)) { return preloads[int(v)] } diff --git a/mathlib.go b/mathlib.go index e612f2f0..28540591 100644 --- a/mathlib.go +++ b/mathlib.go @@ -8,7 +8,7 @@ import ( func OpenMath(L *LState) int { mod := L.RegisterModule(MathLibName, mathFuncs).(*LTable) mod.RawSetString("pi", LNumber(math.Pi)) - mod.RawSetString("huge", LNumber(math.MaxFloat64)) + mod.RawSetString("huge", LNumber(math.Inf(1))) L.Push(mod) return 1 } diff --git a/stringlib.go b/stringlib.go index f484c2b3..2f144ac9 100644 --- a/stringlib.go +++ b/stringlib.go @@ -133,15 +133,144 @@ func strFind(L *LState) int { return md.CaptureLength()/2 + 1 } +// strFormatQuoted implements the %q conversion the way Lua 5.1's addquoted +// (lstrlib.c) does: '"', '\\' and newline keep a backslash (the newline stays +// a real newline byte), '\r' becomes \r, a NUL byte becomes the three-digit +// \000, and every other byte is copied through untouched. +func strFormatQuoted(buf *[]byte, s string) { + *buf = append(*buf, '"') + for i := 0; i < len(s); i++ { + c := s[i] + switch c { + case '"', '\\', '\n': + *buf = append(*buf, '\\', c) + case '\r': + *buf = append(*buf, '\\', 'r') + case 0: + *buf = append(*buf, '\\', '0', '0', '0') + default: + *buf = append(*buf, c) + } + } + *buf = append(*buf, '"') +} + +func strFormatPad(buf *[]byte, item string, width int, leftAlign bool) { + pad := width - len(item) + if !leftAlign { + for ; pad > 0; pad-- { + *buf = append(*buf, ' ') + } + } + *buf = append(*buf, item...) + if leftAlign { + for ; pad > 0; pad-- { + *buf = append(*buf, ' ') + } + } +} + func strFormat(L *LState) int { str := L.CheckString(1) - args := make([]interface{}, L.GetTop()-1) - top := L.GetTop() - for i := 2; i <= top; i++ { - args[i-2] = L.Get(i) + arg := 1 + out := make([]byte, 0, len(str)+16) + i := 0 + for i < len(str) { + c := str[i] + if c != '%' { + out = append(out, c) + i++ + continue + } + spec := i + i++ + if i < len(str) && str[i] == '%' { + out = append(out, '%') + i++ + continue + } + leftAlign := false + for i < len(str) && strings.IndexByte("-+ #0", str[i]) >= 0 { + if str[i] == '-' { + leftAlign = true + } + i++ + } + width := -1 + for i < len(str) && str[i] >= '0' && str[i] <= '9' { + if width < 0 { + width = 0 + } + width = width*10 + int(str[i]-'0') + i++ + } + prec := -1 + if i < len(str) && str[i] == '.' { + i++ + prec = 0 + for i < len(str) && str[i] >= '0' && str[i] <= '9' { + prec = prec*10 + int(str[i]-'0') + i++ + } + } + if i >= len(str) { + L.RaiseError("invalid format string to 'format'") + } + d := str[i] + i++ + arg++ + switch d { + case 's': + var ss string + switch v := L.Get(arg).(type) { + case LString: + ss = string(v) + case LNumber: + ss = v.String() + default: + L.TypeError(arg, LTString) + } + // Lua 5.1's str_format keeps a string with no precision and + // length >= 100 whole (too long for its C sprintf buffer), so + // only shorter strings are cut at the first NUL byte the way + // C sprintf's %s would cut them. + if prec < 0 && len(ss) >= 100 { + strFormatPad(&out, ss, width, leftAlign) + break + } + if i0 := strings.IndexByte(ss, 0); i0 >= 0 { + ss = ss[:i0] + } + if prec >= 0 && len(ss) > prec { + ss = ss[:prec] + } + strFormatPad(&out, ss, width, leftAlign) + case 'q': + var qs string + switch v := L.Get(arg).(type) { + case LString: + qs = string(v) + case LNumber: + qs = v.String() + default: + L.TypeError(arg, LTString) + } + strFormatQuoted(&out, qs) + case 'c': + // Lua 5.1 writes (char)n through sprintf and measures the + // result with strlen, so %c emits a single byte and a zero + // byte adds nothing. + n := int64(L.CheckNumber(arg)) + item := "" + if b := byte(n); b != 0 { + item = string([]byte{b}) + } + strFormatPad(&out, item, width, leftAlign) + default: + out = append(out, []byte(fmt.Sprintf(str[spec:i], L.Get(arg)))...) + } } - npat := strings.Count(str, "%") - strings.Count(str, "%%") - L.Push(LString(fmt.Sprintf(str, args[:intMin(npat, len(args))]...))) + L.Push(LString(string(out))) return 1 } diff --git a/value.go b/value.go index 4156e9d5..5a979d79 100644 --- a/value.go +++ b/value.go @@ -3,6 +3,7 @@ package lua import ( "context" "fmt" + "math" "os" ) @@ -111,11 +112,24 @@ func (st LString) Format(f fmt.State, c rune) { } } +// String renders the number the way the reference Lua 5.1 interpreter does: +// lua_number2str uses LUAI_NUMFMT "%.14g" (luaconf.h), and infinities and +// NaNs print as C's printf spells them ("inf", "-inf", "nan", "-nan"). func (nm LNumber) String() string { - if isInteger(nm) { - return fmt.Sprint(int64(nm)) + f := float64(nm) + if math.IsInf(f, 1) { + return "inf" } - return fmt.Sprint(float64(nm)) + if math.IsInf(f, -1) { + return "-inf" + } + if math.IsNaN(f) { + if math.Signbit(f) { + return "-nan" + } + return "nan" + } + return fmt.Sprintf("%.14g", f) } func (nm LNumber) Type() LValueType { return LTNumber }