whatcanGOwrong

This commit is contained in:
2024-09-19 21:38:24 -04:00
commit d0ae4d841d
17908 changed files with 4096831 additions and 0 deletions
@@ -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
@@ -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
}
@@ -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") }
@@ -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))
}
@@ -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)
}
}
@@ -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()
@@ -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)
@@ -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')
@@ -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`
@@ -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.
@@ -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])
@@ -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")
@@ -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()
@@ -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\\?")
@@ -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
@@ -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")
@@ -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')
---
@@ -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")
@@ -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)
@@ -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`
@@ -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\\?")
@@ -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,
)
@@ -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"])
@@ -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)
@@ -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]))
@@ -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")
@@ -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)
@@ -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).
@@ -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)
}
})
}
}