github.com/kolbycrouch/elvish@v0.14.1-0.20210614162631-215b9ac1c423/pkg/eval/eval.go (about)

     1  // Package eval handles evaluation of parsed Elvish code and provides runtime
     2  // facilities.
     3  package eval
     4  
     5  import (
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"strconv"
    10  	"sync"
    11  
    12  	"src.elv.sh/pkg/daemon"
    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  
    22  var logger = logutil.GetLogger("[eval] ")
    23  
    24  const (
    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  
    35  const (
    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.
    44  type Evaler struct {
    45  	// All mutations to Evaler should be guarded by this mutex.
    46  	//
    47  	// Note that this is *not* a GIL; most state mutations when executing Elvish
    48  	// code is localized and do not need to hold this mutex.
    49  	//
    50  	// TODO: Actually guard all mutations by this mutex.
    51  	mu sync.RWMutex
    52  
    53  	global, builtin *Ns
    54  
    55  	deprecations deprecationRegistry
    56  
    57  	// State of the module system.
    58  	//
    59  	// Library directory.
    60  	libDir string
    61  	// Internal modules are indexed by use specs. External modules are indexed by
    62  	// absolute paths.
    63  	modules map[string]*Ns
    64  
    65  	// Various states and configs exposed to Elvish code.
    66  	//
    67  	// The prefix to prepend to value outputs when writing them to terminal,
    68  	// exposed as $value-out-prefix.
    69  	valuePrefix string
    70  	// Whether to notify the success of background jobs, exposed as
    71  	// $notify-bg-job-sucess.
    72  	notifyBgJobSuccess bool
    73  	// The current number of background jobs, exposed as $num-bg-jobs.
    74  	numBgJobs int
    75  	// Command-line arguments, exposed as $args.
    76  	args vals.List
    77  	// Chdir hooks, exposed indirectly as $before-chdir and $after-chdir.
    78  	beforeChdir, afterChdir []func(string)
    79  
    80  	// Dependencies.
    81  	//
    82  	// TODO: Remove these dependency by providing more general extension points.
    83  	daemonClient daemon.Client
    84  }
    85  
    86  // Editor is the interface that the line editor has to satisfy. It is needed so
    87  // that this package does not depend on the edit package.
    88  type Editor interface {
    89  	RunAfterCommandHooks(src parse.Source, duration float64, err error)
    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.
   147  func 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: map[string]*Ns{"builtin": builtin},
   158  
   159  		valuePrefix:        defaultValuePrefix,
   160  		notifyBgJobSuccess: defaultNotifyBgJobSuccess,
   161  		numBgJobs:          0,
   162  		args:               vals.EmptyList,
   163  	}
   164  
   165  	ev.beforeChdir = []func(string){
   166  		adaptChdirHook("before-chdir", ev, &beforeChdirElvish)}
   167  	ev.afterChdir = []func(string){
   168  		adaptChdirHook("after-chdir", ev, &afterChdirElvish)}
   169  
   170  	moreBuiltins := NsBuilder{}.
   171  		Add("pwd", NewPwdVar(ev)).
   172  		Add("before-chdir", vars.FromPtr(&beforeChdirElvish)).
   173  		Add("after-chdir", vars.FromPtr(&afterChdirElvish)).
   174  		Add("value-out-indicator", vars.FromPtrWithMutex(
   175  			&ev.valuePrefix, &ev.mu)).
   176  		Add("notify-bg-job-success", vars.FromPtrWithMutex(
   177  			&ev.notifyBgJobSuccess, &ev.mu)).
   178  		Add("num-bg-jobs", vars.FromGet(func() interface{} {
   179  			return strconv.Itoa(ev.getNumBgJobs())
   180  		})).
   181  		Add("args", vars.FromGet(func() interface{} {
   182  			return ev.getArgs()
   183  		})).
   184  		Ns()
   185  	builtin.slots = append(builtin.slots, moreBuiltins.slots...)
   186  	builtin.names = append(builtin.names, moreBuiltins.names...)
   187  	builtin.deleted = append(builtin.deleted, make([]bool, len(moreBuiltins.names))...)
   188  
   189  	return ev
   190  }
   191  
   192  func adaptChdirHook(name string, ev *Evaler, pfns *vector.Vector) func(string) {
   193  	return func(path string) {
   194  		ports, cleanup := PortsFromStdFiles(ev.ValuePrefix())
   195  		defer cleanup()
   196  		callCfg := CallCfg{Args: []interface{}{path}, From: "[hook " + name + "]"}
   197  		evalCfg := EvalCfg{Ports: ports[:]}
   198  		for it := (*pfns).Iterator(); it.HasElem(); it.Next() {
   199  			fn, ok := it.Elem().(Callable)
   200  			if !ok {
   201  				fmt.Fprintln(os.Stderr, name, "hook must be callable")
   202  				continue
   203  			}
   204  			err := ev.Call(fn, callCfg, evalCfg)
   205  			if err != nil {
   206  				// TODO: Stack trace
   207  				fmt.Fprintln(os.Stderr, err)
   208  			}
   209  		}
   210  	}
   211  }
   212  
   213  // Access methods.
   214  
   215  // Global returns the global Ns.
   216  func (ev *Evaler) Global() *Ns {
   217  	ev.mu.RLock()
   218  	defer ev.mu.RUnlock()
   219  	return ev.global
   220  }
   221  
   222  // AddGlobal merges the given *Ns into the global namespace.
   223  func (ev *Evaler) AddGlobal(ns *Ns) {
   224  	ev.mu.Lock()
   225  	defer ev.mu.Unlock()
   226  	ev.global = CombineNs(ev.global, ns)
   227  }
   228  
   229  // Builtin returns the builtin Ns.
   230  func (ev *Evaler) Builtin() *Ns {
   231  	ev.mu.RLock()
   232  	defer ev.mu.RUnlock()
   233  	return ev.builtin
   234  }
   235  
   236  // AddBuiltin merges the given *Ns into the builtin namespace.
   237  func (ev *Evaler) AddBuiltin(ns *Ns) {
   238  	ev.mu.Lock()
   239  	defer ev.mu.Unlock()
   240  	ev.builtin = CombineNs(ev.builtin, ns)
   241  }
   242  
   243  func (ev *Evaler) registerDeprecation(d deprecation) bool {
   244  	ev.mu.Lock()
   245  	defer ev.mu.Unlock()
   246  	return ev.deprecations.register(d)
   247  }
   248  
   249  // Returns libdir.
   250  func (ev *Evaler) getLibDir() string {
   251  	ev.mu.RLock()
   252  	defer ev.mu.RUnlock()
   253  	return ev.libDir
   254  }
   255  
   256  // SetLibDir sets the library directory for finding external modules.
   257  func (ev *Evaler) SetLibDir(libDir string) {
   258  	ev.mu.Lock()
   259  	defer ev.mu.Unlock()
   260  	ev.libDir = libDir
   261  }
   262  
   263  // AddModule add an internal module so that it can be used with "use $name" from
   264  // script.
   265  func (ev *Evaler) AddModule(name string, mod *Ns) {
   266  	ev.mu.Lock()
   267  	defer ev.mu.Unlock()
   268  	ev.modules[name] = mod
   269  }
   270  
   271  // ValuePrefix returns the prefix to prepend to value outputs when writing them
   272  // to terminal.
   273  func (ev *Evaler) ValuePrefix() string {
   274  	ev.mu.RLock()
   275  	defer ev.mu.RUnlock()
   276  	return ev.valuePrefix
   277  }
   278  
   279  func (ev *Evaler) getNotifyBgJobSuccess() bool {
   280  	ev.mu.RLock()
   281  	defer ev.mu.RUnlock()
   282  	return ev.notifyBgJobSuccess
   283  }
   284  
   285  func (ev *Evaler) getNumBgJobs() int {
   286  	ev.mu.RLock()
   287  	defer ev.mu.RUnlock()
   288  	return ev.numBgJobs
   289  }
   290  
   291  func (ev *Evaler) addNumBgJobs(delta int) {
   292  	ev.mu.Lock()
   293  	defer ev.mu.Unlock()
   294  	ev.numBgJobs += delta
   295  }
   296  
   297  func (ev *Evaler) getArgs() vals.List {
   298  	ev.mu.RLock()
   299  	defer ev.mu.RUnlock()
   300  	return ev.args
   301  }
   302  
   303  // SetArgs sets the value of the $args variable to a list of strings, built from
   304  // the given slice.
   305  func (ev *Evaler) SetArgs(args []string) {
   306  	v := listOfStrings(args)
   307  	ev.mu.Lock()
   308  	defer ev.mu.Unlock()
   309  	ev.args = v
   310  }
   311  
   312  // Returns copies of beforeChdir and afterChdir.
   313  func (ev *Evaler) chdirHooks() ([]func(string), []func(string)) {
   314  	ev.mu.RLock()
   315  	defer ev.mu.RUnlock()
   316  	return append(([]func(string))(nil), ev.beforeChdir...),
   317  		append(([]func(string))(nil), ev.afterChdir...)
   318  }
   319  
   320  // AddBeforeChdir adds a function to run before changing directory.
   321  func (ev *Evaler) AddBeforeChdir(f func(string)) {
   322  	ev.mu.Lock()
   323  	defer ev.mu.Unlock()
   324  	ev.beforeChdir = append(ev.beforeChdir, f)
   325  }
   326  
   327  // AddAfterChdir adds a function to run after changing directory.
   328  func (ev *Evaler) AddAfterChdir(f func(string)) {
   329  	ev.mu.Lock()
   330  	defer ev.mu.Unlock()
   331  	ev.afterChdir = append(ev.afterChdir, f)
   332  }
   333  
   334  // SetDaemonClient sets the daemon client associated with the Evaler.
   335  func (ev *Evaler) SetDaemonClient(client daemon.Client) {
   336  	ev.mu.Lock()
   337  	defer ev.mu.Unlock()
   338  	ev.daemonClient = client
   339  }
   340  
   341  // DaemonClient returns the daemon client associated with the Evaler.
   342  func (ev *Evaler) DaemonClient() daemon.Client {
   343  	ev.mu.RLock()
   344  	defer ev.mu.RUnlock()
   345  	return ev.daemonClient
   346  }
   347  
   348  // Chdir changes the current directory. On success it also updates the PWD
   349  // environment variable and records the new directory in the directory history.
   350  // It runs the functions in beforeChdir immediately before changing the
   351  // directory, and the functions in afterChdir immediately after (if chdir was
   352  // successful). It returns nil as long as the directory changing part succeeds.
   353  func (ev *Evaler) Chdir(path string) error {
   354  	beforeChdir, afterChdir := ev.chdirHooks()
   355  
   356  	for _, hook := range beforeChdir {
   357  		hook(path)
   358  	}
   359  
   360  	err := os.Chdir(path)
   361  	if err != nil {
   362  		return err
   363  	}
   364  
   365  	for _, hook := range afterChdir {
   366  		hook(path)
   367  	}
   368  
   369  	pwd, err := os.Getwd()
   370  	if err != nil {
   371  		logger.Println("getwd after cd:", err)
   372  		return nil
   373  	}
   374  	os.Setenv(env.PWD, pwd)
   375  
   376  	return nil
   377  }
   378  
   379  // EvalCfg keeps configuration for the (*Evaler).Eval method.
   380  type EvalCfg struct {
   381  	// Ports to use in evaluation. The first 3 elements, if not specified
   382  	// (either being nil or Ports containing fewer than 3 elements),
   383  	// will be filled with DummyInputPort, DummyOutputPort and
   384  	// DummyOutputPort respectively.
   385  	Ports []*Port
   386  	// Callback to get a channel of interrupt signals and a function to call
   387  	// when the channel is no longer needed.
   388  	Interrupt func() (<-chan struct{}, func())
   389  	// Whether the Eval method should try to put the Elvish in the foreground
   390  	// after the code is executed.
   391  	PutInFg bool
   392  	// If not nil, used the given global namespace, instead of Evaler's own.
   393  	Global *Ns
   394  }
   395  
   396  func (cfg *EvalCfg) fillDefaults() {
   397  	if len(cfg.Ports) < 3 {
   398  		cfg.Ports = append(cfg.Ports, make([]*Port, 3-len(cfg.Ports))...)
   399  	}
   400  	if cfg.Ports[0] == nil {
   401  		cfg.Ports[0] = DummyInputPort
   402  	}
   403  	if cfg.Ports[1] == nil {
   404  		cfg.Ports[1] = DummyOutputPort
   405  	}
   406  	if cfg.Ports[2] == nil {
   407  		cfg.Ports[2] = DummyOutputPort
   408  	}
   409  }
   410  
   411  // Eval evaluates a piece of source code with the given configuration. The
   412  // returned error may be a parse error, compilation error or exception.
   413  func (ev *Evaler) Eval(src parse.Source, cfg EvalCfg) error {
   414  	cfg.fillDefaults()
   415  	errFile := cfg.Ports[2].File
   416  
   417  	tree, err := parse.Parse(src, parse.Config{WarningWriter: errFile})
   418  	if err != nil {
   419  		return err
   420  	}
   421  
   422  	ev.mu.Lock()
   423  	b := ev.builtin
   424  	defaultGlobal := cfg.Global == nil
   425  	if defaultGlobal {
   426  		// If cfg.Global is nil, use the Evaler's default global, and also
   427  		// mutate the default global.
   428  		cfg.Global = ev.global
   429  		// Continue to hold the mutex; it will be released when ev.global gets
   430  		// mutated.
   431  	} else {
   432  		ev.mu.Unlock()
   433  	}
   434  
   435  	op, err := compile(b.static(), cfg.Global.static(), tree, errFile)
   436  	if err != nil {
   437  		if defaultGlobal {
   438  			ev.mu.Unlock()
   439  		}
   440  		return err
   441  	}
   442  
   443  	fm, cleanup := ev.prepareFrame(src, cfg)
   444  	defer cleanup()
   445  
   446  	newLocal, exec := op.prepare(fm)
   447  	if defaultGlobal {
   448  		ev.global = newLocal
   449  		ev.mu.Unlock()
   450  	}
   451  
   452  	return exec()
   453  }
   454  
   455  // CallCfg keeps configuration for the (*Evaler).Call method.
   456  type CallCfg struct {
   457  	// Arguments to pass to the the function.
   458  	Args []interface{}
   459  	// Options to pass to the function.
   460  	Opts map[string]interface{}
   461  	// The name of the internal source that is calling the function.
   462  	From string
   463  }
   464  
   465  func (cfg *CallCfg) fillDefaults() {
   466  	if cfg.Opts == nil {
   467  		cfg.Opts = NoOpts
   468  	}
   469  	if cfg.From == "" {
   470  		cfg.From = "[internal]"
   471  	}
   472  }
   473  
   474  // Call calls a given function.
   475  func (ev *Evaler) Call(f Callable, callCfg CallCfg, evalCfg EvalCfg) error {
   476  	callCfg.fillDefaults()
   477  	evalCfg.fillDefaults()
   478  	if evalCfg.Global == nil {
   479  		evalCfg.Global = ev.Global()
   480  	}
   481  	fm, cleanup := ev.prepareFrame(parse.Source{Name: callCfg.From}, evalCfg)
   482  	defer cleanup()
   483  	return f.Call(fm, callCfg.Args, callCfg.Opts)
   484  }
   485  
   486  func (ev *Evaler) prepareFrame(src parse.Source, cfg EvalCfg) (*Frame, func()) {
   487  	var intCh <-chan struct{}
   488  	var intChCleanup func()
   489  	if cfg.Interrupt != nil {
   490  		intCh, intChCleanup = cfg.Interrupt()
   491  	}
   492  
   493  	fm := &Frame{ev, src, cfg.Global, new(Ns), intCh, cfg.Ports, nil, false}
   494  	return fm, func() {
   495  		if intChCleanup != nil {
   496  			intChCleanup()
   497  		}
   498  		if cfg.PutInFg {
   499  			err := putSelfInFg()
   500  			if err != nil {
   501  				fmt.Fprintln(cfg.Ports[2].File,
   502  					"failed to put myself in foreground:", err)
   503  			}
   504  		}
   505  	}
   506  }
   507  
   508  // Check checks the given source code for any parse error and compilation error.
   509  // It always tries to compile the code even if there is a parse error; both
   510  // return values may be non-nil. If w is not nil, deprecation messages are
   511  // written to it.
   512  func (ev *Evaler) Check(src parse.Source, w io.Writer) (*parse.Error, *diag.Error) {
   513  	tree, parseErr := parse.Parse(src, parse.Config{WarningWriter: w})
   514  	return parse.GetError(parseErr), ev.CheckTree(tree, w)
   515  }
   516  
   517  // CheckTree checks the given parsed source tree for compilation errors. If w is
   518  // not nil, deprecation messages are written to it.
   519  func (ev *Evaler) CheckTree(tree parse.Tree, w io.Writer) *diag.Error {
   520  	_, compileErr := ev.compile(tree, ev.Global(), w)
   521  	return GetCompilationError(compileErr)
   522  }
   523  
   524  // Compiles a parsed tree.
   525  func (ev *Evaler) compile(tree parse.Tree, g *Ns, w io.Writer) (nsOp, error) {
   526  	return compile(ev.Builtin().static(), g.static(), tree, w)
   527  }