blob: 97109c6af0f149734e5771ae9e2f91fe38f8e3a3 [file] [log] [blame]
alandonovan5c7d5aa2018-12-03 17:05:15 -05001// Package repl provides a read/eval/print loop for Starlark.
alandonovan55968252017-12-22 15:50:31 -05002//
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.
Alan Donovan551f3002018-11-01 09:44:00 -040013package repl // import "go.starlark.net/repl"
alandonovan55968252017-12-22 15:50:31 -050014
alandonovan55968252017-12-22 15:50:31 -050015import (
alandonovan55968252017-12-22 15:50:31 -050016 "context"
17 "fmt"
alandonovan30e71c62019-01-04 13:48:12 -050018 "io"
alandonovan55968252017-12-22 15:50:31 -050019 "os"
20 "os/signal"
alandonovan55968252017-12-22 15:50:31 -050021
22 "github.com/chzyer/readline"
alandonovan77c10992019-07-11 17:30:17 -040023 "go.starlark.net/resolve"
Alan Donovan6beab7e2018-10-31 17:53:09 -040024 "go.starlark.net/starlark"
25 "go.starlark.net/syntax"
alandonovan55968252017-12-22 15:50:31 -050026)
27
28var interrupted = make(chan os.Signal, 1)
29
30// REPL executes a read, eval, print loop.
31//
Alan Donovane3deafe2018-10-23 11:05:09 -040032// Before evaluating each expression, it sets the Starlark thread local
alandonovan55968252017-12-22 15:50:31 -050033// 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//
Alan Donovane3deafe2018-10-23 11:05:09 -040037func REPL(thread *starlark.Thread, globals starlark.StringDict) {
alandonovan55968252017-12-22 15:50:31 -050038 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)
Alan Donovane3deafe2018-10-23 11:05:09 -040062// only if readline failed. Starlark errors are printed.
63func rep(rl *readline.Instance, thread *starlark.Thread, globals starlark.StringDict) error {
alandonovan55968252017-12-22 15:50:31 -050064 // 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
alandonovan30e71c62019-01-04 13:48:12 -050081 eof := false
82
83 // readline returns EOF, ErrInterrupted, or a line including "\n".
alandonovan55968252017-12-22 15:50:31 -050084 rl.SetPrompt(">>> ")
alandonovan30e71c62019-01-04 13:48:12 -050085 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
alandonovan55968252017-12-22 15:50:31 -050093 }
alandonovan30e71c62019-01-04 13:48:12 -050094 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)
alandonovan55968252017-12-22 15:50:31 -0500104 return nil
105 }
106
alandonovan77c10992019-07-11 17:30:17 -0400107 // Treat load bindings as global (like they used to be) in the REPL.
108 // This is a workaround for github.com/google/starlark-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
alandonovan30e71c62019-01-04 13:48:12 -0500114 if expr := soleExpr(f); expr != nil {
115 // eval
116 v, err := starlark.EvalExpr(thread, expr, globals)
117 if err != nil {
118 PrintError(err)
alandonovan55968252017-12-22 15:50:31 -0500119 return nil
120 }
alandonovan55968252017-12-22 15:50:31 -0500121
alandonovan30e71c62019-01-04 13:48:12 -0500122 // print
123 if v != starlark.None {
alandonovan55968252017-12-22 15:50:31 -0500124 fmt.Println(v)
125 }
alandonovan28350e62019-10-21 14:58:36 -0400126 } else if err := starlark.ExecREPLChunk(f, thread, globals); err != nil {
127 PrintError(err)
128 return nil
alandonovan55968252017-12-22 15:50:31 -0500129 }
130
131 return nil
132}
133
alandonovan30e71c62019-01-04 13:48:12 -0500134func 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 }
alandonovan55968252017-12-22 15:50:31 -0500139 }
alandonovan30e71c62019-01-04 13:48:12 -0500140 return nil
alandonovan55968252017-12-22 15:50:31 -0500141}
142
143// PrintError prints the error to stderr,
Alan Donovane3deafe2018-10-23 11:05:09 -0400144// or its backtrace if it is a Starlark evaluation error.
alandonovan55968252017-12-22 15:50:31 -0500145func PrintError(err error) {
Alan Donovane3deafe2018-10-23 11:05:09 -0400146 if evalErr, ok := err.(*starlark.EvalError); ok {
alandonovan55968252017-12-22 15:50:31 -0500147 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.
Alan Donovane3deafe2018-10-23 11:05:09 -0400156func MakeLoad() func(thread *starlark.Thread, module string) (starlark.StringDict, error) {
alandonovan55968252017-12-22 15:50:31 -0500157 type entry struct {
Alan Donovane3deafe2018-10-23 11:05:09 -0400158 globals starlark.StringDict
alandonovan55968252017-12-22 15:50:31 -0500159 err error
160 }
161
162 var cache = make(map[string]*entry)
163
Alan Donovane3deafe2018-10-23 11:05:09 -0400164 return func(thread *starlark.Thread, module string) (starlark.StringDict, error) {
alandonovan55968252017-12-22 15:50:31 -0500165 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.
alandonovan2c1f3622018-12-17 13:10:16 -0500176 thread := &starlark.Thread{Name: "exec " + module, Load: thread.Load}
Alan Donovane3deafe2018-10-23 11:05:09 -0400177 globals, err := starlark.ExecFile(thread, module, nil, nil)
alandonovan55968252017-12-22 15:50:31 -0500178 e = &entry{globals, err}
179
180 // Update the cache.
181 cache[module] = e
182 }
183 return e.globals, e.err
184 }
185}