go.starlark.net@v0.0.0-20231101134539-556fd59b42f6/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 "go.starlark.net/repl" 14 15 import ( 16 "context" 17 "fmt" 18 "io" 19 "os" 20 "os/signal" 21 22 "github.com/chzyer/readline" 23 "go.starlark.net/starlark" 24 "go.starlark.net/syntax" 25 ) 26 27 var interrupted = make(chan os.Signal, 1) 28 29 // REPL calls [REPLOptions] using [syntax.LegacyFileOptions]. 30 // Deprecated: relies on legacy global variables. 31 func REPL(thread *starlark.Thread, globals starlark.StringDict) { 32 REPLOptions(syntax.LegacyFileOptions(), thread, globals) 33 } 34 35 // REPLOptions executes a read, eval, print loop. 36 // 37 // Before evaluating each expression, it sets the Starlark thread local 38 // variable named "context" to a context.Context that is cancelled by a 39 // SIGINT (Control-C). Client-supplied global functions may use this 40 // context to make long-running operations interruptable. 41 func REPLOptions(opts *syntax.FileOptions, thread *starlark.Thread, globals starlark.StringDict) { 42 signal.Notify(interrupted, os.Interrupt) 43 defer signal.Stop(interrupted) 44 45 rl, err := readline.New(">>> ") 46 if err != nil { 47 PrintError(err) 48 return 49 } 50 defer rl.Close() 51 for { 52 if err := rep(opts, rl, thread, globals); err != nil { 53 if err == readline.ErrInterrupt { 54 fmt.Println(err) 55 continue 56 } 57 break 58 } 59 } 60 } 61 62 // rep reads, evaluates, and prints one item. 63 // 64 // It returns an error (possibly readline.ErrInterrupt) 65 // only if readline failed. Starlark errors are printed. 66 func rep(opts *syntax.FileOptions, rl *readline.Instance, thread *starlark.Thread, globals starlark.StringDict) error { 67 // Each item gets its own context, 68 // which is cancelled by a SIGINT. 69 // 70 // Note: during Readline calls, Control-C causes Readline to return 71 // ErrInterrupt but does not generate a SIGINT. 72 ctx, cancel := context.WithCancel(context.Background()) 73 defer cancel() 74 go func() { 75 select { 76 case <-interrupted: 77 cancel() 78 case <-ctx.Done(): 79 } 80 }() 81 82 thread.SetLocal("context", ctx) 83 84 eof := false 85 86 // readline returns EOF, ErrInterrupted, or a line including "\n". 87 rl.SetPrompt(">>> ") 88 readline := func() ([]byte, error) { 89 line, err := rl.Readline() 90 rl.SetPrompt("... ") 91 if err != nil { 92 if err == io.EOF { 93 eof = true 94 } 95 return nil, err 96 } 97 return []byte(line + "\n"), nil 98 } 99 100 // Treat load bindings as global (like they used to be) in the REPL. 101 // Fixes github.com/google/starlark-go/issues/224. 102 opts2 := *opts 103 opts2.LoadBindsGlobally = true 104 opts = &opts2 105 106 // parse 107 f, err := opts.ParseCompoundStmt("<stdin>", readline) 108 if err != nil { 109 if eof { 110 return io.EOF 111 } 112 PrintError(err) 113 return nil 114 } 115 116 if expr := soleExpr(f); expr != nil { 117 // eval 118 v, err := starlark.EvalExprOptions(f.Options, thread, expr, globals) 119 if err != nil { 120 PrintError(err) 121 return nil 122 } 123 124 // print 125 if v != starlark.None { 126 fmt.Println(v) 127 } 128 } else if err := starlark.ExecREPLChunk(f, thread, globals); err != nil { 129 PrintError(err) 130 return nil 131 } 132 133 return nil 134 } 135 136 func soleExpr(f *syntax.File) syntax.Expr { 137 if len(f.Stmts) == 1 { 138 if stmt, ok := f.Stmts[0].(*syntax.ExprStmt); ok { 139 return stmt.X 140 } 141 } 142 return nil 143 } 144 145 // PrintError prints the error to stderr, 146 // or its backtrace if it is a Starlark evaluation error. 147 func PrintError(err error) { 148 if evalErr, ok := err.(*starlark.EvalError); ok { 149 fmt.Fprintln(os.Stderr, evalErr.Backtrace()) 150 } else { 151 fmt.Fprintln(os.Stderr, err) 152 } 153 } 154 155 // MakeLoad calls [MakeLoadOptions] using [syntax.LegacyFileOptions]. 156 // Deprecated: relies on legacy global variables. 157 func MakeLoad() func(thread *starlark.Thread, module string) (starlark.StringDict, error) { 158 return MakeLoadOptions(syntax.LegacyFileOptions()) 159 } 160 161 // MakeLoadOptions returns a simple sequential implementation of module loading 162 // suitable for use in the REPL. 163 // Each function returned by MakeLoadOptions accesses a distinct private cache. 164 func MakeLoadOptions(opts *syntax.FileOptions) func(thread *starlark.Thread, module string) (starlark.StringDict, error) { 165 type entry struct { 166 globals starlark.StringDict 167 err error 168 } 169 170 var cache = make(map[string]*entry) 171 172 return func(thread *starlark.Thread, module string) (starlark.StringDict, error) { 173 e, ok := cache[module] 174 if e == nil { 175 if ok { 176 // request for package whose loading is in progress 177 return nil, fmt.Errorf("cycle in load graph") 178 } 179 180 // Add a placeholder to indicate "load in progress". 181 cache[module] = nil 182 183 // Load it. 184 thread := &starlark.Thread{Name: "exec " + module, Load: thread.Load} 185 globals, err := starlark.ExecFileOptions(opts, thread, module, nil, nil) 186 e = &entry{globals, err} 187 188 // Update the cache. 189 cache[module] = e 190 } 191 return e.globals, e.err 192 } 193 }