github.com/lab47/exprcore@v0.0.0-20210525052339-fb7d6bd9331e/repl/repl.go (about)

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