aboutsummaryrefslogtreecommitdiff
path: root/starlark/example_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'starlark/example_test.go')
-rw-r--r--starlark/example_test.go322
1 files changed, 322 insertions, 0 deletions
diff --git a/starlark/example_test.go b/starlark/example_test.go
new file mode 100644
index 0000000..5feca38
--- /dev/null
+++ b/starlark/example_test.go
@@ -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
+}