github.com/markusbkk/elvish@v0.0.0-20231204143114-91dc52438621/pkg/eval/builtin_fn_flow.go (about) 1 package eval 2 3 import ( 4 "errors" 5 "sync" 6 "sync/atomic" 7 8 "github.com/markusbkk/elvish/pkg/diag" 9 "github.com/markusbkk/elvish/pkg/eval/vals" 10 ) 11 12 // Flow control. 13 14 // TODO(xiaq): Document "multi-error". 15 16 func init() { 17 addBuiltinFns(map[string]interface{}{ 18 "run-parallel": runParallel, 19 // Exception and control 20 "fail": fail, 21 "multi-error": multiErrorFn, 22 "return": returnFn, 23 "break": breakFn, 24 "continue": continueFn, 25 "defer": deferFn, 26 // Iterations. 27 "each": each, 28 "peach": peach, 29 }) 30 } 31 32 //elvdoc:fn run-parallel 33 // 34 // ```elvish 35 // run-parallel $callable ... 36 // ``` 37 // 38 // Run several callables in parallel, and wait for all of them to finish. 39 // 40 // If one or more callables throw exceptions, the other callables continue running, 41 // and a composite exception is thrown when all callables finish execution. 42 // 43 // The behavior of `run-parallel` is consistent with the behavior of pipelines, 44 // except that it does not perform any redirections. 45 // 46 // Here is an example that lets you pipe the stdout and stderr of a command to two 47 // different commands in order to independently capture the output of each byte stream: 48 // 49 // ```elvish-transcript 50 // ~> fn capture {|f| 51 // var pout = (file:pipe) 52 // var perr = (file:pipe) 53 // var out err 54 // run-parallel { 55 // $f > $pout[w] 2> $perr[w] 56 // file:close $pout[w] 57 // file:close $perr[w] 58 // } { 59 // set out = (slurp < $pout[r]) 60 // file:close $pout[r] 61 // } { 62 // set err = (slurp < $perr[r]) 63 // file:close $perr[r] 64 // } 65 // put $out $err 66 // } 67 // ~> capture { echo stdout-test; echo stderr-test >&2 } 68 // ▶ "stdout-test\n" 69 // ▶ "stderr-test\n" 70 // ``` 71 // 72 // This command is intended for doing a fixed number of heterogeneous things in 73 // parallel. If you need homogeneous parallel processing of possibly unbound data, 74 // use `peach` instead. 75 // 76 // @cf peach 77 78 func runParallel(fm *Frame, functions ...Callable) error { 79 var wg sync.WaitGroup 80 wg.Add(len(functions)) 81 exceptions := make([]Exception, len(functions)) 82 for i, function := range functions { 83 go func(fm2 *Frame, function Callable, pexc *Exception) { 84 err := function.Call(fm2, NoArgs, NoOpts) 85 if err != nil { 86 *pexc = err.(Exception) 87 } 88 wg.Done() 89 }(fm.Fork("[run-parallel function]"), function, &exceptions[i]) 90 } 91 92 wg.Wait() 93 return MakePipelineError(exceptions) 94 } 95 96 //elvdoc:fn each 97 // 98 // ```elvish 99 // each $f $inputs? 100 // ``` 101 // 102 // Calls `$f` on each [value input](#value-inputs). 103 // 104 // An exception raised from [`break`](#break) is caught by `each`, and will 105 // cause it to terminate early. 106 // 107 // An exception raised from [`continue`](#continue) is swallowed and can be used 108 // to terminate a single iteration early. 109 // 110 // Examples: 111 // 112 // ```elvish-transcript 113 // ~> range 5 8 | each {|x| * $x $x } 114 // ▶ 25 115 // ▶ 36 116 // ▶ 49 117 // ~> each {|x| put $x[:3] } [lorem ipsum] 118 // ▶ lor 119 // ▶ ips 120 // ``` 121 // 122 // @cf peach 123 // 124 // Etymology: Various languages, as `for each`. Happens to have the same name as 125 // the iteration construct of 126 // [Factor](http://docs.factorcode.org/content/word-each,sequences.html). 127 128 func each(fm *Frame, f Callable, inputs Inputs) error { 129 broken := false 130 var err error 131 inputs(func(v interface{}) { 132 if broken { 133 return 134 } 135 newFm := fm.Fork("closure of each") 136 ex := f.Call(newFm, []interface{}{v}, NoOpts) 137 newFm.Close() 138 139 if ex != nil { 140 switch Reason(ex) { 141 case nil, Continue: 142 // nop 143 case Break: 144 broken = true 145 default: 146 broken = true 147 err = ex 148 } 149 } 150 }) 151 return err 152 } 153 154 //elvdoc:fn peach 155 // 156 // ```elvish 157 // peach $f $inputs? 158 // ``` 159 // 160 // Calls `$f` for each [value input](#value-inputs), possibly in parallel. 161 // 162 // Like `each`, an exception raised from [`break`](#break) will cause `peach` 163 // to terminate early. However due to the parallel nature of `peach`, the exact 164 // time of termination is non-deterministic, and termination is not guaranteed. 165 // 166 // An exception raised from [`continue`](#continue) is swallowed and can be used 167 // to terminate a single iteration early. 168 // 169 // Example (your output will differ): 170 // 171 // ```elvish-transcript 172 // ~> range 1 10 | peach {|x| + $x 10 } 173 // ▶ (num 12) 174 // ▶ (num 13) 175 // ▶ (num 11) 176 // ▶ (num 16) 177 // ▶ (num 18) 178 // ▶ (num 14) 179 // ▶ (num 17) 180 // ▶ (num 15) 181 // ▶ (num 19) 182 // ~> range 1 101 | 183 // peach {|x| if (== 50 $x) { break } else { put $x } } | 184 // + (all) # 1+...+49 = 1225; 1+...+100 = 5050 185 // ▶ (num 1328) 186 // ``` 187 // 188 // This command is intended for homogeneous processing of possibly unbound data. If 189 // you need to do a fixed number of heterogeneous things in parallel, use 190 // `run-parallel`. 191 // 192 // @cf each run-parallel 193 194 func peach(fm *Frame, f Callable, inputs Inputs) error { 195 var wg sync.WaitGroup 196 var broken int32 197 var errMu sync.Mutex 198 var err error 199 200 inputs(func(v interface{}) { 201 if atomic.LoadInt32(&broken) != 0 { 202 return 203 } 204 wg.Add(1) 205 go func() { 206 newFm := fm.Fork("closure of peach") 207 newFm.ports[0] = DummyInputPort 208 ex := f.Call(newFm, []interface{}{v}, NoOpts) 209 newFm.Close() 210 211 if ex != nil { 212 switch Reason(ex) { 213 case nil, Continue: 214 // nop 215 case Break: 216 atomic.StoreInt32(&broken, 1) 217 default: 218 errMu.Lock() 219 err = diag.Errors(err, ex) 220 defer errMu.Unlock() 221 atomic.StoreInt32(&broken, 1) 222 } 223 } 224 wg.Done() 225 }() 226 }) 227 wg.Wait() 228 return err 229 } 230 231 // FailError is an error returned by the "fail" command. 232 type FailError struct{ Content interface{} } 233 234 // Error returns the string representation of the cause. 235 func (e FailError) Error() string { return vals.ToString(e.Content) } 236 237 // Fields returns a structmap for accessing fields from Elvish. 238 func (e FailError) Fields() vals.StructMap { return failFields{e} } 239 240 type failFields struct{ e FailError } 241 242 func (failFields) IsStructMap() {} 243 244 func (f failFields) Type() string { return "fail" } 245 func (f failFields) Content() interface{} { return f.e.Content } 246 247 //elvdoc:fn fail 248 // 249 // ```elvish 250 // fail $v 251 // ``` 252 // 253 // Throws an exception; `$v` may be any type. If `$v` is already an exception, 254 // `fail` rethrows it. 255 // 256 // ```elvish-transcript 257 // ~> fail bad 258 // Exception: bad 259 // [tty 9], line 1: fail bad 260 // ~> put ?(fail bad) 261 // ▶ ?(fail bad) 262 // ~> fn f { fail bad } 263 // ~> fail ?(f) 264 // Exception: bad 265 // Traceback: 266 // [tty 7], line 1: 267 // fn f { fail bad } 268 // [tty 8], line 1: 269 // fail ?(f) 270 // ``` 271 272 func fail(v interface{}) error { 273 if e, ok := v.(error); ok { 274 // MAYBE TODO: if v is an exception, attach a "rethrown" stack trace, 275 // like Java 276 return e 277 } 278 return FailError{v} 279 } 280 281 func multiErrorFn(excs ...Exception) error { 282 return PipelineError{excs} 283 } 284 285 //elvdoc:fn return 286 // 287 // Raises the special "return" exception. When raised inside a named function 288 // (defined by the [`fn` keyword](language.html#fn)) it is captured by the 289 // function and causes the function to terminate. It is not captured by an 290 // ordinary anonymous function. 291 // 292 // Because `return` raises an exception it can be caught by a 293 // [`try`](language.html#try) block. If not caught, either implicitly by a 294 // named function or explicitly, it causes a failure like any other uncaught 295 // exception. 296 // 297 // See the discussion about [flow commands and 298 // exceptions](language.html#exception-and-flow-commands) 299 // 300 // **Note**: If you want to shadow the builtin `return` function with a local 301 // wrapper, do not define it with `fn` as `fn` swallows the special exception 302 // raised by return. Consider this example: 303 // 304 // ```elvish-transcript 305 // ~> use builtin 306 // ~> fn return { put return; builtin:return } 307 // ~> fn test-return { put before; return; put after } 308 // ~> test-return 309 // ▶ before 310 // ▶ return 311 // ▶ after 312 // ``` 313 // 314 // Instead, shadow the function by directly assigning to `return~`: 315 // 316 // ```elvish-transcript 317 // ~> use builtin 318 // ~> var return~ = { put return; builtin:return } 319 // ~> fn test-return { put before; return; put after } 320 // ~> test-return 321 // ▶ before 322 // ▶ return 323 // ``` 324 325 func returnFn() error { 326 return Return 327 } 328 329 //elvdoc:fn break 330 // 331 // Raises the special "break" exception. When raised inside a loop it is 332 // captured and causes the loop to terminate. 333 // 334 // Because `break` raises an exception it can be caught by a 335 // [`try`](language.html#try) block. If not caught, either implicitly by a loop 336 // or explicitly, it causes a failure like any other uncaught exception. 337 // 338 // See the discussion about [flow commands and exceptions](language.html#exception-and-flow-commands) 339 // 340 // **Note**: You can create a `break` function and it will shadow the builtin 341 // command. If you do so you should explicitly invoke the builtin. For example: 342 // 343 // ```elvish-transcript 344 // ~> use builtin 345 // ~> fn break { put 'break'; builtin:break; put 'should not appear' } 346 // ~> for x [a b c] { put $x; break; put 'unexpected' } 347 // ▶ a 348 // ▶ break 349 // ``` 350 351 func breakFn() error { 352 return Break 353 } 354 355 //elvdoc:fn continue 356 // 357 // Raises the special "continue" exception. When raised inside a loop it is 358 // captured and causes the loop to begin its next iteration. 359 // 360 // Because `continue` raises an exception it can be caught by a 361 // [`try`](language.html#try) block. If not caught, either implicitly by a loop 362 // or explicitly, it causes a failure like any other uncaught exception. 363 // 364 // See the discussion about [flow commands and exceptions](language.html#exception-and-flow-commands) 365 // 366 // **Note**: You can create a `continue` function and it will shadow the builtin 367 // command. If you do so you should explicitly invoke the builtin. For example: 368 // 369 // ```elvish-transcript 370 // ~> use builtin 371 // ~> fn continue { put 'continue'; builtin:continue; put 'should not appear' } 372 // ~> for x [a b c] { put $x; continue; put 'unexpected' } 373 // ▶ a 374 // ▶ continue 375 // ▶ b 376 // ▶ continue 377 // ▶ c 378 // ▶ continue 379 // ``` 380 381 func continueFn() error { 382 return Continue 383 } 384 385 //elvdoc:fn defer 386 // 387 // ```elvish 388 // defer $fn 389 // ``` 390 // 391 // Schedules a function to be called when execution reaches the end of the 392 // current closure. The function is called with no arguments or options, and any 393 // exception it throws gets propagated. 394 // 395 // Examples: 396 // 397 // ```elvish-transcript 398 // ~> { defer { put foo }; put bar } 399 // ▶ bar 400 // ▶ foo 401 // ~> defer { put foo } 402 // Exception: defer must be called from within a closure 403 // [tty 2], line 1: defer { put foo } 404 // ``` 405 406 var errDeferNotInClosure = errors.New("defer must be called from within a closure") 407 408 func deferFn(fm *Frame, fn Callable) error { 409 if fm.defers == nil { 410 return errDeferNotInClosure 411 } 412 deferTraceback := fm.traceback 413 fm.addDefer(func(fm *Frame) Exception { 414 err := fn.Call(fm, NoArgs, NoOpts) 415 if exc, ok := err.(Exception); ok { 416 return exc 417 } 418 return &exception{err, deferTraceback} 419 }) 420 return nil 421 }