blob: 5feca385e6fbad0b8e2d1810210bfc9e9fdfe567 [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")
alandonovandbbb7612019-07-02 15:30:00 -040026print(repeat("one"))
27print(repeat("mur", 2))
Alan Donovan312d1a52017-10-02 10:10:28 -040028squares = [x*x for x in range(10)]
29`
30
alandonovandbbb7612019-07-02 15:30:00 -040031 // 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.
Alan Donovane3deafe2018-10-23 11:05:09 -040043 thread := &starlark.Thread{
alandonovan2c1f3622018-12-17 13:10:16 -050044 Name: "example",
Alan Donovane3deafe2018-10-23 11:05:09 -040045 Print: func(_ *starlark.Thread, msg string) { fmt.Println(msg) },
Alan Donovan312d1a52017-10-02 10:10:28 -040046 }
alandonovandbbb7612019-07-02 15:30:00 -040047
48 // This dictionary defines the pre-declared environment.
Alan Donovane3deafe2018-10-23 11:05:09 -040049 predeclared := starlark.StringDict{
50 "greeting": starlark.String("hello"),
alandonovandbbb7612019-07-02 15:30:00 -040051 "repeat": starlark.NewBuiltin("repeat", repeat),
Alan Donovan312d1a52017-10-02 10:10:28 -040052 }
alandonovandbbb7612019-07-02 15:30:00 -040053
54 // Execute a program.
Alan Donovane3deafe2018-10-23 11:05:09 -040055 globals, err := starlark.ExecFile(thread, "apparent/filename.star", data, predeclared)
alandonovana1b28d82018-03-13 10:59:24 -040056 if err != nil {
Alan Donovane3deafe2018-10-23 11:05:09 -040057 if evalErr, ok := err.(*starlark.EvalError); ok {
Alan Donovan312d1a52017-10-02 10:10:28 -040058 log.Fatal(evalErr.Backtrace())
59 }
60 log.Fatal(err)
61 }
62
63 // Print the global environment.
Alan Donovan312d1a52017-10-02 10:10:28 -040064 fmt.Println("\nGlobals:")
alandonovan0ed7e5b2019-01-03 16:11:58 -050065 for _, name := range globals.Keys() {
Alan Donovan312d1a52017-10-02 10:10:28 -040066 v := globals[name]
67 fmt.Printf("%s (%s) = %s\n", name, v.Type(), v.String())
68 }
69
70 // Output:
71 // hello, world
alandonovandbbb7612019-07-02 15:30:00 -040072 // one
73 // murmur
Alan Donovan312d1a52017-10-02 10:10:28 -040074 //
75 // Globals:
Alan Donovan312d1a52017-10-02 10:10:28 -040076 // squares (list) = [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
77}
78
Nick Santos572cea22018-05-25 16:41:09 -040079// ExampleThread_Load_sequential demonstrates a simple caching
Alan Donovan312d1a52017-10-02 10:10:28 -040080// implementation of 'load' that works sequentially.
Nick Santos572cea22018-05-25 16:41:09 -040081func ExampleThread_Load_sequential() {
Alan Donovan312d1a52017-10-02 10:10:28 -040082 fakeFilesystem := map[string]string{
Alan Donovane3deafe2018-10-23 11:05:09 -040083 "c.star": `load("b.star", "b"); c = b + "!"`,
84 "b.star": `load("a.star", "a"); b = a + ", world"`,
85 "a.star": `a = "Hello"`,
Alan Donovan312d1a52017-10-02 10:10:28 -040086 }
87
88 type entry struct {
Alan Donovane3deafe2018-10-23 11:05:09 -040089 globals starlark.StringDict
Alan Donovan312d1a52017-10-02 10:10:28 -040090 err error
91 }
92
93 cache := make(map[string]*entry)
94
Alan Donovane3deafe2018-10-23 11:05:09 -040095 var load func(_ *starlark.Thread, module string) (starlark.StringDict, error)
96 load = func(_ *starlark.Thread, module string) (starlark.StringDict, error) {
Alan Donovan312d1a52017-10-02 10:10:28 -040097 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
Alan Donovan6c3fcb82017-10-10 15:47:38 -0400107 // Load and initialize the module in a new thread.
Alan Donovan312d1a52017-10-02 10:10:28 -0400108 data := fakeFilesystem[module]
alandonovan2c1f3622018-12-17 13:10:16 -0500109 thread := &starlark.Thread{Name: "exec " + module, Load: load}
Alan Donovane3deafe2018-10-23 11:05:09 -0400110 globals, err := starlark.ExecFile(thread, module, data, nil)
Alan Donovan312d1a52017-10-02 10:10:28 -0400111 e = &entry{globals, err}
112
113 // Update the cache.
114 cache[module] = e
115 }
116 return e.globals, e.err
117 }
118
Xùdōng Yángbc864be2021-01-26 17:14:01 +0100119 globals, err := load(nil, "c.star")
Alan Donovan312d1a52017-10-02 10:10:28 -0400120 if err != nil {
121 log.Fatal(err)
122 }
123 fmt.Println(globals["c"])
124
125 // Output:
126 // "Hello, world!"
127}
128
Nick Santos572cea22018-05-25 16:41:09 -0400129// ExampleThread_Load_parallel demonstrates a parallel implementation
Alan Donovan312d1a52017-10-02 10:10:28 -0400130// of 'load' with caching, duplicate suppression, and cycle detection.
Nick Santos572cea22018-05-25 16:41:09 -0400131func ExampleThread_Load_parallel() {
Alan Donovan312d1a52017-10-02 10:10:28 -0400132 cache := &cache{
133 cache: make(map[string]*entry),
134 fakeFilesystem: map[string]string{
Alan Donovane3deafe2018-10-23 11:05:09 -0400135 "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")`,
Alan Donovan312d1a52017-10-02 10:10:28 -0400138 },
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) {
Alan Donovane3deafe2018-10-23 11:05:09 -0400149 globals, err := cache.Load(name + ".star")
Alan Donovan312d1a52017-10-02 10:10:28 -0400150 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
alandonovan4765c972018-11-20 13:58:46 -0500166// TestThread_Load_parallelCycle demonstrates detection
Alan Donovan312d1a52017-10-02 10:10:28 -0400167// of cycles during parallel loading.
alandonovan4765c972018-11-20 13:58:46 -0500168func TestThreadLoad_ParallelCycle(t *testing.T) {
Alan Donovan312d1a52017-10-02 10:10:28 -0400169 cache := &cache{
170 cache: make(map[string]*entry),
171 fakeFilesystem: map[string]string{
Alan Donovane3deafe2018-10-23 11:05:09 -0400172 "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")`,
Alan Donovan312d1a52017-10-02 10:10:28 -0400175 },
176 }
177
178 ch := make(chan string)
179 for _, name := range "bc" {
180 name := string(name)
181 go func() {
Alan Donovane3deafe2018-10-23 11:05:09 -0400182 _, err := cache.Load(name + ".star")
Alan Donovan312d1a52017-10-02 10:10:28 -0400183 if err == nil {
Alan Donovane3deafe2018-10-23 11:05:09 -0400184 log.Fatalf("Load of %s.star succeeded unexpectedly", name)
Alan Donovan312d1a52017-10-02 10:10:28 -0400185 }
186 ch <- err.Error()
187 }()
188 }
189 got := []string{<-ch, <-ch}
190 sort.Strings(got)
Alan Donovan312d1a52017-10-02 10:10:28 -0400191
alandonovan4765c972018-11-20 13:58:46 -0500192 // 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 }
Alan Donovan312d1a52017-10-02 10:10:28 -0400209}
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.
215type cache struct {
216 cacheMu sync.Mutex
217 cache map[string]*entry
218
219 fakeFilesystem map[string]string
220}
221
222type entry struct {
223 owner unsafe.Pointer // a *cycleChecker; see cycleCheck
Alan Donovane3deafe2018-10-23 11:05:09 -0400224 globals starlark.StringDict
Alan Donovan312d1a52017-10-02 10:10:28 -0400225 err error
226 ready chan struct{}
227}
228
Alan Donovane3deafe2018-10-23 11:05:09 -0400229func (c *cache) Load(module string) (starlark.StringDict, error) {
Alan Donovan312d1a52017-10-02 10:10:28 -0400230 return c.get(new(cycleChecker), module)
231}
232
233// get loads and returns an entry (if not already loaded).
Alan Donovane3deafe2018-10-23 11:05:09 -0400234func (c *cache) get(cc *cycleChecker, module string) (starlark.StringDict, error) {
Alan Donovan312d1a52017-10-02 10:10:28 -0400235 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
Alan Donovane3deafe2018-10-23 11:05:09 -0400266func (c *cache) doLoad(cc *cycleChecker, module string) (starlark.StringDict, error) {
267 thread := &starlark.Thread{
alandonovan2c1f3622018-12-17 13:10:16 -0500268 Name: "exec " + module,
Alan Donovane3deafe2018-10-23 11:05:09 -0400269 Print: func(_ *starlark.Thread, msg string) { fmt.Println(msg) },
270 Load: func(_ *starlark.Thread, module string) (starlark.StringDict, error) {
Alan Donovan312d1a52017-10-02 10:10:28 -0400271 // Tunnel the cycle-checker state for this "thread of loading".
272 return c.get(cc, module)
273 },
274 }
275 data := c.fakeFilesystem[module]
Alan Donovane3deafe2018-10-23 11:05:09 -0400276 return starlark.ExecFile(thread, module, data, nil)
Alan Donovan312d1a52017-10-02 10:10:28 -0400277}
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.
285type cycleChecker struct {
286 waitsFor unsafe.Pointer // an *entry; see cycleCheck
287}
288
289func (cc *cycleChecker) setWaitsFor(e *entry) {
290 atomic.StorePointer(&cc.waitsFor, unsafe.Pointer(e))
291}
292
293func (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.
310func 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}