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