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  }