Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions _glua-tests/base.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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")
11 changes: 11 additions & 0 deletions _glua-tests/math.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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")
54 changes: 54 additions & 0 deletions _glua-tests/strings.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 4 additions & 4 deletions _lua5.1-tests/strings.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
7 changes: 5 additions & 2 deletions alloc.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package lua

import (
"math"
"reflect"
"unsafe"
)
Expand Down Expand Up @@ -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)]
}

Expand Down
2 changes: 1 addition & 1 deletion mathlib.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
141 changes: 135 additions & 6 deletions stringlib.go
Original file line number Diff line number Diff line change
Expand Up @@ -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++
Comment on lines +199 to +205

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve Lua 5.1's two-digit format limits

The scanner accepts a third width digit here and a third precision digit in the loop below. Consequently pcall(string.format, "%100s", "xy") and pcall(string.format, "%.100s", "xy") both succeed on the head; C Lua 5.1.5 rejects them with invalid format (width or precision too long). The %99s and %.99s controls succeed on both. Please mirror scanformat's two-digit limits for width and precision and add these boundary cases. This is also an existing compatibility gap on the base; the new scanner has not closed it.

}
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)
Comment on lines +264 to +268

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Apply %c width before truncating its NUL byte

With zero and a field width, removing the byte before padding changes the result: on this head string.format("L%3c|R", 0) returns "L |R", while Lua 5.1.5 returns "L |R". For "L%-3c|R", the reference returns "L|R", but this branch again inserts three spaces. The same applies to 256 after the byte conversion. Lua pads the one-byte character with sprintf first, then takes strlen of that formatted item. Please pad the one-byte item before appending its prefix up to the first NUL, and cover both alignments. The base also differed from Lua here; this is a remaining gap in the NUL handling rather than a new regression.

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
}

Expand Down
20 changes: 17 additions & 3 deletions value.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package lua
import (
"context"
"fmt"
"math"
"os"
)

Expand Down Expand Up @@ -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 }
Expand Down