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 }