xref: /aosp_15_r20/external/starlark-go/repl/repl.go (revision 4947cdc739c985f6d86941e22894f5cefe7c9e9a)
1*4947cdc7SCole Faust// Package repl provides a read/eval/print loop for Starlark.
2*4947cdc7SCole Faust//
3*4947cdc7SCole Faust// It supports readline-style command editing,
4*4947cdc7SCole Faust// and interrupts through Control-C.
5*4947cdc7SCole Faust//
6*4947cdc7SCole Faust// If an input line can be parsed as an expression,
7*4947cdc7SCole Faust// the REPL parses and evaluates it and prints its result.
8*4947cdc7SCole Faust// Otherwise the REPL reads lines until a blank line,
9*4947cdc7SCole Faust// then tries again to parse the multi-line input as an
10*4947cdc7SCole Faust// expression. If the input still cannot be parsed as an expression,
11*4947cdc7SCole Faust// the REPL parses and executes it as a file (a list of statements),
12*4947cdc7SCole Faust// for side effects.
13*4947cdc7SCole Faustpackage repl // import "go.starlark.net/repl"
14*4947cdc7SCole Faust
15*4947cdc7SCole Faustimport (
16*4947cdc7SCole Faust	"context"
17*4947cdc7SCole Faust	"fmt"
18*4947cdc7SCole Faust	"io"
19*4947cdc7SCole Faust	"os"
20*4947cdc7SCole Faust	"os/signal"
21*4947cdc7SCole Faust
22*4947cdc7SCole Faust	"github.com/chzyer/readline"
23*4947cdc7SCole Faust	"go.starlark.net/resolve"
24*4947cdc7SCole Faust	"go.starlark.net/starlark"
25*4947cdc7SCole Faust	"go.starlark.net/syntax"
26*4947cdc7SCole Faust)
27*4947cdc7SCole Faust
28*4947cdc7SCole Faustvar interrupted = make(chan os.Signal, 1)
29*4947cdc7SCole Faust
30*4947cdc7SCole Faust// REPL executes a read, eval, print loop.
31*4947cdc7SCole Faust//
32*4947cdc7SCole Faust// Before evaluating each expression, it sets the Starlark thread local
33*4947cdc7SCole Faust// variable named "context" to a context.Context that is cancelled by a
34*4947cdc7SCole Faust// SIGINT (Control-C). Client-supplied global functions may use this
35*4947cdc7SCole Faust// context to make long-running operations interruptable.
36*4947cdc7SCole Faust//
37*4947cdc7SCole Faustfunc REPL(thread *starlark.Thread, globals starlark.StringDict) {
38*4947cdc7SCole Faust	signal.Notify(interrupted, os.Interrupt)
39*4947cdc7SCole Faust	defer signal.Stop(interrupted)
40*4947cdc7SCole Faust
41*4947cdc7SCole Faust	rl, err := readline.New(">>> ")
42*4947cdc7SCole Faust	if err != nil {
43*4947cdc7SCole Faust		PrintError(err)
44*4947cdc7SCole Faust		return
45*4947cdc7SCole Faust	}
46*4947cdc7SCole Faust	defer rl.Close()
47*4947cdc7SCole Faust	for {
48*4947cdc7SCole Faust		if err := rep(rl, thread, globals); err != nil {
49*4947cdc7SCole Faust			if err == readline.ErrInterrupt {
50*4947cdc7SCole Faust				fmt.Println(err)
51*4947cdc7SCole Faust				continue
52*4947cdc7SCole Faust			}
53*4947cdc7SCole Faust			break
54*4947cdc7SCole Faust		}
55*4947cdc7SCole Faust	}
56*4947cdc7SCole Faust	fmt.Println()
57*4947cdc7SCole Faust}
58*4947cdc7SCole Faust
59*4947cdc7SCole Faust// rep reads, evaluates, and prints one item.
60*4947cdc7SCole Faust//
61*4947cdc7SCole Faust// It returns an error (possibly readline.ErrInterrupt)
62*4947cdc7SCole Faust// only if readline failed. Starlark errors are printed.
63*4947cdc7SCole Faustfunc rep(rl *readline.Instance, thread *starlark.Thread, globals starlark.StringDict) error {
64*4947cdc7SCole Faust	// Each item gets its own context,
65*4947cdc7SCole Faust	// which is cancelled by a SIGINT.
66*4947cdc7SCole Faust	//
67*4947cdc7SCole Faust	// Note: during Readline calls, Control-C causes Readline to return
68*4947cdc7SCole Faust	// ErrInterrupt but does not generate a SIGINT.
69*4947cdc7SCole Faust	ctx, cancel := context.WithCancel(context.Background())
70*4947cdc7SCole Faust	defer cancel()
71*4947cdc7SCole Faust	go func() {
72*4947cdc7SCole Faust		select {
73*4947cdc7SCole Faust		case <-interrupted:
74*4947cdc7SCole Faust			cancel()
75*4947cdc7SCole Faust		case <-ctx.Done():
76*4947cdc7SCole Faust		}
77*4947cdc7SCole Faust	}()
78*4947cdc7SCole Faust
79*4947cdc7SCole Faust	thread.SetLocal("context", ctx)
80*4947cdc7SCole Faust
81*4947cdc7SCole Faust	eof := false
82*4947cdc7SCole Faust
83*4947cdc7SCole Faust	// readline returns EOF, ErrInterrupted, or a line including "\n".
84*4947cdc7SCole Faust	rl.SetPrompt(">>> ")
85*4947cdc7SCole Faust	readline := func() ([]byte, error) {
86*4947cdc7SCole Faust		line, err := rl.Readline()
87*4947cdc7SCole Faust		rl.SetPrompt("... ")
88*4947cdc7SCole Faust		if err != nil {
89*4947cdc7SCole Faust			if err == io.EOF {
90*4947cdc7SCole Faust				eof = true
91*4947cdc7SCole Faust			}
92*4947cdc7SCole Faust			return nil, err
93*4947cdc7SCole Faust		}
94*4947cdc7SCole Faust		return []byte(line + "\n"), nil
95*4947cdc7SCole Faust	}
96*4947cdc7SCole Faust
97*4947cdc7SCole Faust	// parse
98*4947cdc7SCole Faust	f, err := syntax.ParseCompoundStmt("<stdin>", readline)
99*4947cdc7SCole Faust	if err != nil {
100*4947cdc7SCole Faust		if eof {
101*4947cdc7SCole Faust			return io.EOF
102*4947cdc7SCole Faust		}
103*4947cdc7SCole Faust		PrintError(err)
104*4947cdc7SCole Faust		return nil
105*4947cdc7SCole Faust	}
106*4947cdc7SCole Faust
107*4947cdc7SCole Faust	// Treat load bindings as global (like they used to be) in the REPL.
108*4947cdc7SCole Faust	// This is a workaround for github.com/google/starlark-go/issues/224.
109*4947cdc7SCole Faust	// TODO(adonovan): not safe wrt concurrent interpreters.
110*4947cdc7SCole Faust	// Come up with a more principled solution (or plumb options everywhere).
111*4947cdc7SCole Faust	defer func(prev bool) { resolve.LoadBindsGlobally = prev }(resolve.LoadBindsGlobally)
112*4947cdc7SCole Faust	resolve.LoadBindsGlobally = true
113*4947cdc7SCole Faust
114*4947cdc7SCole Faust	if expr := soleExpr(f); expr != nil {
115*4947cdc7SCole Faust		// eval
116*4947cdc7SCole Faust		v, err := starlark.EvalExpr(thread, expr, globals)
117*4947cdc7SCole Faust		if err != nil {
118*4947cdc7SCole Faust			PrintError(err)
119*4947cdc7SCole Faust			return nil
120*4947cdc7SCole Faust		}
121*4947cdc7SCole Faust
122*4947cdc7SCole Faust		// print
123*4947cdc7SCole Faust		if v != starlark.None {
124*4947cdc7SCole Faust			fmt.Println(v)
125*4947cdc7SCole Faust		}
126*4947cdc7SCole Faust	} else if err := starlark.ExecREPLChunk(f, thread, globals); err != nil {
127*4947cdc7SCole Faust		PrintError(err)
128*4947cdc7SCole Faust		return nil
129*4947cdc7SCole Faust	}
130*4947cdc7SCole Faust
131*4947cdc7SCole Faust	return nil
132*4947cdc7SCole Faust}
133*4947cdc7SCole Faust
134*4947cdc7SCole Faustfunc soleExpr(f *syntax.File) syntax.Expr {
135*4947cdc7SCole Faust	if len(f.Stmts) == 1 {
136*4947cdc7SCole Faust		if stmt, ok := f.Stmts[0].(*syntax.ExprStmt); ok {
137*4947cdc7SCole Faust			return stmt.X
138*4947cdc7SCole Faust		}
139*4947cdc7SCole Faust	}
140*4947cdc7SCole Faust	return nil
141*4947cdc7SCole Faust}
142*4947cdc7SCole Faust
143*4947cdc7SCole Faust// PrintError prints the error to stderr,
144*4947cdc7SCole Faust// or its backtrace if it is a Starlark evaluation error.
145*4947cdc7SCole Faustfunc PrintError(err error) {
146*4947cdc7SCole Faust	if evalErr, ok := err.(*starlark.EvalError); ok {
147*4947cdc7SCole Faust		fmt.Fprintln(os.Stderr, evalErr.Backtrace())
148*4947cdc7SCole Faust	} else {
149*4947cdc7SCole Faust		fmt.Fprintln(os.Stderr, err)
150*4947cdc7SCole Faust	}
151*4947cdc7SCole Faust}
152*4947cdc7SCole Faust
153*4947cdc7SCole Faust// MakeLoad returns a simple sequential implementation of module loading
154*4947cdc7SCole Faust// suitable for use in the REPL.
155*4947cdc7SCole Faust// Each function returned by MakeLoad accesses a distinct private cache.
156*4947cdc7SCole Faustfunc MakeLoad() func(thread *starlark.Thread, module string) (starlark.StringDict, error) {
157*4947cdc7SCole Faust	type entry struct {
158*4947cdc7SCole Faust		globals starlark.StringDict
159*4947cdc7SCole Faust		err     error
160*4947cdc7SCole Faust	}
161*4947cdc7SCole Faust
162*4947cdc7SCole Faust	var cache = make(map[string]*entry)
163*4947cdc7SCole Faust
164*4947cdc7SCole Faust	return func(thread *starlark.Thread, module string) (starlark.StringDict, error) {
165*4947cdc7SCole Faust		e, ok := cache[module]
166*4947cdc7SCole Faust		if e == nil {
167*4947cdc7SCole Faust			if ok {
168*4947cdc7SCole Faust				// request for package whose loading is in progress
169*4947cdc7SCole Faust				return nil, fmt.Errorf("cycle in load graph")
170*4947cdc7SCole Faust			}
171*4947cdc7SCole Faust
172*4947cdc7SCole Faust			// Add a placeholder to indicate "load in progress".
173*4947cdc7SCole Faust			cache[module] = nil
174*4947cdc7SCole Faust
175*4947cdc7SCole Faust			// Load it.
176*4947cdc7SCole Faust			thread := &starlark.Thread{Name: "exec " + module, Load: thread.Load}
177*4947cdc7SCole Faust			globals, err := starlark.ExecFile(thread, module, nil, nil)
178*4947cdc7SCole Faust			e = &entry{globals, err}
179*4947cdc7SCole Faust
180*4947cdc7SCole Faust			// Update the cache.
181*4947cdc7SCole Faust			cache[module] = e
182*4947cdc7SCole Faust		}
183*4947cdc7SCole Faust		return e.globals, e.err
184*4947cdc7SCole Faust	}
185*4947cdc7SCole Faust}
186