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 }