xref: /aosp_15_r20/external/starlark-go/repl/repl.go (revision 4947cdc739c985f6d86941e22894f5cefe7c9e9a)
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.
13package repl // import "go.starlark.net/repl"
14
15import (
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
28var 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//
37func 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	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. Starlark errors are printed.
63func rep(rl *readline.Instance, thread *starlark.Thread, globals starlark.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/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
114	if expr := soleExpr(f); expr != nil {
115		// eval
116		v, err := starlark.EvalExpr(thread, expr, globals)
117		if err != nil {
118			PrintError(err)
119			return nil
120		}
121
122		// print
123		if v != starlark.None {
124			fmt.Println(v)
125		}
126	} else if err := starlark.ExecREPLChunk(f, thread, globals); err != nil {
127		PrintError(err)
128		return nil
129	}
130
131	return nil
132}
133
134func 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 Starlark evaluation error.
145func PrintError(err error) {
146	if evalErr, ok := err.(*starlark.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.
156func MakeLoad() func(thread *starlark.Thread, module string) (starlark.StringDict, error) {
157	type entry struct {
158		globals starlark.StringDict
159		err     error
160	}
161
162	var cache = make(map[string]*entry)
163
164	return func(thread *starlark.Thread, module string) (starlark.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 := &starlark.Thread{Name: "exec " + module, Load: thread.Load}
177			globals, err := starlark.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}
186