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