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