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 }