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 }