aboutsummaryrefslogtreecommitdiff
path: root/repl/repl.go
diff options
context:
space:
mode:
Diffstat (limited to 'repl/repl.go')
-rw-r--r--repl/repl.go185
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
+ }
+}