blob: 94f8947eeb0c646916260db2bb39a7b79f2b0874 [file] [log] [blame]
// 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
}
}
}
// 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
}
}