whatcanGOwrong
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
// Copyright 2018 The Bazel Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package starlark_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"go.starlark.net/lib/json"
|
||||
"go.starlark.net/starlark"
|
||||
"go.starlark.net/starlarktest"
|
||||
)
|
||||
|
||||
func BenchmarkStarlark(b *testing.B) {
|
||||
starlark.Universe["json"] = json.Module
|
||||
|
||||
testdata := starlarktest.DataFile("starlark", ".")
|
||||
thread := new(starlark.Thread)
|
||||
for _, file := range []string{
|
||||
"testdata/benchmark.star",
|
||||
// ...
|
||||
} {
|
||||
|
||||
filename := filepath.Join(testdata, file)
|
||||
|
||||
src, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
continue
|
||||
}
|
||||
opts := getOptions(string(src))
|
||||
|
||||
// Evaluate the file once.
|
||||
globals, err := starlark.ExecFileOptions(opts, thread, filename, src, nil)
|
||||
if err != nil {
|
||||
reportEvalError(b, err)
|
||||
}
|
||||
|
||||
// Repeatedly call each global function named bench_* as a benchmark.
|
||||
for _, name := range globals.Keys() {
|
||||
value := globals[name]
|
||||
if fn, ok := value.(*starlark.Function); ok && strings.HasPrefix(name, "bench_") {
|
||||
b.Run(name, func(b *testing.B) {
|
||||
_, err := starlark.Call(thread, fn, starlark.Tuple{benchmark{b}}, nil)
|
||||
if err != nil {
|
||||
reportEvalError(b, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A benchmark is passed to each bench_xyz(b) function in a bench_*.star file.
|
||||
// It provides b.n, the number of iterations that must be executed by the function,
|
||||
// which is typically of the form:
|
||||
//
|
||||
// def bench_foo(b):
|
||||
// for _ in range(b.n):
|
||||
// ...work...
|
||||
//
|
||||
// It also provides stop, start, and restart methods to stop the clock in case
|
||||
// there is significant set-up work that should not count against the measured
|
||||
// operation.
|
||||
//
|
||||
// (This interface is inspired by Go's testing.B, and is also implemented
|
||||
// by the java.starlark.net implementation; see
|
||||
// https://github.com/bazelbuild/starlark/pull/75#pullrequestreview-275604129.)
|
||||
type benchmark struct {
|
||||
b *testing.B
|
||||
}
|
||||
|
||||
func (benchmark) Freeze() {}
|
||||
func (benchmark) Truth() starlark.Bool { return true }
|
||||
func (benchmark) Type() string { return "benchmark" }
|
||||
func (benchmark) String() string { return "<benchmark>" }
|
||||
func (benchmark) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: benchmark") }
|
||||
func (benchmark) AttrNames() []string { return []string{"n", "restart", "start", "stop"} }
|
||||
func (b benchmark) Attr(name string) (starlark.Value, error) {
|
||||
switch name {
|
||||
case "n":
|
||||
return starlark.MakeInt(b.b.N), nil
|
||||
case "restart":
|
||||
return benchmarkRestart.BindReceiver(b), nil
|
||||
case "start":
|
||||
return benchmarkStart.BindReceiver(b), nil
|
||||
case "stop":
|
||||
return benchmarkStop.BindReceiver(b), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var (
|
||||
benchmarkRestart = starlark.NewBuiltin("restart", benchmarkRestartImpl)
|
||||
benchmarkStart = starlark.NewBuiltin("start", benchmarkStartImpl)
|
||||
benchmarkStop = starlark.NewBuiltin("stop", benchmarkStopImpl)
|
||||
)
|
||||
|
||||
func benchmarkRestartImpl(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
|
||||
b.Receiver().(benchmark).b.ResetTimer()
|
||||
return starlark.None, nil
|
||||
}
|
||||
|
||||
func benchmarkStartImpl(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
|
||||
b.Receiver().(benchmark).b.StartTimer()
|
||||
return starlark.None, nil
|
||||
}
|
||||
|
||||
func benchmarkStopImpl(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
|
||||
b.Receiver().(benchmark).b.StopTimer()
|
||||
return starlark.None, nil
|
||||
}
|
||||
|
||||
// BenchmarkProgram measures operations relevant to compiled programs.
|
||||
// TODO(adonovan): use a bigger testdata program.
|
||||
func BenchmarkProgram(b *testing.B) {
|
||||
// Measure time to read a source file (approx 600us but depends on hardware and file system).
|
||||
filename := starlarktest.DataFile("starlark", "testdata/paths.star")
|
||||
var src []byte
|
||||
b.Run("read", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
var err error
|
||||
src, err = os.ReadFile(filename)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Measure time to turn a source filename into a compiled program (approx 450us).
|
||||
var prog *starlark.Program
|
||||
b.Run("compile", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
var err error
|
||||
_, prog, err = starlark.SourceProgram(filename, src, starlark.StringDict(nil).Has)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Measure time to encode a compiled program to a memory buffer
|
||||
// (approx 20us; was 75-120us with gob encoding).
|
||||
var out bytes.Buffer
|
||||
b.Run("encode", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
out.Reset()
|
||||
if err := prog.Write(&out); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Measure time to decode a compiled program from a memory buffer
|
||||
// (approx 20us; was 135-250us with gob encoding)
|
||||
b.Run("decode", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
in := bytes.NewReader(out.Bytes())
|
||||
if _, err := starlark.CompiledProgram(in); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package starlark
|
||||
|
||||
import "go.starlark.net/syntax"
|
||||
|
||||
// This file defines an experimental API for the debugging tools.
|
||||
// Some of these declarations expose details of internal packages.
|
||||
// (The debugger makes liberal use of exported fields of unexported types.)
|
||||
// Breaking changes may occur without notice.
|
||||
|
||||
// Local returns the value of the i'th local variable.
|
||||
// It may be nil if not yet assigned.
|
||||
//
|
||||
// Local may be called only for frames whose Callable is a *Function (a
|
||||
// function defined by Starlark source code), and only while the frame
|
||||
// is active; it will panic otherwise.
|
||||
//
|
||||
// This function is provided only for debugging tools.
|
||||
//
|
||||
// THIS API IS EXPERIMENTAL AND MAY CHANGE WITHOUT NOTICE.
|
||||
func (fr *frame) Local(i int) Value { return fr.locals[i] }
|
||||
|
||||
// DebugFrame is the debugger API for a frame of the interpreter's call stack.
|
||||
//
|
||||
// Most applications have no need for this API; use CallFrame instead.
|
||||
//
|
||||
// Clients must not retain a DebugFrame nor call any of its methods once
|
||||
// the current built-in call has returned or execution has resumed
|
||||
// after a breakpoint as this may have unpredictable effects, including
|
||||
// but not limited to retention of object that would otherwise be garbage.
|
||||
type DebugFrame interface {
|
||||
Callable() Callable // returns the frame's function
|
||||
Local(i int) Value // returns the value of the (Starlark) frame's ith local variable
|
||||
Position() syntax.Position // returns the current position of execution in this frame
|
||||
}
|
||||
|
||||
// DebugFrame returns the debugger interface for
|
||||
// the specified frame of the interpreter's call stack.
|
||||
// Frame numbering is as for Thread.CallFrame.
|
||||
//
|
||||
// This function is intended for use in debugging tools.
|
||||
// Most applications should have no need for it; use CallFrame instead.
|
||||
func (thread *Thread) DebugFrame(depth int) DebugFrame { return thread.frameAt(depth) }
|
||||
@@ -0,0 +1,3 @@
|
||||
// The presence of this file allows the package to use the
|
||||
// "go:linkname" hack to call non-exported functions in the
|
||||
// Go runtime, such as hardware-accelerated string hashing.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+322
@@ -0,0 +1,322 @@
|
||||
// Copyright 2017 The Bazel Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package starlark_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"go.starlark.net/starlark"
|
||||
)
|
||||
|
||||
// ExampleExecFile demonstrates a simple embedding
|
||||
// of the Starlark interpreter into a Go program.
|
||||
func ExampleExecFile() {
|
||||
const data = `
|
||||
print(greeting + ", world")
|
||||
print(repeat("one"))
|
||||
print(repeat("mur", 2))
|
||||
squares = [x*x for x in range(10)]
|
||||
`
|
||||
|
||||
// repeat(str, n=1) is a Go function called from Starlark.
|
||||
// It behaves like the 'string * int' operation.
|
||||
repeat := func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
|
||||
var s string
|
||||
var n int = 1
|
||||
if err := starlark.UnpackArgs(b.Name(), args, kwargs, "s", &s, "n?", &n); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return starlark.String(strings.Repeat(s, n)), nil
|
||||
}
|
||||
|
||||
// The Thread defines the behavior of the built-in 'print' function.
|
||||
thread := &starlark.Thread{
|
||||
Name: "example",
|
||||
Print: func(_ *starlark.Thread, msg string) { fmt.Println(msg) },
|
||||
}
|
||||
|
||||
// This dictionary defines the pre-declared environment.
|
||||
predeclared := starlark.StringDict{
|
||||
"greeting": starlark.String("hello"),
|
||||
"repeat": starlark.NewBuiltin("repeat", repeat),
|
||||
}
|
||||
|
||||
// Execute a program.
|
||||
globals, err := starlark.ExecFile(thread, "apparent/filename.star", data, predeclared)
|
||||
if err != nil {
|
||||
if evalErr, ok := err.(*starlark.EvalError); ok {
|
||||
log.Fatal(evalErr.Backtrace())
|
||||
}
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Print the global environment.
|
||||
fmt.Println("\nGlobals:")
|
||||
for _, name := range globals.Keys() {
|
||||
v := globals[name]
|
||||
fmt.Printf("%s (%s) = %s\n", name, v.Type(), v.String())
|
||||
}
|
||||
|
||||
// Output:
|
||||
// hello, world
|
||||
// one
|
||||
// murmur
|
||||
//
|
||||
// Globals:
|
||||
// squares (list) = [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
|
||||
}
|
||||
|
||||
// ExampleThread_Load_sequential demonstrates a simple caching
|
||||
// implementation of 'load' that works sequentially.
|
||||
func ExampleThread_Load_sequential() {
|
||||
fakeFilesystem := map[string]string{
|
||||
"c.star": `load("b.star", "b"); c = b + "!"`,
|
||||
"b.star": `load("a.star", "a"); b = a + ", world"`,
|
||||
"a.star": `a = "Hello"`,
|
||||
}
|
||||
|
||||
type entry struct {
|
||||
globals starlark.StringDict
|
||||
err error
|
||||
}
|
||||
|
||||
cache := make(map[string]*entry)
|
||||
|
||||
var load func(_ *starlark.Thread, module string) (starlark.StringDict, error)
|
||||
load = func(_ *starlark.Thread, module string) (starlark.StringDict, error) {
|
||||
e, ok := cache[module]
|
||||
if e == nil {
|
||||
if ok {
|
||||
// request for package whose loading is in progress
|
||||
return nil, fmt.Errorf("cycle in load graph")
|
||||
}
|
||||
|
||||
// Add a placeholder to indicate "load in progress".
|
||||
cache[module] = nil
|
||||
|
||||
// Load and initialize the module in a new thread.
|
||||
data := fakeFilesystem[module]
|
||||
thread := &starlark.Thread{Name: "exec " + module, Load: load}
|
||||
globals, err := starlark.ExecFile(thread, module, data, nil)
|
||||
e = &entry{globals, err}
|
||||
|
||||
// Update the cache.
|
||||
cache[module] = e
|
||||
}
|
||||
return e.globals, e.err
|
||||
}
|
||||
|
||||
globals, err := load(nil, "c.star")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(globals["c"])
|
||||
|
||||
// Output:
|
||||
// "Hello, world!"
|
||||
}
|
||||
|
||||
// ExampleThread_Load_parallel demonstrates a parallel implementation
|
||||
// of 'load' with caching, duplicate suppression, and cycle detection.
|
||||
func ExampleThread_Load_parallel() {
|
||||
cache := &cache{
|
||||
cache: make(map[string]*entry),
|
||||
fakeFilesystem: map[string]string{
|
||||
"c.star": `load("a.star", "a"); c = a * 2`,
|
||||
"b.star": `load("a.star", "a"); b = a * 3`,
|
||||
"a.star": `a = 1; print("loaded a")`,
|
||||
},
|
||||
}
|
||||
|
||||
// We load modules b and c in parallel by concurrent calls to
|
||||
// cache.Load. Both of them load module a, but a is executed
|
||||
// only once, as witnessed by the sole output of its print
|
||||
// statement.
|
||||
|
||||
ch := make(chan string)
|
||||
for _, name := range []string{"b", "c"} {
|
||||
go func(name string) {
|
||||
globals, err := cache.Load(name + ".star")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
ch <- fmt.Sprintf("%s = %s", name, globals[name])
|
||||
}(name)
|
||||
}
|
||||
got := []string{<-ch, <-ch}
|
||||
sort.Strings(got)
|
||||
fmt.Println(strings.Join(got, "\n"))
|
||||
|
||||
// Output:
|
||||
// loaded a
|
||||
// b = 3
|
||||
// c = 2
|
||||
}
|
||||
|
||||
// TestThread_Load_parallelCycle demonstrates detection
|
||||
// of cycles during parallel loading.
|
||||
func TestThreadLoad_ParallelCycle(t *testing.T) {
|
||||
cache := &cache{
|
||||
cache: make(map[string]*entry),
|
||||
fakeFilesystem: map[string]string{
|
||||
"c.star": `load("b.star", "b"); c = b * 2`,
|
||||
"b.star": `load("a.star", "a"); b = a * 3`,
|
||||
"a.star": `load("c.star", "c"); a = c * 5; print("loaded a")`,
|
||||
},
|
||||
}
|
||||
|
||||
ch := make(chan string)
|
||||
for _, name := range "bc" {
|
||||
name := string(name)
|
||||
go func() {
|
||||
_, err := cache.Load(name + ".star")
|
||||
if err == nil {
|
||||
log.Fatalf("Load of %s.star succeeded unexpectedly", name)
|
||||
}
|
||||
ch <- err.Error()
|
||||
}()
|
||||
}
|
||||
got := []string{<-ch, <-ch}
|
||||
sort.Strings(got)
|
||||
|
||||
// Typically, the c goroutine quickly blocks behind b;
|
||||
// b loads a, and a then fails to load c because it forms a cycle.
|
||||
// The errors observed by the two goroutines are:
|
||||
want1 := []string{
|
||||
"cannot load a.star: cannot load c.star: cycle in load graph", // from b
|
||||
"cannot load b.star: cannot load a.star: cannot load c.star: cycle in load graph", // from c
|
||||
}
|
||||
// But if the c goroutine is slow to start, b loads a,
|
||||
// and a loads c; then c fails to load b because it forms a cycle.
|
||||
// The errors this time are:
|
||||
want2 := []string{
|
||||
"cannot load a.star: cannot load c.star: cannot load b.star: cycle in load graph", // from b
|
||||
"cannot load b.star: cycle in load graph", // from c
|
||||
}
|
||||
if !reflect.DeepEqual(got, want1) && !reflect.DeepEqual(got, want2) {
|
||||
t.Error(got)
|
||||
}
|
||||
}
|
||||
|
||||
// cache is a concurrency-safe, duplicate-suppressing,
|
||||
// non-blocking cache of the doLoad function.
|
||||
// See Section 9.7 of gopl.io for an explanation of this structure.
|
||||
// It also features online deadlock (load cycle) detection.
|
||||
type cache struct {
|
||||
cacheMu sync.Mutex
|
||||
cache map[string]*entry
|
||||
|
||||
fakeFilesystem map[string]string
|
||||
}
|
||||
|
||||
type entry struct {
|
||||
owner unsafe.Pointer // a *cycleChecker; see cycleCheck
|
||||
globals starlark.StringDict
|
||||
err error
|
||||
ready chan struct{}
|
||||
}
|
||||
|
||||
func (c *cache) Load(module string) (starlark.StringDict, error) {
|
||||
return c.get(new(cycleChecker), module)
|
||||
}
|
||||
|
||||
// get loads and returns an entry (if not already loaded).
|
||||
func (c *cache) get(cc *cycleChecker, module string) (starlark.StringDict, error) {
|
||||
c.cacheMu.Lock()
|
||||
e := c.cache[module]
|
||||
if e != nil {
|
||||
c.cacheMu.Unlock()
|
||||
// Some other goroutine is getting this module.
|
||||
// Wait for it to become ready.
|
||||
|
||||
// Detect load cycles to avoid deadlocks.
|
||||
if err := cycleCheck(e, cc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cc.setWaitsFor(e)
|
||||
<-e.ready
|
||||
cc.setWaitsFor(nil)
|
||||
} else {
|
||||
// First request for this module.
|
||||
e = &entry{ready: make(chan struct{})}
|
||||
c.cache[module] = e
|
||||
c.cacheMu.Unlock()
|
||||
|
||||
e.setOwner(cc)
|
||||
e.globals, e.err = c.doLoad(cc, module)
|
||||
e.setOwner(nil)
|
||||
|
||||
// Broadcast that the entry is now ready.
|
||||
close(e.ready)
|
||||
}
|
||||
return e.globals, e.err
|
||||
}
|
||||
|
||||
func (c *cache) doLoad(cc *cycleChecker, module string) (starlark.StringDict, error) {
|
||||
thread := &starlark.Thread{
|
||||
Name: "exec " + module,
|
||||
Print: func(_ *starlark.Thread, msg string) { fmt.Println(msg) },
|
||||
Load: func(_ *starlark.Thread, module string) (starlark.StringDict, error) {
|
||||
// Tunnel the cycle-checker state for this "thread of loading".
|
||||
return c.get(cc, module)
|
||||
},
|
||||
}
|
||||
data := c.fakeFilesystem[module]
|
||||
return starlark.ExecFile(thread, module, data, nil)
|
||||
}
|
||||
|
||||
// -- concurrent cycle checking --
|
||||
|
||||
// A cycleChecker is used for concurrent deadlock detection.
|
||||
// Each top-level call to Load creates its own cycleChecker,
|
||||
// which is passed to all recursive calls it makes.
|
||||
// It corresponds to a logical thread in the deadlock detection literature.
|
||||
type cycleChecker struct {
|
||||
waitsFor unsafe.Pointer // an *entry; see cycleCheck
|
||||
}
|
||||
|
||||
func (cc *cycleChecker) setWaitsFor(e *entry) {
|
||||
atomic.StorePointer(&cc.waitsFor, unsafe.Pointer(e))
|
||||
}
|
||||
|
||||
func (e *entry) setOwner(cc *cycleChecker) {
|
||||
atomic.StorePointer(&e.owner, unsafe.Pointer(cc))
|
||||
}
|
||||
|
||||
// cycleCheck reports whether there is a path in the waits-for graph
|
||||
// from resource 'e' to thread 'me'.
|
||||
//
|
||||
// The waits-for graph (WFG) is a bipartite graph whose nodes are
|
||||
// alternately of type entry and cycleChecker. Each node has at most
|
||||
// one outgoing edge. An entry has an "owner" edge to a cycleChecker
|
||||
// while it is being readied by that cycleChecker, and a cycleChecker
|
||||
// has a "waits-for" edge to an entry while it is waiting for that entry
|
||||
// to become ready.
|
||||
//
|
||||
// Before adding a waits-for edge, the cache checks whether the new edge
|
||||
// would form a cycle. If so, this indicates that the load graph is
|
||||
// cyclic and that the following wait operation would deadlock.
|
||||
func cycleCheck(e *entry, me *cycleChecker) error {
|
||||
for e != nil {
|
||||
cc := (*cycleChecker)(atomic.LoadPointer(&e.owner))
|
||||
if cc == nil {
|
||||
break
|
||||
}
|
||||
if cc == me {
|
||||
return fmt.Errorf("cycle in load graph")
|
||||
}
|
||||
e = (*entry)(atomic.LoadPointer(&cc.waitsFor))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
// Copyright 2017 The Bazel Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package starlark
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
_ "unsafe" // for go:linkname hack
|
||||
)
|
||||
|
||||
// hashtable is used to represent Starlark dict and set values.
|
||||
// It is a hash table whose key/value entries form a doubly-linked list
|
||||
// in the order the entries were inserted.
|
||||
//
|
||||
// Initialized instances of hashtable must not be copied.
|
||||
type hashtable struct {
|
||||
table []bucket // len is zero or a power of two
|
||||
bucket0 [1]bucket // inline allocation for small maps.
|
||||
len uint32
|
||||
itercount uint32 // number of active iterators (ignored if frozen)
|
||||
head *entry // insertion order doubly-linked list; may be nil
|
||||
tailLink **entry // address of nil link at end of list (perhaps &head)
|
||||
frozen bool
|
||||
|
||||
_ noCopy // triggers vet copylock check on this type.
|
||||
}
|
||||
|
||||
// noCopy is zero-sized type that triggers vet's copylock check.
|
||||
// See https://github.com/golang/go/issues/8005#issuecomment-190753527.
|
||||
type noCopy struct{}
|
||||
|
||||
func (*noCopy) Lock() {}
|
||||
func (*noCopy) Unlock() {}
|
||||
|
||||
const bucketSize = 8
|
||||
|
||||
type bucket struct {
|
||||
entries [bucketSize]entry
|
||||
next *bucket // linked list of buckets
|
||||
}
|
||||
|
||||
type entry struct {
|
||||
hash uint32 // nonzero => in use
|
||||
key, value Value
|
||||
next *entry // insertion order doubly-linked list; may be nil
|
||||
prevLink **entry // address of link to this entry (perhaps &head)
|
||||
}
|
||||
|
||||
func (ht *hashtable) init(size int) {
|
||||
if size < 0 {
|
||||
panic("size < 0")
|
||||
}
|
||||
nb := 1
|
||||
for overloaded(size, nb) {
|
||||
nb = nb << 1
|
||||
}
|
||||
if nb < 2 {
|
||||
ht.table = ht.bucket0[:1]
|
||||
} else {
|
||||
ht.table = make([]bucket, nb)
|
||||
}
|
||||
ht.tailLink = &ht.head
|
||||
}
|
||||
|
||||
func (ht *hashtable) freeze() {
|
||||
if !ht.frozen {
|
||||
ht.frozen = true
|
||||
for e := ht.head; e != nil; e = e.next {
|
||||
e.key.Freeze()
|
||||
e.value.Freeze()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ht *hashtable) insert(k, v Value) error {
|
||||
if err := ht.checkMutable("insert into"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ht.table == nil {
|
||||
ht.init(1)
|
||||
}
|
||||
h, err := k.Hash()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if h == 0 {
|
||||
h = 1 // zero is reserved
|
||||
}
|
||||
|
||||
retry:
|
||||
var insert *entry
|
||||
|
||||
// Inspect each bucket in the bucket list.
|
||||
p := &ht.table[h&(uint32(len(ht.table)-1))]
|
||||
for {
|
||||
for i := range p.entries {
|
||||
e := &p.entries[i]
|
||||
if e.hash != h {
|
||||
if e.hash == 0 {
|
||||
// Found empty entry; make a note.
|
||||
insert = e
|
||||
}
|
||||
continue
|
||||
}
|
||||
if eq, err := Equal(k, e.key); err != nil {
|
||||
return err // e.g. excessively recursive tuple
|
||||
} else if !eq {
|
||||
continue
|
||||
}
|
||||
// Key already present; update value.
|
||||
e.value = v
|
||||
return nil
|
||||
}
|
||||
if p.next == nil {
|
||||
break
|
||||
}
|
||||
p = p.next
|
||||
}
|
||||
|
||||
// Key not found. p points to the last bucket.
|
||||
|
||||
// Does the number of elements exceed the buckets' load factor?
|
||||
if overloaded(int(ht.len), len(ht.table)) {
|
||||
ht.grow()
|
||||
goto retry
|
||||
}
|
||||
|
||||
if insert == nil {
|
||||
// No space in existing buckets. Add a new one to the bucket list.
|
||||
b := new(bucket)
|
||||
p.next = b
|
||||
insert = &b.entries[0]
|
||||
}
|
||||
|
||||
// Insert key/value pair.
|
||||
insert.hash = h
|
||||
insert.key = k
|
||||
insert.value = v
|
||||
|
||||
// Append entry to doubly-linked list.
|
||||
insert.prevLink = ht.tailLink
|
||||
*ht.tailLink = insert
|
||||
ht.tailLink = &insert.next
|
||||
|
||||
ht.len++
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func overloaded(elems, buckets int) bool {
|
||||
const loadFactor = 6.5 // just a guess
|
||||
return elems >= bucketSize && float64(elems) >= loadFactor*float64(buckets)
|
||||
}
|
||||
|
||||
func (ht *hashtable) grow() {
|
||||
// Double the number of buckets and rehash.
|
||||
//
|
||||
// Even though this makes reentrant calls to ht.insert,
|
||||
// calls Equals unnecessarily (since there can't be duplicate keys),
|
||||
// and recomputes the hash unnecessarily, the gains from
|
||||
// avoiding these steps were found to be too small to justify
|
||||
// the extra logic: -2% on hashtable benchmark.
|
||||
ht.table = make([]bucket, len(ht.table)<<1)
|
||||
oldhead := ht.head
|
||||
ht.head = nil
|
||||
ht.tailLink = &ht.head
|
||||
ht.len = 0
|
||||
for e := oldhead; e != nil; e = e.next {
|
||||
ht.insert(e.key, e.value)
|
||||
}
|
||||
ht.bucket0[0] = bucket{} // clear out unused initial bucket
|
||||
}
|
||||
|
||||
func (ht *hashtable) lookup(k Value) (v Value, found bool, err error) {
|
||||
h, err := k.Hash()
|
||||
if err != nil {
|
||||
return nil, false, err // unhashable
|
||||
}
|
||||
if h == 0 {
|
||||
h = 1 // zero is reserved
|
||||
}
|
||||
if ht.table == nil {
|
||||
return None, false, nil // empty
|
||||
}
|
||||
|
||||
// Inspect each bucket in the bucket list.
|
||||
for p := &ht.table[h&(uint32(len(ht.table)-1))]; p != nil; p = p.next {
|
||||
for i := range p.entries {
|
||||
e := &p.entries[i]
|
||||
if e.hash == h {
|
||||
if eq, err := Equal(k, e.key); err != nil {
|
||||
return nil, false, err // e.g. excessively recursive tuple
|
||||
} else if eq {
|
||||
return e.value, true, nil // found
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return None, false, nil // not found
|
||||
}
|
||||
|
||||
// count returns the number of distinct elements of iter that are elements of ht.
|
||||
func (ht *hashtable) count(iter Iterator) (int, error) {
|
||||
if ht.table == nil {
|
||||
return 0, nil // empty
|
||||
}
|
||||
|
||||
var k Value
|
||||
count := 0
|
||||
|
||||
// Use a bitset per table entry to record seen elements of ht.
|
||||
// Elements are identified by their bucket number and index within the bucket.
|
||||
// Each bitset gets one word initially, but may grow.
|
||||
storage := make([]big.Word, len(ht.table))
|
||||
bitsets := make([]big.Int, len(ht.table))
|
||||
for i := range bitsets {
|
||||
bitsets[i].SetBits(storage[i : i+1 : i+1])
|
||||
}
|
||||
for iter.Next(&k) && count != int(ht.len) {
|
||||
h, err := k.Hash()
|
||||
if err != nil {
|
||||
return 0, err // unhashable
|
||||
}
|
||||
if h == 0 {
|
||||
h = 1 // zero is reserved
|
||||
}
|
||||
|
||||
// Inspect each bucket in the bucket list.
|
||||
bucketId := h & (uint32(len(ht.table) - 1))
|
||||
i := 0
|
||||
for p := &ht.table[bucketId]; p != nil; p = p.next {
|
||||
for j := range p.entries {
|
||||
e := &p.entries[j]
|
||||
if e.hash == h {
|
||||
if eq, err := Equal(k, e.key); err != nil {
|
||||
return 0, err
|
||||
} else if eq {
|
||||
bitIndex := i<<3 + j
|
||||
if bitsets[bucketId].Bit(bitIndex) == 0 {
|
||||
bitsets[bucketId].SetBit(&bitsets[bucketId], bitIndex, 1)
|
||||
count++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// Items returns all the items in the map (as key/value pairs) in insertion order.
|
||||
func (ht *hashtable) items() []Tuple {
|
||||
items := make([]Tuple, 0, ht.len)
|
||||
array := make([]Value, ht.len*2) // allocate a single backing array
|
||||
for e := ht.head; e != nil; e = e.next {
|
||||
pair := Tuple(array[:2:2])
|
||||
array = array[2:]
|
||||
pair[0] = e.key
|
||||
pair[1] = e.value
|
||||
items = append(items, pair)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func (ht *hashtable) first() (Value, bool) {
|
||||
if ht.head != nil {
|
||||
return ht.head.key, true
|
||||
}
|
||||
return None, false
|
||||
}
|
||||
|
||||
func (ht *hashtable) keys() []Value {
|
||||
keys := make([]Value, 0, ht.len)
|
||||
for e := ht.head; e != nil; e = e.next {
|
||||
keys = append(keys, e.key)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func (ht *hashtable) delete(k Value) (v Value, found bool, err error) {
|
||||
if err := ht.checkMutable("delete from"); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if ht.table == nil {
|
||||
return None, false, nil // empty
|
||||
}
|
||||
h, err := k.Hash()
|
||||
if err != nil {
|
||||
return nil, false, err // unhashable
|
||||
}
|
||||
if h == 0 {
|
||||
h = 1 // zero is reserved
|
||||
}
|
||||
|
||||
// Inspect each bucket in the bucket list.
|
||||
for p := &ht.table[h&(uint32(len(ht.table)-1))]; p != nil; p = p.next {
|
||||
for i := range p.entries {
|
||||
e := &p.entries[i]
|
||||
if e.hash == h {
|
||||
if eq, err := Equal(k, e.key); err != nil {
|
||||
return nil, false, err
|
||||
} else if eq {
|
||||
// Remove e from doubly-linked list.
|
||||
*e.prevLink = e.next
|
||||
if e.next == nil {
|
||||
ht.tailLink = e.prevLink // deletion of last entry
|
||||
} else {
|
||||
e.next.prevLink = e.prevLink
|
||||
}
|
||||
|
||||
v := e.value
|
||||
*e = entry{}
|
||||
ht.len--
|
||||
return v, true, nil // found
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(adonovan): opt: remove completely empty bucket from bucket list.
|
||||
|
||||
return None, false, nil // not found
|
||||
}
|
||||
|
||||
// checkMutable reports an error if the hash table should not be mutated.
|
||||
// verb+" dict" should describe the operation.
|
||||
func (ht *hashtable) checkMutable(verb string) error {
|
||||
if ht.frozen {
|
||||
return fmt.Errorf("cannot %s frozen hash table", verb)
|
||||
}
|
||||
if ht.itercount > 0 {
|
||||
return fmt.Errorf("cannot %s hash table during iteration", verb)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ht *hashtable) clear() error {
|
||||
if err := ht.checkMutable("clear"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ht.table != nil {
|
||||
for i := range ht.table {
|
||||
ht.table[i] = bucket{}
|
||||
}
|
||||
}
|
||||
ht.head = nil
|
||||
ht.tailLink = &ht.head
|
||||
ht.len = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ht *hashtable) addAll(other *hashtable) error {
|
||||
for e := other.head; e != nil; e = e.next {
|
||||
if err := ht.insert(e.key, e.value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// dump is provided as an aid to debugging.
|
||||
func (ht *hashtable) dump() {
|
||||
fmt.Printf("hashtable %p len=%d head=%p tailLink=%p",
|
||||
ht, ht.len, ht.head, ht.tailLink)
|
||||
if ht.tailLink != nil {
|
||||
fmt.Printf(" *tailLink=%p", *ht.tailLink)
|
||||
}
|
||||
fmt.Println()
|
||||
for j := range ht.table {
|
||||
fmt.Printf("bucket chain %d\n", j)
|
||||
for p := &ht.table[j]; p != nil; p = p.next {
|
||||
fmt.Printf("bucket %p\n", p)
|
||||
for i := range p.entries {
|
||||
e := &p.entries[i]
|
||||
fmt.Printf("\tentry %d @ %p hash=%d key=%v value=%v\n",
|
||||
i, e, e.hash, e.key, e.value)
|
||||
fmt.Printf("\t\tnext=%p &next=%p prev=%p",
|
||||
e.next, &e.next, e.prevLink)
|
||||
if e.prevLink != nil {
|
||||
fmt.Printf(" *prev=%p", *e.prevLink)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ht *hashtable) iterate() *keyIterator {
|
||||
if !ht.frozen {
|
||||
ht.itercount++
|
||||
}
|
||||
return &keyIterator{ht: ht, e: ht.head}
|
||||
}
|
||||
|
||||
type keyIterator struct {
|
||||
ht *hashtable
|
||||
e *entry
|
||||
}
|
||||
|
||||
func (it *keyIterator) Next(k *Value) bool {
|
||||
if it.e != nil {
|
||||
*k = it.e.key
|
||||
it.e = it.e.next
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (it *keyIterator) Done() {
|
||||
if !it.ht.frozen {
|
||||
it.ht.itercount--
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(adonovan): use go1.19's maphash.String.
|
||||
|
||||
// hashString computes the hash of s.
|
||||
func hashString(s string) uint32 {
|
||||
if len(s) >= 12 {
|
||||
// Call the Go runtime's optimized hash implementation,
|
||||
// which uses the AESENC instruction on amd64 machines.
|
||||
return uint32(goStringHash(s, 0))
|
||||
}
|
||||
return softHashString(s)
|
||||
}
|
||||
|
||||
//go:linkname goStringHash runtime.stringHash
|
||||
func goStringHash(s string, seed uintptr) uintptr
|
||||
|
||||
// softHashString computes the 32-bit FNV-1a hash of s in software.
|
||||
func softHashString(s string) uint32 {
|
||||
var h uint32 = 2166136261
|
||||
for i := 0; i < len(s); i++ {
|
||||
h ^= uint32(s[i])
|
||||
h *= 16777619
|
||||
}
|
||||
return h
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
// Copyright 2017 The Bazel Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package starlark
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHashtable(t *testing.T) {
|
||||
makeTestIntsOnce.Do(makeTestInts)
|
||||
testHashtable(t, make(map[int]bool))
|
||||
}
|
||||
|
||||
func BenchmarkStringHash(b *testing.B) {
|
||||
for len := 1; len <= 1024; len *= 2 {
|
||||
buf := make([]byte, len)
|
||||
rand.New(rand.NewSource(0)).Read(buf)
|
||||
s := string(buf)
|
||||
|
||||
b.Run(fmt.Sprintf("hard-%d", len), func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
hashString(s)
|
||||
}
|
||||
})
|
||||
b.Run(fmt.Sprintf("soft-%d", len), func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
softHashString(s)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkHashtable(b *testing.B) {
|
||||
makeTestIntsOnce.Do(makeTestInts)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
testHashtable(b, nil)
|
||||
}
|
||||
}
|
||||
|
||||
const testIters = 10000
|
||||
|
||||
var (
|
||||
// testInts is a zipf-distributed array of Ints and corresponding ints.
|
||||
// This removes the cost of generating them on the fly during benchmarking.
|
||||
// Without this, Zipf and MakeInt dominate CPU and memory costs, respectively.
|
||||
makeTestIntsOnce sync.Once
|
||||
testInts [3 * testIters]struct {
|
||||
Int Int
|
||||
goInt int
|
||||
}
|
||||
)
|
||||
|
||||
func makeTestInts() {
|
||||
zipf := rand.NewZipf(rand.New(rand.NewSource(0)), 1.1, 1.0, 1000.0)
|
||||
for i := range &testInts {
|
||||
r := int(zipf.Uint64())
|
||||
testInts[i].goInt = r
|
||||
testInts[i].Int = MakeInt(r)
|
||||
}
|
||||
}
|
||||
|
||||
// testHashtable is both a test and a benchmark of hashtable.
|
||||
// When sane != nil, it acts as a test against the semantics of Go's map.
|
||||
func testHashtable(tb testing.TB, sane map[int]bool) {
|
||||
var i int // index into testInts
|
||||
|
||||
var ht hashtable
|
||||
|
||||
// Insert 10000 random ints into the map.
|
||||
for j := 0; j < testIters; j++ {
|
||||
k := testInts[i]
|
||||
i++
|
||||
if err := ht.insert(k.Int, None); err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
if sane != nil {
|
||||
sane[k.goInt] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Do 10000 random lookups in the map.
|
||||
for j := 0; j < testIters; j++ {
|
||||
k := testInts[i]
|
||||
i++
|
||||
_, found, err := ht.lookup(k.Int)
|
||||
if err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
if sane != nil {
|
||||
_, found2 := sane[k.goInt]
|
||||
if found != found2 {
|
||||
tb.Fatal("sanity check failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Do 10000 random deletes from the map.
|
||||
for j := 0; j < testIters; j++ {
|
||||
k := testInts[i]
|
||||
i++
|
||||
_, found, err := ht.delete(k.Int)
|
||||
if err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
if sane != nil {
|
||||
_, found2 := sane[k.goInt]
|
||||
if found != found2 {
|
||||
tb.Fatal("sanity check failed")
|
||||
}
|
||||
delete(sane, k.goInt)
|
||||
}
|
||||
}
|
||||
|
||||
if sane != nil {
|
||||
if int(ht.len) != len(sane) {
|
||||
tb.Fatal("sanity check failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashtableCount(t *testing.T) {
|
||||
const count = 1000
|
||||
ht := new(hashtable)
|
||||
for i := 0; i < count; i++ {
|
||||
ht.insert(MakeInt(i), None)
|
||||
}
|
||||
|
||||
if c, err := ht.count(rangeValue{0, count, 1, count}.Iterate()); err != nil {
|
||||
t.Error(err)
|
||||
} else if c != count {
|
||||
t.Errorf("count doesn't match: expected %d got %d", count, c)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
// Copyright 2017 The Bazel Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package starlark
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"math/big"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
"go.starlark.net/syntax"
|
||||
)
|
||||
|
||||
// Int is the type of a Starlark int.
|
||||
//
|
||||
// The zero value is not a legal value; use MakeInt(0).
|
||||
type Int struct{ impl intImpl }
|
||||
|
||||
// --- high-level accessors ---
|
||||
|
||||
// MakeInt returns a Starlark int for the specified signed integer.
|
||||
func MakeInt(x int) Int { return MakeInt64(int64(x)) }
|
||||
|
||||
// MakeInt64 returns a Starlark int for the specified int64.
|
||||
func MakeInt64(x int64) Int {
|
||||
if math.MinInt32 <= x && x <= math.MaxInt32 {
|
||||
return makeSmallInt(x)
|
||||
}
|
||||
return makeBigInt(big.NewInt(x))
|
||||
}
|
||||
|
||||
// MakeUint returns a Starlark int for the specified unsigned integer.
|
||||
func MakeUint(x uint) Int { return MakeUint64(uint64(x)) }
|
||||
|
||||
// MakeUint64 returns a Starlark int for the specified uint64.
|
||||
func MakeUint64(x uint64) Int {
|
||||
if x <= math.MaxInt32 {
|
||||
return makeSmallInt(int64(x))
|
||||
}
|
||||
return makeBigInt(new(big.Int).SetUint64(x))
|
||||
}
|
||||
|
||||
// MakeBigInt returns a Starlark int for the specified big.Int.
|
||||
// The new Int value will contain a copy of x. The caller is safe to modify x.
|
||||
func MakeBigInt(x *big.Int) Int {
|
||||
if isSmall(x) {
|
||||
return makeSmallInt(x.Int64())
|
||||
}
|
||||
z := new(big.Int).Set(x)
|
||||
return makeBigInt(z)
|
||||
}
|
||||
|
||||
func isSmall(x *big.Int) bool {
|
||||
n := x.BitLen()
|
||||
return n < 32 || n == 32 && x.Int64() == math.MinInt32
|
||||
}
|
||||
|
||||
var (
|
||||
zero, one = makeSmallInt(0), makeSmallInt(1)
|
||||
oneBig = big.NewInt(1)
|
||||
|
||||
_ HasUnary = Int{}
|
||||
)
|
||||
|
||||
// Unary implements the operations +int, -int, and ~int.
|
||||
func (i Int) Unary(op syntax.Token) (Value, error) {
|
||||
switch op {
|
||||
case syntax.MINUS:
|
||||
return zero.Sub(i), nil
|
||||
case syntax.PLUS:
|
||||
return i, nil
|
||||
case syntax.TILDE:
|
||||
return i.Not(), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Int64 returns the value as an int64.
|
||||
// If it is not exactly representable the result is undefined and ok is false.
|
||||
func (i Int) Int64() (_ int64, ok bool) {
|
||||
iSmall, iBig := i.get()
|
||||
if iBig != nil {
|
||||
x, acc := bigintToInt64(iBig)
|
||||
if acc != big.Exact {
|
||||
return // inexact
|
||||
}
|
||||
return x, true
|
||||
}
|
||||
return iSmall, true
|
||||
}
|
||||
|
||||
// BigInt returns a new big.Int with the same value as the Int.
|
||||
func (i Int) BigInt() *big.Int {
|
||||
iSmall, iBig := i.get()
|
||||
if iBig != nil {
|
||||
return new(big.Int).Set(iBig)
|
||||
}
|
||||
return big.NewInt(iSmall)
|
||||
}
|
||||
|
||||
// bigInt returns the value as a big.Int.
|
||||
// It differs from BigInt in that this method returns the actual
|
||||
// reference and any modification will change the state of i.
|
||||
func (i Int) bigInt() *big.Int {
|
||||
iSmall, iBig := i.get()
|
||||
if iBig != nil {
|
||||
return iBig
|
||||
}
|
||||
return big.NewInt(iSmall)
|
||||
}
|
||||
|
||||
// Uint64 returns the value as a uint64.
|
||||
// If it is not exactly representable the result is undefined and ok is false.
|
||||
func (i Int) Uint64() (_ uint64, ok bool) {
|
||||
iSmall, iBig := i.get()
|
||||
if iBig != nil {
|
||||
x, acc := bigintToUint64(iBig)
|
||||
if acc != big.Exact {
|
||||
return // inexact
|
||||
}
|
||||
return x, true
|
||||
}
|
||||
if iSmall < 0 {
|
||||
return // inexact
|
||||
}
|
||||
return uint64(iSmall), true
|
||||
}
|
||||
|
||||
// The math/big API should provide this function.
|
||||
func bigintToInt64(i *big.Int) (int64, big.Accuracy) {
|
||||
sign := i.Sign()
|
||||
if sign > 0 {
|
||||
if i.Cmp(maxint64) > 0 {
|
||||
return math.MaxInt64, big.Below
|
||||
}
|
||||
} else if sign < 0 {
|
||||
if i.Cmp(minint64) < 0 {
|
||||
return math.MinInt64, big.Above
|
||||
}
|
||||
}
|
||||
return i.Int64(), big.Exact
|
||||
}
|
||||
|
||||
// The math/big API should provide this function.
|
||||
func bigintToUint64(i *big.Int) (uint64, big.Accuracy) {
|
||||
sign := i.Sign()
|
||||
if sign > 0 {
|
||||
if i.BitLen() > 64 {
|
||||
return math.MaxUint64, big.Below
|
||||
}
|
||||
} else if sign < 0 {
|
||||
return 0, big.Above
|
||||
}
|
||||
return i.Uint64(), big.Exact
|
||||
}
|
||||
|
||||
var (
|
||||
minint64 = new(big.Int).SetInt64(math.MinInt64)
|
||||
maxint64 = new(big.Int).SetInt64(math.MaxInt64)
|
||||
)
|
||||
|
||||
func (i Int) Format(s fmt.State, ch rune) {
|
||||
iSmall, iBig := i.get()
|
||||
if iBig != nil {
|
||||
iBig.Format(s, ch)
|
||||
return
|
||||
}
|
||||
big.NewInt(iSmall).Format(s, ch)
|
||||
}
|
||||
func (i Int) String() string {
|
||||
iSmall, iBig := i.get()
|
||||
if iBig != nil {
|
||||
return iBig.Text(10)
|
||||
}
|
||||
return strconv.FormatInt(iSmall, 10)
|
||||
}
|
||||
func (i Int) Type() string { return "int" }
|
||||
func (i Int) Freeze() {} // immutable
|
||||
func (i Int) Truth() Bool { return i.Sign() != 0 }
|
||||
func (i Int) Hash() (uint32, error) {
|
||||
iSmall, iBig := i.get()
|
||||
var lo big.Word
|
||||
if iBig != nil {
|
||||
lo = iBig.Bits()[0]
|
||||
} else {
|
||||
lo = big.Word(iSmall)
|
||||
}
|
||||
return 12582917 * uint32(lo+3), nil
|
||||
}
|
||||
|
||||
// Cmp implements comparison of two Int values.
|
||||
// Required by the TotallyOrdered interface.
|
||||
func (i Int) Cmp(v Value, depth int) (int, error) {
|
||||
j := v.(Int)
|
||||
iSmall, iBig := i.get()
|
||||
jSmall, jBig := j.get()
|
||||
if iBig != nil || jBig != nil {
|
||||
return i.bigInt().Cmp(j.bigInt()), nil
|
||||
}
|
||||
return signum64(iSmall - jSmall), nil // safe: int32 operands
|
||||
}
|
||||
|
||||
// Float returns the float value nearest i.
|
||||
func (i Int) Float() Float {
|
||||
iSmall, iBig := i.get()
|
||||
if iBig != nil {
|
||||
// Fast path for hardware int-to-float conversions.
|
||||
if iBig.IsUint64() {
|
||||
return Float(iBig.Uint64())
|
||||
} else if iBig.IsInt64() {
|
||||
return Float(iBig.Int64())
|
||||
} else {
|
||||
// Fast path for very big ints.
|
||||
const maxFiniteLen = 1023 + 1 // max exponent value + implicit mantissa bit
|
||||
if iBig.BitLen() > maxFiniteLen {
|
||||
return Float(math.Inf(iBig.Sign()))
|
||||
}
|
||||
}
|
||||
|
||||
f, _ := new(big.Float).SetInt(iBig).Float64()
|
||||
return Float(f)
|
||||
}
|
||||
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 {
|
||||
return xBig.Sign()
|
||||
}
|
||||
return signum64(xSmall)
|
||||
}
|
||||
|
||||
func (x Int) Add(y Int) Int {
|
||||
xSmall, xBig := x.get()
|
||||
ySmall, yBig := y.get()
|
||||
if xBig != nil || yBig != nil {
|
||||
return MakeBigInt(new(big.Int).Add(x.bigInt(), y.bigInt()))
|
||||
}
|
||||
return MakeInt64(xSmall + ySmall)
|
||||
}
|
||||
func (x Int) Sub(y Int) Int {
|
||||
xSmall, xBig := x.get()
|
||||
ySmall, yBig := y.get()
|
||||
if xBig != nil || yBig != nil {
|
||||
return MakeBigInt(new(big.Int).Sub(x.bigInt(), y.bigInt()))
|
||||
}
|
||||
return MakeInt64(xSmall - ySmall)
|
||||
}
|
||||
func (x Int) Mul(y Int) Int {
|
||||
xSmall, xBig := x.get()
|
||||
ySmall, yBig := y.get()
|
||||
if xBig != nil || yBig != nil {
|
||||
return MakeBigInt(new(big.Int).Mul(x.bigInt(), y.bigInt()))
|
||||
}
|
||||
return MakeInt64(xSmall * ySmall)
|
||||
}
|
||||
func (x Int) Or(y Int) Int {
|
||||
xSmall, xBig := x.get()
|
||||
ySmall, yBig := y.get()
|
||||
if xBig != nil || yBig != nil {
|
||||
return MakeBigInt(new(big.Int).Or(x.bigInt(), y.bigInt()))
|
||||
}
|
||||
return makeSmallInt(xSmall | ySmall)
|
||||
}
|
||||
func (x Int) And(y Int) Int {
|
||||
xSmall, xBig := x.get()
|
||||
ySmall, yBig := y.get()
|
||||
if xBig != nil || yBig != nil {
|
||||
return MakeBigInt(new(big.Int).And(x.bigInt(), y.bigInt()))
|
||||
}
|
||||
return makeSmallInt(xSmall & ySmall)
|
||||
}
|
||||
func (x Int) Xor(y Int) Int {
|
||||
xSmall, xBig := x.get()
|
||||
ySmall, yBig := y.get()
|
||||
if xBig != nil || yBig != nil {
|
||||
return MakeBigInt(new(big.Int).Xor(x.bigInt(), y.bigInt()))
|
||||
}
|
||||
return makeSmallInt(xSmall ^ ySmall)
|
||||
}
|
||||
func (x Int) Not() Int {
|
||||
xSmall, xBig := x.get()
|
||||
if xBig != nil {
|
||||
return MakeBigInt(new(big.Int).Not(xBig))
|
||||
}
|
||||
return makeSmallInt(^xSmall)
|
||||
}
|
||||
func (x Int) Lsh(y uint) Int { return MakeBigInt(new(big.Int).Lsh(x.bigInt(), y)) }
|
||||
func (x Int) Rsh(y uint) Int { return MakeBigInt(new(big.Int).Rsh(x.bigInt(), y)) }
|
||||
|
||||
// Precondition: y is nonzero.
|
||||
func (x Int) Div(y Int) Int {
|
||||
xSmall, xBig := x.get()
|
||||
ySmall, yBig := y.get()
|
||||
// http://python-history.blogspot.com/2010/08/why-pythons-integer-division-floors.html
|
||||
if xBig != nil || yBig != nil {
|
||||
xb, yb := x.bigInt(), y.bigInt()
|
||||
|
||||
var quo, rem big.Int
|
||||
quo.QuoRem(xb, yb, &rem)
|
||||
if (xb.Sign() < 0) != (yb.Sign() < 0) && rem.Sign() != 0 {
|
||||
quo.Sub(&quo, oneBig)
|
||||
}
|
||||
return MakeBigInt(&quo)
|
||||
}
|
||||
quo := xSmall / ySmall
|
||||
rem := xSmall % ySmall
|
||||
if (xSmall < 0) != (ySmall < 0) && rem != 0 {
|
||||
quo -= 1
|
||||
}
|
||||
return MakeInt64(quo)
|
||||
}
|
||||
|
||||
// Precondition: y is nonzero.
|
||||
func (x Int) Mod(y Int) Int {
|
||||
xSmall, xBig := x.get()
|
||||
ySmall, yBig := y.get()
|
||||
if xBig != nil || yBig != nil {
|
||||
xb, yb := x.bigInt(), y.bigInt()
|
||||
|
||||
var quo, rem big.Int
|
||||
quo.QuoRem(xb, yb, &rem)
|
||||
if (xb.Sign() < 0) != (yb.Sign() < 0) && rem.Sign() != 0 {
|
||||
rem.Add(&rem, yb)
|
||||
}
|
||||
return MakeBigInt(&rem)
|
||||
}
|
||||
rem := xSmall % ySmall
|
||||
if (xSmall < 0) != (ySmall < 0) && rem != 0 {
|
||||
rem += ySmall
|
||||
}
|
||||
return makeSmallInt(rem)
|
||||
}
|
||||
|
||||
func (i Int) rational() *big.Rat {
|
||||
iSmall, iBig := i.get()
|
||||
if iBig != nil {
|
||||
return new(big.Rat).SetInt(iBig)
|
||||
}
|
||||
return new(big.Rat).SetInt64(iSmall)
|
||||
}
|
||||
|
||||
// AsInt32 returns the value of x if is representable as an int32.
|
||||
func AsInt32(x Value) (int, error) {
|
||||
i, ok := x.(Int)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("got %s, want int", x.Type())
|
||||
}
|
||||
iSmall, iBig := i.get()
|
||||
if iBig != nil {
|
||||
return 0, fmt.Errorf("%s out of range", i)
|
||||
}
|
||||
return int(iSmall), nil
|
||||
}
|
||||
|
||||
// AsInt sets *ptr to the value of Starlark int x, if it is exactly representable,
|
||||
// otherwise it returns an error.
|
||||
// The type of ptr must be one of the pointer types *int, *int8, *int16, *int32, or *int64,
|
||||
// or one of their unsigned counterparts including *uintptr.
|
||||
func AsInt(x Value, ptr interface{}) error {
|
||||
xint, ok := x.(Int)
|
||||
if !ok {
|
||||
return fmt.Errorf("got %s, want int", x.Type())
|
||||
}
|
||||
|
||||
bits := reflect.TypeOf(ptr).Elem().Size() * 8
|
||||
switch ptr.(type) {
|
||||
case *int, *int8, *int16, *int32, *int64:
|
||||
i, ok := xint.Int64()
|
||||
if !ok || bits < 64 && !(-1<<(bits-1) <= i && i < 1<<(bits-1)) {
|
||||
return fmt.Errorf("%s out of range (want value in signed %d-bit range)", xint, bits)
|
||||
}
|
||||
switch ptr := ptr.(type) {
|
||||
case *int:
|
||||
*ptr = int(i)
|
||||
case *int8:
|
||||
*ptr = int8(i)
|
||||
case *int16:
|
||||
*ptr = int16(i)
|
||||
case *int32:
|
||||
*ptr = int32(i)
|
||||
case *int64:
|
||||
*ptr = int64(i)
|
||||
}
|
||||
|
||||
case *uint, *uint8, *uint16, *uint32, *uint64, *uintptr:
|
||||
i, ok := xint.Uint64()
|
||||
if !ok || bits < 64 && i >= 1<<bits {
|
||||
return fmt.Errorf("%s out of range (want value in unsigned %d-bit range)", xint, bits)
|
||||
}
|
||||
switch ptr := ptr.(type) {
|
||||
case *uint:
|
||||
*ptr = uint(i)
|
||||
case *uint8:
|
||||
*ptr = uint8(i)
|
||||
case *uint16:
|
||||
*ptr = uint16(i)
|
||||
case *uint32:
|
||||
*ptr = uint32(i)
|
||||
case *uint64:
|
||||
*ptr = uint64(i)
|
||||
case *uintptr:
|
||||
*ptr = uintptr(i)
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid argument type: %T", ptr))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NumberToInt converts a number x to an integer value.
|
||||
// An int is returned unchanged, a float is truncated towards zero.
|
||||
// NumberToInt reports an error for all other values.
|
||||
func NumberToInt(x Value) (Int, error) {
|
||||
switch x := x.(type) {
|
||||
case Int:
|
||||
return x, nil
|
||||
case Float:
|
||||
f := float64(x)
|
||||
if math.IsInf(f, 0) {
|
||||
return zero, fmt.Errorf("cannot convert float infinity to integer")
|
||||
} else if math.IsNaN(f) {
|
||||
return zero, fmt.Errorf("cannot convert float NaN to integer")
|
||||
}
|
||||
return finiteFloatToInt(x), nil
|
||||
|
||||
}
|
||||
return zero, fmt.Errorf("cannot convert %s to int", x.Type())
|
||||
}
|
||||
|
||||
// finiteFloatToInt converts f to an Int, truncating towards zero.
|
||||
// f must be finite.
|
||||
func finiteFloatToInt(f Float) Int {
|
||||
// We avoid '<= MaxInt64' so that both constants are exactly representable as floats.
|
||||
// See https://github.com/google/starlark-go/issues/375.
|
||||
if math.MinInt64 <= f && f < math.MaxInt64+1 {
|
||||
// small values
|
||||
return MakeInt64(int64(f))
|
||||
}
|
||||
rat := f.rational()
|
||||
if rat == nil {
|
||||
panic(f) // non-finite
|
||||
}
|
||||
return MakeBigInt(new(big.Int).Div(rat.Num(), rat.Denom()))
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//go:build (!linux && !darwin && !dragonfly && !freebsd && !netbsd && !solaris) || (!amd64 && !arm64 && !mips64x && !ppc64 && !ppc64le && !loong64 && !s390x)
|
||||
|
||||
package starlark
|
||||
|
||||
// generic Int implementation as a union
|
||||
|
||||
import "math/big"
|
||||
|
||||
type intImpl struct {
|
||||
// We use only the signed 32-bit range of small to ensure
|
||||
// that small+small and small*small do not overflow.
|
||||
small_ int64 // minint32 <= small <= maxint32
|
||||
big_ *big.Int // big != nil <=> value is not representable as int32
|
||||
}
|
||||
|
||||
// --- low-level accessors ---
|
||||
|
||||
// get returns the small and big components of the Int.
|
||||
// small is defined only if big is nil.
|
||||
// small is sign-extended to 64 bits for ease of subsequent arithmetic.
|
||||
func (i Int) get() (small int64, big *big.Int) {
|
||||
return i.impl.small_, i.impl.big_
|
||||
}
|
||||
|
||||
// Precondition: math.MinInt32 <= x && x <= math.MaxInt32
|
||||
func makeSmallInt(x int64) Int {
|
||||
return Int{intImpl{small_: x}}
|
||||
}
|
||||
|
||||
// Precondition: x cannot be represented as int32.
|
||||
func makeBigInt(x *big.Int) Int {
|
||||
return Int{intImpl{big_: x}}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
//go:build (linux || darwin || dragonfly || freebsd || netbsd || solaris) && (amd64 || arm64 || mips64x || ppc64 || ppc64le || loong64 || s390x)
|
||||
|
||||
package starlark
|
||||
|
||||
// This file defines an optimized Int implementation for 64-bit machines
|
||||
// running POSIX. It reserves a 4GB portion of the address space using
|
||||
// mmap and represents int32 values as addresses within that range. This
|
||||
// disambiguates int32 values from *big.Int pointers, letting all Int
|
||||
// values be represented as an unsafe.Pointer, so that Int-to-Value
|
||||
// interface conversion need not allocate.
|
||||
|
||||
// Although iOS (which, like macOS, appears as darwin/arm64) is
|
||||
// POSIX-compliant, it limits each process to about 700MB of virtual
|
||||
// address space, which defeats the optimization. Similarly,
|
||||
// OpenBSD's default ulimit for virtual memory is a measly GB or so.
|
||||
// On both those platforms the attempted optimization will fail and
|
||||
// fall back to the slow implementation.
|
||||
|
||||
// An alternative approach to this optimization would be to embed the
|
||||
// int32 values in pointers using odd values, which can be distinguished
|
||||
// from (even) *big.Int pointers. However, the Go runtime does not allow
|
||||
// user programs to manufacture pointers to arbitrary locations such as
|
||||
// within the zero page, or non-span, non-mmap, non-stack locations,
|
||||
// and it may panic if it encounters them; see Issue #382.
|
||||
|
||||
import (
|
||||
"log"
|
||||
"math"
|
||||
"math/big"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// intImpl represents a union of (int32, *big.Int) in a single pointer,
|
||||
// so that Int-to-Value conversions need not allocate.
|
||||
//
|
||||
// The pointer is either a *big.Int, if the value is big, or a pointer into a
|
||||
// reserved portion of the address space (smallints), if the value is small
|
||||
// and the address space allocation succeeded.
|
||||
//
|
||||
// See int_generic.go for the basic representation concepts.
|
||||
type intImpl unsafe.Pointer
|
||||
|
||||
// get returns the (small, big) arms of the union.
|
||||
func (i Int) get() (int64, *big.Int) {
|
||||
if smallints == 0 {
|
||||
// optimization disabled
|
||||
if x := (*big.Int)(i.impl); isSmall(x) {
|
||||
return x.Int64(), nil
|
||||
} else {
|
||||
return 0, x
|
||||
}
|
||||
}
|
||||
|
||||
if ptr := uintptr(i.impl); ptr >= smallints && ptr < smallints+1<<32 {
|
||||
return math.MinInt32 + int64(ptr-smallints), nil
|
||||
}
|
||||
return 0, (*big.Int)(i.impl)
|
||||
}
|
||||
|
||||
// Precondition: math.MinInt32 <= x && x <= math.MaxInt32
|
||||
func makeSmallInt(x int64) Int {
|
||||
if smallints == 0 {
|
||||
// optimization disabled
|
||||
return Int{intImpl(big.NewInt(x))}
|
||||
}
|
||||
|
||||
return Int{intImpl(uintptr(x-math.MinInt32) + smallints)}
|
||||
}
|
||||
|
||||
// Precondition: x cannot be represented as int32.
|
||||
func makeBigInt(x *big.Int) Int { return Int{intImpl(x)} }
|
||||
|
||||
// smallints is the base address of a 2^32 byte memory region.
|
||||
// Pointers to addresses in this region represent int32 values.
|
||||
// We assume smallints is not at the very top of the address space.
|
||||
//
|
||||
// Zero means the optimization is disabled and all Ints allocate a big.Int.
|
||||
var smallints = reserveAddresses(1 << 32)
|
||||
|
||||
func reserveAddresses(len int) uintptr {
|
||||
b, err := unix.Mmap(-1, 0, len, unix.PROT_READ, unix.MAP_PRIVATE|unix.MAP_ANON)
|
||||
if err != nil {
|
||||
log.Printf("Starlark failed to allocate 4GB address space: %v. Integer performance may suffer.", err)
|
||||
return 0 // optimization disabled
|
||||
}
|
||||
return uintptr(unsafe.Pointer(&b[0]))
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
// Copyright 2017 The Bazel Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package starlark
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"math/big"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestIntOpts exercises integer arithmetic, especially at the boundaries.
|
||||
func TestIntOpts(t *testing.T) {
|
||||
f := MakeInt64
|
||||
left, right := big.NewInt(math.MinInt32), big.NewInt(math.MaxInt32)
|
||||
|
||||
for i, test := range []struct {
|
||||
val Int
|
||||
want string
|
||||
}{
|
||||
// Add
|
||||
{f(math.MaxInt32).Add(f(1)), "80000000"},
|
||||
{f(math.MinInt32).Add(f(-1)), "-80000001"},
|
||||
// Mul
|
||||
{f(math.MaxInt32).Mul(f(math.MaxInt32)), "3fffffff00000001"},
|
||||
{f(math.MinInt32).Mul(f(math.MinInt32)), "4000000000000000"},
|
||||
{f(math.MaxUint32).Mul(f(math.MaxUint32)), "fffffffe00000001"},
|
||||
{f(math.MinInt32).Mul(f(-1)), "80000000"},
|
||||
// Div
|
||||
{f(math.MinInt32).Div(f(-1)), "80000000"},
|
||||
{f(1 << 31).Div(f(2)), "40000000"},
|
||||
// And
|
||||
{f(math.MaxInt32).And(f(math.MaxInt32)), "7fffffff"},
|
||||
{f(math.MinInt32).And(f(math.MinInt32)), "-80000000"},
|
||||
{f(1 << 33).And(f(1 << 32)), "0"},
|
||||
// Mod
|
||||
{f(1 << 32).Mod(f(2)), "0"},
|
||||
// Or
|
||||
{f(1 << 32).Or(f(0)), "100000000"},
|
||||
{f(math.MaxInt32).Or(f(0)), "7fffffff"},
|
||||
{f(math.MaxUint32).Or(f(0)), "ffffffff"},
|
||||
{f(math.MinInt32).Or(f(math.MinInt32)), "-80000000"},
|
||||
// Xor
|
||||
{f(math.MinInt32).Xor(f(-1)), "7fffffff"},
|
||||
// Not
|
||||
{f(math.MinInt32).Not(), "7fffffff"},
|
||||
{f(math.MaxInt32).Not(), "-80000000"},
|
||||
// Shift
|
||||
{f(1).Lsh(31), "80000000"},
|
||||
{f(1).Lsh(32), "100000000"},
|
||||
{f(math.MaxInt32 + 1).Rsh(1), "40000000"},
|
||||
{f(math.MinInt32 * 2).Rsh(1), "-80000000"},
|
||||
} {
|
||||
if got := fmt.Sprintf("%x", test.val); got != test.want {
|
||||
t.Errorf("%d equals %s, want %s", i, got, test.want)
|
||||
}
|
||||
small, big := test.val.get()
|
||||
if small < math.MinInt32 || math.MaxInt32 < small {
|
||||
t.Errorf("expected big, %d %s", i, test.val)
|
||||
}
|
||||
if big == nil {
|
||||
continue
|
||||
}
|
||||
if small != 0 {
|
||||
t.Errorf("expected 0 small, %d %s with %d", i, test.val, small)
|
||||
}
|
||||
if big.Cmp(left) >= 0 && big.Cmp(right) <= 0 {
|
||||
t.Errorf("expected small, %d %s", i, test.val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestImmutabilityMakeBigInt(t *testing.T) {
|
||||
// use max int64 for the test
|
||||
expect := int64(^uint64(0) >> 1)
|
||||
|
||||
mutint := big.NewInt(expect)
|
||||
value := MakeBigInt(mutint)
|
||||
mutint.Set(big.NewInt(1))
|
||||
|
||||
got, _ := value.Int64()
|
||||
if got != expect {
|
||||
t.Errorf("expected %d, got %d", expect, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImmutabilityBigInt(t *testing.T) {
|
||||
// use 1 and max int64 for the test
|
||||
for _, expect := range []int64{1, int64(^uint64(0) >> 1)} {
|
||||
value := MakeBigInt(big.NewInt(expect))
|
||||
|
||||
bigint := value.BigInt()
|
||||
bigint.Set(big.NewInt(2))
|
||||
|
||||
got, _ := value.Int64()
|
||||
if got != expect {
|
||||
t.Errorf("expected %d, got %d", expect, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntFallback creates a small Int value in a child process with
|
||||
// limited address space to ensure that it still works, but prints a warning.
|
||||
func TestIntFallback(t *testing.T) {
|
||||
if runtime.GOOS != "linux" {
|
||||
t.Skipf("test disabled on this platform (requires ulimit -v)")
|
||||
}
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
t.Fatalf("can't find file name of executable: %v", err)
|
||||
}
|
||||
// ulimit -v limits the address space in KB. Not portable.
|
||||
// 4GB is enough for the Go runtime but not for the optimization.
|
||||
cmd := exec.Command("/bin/sh", "-c", fmt.Sprintf("ulimit -v 4000000 && %q --entry=intfallback", exe))
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("intfallback subcommand failed: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
// Check the warning was printed.
|
||||
if !strings.Contains(string(out), "Integer performance may suffer") {
|
||||
t.Errorf("expected warning was not printed. Output=<<%s>>", out)
|
||||
}
|
||||
}
|
||||
|
||||
// intfallback is called in a child process with limited address space.
|
||||
func intfallback() {
|
||||
const want = 123
|
||||
if got, _ := MakeBigInt(big.NewInt(want)).Int64(); got != want {
|
||||
log.Fatalf("intfallback: got %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// The --entry flag invokes an alternate entry point, for use in subprocess tests.
|
||||
var testEntry = flag.String("entry", "", "child process entry-point")
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// In some build systems, notably Blaze, flag.Parse is called before TestMain,
|
||||
// in violation of the TestMain contract, making this second call a no-op.
|
||||
flag.Parse()
|
||||
switch *testEntry {
|
||||
case "":
|
||||
os.Exit(m.Run()) // normal case
|
||||
case "intfallback":
|
||||
intfallback()
|
||||
default:
|
||||
log.Fatalf("unknown entry point: %s", *testEntry)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,704 @@
|
||||
package starlark
|
||||
|
||||
// This file defines the bytecode interpreter.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"unsafe"
|
||||
|
||||
"go.starlark.net/internal/compile"
|
||||
"go.starlark.net/internal/spell"
|
||||
"go.starlark.net/syntax"
|
||||
)
|
||||
|
||||
const vmdebug = false // TODO(adonovan): use a bitfield of specific kinds of error.
|
||||
|
||||
// TODO(adonovan):
|
||||
// - optimize position table.
|
||||
// - opt: record MaxIterStack during compilation and preallocate the stack.
|
||||
|
||||
func (fn *Function) CallInternal(thread *Thread, args Tuple, kwargs []Tuple) (Value, error) {
|
||||
// Postcondition: args is not mutated. This is stricter than required by Callable,
|
||||
// but allows CALL to avoid a copy.
|
||||
|
||||
f := fn.funcode
|
||||
if !f.Prog.Recursion {
|
||||
// detect recursion
|
||||
for _, fr := range thread.stack[:len(thread.stack)-1] {
|
||||
// We look for the same function code,
|
||||
// not function value, otherwise the user could
|
||||
// defeat the check by writing the Y combinator.
|
||||
if frfn, ok := fr.Callable().(*Function); ok && frfn.funcode == f {
|
||||
return nil, fmt.Errorf("function %s called recursively", fn.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fr := thread.frameAt(0)
|
||||
|
||||
// Allocate space for stack and locals.
|
||||
// Logically these do not escape from this frame
|
||||
// (See https://github.com/golang/go/issues/20533.)
|
||||
//
|
||||
// This heap allocation looks expensive, but I was unable to get
|
||||
// more than 1% real time improvement in a large alloc-heavy
|
||||
// benchmark (in which this alloc was 8% of alloc-bytes)
|
||||
// by allocating space for 8 Values in each frame, or
|
||||
// by allocating stack by slicing an array held by the Thread
|
||||
// that is expanded in chunks of min(k, nspace), for k=256 or 1024.
|
||||
nlocals := len(f.Locals)
|
||||
nspace := nlocals + f.MaxStack
|
||||
space := make([]Value, nspace)
|
||||
locals := space[:nlocals:nlocals] // local variables, starting with parameters
|
||||
stack := space[nlocals:] // operand stack
|
||||
|
||||
// Digest arguments and set parameters.
|
||||
err := setArgs(locals, fn, args, kwargs)
|
||||
if err != nil {
|
||||
return nil, thread.evalError(err)
|
||||
}
|
||||
|
||||
fr.locals = locals
|
||||
|
||||
if vmdebug {
|
||||
fmt.Printf("Entering %s @ %s\n", f.Name, f.Position(0))
|
||||
fmt.Printf("%d stack, %d locals\n", len(stack), len(locals))
|
||||
defer fmt.Println("Leaving ", f.Name)
|
||||
}
|
||||
|
||||
// Spill indicated locals to cells.
|
||||
// Each cell is a separate alloc to avoid spurious liveness.
|
||||
for _, index := range f.Cells {
|
||||
locals[index] = &cell{locals[index]}
|
||||
}
|
||||
|
||||
// TODO(adonovan): add static check that beneath this point
|
||||
// - there is exactly one return statement
|
||||
// - there is no redefinition of 'err'.
|
||||
|
||||
var iterstack []Iterator // stack of active iterators
|
||||
|
||||
// Use defer so that application panics can pass through
|
||||
// interpreter without leaving thread in a bad state.
|
||||
defer func() {
|
||||
// ITERPOP the rest of the iterator stack.
|
||||
for _, iter := range iterstack {
|
||||
iter.Done()
|
||||
}
|
||||
|
||||
fr.locals = nil
|
||||
}()
|
||||
|
||||
sp := 0
|
||||
var pc uint32
|
||||
var result Value
|
||||
code := f.Code
|
||||
loop:
|
||||
for {
|
||||
thread.Steps++
|
||||
if thread.Steps >= thread.maxSteps {
|
||||
if thread.OnMaxSteps != nil {
|
||||
thread.OnMaxSteps(thread)
|
||||
} else {
|
||||
thread.Cancel("too many steps")
|
||||
}
|
||||
}
|
||||
if reason := atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&thread.cancelReason))); reason != nil {
|
||||
err = fmt.Errorf("Starlark computation cancelled: %s", *(*string)(reason))
|
||||
break loop
|
||||
}
|
||||
|
||||
fr.pc = pc
|
||||
|
||||
op := compile.Opcode(code[pc])
|
||||
pc++
|
||||
var arg uint32
|
||||
if op >= compile.OpcodeArgMin {
|
||||
// TODO(adonovan): opt: profile this.
|
||||
// Perhaps compiling big endian would be less work to decode?
|
||||
for s := uint(0); ; s += 7 {
|
||||
b := code[pc]
|
||||
pc++
|
||||
arg |= uint32(b&0x7f) << s
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if vmdebug {
|
||||
fmt.Fprintln(os.Stderr, stack[:sp]) // very verbose!
|
||||
compile.PrintOp(f, fr.pc, op, arg)
|
||||
}
|
||||
|
||||
switch op {
|
||||
case compile.NOP:
|
||||
// nop
|
||||
|
||||
case compile.DUP:
|
||||
stack[sp] = stack[sp-1]
|
||||
sp++
|
||||
|
||||
case compile.DUP2:
|
||||
stack[sp] = stack[sp-2]
|
||||
stack[sp+1] = stack[sp-1]
|
||||
sp += 2
|
||||
|
||||
case compile.POP:
|
||||
sp--
|
||||
|
||||
case compile.EXCH:
|
||||
stack[sp-2], stack[sp-1] = stack[sp-1], stack[sp-2]
|
||||
|
||||
case compile.EQL, compile.NEQ, compile.GT, compile.LT, compile.LE, compile.GE:
|
||||
op := syntax.Token(op-compile.EQL) + syntax.EQL
|
||||
y := stack[sp-1]
|
||||
x := stack[sp-2]
|
||||
sp -= 2
|
||||
ok, err2 := Compare(op, x, y)
|
||||
if err2 != nil {
|
||||
err = err2
|
||||
break loop
|
||||
}
|
||||
stack[sp] = Bool(ok)
|
||||
sp++
|
||||
|
||||
case compile.PLUS,
|
||||
compile.MINUS,
|
||||
compile.STAR,
|
||||
compile.SLASH,
|
||||
compile.SLASHSLASH,
|
||||
compile.PERCENT,
|
||||
compile.AMP,
|
||||
compile.PIPE,
|
||||
compile.CIRCUMFLEX,
|
||||
compile.LTLT,
|
||||
compile.GTGT,
|
||||
compile.IN:
|
||||
binop := syntax.Token(op-compile.PLUS) + syntax.PLUS
|
||||
if op == compile.IN {
|
||||
binop = syntax.IN // IN token is out of order
|
||||
}
|
||||
y := stack[sp-1]
|
||||
x := stack[sp-2]
|
||||
sp -= 2
|
||||
z, err2 := Binary(binop, x, y)
|
||||
if err2 != nil {
|
||||
err = err2
|
||||
break loop
|
||||
}
|
||||
stack[sp] = z
|
||||
sp++
|
||||
|
||||
case compile.UPLUS, compile.UMINUS, compile.TILDE:
|
||||
var unop syntax.Token
|
||||
if op == compile.TILDE {
|
||||
unop = syntax.TILDE
|
||||
} else {
|
||||
unop = syntax.Token(op-compile.UPLUS) + syntax.PLUS
|
||||
}
|
||||
x := stack[sp-1]
|
||||
y, err2 := Unary(unop, x)
|
||||
if err2 != nil {
|
||||
err = err2
|
||||
break loop
|
||||
}
|
||||
stack[sp-1] = y
|
||||
|
||||
case compile.INPLACE_ADD:
|
||||
y := stack[sp-1]
|
||||
x := stack[sp-2]
|
||||
sp -= 2
|
||||
|
||||
// It's possible that y is not Iterable but
|
||||
// nonetheless defines x+y, in which case we
|
||||
// should fall back to the general case.
|
||||
var z Value
|
||||
if xlist, ok := x.(*List); ok {
|
||||
if yiter, ok := y.(Iterable); ok {
|
||||
if err = xlist.checkMutable("apply += to"); err != nil {
|
||||
break loop
|
||||
}
|
||||
listExtend(xlist, yiter)
|
||||
z = xlist
|
||||
}
|
||||
}
|
||||
if z == nil {
|
||||
z, err = Binary(syntax.PLUS, x, y)
|
||||
if err != nil {
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
stack[sp] = z
|
||||
sp++
|
||||
|
||||
case compile.INPLACE_PIPE:
|
||||
y := stack[sp-1]
|
||||
x := stack[sp-2]
|
||||
sp -= 2
|
||||
|
||||
// It's possible that y is not Dict but
|
||||
// nonetheless defines x|y, in which case we
|
||||
// should fall back to the general case.
|
||||
var z Value
|
||||
if xdict, ok := x.(*Dict); ok {
|
||||
if ydict, ok := y.(*Dict); ok {
|
||||
if err = xdict.ht.checkMutable("apply |= to"); err != nil {
|
||||
break loop
|
||||
}
|
||||
xdict.ht.addAll(&ydict.ht) // can't fail
|
||||
z = xdict
|
||||
}
|
||||
}
|
||||
if z == nil {
|
||||
z, err = Binary(syntax.PIPE, x, y)
|
||||
if err != nil {
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
stack[sp] = z
|
||||
sp++
|
||||
|
||||
case compile.NONE:
|
||||
stack[sp] = None
|
||||
sp++
|
||||
|
||||
case compile.TRUE:
|
||||
stack[sp] = True
|
||||
sp++
|
||||
|
||||
case compile.FALSE:
|
||||
stack[sp] = False
|
||||
sp++
|
||||
|
||||
case compile.MANDATORY:
|
||||
stack[sp] = mandatory{}
|
||||
sp++
|
||||
|
||||
case compile.JMP:
|
||||
pc = arg
|
||||
|
||||
case compile.CALL, compile.CALL_VAR, compile.CALL_KW, compile.CALL_VAR_KW:
|
||||
var kwargs Value
|
||||
if op == compile.CALL_KW || op == compile.CALL_VAR_KW {
|
||||
kwargs = stack[sp-1]
|
||||
sp--
|
||||
}
|
||||
|
||||
var args Value
|
||||
if op == compile.CALL_VAR || op == compile.CALL_VAR_KW {
|
||||
args = stack[sp-1]
|
||||
sp--
|
||||
}
|
||||
|
||||
// named args (pairs)
|
||||
var kvpairs []Tuple
|
||||
if nkvpairs := int(arg & 0xff); nkvpairs > 0 {
|
||||
kvpairs = make([]Tuple, 0, nkvpairs)
|
||||
kvpairsAlloc := make(Tuple, 2*nkvpairs) // allocate a single backing array
|
||||
sp -= 2 * nkvpairs
|
||||
for i := 0; i < nkvpairs; i++ {
|
||||
pair := kvpairsAlloc[:2:2]
|
||||
kvpairsAlloc = kvpairsAlloc[2:]
|
||||
pair[0] = stack[sp+2*i] // name
|
||||
pair[1] = stack[sp+2*i+1] // value
|
||||
kvpairs = append(kvpairs, pair)
|
||||
}
|
||||
}
|
||||
if kwargs != nil {
|
||||
// Add key/value items from **kwargs dictionary.
|
||||
dict, ok := kwargs.(IterableMapping)
|
||||
if !ok {
|
||||
err = fmt.Errorf("argument after ** must be a mapping, not %s", kwargs.Type())
|
||||
break loop
|
||||
}
|
||||
items := dict.Items()
|
||||
for _, item := range items {
|
||||
if _, ok := item[0].(String); !ok {
|
||||
err = fmt.Errorf("keywords must be strings, not %s", item[0].Type())
|
||||
break loop
|
||||
}
|
||||
}
|
||||
if len(kvpairs) == 0 {
|
||||
kvpairs = items
|
||||
} else {
|
||||
kvpairs = append(kvpairs, items...)
|
||||
}
|
||||
}
|
||||
|
||||
// positional args
|
||||
var positional Tuple
|
||||
if npos := int(arg >> 8); npos > 0 {
|
||||
positional = stack[sp-npos : sp]
|
||||
sp -= npos
|
||||
|
||||
// Copy positional arguments into a new array,
|
||||
// unless the callee is another Starlark function,
|
||||
// in which case it can be trusted not to mutate them.
|
||||
if _, ok := stack[sp-1].(*Function); !ok || args != nil {
|
||||
positional = append(Tuple(nil), positional...)
|
||||
}
|
||||
}
|
||||
if args != nil {
|
||||
// Add elements from *args sequence.
|
||||
iter := Iterate(args)
|
||||
if iter == nil {
|
||||
err = fmt.Errorf("argument after * must be iterable, not %s", args.Type())
|
||||
break loop
|
||||
}
|
||||
var elem Value
|
||||
for iter.Next(&elem) {
|
||||
positional = append(positional, elem)
|
||||
}
|
||||
iter.Done()
|
||||
}
|
||||
|
||||
function := stack[sp-1]
|
||||
|
||||
if vmdebug {
|
||||
fmt.Printf("VM call %s args=%s kwargs=%s @%s\n",
|
||||
function, positional, kvpairs, f.Position(fr.pc))
|
||||
}
|
||||
|
||||
thread.endProfSpan()
|
||||
z, err2 := Call(thread, function, positional, kvpairs)
|
||||
thread.beginProfSpan()
|
||||
if err2 != nil {
|
||||
err = err2
|
||||
break loop
|
||||
}
|
||||
if vmdebug {
|
||||
fmt.Printf("Resuming %s @ %s\n", f.Name, f.Position(0))
|
||||
}
|
||||
stack[sp-1] = z
|
||||
|
||||
case compile.ITERPUSH:
|
||||
x := stack[sp-1]
|
||||
sp--
|
||||
iter := Iterate(x)
|
||||
if iter == nil {
|
||||
err = fmt.Errorf("%s value is not iterable", x.Type())
|
||||
break loop
|
||||
}
|
||||
iterstack = append(iterstack, iter)
|
||||
|
||||
case compile.ITERJMP:
|
||||
iter := iterstack[len(iterstack)-1]
|
||||
if iter.Next(&stack[sp]) {
|
||||
sp++
|
||||
} else {
|
||||
pc = arg
|
||||
}
|
||||
|
||||
case compile.ITERPOP:
|
||||
n := len(iterstack) - 1
|
||||
iterstack[n].Done()
|
||||
iterstack = iterstack[:n]
|
||||
|
||||
case compile.NOT:
|
||||
stack[sp-1] = !stack[sp-1].Truth()
|
||||
|
||||
case compile.RETURN:
|
||||
result = stack[sp-1]
|
||||
break loop
|
||||
|
||||
case compile.SETINDEX:
|
||||
z := stack[sp-1]
|
||||
y := stack[sp-2]
|
||||
x := stack[sp-3]
|
||||
sp -= 3
|
||||
err = setIndex(x, y, z)
|
||||
if err != nil {
|
||||
break loop
|
||||
}
|
||||
|
||||
case compile.INDEX:
|
||||
y := stack[sp-1]
|
||||
x := stack[sp-2]
|
||||
sp -= 2
|
||||
z, err2 := getIndex(x, y)
|
||||
if err2 != nil {
|
||||
err = err2
|
||||
break loop
|
||||
}
|
||||
stack[sp] = z
|
||||
sp++
|
||||
|
||||
case compile.ATTR:
|
||||
x := stack[sp-1]
|
||||
name := f.Prog.Names[arg]
|
||||
y, err2 := getAttr(x, name)
|
||||
if err2 != nil {
|
||||
err = err2
|
||||
break loop
|
||||
}
|
||||
stack[sp-1] = y
|
||||
|
||||
case compile.SETFIELD:
|
||||
y := stack[sp-1]
|
||||
x := stack[sp-2]
|
||||
sp -= 2
|
||||
name := f.Prog.Names[arg]
|
||||
if err2 := setField(x, name, y); err2 != nil {
|
||||
err = err2
|
||||
break loop
|
||||
}
|
||||
|
||||
case compile.MAKEDICT:
|
||||
stack[sp] = new(Dict)
|
||||
sp++
|
||||
|
||||
case compile.SETDICT, compile.SETDICTUNIQ:
|
||||
dict := stack[sp-3].(*Dict)
|
||||
k := stack[sp-2]
|
||||
v := stack[sp-1]
|
||||
sp -= 3
|
||||
oldlen := dict.Len()
|
||||
if err2 := dict.SetKey(k, v); err2 != nil {
|
||||
err = err2
|
||||
break loop
|
||||
}
|
||||
if op == compile.SETDICTUNIQ && dict.Len() == oldlen {
|
||||
err = fmt.Errorf("duplicate key: %v", k)
|
||||
break loop
|
||||
}
|
||||
|
||||
case compile.APPEND:
|
||||
elem := stack[sp-1]
|
||||
list := stack[sp-2].(*List)
|
||||
sp -= 2
|
||||
list.elems = append(list.elems, elem)
|
||||
|
||||
case compile.SLICE:
|
||||
x := stack[sp-4]
|
||||
lo := stack[sp-3]
|
||||
hi := stack[sp-2]
|
||||
step := stack[sp-1]
|
||||
sp -= 4
|
||||
res, err2 := slice(x, lo, hi, step)
|
||||
if err2 != nil {
|
||||
err = err2
|
||||
break loop
|
||||
}
|
||||
stack[sp] = res
|
||||
sp++
|
||||
|
||||
case compile.UNPACK:
|
||||
n := int(arg)
|
||||
iterable := stack[sp-1]
|
||||
sp--
|
||||
iter := Iterate(iterable)
|
||||
if iter == nil {
|
||||
err = fmt.Errorf("got %s in sequence assignment", iterable.Type())
|
||||
break loop
|
||||
}
|
||||
i := 0
|
||||
sp += n
|
||||
for i < n && iter.Next(&stack[sp-1-i]) {
|
||||
i++
|
||||
}
|
||||
var dummy Value
|
||||
if iter.Next(&dummy) {
|
||||
// NB: Len may return -1 here in obscure cases.
|
||||
err = fmt.Errorf("too many values to unpack (got %d, want %d)", Len(iterable), n)
|
||||
break loop
|
||||
}
|
||||
iter.Done()
|
||||
if i < n {
|
||||
err = fmt.Errorf("too few values to unpack (got %d, want %d)", i, n)
|
||||
break loop
|
||||
}
|
||||
|
||||
case compile.CJMP:
|
||||
if stack[sp-1].Truth() {
|
||||
pc = arg
|
||||
}
|
||||
sp--
|
||||
|
||||
case compile.CONSTANT:
|
||||
stack[sp] = fn.module.constants[arg]
|
||||
sp++
|
||||
|
||||
case compile.MAKETUPLE:
|
||||
n := int(arg)
|
||||
tuple := make(Tuple, n)
|
||||
sp -= n
|
||||
copy(tuple, stack[sp:])
|
||||
stack[sp] = tuple
|
||||
sp++
|
||||
|
||||
case compile.MAKELIST:
|
||||
n := int(arg)
|
||||
elems := make([]Value, n)
|
||||
sp -= n
|
||||
copy(elems, stack[sp:])
|
||||
stack[sp] = NewList(elems)
|
||||
sp++
|
||||
|
||||
case compile.MAKEFUNC:
|
||||
funcode := f.Prog.Functions[arg]
|
||||
tuple := stack[sp-1].(Tuple)
|
||||
n := len(tuple) - len(funcode.Freevars)
|
||||
defaults := tuple[:n:n]
|
||||
freevars := tuple[n:]
|
||||
stack[sp-1] = &Function{
|
||||
funcode: funcode,
|
||||
module: fn.module,
|
||||
defaults: defaults,
|
||||
freevars: freevars,
|
||||
}
|
||||
|
||||
case compile.LOAD:
|
||||
n := int(arg)
|
||||
module := string(stack[sp-1].(String))
|
||||
sp--
|
||||
|
||||
if thread.Load == nil {
|
||||
err = fmt.Errorf("load not implemented by this application")
|
||||
break loop
|
||||
}
|
||||
|
||||
thread.endProfSpan()
|
||||
dict, err2 := thread.Load(thread, module)
|
||||
thread.beginProfSpan()
|
||||
if err2 != nil {
|
||||
err = wrappedError{
|
||||
msg: fmt.Sprintf("cannot load %s: %v", module, err2),
|
||||
cause: err2,
|
||||
}
|
||||
break loop
|
||||
}
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
from := string(stack[sp-1-i].(String))
|
||||
v, ok := dict[from]
|
||||
if !ok {
|
||||
err = fmt.Errorf("load: name %s not found in module %s", from, module)
|
||||
if n := spell.Nearest(from, dict.Keys()); n != "" {
|
||||
err = fmt.Errorf("%s (did you mean %s?)", err, n)
|
||||
}
|
||||
break loop
|
||||
}
|
||||
stack[sp-1-i] = v
|
||||
}
|
||||
|
||||
case compile.SETLOCAL:
|
||||
locals[arg] = stack[sp-1]
|
||||
sp--
|
||||
|
||||
case compile.SETLOCALCELL:
|
||||
locals[arg].(*cell).v = stack[sp-1]
|
||||
sp--
|
||||
|
||||
case compile.SETGLOBAL:
|
||||
fn.module.globals[arg] = stack[sp-1]
|
||||
sp--
|
||||
|
||||
case compile.LOCAL:
|
||||
x := locals[arg]
|
||||
if x == nil {
|
||||
err = fmt.Errorf("local variable %s referenced before assignment", f.Locals[arg].Name)
|
||||
break loop
|
||||
}
|
||||
stack[sp] = x
|
||||
sp++
|
||||
|
||||
case compile.FREE:
|
||||
stack[sp] = fn.freevars[arg]
|
||||
sp++
|
||||
|
||||
case compile.LOCALCELL:
|
||||
v := locals[arg].(*cell).v
|
||||
if v == nil {
|
||||
err = fmt.Errorf("local variable %s referenced before assignment", f.Locals[arg].Name)
|
||||
break loop
|
||||
}
|
||||
stack[sp] = v
|
||||
sp++
|
||||
|
||||
case compile.FREECELL:
|
||||
v := fn.freevars[arg].(*cell).v
|
||||
if v == nil {
|
||||
err = fmt.Errorf("local variable %s referenced before assignment", f.Freevars[arg].Name)
|
||||
break loop
|
||||
}
|
||||
stack[sp] = v
|
||||
sp++
|
||||
|
||||
case compile.GLOBAL:
|
||||
x := fn.module.globals[arg]
|
||||
if x == nil {
|
||||
err = fmt.Errorf("global variable %s referenced before assignment", f.Prog.Globals[arg].Name)
|
||||
break loop
|
||||
}
|
||||
stack[sp] = x
|
||||
sp++
|
||||
|
||||
case compile.PREDECLARED:
|
||||
name := f.Prog.Names[arg]
|
||||
x := fn.module.predeclared[name]
|
||||
if x == nil {
|
||||
err = fmt.Errorf("internal error: predeclared variable %s is uninitialized", name)
|
||||
break loop
|
||||
}
|
||||
stack[sp] = x
|
||||
sp++
|
||||
|
||||
case compile.UNIVERSAL:
|
||||
stack[sp] = Universe[f.Prog.Names[arg]]
|
||||
sp++
|
||||
|
||||
default:
|
||||
err = fmt.Errorf("unimplemented: %s", op)
|
||||
break loop
|
||||
}
|
||||
}
|
||||
// (deferred cleanup runs here)
|
||||
return result, err
|
||||
}
|
||||
|
||||
type wrappedError struct {
|
||||
msg string
|
||||
cause error
|
||||
}
|
||||
|
||||
func (e wrappedError) Error() string {
|
||||
return e.msg
|
||||
}
|
||||
|
||||
// Implements the xerrors.Wrapper interface
|
||||
// https://godoc.org/golang.org/x/xerrors#Wrapper
|
||||
func (e wrappedError) Unwrap() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
// mandatory is a sentinel value used in a function's defaults tuple
|
||||
// to indicate that a (keyword-only) parameter is mandatory.
|
||||
type mandatory struct{}
|
||||
|
||||
func (mandatory) String() string { return "mandatory" }
|
||||
func (mandatory) Type() string { return "mandatory" }
|
||||
func (mandatory) Freeze() {} // immutable
|
||||
func (mandatory) Truth() Bool { return False }
|
||||
func (mandatory) Hash() (uint32, error) { return 0, nil }
|
||||
|
||||
// A cell is a box containing a Value.
|
||||
// Local variables marked as cells hold their value indirectly
|
||||
// so that they may be shared by outer and inner nested functions.
|
||||
// Cells are always accessed using indirect {FREE,LOCAL,SETLOCAL}CELL instructions.
|
||||
// The FreeVars tuple contains only cells.
|
||||
// The FREE instruction always yields a cell.
|
||||
type cell struct{ v Value }
|
||||
|
||||
func (c *cell) String() string { return "cell" }
|
||||
func (c *cell) Type() string { return "cell" }
|
||||
func (c *cell) Freeze() {
|
||||
if c.v != nil {
|
||||
c.v.Freeze()
|
||||
}
|
||||
}
|
||||
func (c *cell) Truth() Bool { panic("unreachable") }
|
||||
func (c *cell) Hash() (uint32, error) { panic("unreachable") }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,449 @@
|
||||
// Copyright 2019 The Bazel Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package starlark
|
||||
|
||||
// This file defines a simple execution-time profiler for Starlark.
|
||||
// It measures the wall time spent executing Starlark code, and emits a
|
||||
// gzipped protocol message in pprof format (github.com/google/pprof).
|
||||
//
|
||||
// When profiling is enabled, the interpreter calls the profiler to
|
||||
// indicate the start and end of each "span" or time interval. A leaf
|
||||
// function (whether Go or Starlark) has a single span. A function that
|
||||
// calls another function has spans for each interval in which it is the
|
||||
// top of the stack. (A LOAD instruction also ends a span.)
|
||||
//
|
||||
// At the start of a span, the interpreter records the current time in
|
||||
// the thread's topmost frame. At the end of the span, it obtains the
|
||||
// time again and subtracts the span start time. The difference is added
|
||||
// to an accumulator variable in the thread. If the accumulator exceeds
|
||||
// some fixed quantum (10ms, say), the profiler records the current call
|
||||
// stack and sends it to the profiler goroutine, along with the number
|
||||
// of quanta, which are subtracted. For example, if the accumulator
|
||||
// holds 3ms and then a completed span adds 25ms to it, its value is 28ms,
|
||||
// which exceeeds 10ms. The profiler records a stack with the value 20ms
|
||||
// (2 quanta), and the accumulator is left with 8ms.
|
||||
//
|
||||
// The profiler goroutine converts the stacks into the pprof format and
|
||||
// emits a gzip-compressed protocol message to the designated output
|
||||
// file. We use a hand-written streaming proto encoder to avoid
|
||||
// dependencies on pprof and proto, and to avoid the need to
|
||||
// materialize the profile data structure in memory.
|
||||
//
|
||||
// A limitation of this profiler is that it measures wall time, which
|
||||
// does not necessarily correspond to CPU time. A CPU profiler requires
|
||||
// that only running (not runnable) threads are sampled; this is
|
||||
// commonly achieved by having the kernel deliver a (PROF) signal to an
|
||||
// arbitrary running thread, through setitimer(2). The CPU profiler in the
|
||||
// Go runtime uses this mechanism, but it is not possible for a Go
|
||||
// application to register a SIGPROF handler, nor is it possible for a
|
||||
// Go handler for some other signal to read the stack pointer of
|
||||
// the interrupted thread.
|
||||
//
|
||||
// Two caveats:
|
||||
// (1) it is tempting to send the leaf Frame directly to the profiler
|
||||
// goroutine instead of making a copy of the stack, since a Frame is a
|
||||
// spaghetti stack--a linked list. However, as soon as execution
|
||||
// resumes, the stack's Frame.pc values may be mutated, so Frames are
|
||||
// not safe to share with the asynchronous profiler goroutine.
|
||||
// (2) it is tempting to use Callables as keys in a map when tabulating
|
||||
// the pprof protocols's Function entities. However, we cannot assume
|
||||
// that Callables are valid map keys, and furthermore we must not
|
||||
// pin function values in memory indefinitely as this may cause lambda
|
||||
// values to keep their free variables live much longer than necessary.
|
||||
|
||||
// TODO(adonovan):
|
||||
// - make Start/Stop fully thread-safe.
|
||||
// - fix the pc hack.
|
||||
// - experiment with other values of quantum.
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"reflect"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"go.starlark.net/syntax"
|
||||
)
|
||||
|
||||
// StartProfile enables time profiling of all Starlark threads,
|
||||
// and writes a profile in pprof format to w.
|
||||
// It must be followed by a call to StopProfiler to stop
|
||||
// the profiler and finalize the profile.
|
||||
//
|
||||
// StartProfile returns an error if profiling was already enabled.
|
||||
//
|
||||
// StartProfile must not be called concurrently with Starlark execution.
|
||||
func StartProfile(w io.Writer) error {
|
||||
if !atomic.CompareAndSwapUint32(&profiler.on, 0, 1) {
|
||||
return fmt.Errorf("profiler already running")
|
||||
}
|
||||
|
||||
// TODO(adonovan): make the API fully concurrency-safe.
|
||||
// The main challenge is racy reads/writes of profiler.events,
|
||||
// and of send/close races on the channel it refers to.
|
||||
// It's easy to solve them with a mutex but harder to do
|
||||
// it efficiently.
|
||||
|
||||
profiler.events = make(chan *profEvent, 1)
|
||||
profiler.done = make(chan error)
|
||||
|
||||
go profile(w)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopProfile stops the profiler started by a prior call to
|
||||
// StartProfile and finalizes the profile. It returns an error if the
|
||||
// profile could not be completed.
|
||||
//
|
||||
// StopProfile must not be called concurrently with Starlark execution.
|
||||
func StopProfile() error {
|
||||
// Terminate the profiler goroutine and get its result.
|
||||
close(profiler.events)
|
||||
err := <-profiler.done
|
||||
|
||||
profiler.done = nil
|
||||
profiler.events = nil
|
||||
atomic.StoreUint32(&profiler.on, 0)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// globals
|
||||
var profiler struct {
|
||||
on uint32 // nonzero => profiler running
|
||||
events chan *profEvent // profile events from interpreter threads
|
||||
done chan error // indicates profiler goroutine is ready
|
||||
}
|
||||
|
||||
func (thread *Thread) beginProfSpan() {
|
||||
if profiler.events == nil {
|
||||
return // profiling not enabled
|
||||
}
|
||||
|
||||
thread.frameAt(0).spanStart = nanotime()
|
||||
}
|
||||
|
||||
// TODO(adonovan): experiment with smaller values,
|
||||
// which trade space and time for greater precision.
|
||||
const quantum = 10 * time.Millisecond
|
||||
|
||||
func (thread *Thread) endProfSpan() {
|
||||
if profiler.events == nil {
|
||||
return // profiling not enabled
|
||||
}
|
||||
|
||||
// Add the span to the thread's accumulator.
|
||||
thread.proftime += time.Duration(nanotime() - thread.frameAt(0).spanStart)
|
||||
if thread.proftime < quantum {
|
||||
return
|
||||
}
|
||||
|
||||
// Only record complete quanta.
|
||||
n := thread.proftime / quantum
|
||||
thread.proftime -= n * quantum
|
||||
|
||||
// Copy the stack.
|
||||
// (We can't save thread.frame because its pc will change.)
|
||||
ev := &profEvent{
|
||||
thread: thread,
|
||||
time: n * quantum,
|
||||
}
|
||||
ev.stack = ev.stackSpace[:0]
|
||||
for i := range thread.stack {
|
||||
fr := thread.frameAt(i)
|
||||
ev.stack = append(ev.stack, profFrame{
|
||||
pos: fr.Position(),
|
||||
fn: fr.Callable(),
|
||||
pc: fr.pc,
|
||||
})
|
||||
}
|
||||
|
||||
profiler.events <- ev
|
||||
}
|
||||
|
||||
type profEvent struct {
|
||||
thread *Thread // currently unused
|
||||
time time.Duration
|
||||
stack []profFrame
|
||||
stackSpace [8]profFrame // initial space for stack
|
||||
}
|
||||
|
||||
type profFrame struct {
|
||||
fn Callable // don't hold this live for too long (prevents GC of lambdas)
|
||||
pc uint32 // program counter (Starlark frames only)
|
||||
pos syntax.Position // position of pc within this frame
|
||||
}
|
||||
|
||||
// profile is the profiler goroutine.
|
||||
// It runs until StopProfiler is called.
|
||||
func profile(w io.Writer) {
|
||||
// Field numbers from pprof protocol.
|
||||
// See https://github.com/google/pprof/blob/master/proto/profile.proto
|
||||
const (
|
||||
Profile_sample_type = 1 // repeated ValueType
|
||||
Profile_sample = 2 // repeated Sample
|
||||
Profile_mapping = 3 // repeated Mapping
|
||||
Profile_location = 4 // repeated Location
|
||||
Profile_function = 5 // repeated Function
|
||||
Profile_string_table = 6 // repeated string
|
||||
Profile_time_nanos = 9 // int64
|
||||
Profile_duration_nanos = 10 // int64
|
||||
Profile_period_type = 11 // ValueType
|
||||
Profile_period = 12 // int64
|
||||
|
||||
ValueType_type = 1 // int64
|
||||
ValueType_unit = 2 // int64
|
||||
|
||||
Sample_location_id = 1 // repeated uint64
|
||||
Sample_value = 2 // repeated int64
|
||||
Sample_label = 3 // repeated Label
|
||||
|
||||
Label_key = 1 // int64
|
||||
Label_str = 2 // int64
|
||||
Label_num = 3 // int64
|
||||
Label_num_unit = 4 // int64
|
||||
|
||||
Location_id = 1 // uint64
|
||||
Location_mapping_id = 2 // uint64
|
||||
Location_address = 3 // uint64
|
||||
Location_line = 4 // repeated Line
|
||||
|
||||
Line_function_id = 1 // uint64
|
||||
Line_line = 2 // int64
|
||||
|
||||
Function_id = 1 // uint64
|
||||
Function_name = 2 // int64
|
||||
Function_system_name = 3 // int64
|
||||
Function_filename = 4 // int64
|
||||
Function_start_line = 5 // int64
|
||||
)
|
||||
|
||||
bufw := bufio.NewWriter(w) // write file in 4KB (not 240B flate-sized) chunks
|
||||
gz := gzip.NewWriter(bufw)
|
||||
enc := protoEncoder{w: gz}
|
||||
|
||||
// strings
|
||||
stringIndex := make(map[string]int64)
|
||||
str := func(s string) int64 {
|
||||
i, ok := stringIndex[s]
|
||||
if !ok {
|
||||
i = int64(len(stringIndex))
|
||||
enc.string(Profile_string_table, s)
|
||||
stringIndex[s] = i
|
||||
}
|
||||
return i
|
||||
}
|
||||
str("") // entry 0
|
||||
|
||||
// functions
|
||||
//
|
||||
// function returns the ID of a Callable for use in Line.FunctionId.
|
||||
// The ID is the same as the function's logical address,
|
||||
// which is supplied by the caller to avoid the need to recompute it.
|
||||
functionId := make(map[uintptr]uint64)
|
||||
function := func(fn Callable, addr uintptr) uint64 {
|
||||
id, ok := functionId[addr]
|
||||
if !ok {
|
||||
id = uint64(addr)
|
||||
|
||||
var pos syntax.Position
|
||||
if fn, ok := fn.(callableWithPosition); ok {
|
||||
pos = fn.Position()
|
||||
}
|
||||
|
||||
name := fn.Name()
|
||||
if name == "<toplevel>" {
|
||||
name = pos.Filename()
|
||||
}
|
||||
|
||||
nameIndex := str(name)
|
||||
|
||||
fun := new(bytes.Buffer)
|
||||
funenc := protoEncoder{w: fun}
|
||||
funenc.uint(Function_id, id)
|
||||
funenc.int(Function_name, nameIndex)
|
||||
funenc.int(Function_system_name, nameIndex)
|
||||
funenc.int(Function_filename, str(pos.Filename()))
|
||||
funenc.int(Function_start_line, int64(pos.Line))
|
||||
enc.bytes(Profile_function, fun.Bytes())
|
||||
|
||||
functionId[addr] = id
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// locations
|
||||
//
|
||||
// location returns the ID of the location denoted by fr.
|
||||
// For Starlark frames, this is the Frame pc.
|
||||
locationId := make(map[uintptr]uint64)
|
||||
location := func(fr profFrame) uint64 {
|
||||
fnAddr := profFuncAddr(fr.fn)
|
||||
|
||||
// For Starlark functions, the frame position
|
||||
// represents the current PC value.
|
||||
// Mix it into the low bits of the address.
|
||||
// This is super hacky and may result in collisions
|
||||
// in large functions or if functions are numerous.
|
||||
// TODO(adonovan): fix: try making this cleaner by treating
|
||||
// each bytecode segment as a Profile.Mapping.
|
||||
pcAddr := fnAddr
|
||||
if _, ok := fr.fn.(*Function); ok {
|
||||
pcAddr = (pcAddr << 16) ^ uintptr(fr.pc)
|
||||
}
|
||||
|
||||
id, ok := locationId[pcAddr]
|
||||
if !ok {
|
||||
id = uint64(pcAddr)
|
||||
|
||||
line := new(bytes.Buffer)
|
||||
lineenc := protoEncoder{w: line}
|
||||
lineenc.uint(Line_function_id, function(fr.fn, fnAddr))
|
||||
lineenc.int(Line_line, int64(fr.pos.Line))
|
||||
loc := new(bytes.Buffer)
|
||||
locenc := protoEncoder{w: loc}
|
||||
locenc.uint(Location_id, id)
|
||||
locenc.uint(Location_address, uint64(pcAddr))
|
||||
locenc.bytes(Location_line, line.Bytes())
|
||||
enc.bytes(Profile_location, loc.Bytes())
|
||||
|
||||
locationId[pcAddr] = id
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
wallNanos := new(bytes.Buffer)
|
||||
wnenc := protoEncoder{w: wallNanos}
|
||||
wnenc.int(ValueType_type, str("wall"))
|
||||
wnenc.int(ValueType_unit, str("nanoseconds"))
|
||||
|
||||
// informational fields of Profile
|
||||
enc.bytes(Profile_sample_type, wallNanos.Bytes())
|
||||
enc.int(Profile_period, quantum.Nanoseconds()) // magnitude of sampling period
|
||||
enc.bytes(Profile_period_type, wallNanos.Bytes()) // dimension and unit of period
|
||||
enc.int(Profile_time_nanos, time.Now().UnixNano()) // start (real) time of profile
|
||||
|
||||
startNano := nanotime()
|
||||
|
||||
// Read profile events from the channel
|
||||
// until it is closed by StopProfiler.
|
||||
for e := range profiler.events {
|
||||
sample := new(bytes.Buffer)
|
||||
sampleenc := protoEncoder{w: sample}
|
||||
sampleenc.int(Sample_value, e.time.Nanoseconds()) // wall nanoseconds
|
||||
for _, fr := range e.stack {
|
||||
sampleenc.uint(Sample_location_id, location(fr))
|
||||
}
|
||||
enc.bytes(Profile_sample, sample.Bytes())
|
||||
}
|
||||
|
||||
endNano := nanotime()
|
||||
enc.int(Profile_duration_nanos, endNano-startNano)
|
||||
|
||||
err := gz.Close() // Close reports any prior write error
|
||||
if flushErr := bufw.Flush(); err == nil {
|
||||
err = flushErr
|
||||
}
|
||||
profiler.done <- err
|
||||
}
|
||||
|
||||
// nanotime returns the time in nanoseconds since epoch.
|
||||
// It is implemented by runtime.nanotime using the linkname hack;
|
||||
// runtime.nanotime is defined for all OSs/ARCHS and uses the
|
||||
// monotonic system clock, which there is no portable way to access.
|
||||
// Should that function ever go away, these alternatives exist:
|
||||
//
|
||||
// // POSIX only. REALTIME not MONOTONIC. 17ns.
|
||||
// var tv syscall.Timeval
|
||||
// syscall.Gettimeofday(&tv) // can't fail
|
||||
// return tv.Nano()
|
||||
//
|
||||
// // Portable. REALTIME not MONOTONIC. 46ns.
|
||||
// return time.Now().Nanoseconds()
|
||||
//
|
||||
// // POSIX only. Adds a dependency.
|
||||
// import "golang.org/x/sys/unix"
|
||||
// var ts unix.Timespec
|
||||
// unix.ClockGettime(CLOCK_MONOTONIC, &ts) // can't fail
|
||||
// return unix.TimespecToNsec(ts)
|
||||
//
|
||||
//go:linkname nanotime runtime.nanotime
|
||||
func nanotime() int64
|
||||
|
||||
// profFuncAddr returns the canonical "address"
|
||||
// of a Callable for use by the profiler.
|
||||
func profFuncAddr(fn Callable) uintptr {
|
||||
switch fn := fn.(type) {
|
||||
case *Builtin:
|
||||
return reflect.ValueOf(fn.fn).Pointer()
|
||||
case *Function:
|
||||
return uintptr(unsafe.Pointer(fn.funcode))
|
||||
}
|
||||
|
||||
// User-defined callable types are typically of
|
||||
// of kind pointer-to-struct. Handle them specially.
|
||||
if v := reflect.ValueOf(fn); v.Type().Kind() == reflect.Ptr {
|
||||
return v.Pointer()
|
||||
}
|
||||
|
||||
// Address zero is reserved by the protocol.
|
||||
// Use 1 for callables we don't recognize.
|
||||
log.Printf("Starlark profiler: no address for Callable %T", fn)
|
||||
return 1
|
||||
}
|
||||
|
||||
// We encode the protocol message by hand to avoid making
|
||||
// the interpreter depend on both github.com/google/pprof
|
||||
// and github.com/golang/protobuf.
|
||||
//
|
||||
// This also avoids the need to materialize a protocol message object
|
||||
// tree of unbounded size and serialize it all at the end.
|
||||
// The pprof format appears to have been designed to
|
||||
// permit streaming implementations such as this one.
|
||||
//
|
||||
// See https://developers.google.com/protocol-buffers/docs/encoding.
|
||||
type protoEncoder struct {
|
||||
w io.Writer // *bytes.Buffer or *gzip.Writer
|
||||
tmp [binary.MaxVarintLen64]byte
|
||||
}
|
||||
|
||||
func (e *protoEncoder) uvarint(x uint64) {
|
||||
n := binary.PutUvarint(e.tmp[:], x)
|
||||
e.w.Write(e.tmp[:n])
|
||||
}
|
||||
|
||||
func (e *protoEncoder) tag(field, wire uint) {
|
||||
e.uvarint(uint64(field<<3 | wire))
|
||||
}
|
||||
|
||||
func (e *protoEncoder) string(field uint, s string) {
|
||||
e.tag(field, 2) // length-delimited
|
||||
e.uvarint(uint64(len(s)))
|
||||
io.WriteString(e.w, s)
|
||||
}
|
||||
|
||||
func (e *protoEncoder) bytes(field uint, b []byte) {
|
||||
e.tag(field, 2) // length-delimited
|
||||
e.uvarint(uint64(len(b)))
|
||||
e.w.Write(b)
|
||||
}
|
||||
|
||||
func (e *protoEncoder) uint(field uint, x uint64) {
|
||||
e.tag(field, 0) // varint
|
||||
e.uvarint(x)
|
||||
}
|
||||
|
||||
func (e *protoEncoder) int(field uint, x int64) {
|
||||
e.tag(field, 0) // varint
|
||||
e.uvarint(uint64(x))
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
// Copyright 2019 The Bazel Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package starlark_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"go.starlark.net/starlark"
|
||||
)
|
||||
|
||||
// TestProfile is a simple integration test that the profiler
|
||||
// emits minimally plausible pprof-compatible output.
|
||||
func TestProfile(t *testing.T) {
|
||||
prof, err := os.CreateTemp(t.TempDir(), "profile_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer prof.Close()
|
||||
if err := starlark.StartProfile(prof); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
const src = `
|
||||
def fibonacci(n):
|
||||
x, y = 1, 1
|
||||
for i in range(n):
|
||||
x, y = y, x+y
|
||||
return y
|
||||
|
||||
fibonacci(100000)
|
||||
`
|
||||
|
||||
thread := new(starlark.Thread)
|
||||
if _, err := starlark.ExecFile(thread, "foo.star", src, nil); err != nil {
|
||||
_ = starlark.StopProfile()
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := starlark.StopProfile(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
prof.Sync()
|
||||
cmd := exec.Command("go", "tool", "pprof", "-top", prof.Name())
|
||||
cmd.Stderr = new(bytes.Buffer)
|
||||
cmd.Stdout = new(bytes.Buffer)
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Fatalf("pprof failed: %v; output=<<%s>>", err, cmd.Stderr)
|
||||
}
|
||||
|
||||
// Typical output (may vary by go release):
|
||||
//
|
||||
// Type: wall
|
||||
// Time: Apr 4, 2019 at 11:10am (EDT)
|
||||
// Duration: 251.62ms, Total samples = 250ms (99.36%)
|
||||
// Showing nodes accounting for 250ms, 100% of 250ms total
|
||||
// flat flat% sum% cum cum%
|
||||
// 320ms 100% 100% 320ms 100% fibonacci
|
||||
// 0 0% 100% 320ms 100% foo.star
|
||||
//
|
||||
// We'll assert a few key substrings are present.
|
||||
got := fmt.Sprint(cmd.Stdout)
|
||||
for _, want := range []string{
|
||||
"flat%",
|
||||
"fibonacci",
|
||||
"foo.star",
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("output did not contain %q", want)
|
||||
}
|
||||
}
|
||||
if t.Failed() {
|
||||
t.Logf("stderr=%v", cmd.Stderr)
|
||||
t.Logf("stdout=%v", cmd.Stdout)
|
||||
}
|
||||
}
|
||||
Vendored
+354
@@ -0,0 +1,354 @@
|
||||
# Tests of Starlark assignment.
|
||||
|
||||
# This is a "chunked" file: each "---" effectively starts a new file.
|
||||
|
||||
# tuple assignment
|
||||
load("assert.star", "assert")
|
||||
|
||||
() = () # empty ok
|
||||
|
||||
a, b, c = 1, 2, 3
|
||||
assert.eq(a, 1)
|
||||
assert.eq(b, 2)
|
||||
assert.eq(c, 3)
|
||||
|
||||
(d, e, f,) = (1, 2, 3) # trailing comma ok
|
||||
---
|
||||
(a, b, c) = 1 ### "got int in sequence assignment"
|
||||
---
|
||||
(a, b) = () ### "too few values to unpack"
|
||||
---
|
||||
(a, b) = (1,) ### "too few values to unpack"
|
||||
---
|
||||
(a, b, c) = (1, 2) ### "too few values to unpack"
|
||||
---
|
||||
(a, b) = (1, 2, 3) ### "too many values to unpack"
|
||||
---
|
||||
() = 1 ### "got int in sequence assignment"
|
||||
---
|
||||
() = (1,) ### "too many values to unpack"
|
||||
---
|
||||
() = (1, 2) ### "too many values to unpack"
|
||||
---
|
||||
# list assignment
|
||||
load("assert.star", "assert")
|
||||
|
||||
[] = [] # empty ok
|
||||
|
||||
[a, b, c] = [1, 2, 3]
|
||||
assert.eq(a, 1)
|
||||
assert.eq(b, 2)
|
||||
assert.eq(c, 3)
|
||||
|
||||
[d, e, f,] = [1, 2, 3] # trailing comma ok
|
||||
---
|
||||
[a, b, c] = 1 ### "got int in sequence assignment"
|
||||
---
|
||||
[a, b] = [] ### "too few values to unpack"
|
||||
---
|
||||
[a, b] = [1] ### "too few values to unpack"
|
||||
---
|
||||
[a, b, c] = [1, 2] ### "too few values to unpack"
|
||||
---
|
||||
[a, b] = [1, 2, 3] ### "too many values to unpack"
|
||||
---
|
||||
[] = 1 ### "got int in sequence assignment"
|
||||
---
|
||||
[] = [1] ### "too many values to unpack"
|
||||
---
|
||||
[] = [1, 2] ### "too many values to unpack"
|
||||
---
|
||||
# list-tuple assignment
|
||||
load("assert.star", "assert")
|
||||
|
||||
# empty ok
|
||||
[] = ()
|
||||
() = []
|
||||
|
||||
[a, b, c] = (1, 2, 3)
|
||||
assert.eq(a, 1)
|
||||
assert.eq(b, 2)
|
||||
assert.eq(c, 3)
|
||||
|
||||
[a2, b2, c2] = 1, 2, 3 # bare tuple ok
|
||||
|
||||
(d, e, f) = [1, 2, 3]
|
||||
assert.eq(d, 1)
|
||||
assert.eq(e, 2)
|
||||
assert.eq(f, 3)
|
||||
|
||||
[g, h, (i, j)] = (1, 2, [3, 4])
|
||||
assert.eq(g, 1)
|
||||
assert.eq(h, 2)
|
||||
assert.eq(i, 3)
|
||||
assert.eq(j, 4)
|
||||
|
||||
(k, l, [m, n]) = [1, 2, (3, 4)]
|
||||
assert.eq(k, 1)
|
||||
assert.eq(l, 2)
|
||||
assert.eq(m, 3)
|
||||
assert.eq(n, 4)
|
||||
|
||||
---
|
||||
# misc assignment
|
||||
load("assert.star", "assert")
|
||||
|
||||
def assignment():
|
||||
a = [1, 2, 3]
|
||||
a[1] = 5
|
||||
assert.eq(a, [1, 5, 3])
|
||||
a[-2] = 2
|
||||
assert.eq(a, [1, 2, 3])
|
||||
assert.eq("%d %d" % (5, 7), "5 7")
|
||||
x={}
|
||||
x[1] = 2
|
||||
x[1] += 3
|
||||
assert.eq(x[1], 5)
|
||||
def f12(): x[(1, "abc", {})] = 1
|
||||
assert.fails(f12, "unhashable type: dict")
|
||||
|
||||
assignment()
|
||||
|
||||
---
|
||||
# augmented assignment
|
||||
|
||||
load("assert.star", "assert")
|
||||
|
||||
def f():
|
||||
x = 1
|
||||
x += 1
|
||||
assert.eq(x, 2)
|
||||
x *= 3
|
||||
assert.eq(x, 6)
|
||||
f()
|
||||
|
||||
---
|
||||
# effects of evaluating LHS occur only once
|
||||
|
||||
load("assert.star", "assert")
|
||||
|
||||
count = [0] # count[0] is the number of calls to f
|
||||
|
||||
def f():
|
||||
count[0] += 1
|
||||
return count[0]
|
||||
|
||||
x = [1, 2, 3]
|
||||
x[f()] += 1
|
||||
|
||||
assert.eq(x, [1, 3, 3]) # sole call to f returned 1
|
||||
assert.eq(count[0], 1) # f was called only once
|
||||
|
||||
---
|
||||
# Order of evaluation.
|
||||
|
||||
load("assert.star", "assert")
|
||||
|
||||
calls = []
|
||||
|
||||
def f(name, result):
|
||||
calls.append(name)
|
||||
return result
|
||||
|
||||
# The right side is evaluated before the left in an ordinary assignment.
|
||||
calls.clear()
|
||||
f("array", [0])[f("index", 0)] = f("rhs", 0)
|
||||
assert.eq(calls, ["rhs", "array", "index"])
|
||||
|
||||
calls.clear()
|
||||
f("lhs1", [0])[0], f("lhs2", [0])[0] = f("rhs1", 0), f("rhs2", 0)
|
||||
assert.eq(calls, ["rhs1", "rhs2", "lhs1", "lhs2"])
|
||||
|
||||
# Left side is evaluated first (and only once) in an augmented assignment.
|
||||
calls.clear()
|
||||
f("array", [0])[f("index", 0)] += f("addend", 1)
|
||||
assert.eq(calls, ["array", "index", "addend"])
|
||||
|
||||
---
|
||||
# global referenced before assignment
|
||||
|
||||
def f():
|
||||
return g ### "global variable g referenced before assignment"
|
||||
|
||||
f()
|
||||
|
||||
g = 1
|
||||
|
||||
---
|
||||
# Free variables are captured by reference, so this is ok.
|
||||
load("assert.star", "assert")
|
||||
|
||||
def f():
|
||||
def g():
|
||||
return outer
|
||||
outer = 1
|
||||
return g()
|
||||
|
||||
assert.eq(f(), 1)
|
||||
|
||||
---
|
||||
load("assert.star", "assert")
|
||||
|
||||
printok = [False]
|
||||
|
||||
# This program should resolve successfully but fail dynamically.
|
||||
# However, the Java implementation currently reports the dynamic
|
||||
# error at the x=1 statement (b/33975425). I think we need to simplify
|
||||
# the resolver algorithm to what we have implemented.
|
||||
def use_before_def():
|
||||
print(x) # dynamic error: local var referenced before assignment
|
||||
printok[0] = True
|
||||
x = 1 # makes 'x' local
|
||||
|
||||
assert.fails(use_before_def, 'local variable x referenced before assignment')
|
||||
assert.true(not printok[0]) # execution of print statement failed
|
||||
|
||||
---
|
||||
x = [1]
|
||||
x.extend([2]) # ok
|
||||
|
||||
def f():
|
||||
x += [4] ### "local variable x referenced before assignment"
|
||||
|
||||
f()
|
||||
|
||||
---
|
||||
|
||||
z += 3 ### "global variable z referenced before assignment"
|
||||
|
||||
---
|
||||
load("assert.star", "assert")
|
||||
|
||||
# It's ok to define a global that shadows a built-in...
|
||||
list = []
|
||||
assert.eq(type(list), "list")
|
||||
|
||||
# ...but then all uses refer to the global,
|
||||
# even if they occur before the binding use.
|
||||
# See github.com/google/skylark/issues/116.
|
||||
assert.fails(lambda: tuple, "global variable tuple referenced before assignment")
|
||||
tuple = ()
|
||||
|
||||
---
|
||||
# option:set
|
||||
# Same as above, but set is dialect-specific;
|
||||
# we shouldn't notice any difference.
|
||||
load("assert.star", "assert")
|
||||
|
||||
set = [1, 2, 3]
|
||||
assert.eq(type(set), "list")
|
||||
|
||||
# As in Python 2 and Python 3,
|
||||
# all 'in x' expressions in a comprehension are evaluated
|
||||
# in the comprehension's lexical block, except the first,
|
||||
# which is resolved in the outer block.
|
||||
x = [[1, 2]]
|
||||
assert.eq([x for x in x for y in x],
|
||||
[[1, 2], [1, 2]])
|
||||
|
||||
---
|
||||
# A comprehension establishes a single new lexical block,
|
||||
# not one per 'for' clause.
|
||||
x = [1, 2]
|
||||
_ = [x for _ in [3] for x in x] ### "local variable x referenced before assignment"
|
||||
|
||||
---
|
||||
load("assert.star", "assert")
|
||||
|
||||
# assign singleton sequence to 1-tuple
|
||||
(x,) = (1,)
|
||||
assert.eq(x, 1)
|
||||
(y,) = [1]
|
||||
assert.eq(y, 1)
|
||||
|
||||
# assign 1-tuple to variable
|
||||
z = (1,)
|
||||
assert.eq(type(z), "tuple")
|
||||
assert.eq(len(z), 1)
|
||||
assert.eq(z[0], 1)
|
||||
|
||||
# assign value to parenthesized variable
|
||||
(a) = 1
|
||||
assert.eq(a, 1)
|
||||
|
||||
---
|
||||
# assignment to/from fields.
|
||||
load("assert.star", "assert", "freeze")
|
||||
|
||||
hf = hasfields()
|
||||
hf.x = 1
|
||||
assert.eq(hf.x, 1)
|
||||
hf.x = [1, 2]
|
||||
hf.x += [3, 4]
|
||||
assert.eq(hf.x, [1, 2, 3, 4])
|
||||
freeze(hf)
|
||||
def setX(hf):
|
||||
hf.x = 2
|
||||
def setY(hf):
|
||||
hf.y = 3
|
||||
assert.fails(lambda: setX(hf), "cannot set field on a frozen hasfields")
|
||||
assert.fails(lambda: setY(hf), "cannot set field on a frozen hasfields")
|
||||
|
||||
---
|
||||
# destucturing assignment in a for loop.
|
||||
load("assert.star", "assert")
|
||||
|
||||
def f():
|
||||
res = []
|
||||
for (x, y), z in [(["a", "b"], 3), (["c", "d"], 4)]:
|
||||
res.append((x, y, z))
|
||||
return res
|
||||
assert.eq(f(), [("a", "b", 3), ("c", "d", 4)])
|
||||
|
||||
def g():
|
||||
a = {}
|
||||
for i, a[i] in [("one", 1), ("two", 2)]:
|
||||
pass
|
||||
return a
|
||||
assert.eq(g(), {"one": 1, "two": 2})
|
||||
|
||||
---
|
||||
# parenthesized LHS in augmented assignment (success)
|
||||
# option:globalreassign
|
||||
load("assert.star", "assert")
|
||||
|
||||
a = 5
|
||||
(a) += 3
|
||||
assert.eq(a, 8)
|
||||
|
||||
---
|
||||
# parenthesized LHS in augmented assignment (error)
|
||||
|
||||
(a) += 5 ### "global variable a referenced before assignment"
|
||||
|
||||
---
|
||||
# option:globalreassign
|
||||
load("assert.star", "assert")
|
||||
assert = 1
|
||||
load("assert.star", "assert")
|
||||
|
||||
---
|
||||
# option:globalreassign option:loadbindsglobally
|
||||
load("assert.star", "assert")
|
||||
assert = 1
|
||||
load("assert.star", "assert")
|
||||
|
||||
---
|
||||
# option:loadbindsglobally
|
||||
_ = assert ### "global variable assert referenced before assignment"
|
||||
load("assert.star", "assert")
|
||||
|
||||
---
|
||||
_ = assert ### "local variable assert referenced before assignment"
|
||||
load("assert.star", "assert")
|
||||
|
||||
---
|
||||
def f(): assert.eq(1, 1) # forward ref OK
|
||||
load("assert.star", "assert")
|
||||
f()
|
||||
|
||||
---
|
||||
# option:loadbindsglobally
|
||||
def f(): assert.eq(1, 1) # forward ref OK
|
||||
load("assert.star", "assert")
|
||||
f()
|
||||
Vendored
+167
@@ -0,0 +1,167 @@
|
||||
# Benchmarks of Starlark execution
|
||||
# option:set
|
||||
|
||||
def bench_range_construction(b):
|
||||
for _ in range(b.n):
|
||||
range(200)
|
||||
|
||||
def bench_range_iteration(b):
|
||||
for _ in range(b.n):
|
||||
for x in range(200):
|
||||
pass
|
||||
|
||||
# Make a 2-level call tree of 100 * 100 calls.
|
||||
def bench_calling(b):
|
||||
list = range(100)
|
||||
|
||||
def g():
|
||||
for x in list:
|
||||
pass
|
||||
|
||||
def f():
|
||||
for x in list:
|
||||
g()
|
||||
|
||||
for _ in range(b.n):
|
||||
f()
|
||||
|
||||
# Measure overhead of calling a trivial built-in method.
|
||||
emptydict = {}
|
||||
range1000 = range(1000)
|
||||
|
||||
def bench_builtin_method(b):
|
||||
for _ in range(b.n):
|
||||
for _ in range1000:
|
||||
emptydict.get(None)
|
||||
|
||||
def bench_int(b):
|
||||
for _ in range(b.n):
|
||||
a = 0
|
||||
for _ in range1000:
|
||||
a += 1
|
||||
|
||||
def bench_bigint(b):
|
||||
for _ in range(b.n):
|
||||
a = 1 << 31 # maxint32 + 1
|
||||
for _ in range1000:
|
||||
a += 1
|
||||
|
||||
def bench_gauss(b):
|
||||
# Sum of arithmetic series. All results fit in int32.
|
||||
for _ in range(b.n):
|
||||
acc = 0
|
||||
for x in range(92000):
|
||||
acc += x
|
||||
|
||||
def bench_mix(b):
|
||||
"Benchmark of a simple mix of computation (for, if, arithmetic, comprehension)."
|
||||
for _ in range(b.n):
|
||||
x = 0
|
||||
for i in range(50):
|
||||
if i:
|
||||
x += 1
|
||||
a = [x for x in range(i)]
|
||||
|
||||
largedict = {str(v): v for v in range(1000)}
|
||||
|
||||
def bench_dict_equal(b):
|
||||
"Benchmark of dict equality operation."
|
||||
for _ in range(b.n):
|
||||
if largedict != largedict:
|
||||
fail("invalid comparison")
|
||||
|
||||
largeset = set([v for v in range(1000)])
|
||||
|
||||
def bench_set_equal(b):
|
||||
"Benchmark of set union operation."
|
||||
for _ in range(b.n):
|
||||
if largeset != largeset:
|
||||
fail("invalid comparison")
|
||||
|
||||
flat = { "int": 1, "float": 0.2, "string": "string", "list": [], "bool": True, "nil": None, "tuple": (1, 2, 3) }
|
||||
deep = {
|
||||
"type": "int",
|
||||
"value": 1,
|
||||
"next": {
|
||||
"type": "float",
|
||||
"value": 0.2,
|
||||
"next": {
|
||||
"type": "string",
|
||||
"value": "string",
|
||||
"next": {
|
||||
"type": "list",
|
||||
"value": [ 1, "", True, None, (1, 2) ],
|
||||
"next": {
|
||||
"type": "bool",
|
||||
"value": True,
|
||||
"next": {
|
||||
"type": "tuple",
|
||||
"value": (1, 2.0, "3"),
|
||||
"next": None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deep_list = [ deep for _ in range(100) ]
|
||||
|
||||
def bench_to_json_flat_mixed(b):
|
||||
"Benchmark json.encode builtin with flat mixed input"
|
||||
for _ in range(b.n):
|
||||
json.encode(flat)
|
||||
|
||||
def bench_to_json_flat_big(b):
|
||||
"Benchmark json.encode builtin with big flat integer input"
|
||||
for _ in range(b.n):
|
||||
json.encode(largedict)
|
||||
|
||||
def bench_to_json_deep(b):
|
||||
"Benchmark json.encode builtin with deep input"
|
||||
for _ in range(b.n):
|
||||
json.encode(deep)
|
||||
|
||||
def bench_to_json_deep_list(b):
|
||||
"Benchmark json.encode builtin with a list of deep input"
|
||||
for _ in range(b.n):
|
||||
json.encode(deep)
|
||||
|
||||
def bench_issubset_unique_large_small(b):
|
||||
"Benchmark set.issubset builtin"
|
||||
s = set(range(10000))
|
||||
for _ in range(b.n):
|
||||
s.issubset(range(1000))
|
||||
|
||||
def bench_issubset_unique_small_large(b):
|
||||
"Benchmark set.issubset builtin"
|
||||
s = set(range(1000))
|
||||
for _ in range(b.n):
|
||||
s.issubset(range(10000))
|
||||
|
||||
def bench_issubset_unique_same(b):
|
||||
"Benchmark set.issubset builtin"
|
||||
s = set(range(1000))
|
||||
for _ in range(b.n):
|
||||
s.issubset(range(1000))
|
||||
|
||||
def bench_issubset_duplicate_large_small(b):
|
||||
"Benchmark set.issubset builtin"
|
||||
s = set(range(10000))
|
||||
l = list(range(200)) * 5
|
||||
for _ in range(b.n):
|
||||
s.issubset(range(1000))
|
||||
|
||||
def bench_issubset_duplicate_small_large(b):
|
||||
"Benchmark set.issubset builtin"
|
||||
s = set(range(1000))
|
||||
l = list(range(2000)) * 5
|
||||
for _ in range(b.n):
|
||||
s.issubset(l)
|
||||
|
||||
def bench_issubset_duplicate_same(b):
|
||||
"Benchmark set.issubset builtin"
|
||||
s = set(range(1000))
|
||||
l = list(range(200)) * 5
|
||||
for _ in range(b.n):
|
||||
s.issubset(l)
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
# Tests of Starlark 'bool'
|
||||
|
||||
load("assert.star", "assert")
|
||||
|
||||
# truth
|
||||
assert.true(True)
|
||||
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("")],
|
||||
[False, True, False, True, False],
|
||||
)
|
||||
|
||||
# comparison
|
||||
assert.true(None == None)
|
||||
assert.true(None != False)
|
||||
assert.true(None != True)
|
||||
assert.eq(1 == 1, True)
|
||||
assert.eq(1 == 2, False)
|
||||
assert.true(False == False)
|
||||
assert.true(True == True)
|
||||
|
||||
# ordered comparison
|
||||
assert.true(False < True)
|
||||
assert.true(False <= True)
|
||||
assert.true(False <= False)
|
||||
assert.true(True > False)
|
||||
assert.true(True >= False)
|
||||
assert.true(True >= True)
|
||||
|
||||
# conditional expression
|
||||
assert.eq(1 if 3 > 2 else 0, 1)
|
||||
assert.eq(1 if "foo" else 0, 1)
|
||||
assert.eq(1 if "" else 0, 0)
|
||||
|
||||
# short-circuit evaluation of 'and' and 'or':
|
||||
# 'or' yields the first true operand, or the last if all are false.
|
||||
assert.eq(0 or "" or [] or 0, 0)
|
||||
assert.eq(0 or "" or [] or 123 or 1 // 0, 123)
|
||||
assert.fails(lambda : 0 or "" or [] or 0 or 1 // 0, "division by zero")
|
||||
|
||||
# 'and' yields the first false operand, or the last if all are true.
|
||||
assert.eq(1 and "a" and [1] and 123, 123)
|
||||
assert.eq(1 and "a" and [1] and 0 and 1 // 0, 0)
|
||||
assert.fails(lambda : 1 and "a" and [1] and 123 and 1 // 0, "division by zero")
|
||||
|
||||
# Built-ins that want a bool want an actual bool, not a truth value.
|
||||
# See github.com/bazelbuild/starlark/issues/30
|
||||
assert.eq(''.splitlines(True), [])
|
||||
assert.fails(lambda: ''.splitlines(1), 'got int, want bool')
|
||||
assert.fails(lambda: ''.splitlines("hello"), 'got string, want bool')
|
||||
assert.fails(lambda: ''.splitlines(0.0), 'got float, want bool')
|
||||
Vendored
+240
@@ -0,0 +1,240 @@
|
||||
# Tests of Starlark built-in functions
|
||||
# option:set
|
||||
|
||||
load("assert.star", "assert")
|
||||
|
||||
# len
|
||||
assert.eq(len([1, 2, 3]), 3)
|
||||
assert.eq(len((1, 2, 3)), 3)
|
||||
assert.eq(len({1: 2}), 1)
|
||||
assert.fails(lambda: len(1), "int.*has no len")
|
||||
|
||||
# and, or
|
||||
assert.eq(123 or "foo", 123)
|
||||
assert.eq(0 or "foo", "foo")
|
||||
assert.eq(123 and "foo", "foo")
|
||||
assert.eq(0 and "foo", 0)
|
||||
none = None
|
||||
_1 = none and none[0] # rhs is not evaluated
|
||||
_2 = (not none) or none[0] # rhs is not evaluated
|
||||
|
||||
# abs
|
||||
assert.eq(abs(2.0), 2.0)
|
||||
assert.eq(abs(0.0), 0.0)
|
||||
assert.eq(abs(-2.0), 2.0)
|
||||
assert.eq(abs(2), 2)
|
||||
assert.eq(abs(0), 0)
|
||||
assert.eq(abs(-2), 2)
|
||||
assert.eq(abs(float("inf")), float("inf"))
|
||||
assert.eq(abs(float("-inf")), float("inf"))
|
||||
assert.eq(abs(float("nan")), float("nan"))
|
||||
assert.fails(lambda: abs("0"), "got string, want int or float")
|
||||
maxint32 = (1 << 31) - 1
|
||||
assert.eq(abs(+123 * maxint32), +123 * maxint32)
|
||||
assert.eq(abs(-123 * maxint32), +123 * maxint32)
|
||||
|
||||
# any, all
|
||||
assert.true(all([]))
|
||||
assert.true(all([1, True, "foo"]))
|
||||
assert.true(not all([1, True, ""]))
|
||||
assert.true(not any([]))
|
||||
assert.true(any([0, False, "foo"]))
|
||||
assert.true(not any([0, False, ""]))
|
||||
|
||||
# in
|
||||
assert.true(3 in [1, 2, 3])
|
||||
assert.true(4 not in [1, 2, 3])
|
||||
assert.true(3 in (1, 2, 3))
|
||||
assert.true(4 not in (1, 2, 3))
|
||||
assert.fails(lambda: 3 in "foo", "in.*requires string as left operand")
|
||||
assert.true(123 in {123: ""})
|
||||
assert.true(456 not in {123:""})
|
||||
assert.true([] not in {123: ""})
|
||||
|
||||
# sorted
|
||||
assert.eq(sorted([42, 123, 3]), [3, 42, 123])
|
||||
assert.eq(sorted([42, 123, 3], reverse=True), [123, 42, 3])
|
||||
assert.eq(sorted(["wiz", "foo", "bar"]), ["bar", "foo", "wiz"])
|
||||
assert.eq(sorted(["wiz", "foo", "bar"], reverse=True), ["wiz", "foo", "bar"])
|
||||
assert.fails(lambda: sorted([1, 2, None, 3]), "int < NoneType not implemented")
|
||||
assert.fails(lambda: sorted([1, "one"]), "string < int not implemented")
|
||||
# custom key function
|
||||
assert.eq(sorted(["two", "three", "four"], key=len),
|
||||
["two", "four", "three"])
|
||||
assert.eq(sorted(["two", "three", "four"], key=len, reverse=True),
|
||||
["three", "four", "two"])
|
||||
assert.fails(lambda: sorted([1, 2, 3], key=None), "got NoneType, want callable")
|
||||
# sort is stable
|
||||
pairs = [(4, 0), (3, 1), (4, 2), (2, 3), (3, 4), (1, 5), (2, 6), (3, 7)]
|
||||
assert.eq(sorted(pairs, key=lambda x: x[0]),
|
||||
[(1, 5),
|
||||
(2, 3), (2, 6),
|
||||
(3, 1), (3, 4), (3, 7),
|
||||
(4, 0), (4, 2)])
|
||||
assert.fails(lambda: sorted(1), 'sorted: for parameter iterable: got int, want iterable')
|
||||
|
||||
# reversed
|
||||
assert.eq(reversed([1, 144, 81, 16]), [16, 81, 144, 1])
|
||||
|
||||
# set
|
||||
assert.contains(set([1, 2, 3]), 1)
|
||||
assert.true(4 not in set([1, 2, 3]))
|
||||
assert.eq(len(set([1, 2, 3])), 3)
|
||||
assert.eq(sorted([x for x in set([1, 2, 3])]), [1, 2, 3])
|
||||
|
||||
# dict
|
||||
assert.eq(dict([(1, 2), (3, 4)]), {1: 2, 3: 4})
|
||||
assert.eq(dict([(1, 2), (3, 4)], foo="bar"), {1: 2, 3: 4, "foo": "bar"})
|
||||
assert.eq(dict({1:2, 3:4}), {1: 2, 3: 4})
|
||||
assert.eq(dict({1:2, 3:4}.items()), {1: 2, 3: 4})
|
||||
|
||||
# range
|
||||
assert.eq("range", type(range(10)))
|
||||
assert.eq("range(10)", str(range(0, 10, 1)))
|
||||
assert.eq("range(1, 10)", str(range(1, 10)))
|
||||
assert.eq(range(0, 5, 10), range(0, 5, 11))
|
||||
assert.eq("range(0, 10, -1)", str(range(0, 10, -1)))
|
||||
assert.fails(lambda: {range(10): 10}, "unhashable: range")
|
||||
assert.true(bool(range(1, 2)))
|
||||
assert.true(not(range(2, 1))) # an empty range is false
|
||||
assert.eq([x*x for x in range(5)], [0, 1, 4, 9, 16])
|
||||
assert.eq(list(range(5)), [0, 1, 2, 3, 4])
|
||||
assert.eq(list(range(-5)), [])
|
||||
assert.eq(list(range(2, 5)), [2, 3, 4])
|
||||
assert.eq(list(range(5, 2)), [])
|
||||
assert.eq(list(range(-2, -5)), [])
|
||||
assert.eq(list(range(-5, -2)), [-5, -4, -3])
|
||||
assert.eq(list(range(2, 10, 3)), [2, 5, 8])
|
||||
assert.eq(list(range(10, 2, -3)), [10, 7, 4])
|
||||
assert.eq(list(range(-2, -10, -3)), [-2, -5, -8])
|
||||
assert.eq(list(range(-10, -2, 3)), [-10, -7, -4])
|
||||
assert.eq(list(range(10, 2, -1)), [10, 9, 8, 7, 6, 5, 4, 3])
|
||||
assert.eq(list(range(5)[1:]), [1, 2, 3, 4])
|
||||
assert.eq(len(range(5)[1:]), 4)
|
||||
assert.eq(list(range(5)[:2]), [0, 1])
|
||||
assert.eq(list(range(10)[1:]), [1, 2, 3, 4, 5, 6, 7, 8, 9])
|
||||
assert.eq(list(range(10)[1:9:2]), [1, 3, 5, 7])
|
||||
assert.eq(list(range(10)[1:10:2]), [1, 3, 5, 7, 9])
|
||||
assert.eq(list(range(10)[1:11:2]), [1, 3, 5, 7, 9])
|
||||
assert.eq(list(range(10)[::-2]), [9, 7, 5, 3, 1])
|
||||
assert.eq(list(range(0, 10, 2)[::2]), [0, 4, 8])
|
||||
assert.eq(list(range(0, 10, 2)[::-2]), [8, 4, 0])
|
||||
# range() is limited by the width of the Go int type (int32 or int64).
|
||||
assert.fails(lambda: range(1<<64), "... out of range .want value in signed ..-bit range")
|
||||
assert.eq(len(range(0x7fffffff)), 0x7fffffff) # O(1)
|
||||
# Two ranges compare equal if they denote the same sequence:
|
||||
assert.eq(range(0), range(2, 1, 3)) # []
|
||||
assert.eq(range(0, 3, 2), range(0, 4, 2)) # [0, 2]
|
||||
assert.ne(range(1, 10), range(2, 10))
|
||||
assert.fails(lambda: range(0) < range(0), "range < range not implemented")
|
||||
# <number> in <range>
|
||||
assert.contains(range(3), 1)
|
||||
assert.contains(range(3), 2.0) # acts like 2
|
||||
assert.fails(lambda: True in range(3), "requires integer.*not bool") # bools aren't numbers
|
||||
assert.fails(lambda: "one" in range(10), "requires integer.*not string")
|
||||
assert.true(4 not in range(4))
|
||||
assert.true(1e15 not in range(4)) # too big for int32
|
||||
assert.true(1e100 not in range(4)) # too big for int64
|
||||
# https://github.com/google/starlark-go/issues/116
|
||||
assert.fails(lambda: range(0, 0, 2)[:][0], "index 0 out of range: empty range")
|
||||
|
||||
# list
|
||||
assert.eq(list("abc".elems()), ["a", "b", "c"])
|
||||
assert.eq(sorted(list({"a": 1, "b": 2})), ['a', 'b'])
|
||||
|
||||
# min, max
|
||||
assert.eq(min(5, -2, 1, 7, 3), -2)
|
||||
assert.eq(max(5, -2, 1, 7, 3), 7)
|
||||
assert.eq(min([5, -2, 1, 7, 3]), -2)
|
||||
assert.eq(min("one", "two", "three", "four"), "four")
|
||||
assert.eq(max("one", "two", "three", "four"), "two")
|
||||
assert.fails(min, "min requires at least one positional argument")
|
||||
assert.fails(lambda: min(1), "not iterable")
|
||||
assert.fails(lambda: min([]), "empty")
|
||||
assert.eq(min(5, -2, 1, 7, 3, key=lambda x: x*x), 1) # min absolute value
|
||||
assert.eq(min(5, -2, 1, 7, 3, key=lambda x: -x), 7) # min negated value
|
||||
|
||||
# enumerate
|
||||
assert.eq(enumerate("abc".elems()), [(0, "a"), (1, "b"), (2, "c")])
|
||||
assert.eq(enumerate([False, True, None], 42), [(42, False), (43, True), (44, None)])
|
||||
|
||||
# zip
|
||||
assert.eq(zip(), [])
|
||||
assert.eq(zip([]), [])
|
||||
assert.eq(zip([1, 2, 3]), [(1,), (2,), (3,)])
|
||||
assert.eq(zip("".elems()), [])
|
||||
assert.eq(zip("abc".elems(),
|
||||
list("def".elems()),
|
||||
"hijk".elems()),
|
||||
[("a", "d", "h"), ("b", "e", "i"), ("c", "f", "j")])
|
||||
z1 = [1]
|
||||
assert.eq(zip(z1), [(1,)])
|
||||
z1.append(2)
|
||||
assert.eq(zip(z1), [(1,), (2,)])
|
||||
assert.fails(lambda: zip(z1, 1), "zip: argument #2 is not iterable: int")
|
||||
z1.append(3)
|
||||
|
||||
# dir for builtin_function_or_method
|
||||
assert.eq(dir(None), [])
|
||||
assert.eq(dir({})[:3], ["clear", "get", "items"]) # etc
|
||||
assert.eq(dir(1), [])
|
||||
assert.eq(dir([])[:3], ["append", "clear", "extend"]) # etc
|
||||
|
||||
# hasattr, getattr, dir
|
||||
# hasfields is an application-defined type defined in eval_test.go.
|
||||
hf = hasfields()
|
||||
assert.eq(dir(hf), [])
|
||||
assert.true(not hasattr(hf, "x"))
|
||||
assert.fails(lambda: getattr(hf, "x"), "no .x field or method")
|
||||
assert.eq(getattr(hf, "x", 42), 42)
|
||||
hf.x = 1
|
||||
assert.true(hasattr(hf, "x"))
|
||||
assert.eq(getattr(hf, "x"), 1)
|
||||
assert.eq(hf.x, 1)
|
||||
hf.x = 2
|
||||
assert.eq(getattr(hf, "x"), 2)
|
||||
assert.eq(hf.x, 2)
|
||||
# built-in types can have attributes (methods) too.
|
||||
myset = set([])
|
||||
assert.eq(dir(myset), ["add", "clear", "difference", "discard", "intersection", "issubset", "issuperset", "pop", "remove", "symmetric_difference", "union"])
|
||||
assert.true(hasattr(myset, "union"))
|
||||
assert.true(not hasattr(myset, "onion"))
|
||||
assert.eq(str(getattr(myset, "union")), "<built-in method union of set value>")
|
||||
assert.fails(lambda: getattr(myset, "onion"), "no .onion field or method")
|
||||
assert.eq(getattr(myset, "onion", 42), 42)
|
||||
|
||||
# dir returns a new, sorted, mutable list
|
||||
assert.eq(sorted(dir("")), dir("")) # sorted
|
||||
dir("").append("!") # mutable
|
||||
assert.true("!" not in dir("")) # new
|
||||
|
||||
# error messages should suggest spelling corrections
|
||||
hf.one = 1
|
||||
hf.two = 2
|
||||
hf.three = 3
|
||||
hf.forty_five = 45
|
||||
assert.fails(lambda: hf.One, 'no .One field.*did you mean .one')
|
||||
assert.fails(lambda: hf.oone, 'no .oone field.*did you mean .one')
|
||||
assert.fails(lambda: hf.FortyFive, 'no .FortyFive field.*did you mean .forty_five')
|
||||
assert.fails(lambda: hf.trhee, 'no .trhee field.*did you mean .three')
|
||||
assert.fails(lambda: hf.thirty, 'no .thirty field or method$') # no suggestion
|
||||
|
||||
# spell check in setfield too
|
||||
def setfield(): hf.noForty_Five = 46 # "no" prefix => SetField returns NoSuchField
|
||||
assert.fails(setfield, 'no .noForty_Five field.*did you mean .forty_five')
|
||||
|
||||
# repr
|
||||
assert.eq(repr(1), "1")
|
||||
assert.eq(repr("x"), '"x"')
|
||||
assert.eq(repr(["x", 1]), '["x", 1]')
|
||||
|
||||
# fail
|
||||
---
|
||||
fail() ### `fail: $`
|
||||
x = 1//0 # unreachable
|
||||
---
|
||||
fail(1) ### `fail: 1`
|
||||
---
|
||||
fail(1, 2, 3) ### `fail: 1 2 3`
|
||||
---
|
||||
fail(1, 2, 3, sep="/") ### `fail: 1/2/3`
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
# Tests of 'bytes' (immutable byte strings).
|
||||
|
||||
load("assert.star", "assert")
|
||||
|
||||
# bytes(string) -- UTF-k to UTF-8 transcoding with U+FFFD replacement
|
||||
hello = bytes("hello, 世界")
|
||||
goodbye = bytes("goodbye")
|
||||
empty = bytes("")
|
||||
nonprinting = bytes("\t\n\x7F\u200D") # TAB, NEWLINE, DEL, ZERO_WIDTH_JOINER
|
||||
assert.eq(bytes("hello, 世界"[:-1]), b"hello, 世��")
|
||||
|
||||
# bytes(iterable of int) -- construct from numeric byte values
|
||||
assert.eq(bytes([65, 66, 67]), b"ABC")
|
||||
assert.eq(bytes((65, 66, 67)), b"ABC")
|
||||
assert.eq(bytes([0xf0, 0x9f, 0x98, 0xbf]), b"😿")
|
||||
assert.fails(lambda: bytes([300]),
|
||||
"at index 0, 300 out of range .want value in unsigned 8-bit range")
|
||||
assert.fails(lambda: bytes([b"a"]),
|
||||
"at index 0, got bytes, want int")
|
||||
assert.fails(lambda: bytes(1), "want string, bytes, or iterable of ints")
|
||||
|
||||
# literals
|
||||
assert.eq(b"hello, 世界", hello)
|
||||
assert.eq(b"goodbye", goodbye)
|
||||
assert.eq(b"", empty)
|
||||
assert.eq(b"\t\n\x7F\u200D", nonprinting)
|
||||
assert.ne("abc", b"abc")
|
||||
assert.eq(b"\012\xff\u0400\U0001F63F", b"\n\xffЀ😿") # see scanner tests for more
|
||||
assert.eq(rb"\r\n\t", b"\\r\\n\\t") # raw
|
||||
|
||||
# type
|
||||
assert.eq(type(hello), "bytes")
|
||||
|
||||
# len
|
||||
assert.eq(len(hello), 13)
|
||||
assert.eq(len(goodbye), 7)
|
||||
assert.eq(len(empty), 0)
|
||||
assert.eq(len(b"A"), 1)
|
||||
assert.eq(len(b"Ѐ"), 2)
|
||||
assert.eq(len(b"世"), 3)
|
||||
assert.eq(len(b"😿"), 4)
|
||||
|
||||
# truth
|
||||
assert.true(hello)
|
||||
assert.true(goodbye)
|
||||
assert.true(not empty)
|
||||
|
||||
# str(bytes) does UTF-8 to UTF-k transcoding.
|
||||
# TODO(adonovan): specify.
|
||||
assert.eq(str(hello), "hello, 世界")
|
||||
assert.eq(str(hello[:-1]), "hello, 世��") # incomplete UTF-8 encoding => U+FFFD
|
||||
assert.eq(str(goodbye), "goodbye")
|
||||
assert.eq(str(empty), "")
|
||||
assert.eq(str(nonprinting), "\t\n\x7f\u200d")
|
||||
assert.eq(str(b"\xED\xB0\x80"), "���") # UTF-8 encoding of unpaired surrogate => U+FFFD x 3
|
||||
|
||||
# repr
|
||||
assert.eq(repr(hello), r'b"hello, 世界"')
|
||||
assert.eq(repr(hello[:-1]), r'b"hello, 世\xe7\x95"') # (incomplete UTF-8 encoding )
|
||||
assert.eq(repr(goodbye), 'b"goodbye"')
|
||||
assert.eq(repr(empty), 'b""')
|
||||
assert.eq(repr(nonprinting), 'b"\\t\\n\\x7f\\u200d"')
|
||||
|
||||
# equality
|
||||
assert.eq(hello, hello)
|
||||
assert.ne(hello, goodbye)
|
||||
assert.eq(b"goodbye", goodbye)
|
||||
|
||||
# ordered comparison
|
||||
assert.lt(b"abc", b"abd")
|
||||
assert.lt(b"abc", b"abcd")
|
||||
assert.lt(b"\x7f", b"\x80") # bytes compare as uint8, not int8
|
||||
|
||||
# bytes are dict-hashable
|
||||
dict = {hello: 1, goodbye: 2}
|
||||
dict[b"goodbye"] = 3
|
||||
assert.eq(len(dict), 2)
|
||||
assert.eq(dict[goodbye], 3)
|
||||
|
||||
# hash(bytes) is 32-bit FNV-1a.
|
||||
assert.eq(hash(b""), 0x811c9dc5)
|
||||
assert.eq(hash(b"a"), 0xe40c292c)
|
||||
assert.eq(hash(b"ab"), 0x4d2505ca)
|
||||
assert.eq(hash(b"abc"), 0x1a47e90b)
|
||||
|
||||
# indexing
|
||||
assert.eq(goodbye[0], b"g")
|
||||
assert.eq(goodbye[-1], b"e")
|
||||
assert.fails(lambda: goodbye[100], "out of range")
|
||||
|
||||
# slicing
|
||||
assert.eq(goodbye[:4], b"good")
|
||||
assert.eq(goodbye[4:], b"bye")
|
||||
assert.eq(goodbye[::2], b"gobe")
|
||||
assert.eq(goodbye[3:4], b"d") # special case: len=1
|
||||
assert.eq(goodbye[4:4], b"") # special case: len=0
|
||||
|
||||
# bytes in bytes
|
||||
assert.eq(b"bc" in b"abcd", True)
|
||||
assert.eq(b"bc" in b"dcab", False)
|
||||
assert.fails(lambda: "bc" in b"dcab", "requires bytes or int as left operand, not string")
|
||||
|
||||
# int in bytes
|
||||
assert.eq(97 in b"abc", True) # 97='a'
|
||||
assert.eq(100 in b"abc", False) # 100='d'
|
||||
assert.fails(lambda: 256 in b"abc", "int in bytes: 256 out of range")
|
||||
assert.fails(lambda: -1 in b"abc", "int in bytes: -1 out of range")
|
||||
|
||||
# ord TODO(adonovan): specify
|
||||
assert.eq(ord(b"a"), 97)
|
||||
assert.fails(lambda: ord(b"ab"), "ord: bytes has length 2, want 1")
|
||||
assert.fails(lambda: ord(b""), "ord: bytes has length 0, want 1")
|
||||
|
||||
# repeat (bytes * int)
|
||||
assert.eq(goodbye * 3, b"goodbyegoodbyegoodbye")
|
||||
assert.eq(3 * goodbye, b"goodbyegoodbyegoodbye")
|
||||
|
||||
# elems() returns an iterable value over 1-byte substrings.
|
||||
assert.eq(type(hello.elems()), "bytes.elems")
|
||||
assert.eq(str(hello.elems()), "b\"hello, 世界\".elems()")
|
||||
assert.eq(list(hello.elems()), [104, 101, 108, 108, 111, 44, 32, 228, 184, 150, 231, 149, 140])
|
||||
assert.eq(bytes([104, 101, 108, 108, 111, 44, 32, 228, 184, 150, 231, 149, 140]), hello)
|
||||
assert.eq(list(goodbye.elems()), [103, 111, 111, 100, 98, 121, 101])
|
||||
assert.eq(list(empty.elems()), [])
|
||||
assert.eq(bytes(hello.elems()), hello) # bytes(iterable) is dual to bytes.elems()
|
||||
|
||||
# x[i] = ...
|
||||
def f():
|
||||
b"abc"[1] = b"B"
|
||||
|
||||
assert.fails(f, "bytes.*does not support.*assignment")
|
||||
|
||||
# TODO(adonovan): the specification is not finalized in many areas:
|
||||
# - chr, ord functions
|
||||
# - encoding/decoding bytes to string.
|
||||
# - methods: find, index, split, etc.
|
||||
#
|
||||
# Summary of string operations (put this in spec).
|
||||
#
|
||||
# string to number:
|
||||
# - bytes[i] returns numeric value of ith byte.
|
||||
# - ord(string) returns numeric value of sole code point in string.
|
||||
# - ord(string[i]) is not a useful operation: fails on non-ASCII; see below.
|
||||
# Q. Perhaps ord should return the first (not sole) code point? Then it becomes a UTF-8 decoder.
|
||||
# Perhaps ord(string, index=int) should apply the index and relax the len=1 check.
|
||||
# - string.codepoint() iterates over 1-codepoint substrings.
|
||||
# - string.codepoint_ords() iterates over numeric values of code points in string.
|
||||
# - string.elems() iterates over 1-element (UTF-k code) substrings.
|
||||
# - string.elem_ords() iterates over numeric UTF-k code values.
|
||||
# - string.elem_ords()[i] returns numeric value of ith element (UTF-k code).
|
||||
# - string.elems()[i] returns substring of a single element (UTF-k code).
|
||||
# - int(string) parses string as decimal (or other) numeric literal.
|
||||
#
|
||||
# number to string:
|
||||
# - chr(int) returns string, UTF-k encoding of Unicode code point (like Python).
|
||||
# Redundant with '%c' % int (which Python2 calls 'unichr'.)
|
||||
# - bytes(chr(int)) returns byte string containing UTF-8 encoding of one code point.
|
||||
# - bytes([int]) returns 1-byte string (with regrettable list allocation).
|
||||
# - str(int) - format number as decimal.
|
||||
Vendored
+64
@@ -0,0 +1,64 @@
|
||||
# Tests of Starlark control flow
|
||||
|
||||
load("assert.star", "assert")
|
||||
|
||||
def controlflow():
|
||||
# elif
|
||||
x = 0
|
||||
if True:
|
||||
x=1
|
||||
elif False:
|
||||
assert.fail("else of true")
|
||||
else:
|
||||
assert.fail("else of else of true")
|
||||
assert.true(x)
|
||||
|
||||
x = 0
|
||||
if False:
|
||||
assert.fail("then of false")
|
||||
elif True:
|
||||
x = 1
|
||||
else:
|
||||
assert.fail("else of true")
|
||||
assert.true(x)
|
||||
|
||||
x = 0
|
||||
if False:
|
||||
assert.fail("then of false")
|
||||
elif False:
|
||||
assert.fail("then of false")
|
||||
else:
|
||||
x = 1
|
||||
assert.true(x)
|
||||
controlflow()
|
||||
|
||||
def loops():
|
||||
y = ""
|
||||
for x in [1, 2, 3, 4, 5]:
|
||||
if x == 2:
|
||||
continue
|
||||
if x == 4:
|
||||
break
|
||||
y = y + str(x)
|
||||
return y
|
||||
assert.eq(loops(), "13")
|
||||
|
||||
# return
|
||||
g = 123
|
||||
def f(x):
|
||||
for g in (1, 2, 3):
|
||||
if g == x:
|
||||
return g
|
||||
assert.eq(f(2), 2)
|
||||
assert.eq(f(4), None) # falling off end => return None
|
||||
assert.eq(g, 123) # unchanged by local use of g in function
|
||||
|
||||
# infinite sequences
|
||||
def fib(n):
|
||||
seq = []
|
||||
for x in fibonacci: # fibonacci is an infinite iterable defined in eval_test.go
|
||||
if len(seq) == n:
|
||||
break
|
||||
seq.append(x)
|
||||
return seq
|
||||
assert.eq(fib(10), [0, 1, 1, 2, 3, 5, 8, 13, 21, 34])
|
||||
+321
@@ -0,0 +1,321 @@
|
||||
# Tests of Starlark 'dict'
|
||||
|
||||
load("assert.star", "assert", "freeze")
|
||||
|
||||
# literals
|
||||
assert.eq({}, {})
|
||||
assert.eq({"a": 1}, {"a": 1})
|
||||
assert.eq({"a": 1,}, {"a": 1})
|
||||
|
||||
# truth
|
||||
assert.true({False: False})
|
||||
assert.true(not {})
|
||||
|
||||
# dict + dict is no longer supported.
|
||||
assert.fails(lambda: {"a": 1} + {"b": 2}, 'unknown binary op: dict \\+ dict')
|
||||
|
||||
# dict comprehension
|
||||
assert.eq({x: x*x for x in range(3)}, {0: 0, 1: 1, 2: 4})
|
||||
|
||||
# dict.pop
|
||||
x6 = {"a": 1, "b": 2}
|
||||
assert.eq(x6.pop("a"), 1)
|
||||
assert.eq(str(x6), '{"b": 2}')
|
||||
assert.fails(lambda: x6.pop("c"), "pop: missing key")
|
||||
assert.eq(x6.pop("c", 3), 3)
|
||||
assert.eq(x6.pop("c", None), None) # default=None tests an edge case of UnpackArgs
|
||||
assert.eq(x6.pop("b"), 2)
|
||||
assert.eq(len(x6), 0)
|
||||
|
||||
# dict.popitem
|
||||
x7 = {"a": 1, "b": 2}
|
||||
assert.eq([x7.popitem(), x7.popitem()], [("a", 1), ("b", 2)])
|
||||
assert.fails(x7.popitem, "empty dict")
|
||||
assert.eq(len(x7), 0)
|
||||
|
||||
# dict.keys, dict.values
|
||||
x8 = {"a": 1, "b": 2}
|
||||
assert.eq(x8.keys(), ["a", "b"])
|
||||
assert.eq(x8.values(), [1, 2])
|
||||
|
||||
# equality
|
||||
assert.eq({"a": 1, "b": 2}, {"a": 1, "b": 2})
|
||||
assert.eq({"a": 1, "b": 2,}, {"a": 1, "b": 2})
|
||||
assert.eq({"a": 1, "b": 2}, {"b": 2, "a": 1})
|
||||
|
||||
# insertion order is preserved
|
||||
assert.eq(dict([("a", 0), ("b", 1), ("c", 2), ("b", 3)]).keys(), ["a", "b", "c"])
|
||||
assert.eq(dict([("b", 0), ("a", 1), ("b", 2), ("c", 3)]).keys(), ["b", "a", "c"])
|
||||
assert.eq(dict([("b", 0), ("a", 1), ("b", 2), ("c", 3)])["b"], 2)
|
||||
# ...even after rehashing (which currently occurs after key 'i'):
|
||||
small = dict([("a", 0), ("b", 1), ("c", 2)])
|
||||
small.update([("d", 4), ("e", 5), ("f", 6), ("g", 7), ("h", 8), ("i", 9), ("j", 10), ("k", 11)])
|
||||
assert.eq(small.keys(), ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k"])
|
||||
|
||||
# Duplicate keys are not permitted in dictionary expressions (see b/35698444).
|
||||
# (Nor in keyword args to function calls---checked by resolver.)
|
||||
assert.fails(lambda: {"aa": 1, "bb": 2, "cc": 3, "bb": 4}, 'duplicate key: "bb"')
|
||||
|
||||
# Check that even with many positional args, keyword collisions are detected.
|
||||
assert.fails(lambda: dict({'b': 3}, a=4, **dict(a=5)), 'dict: duplicate keyword arg: "a"')
|
||||
assert.fails(lambda: dict({'a': 2, 'b': 3}, a=4, **dict(a=5)), 'dict: duplicate keyword arg: "a"')
|
||||
# positional/keyword arg key collisions are ok
|
||||
assert.eq(dict((['a', 2], ), a=4), {'a': 4})
|
||||
assert.eq(dict((['a', 2], ['a', 3]), a=4), {'a': 4})
|
||||
|
||||
# index
|
||||
def setIndex(d, k, v):
|
||||
d[k] = v
|
||||
|
||||
x9 = {}
|
||||
assert.fails(lambda: x9["a"], 'key "a" not in dict')
|
||||
x9["a"] = 1
|
||||
assert.eq(x9["a"], 1)
|
||||
assert.eq(x9, {"a": 1})
|
||||
assert.fails(lambda: setIndex(x9, [], 2), 'unhashable type: list')
|
||||
freeze(x9)
|
||||
assert.fails(lambda: setIndex(x9, "a", 3), 'cannot insert into frozen hash table')
|
||||
|
||||
x9a = {}
|
||||
x9a[1, 2] = 3 # unparenthesized tuple is allowed here
|
||||
assert.eq(x9a.keys()[0], (1, 2))
|
||||
|
||||
# dict.get
|
||||
x10 = {"a": 1}
|
||||
assert.eq(x10.get("a"), 1)
|
||||
assert.eq(x10.get("b"), None)
|
||||
assert.eq(x10.get("a", 2), 1)
|
||||
assert.eq(x10.get("b", 2), 2)
|
||||
|
||||
# dict.clear
|
||||
x11 = {"a": 1}
|
||||
assert.contains(x11, "a")
|
||||
assert.eq(x11["a"], 1)
|
||||
x11.clear()
|
||||
assert.fails(lambda: x11["a"], 'key "a" not in dict')
|
||||
assert.true("a" not in x11)
|
||||
freeze(x11)
|
||||
assert.fails(x11.clear, "cannot clear frozen hash table")
|
||||
|
||||
# dict.setdefault
|
||||
x12 = {"a": 1}
|
||||
assert.eq(x12.setdefault("a"), 1)
|
||||
assert.eq(x12["a"], 1)
|
||||
assert.eq(x12.setdefault("b"), None)
|
||||
assert.eq(x12["b"], None)
|
||||
assert.eq(x12.setdefault("c", 2), 2)
|
||||
assert.eq(x12["c"], 2)
|
||||
assert.eq(x12.setdefault("c", 3), 2)
|
||||
assert.eq(x12["c"], 2)
|
||||
freeze(x12)
|
||||
assert.eq(x12.setdefault("a", 1), 1) # no change, no error
|
||||
assert.fails(lambda: x12.setdefault("d", 1), "cannot insert into frozen hash table")
|
||||
|
||||
# dict.update
|
||||
x13 = {"a": 1}
|
||||
x13.update(a=2, b=3)
|
||||
assert.eq(x13, {"a": 2, "b": 3})
|
||||
x13.update([("b", 4), ("c", 5)])
|
||||
assert.eq(x13, {"a": 2, "b": 4, "c": 5})
|
||||
x13.update({"c": 6, "d": 7})
|
||||
assert.eq(x13, {"a": 2, "b": 4, "c": 6, "d": 7})
|
||||
freeze(x13)
|
||||
assert.fails(lambda: x13.update({"a": 8}), "cannot insert into frozen hash table")
|
||||
|
||||
# dict as a sequence
|
||||
#
|
||||
# for loop
|
||||
x14 = {1:2, 3:4}
|
||||
def keys(dict):
|
||||
keys = []
|
||||
for k in dict: keys.append(k)
|
||||
return keys
|
||||
assert.eq(keys(x14), [1, 3])
|
||||
#
|
||||
# comprehension
|
||||
assert.eq([x for x in x14], [1, 3])
|
||||
#
|
||||
# varargs
|
||||
def varargs(*args): return args
|
||||
x15 = {"one": 1}
|
||||
assert.eq(varargs(*x15), ("one",))
|
||||
|
||||
# kwargs parameter does not alias the **kwargs dict
|
||||
def kwargs(**kwargs): return kwargs
|
||||
x16 = kwargs(**x15)
|
||||
assert.eq(x16, x15)
|
||||
x15["two"] = 2 # mutate
|
||||
assert.ne(x16, x15)
|
||||
|
||||
# iterator invalidation
|
||||
def iterator1():
|
||||
dict = {1:1, 2:1}
|
||||
for k in dict:
|
||||
dict[2*k] = dict[k]
|
||||
assert.fails(iterator1, "insert.*during iteration")
|
||||
|
||||
def iterator2():
|
||||
dict = {1:1, 2:1}
|
||||
for k in dict:
|
||||
dict.pop(k)
|
||||
assert.fails(iterator2, "delete.*during iteration")
|
||||
|
||||
def iterator3():
|
||||
def f(d):
|
||||
d[3] = 3
|
||||
dict = {1:1, 2:1}
|
||||
_ = [f(dict) for x in dict]
|
||||
assert.fails(iterator3, "insert.*during iteration")
|
||||
|
||||
# This assignment is not a modification-during-iteration:
|
||||
# the sequence x should be completely iterated before
|
||||
# the assignment occurs.
|
||||
def f():
|
||||
x = {1:2, 2:4}
|
||||
a, x[0] = x
|
||||
assert.eq(a, 1)
|
||||
assert.eq(x, {1: 2, 2: 4, 0: 2})
|
||||
f()
|
||||
|
||||
# Regression test for a bug in hashtable.delete
|
||||
def test_delete():
|
||||
d = {}
|
||||
|
||||
# delete tail first
|
||||
d["one"] = 1
|
||||
d["two"] = 2
|
||||
assert.eq(str(d), '{"one": 1, "two": 2}')
|
||||
d.pop("two")
|
||||
assert.eq(str(d), '{"one": 1}')
|
||||
d.pop("one")
|
||||
assert.eq(str(d), '{}')
|
||||
|
||||
# delete head first
|
||||
d["one"] = 1
|
||||
d["two"] = 2
|
||||
assert.eq(str(d), '{"one": 1, "two": 2}')
|
||||
d.pop("one")
|
||||
assert.eq(str(d), '{"two": 2}')
|
||||
d.pop("two")
|
||||
assert.eq(str(d), '{}')
|
||||
|
||||
# delete middle
|
||||
d["one"] = 1
|
||||
d["two"] = 2
|
||||
d["three"] = 3
|
||||
assert.eq(str(d), '{"one": 1, "two": 2, "three": 3}')
|
||||
d.pop("two")
|
||||
assert.eq(str(d), '{"one": 1, "three": 3}')
|
||||
d.pop("three")
|
||||
assert.eq(str(d), '{"one": 1}')
|
||||
d.pop("one")
|
||||
assert.eq(str(d), '{}')
|
||||
|
||||
test_delete()
|
||||
|
||||
# Regression test for github.com/google/starlark-go/issues/128.
|
||||
assert.fails(lambda: dict(None), 'got NoneType, want iterable')
|
||||
assert.fails(lambda: {}.update(None), 'got NoneType, want iterable')
|
||||
|
||||
---
|
||||
# Verify position of an "unhashable key" error in a dict literal.
|
||||
|
||||
_ = {
|
||||
"one": 1,
|
||||
["two"]: 2, ### "unhashable type: list"
|
||||
"three": 3,
|
||||
}
|
||||
|
||||
---
|
||||
# Verify position of a "duplicate key" error in a dict literal.
|
||||
|
||||
_ = {
|
||||
"one": 1,
|
||||
"one": 1, ### `duplicate key: "one"`
|
||||
"three": 3,
|
||||
}
|
||||
|
||||
---
|
||||
# Verify position of an "unhashable key" error in a dict comprehension.
|
||||
|
||||
_ = {
|
||||
k: v ### "unhashable type: list"
|
||||
for k, v in [
|
||||
("one", 1),
|
||||
(["two"], 2),
|
||||
("three", 3),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
---
|
||||
# dict | dict (union)
|
||||
|
||||
load("assert.star", "assert", "freeze")
|
||||
|
||||
empty_dict = dict()
|
||||
dict_with_a_b = dict(a=1, b=[1, 2])
|
||||
dict_with_b = dict(b=[1, 2])
|
||||
dict_with_other_b = dict(b=[3, 4])
|
||||
|
||||
assert.eq(empty_dict | dict_with_a_b, dict_with_a_b)
|
||||
# Verify iteration order.
|
||||
assert.eq((empty_dict | dict_with_a_b).items(), dict_with_a_b.items())
|
||||
assert.eq(dict_with_a_b | empty_dict, dict_with_a_b)
|
||||
assert.eq((dict_with_a_b | empty_dict).items(), dict_with_a_b.items())
|
||||
assert.eq(dict_with_b | dict_with_a_b, dict_with_a_b)
|
||||
assert.eq((dict_with_b | dict_with_a_b).items(), dict(b=[1, 2], a=1).items())
|
||||
assert.eq(dict_with_a_b | dict_with_b, dict_with_a_b)
|
||||
assert.eq((dict_with_a_b | dict_with_b).items(), dict_with_a_b.items())
|
||||
assert.eq(dict_with_b | dict_with_other_b, dict_with_other_b)
|
||||
assert.eq((dict_with_b | dict_with_other_b).items(), dict_with_other_b.items())
|
||||
assert.eq(dict_with_other_b | dict_with_b, dict_with_b)
|
||||
assert.eq((dict_with_other_b | dict_with_b).items(), dict_with_b.items())
|
||||
|
||||
assert.eq(empty_dict, dict())
|
||||
assert.eq(dict_with_b, dict(b=[1,2]))
|
||||
|
||||
assert.fails(lambda: dict() | [], "unknown binary op: dict [|] list")
|
||||
|
||||
# dict |= dict (in-place union)
|
||||
|
||||
def test_dict_union_assignment():
|
||||
x = dict()
|
||||
saved = x
|
||||
x |= {"a": 1}
|
||||
x |= {"b": 2}
|
||||
x |= {"c": "3", 7: 4}
|
||||
x |= {"b": "5", "e": 6}
|
||||
want = {"a": 1, "b": "5", "c": "3", 7: 4, "e": 6}
|
||||
assert.eq(x, want)
|
||||
assert.eq(x.items(), want.items())
|
||||
assert.eq(saved, x) # they are aliases
|
||||
|
||||
a = {8: 1, "b": 2}
|
||||
b = {"b": 1, "c": 6}
|
||||
c = {"d": 7}
|
||||
d = {(5, "a"): ("c", 8)}
|
||||
orig_a, orig_c = a, c
|
||||
a |= b
|
||||
c |= a
|
||||
c |= d
|
||||
expected_2 = {"d": 7, 8: 1, "b": 1, "c": 6, (5, "a"): ("c", 8)}
|
||||
assert.eq(c, expected_2)
|
||||
assert.eq(c.items(), expected_2.items())
|
||||
assert.eq(b, {"b": 1, "c": 6})
|
||||
|
||||
# aliasing:
|
||||
assert.eq(a, orig_a)
|
||||
assert.eq(c, orig_c)
|
||||
a.clear()
|
||||
c.clear()
|
||||
assert.eq(a, orig_a)
|
||||
assert.eq(c, orig_c)
|
||||
|
||||
test_dict_union_assignment()
|
||||
|
||||
def dict_union_assignment_type_mismatch():
|
||||
some_dict = dict()
|
||||
some_dict |= []
|
||||
|
||||
assert.fails(dict_union_assignment_type_mismatch, "unknown binary op: dict [|] list")
|
||||
+508
@@ -0,0 +1,508 @@
|
||||
# Tests of Starlark 'float'
|
||||
# option:set
|
||||
|
||||
load("assert.star", "assert")
|
||||
|
||||
# TODO(adonovan): more tests:
|
||||
# - 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")
|
||||
assert.eq(type(1e+10), "float")
|
||||
assert.eq(type(1e-10), "float")
|
||||
assert.eq(type(1.234e10), "float")
|
||||
assert.eq(type(1.234e+10), "float")
|
||||
assert.eq(type(1.234e-10), "float")
|
||||
|
||||
# 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)
|
||||
|
||||
# Regression test for https://github.com/google/starlark-go/issues/375.
|
||||
maxint64 = (1<<63)-1
|
||||
assert.eq(int(float(maxint64)), 9223372036854775808)
|
||||
|
||||
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)
|
||||
assert.eq(1.0 + 1.0, 2.0)
|
||||
assert.eq(1.25 + 2.75, 4.0)
|
||||
assert.eq(5.0 + 7.0, 12.0)
|
||||
assert.eq(5.1 + 7, 12.1) # float + int
|
||||
assert.eq(7 + 5.1, 12.1) # int + float
|
||||
|
||||
# subtraction
|
||||
assert.eq(5.0 - 7.0, -2.0)
|
||||
assert.eq(5.1 - 7.1, -2.0)
|
||||
assert.eq(5.5 - 7, -1.5)
|
||||
assert.eq(5 - 7.5, -2.5)
|
||||
assert.eq(0.0 - 1.0, -1.0)
|
||||
|
||||
# multiplication
|
||||
assert.eq(5.0 * 7.0, 35.0)
|
||||
assert.eq(5.5 * 2.5, 13.75)
|
||||
assert.eq(5.5 * 7, 38.5)
|
||||
assert.eq(5 * 7.1, 35.5)
|
||||
|
||||
# real division (like Python 3)
|
||||
# The / operator is available only when the 'fp' dialect option is enabled.
|
||||
assert.eq(100.0 / 8.0, 12.5)
|
||||
assert.eq(100.0 / -8.0, -12.5)
|
||||
assert.eq(-100.0 / 8.0, -12.5)
|
||||
assert.eq(-100.0 / -8.0, 12.5)
|
||||
assert.eq(98.0 / 8.0, 12.25)
|
||||
assert.eq(98.0 / -8.0, -12.25)
|
||||
assert.eq(-98.0 / 8.0, -12.25)
|
||||
assert.eq(-98.0 / -8.0, 12.25)
|
||||
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, "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)
|
||||
assert.eq(100.0 // -8.0, -13.0)
|
||||
assert.eq(-100.0 // 8.0, -13.0)
|
||||
assert.eq(-100.0 // -8.0, 12.0)
|
||||
assert.eq(98.0 // 8.0, 12.0)
|
||||
assert.eq(98.0 // -8.0, -13.0)
|
||||
assert.eq(-98.0 // 8.0, -13.0)
|
||||
assert.eq(-98.0 // -8.0, 12.0)
|
||||
assert.eq(2.5 // 2.0, 1.0)
|
||||
assert.eq(2.5 // 2, 1.0)
|
||||
assert.eq(5 // 4.0, 1.0)
|
||||
assert.eq(5 // 4, 1)
|
||||
assert.eq(type(5 // 4), "int")
|
||||
assert.fails(lambda: 1.0 // 0, "floored division by zero")
|
||||
assert.fails(lambda: 1.0 // 0.0, "floored division by zero")
|
||||
assert.fails(lambda: 1 // 0.0, "floored division by zero")
|
||||
|
||||
# remainder
|
||||
assert.eq(100.0 % 8.0, 4.0)
|
||||
assert.eq(100.0 % -8.0, -4.0)
|
||||
assert.eq(-100.0 % 8.0, 4.0)
|
||||
assert.eq(-100.0 % -8.0, -4.0)
|
||||
assert.eq(98.0 % 8.0, 2.0)
|
||||
assert.eq(98.0 % -8.0, -6.0)
|
||||
assert.eq(-98.0 % 8.0, 6.0)
|
||||
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, "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")
|
||||
|
||||
# -- 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
|
||||
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)
|
||||
# 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
|
||||
assert.lt(-inf, -fltmax)
|
||||
assert.lt(-fltmax, -1.0)
|
||||
assert.lt(-1.0, -fltmin)
|
||||
assert.lt(-fltmin, 0.0)
|
||||
assert.lt(0, fltmin)
|
||||
assert.lt(fltmin, 1.0)
|
||||
assert.lt(1.0, fltmax)
|
||||
assert.lt(fltmax, inf)
|
||||
|
||||
# int/float comparisons
|
||||
assert.eq(0, 0.0)
|
||||
assert.eq(1, 1.0)
|
||||
assert.eq(-1, -1.0)
|
||||
assert.ne(-1, -1.0 + 1e-7)
|
||||
assert.lt(-2, -2 + 1e-15)
|
||||
|
||||
# int conversion (rounds towards zero)
|
||||
assert.eq(int(100.1), 100)
|
||||
assert.eq(int(100.0), 100)
|
||||
assert.eq(int(99.9), 99)
|
||||
assert.eq(int(-99.9), -99)
|
||||
assert.eq(int(-100.0), -100)
|
||||
assert.eq(int(-100.1), -100)
|
||||
assert.eq(int(1e100), int("10000000000000000159028911097599180468360808563945281389781327557747838772170381060813469985856815104"))
|
||||
assert.fails(lambda: int(inf), "cannot convert.*infinity")
|
||||
assert.fails(lambda: int(nan), "cannot convert.*NaN")
|
||||
|
||||
# -- 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.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.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.
|
||||
def checkhash():
|
||||
for a in [1.23e100, 1.23e10, 1.23e1, 1.23,
|
||||
1, 4294967295, 8589934591, 9223372036854775807]:
|
||||
for b in [a, -a, 1/a, -1/a]:
|
||||
f = float(b)
|
||||
i = int(b)
|
||||
if f == i:
|
||||
fh = {f: None}
|
||||
ih = {i: None}
|
||||
if fh != ih:
|
||||
assert.true(False, "{%v: None} != {%v: None}: hashes vary" % fh, ih)
|
||||
checkhash()
|
||||
|
||||
# string formatting
|
||||
|
||||
# %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("%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
|
||||
f0 = 1.0
|
||||
assert.eq(type(i0), "int")
|
||||
assert.eq(type(f0), "float")
|
||||
|
||||
ops = {
|
||||
'+': lambda x, y: x + y,
|
||||
'-': lambda x, y: x - y,
|
||||
'*': lambda x, y: x * y,
|
||||
'/': lambda x, y: x / y,
|
||||
'//': lambda x, y: x // y,
|
||||
'%': lambda x, y: x % y,
|
||||
}
|
||||
|
||||
# Check that if either argument is a float, so too is the result.
|
||||
def checktypes():
|
||||
want = set("""
|
||||
int + int = int
|
||||
int + float = float
|
||||
float + int = float
|
||||
float + float = float
|
||||
int - int = int
|
||||
int - float = float
|
||||
float - int = float
|
||||
float - float = float
|
||||
int * int = int
|
||||
int * float = float
|
||||
float * int = float
|
||||
float * float = float
|
||||
int / int = float
|
||||
int / float = float
|
||||
float / int = float
|
||||
float / float = float
|
||||
int // int = int
|
||||
int // float = float
|
||||
float // int = float
|
||||
float // float = float
|
||||
int % int = int
|
||||
int % float = float
|
||||
float % int = float
|
||||
float % float = float
|
||||
"""[1:].splitlines())
|
||||
for opname in ("+", "-", "*", "/", "%"):
|
||||
for x in [i0, f0]:
|
||||
for y in [i0, f0]:
|
||||
op = ops[opname]
|
||||
got = "%s %s %s = %s" % (type(x), opname, type(y), type(op(x, y)))
|
||||
assert.contains(want, got)
|
||||
checktypes()
|
||||
Vendored
+329
@@ -0,0 +1,329 @@
|
||||
# Tests of Starlark 'function'
|
||||
# option:set
|
||||
|
||||
# TODO(adonovan):
|
||||
# - add some introspection functions for looking at function values
|
||||
# and test that functions have correct position, free vars, names of locals, etc.
|
||||
# - move the hard-coded tests of parameter passing from eval_test.go to here.
|
||||
|
||||
load("assert.star", "assert", "freeze")
|
||||
|
||||
# Test lexical scope and closures:
|
||||
def outer(x):
|
||||
def inner(y):
|
||||
return x + x + y # multiple occurrences of x should create only 1 freevar
|
||||
return inner
|
||||
|
||||
z = outer(3)
|
||||
assert.eq(z(5), 11)
|
||||
assert.eq(z(7), 13)
|
||||
z2 = outer(4)
|
||||
assert.eq(z2(5), 13)
|
||||
assert.eq(z2(7), 15)
|
||||
assert.eq(z(5), 11)
|
||||
assert.eq(z(7), 13)
|
||||
|
||||
# Function name
|
||||
assert.eq(str(outer), '<function outer>')
|
||||
assert.eq(str(z), '<function inner>')
|
||||
assert.eq(str(str), '<built-in function str>')
|
||||
assert.eq(str("".startswith), '<built-in method startswith of string value>')
|
||||
|
||||
# Stateful closure
|
||||
def squares():
|
||||
x = [0]
|
||||
def f():
|
||||
x[0] += 1
|
||||
return x[0] * x[0]
|
||||
return f
|
||||
|
||||
sq = squares()
|
||||
assert.eq(sq(), 1)
|
||||
assert.eq(sq(), 4)
|
||||
assert.eq(sq(), 9)
|
||||
assert.eq(sq(), 16)
|
||||
|
||||
# Freezing a closure
|
||||
sq2 = freeze(sq)
|
||||
assert.fails(sq2, "frozen list")
|
||||
|
||||
# recursion detection, simple
|
||||
def fib(x):
|
||||
if x < 2:
|
||||
return x
|
||||
return fib(x-2) + fib(x-1)
|
||||
assert.fails(lambda: fib(10), "function fib called recursively")
|
||||
|
||||
# recursion detection, advanced
|
||||
#
|
||||
# A simplistic recursion check that looks for repeated calls to the
|
||||
# same function value will not detect recursion using the Y
|
||||
# combinator, which creates a new closure at each step of the
|
||||
# recursion. To truly prohibit recursion, the dynamic check must look
|
||||
# for repeated calls of the same syntactic function body.
|
||||
Y = lambda f: (lambda x: x(x))(lambda y: f(lambda *args: y(y)(*args)))
|
||||
fibgen = lambda fib: lambda x: (x if x<2 else fib(x-1)+fib(x-2))
|
||||
fib2 = Y(fibgen)
|
||||
assert.fails(lambda: [fib2(x) for x in range(10)], "function lambda called recursively")
|
||||
|
||||
# However, this stricter check outlaws many useful programs
|
||||
# that are still bounded, and creates a hazard because
|
||||
# helper functions such as map below cannot be used to
|
||||
# call functions that themselves use map:
|
||||
def map(f, seq): return [f(x) for x in seq]
|
||||
def double(x): return x+x
|
||||
assert.eq(map(double, [1, 2, 3]), [2, 4, 6])
|
||||
assert.eq(map(double, ["a", "b", "c"]), ["aa", "bb", "cc"])
|
||||
def mapdouble(x): return map(double, x)
|
||||
assert.fails(lambda: map(mapdouble, ([1, 2, 3], ["a", "b", "c"])),
|
||||
'function map called recursively')
|
||||
# With the -recursion option it would yield [[2, 4, 6], ["aa", "bb", "cc"]].
|
||||
|
||||
# call of function not through its name
|
||||
# (regression test for parsing suffixes of primary expressions)
|
||||
hf = hasfields()
|
||||
hf.x = [len]
|
||||
assert.eq(hf.x[0]("abc"), 3)
|
||||
def f():
|
||||
return lambda: 1
|
||||
assert.eq(f()(), 1)
|
||||
assert.eq(["abc"][0][0].upper(), "A")
|
||||
|
||||
# functions may be recursively defined,
|
||||
# so long as they don't dynamically recur.
|
||||
calls = []
|
||||
def yin(x):
|
||||
calls.append("yin")
|
||||
if x:
|
||||
yang(False)
|
||||
|
||||
def yang(x):
|
||||
calls.append("yang")
|
||||
if x:
|
||||
yin(False)
|
||||
|
||||
yin(True)
|
||||
assert.eq(calls, ["yin", "yang"])
|
||||
|
||||
calls.clear()
|
||||
yang(True)
|
||||
assert.eq(calls, ["yang", "yin"])
|
||||
|
||||
|
||||
# builtin_function_or_method use identity equivalence.
|
||||
closures = set(["".count for _ in range(10)])
|
||||
assert.eq(len(closures), 10)
|
||||
|
||||
---
|
||||
# Default values of function parameters are mutable.
|
||||
load("assert.star", "assert", "freeze")
|
||||
|
||||
def f(x=[0]):
|
||||
return x
|
||||
|
||||
assert.eq(f(), [0])
|
||||
|
||||
f().append(1)
|
||||
assert.eq(f(), [0, 1])
|
||||
|
||||
# Freezing a function value freezes its parameter defaults.
|
||||
freeze(f)
|
||||
assert.fails(lambda: f().append(2), "cannot append to frozen list")
|
||||
|
||||
---
|
||||
# This is a well known corner case of parsing in Python.
|
||||
load("assert.star", "assert")
|
||||
|
||||
f = lambda x: 1 if x else 0
|
||||
assert.eq(f(True), 1)
|
||||
assert.eq(f(False), 0)
|
||||
|
||||
x = True
|
||||
f2 = (lambda x: 1) if x else 0
|
||||
assert.eq(f2(123), 1)
|
||||
|
||||
tf = lambda: True, lambda: False
|
||||
assert.true(tf[0]())
|
||||
assert.true(not tf[1]())
|
||||
|
||||
---
|
||||
# Missing parameters are correctly reported
|
||||
# in functions of more than 64 parameters.
|
||||
# (This tests a corner case of the implementation:
|
||||
# we avoid a map allocation for <64 parameters)
|
||||
|
||||
load("assert.star", "assert")
|
||||
|
||||
def f(a, b, c, d, e, f, g, h,
|
||||
i, j, k, l, m, n, o, p,
|
||||
q, r, s, t, u, v, w, x,
|
||||
y, z, A, B, C, D, E, F,
|
||||
G, H, I, J, K, L, M, N,
|
||||
O, P, Q, R, S, T, U, V,
|
||||
W, X, Y, Z, aa, bb, cc, dd,
|
||||
ee, ff, gg, hh, ii, jj, kk, ll,
|
||||
mm):
|
||||
pass
|
||||
|
||||
assert.fails(lambda: f(
|
||||
1, 2, 3, 4, 5, 6, 7, 8,
|
||||
9, 10, 11, 12, 13, 14, 15, 16,
|
||||
17, 18, 19, 20, 21, 22, 23, 24,
|
||||
25, 26, 27, 28, 29, 30, 31, 32,
|
||||
33, 34, 35, 36, 37, 38, 39, 40,
|
||||
41, 42, 43, 44, 45, 46, 47, 48,
|
||||
49, 50, 51, 52, 53, 54, 55, 56,
|
||||
57, 58, 59, 60, 61, 62, 63, 64), "missing 1 argument \\(mm\\)")
|
||||
|
||||
assert.fails(lambda: f(
|
||||
1, 2, 3, 4, 5, 6, 7, 8,
|
||||
9, 10, 11, 12, 13, 14, 15, 16,
|
||||
17, 18, 19, 20, 21, 22, 23, 24,
|
||||
25, 26, 27, 28, 29, 30, 31, 32,
|
||||
33, 34, 35, 36, 37, 38, 39, 40,
|
||||
41, 42, 43, 44, 45, 46, 47, 48,
|
||||
49, 50, 51, 52, 53, 54, 55, 56,
|
||||
57, 58, 59, 60, 61, 62, 63, 64, 65,
|
||||
mm = 100), 'multiple values for parameter "mm"')
|
||||
|
||||
---
|
||||
# Regression test for github.com/google/starlark-go/issues/21,
|
||||
# which concerns dynamic checks.
|
||||
# Related: https://github.com/bazelbuild/starlark/issues/21,
|
||||
# which concerns static checks.
|
||||
|
||||
load("assert.star", "assert")
|
||||
|
||||
def f(*args, **kwargs):
|
||||
return args, kwargs
|
||||
|
||||
assert.eq(f(x=1, y=2), ((), {"x": 1, "y": 2}))
|
||||
assert.fails(lambda: f(x=1, **dict(x=2)), 'multiple values for parameter "x"')
|
||||
|
||||
def g(x, y):
|
||||
return x, y
|
||||
|
||||
assert.eq(g(1, y=2), (1, 2))
|
||||
assert.fails(lambda: g(1, y=2, **{'y': 3}), 'multiple values for parameter "y"')
|
||||
|
||||
---
|
||||
# Regression test for a bug in CALL_VAR_KW.
|
||||
|
||||
load("assert.star", "assert")
|
||||
|
||||
def f(a, b, x, y):
|
||||
return a+b+x+y
|
||||
|
||||
assert.eq(f(*("a", "b"), **dict(y="y", x="x")) + ".", 'abxy.')
|
||||
---
|
||||
# Order of evaluation of function arguments.
|
||||
# Regression test for github.com/google/skylark/issues/135.
|
||||
load("assert.star", "assert")
|
||||
|
||||
r = []
|
||||
|
||||
def id(x):
|
||||
r.append(x)
|
||||
return x
|
||||
|
||||
def f(*args, **kwargs):
|
||||
return (args, kwargs)
|
||||
|
||||
y = f(id(1), id(2), x=id(3), *[id(4)], **dict(z=id(5)))
|
||||
assert.eq(y, ((1, 2, 4), dict(x=3, z=5)))
|
||||
|
||||
# This matches Python2 and Starlark-in-Java, but not Python3 [1 2 4 3 6].
|
||||
# *args and *kwargs are evaluated last.
|
||||
# (Python[23] also allows keyword arguments after *args.)
|
||||
# See github.com/bazelbuild/starlark#13 for spec change.
|
||||
assert.eq(r, [1, 2, 3, 4, 5])
|
||||
|
||||
---
|
||||
# option:recursion
|
||||
# See github.com/bazelbuild/starlark#170
|
||||
load("assert.star", "assert")
|
||||
|
||||
def a():
|
||||
list = []
|
||||
def b(n):
|
||||
list.append(n)
|
||||
if n > 0:
|
||||
b(n - 1) # recursive reference to b
|
||||
|
||||
b(3)
|
||||
return list
|
||||
|
||||
assert.eq(a(), [3, 2, 1, 0])
|
||||
|
||||
def c():
|
||||
list = []
|
||||
x = 1
|
||||
def d():
|
||||
list.append(x) # this use of x observes both assignments
|
||||
d()
|
||||
x = 2
|
||||
d()
|
||||
return list
|
||||
|
||||
assert.eq(c(), [1, 2])
|
||||
|
||||
def e():
|
||||
def f():
|
||||
return x # forward reference ok: x is a closure cell
|
||||
x = 1
|
||||
return f()
|
||||
|
||||
assert.eq(e(), 1)
|
||||
|
||||
---
|
||||
load("assert.star", "assert")
|
||||
|
||||
def e():
|
||||
x = 1
|
||||
def f():
|
||||
print(x) # this reference to x fails
|
||||
x = 3 # because this assignment makes x local to f
|
||||
f()
|
||||
|
||||
assert.fails(e, "local variable x referenced before assignment")
|
||||
|
||||
def f():
|
||||
def inner():
|
||||
return x
|
||||
if False:
|
||||
x = 0
|
||||
return x # fails (x is an uninitialized cell of this function)
|
||||
|
||||
assert.fails(f, "local variable x referenced before assignment")
|
||||
|
||||
def g():
|
||||
def inner():
|
||||
return x # fails (x is an uninitialized cell of the enclosing function)
|
||||
if False:
|
||||
x = 0
|
||||
return inner()
|
||||
|
||||
assert.fails(g, "local variable x referenced before assignment")
|
||||
|
||||
---
|
||||
# A trailing comma is allowed in any function definition or call.
|
||||
# This reduces the need to edit neighboring lines when editing defs
|
||||
# or calls splayed across multiple lines.
|
||||
|
||||
def a(x,): pass
|
||||
def b(x, y=None, ): pass
|
||||
def c(x, y=None, *args, ): pass
|
||||
def d(x, y=None, *args, z=None, ): pass
|
||||
def e(x, y=None, *args, z=None, **kwargs, ): pass
|
||||
|
||||
a(1,)
|
||||
b(1, y=2, )
|
||||
#c(1, *[], )
|
||||
#d(1, *[], z=None, )
|
||||
#e(1, *[], z=None, *{}, )
|
||||
|
||||
---
|
||||
# Unpack provides spell check for argument names.
|
||||
load("assert.star", "assert")
|
||||
|
||||
assert.fails(lambda: min([], keg=1), ".+did you mean key\\?")
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
# Functions used in starlark_test.TestParamDefault().
|
||||
|
||||
def all_required(a, b, c): pass
|
||||
def all_opt(a="a", b=None, c=""): pass
|
||||
def mix_required_opt(a, b, c="c", d="d"): pass
|
||||
def with_varargs(a, b="b", *args): pass
|
||||
def with_varargs_kwonly(a, b="b", *args, c="c", d): pass
|
||||
def with_kwonly(a, b="b", *, c="c", d): pass
|
||||
def with_kwargs(a, b="b", c="c", **kwargs): pass
|
||||
def with_varargs_kwonly_kwargs(a, b="b", *args, c="c", d, e="e", **kwargs): pass
|
||||
+258
@@ -0,0 +1,258 @@
|
||||
# Tests of Starlark 'int'
|
||||
|
||||
load("assert.star", "assert")
|
||||
|
||||
# basic arithmetic
|
||||
assert.eq(0 - 1, -1)
|
||||
assert.eq(0 + 1, +1)
|
||||
assert.eq(1 + 1, 2)
|
||||
assert.eq(5 + 7, 12)
|
||||
assert.eq(5 * 7, 35)
|
||||
assert.eq(5 - 7, -2)
|
||||
|
||||
# int boundaries
|
||||
maxint64 = (1 << 63) - 1
|
||||
minint64 = -1 << 63
|
||||
maxint32 = (1 << 31) - 1
|
||||
minint32 = -1 << 31
|
||||
assert.eq(maxint64, 9223372036854775807)
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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()
|
||||
|
||||
# int conversion
|
||||
# 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.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(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)
|
||||
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"), 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"), 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("123", 8), 83)
|
||||
assert.eq(int("-123", 8), -83)
|
||||
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)
|
||||
assert.eq(int("12", 16), 18)
|
||||
assert.eq(int("-12", 16), -18)
|
||||
assert.eq(int("0x12", 16), 18)
|
||||
assert.eq(int("-0x12", 16), -18)
|
||||
assert.eq(0x1000000000000001 * 0x1000000000000001, 0x1000000000000002000000000000001)
|
||||
assert.eq(int("1010", 2), 10)
|
||||
assert.eq(int("111111101", 2), 509)
|
||||
assert.eq(int("0b0101", 0), 5)
|
||||
assert.eq(int("0b0101", 2), 5) # prefix is redundant with explicit base
|
||||
assert.eq(int("0b00000", 0), 0)
|
||||
assert.eq(1111111111111111 * 1111111111111111, 1234567901234567654320987654321)
|
||||
assert.fails(lambda: int("0x123", 8), "invalid literal.*base 8")
|
||||
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")
|
||||
|
||||
# Base prefix is honored only if base=0, or if the prefix matches the explicit base.
|
||||
# See https://github.com/google/starlark-go/issues/337
|
||||
assert.fails(lambda: int("0b0"), "invalid literal.*base 10")
|
||||
assert.eq(int("0b0", 0), 0)
|
||||
assert.eq(int("0b0", 2), 0)
|
||||
assert.eq(int("0b0", 16), 0xb0)
|
||||
assert.eq(int("0x0b0", 16), 0xb0)
|
||||
assert.eq(int("0x0b0", 0), 0xb0)
|
||||
assert.eq(int("0x0b0101", 16), 0x0b0101)
|
||||
|
||||
# int from string, auto detect base
|
||||
assert.eq(int("123", 0), 123)
|
||||
assert.eq(int("+123", 0), +123)
|
||||
assert.eq(int("-123", 0), -123)
|
||||
assert.eq(int("0x12", 0), 18)
|
||||
assert.eq(int("+0x12", 0), +18)
|
||||
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")
|
||||
|
||||
# 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")
|
||||
assert.fails(lambda: int("+-4"), "invalid literal with base 10: \\+-4")
|
||||
assert.fails(lambda: int("0x-4", 16), "invalid literal with base 16: 0x-4")
|
||||
|
||||
# bitwise union (int|int), intersection (int&int), XOR (int^int), unary not (~int),
|
||||
# left shift (int<<int), and right shift (int>>int).
|
||||
# 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(2 ^ 2, 0)
|
||||
assert.eq(1 | 0 ^ 1, 1) # check | and ^ operators precedence
|
||||
assert.eq(~1, -2)
|
||||
assert.eq(~(-2), 1)
|
||||
assert.eq(~0, -1)
|
||||
assert.eq(1 << 2, 4)
|
||||
assert.eq(2 >> 1, 1)
|
||||
assert.fails(lambda: 2 << -1, "negative shift count")
|
||||
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)
|
||||
|
||||
comparisons()
|
||||
|
||||
# precision
|
||||
assert.eq(str(maxint64), "9223372036854775807")
|
||||
assert.eq(str(maxint64 + 1), "9223372036854775808")
|
||||
assert.eq(str(minint64), "-9223372036854775808")
|
||||
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(minint32 * minint32), "4611686018427387904")
|
||||
assert.eq(str(minint32 | maxint32), "-1")
|
||||
assert.eq(str(minint32 & minint32), "-2147483648")
|
||||
assert.eq(str(minint32 ^ maxint32), "-1")
|
||||
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("%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.fails(lambda: "%d" % True, "cannot convert bool to int")
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
# Tests of json module.
|
||||
|
||||
load("assert.star", "assert")
|
||||
load("json.star", "json")
|
||||
|
||||
assert.eq(dir(json), ["decode", "encode", "indent"])
|
||||
|
||||
# Some of these cases were inspired by github.com/nst/JSONTestSuite.
|
||||
|
||||
## json.encode
|
||||
|
||||
assert.eq(json.encode(None), "null")
|
||||
assert.eq(json.encode(True), "true")
|
||||
assert.eq(json.encode(False), "false")
|
||||
assert.eq(json.encode(-123), "-123")
|
||||
assert.eq(json.encode(12345*12345*12345*12345*12345*12345), "3539537889086624823140625")
|
||||
assert.eq(json.encode(float(12345*12345*12345*12345*12345*12345)), "3.539537889086625e+24")
|
||||
assert.eq(json.encode(12.345e67), "1.2345e+68")
|
||||
assert.eq(json.encode("hello"), '"hello"')
|
||||
assert.eq(json.encode([1, 2, 3]), "[1,2,3]")
|
||||
assert.eq(json.encode((1, 2, 3)), "[1,2,3]")
|
||||
assert.eq(json.encode(range(3)), "[0,1,2]") # a built-in iterable
|
||||
assert.eq(json.encode(dict(x = 1, y = "two")), '{"x":1,"y":"two"}')
|
||||
assert.eq(json.encode(dict(y = "two", x = 1)), '{"x":1,"y":"two"}') # key, not insertion, order
|
||||
assert.eq(json.encode(struct(x = 1, y = "two")), '{"x":1,"y":"two"}') # a user-defined HasAttrs
|
||||
assert.eq(json.encode("😹"[:1]), '"\\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({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
|
||||
'in field .x: at list index 1: in dict key "x": cannot encode...')
|
||||
encode_error(struct(x=[1, {"x": len}]), # nested failure
|
||||
'in field .x: at list index 1: in dict key "x": cannot encode...')
|
||||
encode_error({1: 2}, 'dict has int key, want string')
|
||||
|
||||
recursive_map = {}
|
||||
recursive_map["r"] = recursive_map
|
||||
encode_error(recursive_map, 'json.encode: in dict key "r": cycle in JSON structure')
|
||||
|
||||
recursive_list = []
|
||||
recursive_list.append(recursive_list)
|
||||
encode_error(recursive_list, 'json.encode: at list index 0: cycle in JSON structure')
|
||||
|
||||
recursive_tuple = (1, 2, [])
|
||||
recursive_tuple[2].append(recursive_tuple)
|
||||
encode_error(recursive_tuple, 'json.encode: at tuple index 2: at list index 0: cycle in JSON structure')
|
||||
|
||||
## json.decode
|
||||
|
||||
assert.eq(json.decode("null"), None)
|
||||
assert.eq(json.decode("true"), True)
|
||||
assert.eq(json.decode("false"), False)
|
||||
assert.eq(json.decode("-123"), -123)
|
||||
assert.eq(json.decode("-0"), -0)
|
||||
assert.eq(json.decode("3539537889086624823140625"), 3539537889086624823140625)
|
||||
assert.eq(json.decode("3539537889086624823140625.0"), float(3539537889086624823140625))
|
||||
assert.eq(json.decode("3.539537889086625e+24"), 3.539537889086625e+24)
|
||||
assert.eq(json.decode("0e+1"), 0)
|
||||
assert.eq(json.decode("-0.0"), -0.0)
|
||||
assert.eq(json.decode(
|
||||
"-0.000000000000000000000000000000000000000000000000000000000000000000000000000001"),
|
||||
-0.000000000000000000000000000000000000000000000000000000000000000000000000000001)
|
||||
assert.eq(json.decode('[]'), [])
|
||||
assert.eq(json.decode('[1]'), [1])
|
||||
assert.eq(json.decode('[1,2,3]'), [1, 2, 3])
|
||||
assert.eq(json.decode('{"one": 1, "two": 2}'), dict(one=1, two=2))
|
||||
assert.eq(json.decode('{"foo\\u0000bar": 42}'), {"foo\x00bar": 42})
|
||||
assert.eq(json.decode('"\\ud83d\\ude39\\ud83d\\udc8d"'), "😹💍")
|
||||
assert.eq(json.decode('"\\u0123"'), 'ģ')
|
||||
assert.eq(json.decode('"\x7f"'), "\x7f")
|
||||
|
||||
def decode_error(expr, error):
|
||||
assert.fails(lambda: json.decode(expr), error)
|
||||
|
||||
decode_error('truefalse',
|
||||
"json.decode: at offset 4, unexpected character 'f' after value")
|
||||
|
||||
decode_error('"abc', "unclosed string literal")
|
||||
decode_error('"ab\\gc"', "invalid character 'g' in string escape code")
|
||||
decode_error("'abc'", "unexpected character '\\\\''")
|
||||
|
||||
decode_error("1.2.3", "invalid number: 1.2.3")
|
||||
decode_error("+1", "unexpected character '\\+'")
|
||||
decode_error("-abc", "invalid number: -")
|
||||
decode_error("-", "invalid number: -")
|
||||
decode_error("-00", "invalid number: -00")
|
||||
decode_error("00", "invalid number: 00")
|
||||
decode_error("--1", "invalid number: --1")
|
||||
decode_error("-+1", "invalid number: -\\+1")
|
||||
decode_error("1e1e1", "invalid number: 1e1e1")
|
||||
decode_error("0123", "invalid number: 0123")
|
||||
decode_error("000.123", "invalid number: 000.123")
|
||||
decode_error("-0123", "invalid number: -0123")
|
||||
decode_error("-000.123", "invalid number: -000.123")
|
||||
decode_error("0x123", "unexpected character 'x' after value")
|
||||
|
||||
decode_error('[1, 2 ', "unexpected end of file")
|
||||
decode_error('[1, 2, ', "unexpected end of file")
|
||||
decode_error('[1, 2, ]', "unexpected character ']'")
|
||||
decode_error('[1, 2, }', "unexpected character '}'")
|
||||
decode_error('[1, 2}', "got '}', want ',' or ']'")
|
||||
|
||||
decode_error('{"one": 1', "unexpected end of file")
|
||||
decode_error('{"one" 1', "after object key, got '1', want ':'")
|
||||
decode_error('{"one": 1 "two": 2', "in object, got '\"', want ',' or '}'")
|
||||
decode_error('{"one": 1,', "unexpected end of file")
|
||||
decode_error('{"one": 1, }', "unexpected character '}'")
|
||||
decode_error('{"one": 1]', "in object, got ']', want ',' or '}'")
|
||||
|
||||
## json.decode with default specified
|
||||
|
||||
assert.eq(json.decode('{"valid": "json"}', default = "default value"), {"valid": "json"})
|
||||
assert.eq(json.decode('{"valid": "json"}', "default value"), {"valid": "json"})
|
||||
assert.eq(json.decode('{"invalid": "json"', default = "default value"), "default value")
|
||||
assert.eq(json.decode('{"invalid": "json"', "default value"), "default value")
|
||||
assert.eq(json.decode('{"invalid": "json"', default = None), None)
|
||||
assert.eq(json.decode('{"invalid": "json"', None), None)
|
||||
|
||||
assert.fails(
|
||||
lambda: json.decode(x = '{"invalid": "json"', default = "default value"),
|
||||
"unexpected keyword argument x"
|
||||
)
|
||||
|
||||
def codec(x):
|
||||
return json.decode(json.encode(x))
|
||||
|
||||
# string round-tripping
|
||||
strings = [
|
||||
"😿", # U+1F63F CRYING_CAT_FACE
|
||||
"🐱👤", # CAT FACE + ZERO WIDTH JOINER + BUST IN SILHOUETTE
|
||||
]
|
||||
assert.eq(codec(strings), strings)
|
||||
|
||||
# codepoints is a string with every 16-bit code point.
|
||||
codepoints = ''.join(['%c' % c for c in range(65536)])
|
||||
assert.eq(codec(codepoints), codepoints)
|
||||
|
||||
# number round-tripping
|
||||
numbers = [
|
||||
0, 1, -1, +1, 1.23e45, -1.23e-45,
|
||||
3539537889086624823140625,
|
||||
float(3539537889086624823140625),
|
||||
]
|
||||
assert.eq(codec(numbers), numbers)
|
||||
|
||||
## json.indent
|
||||
|
||||
s = json.encode(dict(x = 1, y = ["one", "two"]))
|
||||
|
||||
assert.eq(json.indent(s), '''{
|
||||
"x": 1,
|
||||
"y": [
|
||||
"one",
|
||||
"two"
|
||||
]
|
||||
}''')
|
||||
|
||||
assert.eq(json.decode(json.indent(s)), {"x": 1, "y": ["one", "two"]})
|
||||
|
||||
assert.eq(json.indent(s, prefix='¶', indent='–––'), '''{
|
||||
¶–––"x": 1,
|
||||
¶–––"y": [
|
||||
¶––––––"one",
|
||||
¶––––––"two"
|
||||
¶–––]
|
||||
¶}''')
|
||||
|
||||
assert.fails(lambda: json.indent("!@#$%^& this is not json"), 'invalid character')
|
||||
---
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
# Tests of Starlark 'list'
|
||||
|
||||
load("assert.star", "assert", "freeze")
|
||||
|
||||
# literals
|
||||
assert.eq([], [])
|
||||
assert.eq([1], [1])
|
||||
assert.eq([1], [1])
|
||||
assert.eq([1, 2], [1, 2])
|
||||
assert.ne([1, 2, 3], [1, 2, 4])
|
||||
|
||||
# truth
|
||||
assert.true([0])
|
||||
assert.true(not [])
|
||||
|
||||
# indexing, x[i]
|
||||
abc = list("abc".elems())
|
||||
assert.fails(lambda: abc[-4], "list index -4 out of range \\[-3:2]")
|
||||
assert.eq(abc[-3], "a")
|
||||
assert.eq(abc[-2], "b")
|
||||
assert.eq(abc[-1], "c")
|
||||
assert.eq(abc[0], "a")
|
||||
assert.eq(abc[1], "b")
|
||||
assert.eq(abc[2], "c")
|
||||
assert.fails(lambda: abc[3], "list index 3 out of range \\[-3:2]")
|
||||
|
||||
# x[i] = ...
|
||||
x3 = [0, 1, 2]
|
||||
x3[1] = 2
|
||||
x3[2] += 3
|
||||
assert.eq(x3, [0, 2, 5])
|
||||
|
||||
def f2():
|
||||
x3[3] = 4
|
||||
|
||||
assert.fails(f2, "out of range")
|
||||
freeze(x3)
|
||||
|
||||
def f3():
|
||||
x3[0] = 0
|
||||
|
||||
assert.fails(f3, "cannot assign to element of frozen list")
|
||||
assert.fails(x3.clear, "cannot clear frozen list")
|
||||
|
||||
# list + list
|
||||
assert.eq([1, 2, 3] + [3, 4, 5], [1, 2, 3, 3, 4, 5])
|
||||
assert.fails(lambda: [1, 2] + (3, 4), "unknown.*list \\+ tuple")
|
||||
assert.fails(lambda: (1, 2) + [3, 4], "unknown.*tuple \\+ list")
|
||||
|
||||
# list * int, int * list
|
||||
assert.eq(abc * 0, [])
|
||||
assert.eq(abc * -1, [])
|
||||
assert.eq(abc * 1, abc)
|
||||
assert.eq(abc * 3, ["a", "b", "c", "a", "b", "c", "a", "b", "c"])
|
||||
assert.eq(0 * abc, [])
|
||||
assert.eq(-1 * abc, [])
|
||||
assert.eq(1 * abc, abc)
|
||||
assert.eq(3 * abc, ["a", "b", "c", "a", "b", "c", "a", "b", "c"])
|
||||
|
||||
# list comprehensions
|
||||
assert.eq([2 * x for x in [1, 2, 3]], [2, 4, 6])
|
||||
assert.eq([2 * x for x in [1, 2, 3] if x > 1], [4, 6])
|
||||
assert.eq(
|
||||
[(x, y) for x in [1, 2] for y in [3, 4]],
|
||||
[(1, 3), (1, 4), (2, 3), (2, 4)],
|
||||
)
|
||||
assert.eq([(x, y) for x in [1, 2] if x == 2 for y in [3, 4]], [(2, 3), (2, 4)])
|
||||
assert.eq([2 * x for x in (1, 2, 3)], [2, 4, 6])
|
||||
assert.eq([x for x in "abc".elems()], ["a", "b", "c"])
|
||||
assert.eq([x for x in {"a": 1, "b": 2}], ["a", "b"])
|
||||
assert.eq([(y, x) for x, y in {1: 2, 3: 4}.items()], [(2, 1), (4, 3)])
|
||||
|
||||
# corner cases of parsing:
|
||||
assert.eq([x for x in range(12) if x % 2 == 0 if x % 3 == 0], [0, 6])
|
||||
assert.eq([x for x in [1, 2] if lambda: None], [1, 2])
|
||||
assert.eq([x for x in [1, 2] if (lambda: 3 if True else 4)], [1, 2])
|
||||
|
||||
# list function
|
||||
assert.eq(list(), [])
|
||||
assert.eq(list("ab".elems()), ["a", "b"])
|
||||
|
||||
# A list comprehension defines a separate lexical block,
|
||||
# whether at top-level...
|
||||
a = [1, 2]
|
||||
b = [a for a in [3, 4]]
|
||||
assert.eq(a, [1, 2])
|
||||
assert.eq(b, [3, 4])
|
||||
|
||||
# ...or local to a function.
|
||||
def listcompblock():
|
||||
c = [1, 2]
|
||||
d = [c for c in [3, 4]]
|
||||
assert.eq(c, [1, 2])
|
||||
assert.eq(d, [3, 4])
|
||||
|
||||
listcompblock()
|
||||
|
||||
# list.pop
|
||||
x4 = [1, 2, 3, 4, 5]
|
||||
assert.fails(lambda: x4.pop(-6), "index -6 out of range \\[-5:4]")
|
||||
assert.fails(lambda: x4.pop(6), "index 6 out of range \\[-5:4]")
|
||||
assert.eq(x4.pop(), 5)
|
||||
assert.eq(x4, [1, 2, 3, 4])
|
||||
assert.eq(x4.pop(1), 2)
|
||||
assert.eq(x4, [1, 3, 4])
|
||||
assert.eq(x4.pop(0), 1)
|
||||
assert.eq(x4, [3, 4])
|
||||
assert.eq(x4.pop(-2), 3)
|
||||
assert.eq(x4, [4])
|
||||
assert.eq(x4.pop(-1), 4)
|
||||
assert.eq(x4, [])
|
||||
|
||||
# TODO(adonovan): test uses of list as sequence
|
||||
# (for loop, comprehension, library functions).
|
||||
|
||||
# x += y for lists is equivalent to x.extend(y).
|
||||
# y may be a sequence.
|
||||
# TODO: Test that side-effects of 'x' occur only once.
|
||||
def list_extend():
|
||||
a = [1, 2, 3]
|
||||
b = a
|
||||
a = a + [4] # creates a new list
|
||||
assert.eq(a, [1, 2, 3, 4])
|
||||
assert.eq(b, [1, 2, 3]) # b is unchanged
|
||||
|
||||
a = [1, 2, 3]
|
||||
b = a
|
||||
a += [4] # updates a (and thus b) in place
|
||||
assert.eq(a, [1, 2, 3, 4])
|
||||
assert.eq(b, [1, 2, 3, 4]) # alias observes the change
|
||||
|
||||
a = [1, 2, 3]
|
||||
b = a
|
||||
a.extend([4]) # updates existing list
|
||||
assert.eq(a, [1, 2, 3, 4])
|
||||
assert.eq(b, [1, 2, 3, 4]) # alias observes the change
|
||||
|
||||
list_extend()
|
||||
|
||||
# Unlike list.extend(iterable), list += iterable makes its LHS name local.
|
||||
a_list = []
|
||||
|
||||
def f4():
|
||||
a_list += [1] # binding use => a_list is a local var
|
||||
|
||||
assert.fails(f4, "local variable a_list referenced before assignment")
|
||||
|
||||
# list += <not iterable>
|
||||
def f5():
|
||||
x = []
|
||||
x += 1
|
||||
|
||||
assert.fails(f5, "unknown binary op: list \\+ int")
|
||||
|
||||
# frozen list += iterable
|
||||
def f6():
|
||||
x = []
|
||||
freeze(x)
|
||||
x += [1]
|
||||
|
||||
assert.fails(f6, "cannot apply \\+= to frozen list")
|
||||
|
||||
# list += hasfields (hasfields is not iterable but defines list+hasfields)
|
||||
def f7():
|
||||
x = []
|
||||
x += hasfields()
|
||||
return x
|
||||
|
||||
assert.eq(f7(), 42) # weird, but exercises a corner case in list+=x.
|
||||
|
||||
# append
|
||||
x5 = [1, 2, 3]
|
||||
x5.append(4)
|
||||
x5.append("abc")
|
||||
assert.eq(x5, [1, 2, 3, 4, "abc"])
|
||||
|
||||
# extend
|
||||
x5a = [1, 2, 3]
|
||||
x5a.extend("abc".elems()) # string
|
||||
x5a.extend((True, False)) # tuple
|
||||
assert.eq(x5a, [1, 2, 3, "a", "b", "c", True, False])
|
||||
|
||||
# list.insert
|
||||
def insert_at(index):
|
||||
x = list(range(3))
|
||||
x.insert(index, 42)
|
||||
return x
|
||||
|
||||
assert.eq(insert_at(-99), [42, 0, 1, 2])
|
||||
assert.eq(insert_at(-2), [0, 42, 1, 2])
|
||||
assert.eq(insert_at(-1), [0, 1, 42, 2])
|
||||
assert.eq(insert_at(0), [42, 0, 1, 2])
|
||||
assert.eq(insert_at(1), [0, 42, 1, 2])
|
||||
assert.eq(insert_at(2), [0, 1, 42, 2])
|
||||
assert.eq(insert_at(3), [0, 1, 2, 42])
|
||||
assert.eq(insert_at(4), [0, 1, 2, 42])
|
||||
|
||||
# list.remove
|
||||
def remove(v):
|
||||
x = [3, 1, 4, 1]
|
||||
x.remove(v)
|
||||
return x
|
||||
|
||||
assert.eq(remove(3), [1, 4, 1])
|
||||
assert.eq(remove(1), [3, 4, 1])
|
||||
assert.eq(remove(4), [3, 1, 1])
|
||||
assert.fails(lambda: [3, 1, 4, 1].remove(42), "remove: element not found")
|
||||
|
||||
# list.index
|
||||
bananas = list("bananas".elems())
|
||||
assert.eq(bananas.index("a"), 1) # bAnanas
|
||||
assert.fails(lambda: bananas.index("d"), "value not in list")
|
||||
|
||||
# start
|
||||
assert.eq(bananas.index("a", -1000), 1) # bAnanas
|
||||
assert.eq(bananas.index("a", 0), 1) # bAnanas
|
||||
assert.eq(bananas.index("a", 1), 1) # bAnanas
|
||||
assert.eq(bananas.index("a", 2), 3) # banAnas
|
||||
assert.eq(bananas.index("a", 3), 3) # banAnas
|
||||
assert.eq(bananas.index("b", 0), 0) # Bananas
|
||||
assert.eq(bananas.index("n", -3), 4) # banaNas
|
||||
assert.fails(lambda: bananas.index("n", -2), "value not in list")
|
||||
assert.eq(bananas.index("s", -2), 6) # bananaS
|
||||
assert.fails(lambda: bananas.index("b", 1), "value not in list")
|
||||
|
||||
# start, end
|
||||
assert.eq(bananas.index("s", -1000, 7), 6) # bananaS
|
||||
assert.fails(lambda: bananas.index("s", -1000, 6), "value not in list")
|
||||
assert.fails(lambda: bananas.index("d", -1000, 1000), "value not in list")
|
||||
|
||||
# slicing, x[i:j:k]
|
||||
assert.eq(bananas[6::-2], list("snnb".elems()))
|
||||
assert.eq(bananas[5::-2], list("aaa".elems()))
|
||||
assert.eq(bananas[4::-2], list("nnb".elems()))
|
||||
assert.eq(bananas[99::-2], list("snnb".elems()))
|
||||
assert.eq(bananas[100::-2], list("snnb".elems()))
|
||||
# TODO(adonovan): many more tests
|
||||
|
||||
# iterator invalidation
|
||||
def iterator1():
|
||||
list = [0, 1, 2]
|
||||
for x in list:
|
||||
list[x] = 2 * x
|
||||
return list
|
||||
|
||||
assert.fails(iterator1, "assign to element.* during iteration")
|
||||
|
||||
def iterator2():
|
||||
list = [0, 1, 2]
|
||||
for x in list:
|
||||
list.remove(x)
|
||||
|
||||
assert.fails(iterator2, "remove.*during iteration")
|
||||
|
||||
def iterator3():
|
||||
list = [0, 1, 2]
|
||||
for x in list:
|
||||
list.append(3)
|
||||
|
||||
assert.fails(iterator3, "append.*during iteration")
|
||||
|
||||
def iterator4():
|
||||
list = [0, 1, 2]
|
||||
for x in list:
|
||||
list.extend([3, 4])
|
||||
|
||||
assert.fails(iterator4, "extend.*during iteration")
|
||||
|
||||
def iterator5():
|
||||
def f(x):
|
||||
x.append(4)
|
||||
|
||||
list = [1, 2, 3]
|
||||
_ = [f(list) for x in list]
|
||||
|
||||
assert.fails(iterator5, "append.*during iteration")
|
||||
+379
@@ -0,0 +1,379 @@
|
||||
# Tests of math module.
|
||||
|
||||
load('math.star', 'math')
|
||||
load('assert.star', 'assert')
|
||||
|
||||
def near(got, want, threshold):
|
||||
return math.fabs(got-want) < threshold
|
||||
|
||||
inf, nan = float("inf"), float("nan")
|
||||
|
||||
# ceil
|
||||
assert.eq(math.ceil(0.0), 0.0)
|
||||
assert.eq(math.ceil(0.4), 1.0)
|
||||
assert.eq(math.ceil(0.5), 1.0)
|
||||
assert.eq(math.ceil(1.0), 1.0)
|
||||
assert.eq(math.ceil(10.0), 10.0)
|
||||
assert.eq(math.ceil(0), 0.0)
|
||||
assert.eq(math.ceil(1), 1.0)
|
||||
assert.eq(math.ceil(10), 10.0)
|
||||
assert.eq(math.ceil(-0.0), 0.0)
|
||||
assert.eq(math.ceil(-0.4), 0.0)
|
||||
assert.eq(math.ceil(-0.5), 0.0)
|
||||
assert.eq(math.ceil(-1.0), -1.0)
|
||||
assert.eq(math.ceil(-10.0), -10.0)
|
||||
assert.eq(math.ceil(-1), -1.0)
|
||||
assert.eq(math.ceil(-10), -10.0)
|
||||
assert.eq(type(math.ceil(0)), "int")
|
||||
assert.eq(type(math.ceil(0.4)), "int")
|
||||
assert.eq(type(math.ceil(10)), "int")
|
||||
assert.eq(type(math.ceil(-10.0)), "int")
|
||||
assert.eq(type(math.ceil(-0.5)), "int")
|
||||
assert.eq(math.ceil((1<<63) + 0.5), int(float((1<<63) + 1)))
|
||||
assert.fails(
|
||||
lambda: math.ceil(inf), "cannot convert float infinity to integer")
|
||||
assert.fails(
|
||||
lambda: math.ceil(-inf), "cannot convert float infinity to integer")
|
||||
assert.fails(
|
||||
lambda: math.ceil(nan), "cannot convert float NaN to integer")
|
||||
assert.fails(lambda: math.ceil("0"), "got string, want float or int")
|
||||
# fabs
|
||||
assert.eq(math.fabs(2.0), 2.0)
|
||||
assert.eq(math.fabs(0.0), 0.0)
|
||||
assert.eq(math.fabs(-2.0), 2.0)
|
||||
assert.eq(math.fabs(2), 2)
|
||||
assert.eq(math.fabs(0), 0)
|
||||
assert.eq(math.fabs(-2), 2)
|
||||
assert.eq(math.fabs(inf), inf)
|
||||
assert.eq(math.fabs(-inf), inf)
|
||||
assert.eq(math.fabs(nan), nan)
|
||||
assert.fails(lambda: math.fabs("0"), "got string, want float or int")
|
||||
# floor
|
||||
assert.eq(math.floor(0.0), 0.0)
|
||||
assert.eq(math.floor(0.4), 0.0)
|
||||
assert.eq(math.floor(0.5), 0.0)
|
||||
assert.eq(math.floor(1.0), 1.0)
|
||||
assert.eq(math.floor(10.0), 10.0)
|
||||
assert.eq(math.floor(-0.0), 0.0)
|
||||
assert.eq(math.floor(-0.4), -1.0)
|
||||
assert.eq(math.floor(-0.5), -1.0)
|
||||
assert.eq(math.floor(-1.0), -1.0)
|
||||
assert.eq(math.floor(-10.0), -10.0)
|
||||
assert.eq(type(math.floor(0)), "int")
|
||||
assert.eq(type(math.floor(0.4)), "int")
|
||||
assert.eq(type(math.floor(10)), "int")
|
||||
assert.eq(type(math.floor(-10.0)), "int")
|
||||
assert.eq(type(math.floor(-0.5)), "int")
|
||||
assert.eq(math.floor((1<<63) + 0.5), int(float(1<<63)))
|
||||
assert.fails(
|
||||
lambda: math.floor(inf), "cannot convert float infinity to integer")
|
||||
assert.fails(
|
||||
lambda: math.floor(-inf), "cannot convert float infinity to integer")
|
||||
assert.fails(
|
||||
lambda: math.floor(nan), "cannot convert float NaN to integer")
|
||||
assert.fails(lambda: math.floor("0"), "got string, want float or int")
|
||||
# mod
|
||||
assert.eq(math.mod(5, 3), 2)
|
||||
assert.eq(math.mod(inf, 1), nan)
|
||||
assert.eq(math.mod(-inf, 1.0), nan)
|
||||
assert.eq(math.mod(nan, 1.0), nan)
|
||||
assert.eq(math.mod(1.0, 0.0), nan)
|
||||
assert.eq(math.mod(1.0, inf), 1)
|
||||
assert.eq(math.mod(1.0, -inf), 1)
|
||||
assert.eq(math.mod(1.0, nan), nan)
|
||||
assert.fails(lambda: math.mod("0", 1.0), "got string, want float or int")
|
||||
assert.fails(lambda: math.mod(1.0, "0"), "got string, want float or int")
|
||||
# pow
|
||||
assert.eq(math.pow(5, 3), 125)
|
||||
assert.eq(math.pow(5, 0), 1)
|
||||
assert.eq(math.pow(5, 1), 5)
|
||||
assert.eq(math.pow(1, 5), 1)
|
||||
assert.eq(math.pow(inf, 1), inf)
|
||||
assert.eq(math.pow(-inf, 1.0), -inf)
|
||||
assert.eq(math.pow(nan, 1.0), nan)
|
||||
assert.eq(math.pow(1.1, inf), inf)
|
||||
assert.eq(math.pow(1.1, -inf), 0)
|
||||
assert.eq(math.pow(2.0, nan), nan)
|
||||
assert.fails(lambda: math.pow("0", 1.0), "got string, want float or int")
|
||||
assert.fails(lambda: math.pow(1.0, "0"), "got string, want float or int")
|
||||
# copysign
|
||||
assert.eq(math.copysign(3.2, -1), -3.2)
|
||||
assert.eq(math.copysign(inf, -1.0),-inf)
|
||||
assert.eq(math.copysign(-inf, -1), -inf)
|
||||
assert.eq(math.copysign(nan, -1), nan)
|
||||
assert.eq(math.copysign(-1, nan), 1)
|
||||
assert.fails(lambda: math.copysign("0", 1.0), "got string, want float or int")
|
||||
assert.fails(lambda: math.copysign(1.0, "0"), "got string, want float or int")
|
||||
# remainder
|
||||
assert.eq(math.remainder(3, 5), -2)
|
||||
assert.eq(math.remainder(1, 0), nan)
|
||||
assert.eq(math.remainder(2, inf), 2)
|
||||
assert.eq(math.remainder(2, -inf), 2)
|
||||
assert.eq(math.remainder(inf, -1.0), nan)
|
||||
assert.eq(math.remainder(-inf, -1), nan)
|
||||
assert.eq(math.remainder(nan, -1), nan)
|
||||
assert.eq(math.remainder(-1, nan), nan)
|
||||
assert.fails(lambda: math.remainder("0", 1.0), "got string, want float or int")
|
||||
assert.fails(lambda: math.remainder(1.0, "0"), "got string, want float or int")
|
||||
# round
|
||||
assert.eq(math.round(0.0), 0.0)
|
||||
assert.eq(math.round(0.4), 0.0)
|
||||
assert.eq(math.round(0.5), 1.0)
|
||||
assert.eq(math.round(0.6), 1.0)
|
||||
assert.eq(math.round(1.0), 1.0)
|
||||
assert.eq(math.round(10.0), 10.0)
|
||||
assert.eq(math.round(inf), inf)
|
||||
assert.eq(math.round(nan), nan)
|
||||
assert.eq(math.round(-0.4), 0.0)
|
||||
assert.eq(math.round(-0.5), -1.0)
|
||||
assert.eq(math.round(-0.6), -1.0)
|
||||
assert.eq(math.round(-1.0), -1.0)
|
||||
assert.eq(math.round(-10.0), -10.0)
|
||||
assert.eq(math.round(-inf), -inf)
|
||||
assert.fails(lambda: math.round("0"), "got string, want float or int")
|
||||
# exp
|
||||
assert.eq(math.exp(0.0), 1)
|
||||
assert.eq(math.exp(1.0), math.e)
|
||||
assert.true(near(math.exp(2.0), math.e * math.e, 0.00000000000001))
|
||||
assert.eq(math.exp(-1.0), 1 / math.e)
|
||||
assert.eq(math.exp(0), 1)
|
||||
assert.eq(math.exp(1), math.e)
|
||||
assert.true(near(math.exp(2), math.e * math.e, 0.00000000000001))
|
||||
assert.eq(math.exp(-1), 1 / math.e)
|
||||
assert.eq(math.exp(inf), inf)
|
||||
assert.eq(math.exp(-inf), 0)
|
||||
assert.eq(math.exp(nan), nan)
|
||||
assert.fails(lambda: math.exp("0"), "got string, want float or int")
|
||||
# sqrt
|
||||
assert.eq(math.sqrt(0.0), 0.0)
|
||||
assert.eq(math.sqrt(4.0), 2.0)
|
||||
assert.eq(math.sqrt(-4.0), nan)
|
||||
assert.eq(math.sqrt(0), 0)
|
||||
assert.eq(math.sqrt(4), 2)
|
||||
assert.eq(math.sqrt(-4), nan)
|
||||
assert.eq(math.sqrt(nan), nan)
|
||||
assert.eq(math.sqrt(inf), inf)
|
||||
assert.eq(math.sqrt(-inf), nan)
|
||||
assert.fails(lambda: math.sqrt("0"), "got string, want float or int")
|
||||
# acos
|
||||
assert.eq(math.acos(1.0), 0)
|
||||
assert.eq(math.acos(1), 0)
|
||||
assert.eq(math.acos(0.0), math.pi / 2)
|
||||
assert.eq(math.acos(0), math.pi / 2)
|
||||
assert.eq(math.acos(-1.0), math.pi)
|
||||
assert.eq(math.acos(-1), math.pi)
|
||||
assert.eq(math.acos(1.01), nan)
|
||||
assert.eq(math.acos(-1.01), nan)
|
||||
assert.eq(math.acos(inf), nan)
|
||||
assert.eq(math.acos(-inf), nan)
|
||||
assert.eq(math.acos(nan), nan)
|
||||
assert.fails(lambda: math.acos("0"), "got string, want float or int")
|
||||
# asin
|
||||
assert.eq(math.asin(0.0), 0)
|
||||
assert.eq(math.asin(1.0), math.pi / 2)
|
||||
assert.eq(math.asin(-1.0), -math.pi / 2)
|
||||
assert.eq(math.asin(0), 0)
|
||||
assert.eq(math.asin(1), math.pi / 2)
|
||||
assert.eq(math.asin(-1), -math.pi / 2)
|
||||
assert.eq(math.asin(1.01), nan)
|
||||
assert.eq(math.asin(-1.01), nan)
|
||||
assert.eq(math.asin(inf), nan)
|
||||
assert.eq(math.asin(-inf), nan)
|
||||
assert.eq(math.asin(nan), nan)
|
||||
assert.fails(lambda: math.asin("0"), "got string, want float or int")
|
||||
# atan
|
||||
assert.eq(math.atan(0.0), 0)
|
||||
assert.eq(math.atan(1.0), math.pi / 4)
|
||||
assert.eq(math.atan(-1.0), -math.pi / 4)
|
||||
assert.eq(math.atan(1), math.pi / 4)
|
||||
assert.eq(math.atan(-1), -math.pi / 4)
|
||||
assert.eq(math.atan(inf), math.pi / 2)
|
||||
assert.eq(math.atan(-inf), -math.pi / 2)
|
||||
assert.eq(math.atan(nan), nan)
|
||||
assert.fails(lambda: math.atan("0"), "got string, want float or int")
|
||||
# atan2
|
||||
assert.eq(math.atan2(1.0, 1.0), math.pi / 4)
|
||||
assert.eq(math.atan2(-1.0, 1.0), -math.pi / 4)
|
||||
assert.eq(math.atan2(0.0, 10.0), 0)
|
||||
assert.eq(math.atan2(0.0, -10.0), math.pi)
|
||||
assert.eq(math.atan2(-0.0, -10.0), -math.pi)
|
||||
assert.eq(math.atan2(10.0, 0.0), math.pi / 2)
|
||||
assert.eq(math.atan2(-10.0, 0.0), -math.pi / 2)
|
||||
assert.eq(math.atan2(1, 1), math.pi / 4)
|
||||
assert.eq(math.atan2(-1, 1), -math.pi / 4)
|
||||
assert.eq(math.atan2(0, 10.0), 0)
|
||||
assert.eq(math.atan2(0.0, -10), math.pi)
|
||||
assert.eq(math.atan2(-0.0, -10), -math.pi)
|
||||
assert.eq(math.atan2(10.0, 0), math.pi / 2)
|
||||
assert.eq(math.atan2(-10.0, 0), -math.pi / 2)
|
||||
assert.eq(math.atan2(1.0, nan), nan)
|
||||
assert.eq(math.atan2(nan, 1.0), nan)
|
||||
assert.eq(math.atan2(10.0, inf), 0)
|
||||
assert.eq(math.atan2(-10.0, inf), 0)
|
||||
assert.eq(math.atan2(10.0, -inf), math.pi)
|
||||
assert.eq(math.atan2(-10.0, -inf), -math.pi)
|
||||
assert.eq(math.atan2(inf, 10.0), math.pi / 2)
|
||||
assert.eq(math.atan2(inf, -10.0), math.pi / 2)
|
||||
assert.eq(math.atan2(-inf, 10.0), -math.pi / 2)
|
||||
assert.eq(math.atan2(-inf, -10.0), -math.pi / 2)
|
||||
assert.eq(math.atan2(inf, inf), math.pi / 4)
|
||||
assert.eq(math.atan2(-inf, inf), -math.pi / 4)
|
||||
assert.eq(math.atan2(inf, -inf), 3 * math.pi / 4)
|
||||
assert.eq(math.atan2(-inf, -inf), -3 * math.pi / 4)
|
||||
assert.fails(lambda: math.atan2("0", 1.0), "got string, want float or int")
|
||||
assert.fails(lambda: math.atan2(1.0, "0"), "got string, want float or int")
|
||||
# cos
|
||||
assert.eq(math.cos(0.0), 1)
|
||||
assert.true(near(math.cos(math.pi / 2), 0, 0.00000000000001))
|
||||
assert.eq(math.cos(math.pi), -1)
|
||||
assert.true(near(math.cos(-math.pi / 2), 0, 0.00000000000001))
|
||||
assert.eq(math.cos(-math.pi), -1)
|
||||
assert.eq(math.cos(inf), nan)
|
||||
assert.eq(math.cos(-inf), nan)
|
||||
assert.eq(math.cos(nan), nan)
|
||||
assert.fails(lambda: math.cos("0"), "got string, want float or int")
|
||||
# hypot
|
||||
assert.eq(math.hypot(4.0, 3.0), 5.0)
|
||||
assert.eq(math.hypot(4, 3), 5.0)
|
||||
assert.eq(math.hypot(inf, 3.0), inf)
|
||||
assert.eq(math.hypot(-inf, 3.0), inf)
|
||||
assert.eq(math.hypot(3.0, inf), inf)
|
||||
assert.eq(math.hypot(3.0, -inf), inf)
|
||||
assert.eq(math.hypot(nan, 3.0), nan)
|
||||
assert.eq(math.hypot(3.0, nan), nan)
|
||||
assert.fails(lambda: math.hypot("0", 1.0), "got string, want float or int")
|
||||
assert.fails(lambda: math.hypot(1.0, "0"), "got string, want float or int")
|
||||
# sin
|
||||
assert.eq(math.sin(0.0), 0)
|
||||
assert.eq(math.sin(0), 0)
|
||||
assert.eq(math.sin(math.pi / 2), 1)
|
||||
assert.eq(math.sin(-math.pi / 2), -1)
|
||||
assert.eq(math.sin(inf), nan)
|
||||
assert.eq(math.sin(-inf), nan)
|
||||
assert.eq(math.sin(nan), nan)
|
||||
assert.fails(lambda: math.sin("0"), "got string, want float or int")
|
||||
# tan
|
||||
assert.eq(math.tan(0.0), 0)
|
||||
assert.eq(math.tan(0), 0)
|
||||
assert.true(near(math.tan(math.pi / 4), 1, 0.00000000000001))
|
||||
assert.true(near(math.tan(-math.pi / 4), -1, 0.00000000000001))
|
||||
assert.eq(math.tan(inf), nan)
|
||||
assert.eq(math.tan(-inf), nan)
|
||||
assert.eq(math.tan(nan), nan)
|
||||
assert.fails(lambda: math.tan("0"), "got string, want float or int")
|
||||
# degrees
|
||||
oneDeg = 57.29577951308232
|
||||
assert.eq(math.degrees(1.0), oneDeg)
|
||||
assert.eq(math.degrees(1), oneDeg)
|
||||
assert.eq(math.degrees(-1.0), -oneDeg)
|
||||
assert.eq(math.degrees(-1), -oneDeg)
|
||||
assert.eq(math.degrees(inf), inf)
|
||||
assert.eq(math.degrees(-inf), -inf)
|
||||
assert.eq(math.degrees(nan), nan)
|
||||
assert.fails(lambda: math.degrees("0"), "got string, want float or int")
|
||||
# radians
|
||||
oneRad = 0.017453292519943295
|
||||
assert.eq(math.radians(1.0), oneRad)
|
||||
assert.eq(math.radians(-1.0), -oneRad)
|
||||
assert.eq(math.radians(1), oneRad)
|
||||
assert.eq(math.radians(-1), -oneRad)
|
||||
assert.eq(math.radians(inf), inf)
|
||||
assert.eq(math.radians(-inf), -inf)
|
||||
assert.eq(math.radians(nan), nan)
|
||||
assert.fails(lambda: math.radians("0"), "got string, want float or int")
|
||||
# acosh
|
||||
assert.eq(math.acosh(1.0), 0)
|
||||
assert.eq(math.acosh(1), 0)
|
||||
assert.eq(math.acosh(0.99), nan)
|
||||
assert.eq(math.acosh(0), nan)
|
||||
assert.eq(math.acosh(-0.99), nan)
|
||||
assert.eq(math.acosh(-inf), nan)
|
||||
assert.eq(math.acosh(inf), inf)
|
||||
assert.eq(math.acosh(nan), nan)
|
||||
assert.fails(lambda: math.acosh("0"), "got string, want float or int")
|
||||
# asinh
|
||||
asinhOne = 0.8813735870195432
|
||||
assert.eq(math.asinh(0.0), 0)
|
||||
assert.eq(math.asinh(0), 0)
|
||||
assert.true(near(math.asinh(1.0), asinhOne, 0.00000001))
|
||||
assert.true(near(math.asinh(1), asinhOne, 0.00000001))
|
||||
assert.true(near(math.asinh(-1.0), -asinhOne, 0.00000001))
|
||||
assert.true(near(math.asinh(-1), -asinhOne, 0.00000001))
|
||||
assert.eq(math.asinh(inf), inf)
|
||||
assert.eq(math.asinh(-inf), -inf)
|
||||
assert.eq(math.asinh(nan), nan)
|
||||
assert.fails(lambda: math.asinh("0"), "got string, want float or int")
|
||||
# atanh
|
||||
atanhHalf = 0.5493061443340548
|
||||
assert.eq(math.atanh(0.0), 0)
|
||||
assert.eq(math.atanh(0), 0)
|
||||
assert.eq(math.atanh(0.5), atanhHalf)
|
||||
assert.eq(math.atanh(-0.5), -atanhHalf)
|
||||
assert.eq(math.atanh(1), inf)
|
||||
assert.eq(math.atanh(-1), -inf)
|
||||
assert.eq(math.atanh(1.1), nan)
|
||||
assert.eq(math.atanh(-1.1), nan)
|
||||
assert.eq(math.atanh(inf), nan)
|
||||
assert.eq(math.atanh(-inf), nan)
|
||||
assert.eq(math.atanh(nan), nan)
|
||||
assert.fails(lambda: math.atanh("0"), "got string, want float or int")
|
||||
# cosh
|
||||
coshOne = 1.5430806348152437
|
||||
assert.eq(math.cosh(1.0), coshOne)
|
||||
assert.eq(math.cosh(1), coshOne)
|
||||
assert.eq(math.cosh(0.0), 1)
|
||||
assert.eq(math.cosh(0), 1)
|
||||
assert.eq(math.cosh(-inf), inf)
|
||||
assert.eq(math.cosh(inf), inf)
|
||||
assert.eq(math.cosh(nan), nan)
|
||||
assert.fails(lambda: math.cosh("0"), "got string, want float or int")
|
||||
# sinh
|
||||
sinhOne = 1.1752011936438014
|
||||
assert.eq(math.sinh(0.0), 0)
|
||||
assert.eq(math.sinh(0), 0)
|
||||
assert.eq(math.sinh(1.0), sinhOne)
|
||||
assert.eq(math.sinh(1), sinhOne)
|
||||
assert.eq(math.sinh(-1.0), -sinhOne)
|
||||
assert.eq(math.sinh(-1), -sinhOne)
|
||||
assert.eq(math.sinh(-inf), -inf)
|
||||
assert.eq(math.sinh(inf), inf)
|
||||
assert.eq(math.sinh(nan), nan)
|
||||
assert.fails(lambda: math.sinh("0"), "got string, want float or int")
|
||||
# tanh
|
||||
tanhOne = 0.7615941559557649
|
||||
assert.eq(math.tanh(0.0), 0)
|
||||
assert.eq(math.tanh(0), 0)
|
||||
assert.eq(math.tanh(1.0), tanhOne)
|
||||
assert.eq(math.tanh(1), tanhOne)
|
||||
assert.eq(math.tanh(-1.0), -tanhOne)
|
||||
assert.eq(math.tanh(-1), -tanhOne)
|
||||
assert.eq(math.tanh(-inf), -1)
|
||||
assert.eq(math.tanh(inf), 1)
|
||||
assert.eq(math.tanh(nan), nan)
|
||||
assert.fails(lambda: math.tanh("0"), "got string, want float or int")
|
||||
# log
|
||||
assert.eq(math.log(math.e), 1)
|
||||
assert.eq(math.log(10, 10), 1)
|
||||
assert.eq(math.log(10.0, 10.0), 1)
|
||||
assert.eq(math.log(2, 2.0), 1)
|
||||
assert.fails(lambda: math.log(2, 1), "division by zero")
|
||||
assert.fails(lambda: math.log(0.99, 1.0), "division by zero")
|
||||
assert.eq(math.log(0.0), -inf)
|
||||
assert.eq(math.log(0), -inf)
|
||||
assert.eq(math.log(-1.0), nan)
|
||||
assert.eq(math.log(-1), nan)
|
||||
assert.eq(math.log(nan), nan)
|
||||
assert.fails(lambda: math.log("0"), "got string, want float or int")
|
||||
assert.fails(lambda: math.log(10, "10"), "got string, want float or int")
|
||||
# gamma
|
||||
assert.eq(math.gamma(1.0), 1)
|
||||
assert.eq(math.gamma(1), 1)
|
||||
assert.eq(math.gamma(-1), nan)
|
||||
assert.eq(math.gamma(0), inf)
|
||||
assert.eq(math.gamma(-inf), nan)
|
||||
assert.eq(math.gamma(inf), inf)
|
||||
assert.eq(math.gamma(nan), nan)
|
||||
assert.fails(lambda: math.gamma("0"), "got string, want float or int")
|
||||
# Constants
|
||||
assert.eq(math.e, 2.7182818284590452)
|
||||
assert.eq(math.pi, 3.1415926535897932)
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
# Miscellaneous tests of Starlark evaluation.
|
||||
# This is a "chunked" file: each "---" effectively starts a new file.
|
||||
|
||||
# TODO(adonovan): move these tests into more appropriate files.
|
||||
# TODO(adonovan): test coverage:
|
||||
# - stmts: pass; if cond fail; += and failures;
|
||||
# for x fail; for x not iterable; for can't assign; for
|
||||
# error in loop body
|
||||
# - subassign fail
|
||||
# - x[i]=x fail in both operands; frozen x; list index not int; boundscheck
|
||||
# - x.f = ...
|
||||
# - failure in list expr [...]; tuple expr; dict expr (bad key)
|
||||
# - cond expr semantics; failures
|
||||
# - x[i] failures in both args; dict and iterator key and range checks;
|
||||
# unhandled operand types
|
||||
# - +: list/list, int/int, string/string, tuple+tuple, dict/dict;
|
||||
# - * and ** calls: various errors
|
||||
# - call of non-function
|
||||
# - slice x[ijk]
|
||||
# - comprehension: unhashable dict key;
|
||||
# scope of vars (local and toplevel); noniterable for clause
|
||||
# - unknown unary op
|
||||
# - ordering of values
|
||||
# - freeze, transitivity of its effect.
|
||||
# - add an application-defined type to the environment so we can test it.
|
||||
# - even more:
|
||||
#
|
||||
# eval
|
||||
# pass statement
|
||||
# assign to tuple l-value -- illegal
|
||||
# assign to list l-value -- illegal
|
||||
# assign to field
|
||||
# tuple + tuple
|
||||
# call with *args, **kwargs
|
||||
# slice with step
|
||||
# tuple slice
|
||||
# interpolate with %c, %%
|
||||
|
||||
load("assert.star", "assert")
|
||||
|
||||
# Ordered comparisons require values of the same type.
|
||||
assert.fails(lambda: None < None, "not impl")
|
||||
assert.fails(lambda: None < False, "not impl")
|
||||
assert.fails(lambda: False < list, "not impl")
|
||||
assert.fails(lambda: list < {}, "not impl")
|
||||
assert.fails(lambda: {} < (lambda: None), "not impl")
|
||||
assert.fails(lambda: (lambda: None) < 0, "not impl")
|
||||
assert.fails(lambda: 0 < [], "not impl")
|
||||
assert.fails(lambda: [] < "", "not impl")
|
||||
assert.fails(lambda: "" < (), "not impl")
|
||||
# Except int < float:
|
||||
assert.lt(1, 2.0)
|
||||
assert.lt(2.0, 3)
|
||||
|
||||
---
|
||||
# cyclic data structures
|
||||
load("assert.star", "assert")
|
||||
|
||||
cyclic = [1, 2, 3] # list cycle
|
||||
cyclic[1] = cyclic
|
||||
assert.eq(str(cyclic), "[1, [...], 3]")
|
||||
assert.fails(lambda: cyclic < cyclic, "maximum recursion")
|
||||
assert.fails(lambda: cyclic == cyclic, "maximum recursion")
|
||||
cyclic2 = [1, 2, 3]
|
||||
cyclic2[1] = cyclic2
|
||||
assert.fails(lambda: cyclic2 == cyclic, "maximum recursion")
|
||||
|
||||
cyclic3 = [1, [2, 3]] # list-list cycle
|
||||
cyclic3[1][0] = cyclic3
|
||||
assert.eq(str(cyclic3), "[1, [[...], 3]]")
|
||||
cyclic4 = {"x": 1}
|
||||
cyclic4["x"] = cyclic4
|
||||
assert.eq(str(cyclic4), "{\"x\": {...}}")
|
||||
cyclic5 = [0, {"x": 1}] # list-dict cycle
|
||||
cyclic5[1]["x"] = cyclic5
|
||||
assert.eq(str(cyclic5), "[0, {\"x\": [...]}]")
|
||||
assert.eq(str(cyclic5), "[0, {\"x\": [...]}]")
|
||||
assert.fails(lambda: cyclic5 == cyclic5 ,"maximum recursion")
|
||||
cyclic6 = [0, {"x": 1}]
|
||||
cyclic6[1]["x"] = cyclic6
|
||||
assert.fails(lambda: cyclic5 == cyclic6, "maximum recursion")
|
||||
|
||||
---
|
||||
# regression
|
||||
load("assert.star", "assert")
|
||||
|
||||
# was a parse error:
|
||||
assert.eq(("ababab"[2:]).replace("b", "c"), "acac")
|
||||
assert.eq("ababab"[2:].replace("b", "c"), "acac")
|
||||
|
||||
# test parsing of line continuation, at toplevel and in expression.
|
||||
three = 1 + \
|
||||
2
|
||||
assert.eq(1 + \
|
||||
2, three)
|
||||
|
||||
---
|
||||
# A regression test for error position information.
|
||||
|
||||
_ = {}.get(1, default=2) ### "get: unexpected keyword arguments"
|
||||
|
||||
---
|
||||
# Load exposes explicitly declared globals from other modules.
|
||||
load('assert.star', 'assert', 'freeze')
|
||||
assert.eq(str(freeze), '<built-in function freeze>')
|
||||
|
||||
---
|
||||
# Load does not expose pre-declared globals from other modules.
|
||||
# See github.com/google/skylark/issues/75.
|
||||
load('assert.star', 'assert', 'matches') ### "matches not found in module"
|
||||
|
||||
---
|
||||
# Load does not expose universals accessible in other modules.
|
||||
load('assert.star', 'len') ### "len not found in module"
|
||||
|
||||
|
||||
---
|
||||
# Test plus folding optimization.
|
||||
load('assert.star', 'assert')
|
||||
|
||||
s = "s"
|
||||
l = [4]
|
||||
t = (4,)
|
||||
|
||||
assert.eq("a" + "b" + "c", "abc")
|
||||
assert.eq("a" + "b" + s + "c", "absc")
|
||||
assert.eq(() + (1,) + (2, 3), (1, 2, 3))
|
||||
assert.eq(() + (1,) + t + (2, 3), (1, 4, 2, 3))
|
||||
assert.eq([] + [1] + [2, 3], [1, 2, 3])
|
||||
assert.eq([] + [1] + l + [2, 3], [1, 4, 2, 3])
|
||||
|
||||
assert.fails(lambda: "a" + "b" + 1 + "c", "unknown binary op: string \\+ int")
|
||||
assert.fails(lambda: () + () + 1 + (), "unknown binary op: tuple \\+ int")
|
||||
assert.fails(lambda: [] + [] + 1 + [], "unknown binary op: list \\+ int")
|
||||
|
||||
|
||||
|
||||
---
|
||||
load('assert.star', 'froze') ### `name froze not found .*did you mean freeze`
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
# Tests of Module.
|
||||
|
||||
load("assert.star", "assert")
|
||||
|
||||
assert.eq(type(assert), "module")
|
||||
assert.eq(str(assert), '<module "assert">')
|
||||
assert.eq(dir(assert), ["contains", "eq", "fail", "fails", "lt", "ne", "true"])
|
||||
assert.fails(lambda : {assert: None}, "unhashable: module")
|
||||
|
||||
def assignfield():
|
||||
assert.foo = None
|
||||
|
||||
assert.fails(assignfield, "can't assign to .foo field of module")
|
||||
|
||||
# no such field
|
||||
assert.fails(lambda : assert.nonesuch, "module has no .nonesuch field or method$")
|
||||
assert.fails(lambda : assert.falls, "module has no .falls field or method .did you mean .fails\\?")
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
# Copyright 2017 The Bazel Authors. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Skylib module containing file path manipulation functions.
|
||||
|
||||
NOTE: The functions in this module currently only support paths with Unix-style
|
||||
path separators (forward slash, "/"); they do not handle Windows-style paths
|
||||
with backslash separators or drive letters.
|
||||
"""
|
||||
|
||||
# This file is in the Bazel build language dialect of Starlark,
|
||||
# so declarations of 'fail' and 'struct' are required to make
|
||||
# it compile in the core language.
|
||||
def fail(msg):
|
||||
print(msg)
|
||||
|
||||
struct = dict
|
||||
|
||||
def _basename(p):
|
||||
"""Returns the basename (i.e., the file portion) of a path.
|
||||
|
||||
Note that if `p` ends with a slash, this function returns an empty string.
|
||||
This matches the behavior of Python's `os.path.basename`, but differs from
|
||||
the Unix `basename` command (which would return the path segment preceding
|
||||
the final slash).
|
||||
|
||||
Args:
|
||||
p: The path whose basename should be returned.
|
||||
|
||||
Returns:
|
||||
The basename of the path, which includes the extension.
|
||||
"""
|
||||
return p.rpartition("/")[-1]
|
||||
|
||||
def _dirname(p):
|
||||
"""Returns the dirname of a path.
|
||||
|
||||
The dirname is the portion of `p` up to but not including the file portion
|
||||
(i.e., the basename). Any slashes immediately preceding the basename are not
|
||||
included, unless omitting them would make the dirname empty.
|
||||
|
||||
Args:
|
||||
p: The path whose dirname should be returned.
|
||||
|
||||
Returns:
|
||||
The dirname of the path.
|
||||
"""
|
||||
prefix, sep, _ = p.rpartition("/")
|
||||
if not prefix:
|
||||
return sep
|
||||
else:
|
||||
# If there are multiple consecutive slashes, strip them all out as Python's
|
||||
# os.path.dirname does.
|
||||
return prefix.rstrip("/")
|
||||
|
||||
def _is_absolute(path):
|
||||
"""Returns `True` if `path` is an absolute path.
|
||||
|
||||
Args:
|
||||
path: A path (which is a string).
|
||||
|
||||
Returns:
|
||||
`True` if `path` is an absolute path.
|
||||
"""
|
||||
return path.startswith("/") or (len(path) > 2 and path[1] == ":")
|
||||
|
||||
def _join(path, *others):
|
||||
"""Joins one or more path components intelligently.
|
||||
|
||||
This function mimics the behavior of Python's `os.path.join` function on POSIX
|
||||
platform. It returns the concatenation of `path` and any members of `others`,
|
||||
inserting directory separators before each component except the first. The
|
||||
separator is not inserted if the path up until that point is either empty or
|
||||
already ends in a separator.
|
||||
|
||||
If any component is an absolute path, all previous components are discarded.
|
||||
|
||||
Args:
|
||||
path: A path segment.
|
||||
*others: Additional path segments.
|
||||
|
||||
Returns:
|
||||
A string containing the joined paths.
|
||||
"""
|
||||
result = path
|
||||
|
||||
for p in others:
|
||||
if _is_absolute(p):
|
||||
result = p
|
||||
elif not result or result.endswith("/"):
|
||||
result += p
|
||||
else:
|
||||
result += "/" + p
|
||||
|
||||
return result
|
||||
|
||||
def _normalize(path):
|
||||
"""Normalizes a path, eliminating double slashes and other redundant segments.
|
||||
|
||||
This function mimics the behavior of Python's `os.path.normpath` function on
|
||||
POSIX platforms; specifically:
|
||||
|
||||
- If the entire path is empty, "." is returned.
|
||||
- All "." segments are removed, unless the path consists solely of a single
|
||||
"." segment.
|
||||
- Trailing slashes are removed, unless the path consists solely of slashes.
|
||||
- ".." segments are removed as long as there are corresponding segments
|
||||
earlier in the path to remove; otherwise, they are retained as leading ".."
|
||||
segments.
|
||||
- Single and double leading slashes are preserved, but three or more leading
|
||||
slashes are collapsed into a single leading slash.
|
||||
- Multiple adjacent internal slashes are collapsed into a single slash.
|
||||
|
||||
Args:
|
||||
path: A path.
|
||||
|
||||
Returns:
|
||||
The normalized path.
|
||||
"""
|
||||
if not path:
|
||||
return "."
|
||||
|
||||
if path.startswith("//") and not path.startswith("///"):
|
||||
initial_slashes = 2
|
||||
elif path.startswith("/"):
|
||||
initial_slashes = 1
|
||||
else:
|
||||
initial_slashes = 0
|
||||
is_relative = (initial_slashes == 0)
|
||||
|
||||
components = path.split("/")
|
||||
new_components = []
|
||||
|
||||
for component in components:
|
||||
if component in ("", "."):
|
||||
continue
|
||||
if component == "..":
|
||||
if new_components and new_components[-1] != "..":
|
||||
# Only pop the last segment if it isn't another "..".
|
||||
new_components.pop()
|
||||
elif is_relative:
|
||||
# Preserve leading ".." segments for relative paths.
|
||||
new_components.append(component)
|
||||
else:
|
||||
new_components.append(component)
|
||||
|
||||
path = "/".join(new_components)
|
||||
if not is_relative:
|
||||
path = ("/" * initial_slashes) + path
|
||||
|
||||
return path or "."
|
||||
|
||||
def _relativize(path, start):
|
||||
"""Returns the portion of `path` that is relative to `start`.
|
||||
|
||||
Because we do not have access to the underlying file system, this
|
||||
implementation differs slightly from Python's `os.path.relpath` in that it
|
||||
will fail if `path` is not beneath `start` (rather than use parent segments to
|
||||
walk up to the common file system root).
|
||||
|
||||
Relativizing paths that start with parent directory references only works if
|
||||
the path both start with the same initial parent references.
|
||||
|
||||
Args:
|
||||
path: The path to relativize.
|
||||
start: The ancestor path against which to relativize.
|
||||
|
||||
Returns:
|
||||
The portion of `path` that is relative to `start`.
|
||||
"""
|
||||
segments = _normalize(path).split("/")
|
||||
start_segments = _normalize(start).split("/")
|
||||
if start_segments == ["."]:
|
||||
start_segments = []
|
||||
start_length = len(start_segments)
|
||||
|
||||
if (path.startswith("/") != start.startswith("/") or
|
||||
len(segments) < start_length):
|
||||
fail("Path '%s' is not beneath '%s'" % (path, start))
|
||||
|
||||
for ancestor_segment, segment in zip(start_segments, segments):
|
||||
if ancestor_segment != segment:
|
||||
fail("Path '%s' is not beneath '%s'" % (path, start))
|
||||
|
||||
length = len(segments) - start_length
|
||||
result_segments = segments[-length:]
|
||||
return "/".join(result_segments)
|
||||
|
||||
def _replace_extension(p, new_extension):
|
||||
"""Replaces the extension of the file at the end of a path.
|
||||
|
||||
If the path has no extension, the new extension is added to it.
|
||||
|
||||
Args:
|
||||
p: The path whose extension should be replaced.
|
||||
new_extension: The new extension for the file. The new extension should
|
||||
begin with a dot if you want the new filename to have one.
|
||||
|
||||
Returns:
|
||||
The path with the extension replaced (or added, if it did not have one).
|
||||
"""
|
||||
return _split_extension(p)[0] + new_extension
|
||||
|
||||
def _split_extension(p):
|
||||
"""Splits the path `p` into a tuple containing the root and extension.
|
||||
|
||||
Leading periods on the basename are ignored, so
|
||||
`path.split_extension(".bashrc")` returns `(".bashrc", "")`.
|
||||
|
||||
Args:
|
||||
p: The path whose root and extension should be split.
|
||||
|
||||
Returns:
|
||||
A tuple `(root, ext)` such that the root is the path without the file
|
||||
extension, and `ext` is the file extension (which, if non-empty, contains
|
||||
the leading dot). The returned tuple always satisfies the relationship
|
||||
`root + ext == p`.
|
||||
"""
|
||||
b = _basename(p)
|
||||
last_dot_in_basename = b.rfind(".")
|
||||
|
||||
# If there is no dot or the only dot in the basename is at the front, then
|
||||
# there is no extension.
|
||||
if last_dot_in_basename <= 0:
|
||||
return (p, "")
|
||||
|
||||
dot_distance_from_end = len(b) - last_dot_in_basename
|
||||
return (p[:-dot_distance_from_end], p[-dot_distance_from_end:])
|
||||
|
||||
paths = struct(
|
||||
basename = _basename,
|
||||
dirname = _dirname,
|
||||
is_absolute = _is_absolute,
|
||||
join = _join,
|
||||
normalize = _normalize,
|
||||
relativize = _relativize,
|
||||
replace_extension = _replace_extension,
|
||||
split_extension = _split_extension,
|
||||
)
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
# Tests of the experimental 'lib/proto' module.
|
||||
|
||||
load("assert.star", "assert")
|
||||
load("proto.star", "proto")
|
||||
|
||||
schema = proto.file("google/protobuf/descriptor.proto")
|
||||
|
||||
m = schema.FileDescriptorProto(name = "somename.proto", dependency = ["a", "b", "c"])
|
||||
assert.eq(type(m), "proto.Message")
|
||||
assert.eq(m.name, "somename.proto")
|
||||
assert.eq(list(m.dependency), ["a", "b", "c"])
|
||||
m.dependency = ["d", "e"]
|
||||
assert.eq(list(m.dependency), ["d", "e"])
|
||||
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
# Tests of Starlark recursion and while statement.
|
||||
|
||||
# This is a "chunked" file: each "---" effectively starts a new file.
|
||||
|
||||
# option:recursion
|
||||
|
||||
load("assert.star", "assert")
|
||||
|
||||
def fib(n):
|
||||
if n <= 1:
|
||||
return 1
|
||||
return fib(n-1) + fib(n-2)
|
||||
|
||||
assert.eq(fib(5), 8)
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
# Tests of Starlark 'set'
|
||||
# option:set option:globalreassign
|
||||
|
||||
# Sets are not a standard part of Starlark, so the features
|
||||
# tested in this file must be enabled in the application by setting
|
||||
# resolve.AllowSet. (All sets are created by calls to the 'set'
|
||||
# built-in or derived from operations on existing sets.)
|
||||
# The semantics are subject to change as the spec evolves.
|
||||
|
||||
# TODO(adonovan): support set mutation:
|
||||
# - del set[k]
|
||||
# - set.update
|
||||
# - set += iterable, perhaps?
|
||||
# Test iterator invalidation.
|
||||
|
||||
load("assert.star", "assert", "freeze")
|
||||
|
||||
# literals
|
||||
# Parser does not currently support {1, 2, 3}.
|
||||
# TODO(adonovan): add test to syntax/testdata/errors.star.
|
||||
|
||||
# set comprehensions
|
||||
# Parser does not currently support {x for x in y}.
|
||||
# See syntax/testdata/errors.star.
|
||||
|
||||
# set constructor
|
||||
assert.eq(type(set()), "set")
|
||||
assert.eq(list(set()), [])
|
||||
assert.eq(type(set([1, 3, 2, 3])), "set")
|
||||
assert.eq(list(set([1, 3, 2, 3])), [1, 3, 2])
|
||||
assert.eq(type(set("hello".elems())), "set")
|
||||
assert.eq(list(set("hello".elems())), ["h", "e", "l", "o"])
|
||||
assert.eq(list(set(range(3))), [0, 1, 2])
|
||||
assert.fails(lambda : set(1), "got int, want iterable")
|
||||
assert.fails(lambda : set(1, 2, 3), "got 3 arguments")
|
||||
assert.fails(lambda : set([1, 2, {}]), "unhashable type: dict")
|
||||
|
||||
# truth
|
||||
assert.true(not set())
|
||||
assert.true(set([False]))
|
||||
assert.true(set([1, 2, 3]))
|
||||
|
||||
x = set([1, 2, 3])
|
||||
y = set([3, 4, 5])
|
||||
|
||||
# set + any is not defined
|
||||
assert.fails(lambda : x + y, "unknown.*: set \\+ set")
|
||||
|
||||
# set | set
|
||||
assert.eq(list(set("a".elems()) | set("b".elems())), ["a", "b"])
|
||||
assert.eq(list(set("ab".elems()) | set("bc".elems())), ["a", "b", "c"])
|
||||
assert.fails(lambda : set() | [], "unknown binary op: set | list")
|
||||
assert.eq(type(x | y), "set")
|
||||
assert.eq(list(x | y), [1, 2, 3, 4, 5])
|
||||
assert.eq(list(x | set([5, 1])), [1, 2, 3, 5])
|
||||
assert.eq(list(x | set((6, 5, 4))), [1, 2, 3, 6, 5, 4])
|
||||
|
||||
# set.union (allows any iterable for right operand)
|
||||
assert.eq(list(set("a".elems()).union("b".elems())), ["a", "b"])
|
||||
assert.eq(list(set("ab".elems()).union("bc".elems())), ["a", "b", "c"])
|
||||
assert.eq(set().union([]), set())
|
||||
assert.eq(type(x.union(y)), "set")
|
||||
assert.eq(list(x.union(y)), [1, 2, 3, 4, 5])
|
||||
assert.eq(list(x.union([5, 1])), [1, 2, 3, 5])
|
||||
assert.eq(list(x.union((6, 5, 4))), [1, 2, 3, 6, 5, 4])
|
||||
assert.fails(lambda : x.union([1, 2, {}]), "unhashable type: dict")
|
||||
|
||||
# intersection, set & set or set.intersection(iterable)
|
||||
assert.eq(list(set("a".elems()) & set("b".elems())), [])
|
||||
assert.eq(list(set("ab".elems()) & set("bc".elems())), ["b"])
|
||||
assert.eq(list(set("a".elems()).intersection("b".elems())), [])
|
||||
assert.eq(list(set("ab".elems()).intersection("bc".elems())), ["b"])
|
||||
|
||||
# symmetric difference, set ^ set or set.symmetric_difference(iterable)
|
||||
assert.eq(set([1, 2, 3]) ^ set([4, 5, 3]), set([1, 2, 4, 5]))
|
||||
assert.eq(set([1,2,3,4]).symmetric_difference([3,4,5,6]), set([1,2,5,6]))
|
||||
assert.eq(set([1,2,3,4]).symmetric_difference(set([])), set([1,2,3,4]))
|
||||
|
||||
def test_set_augmented_assign():
|
||||
x = set([1, 2, 3])
|
||||
x &= set([2, 3])
|
||||
assert.eq(x, set([2, 3]))
|
||||
x |= set([1])
|
||||
assert.eq(x, set([1, 2, 3]))
|
||||
x ^= set([4, 5, 3])
|
||||
assert.eq(x, set([1, 2, 4, 5]))
|
||||
|
||||
test_set_augmented_assign()
|
||||
|
||||
# len
|
||||
assert.eq(len(x), 3)
|
||||
assert.eq(len(y), 3)
|
||||
assert.eq(len(x | y), 5)
|
||||
|
||||
# str
|
||||
assert.eq(str(set([1])), "set([1])")
|
||||
assert.eq(str(set([2, 3])), "set([2, 3])")
|
||||
assert.eq(str(set([3, 2])), "set([3, 2])")
|
||||
|
||||
# comparison
|
||||
assert.eq(x, x)
|
||||
assert.eq(y, y)
|
||||
assert.true(x != y)
|
||||
assert.eq(set([1, 2, 3]), set([3, 2, 1]))
|
||||
|
||||
# iteration
|
||||
assert.true(type([elem for elem in x]), "list")
|
||||
assert.true(list([elem for elem in x]), [1, 2, 3])
|
||||
|
||||
def iter():
|
||||
list = []
|
||||
for elem in x:
|
||||
list.append(elem)
|
||||
return list
|
||||
|
||||
assert.eq(iter(), [1, 2, 3])
|
||||
|
||||
# sets are not indexable
|
||||
assert.fails(lambda : x[0], "unhandled.*operation")
|
||||
|
||||
# adding and removing
|
||||
add_set = set([1,2,3])
|
||||
add_set.add(4)
|
||||
assert.true(4 in add_set)
|
||||
freeze(add_set) # no mutation of frozen set because key already present
|
||||
add_set.add(4)
|
||||
assert.fails(lambda: add_set.add(5), "add: cannot insert into frozen hash table")
|
||||
|
||||
# remove
|
||||
remove_set = set([1,2,3])
|
||||
remove_set.remove(3)
|
||||
assert.true(3 not in remove_set)
|
||||
assert.fails(lambda: remove_set.remove(3), "remove: missing key")
|
||||
freeze(remove_set)
|
||||
assert.fails(lambda: remove_set.remove(3), "remove: cannot delete from frozen hash table")
|
||||
|
||||
# discard
|
||||
discard_set = set([1,2,3])
|
||||
discard_set.discard(3)
|
||||
assert.true(3 not in discard_set)
|
||||
assert.eq(discard_set.discard(3), None)
|
||||
freeze(discard_set)
|
||||
assert.eq(discard_set.discard(3), None) # no mutation of frozen set because key doesn't exist
|
||||
assert.fails(lambda: discard_set.discard(1), "discard: cannot delete from frozen hash table")
|
||||
|
||||
|
||||
# pop
|
||||
pop_set = set([1,2,3])
|
||||
assert.eq(pop_set.pop(), 1)
|
||||
assert.eq(pop_set.pop(), 2)
|
||||
assert.eq(pop_set.pop(), 3)
|
||||
assert.fails(lambda: pop_set.pop(), "pop: empty set")
|
||||
pop_set.add(1)
|
||||
pop_set.add(2)
|
||||
freeze(pop_set)
|
||||
assert.fails(lambda: pop_set.pop(), "pop: cannot delete from frozen hash table")
|
||||
|
||||
# clear
|
||||
clear_set = set([1,2,3])
|
||||
clear_set.clear()
|
||||
assert.eq(len(clear_set), 0)
|
||||
freeze(clear_set) # no mutation of frozen set because its already empty
|
||||
assert.eq(clear_set.clear(), None)
|
||||
|
||||
other_clear_set = set([1,2,3])
|
||||
freeze(other_clear_set)
|
||||
assert.fails(lambda: other_clear_set.clear(), "clear: cannot clear frozen hash table")
|
||||
|
||||
# difference: set - set or set.difference(iterable)
|
||||
assert.eq(set([1,2,3,4]).difference([1,2,3,4]), set([]))
|
||||
assert.eq(set([1,2,3,4]).difference([1,2]), set([3,4]))
|
||||
assert.eq(set([1,2,3,4]).difference([]), set([1,2,3,4]))
|
||||
assert.eq(set([1,2,3,4]).difference(set([1,2,3])), set([4]))
|
||||
|
||||
assert.eq(set([1,2,3,4]) - set([1,2,3,4]), set())
|
||||
assert.eq(set([1,2,3,4]) - set([1,2]), set([3,4]))
|
||||
|
||||
# issuperset: set >= set or set.issuperset(iterable)
|
||||
assert.true(set([1,2,3]).issuperset([1,2]))
|
||||
assert.true(not set([1,2,3]).issuperset(set([1,2,4])))
|
||||
assert.true(set([1,2,3]) >= set([1,2,3]))
|
||||
assert.true(set([1,2,3]) >= set([1,2]))
|
||||
assert.true(not set([1,2,3]) >= set([1,2,4]))
|
||||
|
||||
# proper superset: set > set
|
||||
assert.true(set([1, 2, 3]) > set([1, 2]))
|
||||
assert.true(not set([1,2, 3]) > set([1, 2, 3]))
|
||||
|
||||
# issubset: set <= set or set.issubset(iterable)
|
||||
assert.true(set([1,2]).issubset([1,2,3]))
|
||||
assert.true(not set([1,2,3]).issubset(set([1,2,4])))
|
||||
assert.true(set([1,2,3]) <= set([1,2,3]))
|
||||
assert.true(set([1,2]) <= set([1,2,3]))
|
||||
assert.true(not set([1,2,3]) <= set([1,2,4]))
|
||||
|
||||
# proper subset: set < set
|
||||
assert.true(set([1,2]) < set([1,2,3]))
|
||||
assert.true(not set([1,2,3]) < set([1,2,3]))
|
||||
Vendored
+493
@@ -0,0 +1,493 @@
|
||||
# Tests of Starlark 'string'
|
||||
# option:set
|
||||
|
||||
load("assert.star", "assert")
|
||||
|
||||
# raw string literals:
|
||||
assert.eq(r"a\bc", "a\\bc")
|
||||
|
||||
# truth
|
||||
assert.true("abc")
|
||||
assert.true(chr(0))
|
||||
assert.true(not "")
|
||||
|
||||
# str + str
|
||||
assert.eq("a" + "b" + "c", "abc")
|
||||
|
||||
# str * int, int * str
|
||||
assert.eq("abc" * 0, "")
|
||||
assert.eq("abc" * -1, "")
|
||||
assert.eq("abc" * 1, "abc")
|
||||
assert.eq("abc" * 5, "abcabcabcabcabc")
|
||||
assert.eq(0 * "abc", "")
|
||||
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")
|
||||
|
||||
# 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)
|
||||
|
||||
# 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.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"), 0x41)
|
||||
assert.eq(ord("Й"), 0x419)
|
||||
assert.eq(ord("世"), 0x4e16)
|
||||
assert.eq(ord("😿"), 0x1F63F)
|
||||
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
|
||||
|
||||
# string.codepoint_ords
|
||||
assert.eq(type("abcЙ😿".codepoint_ords()), "string.codepoints")
|
||||
assert.eq(str("abcЙ😿".codepoint_ords()), '"abcЙ😿".codepoint_ords()')
|
||||
assert.eq(list("abcЙ😿".codepoint_ords()), [97, 98, 99, 1049, 128575])
|
||||
assert.eq(list(("A" + "😿Z"[1:]).codepoint_ords()), [ord("A"), 0xFFFD, 0xFFFD, 0xFFFD, ord("Z")])
|
||||
assert.eq(list("".codepoint_ords()), [])
|
||||
assert.fails(lambda: "abcЙ😿".codepoint_ords()[2], "unhandled index") # not indexable
|
||||
assert.fails(lambda: len("abcЙ😿".codepoint_ords()), "no len") # unknown length
|
||||
|
||||
# string.codepoints
|
||||
assert.eq(type("abcЙ😿".codepoints()), "string.codepoints")
|
||||
assert.eq(str("abcЙ😿".codepoints()), '"abcЙ😿".codepoints()')
|
||||
assert.eq(list("abcЙ😿".codepoints()), ["a", "b", "c", "Й", "😿"])
|
||||
assert.eq(list(("A" + "😿Z"[1:]).codepoints()), ["A", "�", "�", "�", "Z"])
|
||||
assert.eq(list("".codepoints()), [])
|
||||
assert.fails(lambda: "abcЙ😿".codepoints()[2], "unhandled index") # not indexable
|
||||
assert.fails(lambda: len("abcЙ😿".codepoints()), "no len") # unknown length
|
||||
|
||||
# string.elem_ords
|
||||
assert.eq(type("abcЙ😿".elem_ords()), "string.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("".elem_ords()), [])
|
||||
assert.eq("abcЙ😿".elem_ords()[2], 99) # indexable
|
||||
assert.eq(len("abcЙ😿".elem_ords()), 9) # known length
|
||||
|
||||
# string.elems (1-byte substrings, which are invalid text)
|
||||
assert.eq(type("abcЙ😿".elems()), "string.elems")
|
||||
assert.eq(str("abcЙ😿".elems()), '"abcЙ😿".elems()')
|
||||
assert.eq(
|
||||
repr(list("abcЙ😿".elems())),
|
||||
r'["a", "b", "c", "\xd0", "\x99", "\xf0", "\x9f", "\x98", "\xbf"]',
|
||||
)
|
||||
assert.eq(
|
||||
repr(list(("A" + "😿Z"[1:]).elems())),
|
||||
r'["A", "\x9f", "\x98", "\xbf", "Z"]',
|
||||
)
|
||||
assert.eq(list("".elems()), [])
|
||||
assert.eq("abcЙ😿".elems()[2], "c") # indexable
|
||||
assert.eq(len("abcЙ😿".elems()), 9) # known length
|
||||
|
||||
# indexing, x[i]
|
||||
assert.eq("Hello, 世界!"[0], "H")
|
||||
assert.eq(repr("Hello, 世界!"[7]), r'"\xe4"') # (invalid text)
|
||||
assert.eq("Hello, 世界!"[13], "!")
|
||||
assert.fails(lambda: "abc"[-4], "out of range")
|
||||
assert.eq("abc"[-3], "a")
|
||||
assert.eq("abc"[-2], "b")
|
||||
assert.eq("abc"[-1], "c")
|
||||
assert.eq("abc"[0], "a")
|
||||
assert.eq("abc"[1], "b")
|
||||
assert.eq("abc"[2], "c")
|
||||
assert.fails(lambda: "abc"[4], "out of range")
|
||||
|
||||
# x[i] = ...
|
||||
def f():
|
||||
"abc"[1] = "B"
|
||||
|
||||
assert.fails(f, "string.*does not support.*assignment")
|
||||
|
||||
# slicing, x[i:j]
|
||||
assert.eq("abc"[:], "abc")
|
||||
assert.eq("abc"[-4:], "abc")
|
||||
assert.eq("abc"[-3:], "abc")
|
||||
assert.eq("abc"[-2:], "bc")
|
||||
assert.eq("abc"[-1:], "c")
|
||||
assert.eq("abc"[0:], "abc")
|
||||
assert.eq("abc"[1:], "bc")
|
||||
assert.eq("abc"[2:], "c")
|
||||
assert.eq("abc"[3:], "")
|
||||
assert.eq("abc"[4:], "")
|
||||
assert.eq("abc"[:-4], "")
|
||||
assert.eq("abc"[:-3], "")
|
||||
assert.eq("abc"[:-2], "a")
|
||||
assert.eq("abc"[:-1], "ab")
|
||||
assert.eq("abc"[:0], "")
|
||||
assert.eq("abc"[:1], "a")
|
||||
assert.eq("abc"[:2], "ab")
|
||||
assert.eq("abc"[:3], "abc")
|
||||
assert.eq("abc"[:4], "abc")
|
||||
assert.eq("abc"[1:2], "b")
|
||||
assert.eq("abc"[2:1], "")
|
||||
assert.eq(repr("😿"[:1]), r'"\xf0"') # (invalid text)
|
||||
|
||||
# non-unit strides
|
||||
assert.eq("abcd"[0:4:1], "abcd")
|
||||
assert.eq("abcd"[::2], "ac")
|
||||
assert.eq("abcd"[1::2], "bd")
|
||||
assert.eq("abcd"[4:0:-1], "dcb")
|
||||
assert.eq("banana"[7::-2], "aaa")
|
||||
assert.eq("banana"[6::-2], "aaa")
|
||||
assert.eq("banana"[5::-2], "aaa")
|
||||
assert.eq("banana"[4::-2], "nnb")
|
||||
assert.eq("banana"[::-1], "ananab")
|
||||
assert.eq("banana"[None:None:-2], "aaa")
|
||||
assert.fails(lambda: "banana"[1.0::], "invalid start index: got float, want int")
|
||||
assert.fails(lambda: "banana"[:"":], "invalid end index: got string, want int")
|
||||
assert.fails(lambda: "banana"[:"":True], "invalid slice step: got bool, want int")
|
||||
|
||||
# in, not in
|
||||
assert.true("oo" in "food")
|
||||
assert.true("ox" not in "food")
|
||||
assert.true("" in "food")
|
||||
assert.true("" in "")
|
||||
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.ne("hello", "Hello")
|
||||
|
||||
# hash must follow java.lang.String.hashCode.
|
||||
wanthash = {
|
||||
"": 0,
|
||||
"\0" * 100: 0,
|
||||
"hello": 99162322,
|
||||
"world": 113318802,
|
||||
"Hello, 世界!": 417292677,
|
||||
}
|
||||
gothash = {s: hash(s) for s in wanthash}
|
||||
assert.eq(gothash, wanthash)
|
||||
|
||||
# TODO(adonovan): ordered comparisons
|
||||
|
||||
# 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("%%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, "α")
|
||||
assert.eq("%c" % "A", "A")
|
||||
assert.eq("%c" % "α", "α")
|
||||
assert.fails(lambda: "%c" % "abc", "requires a single-character string")
|
||||
assert.fails(lambda: "%c" % "", "requires a single-character string")
|
||||
assert.fails(lambda: "%c" % 65.0, "requires int or single-character string")
|
||||
assert.fails(lambda: "%c" % 10000000, "requires a valid Unicode code point")
|
||||
assert.fails(lambda: "%c" % -1, "requires a valid Unicode code point")
|
||||
# TODO(adonovan): more tests
|
||||
|
||||
# str.format
|
||||
assert.eq("a{}b".format(123), "a123b")
|
||||
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.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.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.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.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")
|
||||
|
||||
# str.split, str.rsplit
|
||||
assert.eq("a.b.c.d".split("."), ["a", "b", "c", "d"])
|
||||
assert.eq("a.b.c.d".rsplit("."), ["a", "b", "c", "d"])
|
||||
assert.eq("a.b.c.d".split(".", -1), ["a", "b", "c", "d"])
|
||||
assert.eq("a.b.c.d".rsplit(".", -1), ["a", "b", "c", "d"])
|
||||
assert.eq("a.b.c.d".split(".", 0), ["a.b.c.d"])
|
||||
assert.eq("a.b.c.d".rsplit(".", 0), ["a.b.c.d"])
|
||||
assert.eq("a.b.c.d".split(".", 1), ["a", "b.c.d"])
|
||||
assert.eq("a.b.c.d".rsplit(".", 1), ["a.b.c", "d"])
|
||||
assert.eq("a.b.c.d".split(".", 2), ["a", "b", "c.d"])
|
||||
assert.eq("a.b.c.d".rsplit(".", 2), ["a.b", "c", "d"])
|
||||
assert.eq(" ".split("."), [" "])
|
||||
assert.eq(" ".rsplit("."), [" "])
|
||||
|
||||
# {,r}split on white space:
|
||||
assert.eq(" a bc\n def \t ghi".split(), ["a", "bc", "def", "ghi"])
|
||||
assert.eq(" a bc\n def \t ghi".split(None), ["a", "bc", "def", "ghi"])
|
||||
assert.eq(" a bc\n def \t ghi".split(None, 0), ["a bc\n def \t ghi"])
|
||||
assert.eq(" a bc\n def \t ghi".rsplit(None, 0), [" a bc\n def \t ghi"])
|
||||
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"])
|
||||
assert.eq(" a bc\n def \t ghi".split(None, 2), ["a", "bc", "def \t ghi"])
|
||||
assert.eq(" a bc\n def \t ghi".rsplit(None, 2), [" a bc", "def", "ghi"])
|
||||
assert.eq(" a bc\n def \t ghi".split(None, 3), ["a", "bc", "def", "ghi"])
|
||||
assert.eq(" a bc\n def \t ghi".rsplit(None, 3), [" a", "bc", "def", "ghi"])
|
||||
assert.eq(" a bc\n def \t ghi".split(None, 4), ["a", "bc", "def", "ghi"])
|
||||
assert.eq(" a bc\n def \t ghi".rsplit(None, 4), ["a", "bc", "def", "ghi"])
|
||||
assert.eq(" a bc\n def \t ghi".rsplit(None, 5), ["a", "bc", "def", "ghi"])
|
||||
|
||||
assert.eq(" a bc\n def \t ghi ".split(None, 0), ["a bc\n def \t ghi "])
|
||||
assert.eq(" a bc\n def \t ghi ".rsplit(None, 0), [" a bc\n def \t ghi"])
|
||||
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("-", 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("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"])
|
||||
|
||||
# str.{,l,r}strip
|
||||
assert.eq(" \tfoo\n ".strip(), "foo")
|
||||
assert.eq(" \tfoo\n ".lstrip(), "foo\n ")
|
||||
assert.eq(" \tfoo\n ".rstrip(), " \tfoo")
|
||||
assert.eq(" \tfoo\n ".strip(""), "foo")
|
||||
assert.eq(" \tfoo\n ".lstrip(""), "foo\n ")
|
||||
assert.eq(" \tfoo\n ".rstrip(""), " \tfoo")
|
||||
assert.eq("blah.h".strip("b.h"), "la")
|
||||
assert.eq("blah.h".lstrip("b.h"), "lah.h")
|
||||
assert.eq("blah.h".rstrip("b.h"), "bla")
|
||||
|
||||
# str.count
|
||||
assert.eq("banana".count("a"), 3)
|
||||
assert.eq("banana".count("a", 2), 2)
|
||||
assert.eq("banana".count("a", -4, -2), 1)
|
||||
assert.eq("banana".count("a", 1, 4), 2)
|
||||
assert.eq("banana".count("a", 0, -100), 0)
|
||||
|
||||
# str.{starts,ends}with
|
||||
assert.true("foo".endswith("oo"))
|
||||
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".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))
|
||||
|
||||
# str.replace
|
||||
assert.eq("banana".replace("a", "o", 1), "bonana")
|
||||
assert.eq("banana".replace("a", "o"), "bonono")
|
||||
# TODO(adonovan): more tests
|
||||
|
||||
# str.{,r}find
|
||||
assert.eq("foofoo".find("oo"), 1)
|
||||
assert.eq("foofoo".find("ox"), -1)
|
||||
assert.eq("foofoo".find("oo", 2), 4)
|
||||
assert.eq("foofoo".rfind("oo"), 4)
|
||||
assert.eq("foofoo".rfind("ox"), -1)
|
||||
assert.eq("foofoo".rfind("oo", 1, 4), 1)
|
||||
assert.eq("foofoo".find(""), 0)
|
||||
assert.eq("foofoo".rfind(""), 6)
|
||||
|
||||
# str.{,r}partition
|
||||
assert.eq("foo/bar/wiz".partition("/"), ("foo", "/", "bar/wiz"))
|
||||
assert.eq("foo/bar/wiz".rpartition("/"), ("foo/bar", "/", "wiz"))
|
||||
assert.eq("foo/bar/wiz".partition("."), ("foo/bar/wiz", "", ""))
|
||||
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")
|
||||
|
||||
# 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))
|
||||
|
||||
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
|
||||
|
||||
# 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)
|
||||
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(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")
|
||||
|
||||
# TODO(adonovan): tests for: {,r}index
|
||||
|
||||
# str.capitalize
|
||||
assert.eq("hElLo, WoRlD!".capitalize(), "Hello, world!")
|
||||
assert.eq("por qué".capitalize(), "Por qué")
|
||||
assert.eq("¿Por qué?".capitalize(), "¿por qué?")
|
||||
|
||||
# str.lower
|
||||
assert.eq("hElLo, WoRlD!".lower(), "hello, world!")
|
||||
assert.eq("por qué".lower(), "por qué")
|
||||
assert.eq("¿Por qué?".lower(), "¿por qué?")
|
||||
assert.eq("LJUBOVIĆ".lower(), "ljubović")
|
||||
assert.true("dženan ljubović".islower())
|
||||
|
||||
# str.upper
|
||||
assert.eq("hElLo, WoRlD!".upper(), "HELLO, WORLD!")
|
||||
assert.eq("por qué".upper(), "POR QUÉ")
|
||||
assert.eq("¿Por qué?".upper(), "¿POR QUÉ?")
|
||||
assert.eq("ljubović".upper(), "LJUBOVIĆ")
|
||||
assert.true("DŽENAN LJUBOVIĆ".isupper())
|
||||
|
||||
# str.title
|
||||
assert.eq("hElLo, WoRlD!".title(), "Hello, World!")
|
||||
assert.eq("por qué".title(), "Por Qué")
|
||||
assert.eq("¿Por qué?".title(), "¿Por Qué?")
|
||||
assert.eq("ljubović".title(), "Ljubović")
|
||||
assert.true("Dženan Ljubović".istitle())
|
||||
assert.true(not "DŽenan LJubović".istitle())
|
||||
|
||||
# method spell check
|
||||
assert.fails(lambda: "".starts_with, "no .starts_with field.*did you mean .startswith")
|
||||
assert.fails(lambda: "".StartsWith, "no .StartsWith field.*did you mean .startswith")
|
||||
assert.fails(lambda: "".fin, "no .fin field.*.did you mean .find")
|
||||
|
||||
|
||||
# removesuffix
|
||||
assert.eq("Apricot".removesuffix("cot"), "Apri")
|
||||
assert.eq("Apricot".removesuffix("Cot"), "Apricot")
|
||||
assert.eq("Apricot".removesuffix("t"), "Aprico")
|
||||
assert.eq("a".removesuffix(""), "a")
|
||||
assert.eq("".removesuffix(""), "")
|
||||
assert.eq("".removesuffix("a"), "")
|
||||
assert.eq("Apricot".removesuffix("co"), "Apricot")
|
||||
assert.eq("Apricotcot".removesuffix("cot"), "Apricot")
|
||||
|
||||
# removeprefix
|
||||
assert.eq("Apricot".removeprefix("Apr"), "icot")
|
||||
assert.eq("Apricot".removeprefix("apr"), "Apricot")
|
||||
assert.eq("Apricot".removeprefix("A"), "pricot")
|
||||
assert.eq("a".removeprefix(""), "a")
|
||||
assert.eq("".removeprefix(""), "")
|
||||
assert.eq("".removeprefix("a"), "")
|
||||
assert.eq("Apricot".removeprefix("pr"), "Apricot")
|
||||
assert.eq("AprApricot".removeprefix("Apr"), "Apricot")
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
# Tests of time module.
|
||||
|
||||
load('assert.star', 'assert')
|
||||
load('time.star', 'time')
|
||||
|
||||
assert.true(time.now() > time.parse_time("2021-03-20T00:00:00Z"))
|
||||
|
||||
assert.eq(time.parse_time("2020-06-26T17:38:36Z"), time.from_timestamp(1593193116))
|
||||
assert.eq(time.parse_time("2020-06-26T17:38:36.123456789", format="2006-01-02T15:04:05.999999999"), time.from_timestamp(1593193116, 123456789))
|
||||
|
||||
assert.eq(time.parse_time("1970-01-01T00:00:00Z").unix, 0)
|
||||
assert.eq(time.parse_time("1970-01-01T00:00:00Z").unix_nano, 0)
|
||||
|
||||
t = time.parse_time("2000-01-02T03:04:05Z")
|
||||
assert.eq(t.year, 2000)
|
||||
assert.eq(t.in_location("US/Eastern"), time.parse_time("2000-01-01T22:04:05-05:00"))
|
||||
assert.eq(t.in_location("US/Eastern").format("3 04 PM"), "10 04 PM")
|
||||
|
||||
assert.eq(t - t, time.parse_duration("0s"))
|
||||
|
||||
d1s = time.parse_duration("1s")
|
||||
assert.eq(d1s - d1s, time.parse_duration("0"))
|
||||
assert.eq(d1s + d1s, time.parse_duration("2s"))
|
||||
assert.eq(d1s * 5, time.parse_duration("5s"))
|
||||
assert.eq(time.parse_duration("0s") + time.parse_duration("3m35s"), time.parse_duration("3m35s"))
|
||||
|
||||
d10h = time.parse_duration("10h")
|
||||
# duration attributes
|
||||
assert.eq(10.0, d10h.hours)
|
||||
assert.eq(10*60.0, d10h.minutes)
|
||||
assert.eq(10*60*60.0, d10h.seconds)
|
||||
assert.eq(10*60*60*1000, d10h.milliseconds)
|
||||
assert.eq(10*60*60*1000000, d10h.microseconds)
|
||||
assert.eq(10*60*60*1000000000, d10h.nanoseconds)
|
||||
|
||||
# duration type
|
||||
assert.eq("time.duration", type(d10h))
|
||||
# duration str
|
||||
assert.eq("10h0m0s", str(d10h))
|
||||
# duration hash
|
||||
durations = {
|
||||
d10h: "10h",
|
||||
d1s: "10s",
|
||||
}
|
||||
assert.eq("10h", durations[d10h])
|
||||
assert.eq("10s", durations[d1s])
|
||||
|
||||
# duration == duration
|
||||
# duration != duration
|
||||
assert.eq(time.parse_duration("1h"), time.parse_duration("1h"))
|
||||
assert.ne(time.parse_duration("1h"), time.parse_duration("1m"))
|
||||
# duration < duration
|
||||
assert.lt(time.parse_duration("1m"), time.parse_duration("1h"))
|
||||
assert.true(not time.parse_duration("1h") < time.parse_duration("1h"))
|
||||
assert.true(not time.parse_duration("1h") < time.parse_duration("1m"))
|
||||
# duration <= duration
|
||||
assert.true(time.parse_duration("1m") <= time.parse_duration("1h"))
|
||||
assert.true(time.parse_duration("1h") <= time.parse_duration("1h"))
|
||||
assert.true(not time.parse_duration("1h") <= time.parse_duration("1m"))
|
||||
# duration > duration
|
||||
assert.true(not time.parse_duration("1m") > time.parse_duration("1h"))
|
||||
assert.true(not time.parse_duration("1h") > time.parse_duration("1h"))
|
||||
assert.true(time.parse_duration("1h") > time.parse_duration("1m"))
|
||||
# duration >= duration
|
||||
assert.true(not time.parse_duration("1m") >= time.parse_duration("1h"))
|
||||
assert.true(time.parse_duration("1h") >= time.parse_duration("1h"))
|
||||
assert.true(time.parse_duration("1h") >= time.parse_duration("1m"))
|
||||
|
||||
refTime = time.parse_time("2011-04-22T13:33:48Z")
|
||||
tenHoursAfterRefTime = time.parse_time("2011-04-22T23:33:48Z")
|
||||
|
||||
# duration + duration = duration
|
||||
assert.eq(d10h + d1s, time.parse_duration("10h01s"))
|
||||
# duration + time = time
|
||||
assert.eq(d10h + refTime, tenHoursAfterRefTime)
|
||||
# duration - duration = duration
|
||||
assert.eq(d10h - d1s, time.parse_duration("9h59m59s"))
|
||||
# duration / duration = float
|
||||
assert.eq(d10h / time.parse_duration("16m"), 37.5)
|
||||
assert.fails(lambda: d10h / time.parse_duration("0"), "division by zero")
|
||||
# duration / int = duration
|
||||
assert.eq(d10h / 20, time.parse_duration("30m"))
|
||||
assert.fails(lambda: d10h / 0, "division by zero")
|
||||
# int / duration = error
|
||||
assert.fails(lambda: 20 / d10h, "unsupported operation")
|
||||
# duration / float = duration
|
||||
assert.eq(d10h / 37.5, time.parse_duration("16m"))
|
||||
assert.fails(lambda: d10h / 0.0, "division by zero")
|
||||
# duration // duration = int
|
||||
assert.eq(d10h // time.parse_duration("16m"), 37)
|
||||
assert.fails(lambda: d10h // time.parse_duration("0"), "division by zero")
|
||||
# duration * int = duration
|
||||
assert.eq(d1s * 1000, time.parse_duration("16m40s"))
|
||||
# int * duration = duration
|
||||
assert.eq(1000 * d1s, time.parse_duration("16m40s"))
|
||||
|
||||
# is_valid_timezone(location)
|
||||
assert.true(time.is_valid_timezone("UTC"))
|
||||
assert.true(time.is_valid_timezone("US/Eastern"))
|
||||
assert.true(not time.is_valid_timezone("UKN"))
|
||||
|
||||
# time(year=..., month=..., day=..., hour=..., minute=..., second=..., nanosecond=..., location=...)
|
||||
assert.fails(lambda: time.time(2009, 6, 12, 12, 6, 10, 99, "US/Eastern"), "unexpected positional argument")
|
||||
t1 = time.time(year=2009, month=6, day=12, hour=12, minute=6, second=10, nanosecond=99, location="US/Eastern")
|
||||
assert.eq(t1, time.parse_time("2009-06-12T12:06:10.000000099", format="2006-01-02T15:04:05.999999999", location="US/Eastern"))
|
||||
assert.eq(time.time(year=2012, month=12, day=31), time.parse_time("2012-12-31T00:00:00Z"))
|
||||
assert.eq(time.time(year=2009, month=6, day=12, hour=12, minute=6, second=10, nanosecond=99, location="UTC"), time.time(year=2009, month=6, day=12, hour=12, minute=6, second=10, nanosecond=99))
|
||||
|
||||
# time attributes
|
||||
assert.eq(2009, t1.year)
|
||||
assert.eq(6, t1.month)
|
||||
assert.eq(12, t1.day)
|
||||
assert.eq(12, t1.hour)
|
||||
assert.eq(6, t1.minute)
|
||||
assert.eq(10, t1.second)
|
||||
assert.eq(99, t1.nanosecond)
|
||||
assert.eq(1244822770, t1.unix)
|
||||
assert.eq(1244822770000000099, t1.unix_nano)
|
||||
assert.true(not time.parse_time("0001-01-01T00:00:00Z"))
|
||||
assert.true(time.parse_time("2022-01-01T00:00:00Z"))
|
||||
|
||||
# time type
|
||||
assert.eq("time.time", type(refTime))
|
||||
# duration str
|
||||
assert.eq("2011-04-22 13:33:48 +0000 UTC", str(refTime))
|
||||
# duration hash
|
||||
times = {
|
||||
refTime: "refTime",
|
||||
t1: "t1",
|
||||
}
|
||||
assert.eq("refTime", times[refTime])
|
||||
assert.eq("t1", times[t1])
|
||||
|
||||
oneSecondAfterRefTime = time.parse_time("2011-04-22T13:33:49Z")
|
||||
oneYearAfterRefTime = time.parse_time("2012-04-22T13:33:48Z")
|
||||
oneYearBeforeRefTime = time.parse_time("2010-04-22T13:33:48Z")
|
||||
twoYearsBeforeRefTime = time.parse_time("2009-04-22T13:33:48Z")
|
||||
tenHoursBeforeRefTime = time.parse_time("2011-04-22T03:33:48Z")
|
||||
|
||||
# time == time
|
||||
# time != time
|
||||
assert.eq(refTime, refTime)
|
||||
assert.ne(refTime, oneSecondAfterRefTime)
|
||||
# time < time
|
||||
assert.lt(oneYearBeforeRefTime, refTime)
|
||||
assert.true(not oneYearBeforeRefTime < oneYearBeforeRefTime)
|
||||
assert.true(not oneYearBeforeRefTime < twoYearsBeforeRefTime)
|
||||
# time <= time
|
||||
assert.true(oneYearBeforeRefTime <= refTime)
|
||||
assert.true(oneYearBeforeRefTime <= oneYearBeforeRefTime)
|
||||
assert.true(not oneYearBeforeRefTime <= twoYearsBeforeRefTime)
|
||||
# time > time
|
||||
assert.true(oneYearAfterRefTime > refTime)
|
||||
assert.true(not refTime > refTime)
|
||||
assert.true(not oneYearBeforeRefTime > refTime)
|
||||
# time >= time
|
||||
assert.true(oneYearAfterRefTime >= refTime)
|
||||
assert.true(refTime >= refTime)
|
||||
assert.true(not oneYearBeforeRefTime >= refTime)
|
||||
# time + duration = time
|
||||
assert.eq(refTime + d10h, tenHoursAfterRefTime)
|
||||
# time - duration = time
|
||||
assert.eq(refTime - d10h, tenHoursBeforeRefTime)
|
||||
# time - time = duration
|
||||
assert.eq(refTime - tenHoursBeforeRefTime, d10h)
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
# Tests of Starlark 'tuple'
|
||||
|
||||
load("assert.star", "assert")
|
||||
|
||||
# literal
|
||||
assert.eq((), ())
|
||||
assert.eq((1), 1)
|
||||
assert.eq((1,), (1,))
|
||||
assert.ne((1), (1,))
|
||||
assert.eq((1, 2), (1, 2))
|
||||
assert.eq((1, 2, 3, 4, 5), (1, 2, 3, 4, 5))
|
||||
assert.ne((1, 2, 3), (1, 2, 4))
|
||||
|
||||
# truth
|
||||
assert.true((False,))
|
||||
assert.true((False, False))
|
||||
assert.true(not ())
|
||||
|
||||
# indexing, x[i]
|
||||
assert.eq(("a", "b")[0], "a")
|
||||
assert.eq(("a", "b")[1], "b")
|
||||
|
||||
# slicing, x[i:j]
|
||||
assert.eq("abcd"[0:4:1], "abcd")
|
||||
assert.eq("abcd"[::2], "ac")
|
||||
assert.eq("abcd"[1::2], "bd")
|
||||
assert.eq("abcd"[4:0:-1], "dcb")
|
||||
banana = tuple("banana".elems())
|
||||
assert.eq(banana[7::-2], tuple("aaa".elems()))
|
||||
assert.eq(banana[6::-2], tuple("aaa".elems()))
|
||||
assert.eq(banana[5::-2], tuple("aaa".elems()))
|
||||
assert.eq(banana[4::-2], tuple("nnb".elems()))
|
||||
|
||||
# tuple
|
||||
assert.eq(tuple(), ())
|
||||
assert.eq(tuple("abc".elems()), ("a", "b", "c"))
|
||||
assert.eq(tuple(["a", "b", "c"]), ("a", "b", "c"))
|
||||
assert.eq(tuple([1]), (1,))
|
||||
assert.fails(lambda: tuple(1), "got int, want iterable")
|
||||
|
||||
# tuple * int, int * tuple
|
||||
abc = tuple("abc".elems())
|
||||
assert.eq(abc * 0, ())
|
||||
assert.eq(abc * -1, ())
|
||||
assert.eq(abc * 1, abc)
|
||||
assert.eq(abc * 3, ("a", "b", "c", "a", "b", "c", "a", "b", "c"))
|
||||
assert.eq(0 * abc, ())
|
||||
assert.eq(-1 * abc, ())
|
||||
assert.eq(1 * abc, abc)
|
||||
assert.eq(3 * abc, ("a", "b", "c", "a", "b", "c", "a", "b", "c"))
|
||||
assert.fails(lambda: abc * (1000000 * 1000000), "repeat count 1000000000000 too large")
|
||||
assert.fails(lambda: abc * 1000000 * 1000000, "excessive repeat \\(3000000 \\* 1000000 elements")
|
||||
|
||||
# TODO(adonovan): test use of tuple as sequence
|
||||
# (for loop, comprehension, library functions).
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# Tests of Starlark while statement.
|
||||
|
||||
# This is a "chunked" file: each "---" effectively starts a new file.
|
||||
|
||||
# option:while
|
||||
|
||||
load("assert.star", "assert")
|
||||
|
||||
def sum(n):
|
||||
r = 0
|
||||
while n > 0:
|
||||
r += n
|
||||
n -= 1
|
||||
return r
|
||||
|
||||
def while_break(n):
|
||||
r = 0
|
||||
while n > 0:
|
||||
if n == 5:
|
||||
break
|
||||
r += n
|
||||
n -= 1
|
||||
return r
|
||||
|
||||
def while_continue(n):
|
||||
r = 0
|
||||
while n > 0:
|
||||
if n % 2 == 0:
|
||||
n -= 1
|
||||
continue
|
||||
r += n
|
||||
n -= 1
|
||||
return r
|
||||
|
||||
assert.eq(sum(5), 5+4+3+2+1)
|
||||
assert.eq(while_break(10), 40)
|
||||
assert.eq(while_continue(10), 25)
|
||||
@@ -0,0 +1,355 @@
|
||||
package starlark
|
||||
|
||||
// This file defines the Unpack helper functions used by
|
||||
// built-in functions to interpret their call arguments.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"go.starlark.net/internal/spell"
|
||||
)
|
||||
|
||||
// An Unpacker defines custom argument unpacking behavior.
|
||||
// See UnpackArgs.
|
||||
type Unpacker interface {
|
||||
Unpack(v Value) error
|
||||
}
|
||||
|
||||
// UnpackArgs unpacks the positional and keyword arguments into the
|
||||
// supplied parameter variables. pairs is an alternating list of names
|
||||
// and pointers to variables.
|
||||
//
|
||||
// If the variable is a bool, integer, string, *List, *Dict, Callable,
|
||||
// Iterable, or user-defined implementation of Value,
|
||||
// UnpackArgs performs the appropriate type check.
|
||||
// Predeclared Go integer types uses the AsInt check.
|
||||
//
|
||||
// If the parameter name ends with "?", it is optional.
|
||||
//
|
||||
// If the parameter name ends with "??", it is optional and treats the None value
|
||||
// as if the argument was absent.
|
||||
//
|
||||
// If a parameter is marked optional, then all following parameters are
|
||||
// implicitly optional where or not they are marked.
|
||||
//
|
||||
// If the variable implements Unpacker, its Unpack argument
|
||||
// is called with the argument value, allowing an application
|
||||
// to define its own argument validation and conversion.
|
||||
//
|
||||
// If the variable implements Value, UnpackArgs may call
|
||||
// its Type() method while constructing the error message.
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// var (
|
||||
// a Value
|
||||
// b = MakeInt(42)
|
||||
// c Value = starlark.None
|
||||
// )
|
||||
//
|
||||
// // 1. mixed parameters, like def f(a, b=42, c=None).
|
||||
// err := UnpackArgs("f", args, kwargs, "a", &a, "b?", &b, "c?", &c)
|
||||
//
|
||||
// // 2. keyword parameters only, like def f(*, a, b, c=None).
|
||||
// if len(args) > 0 {
|
||||
// return fmt.Errorf("f: unexpected positional arguments")
|
||||
// }
|
||||
// err := UnpackArgs("f", args, kwargs, "a", &a, "b?", &b, "c?", &c)
|
||||
//
|
||||
// // 3. positional parameters only, like def f(a, b=42, c=None, /) in Python 3.8.
|
||||
// err := UnpackPositionalArgs("f", args, kwargs, 1, &a, &b, &c)
|
||||
//
|
||||
// More complex forms such as def f(a, b=42, *args, c, d=123, **kwargs)
|
||||
// require additional logic, but their need in built-ins is exceedingly rare.
|
||||
//
|
||||
// In the examples above, the declaration of b with type Int causes UnpackArgs
|
||||
// to require that b's argument value, if provided, is also an int.
|
||||
// To allow arguments of any type, while retaining the default value of 42,
|
||||
// declare b as a Value:
|
||||
//
|
||||
// var b Value = MakeInt(42)
|
||||
//
|
||||
// The zero value of a variable of type Value, such as 'a' in the
|
||||
// examples above, is not a valid Starlark value, so if the parameter is
|
||||
// optional, the caller must explicitly handle the default case by
|
||||
// interpreting nil as None or some computed default. The same is true
|
||||
// for the zero values of variables of type *List, *Dict, Callable, or
|
||||
// Iterable. For example:
|
||||
//
|
||||
// // def myfunc(d=None, e=[], f={})
|
||||
// var (
|
||||
// d Value
|
||||
// e *List
|
||||
// f *Dict
|
||||
// )
|
||||
// err := UnpackArgs("myfunc", args, kwargs, "d?", &d, "e?", &e, "f?", &f)
|
||||
// if d == nil { d = None; }
|
||||
// if e == nil { e = new(List); }
|
||||
// if f == nil { f = new(Dict); }
|
||||
//
|
||||
func UnpackArgs(fnname string, args Tuple, kwargs []Tuple, pairs ...interface{}) error {
|
||||
nparams := len(pairs) / 2
|
||||
var defined intset
|
||||
defined.init(nparams)
|
||||
|
||||
paramName := func(x interface{}) (name string, skipNone bool) { // (no free variables)
|
||||
name = x.(string)
|
||||
if strings.HasSuffix(name, "??") {
|
||||
name = strings.TrimSuffix(name, "??")
|
||||
skipNone = true
|
||||
} else if name[len(name)-1] == '?' {
|
||||
name = name[:len(name)-1]
|
||||
}
|
||||
|
||||
return name, skipNone
|
||||
}
|
||||
|
||||
// positional arguments
|
||||
if len(args) > nparams {
|
||||
return fmt.Errorf("%s: got %d arguments, want at most %d",
|
||||
fnname, len(args), nparams)
|
||||
}
|
||||
for i, arg := range args {
|
||||
defined.set(i)
|
||||
name, skipNone := paramName(pairs[2*i])
|
||||
if skipNone {
|
||||
if _, isNone := arg.(NoneType); isNone {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if err := unpackOneArg(arg, pairs[2*i+1]); err != nil {
|
||||
return fmt.Errorf("%s: for parameter %s: %s", fnname, name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// keyword arguments
|
||||
kwloop:
|
||||
for _, item := range kwargs {
|
||||
name, arg := item[0].(String), item[1]
|
||||
for i := 0; i < nparams; i++ {
|
||||
pName, skipNone := paramName(pairs[2*i])
|
||||
if pName == string(name) {
|
||||
// found it
|
||||
if defined.set(i) {
|
||||
return fmt.Errorf("%s: got multiple values for keyword argument %s",
|
||||
fnname, name)
|
||||
}
|
||||
|
||||
if skipNone {
|
||||
if _, isNone := arg.(NoneType); isNone {
|
||||
continue kwloop
|
||||
}
|
||||
}
|
||||
|
||||
ptr := pairs[2*i+1]
|
||||
if err := unpackOneArg(arg, ptr); err != nil {
|
||||
return fmt.Errorf("%s: for parameter %s: %s", fnname, name, err)
|
||||
}
|
||||
continue kwloop
|
||||
}
|
||||
}
|
||||
err := fmt.Errorf("%s: unexpected keyword argument %s", fnname, name)
|
||||
names := make([]string, 0, nparams)
|
||||
for i := 0; i < nparams; i += 2 {
|
||||
param, _ := paramName(pairs[i])
|
||||
names = append(names, param)
|
||||
}
|
||||
if n := spell.Nearest(string(name), names); n != "" {
|
||||
err = fmt.Errorf("%s (did you mean %s?)", err.Error(), n)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Check that all non-optional parameters are defined.
|
||||
// (We needn't check the first len(args).)
|
||||
for i := len(args); i < nparams; i++ {
|
||||
name := pairs[2*i].(string)
|
||||
if strings.HasSuffix(name, "?") {
|
||||
break // optional
|
||||
}
|
||||
if !defined.get(i) {
|
||||
return fmt.Errorf("%s: missing argument for %s", fnname, name)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnpackPositionalArgs unpacks the positional arguments into
|
||||
// corresponding variables. Each element of vars is a pointer; see
|
||||
// UnpackArgs for allowed types and conversions.
|
||||
//
|
||||
// UnpackPositionalArgs reports an error if the number of arguments is
|
||||
// less than min or greater than len(vars), if kwargs is nonempty, or if
|
||||
// any conversion fails.
|
||||
//
|
||||
// See UnpackArgs for general comments.
|
||||
func UnpackPositionalArgs(fnname string, args Tuple, kwargs []Tuple, min int, vars ...interface{}) error {
|
||||
if len(kwargs) > 0 {
|
||||
return fmt.Errorf("%s: unexpected keyword arguments", fnname)
|
||||
}
|
||||
max := len(vars)
|
||||
if len(args) < min {
|
||||
var atleast string
|
||||
if min < max {
|
||||
atleast = "at least "
|
||||
}
|
||||
return fmt.Errorf("%s: got %d arguments, want %s%d", fnname, len(args), atleast, min)
|
||||
}
|
||||
if len(args) > max {
|
||||
var atmost string
|
||||
if max > min {
|
||||
atmost = "at most "
|
||||
}
|
||||
return fmt.Errorf("%s: got %d arguments, want %s%d", fnname, len(args), atmost, max)
|
||||
}
|
||||
for i, arg := range args {
|
||||
if err := unpackOneArg(arg, vars[i]); err != nil {
|
||||
return fmt.Errorf("%s: for parameter %d: %s", fnname, i+1, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unpackOneArg(v Value, ptr interface{}) error {
|
||||
// On failure, don't clobber *ptr.
|
||||
switch ptr := ptr.(type) {
|
||||
case Unpacker:
|
||||
return ptr.Unpack(v)
|
||||
case *Value:
|
||||
*ptr = v
|
||||
case *string:
|
||||
s, ok := AsString(v)
|
||||
if !ok {
|
||||
return fmt.Errorf("got %s, want string", v.Type())
|
||||
}
|
||||
*ptr = s
|
||||
case *bool:
|
||||
b, ok := v.(Bool)
|
||||
if !ok {
|
||||
return fmt.Errorf("got %s, want bool", v.Type())
|
||||
}
|
||||
*ptr = bool(b)
|
||||
case *int, *int8, *int16, *int32, *int64,
|
||||
*uint, *uint8, *uint16, *uint32, *uint64, *uintptr:
|
||||
return AsInt(v, ptr)
|
||||
case *float64:
|
||||
f, ok := v.(Float)
|
||||
if !ok {
|
||||
return fmt.Errorf("got %s, want float", v.Type())
|
||||
}
|
||||
*ptr = float64(f)
|
||||
case **List:
|
||||
list, ok := v.(*List)
|
||||
if !ok {
|
||||
return fmt.Errorf("got %s, want list", v.Type())
|
||||
}
|
||||
*ptr = list
|
||||
case **Dict:
|
||||
dict, ok := v.(*Dict)
|
||||
if !ok {
|
||||
return fmt.Errorf("got %s, want dict", v.Type())
|
||||
}
|
||||
*ptr = dict
|
||||
case *Callable:
|
||||
f, ok := v.(Callable)
|
||||
if !ok {
|
||||
return fmt.Errorf("got %s, want callable", v.Type())
|
||||
}
|
||||
*ptr = f
|
||||
case *Iterable:
|
||||
it, ok := v.(Iterable)
|
||||
if !ok {
|
||||
return fmt.Errorf("got %s, want iterable", v.Type())
|
||||
}
|
||||
*ptr = it
|
||||
default:
|
||||
// v must have type *V, where V is some subtype of starlark.Value.
|
||||
ptrv := reflect.ValueOf(ptr)
|
||||
if ptrv.Kind() != reflect.Ptr {
|
||||
log.Panicf("internal error: not a pointer: %T", ptr)
|
||||
}
|
||||
paramVar := ptrv.Elem()
|
||||
if !reflect.TypeOf(v).AssignableTo(paramVar.Type()) {
|
||||
// The value is not assignable to the variable.
|
||||
|
||||
// Detect a possible bug in the Go program that called Unpack:
|
||||
// If the variable *ptr is not a subtype of Value,
|
||||
// no value of v can possibly work.
|
||||
if !paramVar.Type().AssignableTo(reflect.TypeOf(new(Value)).Elem()) {
|
||||
log.Panicf("pointer element type does not implement Value: %T", ptr)
|
||||
}
|
||||
|
||||
// Report Starlark dynamic type error.
|
||||
//
|
||||
// We prefer the Starlark Value.Type name over
|
||||
// its Go reflect.Type name, but calling the
|
||||
// Value.Type method on the variable is not safe
|
||||
// in general. If the variable is an interface,
|
||||
// the call will fail. Even if the variable has
|
||||
// a concrete type, it might not be safe to call
|
||||
// Type() on a zero instance. Thus we must use
|
||||
// recover.
|
||||
|
||||
// Default to Go reflect.Type name
|
||||
paramType := paramVar.Type().String()
|
||||
|
||||
// Attempt to call Value.Type method.
|
||||
func() {
|
||||
defer func() { recover() }()
|
||||
if typer, _ := paramVar.Interface().(interface{ Type() string }); typer != nil {
|
||||
paramType = typer.Type()
|
||||
}
|
||||
}()
|
||||
return fmt.Errorf("got %s, want %s", v.Type(), paramType)
|
||||
}
|
||||
paramVar.Set(reflect.ValueOf(v))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type intset struct {
|
||||
small uint64 // bitset, used if n < 64
|
||||
large map[int]bool // set, used if n >= 64
|
||||
}
|
||||
|
||||
func (is *intset) init(n int) {
|
||||
if n >= 64 {
|
||||
is.large = make(map[int]bool)
|
||||
}
|
||||
}
|
||||
|
||||
func (is *intset) set(i int) (prev bool) {
|
||||
if is.large == nil {
|
||||
prev = is.small&(1<<uint(i)) != 0
|
||||
is.small |= 1 << uint(i)
|
||||
} else {
|
||||
prev = is.large[i]
|
||||
is.large[i] = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (is *intset) get(i int) bool {
|
||||
if is.large == nil {
|
||||
return is.small&(1<<uint(i)) != 0
|
||||
}
|
||||
return is.large[i]
|
||||
}
|
||||
|
||||
func (is *intset) len() int {
|
||||
if is.large == nil {
|
||||
// Suboptimal, but used only for error reporting.
|
||||
len := 0
|
||||
for i := 0; i < 64; i++ {
|
||||
if is.small&(1<<uint(i)) != 0 {
|
||||
len++
|
||||
}
|
||||
}
|
||||
return len
|
||||
}
|
||||
return len(is.large)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,157 @@
|
||||
// Copyright 2017 The Bazel Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package starlark_test
|
||||
|
||||
// This file defines tests of the Value API.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"go.starlark.net/starlark"
|
||||
)
|
||||
|
||||
func TestStringMethod(t *testing.T) {
|
||||
s := starlark.String("hello")
|
||||
for i, test := range [][2]string{
|
||||
// quoted string:
|
||||
{s.String(), `"hello"`},
|
||||
{fmt.Sprintf("%s", s), `"hello"`},
|
||||
{fmt.Sprintf("%+s", s), `"hello"`},
|
||||
{fmt.Sprintf("%v", s), `"hello"`},
|
||||
{fmt.Sprintf("%+v", s), `"hello"`},
|
||||
// unquoted:
|
||||
{s.GoString(), `hello`},
|
||||
{fmt.Sprintf("%#v", s), `hello`},
|
||||
} {
|
||||
got, want := test[0], test[1]
|
||||
if got != want {
|
||||
t.Errorf("#%d: got <<%s>>, want <<%s>>", i, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestListAppend(t *testing.T) {
|
||||
l := starlark.NewList(nil)
|
||||
l.Append(starlark.String("hello"))
|
||||
res, ok := starlark.AsString(l.Index(0))
|
||||
if !ok {
|
||||
t.Errorf("failed list.Append() got: %s, want: starlark.String", l.Index(0).Type())
|
||||
}
|
||||
if res != "hello" {
|
||||
t.Errorf("failed list.Append() got: %+v, want: hello", res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParamDefault(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
starFn string
|
||||
wantDefaults []starlark.Value
|
||||
}{
|
||||
{
|
||||
desc: "function with all required params",
|
||||
starFn: "all_required",
|
||||
wantDefaults: []starlark.Value{nil, nil, nil},
|
||||
},
|
||||
{
|
||||
desc: "function with all optional params",
|
||||
starFn: "all_opt",
|
||||
wantDefaults: []starlark.Value{
|
||||
starlark.String("a"),
|
||||
starlark.None,
|
||||
starlark.String(""),
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "function with required and optional params",
|
||||
starFn: "mix_required_opt",
|
||||
wantDefaults: []starlark.Value{
|
||||
nil,
|
||||
nil,
|
||||
starlark.String("c"),
|
||||
starlark.String("d"),
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "function with required, optional, and varargs params",
|
||||
starFn: "with_varargs",
|
||||
wantDefaults: []starlark.Value{
|
||||
nil,
|
||||
starlark.String("b"),
|
||||
nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "function with required, optional, varargs, and keyword-only params",
|
||||
starFn: "with_varargs_kwonly",
|
||||
wantDefaults: []starlark.Value{
|
||||
nil,
|
||||
starlark.String("b"),
|
||||
starlark.String("c"),
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "function with required, optional, and keyword-only params",
|
||||
starFn: "with_kwonly",
|
||||
wantDefaults: []starlark.Value{
|
||||
nil,
|
||||
starlark.String("b"),
|
||||
starlark.String("c"),
|
||||
nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "function with required, optional, and kwargs params",
|
||||
starFn: "with_kwargs",
|
||||
wantDefaults: []starlark.Value{
|
||||
nil,
|
||||
starlark.String("b"),
|
||||
starlark.String("c"),
|
||||
nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "function with required, optional, varargs, kw-only, and kwargs params",
|
||||
starFn: "with_varargs_kwonly_kwargs",
|
||||
wantDefaults: []starlark.Value{
|
||||
nil,
|
||||
starlark.String("b"),
|
||||
starlark.String("c"),
|
||||
nil,
|
||||
starlark.String("e"),
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
thread := &starlark.Thread{}
|
||||
filename := "testdata/function_param.star"
|
||||
globals, err := starlark.ExecFile(thread, filename, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("ExecFile(%v, %q) failed: %v", thread, filename, err)
|
||||
}
|
||||
|
||||
fn, ok := globals[tt.starFn].(*starlark.Function)
|
||||
if !ok {
|
||||
t.Fatalf("value %v is not a Starlark function", globals[tt.starFn])
|
||||
}
|
||||
|
||||
var paramDefaults []starlark.Value
|
||||
for i := 0; i < fn.NumParams(); i++ {
|
||||
paramDefaults = append(paramDefaults, fn.ParamDefault(i))
|
||||
}
|
||||
if diff := cmp.Diff(tt.wantDefaults, paramDefaults); diff != "" {
|
||||
t.Errorf("param defaults got diff (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user