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