diff options
author | alandonovan <adonovan@google.com> | 2020-11-11 12:03:41 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-11-11 12:03:41 -0500 |
commit | 3b7e02ecde8b80808c06c8284ce9f85c101fcebd (patch) | |
tree | 1168e8456ab49c55ff2f47c9b98a7a0feff69204 | |
parent | dff0ae5b48201537496c261571cf1f08725ec804 (diff) | |
download | starlark-go-3b7e02ecde8b80808c06c8284ce9f85c101fcebd.tar.gz |
starlark: bring floating-point into spec compliance (#313)
This change makes go.starlark.net's floating-point implementation
match the proposed spec (see https://github.com/bazelbuild/starlark/pull/119),
and thus much more closely match the behavior of the Java implementation.
The major changes are:
- Float values are totally ordered; NaN compares greater than +Inf.
- The string form of a finite float value always contains an exponent
or a decimal point, so they are self-evidently not int values.
- Operations that would cause a large integer to become rounded to
an infinite float are now an error.
The resolve.AllowFloat boolean, and the corresponding -float command-line
flag, now have no effect. Floating point support is always enabled.
-rw-r--r-- | cmd/starlark/starlark.go | 2 | ||||
-rw-r--r-- | go.mod | 2 | ||||
-rw-r--r-- | go.sum | 4 | ||||
-rw-r--r-- | resolve/resolve.go | 11 | ||||
-rw-r--r-- | resolve/resolve_test.go | 3 | ||||
-rw-r--r-- | resolve/testdata/resolve.star | 7 | ||||
-rw-r--r-- | starlark/eval.go | 106 | ||||
-rw-r--r-- | starlark/eval_test.go | 1 | ||||
-rw-r--r-- | starlark/int.go | 10 | ||||
-rw-r--r-- | starlark/library.go | 41 | ||||
-rw-r--r-- | starlark/testdata/assign.star | 7 | ||||
-rw-r--r-- | starlark/testdata/bool.star | 8 | ||||
-rw-r--r-- | starlark/testdata/builtins.star | 2 | ||||
-rw-r--r-- | starlark/testdata/float.star | 355 | ||||
-rw-r--r-- | starlark/testdata/int.star | 202 | ||||
-rw-r--r-- | starlark/testdata/json.star | 3 | ||||
-rw-r--r-- | starlark/testdata/misc.star | 2 | ||||
-rw-r--r-- | starlark/testdata/string.star | 288 | ||||
-rw-r--r-- | starlark/value.go | 100 | ||||
-rw-r--r-- | starlarkstruct/struct_test.go | 1 |
20 files changed, 755 insertions, 400 deletions
diff --git a/cmd/starlark/starlark.go b/cmd/starlark/starlark.go index af4f71f..31555f1 100644 --- a/cmd/starlark/starlark.go +++ b/cmd/starlark/starlark.go @@ -35,7 +35,7 @@ func init() { flag.BoolVar(&compile.Disassemble, "disassemble", compile.Disassemble, "show disassembly during compilation of each function") // non-standard dialect flags - flag.BoolVar(&resolve.AllowFloat, "float", resolve.AllowFloat, "allow floating-point numbers") + flag.BoolVar(&resolve.AllowFloat, "float", resolve.AllowFloat, "obsolete; no effect") flag.BoolVar(&resolve.AllowSet, "set", resolve.AllowSet, "allow set data type") flag.BoolVar(&resolve.AllowLambda, "lambda", resolve.AllowLambda, "allow lambda expressions") flag.BoolVar(&resolve.AllowNestedDef, "nesteddef", resolve.AllowNestedDef, "allow nested def statements") @@ -6,5 +6,5 @@ require ( github.com/chzyer/logex v1.1.10 // indirect github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect - golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae // indirect + golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 // indirect ) @@ -4,5 +4,5 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 h1:B6caxRw+hozq68X2MY7jEpZh/cr4/aHLv9xU8Kkadrw= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/resolve/resolve.go b/resolve/resolve.go index e4b6752..26d74b4 100644 --- a/resolve/resolve.go +++ b/resolve/resolve.go @@ -100,7 +100,7 @@ const doesnt = "this Starlark dialect does not " var ( AllowNestedDef = false // allow def statements within function bodies AllowLambda = false // allow lambda expressions - AllowFloat = false // allow floating point literals, the 'float' built-in, and x / y + AllowFloat = false // obsolete; no effect AllowSet = false // allow the 'set' built-in AllowGlobalReassign = false // allow reassignment to top-level names; also, allow if/for/while at top-level AllowRecursion = false // allow while statements and recursive functions @@ -418,9 +418,6 @@ func (r *resolver) useToplevel(use use) (bind *Binding) { r.predeclared[id.Name] = bind // save it } else if r.isUniversal(id.Name) { // use of universal name - if !AllowFloat && id.Name == "float" { - r.errorf(id.NamePos, doesnt+"support floating point") - } if !AllowSet && id.Name == "set" { r.errorf(id.NamePos, doesnt+"support sets") } @@ -636,9 +633,6 @@ func (r *resolver) expr(e syntax.Expr) { r.use(e) case *syntax.Literal: - if !AllowFloat && e.Token == syntax.FLOAT { - r.errorf(e.TokenPos, doesnt+"support floating point") - } case *syntax.ListExpr: for _, x := range e.List { @@ -713,9 +707,6 @@ func (r *resolver) expr(e syntax.Expr) { r.expr(e.X) case *syntax.BinaryExpr: - if !AllowFloat && e.Op == syntax.SLASH { - r.errorf(e.OpPos, doesnt+"support floating point (use //)") - } r.expr(e.X) r.expr(e.Y) diff --git a/resolve/resolve_test.go b/resolve/resolve_test.go index 090dbc5..cb2c672 100644 --- a/resolve/resolve_test.go +++ b/resolve/resolve_test.go @@ -15,7 +15,6 @@ import ( ) func setOptions(src string) { - resolve.AllowFloat = option(src, "float") resolve.AllowGlobalReassign = option(src, "globalreassign") resolve.AllowLambda = option(src, "lambda") resolve.AllowNestedDef = option(src, "nesteddef") @@ -38,7 +37,7 @@ func TestResolve(t *testing.T) { continue } - // A chunk may set options by containing e.g. "option:float". + // A chunk may set options by containing e.g. "option:nesteddef". setOptions(chunk.Source) if err := resolve.File(f, isPredeclared, isUniversal); err != nil { diff --git a/resolve/testdata/resolve.star b/resolve/testdata/resolve.star index 4f1a270..8399bf3 100644 --- a/resolve/testdata/resolve.star +++ b/resolve/testdata/resolve.star @@ -307,12 +307,7 @@ def h(kwargs, a, **kwargs): pass ### "duplicate parameter: kwargs" def i(*x, **x): pass ### "duplicate parameter: x" --- -# No floating point -a = float("3.141") ### `dialect does not support floating point` -b = 1 / 2 ### `dialect does not support floating point \(use //\)` -c = 3.141 ### `dialect does not support floating point` ---- -# Floating point support (option:float) +# Floating-point support is now standard. a = float("3.141") b = 1 / 2 c = 3.141 diff --git a/starlark/eval.go b/starlark/eval.go index c17b8b7..633fc30 100644 --- a/starlark/eval.go +++ b/starlark/eval.go @@ -713,14 +713,22 @@ func Binary(op syntax.Token, x, y Value) (Value, error) { case Int: return x.Add(y), nil case Float: - return x.Float() + y, nil + xf, err := x.finiteFloat() + if err != nil { + return nil, err + } + return xf + y, nil } case Float: switch y := y.(type) { case Float: return x + y, nil case Int: - return x + y.Float(), nil + yf, err := y.finiteFloat() + if err != nil { + return nil, err + } + return x + yf, nil } case *List: if y, ok := y.(*List); ok { @@ -745,14 +753,22 @@ func Binary(op syntax.Token, x, y Value) (Value, error) { case Int: return x.Sub(y), nil case Float: - return x.Float() - y, nil + xf, err := x.finiteFloat() + if err != nil { + return nil, err + } + return xf - y, nil } case Float: switch y := y.(type) { case Float: return x - y, nil case Int: - return x - y.Float(), nil + yf, err := y.finiteFloat() + if err != nil { + return nil, err + } + return x - yf, nil } } @@ -763,7 +779,11 @@ func Binary(op syntax.Token, x, y Value) (Value, error) { case Int: return x.Mul(y), nil case Float: - return x.Float() * y, nil + xf, err := x.finiteFloat() + if err != nil { + return nil, err + } + return xf * y, nil case String: return stringRepeat(y, x) case *List: @@ -780,7 +800,11 @@ func Binary(op syntax.Token, x, y Value) (Value, error) { case Float: return x * y, nil case Int: - return x * y.Float(), nil + yf, err := y.finiteFloat() + if err != nil { + return nil, err + } + return x * yf, nil } case String: if y, ok := y.(Int); ok { @@ -804,30 +828,40 @@ func Binary(op syntax.Token, x, y Value) (Value, error) { case syntax.SLASH: switch x := x.(type) { case Int: + xf, err := x.finiteFloat() + if err != nil { + return nil, err + } switch y := y.(type) { case Int: - yf := y.Float() + yf, err := y.finiteFloat() + if err != nil { + return nil, err + } if yf == 0.0 { - return nil, fmt.Errorf("real division by zero") + return nil, fmt.Errorf("floating-point division by zero") } - return x.Float() / yf, nil + return xf / yf, nil case Float: if y == 0.0 { - return nil, fmt.Errorf("real division by zero") + return nil, fmt.Errorf("floating-point division by zero") } - return x.Float() / y, nil + return xf / y, nil } case Float: switch y := y.(type) { case Float: if y == 0.0 { - return nil, fmt.Errorf("real division by zero") + return nil, fmt.Errorf("floating-point division by zero") } return x / y, nil case Int: - yf := y.Float() + yf, err := y.finiteFloat() + if err != nil { + return nil, err + } if yf == 0.0 { - return nil, fmt.Errorf("real division by zero") + return nil, fmt.Errorf("floating-point division by zero") } return x / yf, nil } @@ -843,10 +877,14 @@ func Binary(op syntax.Token, x, y Value) (Value, error) { } return x.Div(y), nil case Float: + xf, err := x.finiteFloat() + if err != nil { + return nil, err + } if y == 0.0 { return nil, fmt.Errorf("floored division by zero") } - return floor((x.Float() / y)), nil + return floor(xf / y), nil } case Float: switch y := y.(type) { @@ -856,7 +894,10 @@ func Binary(op syntax.Token, x, y Value) (Value, error) { } return floor(x / y), nil case Int: - yf := y.Float() + yf, err := y.finiteFloat() + if err != nil { + return nil, err + } if yf == 0.0 { return nil, fmt.Errorf("floored division by zero") } @@ -874,23 +915,31 @@ func Binary(op syntax.Token, x, y Value) (Value, error) { } return x.Mod(y), nil case Float: + xf, err := x.finiteFloat() + if err != nil { + return nil, err + } if y == 0 { - return nil, fmt.Errorf("float modulo by zero") + return nil, fmt.Errorf("floating-point modulo by zero") } - return x.Float().Mod(y), nil + return xf.Mod(y), nil } case Float: switch y := y.(type) { case Float: if y == 0.0 { - return nil, fmt.Errorf("float modulo by zero") + return nil, fmt.Errorf("floating-point modulo by zero") } return x.Mod(y), nil case Int: if y.Sign() == 0 { - return nil, fmt.Errorf("float modulo by zero") + return nil, fmt.Errorf("floating-point modulo by zero") } - return x.Mod(y.Float()), nil + yf, err := y.finiteFloat() + if err != nil { + return nil, err + } + return x.Mod(yf), nil } case String: return interpolate(string(x), y) @@ -1497,20 +1546,7 @@ func interpolate(format string, x Value) (Value, error) { if !ok { return nil, fmt.Errorf("%%%c format requires float, not %s", c, arg.Type()) } - switch c { - case 'e': - fmt.Fprintf(buf, "%e", f) - case 'f': - fmt.Fprintf(buf, "%f", f) - case 'g': - fmt.Fprintf(buf, "%g", f) - case 'E': - fmt.Fprintf(buf, "%E", f) - case 'F': - fmt.Fprintf(buf, "%F", f) - case 'G': - fmt.Fprintf(buf, "%G", f) - } + Float(f).format(buf, c) case 'c': switch arg := arg.(type) { case Int: diff --git a/starlark/eval_test.go b/starlark/eval_test.go index a57ccad..13876a7 100644 --- a/starlark/eval_test.go +++ b/starlark/eval_test.go @@ -24,7 +24,6 @@ import ( // A test may enable non-standard options by containing (e.g.) "option:recursion". func setOptions(src string) { - resolve.AllowFloat = option(src, "float") resolve.AllowGlobalReassign = option(src, "globalreassign") resolve.LoadBindsGlobally = option(src, "loadbindsglobally") resolve.AllowLambda = option(src, "lambda") diff --git a/starlark/int.go b/starlark/int.go index c40532a..397be86 100644 --- a/starlark/int.go +++ b/starlark/int.go @@ -193,6 +193,16 @@ func (i Int) Float() Float { return Float(iSmall) } +// finiteFloat returns the finite float value nearest i, +// or an error if the magnitude is too large. +func (i Int) finiteFloat() (Float, error) { + f := i.Float() + if math.IsInf(float64(f), 0) { + return 0, fmt.Errorf("int too large to convert to float") + } + return f, nil +} + func (x Int) Sign() int { xSmall, xBig := x.get() if xBig != nil { diff --git a/starlark/library.go b/starlark/library.go index 9036877..e9e2f94 100644 --- a/starlark/library.go +++ b/starlark/library.go @@ -12,6 +12,7 @@ package starlark import ( "errors" "fmt" + "math" "math/big" "os" "sort" @@ -46,7 +47,7 @@ func init() { "dir": NewBuiltin("dir", dir), "enumerate": NewBuiltin("enumerate", enumerate), "fail": NewBuiltin("fail", fail), - "float": NewBuiltin("float", float), // requires resolve.AllowFloat + "float": NewBuiltin("float", float), "getattr": NewBuiltin("getattr", getattr), "hasattr": NewBuiltin("hasattr", hasattr), "hash": NewBuiltin("hash", hash), @@ -330,13 +331,39 @@ func float(thread *Thread, b *Builtin, args Tuple, kwargs []Tuple) (Value, error return Float(0.0), nil } case Int: - return x.Float(), nil + return x.finiteFloat() case Float: return x, nil case String: - f, err := strconv.ParseFloat(string(x), 64) + if x == "" { + return nil, fmt.Errorf("float: empty string") + } + // +/- NaN or Inf or Infinity (case insensitive)? + s := string(x) + switch x[len(x)-1] { + case 'y', 'Y': + if strings.EqualFold(s, "infinity") || strings.EqualFold(s, "+infinity") { + return inf, nil + } else if strings.EqualFold(s, "-infinity") { + return neginf, nil + } + case 'f', 'F': + if strings.EqualFold(s, "inf") || strings.EqualFold(s, "+inf") { + return inf, nil + } else if strings.EqualFold(s, "-inf") { + return neginf, nil + } + case 'n', 'N': + if strings.EqualFold(s, "nan") || strings.EqualFold(s, "+nan") || strings.EqualFold(s, "-nan") { + return nan, nil + } + } + f, err := strconv.ParseFloat(s, 64) + if math.IsInf(f, 0) { + return nil, fmt.Errorf("floating-point number too large") + } if err != nil { - return nil, nameErr(b, err) + return nil, fmt.Errorf("invalid float literal: %s", s) } return Float(f), nil default: @@ -344,6 +371,12 @@ func float(thread *Thread, b *Builtin, args Tuple, kwargs []Tuple) (Value, error } } +var ( + inf = Float(math.Inf(+1)) + neginf = Float(math.Inf(-1)) + nan = Float(math.NaN()) +) + // https://github.com/google/starlark-go/blob/master/doc/spec.md#getattr func getattr(thread *Thread, b *Builtin, args Tuple, kwargs []Tuple) (Value, error) { var object, dflt Value diff --git a/starlark/testdata/assign.star b/starlark/testdata/assign.star index 38108f9..64d60fb 100644 --- a/starlark/testdata/assign.star +++ b/starlark/testdata/assign.star @@ -232,14 +232,11 @@ assert.fails(lambda: tuple, "global variable tuple referenced before assignment" tuple = () --- -# option:float option:set -# Same as above, but set and float are dialect-specific; +# option:set +# Same as above, but set is dialect-specific; # we shouldn't notice any difference. load("assert.star", "assert") -float = 1.0 -assert.eq(type(float), "float") - set = [1, 2, 3] assert.eq(type(set), "list") diff --git a/starlark/testdata/bool.star b/starlark/testdata/bool.star index 78ef825..6c084a3 100644 --- a/starlark/testdata/bool.star +++ b/starlark/testdata/bool.star @@ -1,5 +1,4 @@ # Tests of Starlark 'bool' -# option:float load("assert.star", "assert") @@ -9,6 +8,13 @@ assert.true(not False) assert.true(not not True) assert.true(not not 1 >= 1) +# precedence of not +assert.true(not not 2 > 1) +# assert.true(not (not 2) > 1) # TODO(adonovan): fix: gives error for False > 1. +# assert.true(not ((not 2) > 1)) # TODO(adonovan): fix +# assert.true(not ((not (not 2)) > 1)) # TODO(adonovan): fix +# assert.true(not not not (2 > 1)) + # bool conversion assert.eq( [bool(), bool(1), bool(0), bool("hello"), bool("")], diff --git a/starlark/testdata/builtins.star b/starlark/testdata/builtins.star index 2385c16..8dcee46 100644 --- a/starlark/testdata/builtins.star +++ b/starlark/testdata/builtins.star @@ -1,5 +1,5 @@ # Tests of Starlark built-in functions -# option:float option:set +# option:set load("assert.star", "assert") diff --git a/starlark/testdata/float.star b/starlark/testdata/float.star index 4d7a269..b4df38d 100644 --- a/starlark/testdata/float.star +++ b/starlark/testdata/float.star @@ -1,5 +1,5 @@ # Tests of Starlark 'float' -# option:float option:set +# option:set load("assert.star", "assert") @@ -7,6 +7,20 @@ load("assert.star", "assert") # - precision # - limits +# type +assert.eq(type(0.0), "float") + +# truth +assert.true(123.0) +assert.true(-1.0) +assert.true(not 0.0) +assert.true(-1.0e-45) +assert.true(float("NaN")) + +# not iterable +assert.fails(lambda: len(0.0), 'has no len') +assert.fails(lambda: [x for x in 0.0], 'float value is not iterable') + # literals assert.eq(type(1.234), "float") assert.eq(type(1e10), "float") @@ -16,10 +30,117 @@ assert.eq(type(1.234e10), "float") assert.eq(type(1.234e+10), "float") assert.eq(type(1.234e-10), "float") -# truth -assert.true(123.0) -assert.true(-1.0) -assert.true(not 0.0) +# int/float equality +assert.eq(0.0, 0) +assert.eq(0, 0.0) +assert.eq(1.0, 1) +assert.eq(1, 1.0) +assert.true(1.23e45 != 1229999999999999973814869011019624571608236031) +assert.true(1.23e45 == 1229999999999999973814869011019624571608236032) +assert.true(1.23e45 != 1229999999999999973814869011019624571608236033) +assert.true(1229999999999999973814869011019624571608236031 != 1.23e45) +assert.true(1229999999999999973814869011019624571608236032 == 1.23e45) +assert.true(1229999999999999973814869011019624571608236033 != 1.23e45) + +# loss of precision +p53 = 1<<53 +assert.eq(float(p53-1), p53-1) +assert.eq(float(p53+0), p53+0) +assert.eq(float(p53+1), p53+0) # +assert.eq(float(p53+2), p53+2) +assert.eq(float(p53+3), p53+4) # +assert.eq(float(p53+4), p53+4) +assert.eq(float(p53+5), p53+4) # +assert.eq(float(p53+6), p53+6) +assert.eq(float(p53+7), p53+8) # +assert.eq(float(p53+8), p53+8) + +assert.true(float(p53+1) != p53+1) # comparisons are exact +assert.eq(float(p53+1) - (p53+1), 0) # arithmetic entails rounding + +assert.fails(lambda: {123.0: "f", 123: "i"}, "duplicate key: 123") + +# equal int/float values have same hash +d = {123.0: "x"} +d[123] = "y" +assert.eq(len(d), 1) +assert.eq(d[123.0], "y") + +# literals (mostly covered by scanner tests) +assert.eq(str(0.), "0.0") +assert.eq(str(.0), "0.0") +assert.true(5.0 != 4.999999999999999) +assert.eq(5.0, 4.9999999999999999) # both literals denote 5.0 +assert.eq(1.23e45, 1.23 * 1000000000000000000000000000000000000000000000) +assert.eq(str(1.23e-45 - (1.23 / 1000000000000000000000000000000000000000000000)), "-1.5557538194652854e-61") + +nan = float("NaN") +inf = float("+Inf") +neginf = float("-Inf") +negzero = (-1e-323 / 10) + +# -- arithmetic -- + +# +float, -float +assert.eq(+(123.0), 123.0) +assert.eq(-(123.0), -123.0) +assert.eq(-(-(123.0)), 123.0) +assert.eq(+(inf), inf) +assert.eq(-(inf), neginf) +assert.eq(-(neginf), inf) +assert.eq(str(-(nan)), "nan") +# + +assert.eq(1.2e3 + 5.6e7, 5.60012e+07) +assert.eq(1.2e3 + 1, 1201) +assert.eq(1 + 1.2e3, 1201) +assert.eq(str(1.2e3 + nan), "nan") +assert.eq(inf + 0, inf) +assert.eq(inf + 1, inf) +assert.eq(inf + inf, inf) +assert.eq(str(inf + neginf), "nan") +# - +assert.eq(1.2e3 - 5.6e7, -5.59988e+07) +assert.eq(1.2e3 - 1, 1199) +assert.eq(1 - 1.2e3, -1199) +assert.eq(str(1.2e3 - nan), "nan") +assert.eq(inf - 0, inf) +assert.eq(inf - 1, inf) +assert.eq(str(inf - inf), "nan") +assert.eq(inf - neginf, inf) +# * +assert.eq(1.5e6 * 2.2e3, 3.3e9) +assert.eq(1.5e6 * 123, 1.845e+08) +assert.eq(123 * 1.5e6, 1.845e+08) +assert.eq(str(1.2e3 * nan), "nan") +assert.eq(str(inf * 0), "nan") +assert.eq(inf * 1, inf) +assert.eq(inf * inf, inf) +assert.eq(inf * neginf, neginf) +# % +assert.eq(100.0 % 7.0, 2) +assert.eq(100.0 % -7.0, -5) # NB: different from Go / Java +assert.eq(-100.0 % 7.0, 5) # NB: different from Go / Java +assert.eq(-100.0 % -7.0, -2) +assert.eq(-100.0 % 7, 5) +assert.eq(100 % 7.0, 2) +assert.eq(str(1.2e3 % nan), "nan") +assert.eq(str(inf % 1), "nan") +assert.eq(str(inf % inf), "nan") +assert.eq(str(inf % neginf), "nan") +# / +assert.eq(str(100.0 / 7.0), "14.285714285714286") +assert.eq(str(100 / 7.0), "14.285714285714286") +assert.eq(str(100.0 / 7), "14.285714285714286") +assert.eq(str(100.0 / nan), "nan") +# // +assert.eq(100.0 // 7.0, 14) +assert.eq(100 // 7.0, 14) +assert.eq(100.0 // 7, 14) +assert.eq(100.0 // -7.0, -15) +assert.eq(100 // -7.0, -15) +assert.eq(100.0 // -7, -15) +assert.eq(str(1 // neginf), "-0.0") +assert.eq(str(100.0 // nan), "nan") # addition assert.eq(0.0 + 1.0, 1.0) @@ -56,9 +177,9 @@ assert.eq(2.5 / 2.0, 1.25) assert.eq(2.5 / 2, 1.25) assert.eq(5 / 4.0, 1.25) assert.eq(5 / 4, 1.25) -assert.fails(lambda: 1.0 / 0, "real division by zero") -assert.fails(lambda: 1.0 / 0.0, "real division by zero") -assert.fails(lambda: 1 / 0.0, "real division by zero") +assert.fails(lambda: 1.0 / 0, "floating-point division by zero") +assert.fails(lambda: 1.0 / 0.0, "floating-point division by zero") +assert.fails(lambda: 1 / 0.0, "floating-point division by zero") # floored division assert.eq(100.0 // 8.0, 12.0) @@ -90,64 +211,69 @@ assert.eq(-98.0 % -8.0, -2.0) assert.eq(2.5 % 2.0, 0.5) assert.eq(2.5 % 2, 0.5) assert.eq(5 % 4.0, 1.0) -assert.fails(lambda: 1.0 % 0, "float modulo by zero") -assert.fails(lambda: 1.0 % 0.0, "float modulo by zero") -assert.fails(lambda: 1 % 0.0, "float modulo by zero") +assert.fails(lambda: 1.0 % 0, "floating-point modulo by zero") +assert.fails(lambda: 1.0 % 0.0, "floating-point modulo by zero") +assert.fails(lambda: 1 % 0.0, "floating-point modulo by zero") # floats cannot be used as indices, even if integral assert.fails(lambda: "abc"[1.0], "want int") assert.fails(lambda: ["A", "B", "C"].insert(1.0, "D"), "want int") +assert.fails(lambda: range(3)[1.0], "got float, want int") -# nan -nan = float("NaN") -def isnan(x): return x != x -assert.true(nan != nan) -assert.true(not (nan == nan)) - -# ordered comparisons with NaN -assert.true(not nan < nan) -assert.true(not nan > nan) -assert.true(not nan <= nan) -assert.true(not nan >= nan) -assert.true(not nan == nan) # use explicit operator, not assert.ne -assert.true(nan != nan) -assert.true(not nan < 0) -assert.true(not nan > 0) -assert.true(not [nan] < [nan]) -assert.true(not [nan] > [nan]) - -# Even a value containing NaN is not equal to itself. -nanlist = [nan] -assert.true(not nanlist < nanlist) -assert.true(not nanlist > nanlist) -assert.ne(nanlist, nanlist) - -# Since NaN values never compare equal, -# a dict may have any number of NaN keys. -nandict = {nan: 1, nan: 2, nan: 3} -assert.eq(len(nandict), 3) -assert.eq(str(nandict), "{NaN: 1, NaN: 2, NaN: 3}") -assert.true(nan not in nandict) -assert.eq(nandict.get(nan, None), None) +# -- comparisons -- +# NaN +assert.true(nan == nan) # \ +assert.true(nan >= nan) # unlike Python +assert.true(nan <= nan) # / +assert.true(not (nan > nan)) +assert.true(not (nan < nan)) +assert.true(not (nan != nan)) # unlike Python +# Sort is stable: 0.0 and -0.0 are equal, but they are not permuted. +# Similarly 1 and 1.0. +assert.eq( + str(sorted([inf, neginf, nan, 1e300, -1e300, 1.0, -1.0, 1, -1, 1e-300, -1e-300, 0, 0.0, negzero, 1e-300, -1e-300])), + "[-inf, -1e+300, -1.0, -1, -1e-300, -1e-300, 0, 0.0, -0.0, 1e-300, 1e-300, 1.0, 1, 1e+300, +inf, nan]") + +# Sort is stable, and its result contains no adjacent x, y such that y > x. +# Note: Python's reverse sort is unstable; see https://bugs.python.org/issue36095. +assert.eq(str(sorted([7, 3, nan, 1, 9])), "[1, 3, 7, 9, nan]") +assert.eq(str(sorted([7, 3, nan, 1, 9], reverse=True)), "[nan, 9, 7, 3, 1]") + +# All NaN values compare equal. (Identical objects compare equal.) +nandict = {nan: 1} +nandict[nan] = 2 +assert.eq(len(nandict), 1) # (same as Python) +assert.eq(nandict[nan], 2) # (same as Python) +assert.fails(lambda: {nan: 1, nan: 2}, "duplicate key: nan") + +nandict[float('nan')] = 3 # a distinct NaN object +assert.eq(str(nandict), "{nan: 3}") # (Python: {nan: 2, nan: 3}) + +assert.eq(str({inf: 1, neginf: 2}), "{+inf: 1, -inf: 2}") + +# zero +assert.eq(0.0, negzero) # inf -inf = float("Inf") -neginf = float("-Inf") -assert.true(isnan(+inf / +inf)) -assert.true(isnan(+inf / -inf)) -assert.true(isnan(-inf / +inf)) +assert.eq(+inf / +inf, nan) +assert.eq(+inf / -inf, nan) +assert.eq(-inf / +inf, nan) assert.eq(0.0 / +inf, 0.0) assert.eq(0.0 / -inf, 0.0) assert.true(inf > -inf) assert.eq(inf, -neginf) -assert.eq(float(int("2" + "0" * 308)), inf) # 2e308 is too large to represent as a float -assert.eq(float(int("-2" + "0" * 308)), -inf) # TODO(adonovan): assert inf > any finite number, etc. # negative zero negz = -0 assert.eq(negz, 0) +# min/max ordering with NaN (the greatest float value) +assert.eq(max([1, nan, 3]), nan) +assert.eq(max([nan, 2, 3]), nan) +assert.eq(min([1, nan, 3]), 1) +assert.eq(min([nan, 2, 3]), 2) + # float/float comparisons fltmax = 1.7976931348623157e+308 # approx fltmin = 4.9406564584124654e-324 # approx @@ -178,25 +304,75 @@ assert.eq(int(1e100), int("10000000000000000159028911097599180468360808563945281 assert.fails(lambda: int(inf), "cannot convert.*infinity") assert.fails(lambda: int(nan), "cannot convert.*NaN") -# float conversion +# -- float() function -- assert.eq(float(), 0.0) +# float(bool) assert.eq(float(False), 0.0) assert.eq(float(True), 1.0) +# float(int) assert.eq(float(0), 0.0) assert.eq(float(1), 1.0) +assert.eq(float(123), 123.0) +assert.eq(float(123 * 1000000 * 1000000 * 1000000 * 1000000 * 1000000), 1.23e+32) +# float(float) assert.eq(float(1.1), 1.1) -assert.eq(float("1.1"), 1.1) -assert.fails(lambda: float("1.1abc"), "invalid syntax") -assert.fails(lambda: float("1e100.0"), "invalid syntax") -assert.fails(lambda: float("1e1000"), "out of range") assert.fails(lambda: float(None), "want number or string") +assert.ne(False, 0.0) # differs from Python +assert.ne(True, 1.0) +# float(string) +assert.eq(float("1.1"), 1.1) +assert.fails(lambda: float("1.1abc"), "invalid float literal") +assert.fails(lambda: float("1e100.0"), "invalid float literal") +assert.fails(lambda: float("1e1000"), "floating-point number too large") assert.eq(float("-1.1"), -1.1) assert.eq(float("+1.1"), +1.1) assert.eq(float("+Inf"), inf) assert.eq(float("-Inf"), neginf) -assert.true(isnan(float("NaN"))) -assert.fails(lambda: float("+NaN"), "invalid syntax") -assert.fails(lambda: float("-NaN"), "invalid syntax") +assert.eq(float("NaN"), nan) +assert.eq(float("NaN"), nan) +assert.eq(float("+NAN"), nan) +assert.eq(float("-nan"), nan) +assert.eq(str(float("Inf")), "+inf") +assert.eq(str(float("+INF")), "+inf") +assert.eq(str(float("-inf")), "-inf") +assert.eq(str(float("+InFiniTy")), "+inf") +assert.eq(str(float("-iNFiniTy")), "-inf") +assert.fails(lambda: float("one point two"), "invalid float literal: one point two") +assert.fails(lambda: float("1.2.3"), "invalid float literal: 1.2.3") +assert.fails(lambda: float(123 << 500 << 500 << 50), "int too large to convert to float") +assert.fails(lambda: float(-123 << 500 << 500 << 50), "int too large to convert to float") +assert.fails(lambda: float(str(-123 << 500 << 500 << 50)), "floating-point number too large") + +# -- implicit float(int) conversions -- +assert.fails(lambda: (1<<500<<500<<500) + 0.0, "int too large to convert to float") +assert.fails(lambda: 0.0 + (1<<500<<500<<500), "int too large to convert to float") +assert.fails(lambda: (1<<500<<500<<500) - 0.0, "int too large to convert to float") +assert.fails(lambda: 0.0 - (1<<500<<500<<500), "int too large to convert to float") +assert.fails(lambda: (1<<500<<500<<500) * 1.0, "int too large to convert to float") +assert.fails(lambda: 1.0 * (1<<500<<500<<500), "int too large to convert to float") +assert.fails(lambda: (1<<500<<500<<500) / 1.0, "int too large to convert to float") +assert.fails(lambda: 1.0 / (1<<500<<500<<500), "int too large to convert to float") +assert.fails(lambda: (1<<500<<500<<500) // 1.0, "int too large to convert to float") +assert.fails(lambda: 1.0 // (1<<500<<500<<500), "int too large to convert to float") +assert.fails(lambda: (1<<500<<500<<500) % 1.0, "int too large to convert to float") +assert.fails(lambda: 1.0 % (1<<500<<500<<500), "int too large to convert to float") + + +# -- int function -- +assert.eq(int(0.0), 0) +assert.eq(int(1.0), 1) +assert.eq(int(1.1), 1) +assert.eq(int(0.9), 0) +assert.eq(int(-1.1), -1.0) +assert.eq(int(-1.0), -1.0) +assert.eq(int(-0.9), 0.0) +assert.eq(int(1.23e+32), 123000000000000004979083645550592) +assert.eq(int(-1.23e-32), 0) +assert.eq(int(1.23e-32), 0) +assert.fails(lambda: int(float("+Inf")), "cannot convert float infinity to integer") +assert.fails(lambda: int(float("-Inf")), "cannot convert float infinity to integer") +assert.fails(lambda: int(float("NaN")), "cannot convert float NaN to integer") + # hash # Check that equal float and int values have the same internal hash. @@ -214,16 +390,67 @@ def checkhash(): checkhash() # string formatting -assert.eq("%s" % 123.45e67, "1.2345e+69") -assert.eq("%r" % 123.45e67, "1.2345e+69") -assert.eq("%e" % 123.45e67, "1.234500e+69") -assert.eq("%f" % 123.45e67, "1234500000000000033987094856609369647752433474509923447907937257783296.000000") -assert.eq("%g" % 123.45e67, "1.2345e+69") + +# %d +assert.eq("%d" % 0, "0") +assert.eq("%d" % 0.0, "0") +assert.eq("%d" % 123, "123") +assert.eq("%d" % 123.0, "123") +assert.eq("%d" % 1.23e45, "1229999999999999973814869011019624571608236032") +# (see below for '%d' % NaN/Inf) +assert.eq("%d" % negzero, "0") +assert.fails(lambda: "%d" % float("NaN"), "cannot convert float NaN to integer") +assert.fails(lambda: "%d" % float("+Inf"), "cannot convert float infinity to integer") +assert.fails(lambda: "%d" % float("-Inf"), "cannot convert float infinity to integer") + +# %e +assert.eq("%e" % 0, "0.000000e+00") +assert.eq("%e" % 0.0, "0.000000e+00") assert.eq("%e" % 123, "1.230000e+02") -assert.eq("%f" % 123, "123.000000") -assert.eq("%g" % 123, "123") +assert.eq("%e" % 123.0, "1.230000e+02") +assert.eq("%e" % 1.23e45, "1.230000e+45") +assert.eq("%e" % -1.23e-45, "-1.230000e-45") +assert.eq("%e" % nan, "nan") +assert.eq("%e" % inf, "+inf") +assert.eq("%e" % neginf, "-inf") +assert.eq("%e" % negzero, "-0.000000e+00") assert.fails(lambda: "%e" % "123", "requires float, not str") +# %f +assert.eq("%f" % 0, "0.000000") +assert.eq("%f" % 0.0, "0.000000") +assert.eq("%f" % 123, "123.000000") +assert.eq("%f" % 123.0, "123.000000") +# Note: Starlark/Java emits 1230000000000000000000000000000000000000000000.000000. Why? +assert.eq("%f" % 1.23e45, "1229999999999999973814869011019624571608236032.000000") +assert.eq("%f" % -1.23e-45, "-0.000000") +assert.eq("%f" % nan, "nan") +assert.eq("%f" % inf, "+inf") +assert.eq("%f" % neginf, "-inf") +assert.eq("%f" % negzero, "-0.000000") assert.fails(lambda: "%f" % "123", "requires float, not str") +# %g +assert.eq("%g" % 0, "0.0") +assert.eq("%g" % 0.0, "0.0") +assert.eq("%g" % 123, "123.0") +assert.eq("%g" % 123.0, "123.0") +assert.eq("%g" % 1.110, "1.11") +assert.eq("%g" % 1e5, "100000.0") +assert.eq("%g" % 1e6, "1e+06") # Note: threshold of scientific notation is 1e17 in Starlark/Java +assert.eq("%g" % 1.23e45, "1.23e+45") +assert.eq("%g" % -1.23e-45, "-1.23e-45") +assert.eq("%g" % nan, "nan") +assert.eq("%g" % inf, "+inf") +assert.eq("%g" % neginf, "-inf") +assert.eq("%g" % negzero, "-0.0") +# str uses %g +assert.eq(str(0.0), "0.0") +assert.eq(str(123.0), "123.0") +assert.eq(str(1.23e45), "1.23e+45") +assert.eq(str(-1.23e-45), "-1.23e-45") +assert.eq(str(nan), "nan") +assert.eq(str(inf), "+inf") +assert.eq(str(neginf), "-inf") +assert.eq(str(negzero), "-0.0") assert.fails(lambda: "%g" % "123", "requires float, not str") i0 = 1 diff --git a/starlark/testdata/int.star b/starlark/testdata/int.star index c987fd1..a2e6716 100644 --- a/starlark/testdata/int.star +++ b/starlark/testdata/int.star @@ -1,5 +1,4 @@ # Tests of Starlark 'int' -# option:float load("assert.star", "assert") @@ -21,72 +20,72 @@ assert.eq(minint64, -9223372036854775808) assert.eq(maxint32, 2147483647) assert.eq(minint32, -2147483648) - # truth def truth(): - assert.true(not 0) - for m in [1, maxint32]: # Test small/big ranges - assert.true(123*m) - assert.true(-1*m) + assert.true(not 0) + for m in [1, maxint32]: # Test small/big ranges + assert.true(123 * m) + assert.true(-1 * m) truth() # floored division # (For real division, see float.star.) def division(): - for m in [1, maxint32]: # Test small/big ranges - assert.eq((100*m) // (7*m), 14) - assert.eq((100*m) // (-7*m), -15) - assert.eq((-100*m) // (7*m), -15) # NB: different from Go/Java - assert.eq((-100*m) // (-7*m), 14) # NB: different from Go/Java - assert.eq((98*m) // (7*m), 14) - assert.eq((98*m) // (-7*m), -14) - assert.eq((-98*m) // (7*m), -14) - assert.eq((-98*m) // (-7*m), 14) + for m in [1, maxint32]: # Test small/big ranges + assert.eq((100 * m) // (7 * m), 14) + assert.eq((100 * m) // (-7 * m), -15) + assert.eq((-100 * m) // (7 * m), -15) # NB: different from Go/Java + assert.eq((-100 * m) // (-7 * m), 14) # NB: different from Go/Java + assert.eq((98 * m) // (7 * m), 14) + assert.eq((98 * m) // (-7 * m), -14) + assert.eq((-98 * m) // (7 * m), -14) + assert.eq((-98 * m) // (-7 * m), 14) division() # remainder def remainder(): - for m in [1, maxint32]: # Test small/big ranges - assert.eq((100*m) % (7*m), 2*m) - assert.eq((100*m) % (-7*m), -5*m) # NB: different from Go/Java - assert.eq((-100*m) % (7*m), 5*m) # NB: different from Go/Java - assert.eq((-100*m) % (-7*m), -2*m) - assert.eq((98*m) % (7*m), 0) - assert.eq((98*m) % (-7*m), 0) - assert.eq((-98*m) % (7*m), 0) - assert.eq((-98*m) % (-7*m), 0) + for m in [1, maxint32]: # Test small/big ranges + assert.eq((100 * m) % (7 * m), 2 * m) + assert.eq((100 * m) % (-7 * m), -5 * m) # NB: different from Go/Java + assert.eq((-100 * m) % (7 * m), 5 * m) # NB: different from Go/Java + assert.eq((-100 * m) % (-7 * m), -2 * m) + assert.eq((98 * m) % (7 * m), 0) + assert.eq((98 * m) % (-7 * m), 0) + assert.eq((-98 * m) % (7 * m), 0) + assert.eq((-98 * m) % (-7 * m), 0) remainder() # compound assignment def compound(): - x = 1 - x += 1 - assert.eq(x, 2) - x -= 3 - assert.eq(x, -1) - x *= 39 - assert.eq(x, -39) - x //= 4 - assert.eq(x, -10) - x /= -2 - assert.eq(x, 5) - x %= 3 - assert.eq(x, 2) - # use resolve.AllowBitwise to enable the ops: - x = 2 - x &= 1 - assert.eq(x, 0) - x |= 2 - assert.eq(x, 2) - x ^= 3 - assert.eq(x, 1) - x <<= 2 - assert.eq(x, 4) - x >>=2 - assert.eq(x, 1) + x = 1 + x += 1 + assert.eq(x, 2) + x -= 3 + assert.eq(x, -1) + x *= 39 + assert.eq(x, -39) + x //= 4 + assert.eq(x, -10) + x /= -2 + assert.eq(x, 5) + x %= 3 + assert.eq(x, 2) + + # use resolve.AllowBitwise to enable the ops: + x = 2 + x &= 1 + assert.eq(x, 0) + x |= 2 + assert.eq(x, 2) + x ^= 3 + assert.eq(x, 1) + x <<= 2 + assert.eq(x, 4) + x >>= 2 + assert.eq(x, 1) compound() @@ -94,56 +93,58 @@ compound() # See float.star for float-to-int conversions. # We follow Python 3 here, but I can't see the method in its madness. # int from bool/int/float -assert.fails(int, 'missing argument') # int() +assert.fails(int, "missing argument") # int() assert.eq(int(False), 0) assert.eq(int(True), 1) assert.eq(int(3), 3) assert.eq(int(3.1), 3) -assert.fails(lambda: int(3, base=10), "non-string with explicit base") +assert.fails(lambda: int(3, base = 10), "non-string with explicit base") assert.fails(lambda: int(True, 10), "non-string with explicit base") + # int from string, base implicitly 10 assert.eq(int("100000000000000000000"), 10000000000 * 10000000000) assert.eq(int("-100000000000000000000"), -10000000000 * 10000000000) assert.eq(int("123"), 123) assert.eq(int("-123"), -123) -assert.eq(int("0123"), 123) # not octal +assert.eq(int("0123"), 123) # not octal assert.eq(int("-0123"), -123) assert.fails(lambda: int("0x12"), "invalid literal with base 10") assert.fails(lambda: int("-0x12"), "invalid literal with base 10") assert.fails(lambda: int("0o123"), "invalid literal.*base 10") assert.fails(lambda: int("-0o123"), "invalid literal.*base 10") + # int from string, explicit base assert.eq(int("0"), 0) assert.eq(int("00"), 0) -assert.eq(int("0", base=10), 0) -assert.eq(int("00", base=10), 0) -assert.eq(int("0", base=8), 0) -assert.eq(int("00", base=8), 0) +assert.eq(int("0", base = 10), 0) +assert.eq(int("00", base = 10), 0) +assert.eq(int("0", base = 8), 0) +assert.eq(int("00", base = 8), 0) assert.eq(int("-0"), 0) assert.eq(int("-00"), 0) -assert.eq(int("-0", base=10), 0) -assert.eq(int("-00", base=10), 0) -assert.eq(int("-0", base=8), 0) -assert.eq(int("-00", base=8), 0) +assert.eq(int("-0", base = 10), 0) +assert.eq(int("-00", base = 10), 0) +assert.eq(int("-0", base = 8), 0) +assert.eq(int("-00", base = 8), 0) assert.eq(int("+0"), 0) assert.eq(int("+00"), 0) -assert.eq(int("+0", base=10), 0) -assert.eq(int("+00", base=10), 0) -assert.eq(int("+0", base=8), 0) -assert.eq(int("+00", base=8), 0) -assert.eq(int("11", base=9), 10) -assert.eq(int("-11", base=9), -10) -assert.eq(int("10011", base=2), 19) -assert.eq(int("-10011", base=2), -19) +assert.eq(int("+0", base = 10), 0) +assert.eq(int("+00", base = 10), 0) +assert.eq(int("+0", base = 8), 0) +assert.eq(int("+00", base = 8), 0) +assert.eq(int("11", base = 9), 10) +assert.eq(int("-11", base = 9), -10) +assert.eq(int("10011", base = 2), 19) +assert.eq(int("-10011", base = 2), -19) assert.eq(int("123", 8), 83) assert.eq(int("-123", 8), -83) -assert.eq(int("0123", 8), 83) # redundant zeros permitted +assert.eq(int("0123", 8), 83) # redundant zeros permitted assert.eq(int("-0123", 8), -83) assert.eq(int("00123", 8), 83) assert.eq(int("-00123", 8), -83) assert.eq(int("0o123", 8), 83) assert.eq(int("-0o123", 8), -83) -assert.eq(int("123", 7), 66) # 1*7*7 + 2*7 + 3 +assert.eq(int("123", 7), 66) # 1*7*7 + 2*7 + 3 assert.eq(int("-123", 7), -66) assert.eq(int("12", 16), 18) assert.eq(int("-12", 16), -18) @@ -160,6 +161,7 @@ assert.fails(lambda: int("-0x123", 8), "invalid literal.*base 8") assert.fails(lambda: int("0o123", 16), "invalid literal.*base 16") assert.fails(lambda: int("-0o123", 16), "invalid literal.*base 16") assert.fails(lambda: int("0x110", 2), "invalid literal.*base 2") + # int from string, auto detect base assert.eq(int("123", 0), 123) assert.eq(int("+123", 0), +123) @@ -170,10 +172,12 @@ assert.eq(int("-0x12", 0), -18) assert.eq(int("0o123", 0), 83) assert.eq(int("+0o123", 0), +83) assert.eq(int("-0o123", 0), -83) -assert.fails(lambda: int("0123", 0), "invalid literal.*base 0") # valid in Python 2.7 +assert.fails(lambda: int("0123", 0), "invalid literal.*base 0") # valid in Python 2.7 assert.fails(lambda: int("-0123", 0), "invalid literal.*base 0") + # github.com/google/starlark-go/issues/108 assert.fails(lambda: int("0Oxa", 8), "invalid literal with base 8: 0Oxa") + # follow-on bugs to issue 108 assert.fails(lambda: int("--4"), "invalid literal with base 10: --4") assert.fails(lambda: int("++4"), "invalid literal with base 10: \\+\\+4") @@ -185,14 +189,14 @@ assert.fails(lambda: int("0x-4", 16), "invalid literal with base 16: 0x-4") # use resolve.AllowBitwise to enable the ops. # TODO(adonovan): this is not yet in the Starlark spec, # but there is consensus that it should be. -assert.eq(1|2, 3) -assert.eq(3|6, 7) -assert.eq((1|2) & (2|4), 2) +assert.eq(1 | 2, 3) +assert.eq(3 | 6, 7) +assert.eq((1 | 2) & (2 | 4), 2) assert.eq(1 ^ 2, 3) assert.eq(2 ^ 2, 0) -assert.eq(1 | 0 ^ 1, 1) # check | and ^ operators precedence +assert.eq(1 | 0 ^ 1, 1) # check | and ^ operators precedence assert.eq(~1, -2) -assert.eq(~-2, 1) +assert.eq(~(-2), 1) assert.eq(~0, -1) assert.eq(1 << 2, 4) assert.eq(2 >> 1, 1) @@ -202,30 +206,30 @@ assert.fails(lambda: 1 << 512, "shift count too large") # comparisons # TODO(adonovan): test: < > == != etc def comparisons(): - for m in [1, maxint32/2, maxint32]: # Test small/big ranges - assert.lt(-2*m, -1*m) - assert.lt(-1*m, 0*m) - assert.lt(0*m, 1*m) - assert.lt(1*m, 2*m) - assert.true(2*m >= 2*m) - assert.true(2*m > 1*m) - assert.true(1*m >= 1*m) - assert.true(1*m > 0*m) - assert.true(0*m >= 0*m) - assert.true(0*m > -1*m) - assert.true(-1*m >= -1*m) - assert.true(-1*m > -2*m) + for m in [1, maxint32 / 2, maxint32]: # Test small/big ranges + assert.lt(-2 * m, -1 * m) + assert.lt(-1 * m, 0 * m) + assert.lt(0 * m, 1 * m) + assert.lt(1 * m, 2 * m) + assert.true(2 * m >= 2 * m) + assert.true(2 * m > 1 * m) + assert.true(1 * m >= 1 * m) + assert.true(1 * m > 0 * m) + assert.true(0 * m >= 0 * m) + assert.true(0 * m > -1 * m) + assert.true(-1 * m >= -1 * m) + assert.true(-1 * m > -2 * m) comparisons() # precision assert.eq(str(maxint64), "9223372036854775807") -assert.eq(str(maxint64+1), "9223372036854775808") +assert.eq(str(maxint64 + 1), "9223372036854775808") assert.eq(str(minint64), "-9223372036854775808") -assert.eq(str(minint64-1), "-9223372036854775809") +assert.eq(str(minint64 - 1), "-9223372036854775809") assert.eq(str(minint64 * minint64), "85070591730234615865843651857942052864") -assert.eq(str(maxint32+1), "2147483648") -assert.eq(str(minint32-1), "-2147483649") +assert.eq(str(maxint32 + 1), "2147483648") +assert.eq(str(minint32 - 1), "-2147483649") assert.eq(str(minint32 * minint32), "4611686018427387904") assert.eq(str(minint32 | maxint32), "-1") assert.eq(str(minint32 & minint32), "-2147483648") @@ -235,11 +239,11 @@ assert.eq(str(minint32 // -1), "2147483648") # string formatting assert.eq("%o %x %d" % (0o755, 0xDEADBEEF, 42), "755 deadbeef 42") nums = [-95, -1, 0, +1, +95] -assert.eq(' '.join(["%o" % x for x in nums]), "-137 -1 0 1 137") -assert.eq(' '.join(["%d" % x for x in nums]), "-95 -1 0 1 95") -assert.eq(' '.join(["%i" % x for x in nums]), "-95 -1 0 1 95") -assert.eq(' '.join(["%x" % x for x in nums]), "-5f -1 0 1 5f") -assert.eq(' '.join(["%X" % x for x in nums]), "-5F -1 0 1 5F") +assert.eq(" ".join(["%o" % x for x in nums]), "-137 -1 0 1 137") +assert.eq(" ".join(["%d" % x for x in nums]), "-95 -1 0 1 95") +assert.eq(" ".join(["%i" % x for x in nums]), "-95 -1 0 1 95") +assert.eq(" ".join(["%x" % x for x in nums]), "-5f -1 0 1 5f") +assert.eq(" ".join(["%X" % x for x in nums]), "-5F -1 0 1 5F") assert.eq("%o %x %d" % (123, 123, 123), "173 7b 123") -assert.eq("%o %x %d" % (123.1, 123.1, 123.1), "173 7b 123") # non-int operands are acceptable +assert.eq("%o %x %d" % (123.1, 123.1, 123.1), "173 7b 123") # non-int operands are acceptable assert.fails(lambda: "%d" % True, "cannot convert bool to int") diff --git a/starlark/testdata/json.star b/starlark/testdata/json.star index 615d70b..37c9070 100644 --- a/starlark/testdata/json.star +++ b/starlark/testdata/json.star @@ -1,5 +1,4 @@ # Tests of json module. -# option:float load("assert.star", "assert") load("json.star", "json") @@ -28,7 +27,7 @@ assert.eq(json.encode("\x80"), '"\\ufffd"') # invalid UTF-8 -> replacement char def encode_error(expr, error): assert.fails(lambda: json.encode(expr), error) -encode_error(float("NaN"), "json.encode: cannot encode non-finite float NaN") +encode_error(float("NaN"), "json.encode: cannot encode non-finite float nan") encode_error({1: "two"}, "dict has int key, want string") encode_error(len, "cannot encode builtin_function_or_method as JSON") encode_error(struct(x=[1, {"x": len}]), # nested failure diff --git a/starlark/testdata/misc.star b/starlark/testdata/misc.star index 0cb973a..84ef84c 100644 --- a/starlark/testdata/misc.star +++ b/starlark/testdata/misc.star @@ -36,8 +36,6 @@ # tuple slice # interpolate with %c, %% -# option:float - load("assert.star", "assert") # Ordered comparisons require values of the same type. diff --git a/starlark/testdata/string.star b/starlark/testdata/string.star index 1d78dd7..859f645 100644 --- a/starlark/testdata/string.star +++ b/starlark/testdata/string.star @@ -1,10 +1,10 @@ # Tests of Starlark 'string' -# option:float option:set +# option:set load("assert.star", "assert") # raw string literals: -assert.eq(r'a\bc', "a\\bc") +assert.eq(r"a\bc", "a\\bc") # truth assert.true("abc") @@ -12,7 +12,7 @@ assert.true(chr(0)) assert.true(not "") # str + str -assert.eq("a"+"b"+"c", "abc") +assert.eq("a" + "b" + "c", "abc") # str * int, int * str assert.eq("abc" * 0, "") @@ -24,26 +24,26 @@ assert.eq(-1 * "abc", "") assert.eq(1 * "abc", "abc") assert.eq(5 * "abc", "abcabcabcabcabc") assert.fails(lambda: 1.0 * "abc", "unknown.*float \\* str") -assert.fails(lambda : "abc" * (1000000 * 1000000), "repeat count 1000000000000 too large") -assert.fails(lambda : "abc" * 1000000 * 1000000, "excessive repeat \\(3000000 \\* 1000000 elements") +assert.fails(lambda: "abc" * (1000000 * 1000000), "repeat count 1000000000000 too large") +assert.fails(lambda: "abc" * 1000000 * 1000000, "excessive repeat \\(3000000 \\* 1000000 elements") # len assert.eq(len("Hello, 世界!"), 14) -assert.eq(len("𐐷"), 4) # U+10437 has a 4-byte UTF-8 encoding (and a 2-code UTF-16 encoding) +assert.eq(len("𐐷"), 4) # U+10437 has a 4-byte UTF-8 encoding (and a 2-code UTF-16 encoding) # chr & ord -assert.eq(chr(65), "A") # 1-byte UTF-8 encoding -assert.eq(chr(1049), "Й") # 2-byte UTF-8 encoding -assert.eq(chr(0x1F63F), "😿") # 4-byte UTF-8 encoding +assert.eq(chr(65), "A") # 1-byte UTF-8 encoding +assert.eq(chr(1049), "Й") # 2-byte UTF-8 encoding +assert.eq(chr(0x1F63F), "😿") # 4-byte UTF-8 encoding assert.fails(lambda: chr(-1), "Unicode code point -1 out of range \\(<0\\)") assert.fails(lambda: chr(0x110000), "Unicode code point U\\+110000 out of range \\(>0x10FFFF\\)") assert.eq(ord("A"), 65) assert.eq(ord("Й"), 1049) assert.eq(ord("😿"), 0x1F63F) -assert.eq(ord("Й"[1:]), 0xFFFD) # = Unicode replacement character +assert.eq(ord("Й"[1:]), 0xFFFD) # = Unicode replacement character assert.fails(lambda: ord("abc"), "string encodes 3 Unicode code points, want 1") assert.fails(lambda: ord(""), "string encodes 0 Unicode code points, want 1") -assert.fails(lambda: ord("😿"[1:]), "string encodes 3 Unicode code points, want 1") # 3 x 0xFFFD +assert.fails(lambda: ord("😿"[1:]), "string encodes 3 Unicode code points, want 1") # 3 x 0xFFFD # string.codepoint_ords assert.eq(type("abcЙ😿".codepoint_ords()), "codepoints") @@ -62,17 +62,21 @@ assert.eq(list("".codepoints()), []) # string.elem_ords assert.eq(type("abcЙ😿".elem_ords()), "elems") assert.eq(str("abcЙ😿".elem_ords()), '"abcЙ😿".elem_ords()') -assert.eq(list("abcЙ😿".elem_ords()), [97, 98, 99, 208, 153, 240, 159, 152, 191]) -assert.eq(list(("A" + "😿Z"[1:]).elem_ords()), [65, 159, 152, 191, 90]) +assert.eq(list("abcЙ😿".elem_ords()), [97, 98, 99, 208, 153, 240, 159, 152, 191]) +assert.eq(list(("A" + "😿Z"[1:]).elem_ords()), [65, 159, 152, 191, 90]) assert.eq(list("".elem_ords()), []) # string.elems assert.eq(type("abcЙ😿".elems()), "elems") assert.eq(str("abcЙ😿".elems()), '"abcЙ😿".elems()') -assert.eq(list("abcЙ😿".elems()), - ["a", "b", "c", "\xd0", "\x99", "\xf0", "\x9f", "\x98", "\xbf"]) -assert.eq(list(("A" + "😿Z"[1:]).elems()), - ["A", "\x9f", "\x98", "\xbf", "Z"]) +assert.eq( + list("abcЙ😿".elems()), + ["a", "b", "c", "\xd0", "\x99", "\xf0", "\x9f", "\x98", "\xbf"], +) +assert.eq( + list(("A" + "😿Z"[1:]).elems()), + ["A", "\x9f", "\x98", "\xbf", "Z"], +) assert.eq(list("".elems()), []) # indexing, x[i] @@ -90,7 +94,10 @@ assert.fails(lambda: "abc"[4], "out of range") # x[i] = ... x2 = "abc" -def f(): x2[1] = 'B' + +def f(): + x2[1] = "B" + assert.fails(f, "string.*does not support.*assignment") # slicing, x[i:j] @@ -115,6 +122,7 @@ assert.eq("abc"[:3], "abc") assert.eq("abc"[:4], "abc") assert.eq("abc"[1:2], "b") assert.eq("abc"[2:1], "") + # non-unit strides assert.eq("abcd"[0:4:1], "abcd") assert.eq("abcd"[::2], "ac") @@ -139,7 +147,7 @@ assert.fails(lambda: 1 in "", "requires string as left operand") assert.fails(lambda: "" in 1, "unknown binary op: string in int") # ==, != -assert.eq("hello", "he"+"llo") +assert.eq("hello", "he" + "llo") assert.ne("hello", "Hello") # hash must follow java.lang.String.hashCode. @@ -157,12 +165,13 @@ assert.eq(gothash, wanthash) # string % tuple formatting assert.eq("A %d %x Z" % (123, 456), "A 123 1c8 Z") -assert.eq("A %(foo)d %(bar)s Z" % {"foo": 123, "bar":"hi"}, "A 123 hi Z") -assert.eq("%s %r" % ("hi", "hi"), 'hi "hi"') # TODO(adonovan): use ''-quotation +assert.eq("A %(foo)d %(bar)s Z" % {"foo": 123, "bar": "hi"}, "A 123 hi Z") +assert.eq("%s %r" % ("hi", "hi"), 'hi "hi"') # TODO(adonovan): use ''-quotation assert.eq("%%d %d" % 1, "%d 1") assert.fails(lambda: "%d %d" % 1, "not enough arguments for format string") assert.fails(lambda: "%d %d" % (1, 2, 3), "too many arguments for format string") assert.fails(lambda: "" % 1, "too many arguments for format string") + # %c assert.eq("%c" % 65, "A") assert.eq("%c" % 0x3b1, "α") @@ -181,31 +190,31 @@ assert.eq("a{}b{}c{}d{}".format(1, 2, 3, 4), "a1b2c3d4") assert.eq("a{{b".format(), "a{b") assert.eq("a}}b".format(), "a}b") assert.eq("a{{b}}c".format(), "a{b}c") -assert.eq("a{x}b{y}c{}".format(1, x=2, y=3), "a2b3c1") -assert.fails(lambda: "a{z}b".format(x=1), "keyword z not found") +assert.eq("a{x}b{y}c{}".format(1, x = 2, y = 3), "a2b3c1") +assert.fails(lambda: "a{z}b".format(x = 1), "keyword z not found") assert.fails(lambda: "{-1}".format(1), "keyword -1 not found") assert.fails(lambda: "{-0}".format(1), "keyword -0 not found") assert.fails(lambda: "{+0}".format(1), "keyword \\+0 not found") -assert.fails(lambda: "{+1}".format(1), "keyword \\+1 not found") # starlark-go/issues/114 +assert.fails(lambda: "{+1}".format(1), "keyword \\+1 not found") # starlark-go/issues/114 assert.eq("{0000000000001}".format(0, 1), "1") -assert.eq("{012}".format(*range(100)), "12") # decimal, despite leading zeros -assert.fails(lambda: '{0,1} and {1}'.format(1, 2), "keyword 0,1 not found") +assert.eq("{012}".format(*range(100)), "12") # decimal, despite leading zeros +assert.fails(lambda: "{0,1} and {1}".format(1, 2), "keyword 0,1 not found") assert.fails(lambda: "a{123}b".format(), "tuple index out of range") assert.fails(lambda: "a{}b{}c".format(1), "tuple index out of range") -assert.eq("a{010}b".format(0,1,2,3,4,5,6,7,8,9,10), "a10b") # index is decimal +assert.eq("a{010}b".format(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10), "a10b") # index is decimal assert.fails(lambda: "a{}b{1}c".format(1, 2), "cannot switch from automatic field numbering to manual") assert.eq("a{!s}c".format("b"), "abc") assert.eq("a{!r}c".format("b"), r'a"b"c') -assert.eq("a{x!r}c".format(x='b'), r'a"b"c') -assert.fails(lambda: "{x!}".format(x=1), "unknown conversion") -assert.fails(lambda: "{x!:}".format(x=1), "unknown conversion") -assert.fails(lambda: '{a.b}'.format(1), "syntax x.y is not supported") -assert.fails(lambda: '{a[0]}'.format(1), "syntax a\\[i\\] is not supported") -assert.fails(lambda: '{ {} }'.format(1), "nested replacement fields not supported") -assert.fails(lambda: '{{}'.format(1), "single '}' in format") -assert.fails(lambda: '{}}'.format(1), "single '}' in format") -assert.fails(lambda: '}}{'.format(1), "unmatched '{' in format") -assert.fails(lambda: '}{{'.format(1), "single '}' in format") +assert.eq("a{x!r}c".format(x = "b"), r'a"b"c') +assert.fails(lambda: "{x!}".format(x = 1), "unknown conversion") +assert.fails(lambda: "{x!:}".format(x = 1), "unknown conversion") +assert.fails(lambda: "{a.b}".format(1), "syntax x.y is not supported") +assert.fails(lambda: "{a[0]}".format(1), "syntax a\\[i\\] is not supported") +assert.fails(lambda: "{ {} }".format(1), "nested replacement fields not supported") +assert.fails(lambda: "{{}".format(1), "single '}' in format") +assert.fails(lambda: "{}}".format(1), "single '}' in format") +assert.fails(lambda: "}}{".format(1), "unmatched '{' in format") +assert.fails(lambda: "}{{".format(1), "single '}' in format") # str.split, str.rsplit assert.eq("a.b.c.d".split("."), ["a", "b", "c", "d"]) @@ -242,44 +251,46 @@ assert.eq(" a bc\n def \t ghi ".split(None, 1), ["a", "bc\n def \t ghi "]) assert.eq(" a bc\n def \t ghi ".rsplit(None, 1), [" a bc\n def", "ghi"]) # Observe the algorithmic difference when splitting on spaces versus other delimiters. -assert.eq('--aa--bb--cc--'.split('-', 0), ['--aa--bb--cc--']) # contrast this -assert.eq(' aa bb cc '.split(None, 0), ['aa bb cc ']) # with this -assert.eq('--aa--bb--cc--'.rsplit('-', 0), ['--aa--bb--cc--']) # ditto this -assert.eq(' aa bb cc '.rsplit(None, 0), [' aa bb cc']) # and this +assert.eq("--aa--bb--cc--".split("-", 0), ["--aa--bb--cc--"]) # contrast this +assert.eq(" aa bb cc ".split(None, 0), ["aa bb cc "]) # with this +assert.eq("--aa--bb--cc--".rsplit("-", 0), ["--aa--bb--cc--"]) # ditto this +assert.eq(" aa bb cc ".rsplit(None, 0), [" aa bb cc"]) # and this + # -assert.eq('--aa--bb--cc--'.split('-', 1), ['', '-aa--bb--cc--']) -assert.eq('--aa--bb--cc--'.rsplit('-', 1), ['--aa--bb--cc-', '']) -assert.eq(' aa bb cc '.split(None, 1), ['aa', 'bb cc ']) -assert.eq(' aa bb cc '.rsplit(None, 1), [' aa bb', 'cc']) +assert.eq("--aa--bb--cc--".split("-", 1), ["", "-aa--bb--cc--"]) +assert.eq("--aa--bb--cc--".rsplit("-", 1), ["--aa--bb--cc-", ""]) +assert.eq(" aa bb cc ".split(None, 1), ["aa", "bb cc "]) +assert.eq(" aa bb cc ".rsplit(None, 1), [" aa bb", "cc"]) + # -assert.eq('--aa--bb--cc--'.split('-', -1), ['', '', 'aa', '', 'bb', '', 'cc', '', '']) -assert.eq('--aa--bb--cc--'.rsplit('-', -1), ['', '', 'aa', '', 'bb', '', 'cc', '', '']) -assert.eq(' aa bb cc '.split(None, -1), ['aa', 'bb', 'cc']) -assert.eq(' aa bb cc '.rsplit(None, -1), ['aa', 'bb', 'cc']) -assert.eq(' '.split(None), []) -assert.eq(' '.rsplit(None), []) +assert.eq("--aa--bb--cc--".split("-", -1), ["", "", "aa", "", "bb", "", "cc", "", ""]) +assert.eq("--aa--bb--cc--".rsplit("-", -1), ["", "", "aa", "", "bb", "", "cc", "", ""]) +assert.eq(" aa bb cc ".split(None, -1), ["aa", "bb", "cc"]) +assert.eq(" aa bb cc ".rsplit(None, -1), ["aa", "bb", "cc"]) +assert.eq(" ".split(None), []) +assert.eq(" ".rsplit(None), []) assert.eq("localhost:80".rsplit(":", 1)[-1], "80") # str.splitlines -assert.eq('\nabc\ndef'.splitlines(), ['', 'abc', 'def']) -assert.eq('\nabc\ndef'.splitlines(True), ['\n', 'abc\n', 'def']) -assert.eq('\nabc\ndef\n'.splitlines(), ['', 'abc', 'def']) -assert.eq('\nabc\ndef\n'.splitlines(True), ['\n', 'abc\n', 'def\n']) -assert.eq(''.splitlines(), []) # -assert.eq(''.splitlines(True), []) # -assert.eq('a'.splitlines(), ['a']) -assert.eq('a'.splitlines(True), ['a']) -assert.eq('\n'.splitlines(), ['']) -assert.eq('\n'.splitlines(True), ['\n']) -assert.eq('a\n'.splitlines(), ['a']) -assert.eq('a\n'.splitlines(True), ['a\n']) -assert.eq('a\n\nb'.splitlines(), ['a', '', 'b']) -assert.eq('a\n\nb'.splitlines(True), ['a\n', '\n', 'b']) -assert.eq('a\nb\nc'.splitlines(), ['a', 'b', 'c']) -assert.eq('a\nb\nc'.splitlines(True), ['a\n', 'b\n', 'c']) -assert.eq('a\nb\nc\n'.splitlines(), ['a', 'b', 'c']) -assert.eq('a\nb\nc\n'.splitlines(True), ['a\n', 'b\n', 'c\n']) +assert.eq("\nabc\ndef".splitlines(), ["", "abc", "def"]) +assert.eq("\nabc\ndef".splitlines(True), ["\n", "abc\n", "def"]) +assert.eq("\nabc\ndef\n".splitlines(), ["", "abc", "def"]) +assert.eq("\nabc\ndef\n".splitlines(True), ["\n", "abc\n", "def\n"]) +assert.eq("".splitlines(), []) # +assert.eq("".splitlines(True), []) # +assert.eq("a".splitlines(), ["a"]) +assert.eq("a".splitlines(True), ["a"]) +assert.eq("\n".splitlines(), [""]) +assert.eq("\n".splitlines(True), ["\n"]) +assert.eq("a\n".splitlines(), ["a"]) +assert.eq("a\n".splitlines(True), ["a\n"]) +assert.eq("a\n\nb".splitlines(), ["a", "", "b"]) +assert.eq("a\n\nb".splitlines(True), ["a\n", "\n", "b"]) +assert.eq("a\nb\nc".splitlines(), ["a", "b", "c"]) +assert.eq("a\nb\nc".splitlines(True), ["a\n", "b\n", "c"]) +assert.eq("a\nb\nc\n".splitlines(), ["a", "b", "c"]) +assert.eq("a\nb\nc\n".splitlines(True), ["a\n", "b\n", "c\n"]) # str.{,l,r}strip assert.eq(" \tfoo\n ".strip(), "foo") @@ -305,23 +316,26 @@ assert.true(not "foo".endswith("x")) assert.true("foo".startswith("fo")) assert.true(not "foo".startswith("x")) assert.fails(lambda: "foo".startswith(1), "got int.*want string") + # -assert.true('abc'.startswith(('a', 'A'))) -assert.true('ABC'.startswith(('a', 'A'))) -assert.true(not 'ABC'.startswith(('b', 'B'))) -assert.fails(lambda: '123'.startswith((1, 2)), 'got int, for element 0') -assert.fails(lambda: '123'.startswith(['3']), 'got list') +assert.true("abc".startswith(("a", "A"))) +assert.true("ABC".startswith(("a", "A"))) +assert.true(not "ABC".startswith(("b", "B"))) +assert.fails(lambda: "123".startswith((1, 2)), "got int, for element 0") +assert.fails(lambda: "123".startswith(["3"]), "got list") + # -assert.true('abc'.endswith(('c', 'C'))) -assert.true('ABC'.endswith(('c', 'C'))) -assert.true(not 'ABC'.endswith(('b', 'B'))) -assert.fails(lambda: '123'.endswith((1, 2)), 'got int, for element 0') -assert.fails(lambda: '123'.endswith(['3']), 'got list') +assert.true("abc".endswith(("c", "C"))) +assert.true("ABC".endswith(("c", "C"))) +assert.true(not "ABC".endswith(("b", "B"))) +assert.fails(lambda: "123".endswith((1, 2)), "got int, for element 0") +assert.fails(lambda: "123".endswith(["3"]), "got list") + # start/end -assert.true('abc'.startswith('bc', 1)) -assert.true(not 'abc'.startswith('b', 999)) -assert.true('abc'.endswith('ab', None, -1)) -assert.true(not 'abc'.endswith('b', None, -999)) +assert.true("abc".startswith("bc", 1)) +assert.true(not "abc".startswith("b", 999)) +assert.true("abc".endswith("ab", None, -1)) +assert.true(not "abc".endswith("b", None, -999)) # str.replace assert.eq("banana".replace("a", "o", 1), "bonana") @@ -346,71 +360,77 @@ assert.eq("foo/bar/wiz".rpartition("."), ("", "", "foo/bar/wiz")) assert.fails(lambda: "foo/bar/wiz".partition(""), "empty separator") assert.fails(lambda: "foo/bar/wiz".rpartition(""), "empty separator") -assert.eq('?'.join(["foo", "a/b/c.go".rpartition("/")[0]]), 'foo?a/b') +assert.eq("?".join(["foo", "a/b/c.go".rpartition("/")[0]]), "foo?a/b") # str.is{alpha,...} def test_predicates(): - predicates = ["alnum", "alpha", "digit", "lower", "space", "title", "upper"] - table = { - "Hello, World!": "title", - "hello, world!": "lower", - "base64": "alnum lower", - "HAL-9000": "upper", - "Catch-22": "title", - "": "", - "\n\t\r": "space", - "abc": "alnum alpha lower", - "ABC": "alnum alpha upper", - "123": "alnum digit", - "DŽLJ": "alnum alpha upper", - "DžLj": "alnum alpha", - "Dž Lj": "title", - "džlj": "alnum alpha lower", - } - for str, want in table.items(): - got = ' '.join([name for name in predicates if getattr(str, "is"+name)()]) - if got != want: - assert.fail("%r matched [%s], want [%s]" % (str, got, want)) + predicates = ["alnum", "alpha", "digit", "lower", "space", "title", "upper"] + table = { + "Hello, World!": "title", + "hello, world!": "lower", + "base64": "alnum lower", + "HAL-9000": "upper", + "Catch-22": "title", + "": "", + "\n\t\r": "space", + "abc": "alnum alpha lower", + "ABC": "alnum alpha upper", + "123": "alnum digit", + "DŽLJ": "alnum alpha upper", + "DžLj": "alnum alpha", + "Dž Lj": "title", + "džlj": "alnum alpha lower", + } + for str, want in table.items(): + got = " ".join([name for name in predicates if getattr(str, "is" + name)()]) + if got != want: + assert.fail("%r matched [%s], want [%s]" % (str, got, want)) + test_predicates() # Strings are not iterable. # ok -assert.eq(len("abc"), 3) # len -assert.true("a" in "abc") # str in str -assert.eq("abc"[1], "b") # indexing +assert.eq(len("abc"), 3) # len +assert.true("a" in "abc") # str in str +assert.eq("abc"[1], "b") # indexing + # not ok def for_string(): - for x in "abc": - pass -def args(*args): return args -assert.fails(lambda: args(*"abc"), "must be iterable, not string") # varargs -assert.fails(lambda: list("abc"), "got string, want iterable") # list(str) -assert.fails(lambda: tuple("abc"), "got string, want iterable") # tuple(str) -assert.fails(lambda: set("abc"), "got string, want iterable") # set(str) + for x in "abc": + pass + +def args(*args): + return args + +assert.fails(lambda: args(*"abc"), "must be iterable, not string") # varargs +assert.fails(lambda: list("abc"), "got string, want iterable") # list(str) +assert.fails(lambda: tuple("abc"), "got string, want iterable") # tuple(str) +assert.fails(lambda: set("abc"), "got string, want iterable") # set(str) assert.fails(lambda: set() | "abc", "unknown binary op: set | string") # set union -assert.fails(lambda: enumerate("ab"), "got string, want iterable") # enumerate -assert.fails(lambda: sorted("abc"), "got string, want iterable") # sorted -assert.fails(lambda: [].extend("bc"), "got string, want iterable") # list.extend -assert.fails(lambda: ",".join("abc"), "got string, want iterable") # string.join -assert.fails(lambda: dict(["ab"]), "not iterable .*string") # dict +assert.fails(lambda: enumerate("ab"), "got string, want iterable") # enumerate +assert.fails(lambda: sorted("abc"), "got string, want iterable") # sorted +assert.fails(lambda: [].extend("bc"), "got string, want iterable") # list.extend +assert.fails(lambda: ",".join("abc"), "got string, want iterable") # string.join +assert.fails(lambda: dict(["ab"]), "not iterable .*string") # dict + # The Java implementation does not correctly reject the following cases: # (See Google Issue b/34385336) -assert.fails(for_string, "string value is not iterable") # for loop -assert.fails(lambda: [x for x in "abc"], "string value is not iterable") # comprehension -assert.fails(lambda: all("abc"), "got string, want iterable") # all -assert.fails(lambda: any("abc"), "got string, want iterable") # any -assert.fails(lambda: reversed("abc"), "got string, want iterable") # reversed -assert.fails(lambda: zip("ab", "cd"), "not iterable: string") # zip +assert.fails(for_string, "string value is not iterable") # for loop +assert.fails(lambda: [x for x in "abc"], "string value is not iterable") # comprehension +assert.fails(lambda: all("abc"), "got string, want iterable") # all +assert.fails(lambda: any("abc"), "got string, want iterable") # any +assert.fails(lambda: reversed("abc"), "got string, want iterable") # reversed +assert.fails(lambda: zip("ab", "cd"), "not iterable: string") # zip # str.join -assert.eq(','.join([]), '') -assert.eq(','.join(["a"]), 'a') -assert.eq(','.join(["a", "b"]), 'a,b') -assert.eq(','.join(["a", "b", "c"]), 'a,b,c') -assert.eq(','.join(("a", "b", "c")), 'a,b,c') -assert.eq(''.join(("a", "b", "c")), 'abc') -assert.fails(lambda: ''.join(None), 'got NoneType, want iterable') -assert.fails(lambda: ''.join(["one", 2]), 'join: in list, want string, got int') +assert.eq(",".join([]), "") +assert.eq(",".join(["a"]), "a") +assert.eq(",".join(["a", "b"]), "a,b") +assert.eq(",".join(["a", "b", "c"]), "a,b,c") +assert.eq(",".join(("a", "b", "c")), "a,b,c") +assert.eq("".join(("a", "b", "c")), "abc") +assert.fails(lambda: "".join(None), "got NoneType, want iterable") +assert.fails(lambda: "".join(["one", 2]), "join: in list, want string, got int") # TODO(adonovan): tests for: {,r}index diff --git a/starlark/value.go b/starlark/value.go index eb0e84e..8d1b88a 100644 --- a/starlark/value.go +++ b/starlark/value.go @@ -385,10 +385,47 @@ func (x Bool) CompareSameType(op syntax.Token, y_ Value, depth int) (bool, error // Float is the type of a Starlark float. type Float float64 -func (f Float) String() string { return strconv.FormatFloat(float64(f), 'g', 6, 64) } -func (f Float) Type() string { return "float" } -func (f Float) Freeze() {} // immutable -func (f Float) Truth() Bool { return f != 0.0 } +func (f Float) String() string { + var buf strings.Builder + f.format(&buf, 'g') + return buf.String() +} + +func (f Float) format(buf *strings.Builder, conv byte) { + ff := float64(f) + if !isFinite(ff) { + if math.IsInf(ff, +1) { + buf.WriteString("+inf") + } else if math.IsInf(ff, -1) { + buf.WriteString("-inf") + } else { + buf.WriteString("nan") + } + return + } + + // %g is the default format used by str. + // It uses the minimum precision to avoid ambiguity, + // and always includes a '.' or an 'e' so that the value + // is self-evidently a float, not an int. + if conv == 'g' || conv == 'G' { + s := strconv.FormatFloat(ff, conv, -1, 64) + buf.WriteString(s) + // Ensure result always has a decimal point if no exponent. + // "123" -> "123.0" + if strings.IndexByte(s, conv-'g'+'e') < 0 && strings.IndexByte(s, '.') < 0 { + buf.WriteString(".0") + } + return + } + + // %[eEfF] use 6-digit precision + buf.WriteString(strconv.FormatFloat(ff, conv, 6, 64)) +} + +func (f Float) Type() string { return "float" } +func (f Float) Freeze() {} // immutable +func (f Float) Truth() Bool { return f != 0.0 } func (f Float) Hash() (uint32, error) { // Equal float and int values must yield the same hash. // TODO(adonovan): opt: if f is non-integral, and thus not equal @@ -409,27 +446,34 @@ func isFinite(f float64) bool { func (x Float) CompareSameType(op syntax.Token, y_ Value, depth int) (bool, error) { y := y_.(Float) - switch op { - case syntax.EQL: - return x == y, nil - case syntax.NEQ: - return x != y, nil - case syntax.LE: - return x <= y, nil - case syntax.LT: - return x < y, nil - case syntax.GE: - return x >= y, nil - case syntax.GT: - return x > y, nil + return threeway(op, floatCmp(x, y)), nil +} + +// floatCmp performs a three-valued comparison on floats, +// which are totally ordered with NaN > +Inf. +func floatCmp(x, y Float) int { + if x > y { + return +1 + } else if x < y { + return -1 + } else if x == y { + return 0 } - panic(op) + + // At least one operand is NaN. + if x == x { + return -1 // y is NaN + } else if y == y { + return +1 // x is NaN + } + return 0 // both NaN } func (f Float) rational() *big.Rat { return new(big.Rat).SetFloat64(float64(f)) } // AsFloat returns the float64 value closest to x. -// The f result is undefined if x is not a float or int. +// The f result is undefined if x is not a float or Int. +// The result may be infinite if x is a very large Int. func AsFloat(x Value) (f float64, ok bool) { switch x := x.(type) { case Float: @@ -1199,11 +1243,10 @@ func CompareDepth(op syntax.Token, x, y Value, depth int) (bool, error) { switch x := x.(type) { case Int: if y, ok := y.(Float); ok { - if y != y { - return false, nil // y is NaN - } var cmp int - if !math.IsInf(float64(y), 0) { + if y != y { + cmp = -1 // y is NaN + } else if !math.IsInf(float64(y), 0) { cmp = x.rational().Cmp(y.rational()) // y is finite } else if y > 0 { cmp = -1 // y is +Inf @@ -1214,16 +1257,15 @@ func CompareDepth(op syntax.Token, x, y Value, depth int) (bool, error) { } case Float: if y, ok := y.(Int); ok { - if x != x { - return false, nil // x is NaN - } var cmp int - if !math.IsInf(float64(x), 0) { + if x != x { + cmp = +1 // x is NaN + } else if !math.IsInf(float64(x), 0) { cmp = x.rational().Cmp(y.rational()) // x is finite } else if x > 0 { - cmp = -1 // x is +Inf + cmp = +1 // x is +Inf } else { - cmp = +1 // x is -Inf + cmp = -1 // x is -Inf } return threeway(op, cmp), nil } diff --git a/starlarkstruct/struct_test.go b/starlarkstruct/struct_test.go index 8e6a93d..5de7020 100644 --- a/starlarkstruct/struct_test.go +++ b/starlarkstruct/struct_test.go @@ -19,7 +19,6 @@ func init() { // The tests make extensive use of these not-yet-standard features. resolve.AllowLambda = true resolve.AllowNestedDef = true - resolve.AllowFloat = true resolve.AllowSet = true } |