github.com/k14s/starlark-go@v0.0.0-20200720175618-3a5c849cc368/repl/repl.go (about) 1 // Package repl provides a read/eval/print loop for Starlark. 2 // 3 // It supports readline-style command editing, 4 // and interrupts through Control-C. 5 // 6 // If an input line can be parsed as an expression, 7 // the REPL parses and evaluates it and prints its result. 8 // Otherwise the REPL reads lines until a blank line, 9 // then tries again to parse the multi-line input as an 10 // expression. If the input still cannot be parsed as an expression, 11 // the REPL parses and executes it as a file (a list of statements), 12 // for side effects. 13 package repl // import "github.com/k14s/starlark-go/repl" 14 15 // TODO(adonovan): 16 // 17 // - Unparenthesized tuples are not parsed as a single expression: 18 // >>> (1, 2) 19 // (1, 2) 20 // >>> 1, 2 21 // ... 22 // >>> 23 // This is not necessarily a bug. 24 25 import ( 26 "context" 27 "fmt" 28 "io" 29 "os" 30 "os/signal" 31 32 "github.com/chzyer/readline" 33 "github.com/k14s/starlark-go/resolve" 34 "github.com/k14s/starlark-go/starlark" 35 "github.com/k14s/starlark-go/syntax" 36 ) 37 38 var interrupted = make(chan os.Signal, 1) 39 40 // REPL executes a read, eval, print loop. 41 // 42 // Before evaluating each expression, it sets the Starlark thread local 43 // variable named "context" to a context.Context that is cancelled by a 44 // SIGINT (Control-C). Client-supplied global functions may use this 45 // context to make long-running operations interruptable. 46 // 47 func REPL(thread *starlark.Thread, globals starlark.StringDict) { 48 signal.Notify(interrupted, os.Interrupt) 49 defer signal.Stop(interrupted) 50 51 rl, err := readline.New(">>> ") 52 if err != nil { 53 PrintError(err) 54 return 55 } 56 defer rl.Close() 57 for { 58 if err := rep(rl, thread, globals); err != nil { 59 if err == readline.ErrInterrupt { 60 fmt.Println(err) 61 continue 62 } 63 break 64 } 65 } 66 fmt.Println() 67 } 68 69 // rep reads, evaluates, and prints one item. 70 // 71 // It returns an error (possibly readline.ErrInterrupt) 72 // only if readline failed. Starlark errors are printed. 73 func rep(rl *readline.Instance, thread *starlark.Thread, globals starlark.StringDict) error { 74 // Each item gets its own context, 75 // which is cancelled by a SIGINT. 76 // 77 // Note: during Readline calls, Control-C causes Readline to return 78 // ErrInterrupt but does not generate a SIGINT. 79 ctx, cancel := context.WithCancel(context.Background()) 80 defer cancel() 81 go func() { 82 select { 83 case <-interrupted: 84 cancel() 85 case <-ctx.Done(): 86 } 87 }() 88 89 thread.SetLocal("context", ctx) 90 91 eof := false 92 93 // readline returns EOF, ErrInterrupted, or a line including "\n". 94 rl.SetPrompt(">>> ") 95 readline := func() ([]byte, error) { 96 line, err := rl.Readline() 97 rl.SetPrompt("... ") 98 if err != nil { 99 if err == io.EOF { 100 eof = true 101 } 102 return nil, err 103 } 104 return []byte(line + "\n"), nil 105 } 106 107 // parse 108 f, err := syntax.ParseCompoundStmt("<stdin>", readline) 109 if err != nil { 110 if eof { 111 return io.EOF 112 } 113 PrintError(err) 114 return nil 115 } 116 117 // Treat load bindings as global (like they used to be) in the REPL. 118 // This is a workaround for github.com/google/starlark-go/issues/224. 119 // TODO(adonovan): not safe wrt concurrent interpreters. 120 // Come up with a more principled solution (or plumb options everywhere). 121 defer func(prev bool) { resolve.LoadBindsGlobally = prev }(resolve.LoadBindsGlobally) 122 resolve.LoadBindsGlobally = true 123 124 if expr := soleExpr(f); expr != nil { 125 // eval 126 v, err := starlark.EvalExpr(thread, expr, globals) 127 if err != nil { 128 PrintError(err) 129 return nil 130 } 131 132 // print 133 if v != starlark.None { 134 fmt.Println(v) 135 } 136 } else { 137 // compile 138 prog, err := starlark.FileProgram(f, globals.Has) 139 if err != nil { 140 PrintError(err) 141 return nil 142 } 143 144 // execute (but do not freeze) 145 res, err := prog.Init(thread, globals) 146 if err != nil { 147 PrintError(err) 148 } 149 150 // The global names from the previous call become 151 // the predeclared names of this call. 152 // If execution failed, some globals may be undefined. 153 for k, v := range res { 154 globals[k] = v 155 } 156 } 157 158 return nil 159 } 160 161 func soleExpr(f *syntax.File) syntax.Expr { 162 if len(f.Stmts) == 1 { 163 if stmt, ok := f.Stmts[0].(*syntax.ExprStmt); ok { 164 return stmt.X 165 } 166 } 167 return nil 168 } 169 170 // PrintError prints the error to stderr, 171 // or its backtrace if it is a Starlark evaluation error. 172 func PrintError(err error) { 173 if evalErr, ok := err.(*starlark.EvalError); ok { 174 fmt.Fprintln(os.Stderr, evalErr.Backtrace()) 175 } else { 176 fmt.Fprintln(os.Stderr, err) 177 } 178 } 179 180 // MakeLoad returns a simple sequential implementation of module loading 181 // suitable for use in the REPL. 182 // Each function returned by MakeLoad accesses a distinct private cache. 183 func MakeLoad() func(thread *starlark.Thread, module string) (starlark.StringDict, error) { 184 type entry struct { 185 globals starlark.StringDict 186 err error 187 } 188 189 var cache = make(map[string]*entry) 190 191 return func(thread *starlark.Thread, module string) (starlark.StringDict, error) { 192 e, ok := cache[module] 193 if e == nil { 194 if ok { 195 // request for package whose loading is in progress 196 return nil, fmt.Errorf("cycle in load graph") 197 } 198 199 // Add a placeholder to indicate "load in progress". 200 cache[module] = nil 201 202 // Load it. 203 thread := &starlark.Thread{Name: "exec " + module, Load: thread.Load} 204 globals, err := starlark.ExecFile(thread, module, nil, nil) 205 e = &entry{globals, err} 206 207 // Update the cache. 208 cache[module] = e 209 } 210 return e.globals, e.err 211 } 212 }