Files
nushell/crates/nu-engine/src/eval_ir.rs
Devyn Cairns d7392f1f3b Internal representation (IR) compiler and evaluator (#13330)
# Description

This PR adds an internal representation language to Nushell, offering an
alternative evaluator based on simple instructions, stream-containing
registers, and indexed control flow. The number of registers required is
determined statically at compile-time, and the fixed size required is
allocated upon entering the block.

Each instruction is associated with a span, which makes going backwards
from IR instructions to source code very easy.

Motivations for IR:

1. **Performance.** By simplifying the evaluation path and making it
more cache-friendly and branch predictor-friendly, code that does a lot
of computation in Nushell itself can be sped up a decent bit. Because
the IR is fairly easy to reason about, we can also implement
optimization passes in the future to eliminate and simplify code.
2. **Correctness.** The instructions mostly have very simple and
easily-specified behavior, so hopefully engine changes are a little bit
easier to reason about, and they can be specified in a more formal way
at some point. I have made an effort to document each of the
instructions in the docs for the enum itself in a reasonably specific
way. Some of the errors that would have happened during evaluation
before are now moved to the compilation step instead, because they don't
make sense to check during evaluation.
3. **As an intermediate target.** This is a good step for us to bring
the [`new-nu-parser`](https://github.com/nushell/new-nu-parser) in at
some point, as code generated from new AST can be directly compared to
code generated from old AST. If the IR code is functionally equivalent,
it will behave the exact same way.
4. **Debugging.** With a little bit more work, we can probably give
control over advancing the virtual machine that `IrBlock`s run on to
some sort of external driver, making things like breakpoints and single
stepping possible. Tools like `view ir` and [`explore
ir`](https://github.com/devyn/nu_plugin_explore_ir) make it easier than
before to see what exactly is going on with your Nushell code.

The goal is to eventually replace the AST evaluator entirely, once we're
sure it's working just as well. You can help dogfood this by running
Nushell with `$env.NU_USE_IR` set to some value. The environment
variable is checked when Nushell starts, so config runs with IR, or it
can also be set on a line at the REPL to change it dynamically. It is
also checked when running `do` in case within a script you want to just
run a specific piece of code with or without IR.

# Example

```nushell
view ir { |data|
  mut sum = 0
  for n in $data {
    $sum += $n
  }
  $sum
}
```
  
```gas
# 3 registers, 19 instructions, 0 bytes of data
   0: load-literal           %0, int(0)
   1: store-variable         var 904, %0 # let
   2: drain                  %0
   3: drop                   %0
   4: load-variable          %1, var 903
   5: iterate                %0, %1, end 15 # for, label(1), from(14:)
   6: store-variable         var 905, %0
   7: load-variable          %0, var 904
   8: load-variable          %2, var 905
   9: binary-op              %0, Math(Plus), %2
  10: span                   %0
  11: store-variable         var 904, %0
  12: load-literal           %0, nothing
  13: drain                  %0
  14: jump                   5
  15: drop                   %0          # label(0), from(5:)
  16: drain                  %0
  17: load-variable          %0, var 904
  18: return                 %0
```

# Benchmarks

All benchmarks run on a base model Mac Mini M1.

## Iterative Fibonacci sequence

This is about as best case as possible, making use of the much faster
control flow. Most code will not experience a speed improvement nearly
this large.

```nushell
def fib [n: int] {
  mut a = 0
  mut b = 1
  for _ in 2..=$n {
    let c = $a + $b
    $a = $b
    $b = $c
  }
  $b
}
use std bench
bench { 0..50 | each { |n| fib $n } }
```

IR disabled:

```
╭───────┬─────────────────╮
│ mean  │ 1ms 924µs 665ns │
│ min   │ 1ms 700µs 83ns  │
│ max   │ 3ms 450µs 125ns │
│ std   │ 395µs 759ns     │
│ times │ [list 50 items] │
╰───────┴─────────────────╯
```

IR enabled:

```
╭───────┬─────────────────╮
│ mean  │ 452µs 820ns     │
│ min   │ 427µs 417ns     │
│ max   │ 540µs 167ns     │
│ std   │ 17µs 158ns      │
│ times │ [list 50 items] │
╰───────┴─────────────────╯
```

![explore ir
view](https://github.com/nushell/nushell/assets/10729/d7bccc03-5222-461c-9200-0dce71b83b83)

##
[gradient_benchmark_no_check.nu](https://github.com/nushell/nu_scripts/blob/main/benchmarks/gradient_benchmark_no_check.nu)

IR disabled:

```
╭───┬──────────────────╮
│ 0 │ 27ms 929µs 958ns │
│ 1 │ 21ms 153µs 459ns │
│ 2 │ 18ms 639µs 666ns │
│ 3 │ 19ms 554µs 583ns │
│ 4 │ 13ms 383µs 375ns │
│ 5 │ 11ms 328µs 208ns │
│ 6 │  5ms 659µs 542ns │
╰───┴──────────────────╯
```

IR enabled:

```
╭───┬──────────────────╮
│ 0 │       22ms 662µs │
│ 1 │ 17ms 221µs 792ns │
│ 2 │ 14ms 786µs 708ns │
│ 3 │ 13ms 876µs 834ns │
│ 4 │  13ms 52µs 875ns │
│ 5 │ 11ms 269µs 666ns │
│ 6 │  6ms 942µs 500ns │
╰───┴──────────────────╯
```

##
[random-bytes.nu](https://github.com/nushell/nu_scripts/blob/main/benchmarks/random-bytes.nu)

I got pretty random results out of this benchmark so I decided not to
include it. Not clear why.

# User-Facing Changes
- IR compilation errors may appear even if the user isn't evaluating
with IR.
- IR evaluation can be enabled by setting the `NU_USE_IR` environment
variable to any value.
- New command `view ir` pretty-prints the IR for a block, and `view ir
--json` can be piped into an external tool like [`explore
ir`](https://github.com/devyn/nu_plugin_explore_ir).

# Tests + Formatting
All tests are passing with `NU_USE_IR=1`, and I've added some more eval
tests to compare the results for some very core operations. I will
probably want to add some more so we don't have to always check
`NU_USE_IR=1 toolkit test --workspace` on a regular basis.

# After Submitting
- [ ] release notes
- [ ] further documentation of instructions?
- [ ] post-release: publish `nu_plugin_explore_ir`
2024-07-10 17:33:59 -07:00

1463 lines
54 KiB
Rust

use std::{borrow::Cow, fs::File, sync::Arc};
use nu_path::{expand_path_with, AbsolutePathBuf};
use nu_protocol::{
ast::{Bits, Block, Boolean, CellPath, Comparison, Math, Operator},
debugger::DebugContext,
engine::{Argument, Closure, EngineState, ErrorHandler, Matcher, Redirection, Stack},
ir::{Call, DataSlice, Instruction, IrAstRef, IrBlock, Literal, RedirectMode},
record, ByteStreamSource, DeclId, ErrSpan, Flag, IntoPipelineData, IntoSpanned, ListStream,
OutDest, PipelineData, PositionalArg, Range, Record, RegId, ShellError, Signals, Signature,
Span, Spanned, Type, Value, VarId, ENV_VARIABLE_ID,
};
use nu_utils::IgnoreCaseExt;
use crate::{eval::is_automatic_env_var, eval_block_with_early_return};
/// Evaluate the compiled representation of a [`Block`].
pub fn eval_ir_block<D: DebugContext>(
engine_state: &EngineState,
stack: &mut Stack,
block: &Block,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
// Rust does not check recursion limits outside of const evaluation.
// But nu programs run in the same process as the shell.
// To prevent a stack overflow in user code from crashing the shell,
// we limit the recursion depth of function calls.
let maximum_call_stack_depth: u64 = engine_state.config.recursion_limit as u64;
if stack.recursion_count > maximum_call_stack_depth {
return Err(ShellError::RecursionLimitReached {
recursion_limit: maximum_call_stack_depth,
span: block.span,
});
}
if let Some(ir_block) = &block.ir_block {
D::enter_block(engine_state, block);
let args_base = stack.arguments.get_base();
let error_handler_base = stack.error_handlers.get_base();
// Allocate and initialize registers. I've found that it's not really worth trying to avoid
// the heap allocation here by reusing buffers - our allocator is fast enough
let mut registers = Vec::with_capacity(ir_block.register_count as usize);
for _ in 0..ir_block.register_count {
registers.push(PipelineData::Empty);
}
// Initialize file storage.
let mut files = vec![None; ir_block.file_count as usize];
let result = eval_ir_block_impl::<D>(
&mut EvalContext {
engine_state,
stack,
data: &ir_block.data,
block_span: &block.span,
args_base,
error_handler_base,
redirect_out: None,
redirect_err: None,
matches: vec![],
registers: &mut registers[..],
files: &mut files[..],
},
ir_block,
input,
);
stack.error_handlers.leave_frame(error_handler_base);
stack.arguments.leave_frame(args_base);
D::leave_block(engine_state, block);
result
} else {
// FIXME blocks having IR should not be optional
Err(ShellError::GenericError {
error: "Can't evaluate block in IR mode".into(),
msg: "block is missing compiled representation".into(),
span: block.span,
help: Some("the IrBlock is probably missing due to a compilation error".into()),
inner: vec![],
})
}
}
/// All of the pointers necessary for evaluation
struct EvalContext<'a> {
engine_state: &'a EngineState,
stack: &'a mut Stack,
data: &'a Arc<[u8]>,
/// The span of the block
block_span: &'a Option<Span>,
/// Base index on the argument stack to reset to after a call
args_base: usize,
/// Base index on the error handler stack to reset to after a call
error_handler_base: usize,
/// State set by redirect-out
redirect_out: Option<Redirection>,
/// State set by redirect-err
redirect_err: Option<Redirection>,
/// Scratch space to use for `match`
matches: Vec<(VarId, Value)>,
/// Intermediate pipeline data storage used by instructions, indexed by RegId
registers: &'a mut [PipelineData],
/// Holds open files used by redirections
files: &'a mut [Option<Arc<File>>],
}
impl<'a> EvalContext<'a> {
/// Replace the contents of a register with a new value
#[inline]
fn put_reg(&mut self, reg_id: RegId, new_value: PipelineData) {
// log::trace!("{reg_id} <- {new_value:?}");
self.registers[reg_id.0 as usize] = new_value;
}
/// Borrow the contents of a register.
#[inline]
fn borrow_reg(&self, reg_id: RegId) -> &PipelineData {
&self.registers[reg_id.0 as usize]
}
/// Replace the contents of a register with `Empty` and then return the value that it contained
#[inline]
fn take_reg(&mut self, reg_id: RegId) -> PipelineData {
// log::trace!("<- {reg_id}");
std::mem::replace(&mut self.registers[reg_id.0 as usize], PipelineData::Empty)
}
/// Clone data from a register. Must be collected first.
fn clone_reg(&mut self, reg_id: RegId, error_span: Span) -> Result<PipelineData, ShellError> {
match &self.registers[reg_id.0 as usize] {
PipelineData::Empty => Ok(PipelineData::Empty),
PipelineData::Value(val, meta) => Ok(PipelineData::Value(val.clone(), meta.clone())),
_ => Err(ShellError::IrEvalError {
msg: "Must collect to value before using instruction that clones from a register"
.into(),
span: Some(error_span),
}),
}
}
/// Clone a value from a register. Must be collected first.
fn clone_reg_value(&mut self, reg_id: RegId, fallback_span: Span) -> Result<Value, ShellError> {
match self.clone_reg(reg_id, fallback_span)? {
PipelineData::Empty => Ok(Value::nothing(fallback_span)),
PipelineData::Value(val, _) => Ok(val),
_ => unreachable!("clone_reg should never return stream data"),
}
}
/// Take and implicitly collect a register to a value
fn collect_reg(&mut self, reg_id: RegId, fallback_span: Span) -> Result<Value, ShellError> {
let data = self.take_reg(reg_id);
let span = data.span().unwrap_or(fallback_span);
data.into_value(span)
}
/// Get a string from data or produce evaluation error if it's invalid UTF-8
fn get_str(&self, slice: DataSlice, error_span: Span) -> Result<&'a str, ShellError> {
std::str::from_utf8(&self.data[slice]).map_err(|_| ShellError::IrEvalError {
msg: format!("data slice does not refer to valid UTF-8: {slice:?}"),
span: Some(error_span),
})
}
}
/// Eval an IR block on the provided slice of registers.
fn eval_ir_block_impl<D: DebugContext>(
ctx: &mut EvalContext<'_>,
ir_block: &IrBlock,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
if !ctx.registers.is_empty() {
ctx.registers[0] = input;
}
// Program counter, starts at zero.
let mut pc = 0;
while pc < ir_block.instructions.len() {
let instruction = &ir_block.instructions[pc];
let span = &ir_block.spans[pc];
let ast = &ir_block.ast[pc];
log::trace!(
"{pc:-4}: {}",
instruction.display(ctx.engine_state, ctx.data)
);
match eval_instruction::<D>(ctx, instruction, span, ast) {
Ok(InstructionResult::Continue) => {
pc += 1;
}
Ok(InstructionResult::Branch(next_pc)) => {
pc = next_pc;
}
Ok(InstructionResult::Return(reg_id)) => {
return Ok(ctx.take_reg(reg_id));
}
Ok(InstructionResult::ExitCode(exit_code)) => {
if let Some(error_handler) = ctx.stack.error_handlers.pop(ctx.error_handler_base) {
// If an error handler is set, branch there
prepare_error_handler(ctx, error_handler, None);
pc = error_handler.handler_index;
} else {
// If not, exit the block with the exit code
return Ok(PipelineData::new_external_stream_with_only_exit_code(
exit_code,
));
}
}
Err(
err @ (ShellError::Return { .. }
| ShellError::Continue { .. }
| ShellError::Break { .. }),
) => {
// These block control related errors should be passed through
return Err(err);
}
Err(err) => {
if let Some(error_handler) = ctx.stack.error_handlers.pop(ctx.error_handler_base) {
// 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 not, exit the block with the error
return Err(err);
}
}
}
}
// Fell out of the loop, without encountering a Return.
Err(ShellError::IrEvalError {
msg: format!(
"Program counter out of range (pc={pc}, len={len})",
len = ir_block.instructions.len(),
),
span: *ctx.block_span,
})
}
/// Prepare the context for an error handler
fn prepare_error_handler(
ctx: &mut EvalContext<'_>,
error_handler: ErrorHandler,
error: Option<Spanned<ShellError>>,
) {
if let Some(reg_id) = error_handler.error_register {
if let Some(error) = error {
// Create the error value and put it in the register
let value = Value::record(
record! {
"msg" => Value::string(format!("{}", error.item), error.span),
"debug" => Value::string(format!("{:?}", error.item), error.span),
"raw" => Value::error(error.item, error.span),
},
error.span,
);
ctx.put_reg(reg_id, PipelineData::Value(value, None));
} else {
// Set the register to empty
ctx.put_reg(reg_id, PipelineData::Empty);
}
}
}
/// The result of performing an instruction. Describes what should happen next
#[derive(Debug)]
enum InstructionResult {
Continue,
Branch(usize),
Return(RegId),
ExitCode(i32),
}
/// Perform an instruction
fn eval_instruction<D: DebugContext>(
ctx: &mut EvalContext<'_>,
instruction: &Instruction,
span: &Span,
ast: &Option<IrAstRef>,
) -> Result<InstructionResult, ShellError> {
use self::InstructionResult::*;
// See the docs for `Instruction` for more information on what these instructions are supposed
// to do.
match instruction {
Instruction::Unreachable => Err(ShellError::IrEvalError {
msg: "Reached unreachable code".into(),
span: Some(*span),
}),
Instruction::LoadLiteral { dst, lit } => load_literal(ctx, *dst, lit, *span),
Instruction::LoadValue { dst, val } => {
ctx.put_reg(*dst, Value::clone(val).into_pipeline_data());
Ok(Continue)
}
Instruction::Move { dst, src } => {
let val = ctx.take_reg(*src);
ctx.put_reg(*dst, val);
Ok(Continue)
}
Instruction::Clone { dst, src } => {
let data = ctx.clone_reg(*src, *span)?;
ctx.put_reg(*dst, data);
Ok(Continue)
}
Instruction::Collect { src_dst } => {
let data = ctx.take_reg(*src_dst);
let value = collect(data, *span)?;
ctx.put_reg(*src_dst, value);
Ok(Continue)
}
Instruction::Span { src_dst } => {
let data = ctx.take_reg(*src_dst);
let spanned = data.with_span(*span);
ctx.put_reg(*src_dst, spanned);
Ok(Continue)
}
Instruction::Drop { src } => {
ctx.take_reg(*src);
Ok(Continue)
}
Instruction::Drain { src } => {
let data = ctx.take_reg(*src);
drain(ctx, data)
}
Instruction::LoadVariable { dst, var_id } => {
let value = get_var(ctx, *var_id, *span)?;
ctx.put_reg(*dst, value.into_pipeline_data());
Ok(Continue)
}
Instruction::StoreVariable { var_id, src } => {
let value = ctx.collect_reg(*src, *span)?;
ctx.stack.add_var(*var_id, value);
Ok(Continue)
}
Instruction::LoadEnv { dst, key } => {
let key = ctx.get_str(*key, *span)?;
if let Some(value) = get_env_var_case_insensitive(ctx, key) {
let new_value = value.clone().into_pipeline_data();
ctx.put_reg(*dst, new_value);
Ok(Continue)
} else {
// FIXME: using the same span twice, shouldn't this really be
// EnvVarNotFoundAtRuntime? There are tests that depend on CantFindColumn though...
Err(ShellError::CantFindColumn {
col_name: key.into(),
span: Some(*span),
src_span: *span,
})
}
}
Instruction::LoadEnvOpt { dst, key } => {
let key = ctx.get_str(*key, *span)?;
let value = get_env_var_case_insensitive(ctx, key)
.cloned()
.unwrap_or(Value::nothing(*span));
ctx.put_reg(*dst, value.into_pipeline_data());
Ok(Continue)
}
Instruction::StoreEnv { key, src } => {
let key = ctx.get_str(*key, *span)?;
let value = ctx.collect_reg(*src, *span)?;
let key = get_env_var_name_case_insensitive(ctx, key);
if !is_automatic_env_var(&key) {
ctx.stack.add_env_var(key.into_owned(), value);
Ok(Continue)
} else {
Err(ShellError::AutomaticEnvVarSetManually {
envvar_name: key.into(),
span: *span,
})
}
}
Instruction::PushPositional { src } => {
let val = ctx.collect_reg(*src, *span)?.with_span(*span);
ctx.stack.arguments.push(Argument::Positional {
span: *span,
val,
ast: ast.clone().map(|ast_ref| ast_ref.0),
});
Ok(Continue)
}
Instruction::AppendRest { src } => {
let vals = ctx.collect_reg(*src, *span)?.with_span(*span);
ctx.stack.arguments.push(Argument::Spread {
span: *span,
vals,
ast: ast.clone().map(|ast_ref| ast_ref.0),
});
Ok(Continue)
}
Instruction::PushFlag { name } => {
let data = ctx.data.clone();
ctx.stack.arguments.push(Argument::Flag {
data,
name: *name,
short: DataSlice::empty(),
span: *span,
});
Ok(Continue)
}
Instruction::PushShortFlag { short } => {
let data = ctx.data.clone();
ctx.stack.arguments.push(Argument::Flag {
data,
name: DataSlice::empty(),
short: *short,
span: *span,
});
Ok(Continue)
}
Instruction::PushNamed { name, src } => {
let val = ctx.collect_reg(*src, *span)?.with_span(*span);
let data = ctx.data.clone();
ctx.stack.arguments.push(Argument::Named {
data,
name: *name,
short: DataSlice::empty(),
span: *span,
val,
ast: ast.clone().map(|ast_ref| ast_ref.0),
});
Ok(Continue)
}
Instruction::PushShortNamed { short, src } => {
let val = ctx.collect_reg(*src, *span)?.with_span(*span);
let data = ctx.data.clone();
ctx.stack.arguments.push(Argument::Named {
data,
name: DataSlice::empty(),
short: *short,
span: *span,
val,
ast: ast.clone().map(|ast_ref| ast_ref.0),
});
Ok(Continue)
}
Instruction::PushParserInfo { name, info } => {
let data = ctx.data.clone();
ctx.stack.arguments.push(Argument::ParserInfo {
data,
name: *name,
info: info.clone(),
});
Ok(Continue)
}
Instruction::RedirectOut { mode } => {
ctx.redirect_out = eval_redirection(ctx, mode, *span, RedirectionStream::Out)?;
Ok(Continue)
}
Instruction::RedirectErr { mode } => {
ctx.redirect_err = eval_redirection(ctx, mode, *span, RedirectionStream::Err)?;
Ok(Continue)
}
Instruction::CheckErrRedirected { src } => match ctx.borrow_reg(*src) {
PipelineData::ByteStream(stream, _)
if matches!(stream.source(), ByteStreamSource::Child(_)) =>
{
Ok(Continue)
}
_ => Err(ShellError::GenericError {
error: "Can't redirect stderr of internal command output".into(),
msg: "piping stderr only works on external commands".into(),
span: Some(*span),
help: None,
inner: vec![],
}),
},
Instruction::OpenFile {
file_num,
path,
append,
} => {
let path = ctx.collect_reg(*path, *span)?;
let file = open_file(ctx, &path, *append)?;
ctx.files[*file_num as usize] = Some(file);
Ok(Continue)
}
Instruction::WriteFile { file_num, src } => {
let src = ctx.take_reg(*src);
let file = ctx
.files
.get(*file_num as usize)
.cloned()
.flatten()
.ok_or_else(|| ShellError::IrEvalError {
msg: format!("Tried to write to file #{file_num}, but it is not open"),
span: Some(*span),
})?;
let result = {
let mut stack = ctx
.stack
.push_redirection(Some(Redirection::File(file)), None);
src.write_to_out_dests(ctx.engine_state, &mut stack)?
};
// Abort execution if there's an exit code from a failed external
drain(ctx, result)
}
Instruction::CloseFile { file_num } => {
if ctx.files[*file_num as usize].take().is_some() {
Ok(Continue)
} else {
Err(ShellError::IrEvalError {
msg: format!("Tried to close file #{file_num}, but it is not open"),
span: Some(*span),
})
}
}
Instruction::Call { decl_id, src_dst } => {
let input = ctx.take_reg(*src_dst);
let result = eval_call::<D>(ctx, *decl_id, *span, input)?;
ctx.put_reg(*src_dst, result);
Ok(Continue)
}
Instruction::StringAppend { src_dst, val } => {
let string_value = ctx.collect_reg(*src_dst, *span)?;
let operand_value = ctx.collect_reg(*val, *span)?;
let string_span = string_value.span();
let mut string = string_value.into_string()?;
let operand = if let Value::String { val, .. } = operand_value {
// Small optimization, so we don't have to copy the string *again*
val
} else {
operand_value.to_expanded_string(", ", ctx.engine_state.get_config())
};
string.push_str(&operand);
let new_string_value = Value::string(string, string_span);
ctx.put_reg(*src_dst, new_string_value.into_pipeline_data());
Ok(Continue)
}
Instruction::GlobFrom { src_dst, no_expand } => {
let string_value = ctx.collect_reg(*src_dst, *span)?;
let glob_value = if matches!(string_value, Value::Glob { .. }) {
// It already is a glob, so don't touch it.
string_value
} else {
// Treat it as a string, then cast
let string = string_value.into_string()?;
Value::glob(string, *no_expand, *span)
};
ctx.put_reg(*src_dst, glob_value.into_pipeline_data());
Ok(Continue)
}
Instruction::ListPush { src_dst, item } => {
let list_value = ctx.collect_reg(*src_dst, *span)?;
let item = ctx.collect_reg(*item, *span)?;
let list_span = list_value.span();
let mut list = list_value.into_list()?;
list.push(item);
ctx.put_reg(*src_dst, Value::list(list, list_span).into_pipeline_data());
Ok(Continue)
}
Instruction::ListSpread { src_dst, items } => {
let list_value = ctx.collect_reg(*src_dst, *span)?;
let items = ctx.collect_reg(*items, *span)?;
let list_span = list_value.span();
let items_span = items.span();
let mut list = list_value.into_list()?;
list.extend(
items
.into_list()
.map_err(|_| ShellError::CannotSpreadAsList { span: items_span })?,
);
ctx.put_reg(*src_dst, Value::list(list, list_span).into_pipeline_data());
Ok(Continue)
}
Instruction::RecordInsert { src_dst, key, val } => {
let record_value = ctx.collect_reg(*src_dst, *span)?;
let key = ctx.collect_reg(*key, *span)?;
let val = ctx.collect_reg(*val, *span)?;
let record_span = record_value.span();
let mut record = record_value.into_record()?;
let key = key.coerce_into_string()?;
if let Some(old_value) = record.insert(&key, val) {
return Err(ShellError::ColumnDefinedTwice {
col_name: key,
second_use: *span,
first_use: old_value.span(),
});
}
ctx.put_reg(
*src_dst,
Value::record(record, record_span).into_pipeline_data(),
);
Ok(Continue)
}
Instruction::RecordSpread { src_dst, items } => {
let record_value = ctx.collect_reg(*src_dst, *span)?;
let items = ctx.collect_reg(*items, *span)?;
let record_span = record_value.span();
let items_span = items.span();
let mut record = record_value.into_record()?;
// Not using .extend() here because it doesn't handle duplicates
for (key, val) in items
.into_record()
.map_err(|_| ShellError::CannotSpreadAsRecord { span: items_span })?
{
if let Some(first_value) = record.insert(&key, val) {
return Err(ShellError::ColumnDefinedTwice {
col_name: key,
second_use: *span,
first_use: first_value.span(),
});
}
}
ctx.put_reg(
*src_dst,
Value::record(record, record_span).into_pipeline_data(),
);
Ok(Continue)
}
Instruction::Not { src_dst } => {
let bool = ctx.collect_reg(*src_dst, *span)?;
let negated = !bool.as_bool()?;
ctx.put_reg(
*src_dst,
Value::bool(negated, bool.span()).into_pipeline_data(),
);
Ok(Continue)
}
Instruction::BinaryOp { lhs_dst, op, rhs } => binary_op(ctx, *lhs_dst, op, *rhs, *span),
Instruction::FollowCellPath { src_dst, path } => {
let data = ctx.take_reg(*src_dst);
let path = ctx.take_reg(*path);
if let PipelineData::Value(Value::CellPath { val: path, .. }, _) = path {
let value = data.follow_cell_path(&path.members, *span, true)?;
ctx.put_reg(*src_dst, value.into_pipeline_data());
Ok(Continue)
} else if let PipelineData::Value(Value::Error { error, .. }, _) = path {
Err(*error)
} else {
Err(ShellError::TypeMismatch {
err_message: "expected cell path".into(),
span: path.span().unwrap_or(*span),
})
}
}
Instruction::CloneCellPath { dst, src, path } => {
let value = ctx.clone_reg_value(*src, *span)?;
let path = ctx.take_reg(*path);
if let PipelineData::Value(Value::CellPath { val: path, .. }, _) = path {
// TODO: make follow_cell_path() not have to take ownership, probably using Cow
let value = value.follow_cell_path(&path.members, true)?;
ctx.put_reg(*dst, value.into_pipeline_data());
Ok(Continue)
} else if let PipelineData::Value(Value::Error { error, .. }, _) = path {
Err(*error)
} else {
Err(ShellError::TypeMismatch {
err_message: "expected cell path".into(),
span: path.span().unwrap_or(*span),
})
}
}
Instruction::UpsertCellPath {
src_dst,
path,
new_value,
} => {
let data = ctx.take_reg(*src_dst);
let metadata = data.metadata();
// Change the span because we're modifying it
let mut value = data.into_value(*span)?;
let path = ctx.take_reg(*path);
let new_value = ctx.collect_reg(*new_value, *span)?;
if let PipelineData::Value(Value::CellPath { val: path, .. }, _) = path {
value.upsert_data_at_cell_path(&path.members, new_value)?;
ctx.put_reg(*src_dst, value.into_pipeline_data_with_metadata(metadata));
Ok(Continue)
} else if let PipelineData::Value(Value::Error { error, .. }, _) = path {
Err(*error)
} else {
Err(ShellError::TypeMismatch {
err_message: "expected cell path".into(),
span: path.span().unwrap_or(*span),
})
}
}
Instruction::Jump { index } => Ok(Branch(*index)),
Instruction::BranchIf { cond, index } => {
let data = ctx.take_reg(*cond);
let data_span = data.span();
let val = match data {
PipelineData::Value(Value::Bool { val, .. }, _) => val,
PipelineData::Value(Value::Error { error, .. }, _) => {
return Err(*error);
}
_ => {
return Err(ShellError::TypeMismatch {
err_message: "expected bool".into(),
span: data_span.unwrap_or(*span),
});
}
};
if val {
Ok(Branch(*index))
} else {
Ok(Continue)
}
}
Instruction::BranchIfEmpty { src, index } => {
let is_empty = matches!(
ctx.borrow_reg(*src),
PipelineData::Empty | PipelineData::Value(Value::Nothing { .. }, _)
);
if is_empty {
Ok(Branch(*index))
} else {
Ok(Continue)
}
}
Instruction::Match {
pattern,
src,
index,
} => {
let value = ctx.clone_reg_value(*src, *span)?;
ctx.matches.clear();
if pattern.match_value(&value, &mut ctx.matches) {
// Match succeeded: set variables and branch
for (var_id, match_value) in ctx.matches.drain(..) {
ctx.stack.add_var(var_id, match_value);
}
Ok(Branch(*index))
} else {
// Failed to match, put back original value
ctx.matches.clear();
Ok(Continue)
}
}
Instruction::CheckMatchGuard { src } => {
if matches!(
ctx.borrow_reg(*src),
PipelineData::Value(Value::Bool { .. }, _)
) {
Ok(Continue)
} else {
Err(ShellError::MatchGuardNotBool { span: *span })
}
}
Instruction::Iterate {
dst,
stream,
end_index,
} => eval_iterate(ctx, *dst, *stream, *end_index),
Instruction::OnError { index } => {
ctx.stack.error_handlers.push(ErrorHandler {
handler_index: *index,
error_register: None,
});
Ok(Continue)
}
Instruction::OnErrorInto { index, dst } => {
ctx.stack.error_handlers.push(ErrorHandler {
handler_index: *index,
error_register: Some(*dst),
});
Ok(Continue)
}
Instruction::PopErrorHandler => {
ctx.stack.error_handlers.pop(ctx.error_handler_base);
Ok(Continue)
}
Instruction::CheckExternalFailed { dst, src } => {
let data = ctx.take_reg(*src);
let (data, failed) = data.check_external_failed()?;
ctx.put_reg(*src, data);
ctx.put_reg(*dst, Value::bool(failed, *span).into_pipeline_data());
Ok(Continue)
}
Instruction::ReturnEarly { src } => {
let val = ctx.collect_reg(*src, *span)?;
Err(ShellError::Return {
span: *span,
value: Box::new(val),
})
}
Instruction::Return { src } => Ok(Return(*src)),
}
}
/// Load a literal value into a register
fn load_literal(
ctx: &mut EvalContext<'_>,
dst: RegId,
lit: &Literal,
span: Span,
) -> Result<InstructionResult, ShellError> {
let value = literal_value(ctx, lit, span)?;
ctx.put_reg(dst, PipelineData::Value(value, None));
Ok(InstructionResult::Continue)
}
fn literal_value(
ctx: &mut EvalContext<'_>,
lit: &Literal,
span: Span,
) -> Result<Value, ShellError> {
Ok(match lit {
Literal::Bool(b) => Value::bool(*b, span),
Literal::Int(i) => Value::int(*i, span),
Literal::Float(f) => Value::float(*f, span),
Literal::Filesize(q) => Value::filesize(*q, span),
Literal::Duration(q) => Value::duration(*q, span),
Literal::Binary(bin) => Value::binary(&ctx.data[*bin], span),
Literal::Block(block_id) | Literal::RowCondition(block_id) | Literal::Closure(block_id) => {
let block = ctx.engine_state.get_block(*block_id);
let captures = block
.captures
.iter()
.map(|var_id| get_var(ctx, *var_id, span).map(|val| (*var_id, val)))
.collect::<Result<Vec<_>, ShellError>>()?;
Value::closure(
Closure {
block_id: *block_id,
captures,
},
span,
)
}
Literal::Range {
start,
step,
end,
inclusion,
} => {
let start = ctx.collect_reg(*start, span)?;
let step = ctx.collect_reg(*step, span)?;
let end = ctx.collect_reg(*end, span)?;
let range = Range::new(start, step, end, *inclusion, span)?;
Value::range(range, span)
}
Literal::List { capacity } => Value::list(Vec::with_capacity(*capacity), span),
Literal::Record { capacity } => Value::record(Record::with_capacity(*capacity), span),
Literal::Filepath {
val: path,
no_expand,
} => {
let path = ctx.get_str(*path, span)?;
if *no_expand {
Value::string(path, span)
} else {
let cwd = ctx.engine_state.cwd(Some(ctx.stack))?;
let path = expand_path_with(path, cwd, true);
Value::string(path.to_string_lossy(), span)
}
}
Literal::Directory {
val: path,
no_expand,
} => {
let path = ctx.get_str(*path, span)?;
if path == "-" {
Value::string("-", span)
} else if *no_expand {
Value::string(path, span)
} else {
let cwd = ctx
.engine_state
.cwd(Some(ctx.stack))
.map(AbsolutePathBuf::into_std_path_buf)
.unwrap_or_default();
let path = expand_path_with(path, cwd, true);
Value::string(path.to_string_lossy(), span)
}
}
Literal::GlobPattern { val, no_expand } => {
Value::glob(ctx.get_str(*val, span)?, *no_expand, span)
}
Literal::String(s) => Value::string(ctx.get_str(*s, span)?, span),
Literal::RawString(s) => Value::string(ctx.get_str(*s, span)?, span),
Literal::CellPath(path) => Value::cell_path(CellPath::clone(path), span),
Literal::Date(dt) => Value::date(**dt, span),
Literal::Nothing => Value::nothing(span),
})
}
fn binary_op(
ctx: &mut EvalContext<'_>,
lhs_dst: RegId,
op: &Operator,
rhs: RegId,
span: Span,
) -> Result<InstructionResult, ShellError> {
let lhs_val = ctx.collect_reg(lhs_dst, span)?;
let rhs_val = ctx.collect_reg(rhs, span)?;
// Handle binary op errors early
if let Value::Error { error, .. } = lhs_val {
return Err(*error);
}
if let Value::Error { error, .. } = rhs_val {
return Err(*error);
}
// We only have access to one span here, but the generated code usually adds a `span`
// instruction to set the output span to the right span.
let op_span = span;
let result = match op {
Operator::Comparison(cmp) => match cmp {
Comparison::Equal => lhs_val.eq(op_span, &rhs_val, span)?,
Comparison::NotEqual => lhs_val.ne(op_span, &rhs_val, span)?,
Comparison::LessThan => lhs_val.lt(op_span, &rhs_val, span)?,
Comparison::GreaterThan => lhs_val.gt(op_span, &rhs_val, span)?,
Comparison::LessThanOrEqual => lhs_val.lte(op_span, &rhs_val, span)?,
Comparison::GreaterThanOrEqual => lhs_val.gte(op_span, &rhs_val, span)?,
Comparison::RegexMatch => {
lhs_val.regex_match(ctx.engine_state, op_span, &rhs_val, false, span)?
}
Comparison::NotRegexMatch => {
lhs_val.regex_match(ctx.engine_state, op_span, &rhs_val, true, span)?
}
Comparison::In => lhs_val.r#in(op_span, &rhs_val, span)?,
Comparison::NotIn => lhs_val.not_in(op_span, &rhs_val, span)?,
Comparison::StartsWith => lhs_val.starts_with(op_span, &rhs_val, span)?,
Comparison::EndsWith => lhs_val.ends_with(op_span, &rhs_val, span)?,
},
Operator::Math(mat) => match mat {
Math::Plus => lhs_val.add(op_span, &rhs_val, span)?,
Math::Append => lhs_val.append(op_span, &rhs_val, span)?,
Math::Minus => lhs_val.sub(op_span, &rhs_val, span)?,
Math::Multiply => lhs_val.mul(op_span, &rhs_val, span)?,
Math::Divide => lhs_val.div(op_span, &rhs_val, span)?,
Math::Modulo => lhs_val.modulo(op_span, &rhs_val, span)?,
Math::FloorDivision => lhs_val.floor_div(op_span, &rhs_val, span)?,
Math::Pow => lhs_val.pow(op_span, &rhs_val, span)?,
},
Operator::Boolean(bl) => match bl {
Boolean::And => lhs_val.and(op_span, &rhs_val, span)?,
Boolean::Or => lhs_val.or(op_span, &rhs_val, span)?,
Boolean::Xor => lhs_val.xor(op_span, &rhs_val, span)?,
},
Operator::Bits(bit) => match bit {
Bits::BitOr => lhs_val.bit_or(op_span, &rhs_val, span)?,
Bits::BitXor => lhs_val.bit_xor(op_span, &rhs_val, span)?,
Bits::BitAnd => lhs_val.bit_and(op_span, &rhs_val, span)?,
Bits::ShiftLeft => lhs_val.bit_shl(op_span, &rhs_val, span)?,
Bits::ShiftRight => lhs_val.bit_shr(op_span, &rhs_val, span)?,
},
Operator::Assignment(_asg) => {
return Err(ShellError::IrEvalError {
msg: "can't eval assignment with the `binary-op` instruction".into(),
span: Some(span),
})
}
};
ctx.put_reg(lhs_dst, PipelineData::Value(result, None));
Ok(InstructionResult::Continue)
}
/// Evaluate a call
fn eval_call<D: DebugContext>(
ctx: &mut EvalContext<'_>,
decl_id: DeclId,
head: Span,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let EvalContext {
engine_state,
stack: caller_stack,
args_base,
redirect_out,
redirect_err,
..
} = ctx;
let args_len = caller_stack.arguments.get_len(*args_base);
let decl = engine_state.get_decl(decl_id);
// Set up redirect modes
let mut caller_stack = caller_stack.push_redirection(redirect_out.take(), redirect_err.take());
let result;
if let Some(block_id) = decl.block_id() {
// If the decl is a custom command
let block = engine_state.get_block(block_id);
// Set up a callee stack with the captures and move arguments from the stack into variables
let mut callee_stack = caller_stack.gather_captures(engine_state, &block.captures);
gather_arguments(
engine_state,
block,
&mut caller_stack,
&mut callee_stack,
*args_base,
args_len,
head,
)?;
// Add one to the recursion count, so we don't recurse too deep. Stack overflows are not
// recoverable in Rust.
callee_stack.recursion_count += 1;
result = eval_block_with_early_return::<D>(engine_state, &mut callee_stack, block, input);
// Move environment variables back into the caller stack scope if requested to do so
if block.redirect_env {
redirect_env(engine_state, &mut caller_stack, &callee_stack);
}
} else {
// FIXME: precalculate this and save it somewhere
let span = Span::merge_many(
std::iter::once(head).chain(
caller_stack
.arguments
.get_args(*args_base, args_len)
.iter()
.flat_map(|arg| arg.span()),
),
);
let call = Call {
decl_id,
head,
span,
args_base: *args_base,
args_len,
};
// Run the call
result = decl.run(engine_state, &mut caller_stack, &(&call).into(), input);
};
drop(caller_stack);
// Important that this runs, to reset state post-call:
ctx.stack.arguments.leave_frame(ctx.args_base);
ctx.redirect_out = None;
ctx.redirect_err = None;
result
}
fn find_named_var_id(
sig: &Signature,
name: &[u8],
short: &[u8],
span: Span,
) -> Result<VarId, ShellError> {
sig.named
.iter()
.find(|n| {
if !n.long.is_empty() {
n.long.as_bytes() == name
} else {
// It's possible to only have a short name and no long name
n.short
.is_some_and(|s| s.encode_utf8(&mut [0; 4]).as_bytes() == short)
}
})
.ok_or_else(|| ShellError::IrEvalError {
msg: format!(
"block does not have an argument named `{}`",
String::from_utf8_lossy(name)
),
span: Some(span),
})
.and_then(|flag| expect_named_var_id(flag, span))
}
fn expect_named_var_id(arg: &Flag, span: Span) -> Result<VarId, ShellError> {
arg.var_id.ok_or_else(|| ShellError::IrEvalError {
msg: format!(
"block signature is missing var id for named arg `{}`",
arg.long
),
span: Some(span),
})
}
fn expect_positional_var_id(arg: &PositionalArg, span: Span) -> Result<VarId, ShellError> {
arg.var_id.ok_or_else(|| ShellError::IrEvalError {
msg: format!(
"block signature is missing var id for positional arg `{}`",
arg.name
),
span: Some(span),
})
}
/// Move arguments from the stack into variables for a custom command
fn gather_arguments(
engine_state: &EngineState,
block: &Block,
caller_stack: &mut Stack,
callee_stack: &mut Stack,
args_base: usize,
args_len: usize,
call_head: Span,
) -> Result<(), ShellError> {
let mut positional_iter = block
.signature
.required_positional
.iter()
.map(|p| (p, true))
.chain(
block
.signature
.optional_positional
.iter()
.map(|p| (p, false)),
);
// Arguments that didn't get consumed by required/optional
let mut rest = vec![];
// If we encounter a spread, all further positionals should go to rest
let mut always_spread = false;
for arg in caller_stack.arguments.drain_args(args_base, args_len) {
match arg {
Argument::Positional { span, val, .. } => {
// Don't check next positional arg if we encountered a spread previously
let next = (!always_spread).then(|| positional_iter.next()).flatten();
if let Some((positional_arg, required)) = next {
let var_id = expect_positional_var_id(positional_arg, span)?;
if required {
// By checking the type of the bound variable rather than converting the
// SyntaxShape here, we might be able to save some allocations and effort
let variable = engine_state.get_var(var_id);
check_type(&val, &variable.ty)?;
}
callee_stack.add_var(var_id, val);
} else {
rest.push(val);
}
}
Argument::Spread { vals, .. } => {
if let Value::List { vals, .. } = vals {
rest.extend(vals);
// All further positional args should go to spread
always_spread = true;
} else if let Value::Error { error, .. } = vals {
return Err(*error);
} else {
return Err(ShellError::CannotSpreadAsList { span: vals.span() });
}
}
Argument::Flag {
data,
name,
short,
span,
} => {
let var_id = find_named_var_id(&block.signature, &data[name], &data[short], span)?;
callee_stack.add_var(var_id, Value::bool(true, span))
}
Argument::Named {
data,
name,
short,
span,
val,
..
} => {
let var_id = find_named_var_id(&block.signature, &data[name], &data[short], span)?;
callee_stack.add_var(var_id, val)
}
Argument::ParserInfo { .. } => (),
}
}
// Add the collected rest of the arguments if a spread argument exists
if let Some(rest_arg) = &block.signature.rest_positional {
let rest_span = rest.first().map(|v| v.span()).unwrap_or(call_head);
let var_id = expect_positional_var_id(rest_arg, rest_span)?;
callee_stack.add_var(var_id, Value::list(rest, rest_span));
}
// Check for arguments that haven't yet been set and set them to their defaults
for (positional_arg, _) in positional_iter {
let var_id = expect_positional_var_id(positional_arg, call_head)?;
callee_stack.add_var(
var_id,
positional_arg
.default_value
.clone()
.unwrap_or(Value::nothing(call_head)),
);
}
for named_arg in &block.signature.named {
if let Some(var_id) = named_arg.var_id {
// For named arguments, we do this check by looking to see if the variable was set yet on
// the stack. This assumes that the stack's variables was previously empty, but that's a
// fair assumption for a brand new callee stack.
if !callee_stack.vars.iter().any(|(id, _)| *id == var_id) {
let val = if named_arg.arg.is_none() {
Value::bool(false, call_head)
} else if let Some(value) = &named_arg.default_value {
value.clone()
} else {
Value::nothing(call_head)
};
callee_stack.add_var(var_id, val);
}
}
}
Ok(())
}
/// Type check helper. Produces `CantConvert` error if `val` is not compatible with `ty`.
fn check_type(val: &Value, ty: &Type) -> Result<(), ShellError> {
if match val {
// An empty list is compatible with any list or table type
Value::List { vals, .. } if vals.is_empty() => {
matches!(ty, Type::Any | Type::List(_) | Type::Table(_))
}
// FIXME: the allocation that might be required here is not great, it would be nice to be
// able to just directly check whether a value is compatible with a type
_ => val.get_type().is_subtype(ty),
} {
Ok(())
} else {
Err(ShellError::CantConvert {
to_type: ty.to_string(),
from_type: val.get_type().to_string(),
span: val.span(),
help: None,
})
}
}
/// Get variable from [`Stack`] or [`EngineState`]
fn get_var(ctx: &EvalContext<'_>, var_id: VarId, span: Span) -> Result<Value, ShellError> {
match var_id {
// $env
ENV_VARIABLE_ID => {
let env_vars = ctx.stack.get_env_vars(ctx.engine_state);
let env_columns = env_vars.keys();
let env_values = env_vars.values();
let mut pairs = env_columns
.map(|x| x.to_string())
.zip(env_values.cloned())
.collect::<Vec<(String, Value)>>();
pairs.sort_by(|a, b| a.0.cmp(&b.0));
Ok(Value::record(pairs.into_iter().collect(), span))
}
_ => ctx.stack.get_var(var_id, span).or_else(|err| {
// $nu is handled by getting constant
if let Some(const_val) = ctx.engine_state.get_constant(var_id).cloned() {
Ok(const_val.with_span(span))
} else {
Err(err)
}
}),
}
}
/// Get an environment variable, case-insensitively
fn get_env_var_case_insensitive<'a>(ctx: &'a mut EvalContext<'_>, key: &str) -> Option<&'a Value> {
// Read scopes in order
ctx.stack
.env_vars
.iter()
.rev()
.chain(std::iter::once(ctx.engine_state.env_vars.as_ref()))
.flat_map(|overlays| {
// Read overlays in order
ctx.stack
.active_overlays
.iter()
.rev()
.filter_map(|name| overlays.get(name))
})
.find_map(|map| {
// Use the hashmap first to try to be faster?
map.get(key).or_else(|| {
// Check to see if it exists at all in the map
map.iter()
.find_map(|(k, v)| k.eq_ignore_case(key).then_some(v))
})
})
}
/// Get the existing name of an environment variable, case-insensitively. This is used to implement
/// case preservation of environment variables, so that changing an environment variable that
/// already exists always uses the same case.
fn get_env_var_name_case_insensitive<'a>(ctx: &mut EvalContext<'_>, key: &'a str) -> Cow<'a, str> {
// Read scopes in order
ctx.stack
.env_vars
.iter()
.rev()
.chain(std::iter::once(ctx.engine_state.env_vars.as_ref()))
.flat_map(|overlays| {
// Read overlays in order
ctx.stack
.active_overlays
.iter()
.rev()
.filter_map(|name| overlays.get(name))
})
.find_map(|map| {
// Use the hashmap first to try to be faster?
if map.contains_key(key) {
Some(Cow::Borrowed(key))
} else {
map.keys().find(|k| k.eq_ignore_case(key)).map(|k| {
// it exists, but with a different case
Cow::Owned(k.to_owned())
})
}
})
// didn't exist.
.unwrap_or(Cow::Borrowed(key))
}
/// Helper to collect values into [`PipelineData`], preserving original span and metadata
fn collect(data: PipelineData, fallback_span: Span) -> Result<PipelineData, ShellError> {
let span = data.span().unwrap_or(fallback_span);
let metadata = data.metadata();
let value = data.into_value(span)?;
Ok(PipelineData::Value(value, metadata))
}
/// Helper for drain behavior. Returns `Ok(ExitCode)` on failed external.
fn drain(ctx: &mut EvalContext<'_>, data: PipelineData) -> Result<InstructionResult, ShellError> {
use self::InstructionResult::*;
let span = data.span().unwrap_or(Span::unknown());
if let Some(exit_status) = data.drain()? {
ctx.stack.add_env_var(
"LAST_EXIT_CODE".into(),
Value::int(exit_status.code() as i64, span),
);
if exit_status.code() == 0 {
Ok(Continue)
} else {
Ok(ExitCode(exit_status.code()))
}
} else {
Ok(Continue)
}
}
enum RedirectionStream {
Out,
Err,
}
/// Open a file for redirection
fn open_file(ctx: &EvalContext<'_>, path: &Value, append: bool) -> Result<Arc<File>, ShellError> {
let path_expanded =
expand_path_with(path.as_str()?, ctx.engine_state.cwd(Some(ctx.stack))?, true);
let mut options = File::options();
if append {
options.append(true);
} else {
options.write(true).truncate(true);
}
let file = options
.create(true)
.open(path_expanded)
.err_span(path.span())?;
Ok(Arc::new(file))
}
/// Set up a [`Redirection`] from a [`RedirectMode`]
fn eval_redirection(
ctx: &mut EvalContext<'_>,
mode: &RedirectMode,
span: Span,
which: RedirectionStream,
) -> Result<Option<Redirection>, ShellError> {
match mode {
RedirectMode::Pipe => Ok(Some(Redirection::Pipe(OutDest::Pipe))),
RedirectMode::Capture => Ok(Some(Redirection::Pipe(OutDest::Capture))),
RedirectMode::Null => Ok(Some(Redirection::Pipe(OutDest::Null))),
RedirectMode::Inherit => Ok(Some(Redirection::Pipe(OutDest::Inherit))),
RedirectMode::File { file_num } => {
let file = ctx
.files
.get(*file_num as usize)
.cloned()
.flatten()
.ok_or_else(|| ShellError::IrEvalError {
msg: format!("Tried to redirect to file #{file_num}, but it is not open"),
span: Some(span),
})?;
Ok(Some(Redirection::File(file)))
}
RedirectMode::Caller => Ok(match which {
RedirectionStream::Out => ctx.stack.pipe_stdout().cloned().map(Redirection::Pipe),
RedirectionStream::Err => ctx.stack.pipe_stderr().cloned().map(Redirection::Pipe),
}),
}
}
/// Do an `iterate` instruction. This can be called repeatedly to get more values from an iterable
fn eval_iterate(
ctx: &mut EvalContext<'_>,
dst: RegId,
stream: RegId,
end_index: usize,
) -> Result<InstructionResult, ShellError> {
let mut data = ctx.take_reg(stream);
if let PipelineData::ListStream(list_stream, _) = &mut data {
// Modify the stream, taking one value off, and branching if it's empty
if let Some(val) = list_stream.next_value() {
ctx.put_reg(dst, val.into_pipeline_data());
ctx.put_reg(stream, data); // put the stream back so it can be iterated on again
Ok(InstructionResult::Continue)
} else {
ctx.put_reg(dst, PipelineData::Empty);
Ok(InstructionResult::Branch(end_index))
}
} else {
// Convert the PipelineData to an iterator, and wrap it in a ListStream so it can be
// iterated on
let metadata = data.metadata();
let span = data.span().unwrap_or(Span::unknown());
ctx.put_reg(
stream,
PipelineData::ListStream(
ListStream::new(data.into_iter(), span, Signals::EMPTY),
metadata,
),
);
eval_iterate(ctx, dst, stream, end_index)
}
}
/// Redirect environment from the callee stack to the caller stack
fn redirect_env(engine_state: &EngineState, caller_stack: &mut Stack, callee_stack: &Stack) {
// TODO: make this more efficient
// Grab all environment variables from the callee
let caller_env_vars = caller_stack.get_env_var_names(engine_state);
// remove env vars that are present in the caller but not in the callee
// (the callee hid them)
for var in caller_env_vars.iter() {
if !callee_stack.has_env_var(engine_state, var) {
caller_stack.remove_env_var(engine_state, var);
}
}
// add new env vars from callee to caller
for (var, value) in callee_stack.get_stack_env_vars() {
caller_stack.add_env_var(var, value);
}
}