blob: 9df9d368f9939b2edca5ccd562503b0f321547df [file] [log] [blame]
Alan Donovan312d1a52017-10-02 10:10:28 -04001// 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
Alan Donovane3deafe2018-10-23 11:05:09 -04005package starlark_test
Alan Donovan312d1a52017-10-02 10:10:28 -04006
7import (
8 "fmt"
9 "log"
alandonovan4765c972018-11-20 13:58:46 -050010 "reflect"
Alan Donovan312d1a52017-10-02 10:10:28 -040011 "sort"
12 "strings"
13 "sync"
14 "sync/atomic"
alandonovan4765c972018-11-20 13:58:46 -050015 "testing"
Alan Donovan312d1a52017-10-02 10:10:28 -040016 "unsafe"
17
Alan Donovan6beab7e2018-10-31 17:53:09 -040018 "go.starlark.net/starlark"
Alan Donovan312d1a52017-10-02 10:10:28 -040019)
20
Nick Santos572cea22018-05-25 16:41:09 -040021// ExampleExecFile demonstrates a simple embedding
Alan Donovane3deafe2018-10-23 11:05:09 -040022// of the Starlark interpreter into a Go program.
Nick Santos572cea22018-05-25 16:41:09 -040023func ExampleExecFile() {
Alan Donovan312d1a52017-10-02 10:10:28 -040024 const data = `
25print(greeting + ", world")
26
27squares = [x*x for x in range(10)]
28`
29
Alan Donovane3deafe2018-10-23 11:05:09 -040030 thread := &starlark.Thread{
alandonovan2c1f3622018-12-17 13:10:16 -050031 Name: "example",
Alan Donovane3deafe2018-10-23 11:05:09 -040032 Print: func(_ *starlark.Thread, msg string) { fmt.Println(msg) },
Alan Donovan312d1a52017-10-02 10:10:28 -040033 }
Alan Donovane3deafe2018-10-23 11:05:09 -040034 predeclared := starlark.StringDict{
35 "greeting": starlark.String("hello"),
Alan Donovan312d1a52017-10-02 10:10:28 -040036 }
Alan Donovane3deafe2018-10-23 11:05:09 -040037 globals, err := starlark.ExecFile(thread, "apparent/filename.star", data, predeclared)
alandonovana1b28d82018-03-13 10:59:24 -040038 if err != nil {
Alan Donovane3deafe2018-10-23 11:05:09 -040039 if evalErr, ok := err.(*starlark.EvalError); ok {
Alan Donovan312d1a52017-10-02 10:10:28 -040040 log.Fatal(evalErr.Backtrace())
41 }
42 log.Fatal(err)
43 }
44
45 // Print the global environment.
46 var names []string
47 for name := range globals {
48 names = append(names, name)
49 }
50 sort.Strings(names)
51 fmt.Println("\nGlobals:")
52 for _, name := range names {
53 v := globals[name]
54 fmt.Printf("%s (%s) = %s\n", name, v.Type(), v.String())
55 }
56
57 // Output:
58 // hello, world
59 //
60 // Globals:
Alan Donovan312d1a52017-10-02 10:10:28 -040061 // squares (list) = [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
62}
63
Nick Santos572cea22018-05-25 16:41:09 -040064// ExampleThread_Load_sequential demonstrates a simple caching
Alan Donovan312d1a52017-10-02 10:10:28 -040065// implementation of 'load' that works sequentially.
Nick Santos572cea22018-05-25 16:41:09 -040066func ExampleThread_Load_sequential() {
Alan Donovan312d1a52017-10-02 10:10:28 -040067 fakeFilesystem := map[string]string{
Alan Donovane3deafe2018-10-23 11:05:09 -040068 "c.star": `load("b.star", "b"); c = b + "!"`,
69 "b.star": `load("a.star", "a"); b = a + ", world"`,
70 "a.star": `a = "Hello"`,
Alan Donovan312d1a52017-10-02 10:10:28 -040071 }
72
73 type entry struct {
Alan Donovane3deafe2018-10-23 11:05:09 -040074 globals starlark.StringDict
Alan Donovan312d1a52017-10-02 10:10:28 -040075 err error
76 }
77
78 cache := make(map[string]*entry)
79
Alan Donovane3deafe2018-10-23 11:05:09 -040080 var load func(_ *starlark.Thread, module string) (starlark.StringDict, error)
81 load = func(_ *starlark.Thread, module string) (starlark.StringDict, error) {
Alan Donovan312d1a52017-10-02 10:10:28 -040082 e, ok := cache[module]
83 if e == nil {
84 if ok {
85 // request for package whose loading is in progress
86 return nil, fmt.Errorf("cycle in load graph")
87 }
88
89 // Add a placeholder to indicate "load in progress".
90 cache[module] = nil
91
Alan Donovan6c3fcb82017-10-10 15:47:38 -040092 // Load and initialize the module in a new thread.
Alan Donovan312d1a52017-10-02 10:10:28 -040093 data := fakeFilesystem[module]
alandonovan2c1f3622018-12-17 13:10:16 -050094 thread := &starlark.Thread{Name: "exec " + module, Load: load}
Alan Donovane3deafe2018-10-23 11:05:09 -040095 globals, err := starlark.ExecFile(thread, module, data, nil)
Alan Donovan312d1a52017-10-02 10:10:28 -040096 e = &entry{globals, err}
97
98 // Update the cache.
99 cache[module] = e
100 }
101 return e.globals, e.err
102 }
103
alandonovan2c1f3622018-12-17 13:10:16 -0500104 thread := &starlark.Thread{Name: "exec c.star", Load: load}
Alan Donovane3deafe2018-10-23 11:05:09 -0400105 globals, err := load(thread, "c.star")
Alan Donovan312d1a52017-10-02 10:10:28 -0400106 if err != nil {
107 log.Fatal(err)
108 }
109 fmt.Println(globals["c"])
110
111 // Output:
112 // "Hello, world!"
113}
114
Nick Santos572cea22018-05-25 16:41:09 -0400115// ExampleThread_Load_parallel demonstrates a parallel implementation
Alan Donovan312d1a52017-10-02 10:10:28 -0400116// of 'load' with caching, duplicate suppression, and cycle detection.
Nick Santos572cea22018-05-25 16:41:09 -0400117func ExampleThread_Load_parallel() {
Alan Donovan312d1a52017-10-02 10:10:28 -0400118 cache := &cache{
119 cache: make(map[string]*entry),
120 fakeFilesystem: map[string]string{
Alan Donovane3deafe2018-10-23 11:05:09 -0400121 "c.star": `load("a.star", "a"); c = a * 2`,
122 "b.star": `load("a.star", "a"); b = a * 3`,
123 "a.star": `a = 1; print("loaded a")`,
Alan Donovan312d1a52017-10-02 10:10:28 -0400124 },
125 }
126
127 // We load modules b and c in parallel by concurrent calls to
128 // cache.Load. Both of them load module a, but a is executed
129 // only once, as witnessed by the sole output of its print
130 // statement.
131
132 ch := make(chan string)
133 for _, name := range []string{"b", "c"} {
134 go func(name string) {
Alan Donovane3deafe2018-10-23 11:05:09 -0400135 globals, err := cache.Load(name + ".star")
Alan Donovan312d1a52017-10-02 10:10:28 -0400136 if err != nil {
137 log.Fatal(err)
138 }
139 ch <- fmt.Sprintf("%s = %s", name, globals[name])
140 }(name)
141 }
142 got := []string{<-ch, <-ch}
143 sort.Strings(got)
144 fmt.Println(strings.Join(got, "\n"))
145
146 // Output:
147 // loaded a
148 // b = 3
149 // c = 2
150}
151
alandonovan4765c972018-11-20 13:58:46 -0500152// TestThread_Load_parallelCycle demonstrates detection
Alan Donovan312d1a52017-10-02 10:10:28 -0400153// of cycles during parallel loading.
alandonovan4765c972018-11-20 13:58:46 -0500154func TestThreadLoad_ParallelCycle(t *testing.T) {
Alan Donovan312d1a52017-10-02 10:10:28 -0400155 cache := &cache{
156 cache: make(map[string]*entry),
157 fakeFilesystem: map[string]string{
Alan Donovane3deafe2018-10-23 11:05:09 -0400158 "c.star": `load("b.star", "b"); c = b * 2`,
159 "b.star": `load("a.star", "a"); b = a * 3`,
160 "a.star": `load("c.star", "c"); a = c * 5; print("loaded a")`,
Alan Donovan312d1a52017-10-02 10:10:28 -0400161 },
162 }
163
164 ch := make(chan string)
165 for _, name := range "bc" {
166 name := string(name)
167 go func() {
Alan Donovane3deafe2018-10-23 11:05:09 -0400168 _, err := cache.Load(name + ".star")
Alan Donovan312d1a52017-10-02 10:10:28 -0400169 if err == nil {
Alan Donovane3deafe2018-10-23 11:05:09 -0400170 log.Fatalf("Load of %s.star succeeded unexpectedly", name)
Alan Donovan312d1a52017-10-02 10:10:28 -0400171 }
172 ch <- err.Error()
173 }()
174 }
175 got := []string{<-ch, <-ch}
176 sort.Strings(got)
Alan Donovan312d1a52017-10-02 10:10:28 -0400177
alandonovan4765c972018-11-20 13:58:46 -0500178 // Typically, the c goroutine quickly blocks behind b;
179 // b loads a, and a then fails to load c because it forms a cycle.
180 // The errors observed by the two goroutines are:
181 want1 := []string{
182 "cannot load a.star: cannot load c.star: cycle in load graph", // from b
183 "cannot load b.star: cannot load a.star: cannot load c.star: cycle in load graph", // from c
184 }
185 // But if the c goroutine is slow to start, b loads a,
186 // and a loads c; then c fails to load b because it forms a cycle.
187 // The errors this time are:
188 want2 := []string{
189 "cannot load a.star: cannot load c.star: cannot load b.star: cycle in load graph", // from b
190 "cannot load b.star: cycle in load graph", // from c
191 }
192 if !reflect.DeepEqual(got, want1) && !reflect.DeepEqual(got, want2) {
193 t.Error(got)
194 }
Alan Donovan312d1a52017-10-02 10:10:28 -0400195}
196
197// cache is a concurrency-safe, duplicate-suppressing,
198// non-blocking cache of the doLoad function.
199// See Section 9.7 of gopl.io for an explanation of this structure.
200// It also features online deadlock (load cycle) detection.
201type cache struct {
202 cacheMu sync.Mutex
203 cache map[string]*entry
204
205 fakeFilesystem map[string]string
206}
207
208type entry struct {
209 owner unsafe.Pointer // a *cycleChecker; see cycleCheck
Alan Donovane3deafe2018-10-23 11:05:09 -0400210 globals starlark.StringDict
Alan Donovan312d1a52017-10-02 10:10:28 -0400211 err error
212 ready chan struct{}
213}
214
Alan Donovane3deafe2018-10-23 11:05:09 -0400215func (c *cache) Load(module string) (starlark.StringDict, error) {
Alan Donovan312d1a52017-10-02 10:10:28 -0400216 return c.get(new(cycleChecker), module)
217}
218
219// get loads and returns an entry (if not already loaded).
Alan Donovane3deafe2018-10-23 11:05:09 -0400220func (c *cache) get(cc *cycleChecker, module string) (starlark.StringDict, error) {
Alan Donovan312d1a52017-10-02 10:10:28 -0400221 c.cacheMu.Lock()
222 e := c.cache[module]
223 if e != nil {
224 c.cacheMu.Unlock()
225 // Some other goroutine is getting this module.
226 // Wait for it to become ready.
227
228 // Detect load cycles to avoid deadlocks.
229 if err := cycleCheck(e, cc); err != nil {
230 return nil, err
231 }
232
233 cc.setWaitsFor(e)
234 <-e.ready
235 cc.setWaitsFor(nil)
236 } else {
237 // First request for this module.
238 e = &entry{ready: make(chan struct{})}
239 c.cache[module] = e
240 c.cacheMu.Unlock()
241
242 e.setOwner(cc)
243 e.globals, e.err = c.doLoad(cc, module)
244 e.setOwner(nil)
245
246 // Broadcast that the entry is now ready.
247 close(e.ready)
248 }
249 return e.globals, e.err
250}
251
Alan Donovane3deafe2018-10-23 11:05:09 -0400252func (c *cache) doLoad(cc *cycleChecker, module string) (starlark.StringDict, error) {
253 thread := &starlark.Thread{
alandonovan2c1f3622018-12-17 13:10:16 -0500254 Name: "exec " + module,
Alan Donovane3deafe2018-10-23 11:05:09 -0400255 Print: func(_ *starlark.Thread, msg string) { fmt.Println(msg) },
256 Load: func(_ *starlark.Thread, module string) (starlark.StringDict, error) {
Alan Donovan312d1a52017-10-02 10:10:28 -0400257 // Tunnel the cycle-checker state for this "thread of loading".
258 return c.get(cc, module)
259 },
260 }
261 data := c.fakeFilesystem[module]
Alan Donovane3deafe2018-10-23 11:05:09 -0400262 return starlark.ExecFile(thread, module, data, nil)
Alan Donovan312d1a52017-10-02 10:10:28 -0400263}
264
265// -- concurrent cycle checking --
266
267// A cycleChecker is used for concurrent deadlock detection.
268// Each top-level call to Load creates its own cycleChecker,
269// which is passed to all recursive calls it makes.
270// It corresponds to a logical thread in the deadlock detection literature.
271type cycleChecker struct {
272 waitsFor unsafe.Pointer // an *entry; see cycleCheck
273}
274
275func (cc *cycleChecker) setWaitsFor(e *entry) {
276 atomic.StorePointer(&cc.waitsFor, unsafe.Pointer(e))
277}
278
279func (e *entry) setOwner(cc *cycleChecker) {
280 atomic.StorePointer(&e.owner, unsafe.Pointer(cc))
281}
282
283// cycleCheck reports whether there is a path in the waits-for graph
284// from resource 'e' to thread 'me'.
285//
286// The waits-for graph (WFG) is a bipartite graph whose nodes are
287// alternately of type entry and cycleChecker. Each node has at most
288// one outgoing edge. An entry has an "owner" edge to a cycleChecker
289// while it is being readied by that cycleChecker, and a cycleChecker
290// has a "waits-for" edge to an entry while it is waiting for that entry
291// to become ready.
292//
293// Before adding a waits-for edge, the cache checks whether the new edge
294// would form a cycle. If so, this indicates that the load graph is
295// cyclic and that the following wait operation would deadlock.
296func cycleCheck(e *entry, me *cycleChecker) error {
297 for e != nil {
298 cc := (*cycleChecker)(atomic.LoadPointer(&e.owner))
299 if cc == nil {
300 break
301 }
302 if cc == me {
303 return fmt.Errorf("cycle in load graph")
304 }
305 e = (*entry)(atomic.LoadPointer(&cc.waitsFor))
306 }
307 return nil
308}