src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/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  	"context"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"strconv"
    11  	"sync"
    12  
    13  	"src.elv.sh/pkg/env"
    14  	"src.elv.sh/pkg/eval/vals"
    15  	"src.elv.sh/pkg/eval/vars"
    16  	"src.elv.sh/pkg/logutil"
    17  	"src.elv.sh/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  	PreExitHooks []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  	// Path to the rc file, and path to the rc file actually evaluated. These
    59  	// are not used by the Evaler itself right now; they are here so that they
    60  	// can be exposed to the runtime: module.
    61  	RcPath, EffectiveRcPath string
    62  
    63  	mu sync.RWMutex
    64  	// Mutations to fields below must be guarded by mutex.
    65  	//
    66  	// Note that this is *not* a GIL; most state mutations when executing Elvish
    67  	// code is localized and do not need to hold this mutex.
    68  	//
    69  	// TODO: Actually guard all mutations by this mutex.
    70  
    71  	global, builtin *Ns
    72  
    73  	deprecations deprecationRegistry
    74  
    75  	// Internal modules are indexed by use specs. External modules are indexed by
    76  	// absolute paths.
    77  	modules map[string]*Ns
    78  
    79  	// Various states and configs exposed to Elvish code.
    80  	//
    81  	// The prefix to prepend to value outputs when writing them to terminal,
    82  	// exposed as $value-out-prefix.
    83  	valuePrefix string
    84  	// Whether to notify the success of background jobs, exposed as
    85  	// $notify-bg-job-sucess.
    86  	notifyBgJobSuccess bool
    87  	// The current number of background jobs, exposed as $num-bg-jobs.
    88  	numBgJobs int
    89  }
    90  
    91  // NewEvaler creates a new Evaler.
    92  func NewEvaler() *Evaler {
    93  	builtin := builtinNs.Ns()
    94  
    95  	newListVar := func(l vals.List) vars.PtrVar { return vars.FromPtr(&l) }
    96  	beforeExitHookElvish := newListVar(vals.EmptyList)
    97  	beforeChdirElvish := newListVar(vals.EmptyList)
    98  	afterChdirElvish := newListVar(vals.EmptyList)
    99  
   100  	ev := &Evaler{
   101  		global:  new(Ns),
   102  		builtin: builtin,
   103  
   104  		deprecations: newDeprecationRegistry(),
   105  
   106  		modules:        make(map[string]*Ns),
   107  		BundledModules: make(map[string]string),
   108  
   109  		valuePrefix:        defaultValuePrefix,
   110  		notifyBgJobSuccess: defaultNotifyBgJobSuccess,
   111  		numBgJobs:          0,
   112  		Args:               vals.EmptyList,
   113  	}
   114  
   115  	ev.PreExitHooks = []func(){func() {
   116  		CallHook(ev, nil, "before-exit", beforeExitHookElvish.Get().(vals.List))
   117  	}}
   118  	ev.BeforeChdir = []func(string){func(path string) {
   119  		CallHook(ev, nil, "before-chdir", beforeChdirElvish.Get().(vals.List), path)
   120  	}}
   121  	ev.AfterChdir = []func(string){func(path string) {
   122  		CallHook(ev, nil, "after-chdir", afterChdirElvish.Get().(vals.List), path)
   123  	}}
   124  
   125  	ev.ExtendBuiltin(BuildNs().
   126  		AddVar("pwd", NewPwdVar(ev)).
   127  		AddVar("before-exit", beforeExitHookElvish).
   128  		AddVar("before-chdir", beforeChdirElvish).
   129  		AddVar("after-chdir", afterChdirElvish).
   130  		AddVar("value-out-indicator",
   131  			vars.FromPtrWithMutex(&ev.valuePrefix, &ev.mu)).
   132  		AddVar("notify-bg-job-success",
   133  			vars.FromPtrWithMutex(&ev.notifyBgJobSuccess, &ev.mu)).
   134  		AddVar("num-bg-jobs",
   135  			vars.FromGet(func() any { return strconv.Itoa(ev.getNumBgJobs()) })).
   136  		AddVar("args", vars.FromGet(func() any { return ev.Args })))
   137  
   138  	// Install the "builtin" module after extension is complete.
   139  	ev.modules["builtin"] = ev.builtin
   140  
   141  	return ev
   142  }
   143  
   144  // PreExit runs all pre-exit hooks.
   145  func (ev *Evaler) PreExit() {
   146  	for _, hook := range ev.PreExitHooks {
   147  		hook()
   148  	}
   149  }
   150  
   151  // Access methods.
   152  
   153  // Global returns the global Ns.
   154  func (ev *Evaler) Global() *Ns {
   155  	ev.mu.RLock()
   156  	defer ev.mu.RUnlock()
   157  	return ev.global
   158  }
   159  
   160  // ExtendGlobal extends the global namespace with the given namespace.
   161  func (ev *Evaler) ExtendGlobal(ns Nser) {
   162  	ev.mu.Lock()
   163  	defer ev.mu.Unlock()
   164  	ev.global = CombineNs(ev.global, ns.Ns())
   165  }
   166  
   167  // DeleteFromGlobal deletes names from the global namespace.
   168  func (ev *Evaler) DeleteFromGlobal(names map[string]struct{}) {
   169  	ev.mu.Lock()
   170  	defer ev.mu.Unlock()
   171  	g := ev.global.clone()
   172  	for i := range g.infos {
   173  		if _, ok := names[g.infos[i].name]; ok {
   174  			g.infos[i].deleted = true
   175  		}
   176  	}
   177  	ev.global = g
   178  }
   179  
   180  // Builtin returns the builtin Ns.
   181  func (ev *Evaler) Builtin() *Ns {
   182  	ev.mu.RLock()
   183  	defer ev.mu.RUnlock()
   184  	return ev.builtin
   185  }
   186  
   187  // ExtendBuiltin extends the builtin namespace with the given namespace.
   188  func (ev *Evaler) ExtendBuiltin(ns Nser) {
   189  	ev.mu.Lock()
   190  	defer ev.mu.Unlock()
   191  	ev.builtin = CombineNs(ev.builtin, ns.Ns())
   192  }
   193  
   194  // ReplaceBuiltin replaces the builtin namespace. It should only be used in
   195  // tests.
   196  func (ev *Evaler) ReplaceBuiltin(ns *Ns) {
   197  	ev.mu.Lock()
   198  	defer ev.mu.Unlock()
   199  	ev.builtin = ns
   200  }
   201  
   202  func (ev *Evaler) registerDeprecation(d deprecation) bool {
   203  	ev.mu.Lock()
   204  	defer ev.mu.Unlock()
   205  	return ev.deprecations.register(d)
   206  }
   207  
   208  // AddModule add an internal module so that it can be used with "use $name" from
   209  // script.
   210  func (ev *Evaler) AddModule(name string, mod *Ns) {
   211  	ev.mu.Lock()
   212  	defer ev.mu.Unlock()
   213  	ev.modules[name] = mod
   214  }
   215  
   216  // ValuePrefix returns the prefix to prepend to value outputs when writing them
   217  // to terminal.
   218  func (ev *Evaler) ValuePrefix() string {
   219  	ev.mu.RLock()
   220  	defer ev.mu.RUnlock()
   221  	return ev.valuePrefix
   222  }
   223  
   224  func (ev *Evaler) getNotifyBgJobSuccess() bool {
   225  	ev.mu.RLock()
   226  	defer ev.mu.RUnlock()
   227  	return ev.notifyBgJobSuccess
   228  }
   229  
   230  func (ev *Evaler) getNumBgJobs() int {
   231  	ev.mu.RLock()
   232  	defer ev.mu.RUnlock()
   233  	return ev.numBgJobs
   234  }
   235  
   236  func (ev *Evaler) addNumBgJobs(delta int) {
   237  	ev.mu.Lock()
   238  	defer ev.mu.Unlock()
   239  	ev.numBgJobs += delta
   240  }
   241  
   242  // Chdir changes the current directory, and updates $E:PWD on success
   243  //
   244  // It runs the functions in beforeChdir immediately before changing the
   245  // directory, and the functions in afterChdir immediately after (if chdir was
   246  // successful). It returns nil as long as the directory changing part succeeds.
   247  func (ev *Evaler) Chdir(path string) error {
   248  	for _, hook := range ev.BeforeChdir {
   249  		hook(path)
   250  	}
   251  
   252  	err := os.Chdir(path)
   253  	if err != nil {
   254  		return err
   255  	}
   256  
   257  	for _, hook := range ev.AfterChdir {
   258  		hook(path)
   259  	}
   260  
   261  	pwd, err := os.Getwd()
   262  	if err != nil {
   263  		logger.Println("getwd after cd:", err)
   264  		return nil
   265  	}
   266  	os.Setenv(env.PWD, pwd)
   267  
   268  	return nil
   269  }
   270  
   271  // EvalCfg keeps configuration for the (*Evaler).Eval method.
   272  type EvalCfg struct {
   273  	// Context that can be used to cancel the evaluation.
   274  	Interrupts context.Context
   275  	// Ports to use in evaluation. The first 3 elements, if not specified
   276  	// (either being nil or Ports containing fewer than 3 elements),
   277  	// will be filled with DummyInputPort, DummyOutputPort and
   278  	// DummyOutputPort respectively.
   279  	Ports []*Port
   280  	// Whether the Eval method should try to put the Elvish in the foreground
   281  	// after the code is executed.
   282  	PutInFg bool
   283  	// If not nil, used the given global namespace, instead of Evaler's own.
   284  	Global *Ns
   285  }
   286  
   287  func (cfg *EvalCfg) fillDefaults() {
   288  	if len(cfg.Ports) < 3 {
   289  		cfg.Ports = append(cfg.Ports, make([]*Port, 3-len(cfg.Ports))...)
   290  	}
   291  	if cfg.Ports[0] == nil {
   292  		cfg.Ports[0] = DummyInputPort
   293  	}
   294  	if cfg.Ports[1] == nil {
   295  		cfg.Ports[1] = DummyOutputPort
   296  	}
   297  	if cfg.Ports[2] == nil {
   298  		cfg.Ports[2] = DummyOutputPort
   299  	}
   300  }
   301  
   302  // Eval evaluates a piece of source code with the given configuration. The
   303  // returned error may be a parse error, compilation error or exception.
   304  func (ev *Evaler) Eval(src parse.Source, cfg EvalCfg) error {
   305  	cfg.fillDefaults()
   306  	errFile := cfg.Ports[2].File
   307  
   308  	tree, err := parse.Parse(src, parse.Config{WarningWriter: errFile})
   309  	if err != nil {
   310  		return err
   311  	}
   312  
   313  	ev.mu.Lock()
   314  	b := ev.builtin
   315  	defaultGlobal := cfg.Global == nil
   316  	if defaultGlobal {
   317  		// If cfg.Global is nil, use the Evaler's default global, and also
   318  		// mutate the default global.
   319  		cfg.Global = ev.global
   320  		// Continue to hold the mutex; it will be released when ev.global gets
   321  		// mutated.
   322  	} else {
   323  		ev.mu.Unlock()
   324  	}
   325  
   326  	op, _, err := compile(b.static(), cfg.Global.static(), nil, tree, errFile)
   327  	if err != nil {
   328  		if defaultGlobal {
   329  			ev.mu.Unlock()
   330  		}
   331  		return err
   332  	}
   333  
   334  	fm, cleanup := ev.prepareFrame(src, cfg)
   335  	defer cleanup()
   336  
   337  	newLocal, exec := op.prepare(fm)
   338  	if defaultGlobal {
   339  		ev.global = newLocal
   340  		ev.mu.Unlock()
   341  	}
   342  
   343  	return exec()
   344  }
   345  
   346  // CallCfg keeps configuration for the (*Evaler).Call method.
   347  type CallCfg struct {
   348  	// Arguments to pass to the function.
   349  	Args []any
   350  	// Options to pass to the function.
   351  	Opts map[string]any
   352  	// The name of the internal source that is calling the function.
   353  	From string
   354  }
   355  
   356  func (cfg *CallCfg) fillDefaults() {
   357  	if cfg.Opts == nil {
   358  		cfg.Opts = NoOpts
   359  	}
   360  	if cfg.From == "" {
   361  		cfg.From = "[internal]"
   362  	}
   363  }
   364  
   365  // Call calls a given function.
   366  func (ev *Evaler) Call(f Callable, callCfg CallCfg, evalCfg EvalCfg) error {
   367  	callCfg.fillDefaults()
   368  	evalCfg.fillDefaults()
   369  	if evalCfg.Global == nil {
   370  		evalCfg.Global = ev.Global()
   371  	}
   372  	fm, cleanup := ev.prepareFrame(parse.Source{Name: callCfg.From}, evalCfg)
   373  	defer cleanup()
   374  	return f.Call(fm, callCfg.Args, callCfg.Opts)
   375  }
   376  
   377  func (ev *Evaler) prepareFrame(src parse.Source, cfg EvalCfg) (*Frame, func()) {
   378  	intCtx := cfg.Interrupts
   379  	if intCtx == nil {
   380  		intCtx = context.Background()
   381  	}
   382  
   383  	ports := fillDefaultDummyPorts(cfg.Ports)
   384  
   385  	fm := &Frame{ev, src, cfg.Global, new(Ns), nil, intCtx, ports, nil, false}
   386  	return fm, func() {
   387  		if cfg.PutInFg {
   388  			err := putSelfInFg()
   389  			if err != nil {
   390  				fmt.Fprintln(ports[2].File,
   391  					"failed to put myself in foreground:", err)
   392  			}
   393  		}
   394  	}
   395  }
   396  
   397  func fillDefaultDummyPorts(ports []*Port) []*Port {
   398  	growPorts(&ports, 3)
   399  	if ports[0] == nil {
   400  		ports[0] = DummyInputPort
   401  	}
   402  	if ports[1] == nil {
   403  		ports[1] = DummyOutputPort
   404  	}
   405  	if ports[2] == nil {
   406  		ports[2] = DummyOutputPort
   407  	}
   408  	return ports
   409  }
   410  
   411  // Check checks the given source code for any parse error, autofixes, and
   412  // compilation error. It always tries to compile the code even if there is a
   413  // parse error. If w is not nil, deprecation messages are written to it.
   414  func (ev *Evaler) Check(src parse.Source, w io.Writer) (error, []string, error) {
   415  	tree, parseErr := parse.Parse(src, parse.Config{WarningWriter: w})
   416  	autofixes, compileErr := ev.CheckTree(tree, w)
   417  	return parseErr, autofixes, compileErr
   418  }
   419  
   420  // CheckTree checks the given parsed source tree for autofixes and compilation
   421  // errors. If w is not nil, deprecation messages are written to it.
   422  func (ev *Evaler) CheckTree(tree parse.Tree, w io.Writer) ([]string, error) {
   423  	ev.mu.RLock()
   424  	b, g, m := ev.builtin, ev.global, ev.modules
   425  	ev.mu.RUnlock()
   426  	_, autofixes, compileErr := compile(b.static(), g.static(), mapKeys(m), tree, w)
   427  	return autofixes, compileErr
   428  }