...

Source file src/go.starlark.net/repl/repl.go

Documentation: go.starlark.net/repl

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

View as plain text