diff options
Diffstat (limited to 'repl/repl.go')
-rw-r--r-- | repl/repl.go | 185 |
1 files changed, 185 insertions, 0 deletions
diff --git a/repl/repl.go b/repl/repl.go new file mode 100644 index 0000000..97109c6 --- /dev/null +++ b/repl/repl.go @@ -0,0 +1,185 @@ +// Package repl provides a read/eval/print loop for Starlark. +// +// It supports readline-style command editing, +// and interrupts through Control-C. +// +// If an input line can be parsed as an expression, +// the REPL parses and evaluates it and prints its result. +// Otherwise the REPL reads lines until a blank line, +// then tries again to parse the multi-line input as an +// expression. If the input still cannot be parsed as an expression, +// the REPL parses and executes it as a file (a list of statements), +// for side effects. +package repl // import "go.starlark.net/repl" + +import ( + "context" + "fmt" + "io" + "os" + "os/signal" + + "github.com/chzyer/readline" + "go.starlark.net/resolve" + "go.starlark.net/starlark" + "go.starlark.net/syntax" +) + +var interrupted = make(chan os.Signal, 1) + +// REPL executes a read, eval, print loop. +// +// Before evaluating each expression, it sets the Starlark thread local +// variable named "context" to a context.Context that is cancelled by a +// SIGINT (Control-C). Client-supplied global functions may use this +// context to make long-running operations interruptable. +// +func REPL(thread *starlark.Thread, globals starlark.StringDict) { + signal.Notify(interrupted, os.Interrupt) + defer signal.Stop(interrupted) + + rl, err := readline.New(">>> ") + if err != nil { + PrintError(err) + return + } + defer rl.Close() + for { + if err := rep(rl, thread, globals); err != nil { + if err == readline.ErrInterrupt { + fmt.Println(err) + continue + } + break + } + } + fmt.Println() +} + +// rep reads, evaluates, and prints one item. +// +// It returns an error (possibly readline.ErrInterrupt) +// only if readline failed. Starlark errors are printed. +func rep(rl *readline.Instance, thread *starlark.Thread, globals starlark.StringDict) error { + // Each item gets its own context, + // which is cancelled by a SIGINT. + // + // Note: during Readline calls, Control-C causes Readline to return + // ErrInterrupt but does not generate a SIGINT. + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go func() { + select { + case <-interrupted: + cancel() + case <-ctx.Done(): + } + }() + + thread.SetLocal("context", ctx) + + eof := false + + // readline returns EOF, ErrInterrupted, or a line including "\n". + rl.SetPrompt(">>> ") + readline := func() ([]byte, error) { + line, err := rl.Readline() + rl.SetPrompt("... ") + if err != nil { + if err == io.EOF { + eof = true + } + return nil, err + } + return []byte(line + "\n"), nil + } + + // parse + f, err := syntax.ParseCompoundStmt("<stdin>", readline) + if err != nil { + if eof { + return io.EOF + } + PrintError(err) + return nil + } + + // Treat load bindings as global (like they used to be) in the REPL. + // This is a workaround for github.com/google/starlark-go/issues/224. + // TODO(adonovan): not safe wrt concurrent interpreters. + // Come up with a more principled solution (or plumb options everywhere). + defer func(prev bool) { resolve.LoadBindsGlobally = prev }(resolve.LoadBindsGlobally) + resolve.LoadBindsGlobally = true + + if expr := soleExpr(f); expr != nil { + // eval + v, err := starlark.EvalExpr(thread, expr, globals) + if err != nil { + PrintError(err) + return nil + } + + // print + if v != starlark.None { + fmt.Println(v) + } + } else if err := starlark.ExecREPLChunk(f, thread, globals); err != nil { + PrintError(err) + return nil + } + + return nil +} + +func soleExpr(f *syntax.File) syntax.Expr { + if len(f.Stmts) == 1 { + if stmt, ok := f.Stmts[0].(*syntax.ExprStmt); ok { + return stmt.X + } + } + return nil +} + +// PrintError prints the error to stderr, +// or its backtrace if it is a Starlark evaluation error. +func PrintError(err error) { + if evalErr, ok := err.(*starlark.EvalError); ok { + fmt.Fprintln(os.Stderr, evalErr.Backtrace()) + } else { + fmt.Fprintln(os.Stderr, err) + } +} + +// MakeLoad returns a simple sequential implementation of module loading +// suitable for use in the REPL. +// Each function returned by MakeLoad accesses a distinct private cache. +func MakeLoad() func(thread *starlark.Thread, module string) (starlark.StringDict, error) { + type entry struct { + globals starlark.StringDict + err error + } + + var cache = make(map[string]*entry) + + return func(thread *starlark.Thread, module string) (starlark.StringDict, error) { + e, ok := cache[module] + if e == nil { + if ok { + // request for package whose loading is in progress + return nil, fmt.Errorf("cycle in load graph") + } + + // Add a placeholder to indicate "load in progress". + cache[module] = nil + + // Load it. + thread := &starlark.Thread{Name: "exec " + module, Load: thread.Load} + globals, err := starlark.ExecFile(thread, module, nil, nil) + e = &entry{globals, err} + + // Update the cache. + cache[module] = e + } + return e.globals, e.err + } +} |