go.starlark.net@v0.0.0-20231101134539-556fd59b42f6/starlark/example_test.go (about)

     1  // Copyright 2017 The Bazel Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package starlark_test
     6  
     7  import (
     8  	"fmt"
     9  	"log"
    10  	"reflect"
    11  	"sort"
    12  	"strings"
    13  	"sync"
    14  	"sync/atomic"
    15  	"testing"
    16  	"unsafe"
    17  
    18  	"go.starlark.net/starlark"
    19  )
    20  
    21  // ExampleExecFile demonstrates a simple embedding
    22  // of the Starlark interpreter into a Go program.
    23  func ExampleExecFile() {
    24  	const data = `
    25  print(greeting + ", world")
    26  print(repeat("one"))
    27  print(repeat("mur", 2))
    28  squares = [x*x for x in range(10)]
    29  `
    30  
    31  	// repeat(str, n=1) is a Go function called from Starlark.
    32  	// It behaves like the 'string * int' operation.
    33  	repeat := func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
    34  		var s string
    35  		var n int = 1
    36  		if err := starlark.UnpackArgs(b.Name(), args, kwargs, "s", &s, "n?", &n); err != nil {
    37  			return nil, err
    38  		}
    39  		return starlark.String(strings.Repeat(s, n)), nil
    40  	}
    41  
    42  	// The Thread defines the behavior of the built-in 'print' function.
    43  	thread := &starlark.Thread{
    44  		Name:  "example",
    45  		Print: func(_ *starlark.Thread, msg string) { fmt.Println(msg) },
    46  	}
    47  
    48  	// This dictionary defines the pre-declared environment.
    49  	predeclared := starlark.StringDict{
    50  		"greeting": starlark.String("hello"),
    51  		"repeat":   starlark.NewBuiltin("repeat", repeat),
    52  	}
    53  
    54  	// Execute a program.
    55  	globals, err := starlark.ExecFile(thread, "apparent/filename.star", data, predeclared)
    56  	if err != nil {
    57  		if evalErr, ok := err.(*starlark.EvalError); ok {
    58  			log.Fatal(evalErr.Backtrace())
    59  		}
    60  		log.Fatal(err)
    61  	}
    62  
    63  	// Print the global environment.
    64  	fmt.Println("\nGlobals:")
    65  	for _, name := range globals.Keys() {
    66  		v := globals[name]
    67  		fmt.Printf("%s (%s) = %s\n", name, v.Type(), v.String())
    68  	}
    69  
    70  	// Output:
    71  	// hello, world
    72  	// one
    73  	// murmur
    74  	//
    75  	// Globals:
    76  	// squares (list) = [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
    77  }
    78  
    79  // ExampleThread_Load_sequential demonstrates a simple caching
    80  // implementation of 'load' that works sequentially.
    81  func ExampleThread_Load_sequential() {
    82  	fakeFilesystem := map[string]string{
    83  		"c.star": `load("b.star", "b"); c = b + "!"`,
    84  		"b.star": `load("a.star", "a"); b = a + ", world"`,
    85  		"a.star": `a = "Hello"`,
    86  	}
    87  
    88  	type entry struct {
    89  		globals starlark.StringDict
    90  		err     error
    91  	}
    92  
    93  	cache := make(map[string]*entry)
    94  
    95  	var load func(_ *starlark.Thread, module string) (starlark.StringDict, error)
    96  	load = func(_ *starlark.Thread, module string) (starlark.StringDict, error) {
    97  		e, ok := cache[module]
    98  		if e == nil {
    99  			if ok {
   100  				// request for package whose loading is in progress
   101  				return nil, fmt.Errorf("cycle in load graph")
   102  			}
   103  
   104  			// Add a placeholder to indicate "load in progress".
   105  			cache[module] = nil
   106  
   107  			// Load and initialize the module in a new thread.
   108  			data := fakeFilesystem[module]
   109  			thread := &starlark.Thread{Name: "exec " + module, Load: load}
   110  			globals, err := starlark.ExecFile(thread, module, data, nil)
   111  			e = &entry{globals, err}
   112  
   113  			// Update the cache.
   114  			cache[module] = e
   115  		}
   116  		return e.globals, e.err
   117  	}
   118  
   119  	globals, err := load(nil, "c.star")
   120  	if err != nil {
   121  		log.Fatal(err)
   122  	}
   123  	fmt.Println(globals["c"])
   124  
   125  	// Output:
   126  	// "Hello, world!"
   127  }
   128  
   129  // ExampleThread_Load_parallel demonstrates a parallel implementation
   130  // of 'load' with caching, duplicate suppression, and cycle detection.
   131  func ExampleThread_Load_parallel() {
   132  	cache := &cache{
   133  		cache: make(map[string]*entry),
   134  		fakeFilesystem: map[string]string{
   135  			"c.star": `load("a.star", "a"); c = a * 2`,
   136  			"b.star": `load("a.star", "a"); b = a * 3`,
   137  			"a.star": `a = 1; print("loaded a")`,
   138  		},
   139  	}
   140  
   141  	// We load modules b and c in parallel by concurrent calls to
   142  	// cache.Load.  Both of them load module a, but a is executed
   143  	// only once, as witnessed by the sole output of its print
   144  	// statement.
   145  
   146  	ch := make(chan string)
   147  	for _, name := range []string{"b", "c"} {
   148  		go func(name string) {
   149  			globals, err := cache.Load(name + ".star")
   150  			if err != nil {
   151  				log.Fatal(err)
   152  			}
   153  			ch <- fmt.Sprintf("%s = %s", name, globals[name])
   154  		}(name)
   155  	}
   156  	got := []string{<-ch, <-ch}
   157  	sort.Strings(got)
   158  	fmt.Println(strings.Join(got, "\n"))
   159  
   160  	// Output:
   161  	// loaded a
   162  	// b = 3
   163  	// c = 2
   164  }
   165  
   166  // TestThread_Load_parallelCycle demonstrates detection
   167  // of cycles during parallel loading.
   168  func TestThreadLoad_ParallelCycle(t *testing.T) {
   169  	cache := &cache{
   170  		cache: make(map[string]*entry),
   171  		fakeFilesystem: map[string]string{
   172  			"c.star": `load("b.star", "b"); c = b * 2`,
   173  			"b.star": `load("a.star", "a"); b = a * 3`,
   174  			"a.star": `load("c.star", "c"); a = c * 5; print("loaded a")`,
   175  		},
   176  	}
   177  
   178  	ch := make(chan string)
   179  	for _, name := range "bc" {
   180  		name := string(name)
   181  		go func() {
   182  			_, err := cache.Load(name + ".star")
   183  			if err == nil {
   184  				log.Fatalf("Load of %s.star succeeded unexpectedly", name)
   185  			}
   186  			ch <- err.Error()
   187  		}()
   188  	}
   189  	got := []string{<-ch, <-ch}
   190  	sort.Strings(got)
   191  
   192  	// Typically, the c goroutine quickly blocks behind b;
   193  	// b loads a, and a then fails to load c because it forms a cycle.
   194  	// The errors observed by the two goroutines are:
   195  	want1 := []string{
   196  		"cannot load a.star: cannot load c.star: cycle in load graph",                     // from b
   197  		"cannot load b.star: cannot load a.star: cannot load c.star: cycle in load graph", // from c
   198  	}
   199  	// But if the c goroutine is slow to start, b loads a,
   200  	// and a loads c; then c fails to load b because it forms a cycle.
   201  	// The errors this time are:
   202  	want2 := []string{
   203  		"cannot load a.star: cannot load c.star: cannot load b.star: cycle in load graph", // from b
   204  		"cannot load b.star: cycle in load graph",                                         // from c
   205  	}
   206  	if !reflect.DeepEqual(got, want1) && !reflect.DeepEqual(got, want2) {
   207  		t.Error(got)
   208  	}
   209  }
   210  
   211  // cache is a concurrency-safe, duplicate-suppressing,
   212  // non-blocking cache of the doLoad function.
   213  // See Section 9.7 of gopl.io for an explanation of this structure.
   214  // It also features online deadlock (load cycle) detection.
   215  type cache struct {
   216  	cacheMu sync.Mutex
   217  	cache   map[string]*entry
   218  
   219  	fakeFilesystem map[string]string
   220  }
   221  
   222  type entry struct {
   223  	owner   unsafe.Pointer // a *cycleChecker; see cycleCheck
   224  	globals starlark.StringDict
   225  	err     error
   226  	ready   chan struct{}
   227  }
   228  
   229  func (c *cache) Load(module string) (starlark.StringDict, error) {
   230  	return c.get(new(cycleChecker), module)
   231  }
   232  
   233  // get loads and returns an entry (if not already loaded).
   234  func (c *cache) get(cc *cycleChecker, module string) (starlark.StringDict, error) {
   235  	c.cacheMu.Lock()
   236  	e := c.cache[module]
   237  	if e != nil {
   238  		c.cacheMu.Unlock()
   239  		// Some other goroutine is getting this module.
   240  		// Wait for it to become ready.
   241  
   242  		// Detect load cycles to avoid deadlocks.
   243  		if err := cycleCheck(e, cc); err != nil {
   244  			return nil, err
   245  		}
   246  
   247  		cc.setWaitsFor(e)
   248  		<-e.ready
   249  		cc.setWaitsFor(nil)
   250  	} else {
   251  		// First request for this module.
   252  		e = &entry{ready: make(chan struct{})}
   253  		c.cache[module] = e
   254  		c.cacheMu.Unlock()
   255  
   256  		e.setOwner(cc)
   257  		e.globals, e.err = c.doLoad(cc, module)
   258  		e.setOwner(nil)
   259  
   260  		// Broadcast that the entry is now ready.
   261  		close(e.ready)
   262  	}
   263  	return e.globals, e.err
   264  }
   265  
   266  func (c *cache) doLoad(cc *cycleChecker, module string) (starlark.StringDict, error) {
   267  	thread := &starlark.Thread{
   268  		Name:  "exec " + module,
   269  		Print: func(_ *starlark.Thread, msg string) { fmt.Println(msg) },
   270  		Load: func(_ *starlark.Thread, module string) (starlark.StringDict, error) {
   271  			// Tunnel the cycle-checker state for this "thread of loading".
   272  			return c.get(cc, module)
   273  		},
   274  	}
   275  	data := c.fakeFilesystem[module]
   276  	return starlark.ExecFile(thread, module, data, nil)
   277  }
   278  
   279  // -- concurrent cycle checking --
   280  
   281  // A cycleChecker is used for concurrent deadlock detection.
   282  // Each top-level call to Load creates its own cycleChecker,
   283  // which is passed to all recursive calls it makes.
   284  // It corresponds to a logical thread in the deadlock detection literature.
   285  type cycleChecker struct {
   286  	waitsFor unsafe.Pointer // an *entry; see cycleCheck
   287  }
   288  
   289  func (cc *cycleChecker) setWaitsFor(e *entry) {
   290  	atomic.StorePointer(&cc.waitsFor, unsafe.Pointer(e))
   291  }
   292  
   293  func (e *entry) setOwner(cc *cycleChecker) {
   294  	atomic.StorePointer(&e.owner, unsafe.Pointer(cc))
   295  }
   296  
   297  // cycleCheck reports whether there is a path in the waits-for graph
   298  // from resource 'e' to thread 'me'.
   299  //
   300  // The waits-for graph (WFG) is a bipartite graph whose nodes are
   301  // alternately of type entry and cycleChecker.  Each node has at most
   302  // one outgoing edge.  An entry has an "owner" edge to a cycleChecker
   303  // while it is being readied by that cycleChecker, and a cycleChecker
   304  // has a "waits-for" edge to an entry while it is waiting for that entry
   305  // to become ready.
   306  //
   307  // Before adding a waits-for edge, the cache checks whether the new edge
   308  // would form a cycle.  If so, this indicates that the load graph is
   309  // cyclic and that the following wait operation would deadlock.
   310  func cycleCheck(e *entry, me *cycleChecker) error {
   311  	for e != nil {
   312  		cc := (*cycleChecker)(atomic.LoadPointer(&e.owner))
   313  		if cc == nil {
   314  			break
   315  		}
   316  		if cc == me {
   317  			return fmt.Errorf("cycle in load graph")
   318  		}
   319  		e = (*entry)(atomic.LoadPointer(&cc.waitsFor))
   320  	}
   321  	return nil
   322  }