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