Enable nushell error with backtrace (#14945)

# Description
After this pr, nushell is able to raise errors with a backtrace, which
should make users easier to debug. To enable the feature, users need to
set env variable via `$env.NU_BACKTRACE = 1`. But yeah it might not work
perfectly, there are some corner cases which might not be handled.

I think it should close #13379 in another way.

### About the change

The implementation mostly contained with 2 parts:
1. introduce a new `ChainedError` struct as well as a new
`ShellError::ChainedError` variant. If `eval_instruction` returned an
error, it converts the error to `ShellError::ChainedError`.
`ChainedError` struct is responsable to display errors properly. It
needs to handle the following 2 cases:
- if we run a function which runs `error make` internally, it needs to
display the error itself along with caller span.
- if we run a `error make` directly, or some commands directly returns
an error, we just want nushell raise an error about `error make`.

2. Attach caller spans to `ListStream` and `ByteStream`, because they
are lazy streams, and *only* contains the span that runs it
directly(like `^false`, for example), so nushell needs to add all caller
spans to the stream.
For example: in `def a [] { ^false }; def b [] { a; 33 }; b`, when we
run `b`, which runs `a`, which runs `^false`, the `ByteStream` only
contains the span of `^false`, we need to make it contains the span of
`a`, so nushell is able to get all spans if something bad happened.
This behavior is happened after running `Instruction::Call`, if it
returns a `ByteStream` and `ListStream`, it will call `push_caller_span`
method to attach call spans.

# User-Facing Changes
It's better to demostrate how it works by examples, given the following
definition:
```nushell
> $env.NU_BACKTRACE = 1
> def a [x] { if $x == 3 { error make {msg: 'a custom error'}}}
> def a_2 [x] { if $x == 3 { ^false } else { $x } }
> def a_3 [x] { if $x == 3 { [1 2 3] | each {error make {msg: 'a custom error inside list stream'} } } }
> def b [--list-stream --external] {
    if $external == true {
        # error with non-zero exit code, which is generated from external command.
        a_2 1; a_2 3; a_2 2
    } else if $list_stream == true {
        # error generated by list-stream
        a_3 1; a_3 3; a_3 2
    } else {
        # error generated by command directly
        a 1; a 2; a 3
    }
}
```

Run `b` directly shows the following error:

<details>

```nushell
Error: chained_error

  × oops
   ╭─[entry #27:1:1]
 1 │ b
   · ┬
   · ╰── error happened when running this
   ╰────

Error: chained_error

  × oops
    ╭─[entry #26:10:19]
  9 │         # error generated by command directly
 10 │         a 1; a 2; a 3
    ·                   ┬
    ·                   ╰── error happened when running this
 11 │     }
    ╰────

Error:
  × a custom error
   ╭─[entry #6:1:26]
 1 │ def a [x] { if $x == 3 { error make {msg: 'a custom error'}}}
   ·                          ─────┬────
   ·                               ╰── originates from here
   ╰────
```

</details>

Run `b --list-stream` shows the following error

<details>

```nushell
Error: chained_error

  × oops
   ╭─[entry #28:1:1]
 1 │ b --list-stream
   · ┬
   · ╰── error happened when running this
   ╰────

Error: nu:🐚:eval_block_with_input

  × Eval block failed with pipeline input
   ╭─[entry #26:7:16]
 6 │         # error generated by list-stream
 7 │         a_3 1; a_3 3; a_3 2
   ·                ─┬─
   ·                 ╰── source value
 8 │     } else {
   ╰────

Error: nu:🐚:eval_block_with_input

  × Eval block failed with pipeline input
   ╭─[entry #23:1:29]
 1 │ def a_3 [x] { if $x == 3 { [1 2 3] | each {error make {msg: 'a custom error inside list stream'} } } }
   ·                             ┬
   ·                             ╰── source value
   ╰────

Error:
  × a custom error inside list stream
   ╭─[entry #23:1:44]
 1 │ def a_3 [x] { if $x == 3 { [1 2 3] | each {error make {msg: 'a custom error inside list stream'} } } }
   ·                                            ─────┬────
   ·                                                 ╰── originates from here
   ╰────
```

</details>

Run `b --external` shows the following error:

<details>

```nushell
Error: chained_error

  × oops
   ╭─[entry #29:1:1]
 1 │ b --external
   · ┬
   · ╰── error happened when running this
   ╰────

Error: nu:🐚:eval_block_with_input

  × Eval block failed with pipeline input
   ╭─[entry #26:4:16]
 3 │         # error with non-zero exit code, which is generated from external command.
 4 │         a_2 1; a_2 3; a_2 2
   ·                ─┬─
   ·                 ╰── source value
 5 │     } else if $list_stream == true {
   ╰────

Error: nu:🐚:non_zero_exit_code

  × External command had a non-zero exit code
   ╭─[entry #7:1:29]
 1 │ def a_2 [x] { if $x == 3 { ^false } else { $x } }
   ·                             ──┬──
   ·                               ╰── exited with code 1
   ╰────
```

</details>

It also added a message to guide the usage of NU_BACKTRACE, see the last
line in the following example:
```shell
 ls asdfasd
Error: nu:🐚:io::not_found

  × I/O error
  ╰─▶   × Entity not found

   ╭─[entry #17:1:4]
 1 │ ls asdfasd
   ·    ───┬───
   ·       ╰── Entity not found
   ╰────
  help: The error occurred at '/home/windsoilder/projects/nushell/asdfasd'

set the `NU_BACKTRACE=1` environment variable to display a backtrace.
```
# Tests + Formatting
Added some tests for the behavior.

# After Submitting
This commit is contained in:
Wind
2025-02-06 22:05:58 +08:00
committed by GitHub
parent bdc767bf23
commit 2f18b9c856
10 changed files with 326 additions and 8 deletions

View File

@ -187,6 +187,7 @@ fn eval_ir_block_impl<D: DebugContext>(
// Program counter, starts at zero.
let mut pc = 0;
let need_backtrace = ctx.engine_state.get_env_var("NU_BACKTRACE").is_some();
while pc < ir_block.instructions.len() {
let instruction = &ir_block.instructions[pc];
@ -195,7 +196,7 @@ fn eval_ir_block_impl<D: DebugContext>(
D::enter_instruction(ctx.engine_state, ir_block, pc, ctx.registers);
let result = eval_instruction::<D>(ctx, instruction, span, ast);
let result = eval_instruction::<D>(ctx, instruction, span, ast, need_backtrace);
D::leave_instruction(
ctx.engine_state,
@ -228,8 +229,10 @@ fn eval_ir_block_impl<D: DebugContext>(
// If an error handler is set, branch there
prepare_error_handler(ctx, error_handler, Some(err.into_spanned(*span)));
pc = error_handler.handler_index;
} else if need_backtrace {
let err = ShellError::into_chainned(err, *span);
return Err(err);
} else {
// If not, exit the block with the error
return Err(err);
}
}
@ -285,6 +288,7 @@ fn eval_instruction<D: DebugContext>(
instruction: &Instruction,
span: &Span,
ast: &Option<IrAstRef>,
need_backtrace: bool,
) -> Result<InstructionResult, ShellError> {
use self::InstructionResult::*;
@ -548,7 +552,14 @@ fn eval_instruction<D: DebugContext>(
}
Instruction::Call { decl_id, src_dst } => {
let input = ctx.take_reg(*src_dst);
let result = eval_call::<D>(ctx, *decl_id, *span, input)?;
let mut result = eval_call::<D>(ctx, *decl_id, *span, input)?;
if need_backtrace {
match &mut result {
PipelineData::ByteStream(s, ..) => s.push_caller_span(*span),
PipelineData::ListStream(s, ..) => s.push_caller_span(*span),
_ => (),
};
}
ctx.put_reg(*src_dst, result);
Ok(Continue)
}
@ -1457,14 +1468,40 @@ fn drain(ctx: &mut EvalContext<'_>, data: PipelineData) -> Result<InstructionRes
match data {
PipelineData::ByteStream(stream, ..) => {
let span = stream.span();
if let Err(err) = stream.drain() {
let callback_spans = stream.get_caller_spans().clone();
if let Err(mut err) = stream.drain() {
ctx.stack.set_last_error(&err);
return Err(err);
if callback_spans.is_empty() {
return Err(err);
} else {
for s in callback_spans {
err = ShellError::EvalBlockWithInput {
span: s,
sources: vec![err],
}
}
return Err(err);
}
} else {
ctx.stack.set_last_exit_code(0, span);
}
}
PipelineData::ListStream(stream, ..) => stream.drain()?,
PipelineData::ListStream(stream, ..) => {
let callback_spans = stream.get_caller_spans().clone();
if let Err(mut err) = stream.drain() {
if callback_spans.is_empty() {
return Err(err);
} else {
for s in callback_spans {
err = ShellError::EvalBlockWithInput {
span: s,
sources: vec![err],
}
}
return Err(err);
}
}
}
PipelineData::Value(..) | PipelineData::Empty => {}
}
Ok(Continue)