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