1// Package eval handles evaluation of parsed Elvish code and provides runtime
2// facilities.
3package eval
4
5import (
6	"fmt"
7	"io"
8	"os"
9	"strconv"
10	"sync"
11
12	"src.elv.sh/pkg/daemon/daemondefs"
13	"src.elv.sh/pkg/diag"
14	"src.elv.sh/pkg/env"
15	"src.elv.sh/pkg/eval/vals"
16	"src.elv.sh/pkg/eval/vars"
17	"src.elv.sh/pkg/logutil"
18	"src.elv.sh/pkg/parse"
19	"src.elv.sh/pkg/persistent/vector"
20)
21
22var logger = logutil.GetLogger("[eval] ")
23
24const (
25	// FnSuffix is the suffix for the variable names of functions. Defining a
26	// function "foo" is equivalent to setting a variable named "foo~", and vice
27	// versa.
28	FnSuffix = "~"
29	// NsSuffix is the suffix for the variable names of namespaces. Defining a
30	// namespace foo is equivalent to setting a variable named "foo:", and vice
31	// versa.
32	NsSuffix = ":"
33)
34
35const (
36	defaultValuePrefix        = "▶ "
37	defaultNotifyBgJobSuccess = true
38	initIndent                = vals.NoPretty
39)
40
41// Evaler provides methods for evaluating code, and maintains state that is
42// persisted between evaluation of different pieces of code. An Evaler is safe
43// to use concurrently.
44type Evaler struct {
45	// The following fields must only be set before the Evaler is used to
46	// evaluate any code; mutating them afterwards may cause race conditions.
47
48	// Command-line arguments, exposed as $args.
49	Args vals.List
50	// Hooks to run before exit or exec.
51	BeforeExit []func()
52	// Chdir hooks, exposed indirectly as $before-chdir and $after-chdir.
53	BeforeChdir, AfterChdir []func(string)
54	// TODO: Remove after the dir-history command is removed.
55	DaemonClient daemondefs.Client
56	// Directories to search libraries.
57	LibDirs []string
58	// Source code of internal bundled modules indexed by use specs.
59	BundledModules map[string]string
60	// Callback to notify the success or failure of background jobs. Must not be
61	// mutated once the Evaler is used to evaluate any code.
62	BgJobNotify func(string)
63
64	mu sync.RWMutex
65	// Mutations to fields below must be guarded by mutex.
66	//
67	// Note that this is *not* a GIL; most state mutations when executing Elvish
68	// code is localized and do not need to hold this mutex.
69	//
70	// TODO: Actually guard all mutations by this mutex.
71
72	global, builtin *Ns
73
74	deprecations deprecationRegistry
75
76	// Internal modules are indexed by use specs. External modules are indexed by
77	// absolute paths.
78	modules map[string]*Ns
79
80	// Various states and configs exposed to Elvish code.
81	//
82	// The prefix to prepend to value outputs when writing them to terminal,
83	// exposed as $value-out-prefix.
84	valuePrefix string
85	// Whether to notify the success of background jobs, exposed as
86	// $notify-bg-job-sucess.
87	notifyBgJobSuccess bool
88	// The current number of background jobs, exposed as $num-bg-jobs.
89	numBgJobs int
90}
91
92//elvdoc:var after-chdir
93//
94// A list of functions to run after changing directory. These functions are always
95// called with directory to change it, which might be a relative path. The
96// following example also shows `$before-chdir`:
97//
98// ```elvish-transcript
99// ~> before-chdir = [{|dir| echo "Going to change to "$dir", pwd is "$pwd }]
100// ~> after-chdir = [{|dir| echo "Changed to "$dir", pwd is "$pwd }]
101// ~> cd /usr
102// Going to change to /usr, pwd is /Users/xiaq
103// Changed to /usr, pwd is /usr
104// /usr> cd local
105// Going to change to local, pwd is /usr
106// Changed to local, pwd is /usr/local
107// /usr/local>
108// ```
109//
110// @cf before-chdir
111
112//elvdoc:var before-chdir
113//
114// A list of functions to run before changing directory. These functions are always
115// called with the new working directory.
116//
117// @cf after-chdir
118
119//elvdoc:var num-bg-jobs
120//
121// Number of background jobs.
122
123//elvdoc:var notify-bg-job-success
124//
125// Whether to notify success of background jobs, defaulting to `$true`.
126//
127// Failures of background jobs are always notified.
128
129//elvdoc:var value-out-indicator
130//
131// A string put before value outputs (such as those of of `put`). Defaults to
132// `'▶ '`. Example:
133//
134// ```elvish-transcript
135// ~> put lorem ipsum
136// ▶ lorem
137// ▶ ipsum
138// ~> value-out-indicator = 'val> '
139// ~> put lorem ipsum
140// val> lorem
141// val> ipsum
142// ```
143//
144// Note that you almost always want some trailing whitespace for readability.
145
146// NewEvaler creates a new Evaler.
147func NewEvaler() *Evaler {
148	builtin := builtinNs.Ns()
149	beforeChdirElvish, afterChdirElvish := vector.Empty, vector.Empty
150
151	ev := &Evaler{
152		global:  new(Ns),
153		builtin: builtin,
154
155		deprecations: newDeprecationRegistry(),
156
157		modules:        make(map[string]*Ns),
158		BundledModules: make(map[string]string),
159
160		valuePrefix:        defaultValuePrefix,
161		notifyBgJobSuccess: defaultNotifyBgJobSuccess,
162		numBgJobs:          0,
163		Args:               vals.EmptyList,
164	}
165
166	ev.BeforeChdir = []func(string){
167		adaptChdirHook("before-chdir", ev, &beforeChdirElvish)}
168	ev.AfterChdir = []func(string){
169		adaptChdirHook("after-chdir", ev, &afterChdirElvish)}
170
171	ev.ExtendBuiltin(BuildNs().
172		AddVar("pwd", NewPwdVar(ev)).
173		AddVar("before-chdir", vars.FromPtr(&beforeChdirElvish)).
174		AddVar("after-chdir", vars.FromPtr(&afterChdirElvish)).
175		AddVar("value-out-indicator",
176			vars.FromPtrWithMutex(&ev.valuePrefix, &ev.mu)).
177		AddVar("notify-bg-job-success",
178			vars.FromPtrWithMutex(&ev.notifyBgJobSuccess, &ev.mu)).
179		AddVar("num-bg-jobs",
180			vars.FromGet(func() interface{} { return strconv.Itoa(ev.getNumBgJobs()) })).
181		AddVar("args", vars.FromGet(func() interface{} { return ev.Args })))
182
183	// Install the "builtin" module after extension is complete.
184	ev.modules["builtin"] = ev.builtin
185
186	return ev
187}
188
189func adaptChdirHook(name string, ev *Evaler, pfns *vector.Vector) func(string) {
190	return func(path string) {
191		ports, cleanup := PortsFromStdFiles(ev.ValuePrefix())
192		defer cleanup()
193		callCfg := CallCfg{Args: []interface{}{path}, From: "[hook " + name + "]"}
194		evalCfg := EvalCfg{Ports: ports[:]}
195		for it := (*pfns).Iterator(); it.HasElem(); it.Next() {
196			fn, ok := it.Elem().(Callable)
197			if !ok {
198				fmt.Fprintln(os.Stderr, name, "hook must be callable")
199				continue
200			}
201			err := ev.Call(fn, callCfg, evalCfg)
202			if err != nil {
203				// TODO: Stack trace
204				fmt.Fprintln(os.Stderr, err)
205			}
206		}
207	}
208}
209
210// Access methods.
211
212// Global returns the global Ns.
213func (ev *Evaler) Global() *Ns {
214	ev.mu.RLock()
215	defer ev.mu.RUnlock()
216	return ev.global
217}
218
219// ExtendGlobal extends the global namespace with the given namespace.
220func (ev *Evaler) ExtendGlobal(ns Nser) {
221	ev.mu.Lock()
222	defer ev.mu.Unlock()
223	ev.global = CombineNs(ev.global, ns.Ns())
224}
225
226// Builtin returns the builtin Ns.
227func (ev *Evaler) Builtin() *Ns {
228	ev.mu.RLock()
229	defer ev.mu.RUnlock()
230	return ev.builtin
231}
232
233// ExtendBuiltin extends the builtin namespace with the given namespace.
234func (ev *Evaler) ExtendBuiltin(ns Nser) {
235	ev.mu.Lock()
236	defer ev.mu.Unlock()
237	ev.builtin = CombineNs(ev.builtin, ns.Ns())
238}
239
240func (ev *Evaler) registerDeprecation(d deprecation) bool {
241	ev.mu.Lock()
242	defer ev.mu.Unlock()
243	return ev.deprecations.register(d)
244}
245
246// AddModule add an internal module so that it can be used with "use $name" from
247// script.
248func (ev *Evaler) AddModule(name string, mod *Ns) {
249	ev.mu.Lock()
250	defer ev.mu.Unlock()
251	ev.modules[name] = mod
252}
253
254// ValuePrefix returns the prefix to prepend to value outputs when writing them
255// to terminal.
256func (ev *Evaler) ValuePrefix() string {
257	ev.mu.RLock()
258	defer ev.mu.RUnlock()
259	return ev.valuePrefix
260}
261
262func (ev *Evaler) getNotifyBgJobSuccess() bool {
263	ev.mu.RLock()
264	defer ev.mu.RUnlock()
265	return ev.notifyBgJobSuccess
266}
267
268func (ev *Evaler) getNumBgJobs() int {
269	ev.mu.RLock()
270	defer ev.mu.RUnlock()
271	return ev.numBgJobs
272}
273
274func (ev *Evaler) addNumBgJobs(delta int) {
275	ev.mu.Lock()
276	defer ev.mu.Unlock()
277	ev.numBgJobs += delta
278}
279
280// SetArgs sets the value of the $args variable to a list of strings, built from
281// the given slice. This method must be called before the Evaler is used to
282// evaluate any code.
283func (ev *Evaler) SetArgs(args []string) {
284	ev.Args = listOfStrings(args)
285}
286
287// AddBeforeChdir adds a function to run before changing directory. This method
288// must be called before the Evaler is used to evaluate any code.
289func (ev *Evaler) AddBeforeChdir(f func(string)) {
290	ev.BeforeChdir = append(ev.BeforeChdir, f)
291}
292
293// AddAfterChdir adds a function to run after changing directory. This method
294// must be called before the Evaler is used to evaluate any code.
295func (ev *Evaler) AddAfterChdir(f func(string)) {
296	ev.AfterChdir = append(ev.AfterChdir, f)
297}
298
299// AddBeforeExit adds a function to run before the Elvish process exits or gets
300// replaced (via "exec" on UNIX). This method must be called before the Evaler
301// is used to evaluate any code.
302func (ev *Evaler) AddBeforeExit(f func()) {
303	ev.BeforeExit = append(ev.BeforeExit, f)
304}
305
306// Chdir changes the current directory, and updates $E:PWD on success
307//
308// It runs the functions in beforeChdir immediately before changing the
309// directory, and the functions in afterChdir immediately after (if chdir was
310// successful). It returns nil as long as the directory changing part succeeds.
311func (ev *Evaler) Chdir(path string) error {
312	for _, hook := range ev.BeforeChdir {
313		hook(path)
314	}
315
316	err := os.Chdir(path)
317	if err != nil {
318		return err
319	}
320
321	for _, hook := range ev.AfterChdir {
322		hook(path)
323	}
324
325	pwd, err := os.Getwd()
326	if err != nil {
327		logger.Println("getwd after cd:", err)
328		return nil
329	}
330	os.Setenv(env.PWD, pwd)
331
332	return nil
333}
334
335// EvalCfg keeps configuration for the (*Evaler).Eval method.
336type EvalCfg struct {
337	// Ports to use in evaluation. The first 3 elements, if not specified
338	// (either being nil or Ports containing fewer than 3 elements),
339	// will be filled with DummyInputPort, DummyOutputPort and
340	// DummyOutputPort respectively.
341	Ports []*Port
342	// Callback to get a channel of interrupt signals and a function to call
343	// when the channel is no longer needed.
344	Interrupt func() (<-chan struct{}, func())
345	// Whether the Eval method should try to put the Elvish in the foreground
346	// after the code is executed.
347	PutInFg bool
348	// If not nil, used the given global namespace, instead of Evaler's own.
349	Global *Ns
350}
351
352func (cfg *EvalCfg) fillDefaults() {
353	if len(cfg.Ports) < 3 {
354		cfg.Ports = append(cfg.Ports, make([]*Port, 3-len(cfg.Ports))...)
355	}
356	if cfg.Ports[0] == nil {
357		cfg.Ports[0] = DummyInputPort
358	}
359	if cfg.Ports[1] == nil {
360		cfg.Ports[1] = DummyOutputPort
361	}
362	if cfg.Ports[2] == nil {
363		cfg.Ports[2] = DummyOutputPort
364	}
365}
366
367// Eval evaluates a piece of source code with the given configuration. The
368// returned error may be a parse error, compilation error or exception.
369func (ev *Evaler) Eval(src parse.Source, cfg EvalCfg) error {
370	cfg.fillDefaults()
371	errFile := cfg.Ports[2].File
372
373	tree, err := parse.Parse(src, parse.Config{WarningWriter: errFile})
374	if err != nil {
375		return err
376	}
377
378	ev.mu.Lock()
379	b := ev.builtin
380	defaultGlobal := cfg.Global == nil
381	if defaultGlobal {
382		// If cfg.Global is nil, use the Evaler's default global, and also
383		// mutate the default global.
384		cfg.Global = ev.global
385		// Continue to hold the mutex; it will be released when ev.global gets
386		// mutated.
387	} else {
388		ev.mu.Unlock()
389	}
390
391	op, err := compile(b.static(), cfg.Global.static(), tree, errFile)
392	if err != nil {
393		if defaultGlobal {
394			ev.mu.Unlock()
395		}
396		return err
397	}
398
399	fm, cleanup := ev.prepareFrame(src, cfg)
400	defer cleanup()
401
402	newLocal, exec := op.prepare(fm)
403	if defaultGlobal {
404		ev.global = newLocal
405		ev.mu.Unlock()
406	}
407
408	return exec()
409}
410
411// CallCfg keeps configuration for the (*Evaler).Call method.
412type CallCfg struct {
413	// Arguments to pass to the the function.
414	Args []interface{}
415	// Options to pass to the function.
416	Opts map[string]interface{}
417	// The name of the internal source that is calling the function.
418	From string
419}
420
421func (cfg *CallCfg) fillDefaults() {
422	if cfg.Opts == nil {
423		cfg.Opts = NoOpts
424	}
425	if cfg.From == "" {
426		cfg.From = "[internal]"
427	}
428}
429
430// Call calls a given function.
431func (ev *Evaler) Call(f Callable, callCfg CallCfg, evalCfg EvalCfg) error {
432	callCfg.fillDefaults()
433	evalCfg.fillDefaults()
434	if evalCfg.Global == nil {
435		evalCfg.Global = ev.Global()
436	}
437	fm, cleanup := ev.prepareFrame(parse.Source{Name: callCfg.From}, evalCfg)
438	defer cleanup()
439	return f.Call(fm, callCfg.Args, callCfg.Opts)
440}
441
442func (ev *Evaler) prepareFrame(src parse.Source, cfg EvalCfg) (*Frame, func()) {
443	var intCh <-chan struct{}
444	var intChCleanup func()
445	if cfg.Interrupt != nil {
446		intCh, intChCleanup = cfg.Interrupt()
447	}
448
449	ports := fillDefaultDummyPorts(cfg.Ports)
450
451	fm := &Frame{ev, src, cfg.Global, new(Ns), intCh, ports, nil, false}
452	return fm, func() {
453		if intChCleanup != nil {
454			intChCleanup()
455		}
456		if cfg.PutInFg {
457			err := putSelfInFg()
458			if err != nil {
459				fmt.Fprintln(ports[2].File,
460					"failed to put myself in foreground:", err)
461			}
462		}
463	}
464}
465
466func fillDefaultDummyPorts(ports []*Port) []*Port {
467	growPorts(&ports, 3)
468	if ports[0] == nil {
469		ports[0] = DummyInputPort
470	}
471	if ports[1] == nil {
472		ports[1] = DummyOutputPort
473	}
474	if ports[2] == nil {
475		ports[2] = DummyOutputPort
476	}
477	return ports
478}
479
480// Check checks the given source code for any parse error and compilation error.
481// It always tries to compile the code even if there is a parse error; both
482// return values may be non-nil. If w is not nil, deprecation messages are
483// written to it.
484func (ev *Evaler) Check(src parse.Source, w io.Writer) (*parse.Error, *diag.Error) {
485	tree, parseErr := parse.Parse(src, parse.Config{WarningWriter: w})
486	return parse.GetError(parseErr), ev.CheckTree(tree, w)
487}
488
489// CheckTree checks the given parsed source tree for compilation errors. If w is
490// not nil, deprecation messages are written to it.
491func (ev *Evaler) CheckTree(tree parse.Tree, w io.Writer) *diag.Error {
492	_, compileErr := ev.compile(tree, ev.Global(), w)
493	return GetCompilationError(compileErr)
494}
495
496// Compiles a parsed tree.
497func (ev *Evaler) compile(tree parse.Tree, g *Ns, w io.Writer) (nsOp, error) {
498	return compile(ev.Builtin().static(), g.static(), tree, w)
499}
500