src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/eval/exception.go (about)

     1  package eval
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"strconv"
     7  	"syscall"
     8  	"unsafe"
     9  
    10  	"src.elv.sh/pkg/diag"
    11  	"src.elv.sh/pkg/eval/vals"
    12  	"src.elv.sh/pkg/parse"
    13  	"src.elv.sh/pkg/persistent/hash"
    14  )
    15  
    16  // Exception represents exceptions. It is both a Value accessible to Elvish
    17  // code, and can be returned by methods like (*Evaler).Eval.
    18  type Exception interface {
    19  	error
    20  	diag.Shower
    21  	Reason() error
    22  	StackTrace() *StackTrace
    23  	// This is not strictly necessary, but it makes sure that there is only one
    24  	// implementation of Exception, so that the compiler may de-virtualize this
    25  	// interface.
    26  	isException()
    27  }
    28  
    29  // NewException creates a new Exception.
    30  func NewException(reason error, stackTrace *StackTrace) Exception {
    31  	return &exception{reason, stackTrace}
    32  }
    33  
    34  // Implementation of the Exception interface.
    35  type exception struct {
    36  	reason     error
    37  	stackTrace *StackTrace
    38  }
    39  
    40  var _ vals.PseudoMap = &exception{}
    41  
    42  // StackTrace represents a stack trace as a linked list of diag.Context. The
    43  // head is the innermost stack.
    44  //
    45  // Since pipelines can call multiple functions in parallel, all the StackTrace
    46  // nodes form a DAG.
    47  type StackTrace struct {
    48  	Head *diag.Context
    49  	Next *StackTrace
    50  }
    51  
    52  // Reason returns the Reason field if err is an Exception. Otherwise it returns
    53  // err itself.
    54  func Reason(err error) error {
    55  	if exc, ok := err.(*exception); ok {
    56  		return exc.reason
    57  	}
    58  	return err
    59  }
    60  
    61  // OK is a pointer to a special value of Exception that represents the absence
    62  // of exception.
    63  var OK = &exception{}
    64  
    65  func (exc *exception) isException() {}
    66  
    67  func (exc *exception) Reason() error { return exc.reason }
    68  
    69  func (exc *exception) StackTrace() *StackTrace { return exc.stackTrace }
    70  
    71  // Error returns the message of the cause of the exception.
    72  func (exc *exception) Error() string { return exc.reason.Error() }
    73  
    74  var (
    75  	exceptionCauseStartMarker = "\033[31;1m"
    76  	exceptionCauseEndMarker   = "\033[m"
    77  )
    78  
    79  // Show shows the exception.
    80  func (exc *exception) Show(indent string) string {
    81  	buf := new(bytes.Buffer)
    82  
    83  	var causeDescription string
    84  	if shower, ok := exc.reason.(diag.Shower); ok {
    85  		causeDescription = shower.Show(indent)
    86  	} else if exc.reason == nil {
    87  		causeDescription = "ok"
    88  	} else {
    89  		causeDescription = exceptionCauseStartMarker + exc.reason.Error() + exceptionCauseEndMarker
    90  	}
    91  	fmt.Fprintf(buf, "Exception: %s", causeDescription)
    92  
    93  	if exc.stackTrace != nil {
    94  		for tb := exc.stackTrace; tb != nil; tb = tb.Next {
    95  			buf.WriteString("\n" + indent + "  ")
    96  			buf.WriteString(tb.Head.Show(indent + "  "))
    97  		}
    98  	}
    99  
   100  	if pipeExcs, ok := exc.reason.(PipelineError); ok {
   101  		buf.WriteString("\n" + indent + "Caused by:")
   102  		for _, e := range pipeExcs.Errors {
   103  			if e == OK {
   104  				continue
   105  			}
   106  			buf.WriteString("\n" + indent + "  " + e.Show(indent+"  "))
   107  		}
   108  	}
   109  
   110  	return buf.String()
   111  }
   112  
   113  // Kind returns "exception".
   114  func (exc *exception) Kind() string {
   115  	return "exception"
   116  }
   117  
   118  // Repr returns a representation of the exception. It is lossy in that it does
   119  // not preserve the stacktrace.
   120  func (exc *exception) Repr(indent int) string {
   121  	if exc.reason == nil {
   122  		return "$ok"
   123  	}
   124  	return "[^exception &reason=" + vals.Repr(exc.reason, indent+1) + " &stack-trace=<...>]"
   125  }
   126  
   127  // Equal compares by address.
   128  func (exc *exception) Equal(rhs any) bool {
   129  	return exc == rhs
   130  }
   131  
   132  // Hash returns the hash of the address.
   133  func (exc *exception) Hash() uint32 {
   134  	return hash.Pointer(unsafe.Pointer(exc))
   135  }
   136  
   137  // Bool returns whether this exception has a nil cause; that is, it is $ok.
   138  func (exc *exception) Bool() bool {
   139  	return exc.reason == nil
   140  }
   141  
   142  func (exc *exception) Fields() vals.StructMap { return excFields{exc} }
   143  
   144  type excFields struct{ e *exception }
   145  
   146  func (excFields) IsStructMap()              {}
   147  func (f excFields) Reason() error           { return f.e.reason }
   148  func (f excFields) StackTrace() *StackTrace { return f.e.stackTrace }
   149  
   150  // PipelineError represents the errors of pipelines, in which multiple commands
   151  // may error.
   152  type PipelineError struct {
   153  	Errors []Exception
   154  }
   155  
   156  var _ vals.PseudoMap = PipelineError{}
   157  
   158  // Error returns a plain text representation of the pipeline error.
   159  func (pe PipelineError) Error() string {
   160  	b := new(bytes.Buffer)
   161  	b.WriteString("(")
   162  	for i, e := range pe.Errors {
   163  		if i > 0 {
   164  			b.WriteString(" | ")
   165  		}
   166  		if e == nil || e.Reason() == nil {
   167  			b.WriteString("<nil>")
   168  		} else {
   169  			b.WriteString(e.Error())
   170  		}
   171  	}
   172  	b.WriteString(")")
   173  	return b.String()
   174  }
   175  
   176  // MakePipelineError builds an error from the execution results of multiple
   177  // commands in a pipeline.
   178  //
   179  // If all elements are either nil or OK, it returns nil. If there is exactly
   180  // non-nil non-OK Exception, it returns it. Otherwise, it return a PipelineError
   181  // built from the slice, with nil items turned into OK's for easier access from
   182  // Elvish code.
   183  func MakePipelineError(excs []Exception) error {
   184  	newexcs := make([]Exception, len(excs))
   185  	notOK, lastNotOK := 0, 0
   186  	for i, e := range excs {
   187  		if e == nil {
   188  			newexcs[i] = OK
   189  		} else {
   190  			newexcs[i] = e
   191  			if e.Reason() != nil {
   192  				notOK++
   193  				lastNotOK = i
   194  			}
   195  		}
   196  	}
   197  	switch notOK {
   198  	case 0:
   199  		return nil
   200  	case 1:
   201  		return newexcs[lastNotOK]
   202  	default:
   203  		return PipelineError{newexcs}
   204  	}
   205  }
   206  
   207  func (pe PipelineError) Kind() string           { return "pipeline-error" }
   208  func (pe PipelineError) Fields() vals.StructMap { return peFields{pe} }
   209  
   210  type peFields struct{ pe PipelineError }
   211  
   212  func (peFields) IsStructMap() {}
   213  
   214  func (f peFields) Type() string { return "pipeline" }
   215  
   216  func (f peFields) Exceptions() vals.List {
   217  	li := vals.EmptyList
   218  	for _, exc := range f.pe.Errors {
   219  		li = li.Conj(exc)
   220  	}
   221  	return li
   222  }
   223  
   224  // Flow is a special type of error used for control flows.
   225  type Flow uint
   226  
   227  var _ vals.PseudoMap = Flow(0)
   228  
   229  // Control flows.
   230  const (
   231  	Return Flow = iota
   232  	Break
   233  	Continue
   234  )
   235  
   236  var flowNames = [...]string{
   237  	"return", "break", "continue",
   238  }
   239  
   240  func (f Flow) Error() string {
   241  	if f >= Flow(len(flowNames)) {
   242  		return fmt.Sprintf("!(BAD FLOW: %d)", f)
   243  	}
   244  	return flowNames[f]
   245  }
   246  
   247  // Show shows the flow "error".
   248  func (f Flow) Show(string) string {
   249  	return "\033[33;1m" + f.Error() + "\033[m"
   250  }
   251  
   252  func (f Flow) Kind() string           { return "flow-error" }
   253  func (f Flow) Fields() vals.StructMap { return flowFields{f} }
   254  
   255  type flowFields struct{ f Flow }
   256  
   257  func (flowFields) IsStructMap() {}
   258  
   259  func (f flowFields) Type() string { return "flow" }
   260  func (f flowFields) Name() string { return f.f.Error() }
   261  
   262  // ExternalCmdExit contains the exit status of external commands.
   263  type ExternalCmdExit struct {
   264  	syscall.WaitStatus
   265  	CmdName string
   266  	Pid     int
   267  }
   268  
   269  var _ vals.PseudoMap = ExternalCmdExit{}
   270  
   271  // NewExternalCmdExit constructs an error for representing a non-zero exit from
   272  // an external command.
   273  func NewExternalCmdExit(name string, ws syscall.WaitStatus, pid int) error {
   274  	if ws.Exited() && ws.ExitStatus() == 0 {
   275  		return nil
   276  	}
   277  	return ExternalCmdExit{ws, name, pid}
   278  }
   279  
   280  func (exit ExternalCmdExit) Error() string {
   281  	ws := exit.WaitStatus
   282  	quotedName := parse.Quote(exit.CmdName)
   283  	switch {
   284  	case ws.Exited():
   285  		return quotedName + " exited with " + strconv.Itoa(ws.ExitStatus())
   286  	case ws.Signaled():
   287  		causeDescription := quotedName + " killed by signal " + ws.Signal().String()
   288  		if ws.CoreDump() {
   289  			causeDescription += " (core dumped)"
   290  		}
   291  		return causeDescription
   292  	case ws.Stopped():
   293  		causeDescription := quotedName + " stopped by signal " + fmt.Sprintf("%s (pid=%d)", ws.StopSignal(), exit.Pid)
   294  		trap := ws.TrapCause()
   295  		if trap != -1 {
   296  			causeDescription += fmt.Sprintf(" (trapped %v)", trap)
   297  		}
   298  		return causeDescription
   299  	default:
   300  		return fmt.Sprint(quotedName, " has unknown WaitStatus ", ws)
   301  	}
   302  }
   303  
   304  func (exit ExternalCmdExit) Kind() string {
   305  	return "external-cmd-error"
   306  }
   307  
   308  func (exit ExternalCmdExit) Fields() vals.StructMap {
   309  	ws := exit.WaitStatus
   310  	f := exitFieldsCommon{exit}
   311  	switch {
   312  	case ws.Exited():
   313  		return exitFieldsExited{f}
   314  	case ws.Signaled():
   315  		return exitFieldsSignaled{f}
   316  	case ws.Stopped():
   317  		return exitFieldsStopped{f}
   318  	default:
   319  		return exitFieldsUnknown{f}
   320  	}
   321  }
   322  
   323  type exitFieldsCommon struct{ e ExternalCmdExit }
   324  
   325  func (exitFieldsCommon) IsStructMap()      {}
   326  func (f exitFieldsCommon) CmdName() string { return f.e.CmdName }
   327  func (f exitFieldsCommon) Pid() string     { return strconv.Itoa(f.e.Pid) }
   328  
   329  type exitFieldsExited struct{ exitFieldsCommon }
   330  
   331  func (exitFieldsExited) Type() string         { return "external-cmd/exited" }
   332  func (f exitFieldsExited) ExitStatus() string { return strconv.Itoa(f.e.ExitStatus()) }
   333  
   334  type exitFieldsSignaled struct{ exitFieldsCommon }
   335  
   336  func (f exitFieldsSignaled) Type() string         { return "external-cmd/signaled" }
   337  func (f exitFieldsSignaled) SignalName() string   { return f.e.Signal().String() }
   338  func (f exitFieldsSignaled) SignalNumber() string { return strconv.Itoa(int(f.e.Signal())) }
   339  func (f exitFieldsSignaled) CoreDumped() bool     { return f.e.CoreDump() }
   340  
   341  type exitFieldsStopped struct{ exitFieldsCommon }
   342  
   343  func (f exitFieldsStopped) Type() string         { return "external-cmd/stopped" }
   344  func (f exitFieldsStopped) SignalName() string   { return f.e.StopSignal().String() }
   345  func (f exitFieldsStopped) SignalNumber() string { return strconv.Itoa(int(f.e.StopSignal())) }
   346  func (f exitFieldsStopped) TrapCause() int       { return f.e.TrapCause() }
   347  
   348  type exitFieldsUnknown struct{ exitFieldsCommon }
   349  
   350  func (exitFieldsUnknown) Type() string { return "external-cmd/unknown" }