Skip to main content

nu_engine/
eval_ir.rs

1use std::{borrow::Cow, fs::File, sync::Arc};
2
3use nu_path::{dots::expand_ndots_safe, expand_path, expand_path_with, expand_tilde};
4#[cfg(feature = "os")]
5use nu_protocol::process::check_exit_status_future;
6use nu_protocol::{
7    CompareTypes, DeclId, ENV_VARIABLE_ID, Flag, IntoPipelineData, IntoSpanned, LabeledError,
8    ListStream, OutDest, PipelineData, PipelineExecutionData, PositionalArg, Range, Record, RegId,
9    ShellError, Signals, Signature, Span, Spanned, Type, Value, VarId,
10    ast::{Bits, Block, Boolean, CellPath, Comparison, Math, Operator},
11    combined_type_string,
12    debugger::DebugContext,
13    engine::{
14        Argument, Closure, EngineState, EnvName, ErrorHandler, Matcher, Redirection, Stack,
15        StateWorkingSet,
16    },
17    ir::{Call, DataSlice, Instruction, IrAstRef, IrBlock, Literal, RedirectMode},
18    shell_error::{generic::GenericError, io::IoError},
19};
20use nu_utils::IgnoreCaseExt;
21
22use crate::{
23    ENV_CONVERSIONS, convert_env_vars, eval::is_automatic_env_var, eval_block_with_early_return,
24};
25
26/// For `def --wrapped` and `known extern` rest params (`SyntaxShape::ExternalArgument`), convert
27/// non-glob `Value::Glob` values to `Value::String`, expanding tilde and ndots in the process.
28/// This mirrors what `run-external` does in `eval_external_arguments`, so that `$args | to nuon`
29/// returns expanded paths instead of the raw `~` / `...` tokens, while also ensuring that plain
30/// bare-word strings (e.g. `test`) are reported as strings rather than globs.
31fn expand_external_glob_arg(val: Value) -> Value {
32    if let Value::Glob {
33        val: ref s,
34        no_expand,
35        internal_span,
36        ..
37    } = val
38        && !no_expand
39        && !nu_glob::is_glob(s)
40    {
41        let expanded = expand_ndots_safe(expand_tilde(s.as_str()));
42        return Value::string(expanded.to_string_lossy().into_owned(), internal_span);
43    }
44    val
45}
46
47pub fn eval_ir_block<D: DebugContext>(
48    engine_state: &EngineState,
49    stack: &mut Stack,
50    block: &Block,
51    input: PipelineData,
52) -> Result<PipelineExecutionData, ShellError> {
53    // Rust does not check recursion limits outside of const evaluation.
54    // But nu programs run in the same process as the shell.
55    // To prevent a stack overflow in user code from crashing the shell,
56    // we limit the recursion depth of function calls.
57    let maximum_call_stack_depth: u64 = engine_state.config.recursion_limit as u64;
58    if stack.recursion_count > maximum_call_stack_depth {
59        return Err(ShellError::RecursionLimitReached {
60            recursion_limit: maximum_call_stack_depth,
61            span: block.span,
62        });
63    }
64
65    // Whole-block locals (closures / custom commands / top-level script).
66    let pushed_scope = if let Some(bindings) = &block.scope_bindings {
67        stack.push_scope_bindings(bindings.clone());
68        true
69    } else {
70        false
71    };
72
73    // Install this IR block's inlined-scope regions; restore any outer IR state on leave
74    // so nested `eval_ir_block` (e.g. custom command call) does not clobber the caller.
75    let saved_regions = std::mem::take(&mut stack.ir_scope_regions);
76    let saved_pc = stack.ir_instruction_index.take();
77
78    let result = eval_ir_block_inner::<D>(engine_state, stack, block, input);
79
80    stack.ir_scope_regions = saved_regions;
81    stack.ir_instruction_index = saved_pc;
82    if pushed_scope {
83        stack.pop_scope_bindings();
84    }
85    result
86}
87
88fn eval_ir_block_inner<D: DebugContext>(
89    engine_state: &EngineState,
90    stack: &mut Stack,
91    block: &Block,
92    input: PipelineData,
93) -> Result<PipelineExecutionData, ShellError> {
94    if let Some(ir_block) = &block.ir_block {
95        D::enter_block(engine_state, block);
96
97        stack.ir_scope_regions = ir_block.scope_regions.clone();
98        stack.ir_instruction_index = None;
99
100        let args_base = stack.arguments.get_base();
101        let error_handler_base = stack.error_handlers.get_base();
102        let finally_handler_base = stack.finally_run_handlers.get_base();
103
104        // Allocate and initialize registers. I've found that it's not really worth trying to avoid
105        // the heap allocation here by reusing buffers - our allocator is fast enough
106        let mut registers = Vec::with_capacity(ir_block.register_count as usize);
107        for _ in 0..ir_block.register_count {
108            registers.push(PipelineExecutionData::empty());
109        }
110
111        // Initialize file storage.
112        let mut files = vec![None; ir_block.file_count as usize];
113
114        let result = eval_ir_block_impl::<D>(
115            &mut EvalContext {
116                engine_state,
117                stack,
118                data: &ir_block.data,
119                block_span: &block.span,
120                args_base,
121                error_handler_base,
122                finally_handler_base,
123                redirect_out: None,
124                redirect_err: None,
125                matches: vec![],
126                registers: &mut registers[..],
127                files: &mut files[..],
128            },
129            ir_block,
130            input,
131        );
132
133        stack.error_handlers.leave_frame(error_handler_base);
134        stack.finally_run_handlers.leave_frame(finally_handler_base);
135        stack.arguments.leave_frame(args_base);
136        stack.ir_instruction_index = None;
137
138        D::leave_block(engine_state, block);
139
140        result
141    } else {
142        // FIXME blocks having IR should not be optional
143        let error = if let Some(span) = block.span {
144            ShellError::Generic(
145                GenericError::new(
146                    "Can't evaluate block in IR mode",
147                    "block is missing compiled representation",
148                    span,
149                )
150                .with_help("the IrBlock is probably missing due to a compilation error"),
151            )
152        } else {
153            ShellError::Generic(
154                GenericError::new_internal(
155                    "Can't evaluate block in IR mode",
156                    "block is missing compiled representation",
157                )
158                .with_help("the IrBlock is probably missing due to a compilation error"),
159            )
160        };
161        Err(error)
162    }
163}
164
165/// All of the pointers necessary for evaluation
166struct EvalContext<'a> {
167    engine_state: &'a EngineState,
168    stack: &'a mut Stack,
169    data: &'a Arc<[u8]>,
170    /// The span of the block
171    block_span: &'a Option<Span>,
172    /// Base index on the argument stack to reset to after a call
173    args_base: usize,
174    /// Base index on the error handler stack to reset to after a call
175    error_handler_base: usize,
176    /// Base index on the finally handler stack to reset to after a call
177    finally_handler_base: usize,
178    /// State set by redirect-out
179    redirect_out: Option<Redirection>,
180    /// State set by redirect-err
181    redirect_err: Option<Redirection>,
182    /// Scratch space to use for `match`
183    matches: Vec<(VarId, Value)>,
184    /// Intermediate pipeline data storage used by instructions, indexed by RegId
185    registers: &'a mut [PipelineExecutionData],
186    /// Holds open files used by redirections
187    files: &'a mut [Option<Arc<File>>],
188}
189
190impl<'a> EvalContext<'a> {
191    /// Replace the contents of a register with a new value
192    #[inline]
193    fn put_reg(&mut self, reg_id: RegId, new_value: PipelineExecutionData) {
194        // log::trace!("{reg_id} <- {new_value:?}");
195        self.registers[reg_id.get() as usize] = new_value;
196    }
197
198    /// Borrow the contents of a register.
199    #[inline]
200    fn borrow_reg(&self, reg_id: RegId) -> &PipelineData {
201        &self.registers[reg_id.get() as usize]
202    }
203
204    /// Replace the contents of a register with `Empty` and then return the value that it contained
205    #[inline]
206    fn take_reg(&mut self, reg_id: RegId) -> PipelineExecutionData {
207        // log::trace!("<- {reg_id}");
208        std::mem::replace(
209            &mut self.registers[reg_id.get() as usize],
210            PipelineExecutionData::empty(),
211        )
212    }
213
214    /// Clone data from a register. Must be collected first.
215    fn clone_reg(&mut self, reg_id: RegId, error_span: Span) -> Result<PipelineData, ShellError> {
216        // NOTE: here just clone the inner PipelineData
217        // it's suitable for current usage.
218        match &self.registers[reg_id.get() as usize].body {
219            PipelineData::Empty => Ok(PipelineData::empty()),
220            PipelineData::Value(val, meta) => Ok(PipelineData::value(val.clone(), meta.clone())),
221            _ => Err(ShellError::IrEvalError {
222                msg: "Must collect to value before using instruction that clones from a register"
223                    .into(),
224                span: Some(error_span),
225            }),
226        }
227    }
228
229    /// Clone a value from a register. Must be collected first.
230    fn clone_reg_value(&mut self, reg_id: RegId, fallback_span: Span) -> Result<Value, ShellError> {
231        match self.clone_reg(reg_id, fallback_span)? {
232            PipelineData::Empty => Ok(Value::nothing(fallback_span)),
233            PipelineData::Value(val, _) => Ok(val),
234            _ => unreachable!("clone_reg should never return stream data"),
235        }
236    }
237
238    /// Take and implicitly collect a register to a value
239    ///
240    /// It doesn't check exit status when collecting.
241    fn collect_reg(&mut self, reg_id: RegId, fallback_span: Span) -> Result<Value, ShellError> {
242        // NOTE: collect_reg is used to collect the reg to a variable.
243        // So it's good to pick the inner PipelineData directly, and drop the ExitStatus queue.
244        #[cfg(feature = "os")]
245        let body = {
246            let mut data = self.take_reg(reg_id);
247            data.exit.clear();
248            data.body
249        };
250        #[cfg(not(feature = "os"))]
251        let body = self.take_reg(reg_id).body;
252        let span = body.span().unwrap_or(fallback_span);
253        body.into_value(span)
254    }
255
256    /// Get a string from data or produce evaluation error if it's invalid UTF-8
257    fn get_str(&self, slice: DataSlice, error_span: Span) -> Result<&'a str, ShellError> {
258        std::str::from_utf8(&self.data[slice]).map_err(|_| ShellError::IrEvalError {
259            msg: format!("data slice does not refer to valid UTF-8: {slice:?}"),
260            span: Some(error_span),
261        })
262    }
263}
264
265/// Eval an IR block on the provided slice of registers.
266fn eval_ir_block_impl<D: DebugContext>(
267    ctx: &mut EvalContext<'_>,
268    ir_block: &IrBlock,
269    input: PipelineData,
270) -> Result<PipelineExecutionData, ShellError> {
271    if !ctx.registers.is_empty() {
272        ctx.registers[0] = PipelineExecutionData::from(input);
273    }
274
275    // Program counter, starts at zero.
276    let mut pc = 0;
277    let need_backtrace = ctx.engine_state.get_env_var("NU_BACKTRACE").is_some();
278    // The result of an early exit (`return` or an error) that must still run pending `finally`
279    // handlers before it can leave the block. It takes precedence over the register contents at
280    // the terminal `Return` instruction.
281    let mut ret_val: Option<Result<PipelineExecutionData, ShellError>> = None;
282
283    while pc < ir_block.instructions.len() {
284        let instruction = &ir_block.instructions[pc];
285        let span = &ir_block.spans[pc];
286        let ast = &ir_block.ast[pc];
287
288        // So `scope` can match inlined keyword-body bindings via ScopeRegion.
289        ctx.stack.ir_instruction_index = Some(pc);
290
291        D::enter_instruction(ctx.engine_state, ctx.stack, ir_block, pc, ctx.registers);
292
293        let result = eval_instruction::<D>(ctx, instruction, span, ast, need_backtrace);
294
295        D::leave_instruction(
296            ctx.engine_state,
297            ctx.stack,
298            ir_block,
299            pc,
300            ctx.registers,
301            result.as_ref().err(),
302        );
303
304        match result {
305            Ok(InstructionResult::Continue) => {
306                pc += 1;
307            }
308            Ok(InstructionResult::Branch(next_pc)) => {
309                pc = next_pc;
310            }
311            Ok(InstructionResult::Return(reg_id)) => {
312                // need to check if the return value was stashed by an early `return` or an error
313                // that ran a `finally` handler first. If so, we need to respect that value.
314                match ret_val {
315                    Some(res) => return res,
316                    None => return Ok(ctx.take_reg(reg_id)),
317                }
318            }
319            Ok(InstructionResult::ReturnEarly(reg_id)) => {
320                if let Some(always_run_handler) =
321                    ctx.stack.finally_run_handlers.pop(ctx.finally_handler_base)
322                {
323                    // A `finally` block is pending: collect the value first (mirroring the
324                    // `try-collect` the compiler emits on the fall-through path, which also
325                    // preserves metadata), stash it, and run the `finally` block. The stashed
326                    // value is returned at the terminal `Return` instruction.
327                    let data = ctx.take_reg(reg_id);
328                    #[cfg(feature = "os")]
329                    let collected = collect(data, *span, false);
330                    #[cfg(not(feature = "os"))]
331                    let collected = collect(data, *span);
332                    ret_val = Some(
333                        collected.map(|body| PipelineExecutionData::from(body).with_early_return()),
334                    );
335                    prepare_error_handler(ctx, always_run_handler, None);
336                    pc = always_run_handler.handler_index;
337                } else {
338                    // No `finally` pending: this is the same as a tail return, keeping streams
339                    // and metadata intact, except the data is flagged as an early return. The
340                    // nearest custom command or closure call clears that flag; top-level file
341                    // evaluation reads it to skip `main`.
342                    return Ok(ctx.take_reg(reg_id).with_early_return());
343                }
344            }
345            Err(err @ (ShellError::Continue { .. } | ShellError::Break { .. })) => {
346                return Err(err);
347            }
348            Err(err @ ShellError::Exit { abort: false, .. }) => {
349                if let Some(always_run_handler) =
350                    ctx.stack.finally_run_handlers.pop(ctx.finally_handler_base)
351                {
352                    // need to run finally block before exiting.
353                    // and record the exit error firstly.
354                    prepare_error_handler(ctx, always_run_handler, None);
355                    pc = always_run_handler.handler_index;
356                    ret_val = Some(Err(err));
357                } else {
358                    // These block control related errors should be passed through
359                    return Err(err);
360                }
361            }
362            Err(err @ ShellError::Exit { abort: true, .. }) => {
363                return Err(err);
364            }
365            Err(err) => {
366                #[cfg(unix)]
367                let is_terminated_by_signal = matches!(&err, ShellError::TerminatedBySignal { .. });
368                #[cfg(not(unix))]
369                let is_terminated_by_signal = false;
370
371                let is_interrupted =
372                    matches!(err, ShellError::Interrupted { .. }) || is_terminated_by_signal;
373                if let Some(error_handler) = ctx.stack.error_handlers.pop(ctx.error_handler_base) {
374                    if is_interrupted {
375                        ctx.engine_state.signals().reset();
376                    }
377                    // If an error handler is set, branch there
378                    prepare_error_handler(ctx, error_handler, Some(err.into_spanned(*span)));
379                    pc = error_handler.handler_index;
380                } else if let Some(always_run_handler) =
381                    ctx.stack.finally_run_handlers.pop(ctx.finally_handler_base)
382                {
383                    if is_interrupted {
384                        ctx.engine_state.signals().reset();
385                    }
386                    prepare_error_handler(
387                        ctx,
388                        always_run_handler,
389                        Some(err.clone().into_spanned(*span)),
390                    );
391                    pc = always_run_handler.handler_index;
392                    ret_val = Some(Err(err));
393                } else if need_backtrace {
394                    let err = ShellError::into_chained(err, *span);
395                    return Err(err);
396                } else {
397                    return Err(err);
398                }
399            }
400        }
401    }
402
403    // Fell out of the loop, without encountering a Return.
404    Err(ShellError::IrEvalError {
405        msg: format!(
406            "Program counter out of range (pc={pc}, len={len})",
407            len = ir_block.instructions.len(),
408        ),
409        span: *ctx.block_span,
410    })
411}
412
413/// Prepare the context for an error handler
414fn prepare_error_handler(
415    ctx: &mut EvalContext<'_>,
416    error_handler: ErrorHandler,
417    error: Option<Spanned<ShellError>>,
418) {
419    if let Some(reg_id) = error_handler.error_register {
420        if let Some(error) = error {
421            // Stack state has to be updated for stuff like LAST_EXIT_CODE
422            ctx.stack.set_last_error(&error.item);
423            // Create the error value and put it in the register
424            ctx.put_reg(
425                reg_id,
426                PipelineExecutionData::from(
427                    error
428                        .item
429                        .into_full_value(
430                            &StateWorkingSet::new(ctx.engine_state),
431                            ctx.stack,
432                            error.span,
433                        )
434                        .into_pipeline_data(),
435                ),
436            );
437        } else {
438            // Set the register to empty
439            ctx.put_reg(reg_id, PipelineExecutionData::empty());
440        }
441    }
442}
443
444/// The result of performing an instruction. Describes what should happen next
445#[derive(Debug)]
446enum InstructionResult {
447    Continue,
448    Branch(usize),
449    Return(RegId),
450    /// Return from the block before reaching the end, carrying the full register contents.
451    ///
452    /// Unlike `Return`, this runs any pending `finally` handlers before the value leaves the
453    /// block, and flags the resulting data as an early return. The flag exists for one consumer:
454    /// top-level file evaluation, which reads it to skip `main`. Custom command calls and closure
455    /// invocations clear the flag instead, so a `return` in a nested call can't leak out and be
456    /// mistaken for a `return` at the current level.
457    ReturnEarly(RegId),
458}
459
460/// Perform an instruction
461fn eval_instruction<D: DebugContext>(
462    ctx: &mut EvalContext<'_>,
463    instruction: &Instruction,
464    span: &Span,
465    ast: &Option<IrAstRef>,
466    need_backtrace: bool,
467) -> Result<InstructionResult, ShellError> {
468    use self::InstructionResult::*;
469
470    // Check for interrupt if necessary
471    instruction.check_interrupt(ctx.engine_state, span)?;
472
473    // See the docs for `Instruction` for more information on what these instructions are supposed
474    // to do.
475    match instruction {
476        Instruction::Unreachable => Err(ShellError::IrEvalError {
477            msg: "Reached unreachable code".into(),
478            span: Some(*span),
479        }),
480        Instruction::LoadLiteral { dst, lit } => load_literal(ctx, *dst, lit, *span),
481        Instruction::LoadValue { dst, val } => {
482            ctx.put_reg(
483                *dst,
484                PipelineExecutionData::from(Value::clone(val).into_pipeline_data()),
485            );
486            Ok(Continue)
487        }
488        Instruction::Move { dst, src } => {
489            let val = ctx.take_reg(*src);
490            ctx.put_reg(*dst, val);
491            Ok(Continue)
492        }
493        Instruction::Clone { dst, src } => {
494            let data = ctx.clone_reg(*src, *span)?;
495            ctx.put_reg(*dst, PipelineExecutionData::from(data));
496            Ok(Continue)
497        }
498        Instruction::Collect { src_dst } => {
499            let data = ctx.take_reg(*src_dst);
500            #[cfg(feature = "os")]
501            let value = collect(data, *span, true)?;
502            #[cfg(not(feature = "os"))]
503            let value = collect(data, *span)?;
504            ctx.put_reg(*src_dst, PipelineExecutionData::from(value));
505            Ok(Continue)
506        }
507        Instruction::TryCollect { src_dst } => {
508            let data = ctx.take_reg(*src_dst);
509            #[cfg(feature = "os")]
510            let value = collect(data, *span, false)?;
511            #[cfg(not(feature = "os"))]
512            let value = collect(data, *span)?;
513            ctx.put_reg(*src_dst, PipelineExecutionData::from(value));
514            Ok(Continue)
515        }
516        Instruction::Span { src_dst } => {
517            let mut data = ctx.take_reg(*src_dst);
518            data.body = data.body.with_span(*span);
519            ctx.put_reg(*src_dst, data);
520            Ok(Continue)
521        }
522        Instruction::Drop { src } => {
523            ctx.take_reg(*src);
524            Ok(Continue)
525        }
526        Instruction::Drain { src } => {
527            let data = ctx.take_reg(*src);
528            drain(ctx, data)
529        }
530        Instruction::DrainIfEnd { src } => {
531            let data = ctx.take_reg(*src);
532            let res = drain_if_end(ctx, data)?;
533            ctx.put_reg(*src, PipelineExecutionData::from(res));
534            Ok(Continue)
535        }
536        Instruction::LoadVariable { dst, var_id } => {
537            // Restore pipeline metadata for `$ans` (e.g. ls path_columns / colors on `.last`).
538            // Truncation warning is deferred until after print so data is visible first.
539            let data = if *var_id == nu_protocol::LAST_VARIABLE_ID {
540                ctx.stack.defer_last_result_truncation_warning();
541                ctx.stack.last_result_pipeline_data(*span)
542            } else {
543                let value = get_var(ctx, *var_id, *span)?;
544                value.into_pipeline_data()
545            };
546            ctx.put_reg(*dst, PipelineExecutionData::from(data));
547            Ok(Continue)
548        }
549        Instruction::StoreVariable { var_id, src } => {
550            let value = ctx.collect_reg(*src, *span)?;
551            // Perform runtime type checking and conversion for variable assignment
552            if nu_experimental::ENFORCE_RUNTIME_ANNOTATIONS.get() {
553                let variable = ctx.engine_state.get_var(*var_id);
554                let converted_value = check_assignment_type(value, &variable.ty, *span)?;
555                ctx.stack.add_var(*var_id, converted_value);
556            } else {
557                ctx.stack.add_var(*var_id, value);
558            }
559            Ok(Continue)
560        }
561        Instruction::DropVariable { var_id } => {
562            ctx.stack.remove_var(*var_id);
563            Ok(Continue)
564        }
565        Instruction::LoadEnv { dst, key } => {
566            let key = ctx.get_str(*key, *span)?;
567            if let Some(value) = get_env_var(ctx, key) {
568                let new_value = value.clone().into_pipeline_data();
569                ctx.put_reg(*dst, PipelineExecutionData::from(new_value));
570                Ok(Continue)
571            } else {
572                // FIXME: using the same span twice, shouldn't this really be
573                // EnvVarNotFoundAtRuntime? There are tests that depend on CantFindColumn though...
574                Err(ShellError::CantFindColumn {
575                    col_name: key.into(),
576                    span: Some(*span),
577                    src_span: *span,
578                })
579            }
580        }
581        Instruction::LoadEnvOpt { dst, key } => {
582            let key = ctx.get_str(*key, *span)?;
583            let value = get_env_var(ctx, key)
584                .cloned()
585                .unwrap_or(Value::nothing(*span));
586            ctx.put_reg(
587                *dst,
588                PipelineExecutionData::from(value.into_pipeline_data()),
589            );
590            Ok(Continue)
591        }
592        Instruction::StoreEnv { key, src } => {
593            let key = ctx.get_str(*key, *span)?;
594            let value = ctx.collect_reg(*src, *span)?;
595
596            let key = get_env_var_name(ctx, key);
597
598            if !is_automatic_env_var(&key) {
599                let is_config = key == "config";
600                let update_conversions = key == ENV_CONVERSIONS;
601
602                ctx.stack.add_env_var(key.into_owned(), value.clone());
603
604                if is_config {
605                    ctx.stack.update_config(ctx.engine_state)?;
606                }
607                if update_conversions {
608                    convert_env_vars(ctx.stack, ctx.engine_state, &value)?;
609                }
610                Ok(Continue)
611            } else {
612                Err(ShellError::AutomaticEnvVarSetManually {
613                    envvar_name: key.into(),
614                    span: *span,
615                })
616            }
617        }
618        Instruction::PushPositional { src } => {
619            let val = ctx.collect_reg(*src, *span)?.with_span(*span);
620            ctx.stack.arguments.push(Argument::Positional {
621                span: *span,
622                val,
623                ast: ast.clone().map(|ast_ref| ast_ref.0),
624            });
625            Ok(Continue)
626        }
627        Instruction::AppendRest { src } => {
628            let vals = ctx.collect_reg(*src, *span)?.with_span(*span);
629            ctx.stack.arguments.push(Argument::Spread {
630                span: *span,
631                vals,
632                ast: ast.clone().map(|ast_ref| ast_ref.0),
633            });
634            Ok(Continue)
635        }
636        Instruction::PushFlag { name } => {
637            let data = ctx.data.clone();
638            ctx.stack.arguments.push(Argument::Flag {
639                data,
640                name: *name,
641                short: DataSlice::empty(),
642                span: *span,
643            });
644            Ok(Continue)
645        }
646        Instruction::PushShortFlag { short } => {
647            let data = ctx.data.clone();
648            ctx.stack.arguments.push(Argument::Flag {
649                data,
650                name: DataSlice::empty(),
651                short: *short,
652                span: *span,
653            });
654            Ok(Continue)
655        }
656        Instruction::PushNamed { name, src } => {
657            let val = ctx.collect_reg(*src, *span)?.with_span(*span);
658            let data = ctx.data.clone();
659            ctx.stack.arguments.push(Argument::Named {
660                data,
661                name: *name,
662                short: DataSlice::empty(),
663                span: *span,
664                val,
665                ast: ast.clone().map(|ast_ref| ast_ref.0),
666            });
667            Ok(Continue)
668        }
669        Instruction::PushShortNamed { short, src } => {
670            let val = ctx.collect_reg(*src, *span)?.with_span(*span);
671            let data = ctx.data.clone();
672            ctx.stack.arguments.push(Argument::Named {
673                data,
674                name: DataSlice::empty(),
675                short: *short,
676                span: *span,
677                val,
678                ast: ast.clone().map(|ast_ref| ast_ref.0),
679            });
680            Ok(Continue)
681        }
682        Instruction::PushParserInfo { name, info } => {
683            let data = ctx.data.clone();
684            ctx.stack.arguments.push(Argument::ParserInfo {
685                data,
686                name: *name,
687                info: info.clone(),
688            });
689            Ok(Continue)
690        }
691        Instruction::RedirectOut { mode } => {
692            ctx.redirect_out = eval_redirection(ctx, mode, *span, RedirectionStream::Out)?;
693            Ok(Continue)
694        }
695        Instruction::RedirectErr { mode } => {
696            ctx.redirect_err = eval_redirection(ctx, mode, *span, RedirectionStream::Err)?;
697            Ok(Continue)
698        }
699        Instruction::CheckErrRedirected { src } => match ctx.borrow_reg(*src) {
700            #[cfg(feature = "os")]
701            PipelineData::ByteStream(stream, _)
702                if matches!(stream.source(), nu_protocol::ByteStreamSource::Child(_)) =>
703            {
704                Ok(Continue)
705            }
706            _ => Err(ShellError::Generic(GenericError::new(
707                "Can't redirect stderr of internal command output",
708                "piping stderr only works on external commands",
709                *span,
710            ))),
711        },
712        Instruction::OpenFile {
713            file_num,
714            path,
715            append,
716        } => {
717            let path = ctx.collect_reg(*path, *span)?;
718            let file = open_file(ctx, &path, *append)?;
719            ctx.files[*file_num as usize] = Some(file);
720            Ok(Continue)
721        }
722        Instruction::WriteFile { file_num, src } => {
723            let src = ctx.take_reg(*src);
724            let file = ctx
725                .files
726                .get(*file_num as usize)
727                .cloned()
728                .flatten()
729                .ok_or_else(|| ShellError::IrEvalError {
730                    msg: format!("Tried to write to file #{file_num}, but it is not open"),
731                    span: Some(*span),
732                })?;
733            let is_external = if let PipelineData::ByteStream(stream, ..) = &src.body {
734                stream.source().is_external()
735            } else {
736                false
737            };
738            if let Err(err) = src.body.write_to(file.as_ref()) {
739                if is_external {
740                    ctx.stack.set_last_error(&err);
741                }
742                Err(err)?
743            } else {
744                Ok(Continue)
745            }
746        }
747        Instruction::CloseFile { file_num } => {
748            if ctx.files[*file_num as usize].take().is_some() {
749                Ok(Continue)
750            } else {
751                Err(ShellError::IrEvalError {
752                    msg: format!("Tried to close file #{file_num}, but it is not open"),
753                    span: Some(*span),
754                })
755            }
756        }
757        Instruction::Call { decl_id, src_dst } => {
758            let input = ctx.take_reg(*src_dst);
759            // take out exit status future first.
760            let input_data = input.body;
761            let mut result = eval_call::<D>(ctx, *decl_id, *span, input_data)?;
762            if need_backtrace {
763                match &mut result {
764                    PipelineData::ByteStream(s, ..) => s.push_caller_span(*span),
765                    PipelineData::ListStream(s, ..) => s.push_caller_span(*span),
766                    _ => (),
767                };
768            }
769            // After eval_call, attach result's exit_status_future
770            // to `original_exit`, so all exit_status_future are tracked
771            // in the new PipelineData, and wrap it into `PipelineExecutionData`
772            #[cfg(feature = "os")]
773            {
774                let mut original_exit = input.exit;
775                // `complete` converts external process status into data (`exit_code`).
776                // Drop inherited exit futures so downstream collect/print/assignment
777                // does not re-raise the same non-zero status via `pipefail`.
778                if ctx.engine_state.get_decl(*decl_id).name() == "complete" {
779                    original_exit.clear();
780                }
781                let result_exit_status_future = result
782                    .clone_exit_status_future()
783                    .map(|f| f.with_span(*span));
784                original_exit.push(result_exit_status_future);
785                ctx.put_reg(
786                    *src_dst,
787                    PipelineExecutionData {
788                        body: result,
789                        exit: original_exit,
790                        early_return: false,
791                    },
792                );
793            }
794            #[cfg(not(feature = "os"))]
795            ctx.put_reg(
796                *src_dst,
797                PipelineExecutionData {
798                    body: result,
799                    early_return: false,
800                },
801            );
802            Ok(Continue)
803        }
804        Instruction::StringAppend { src_dst, val } => {
805            let string_value = ctx.collect_reg(*src_dst, *span)?;
806            let operand_value = ctx.collect_reg(*val, *span)?;
807            let string_span = string_value.span();
808
809            let mut string = string_value.into_string()?;
810            let operand = if let Value::String { val, .. } = operand_value {
811                // Small optimization, so we don't have to copy the string *again*
812                val
813            } else {
814                operand_value.to_expanded_string(", ", &ctx.stack.get_config(ctx.engine_state))
815            };
816            string.push_str(&operand);
817
818            let new_string_value = Value::string(string, string_span);
819            ctx.put_reg(
820                *src_dst,
821                PipelineExecutionData::from(new_string_value.into_pipeline_data()),
822            );
823            Ok(Continue)
824        }
825        Instruction::GlobFrom { src_dst, no_expand } => {
826            let string_value = ctx.collect_reg(*src_dst, *span)?;
827            let glob_value = if let Value::Glob { .. } = string_value {
828                // It already is a glob, so don't touch it.
829                string_value
830            } else {
831                // Treat it as a string, then cast
832                let string = string_value.into_string()?;
833                Value::glob(string, *no_expand, *span)
834            };
835            ctx.put_reg(
836                *src_dst,
837                PipelineExecutionData::from(glob_value.into_pipeline_data()),
838            );
839            Ok(Continue)
840        }
841        Instruction::ListPush { src_dst, item } => {
842            let list_value = ctx.collect_reg(*src_dst, *span)?;
843            let item = ctx.collect_reg(*item, *span)?;
844            let list_span = list_value.span();
845            let mut list = list_value.into_list()?;
846            list.push(item);
847            ctx.put_reg(
848                *src_dst,
849                PipelineExecutionData::from(Value::list(list, list_span).into_pipeline_data()),
850            );
851            Ok(Continue)
852        }
853        Instruction::ListSpread { src_dst, items } => {
854            let list_value = ctx.collect_reg(*src_dst, *span)?;
855            let items = ctx.collect_reg(*items, *span)?;
856            let list_span = list_value.span();
857            let items_span = items.span();
858            let items = match items {
859                Value::List { vals, .. } => vals.into_owned(),
860                Value::Nothing { .. } => Vec::new(),
861                _ => return Err(ShellError::CannotSpreadAsList { span: items_span }),
862            };
863            let mut list = list_value.into_list()?;
864            list.extend(items);
865            ctx.put_reg(
866                *src_dst,
867                PipelineExecutionData::from(Value::list(list, list_span).into_pipeline_data()),
868            );
869            Ok(Continue)
870        }
871        Instruction::RecordInsert { src_dst, key, val } => {
872            let record_value = ctx.collect_reg(*src_dst, *span)?;
873            let key = ctx.collect_reg(*key, *span)?;
874            let val = ctx.collect_reg(*val, *span)?;
875            let record_span = record_value.span();
876            let mut record = record_value.into_record()?;
877
878            let key = key.coerce_into_string()?;
879            if let Some(old_value) = record.insert(&key, val) {
880                return Err(ShellError::ColumnDefinedTwice {
881                    col_name: key,
882                    second_use: *span,
883                    first_use: old_value.span(),
884                });
885            }
886
887            ctx.put_reg(
888                *src_dst,
889                PipelineExecutionData::from(
890                    Value::record(record, record_span).into_pipeline_data(),
891                ),
892            );
893            Ok(Continue)
894        }
895        Instruction::RecordSpread { src_dst, items } => {
896            let record_value = ctx.collect_reg(*src_dst, *span)?;
897            let items = ctx.collect_reg(*items, *span)?;
898            let record_span = record_value.span();
899            let items_span = items.span();
900            let mut record = record_value.into_record()?;
901            let items = match items {
902                Value::Record { val, .. } => val.into_owned(),
903                Value::Nothing { .. } => Record::new(),
904                _ => return Err(ShellError::CannotSpreadAsRecord { span: items_span }),
905            };
906            // Not using .extend() here because it doesn't handle duplicates
907            for (key, val) in items {
908                if let Some(first_value) = record.insert(&key, val) {
909                    return Err(ShellError::ColumnDefinedTwice {
910                        col_name: key,
911                        second_use: *span,
912                        first_use: first_value.span(),
913                    });
914                }
915            }
916            ctx.put_reg(
917                *src_dst,
918                PipelineExecutionData::from(
919                    Value::record(record, record_span).into_pipeline_data(),
920                ),
921            );
922            Ok(Continue)
923        }
924        Instruction::Not { src_dst } => {
925            let bool = ctx.collect_reg(*src_dst, *span)?;
926            let negated = !bool.as_bool()?;
927            ctx.put_reg(
928                *src_dst,
929                PipelineExecutionData::from(Value::bool(negated, bool.span()).into_pipeline_data()),
930            );
931            Ok(Continue)
932        }
933        Instruction::BinaryOp { lhs_dst, op, rhs } => binary_op(ctx, *lhs_dst, op, *rhs, *span),
934        Instruction::FollowCellPath { src_dst, path } => {
935            let data = ctx.take_reg(*src_dst);
936            let path = ctx.take_reg(*path);
937            if let PipelineData::Value(Value::CellPath { val: path, .. }, _) = path.body {
938                // Reattach `$ans` pipeline metadata when following only `.last`, so
939                // `$ans.last` keeps ls path_columns / colors like the original payload.
940                // Only for pipeline data marked by `last_result_pipeline_data`, not every
941                // record field named `last`.
942                let from_ans = data.body.metadata_ref().is_some_and(|m| {
943                    m.custom
944                        .get(nu_protocol::engine::Stack::ANS_LAST_RESULT_METADATA_KEY)
945                        .is_some()
946                });
947                let is_ans_last = from_ans
948                    && path.members.len() == 1
949                    && matches!(
950                        &path.members[0],
951                        nu_protocol::ast::PathMember::String { val, .. } if val == "last"
952                    );
953                let src_meta = data.body.metadata_ref().cloned();
954                let value = data.body.follow_cell_path(&path.members, *span)?;
955                let metadata = if is_ans_last {
956                    // Drop the ans marker; keep path_columns / content_type for display.
957                    src_meta.map(|mut m| {
958                        m.custom
959                            .remove(nu_protocol::engine::Stack::ANS_LAST_RESULT_METADATA_KEY);
960                        m
961                    })
962                } else {
963                    None
964                };
965                ctx.put_reg(
966                    *src_dst,
967                    PipelineExecutionData::from(PipelineData::value(value, metadata)),
968                );
969                Ok(Continue)
970            } else if let PipelineData::Value(Value::Error { error, .. }, _) = path.body {
971                Err(*error)
972            } else {
973                Err(ShellError::TypeMismatch {
974                    err_message: "expected cell path".into(),
975                    span: path.span().unwrap_or(*span),
976                })
977            }
978        }
979        Instruction::CloneCellPath { dst, src, path } => {
980            let value = ctx.clone_reg_value(*src, *span)?;
981            let path = ctx.take_reg(*path);
982            if let PipelineData::Value(Value::CellPath { val: path, .. }, _) = path.body {
983                let value = value.follow_cell_path(&path.members)?;
984                ctx.put_reg(
985                    *dst,
986                    PipelineExecutionData::from(value.into_owned().into_pipeline_data()),
987                );
988                Ok(Continue)
989            } else if let PipelineData::Value(Value::Error { error, .. }, _) = path.body {
990                Err(*error)
991            } else {
992                Err(ShellError::TypeMismatch {
993                    err_message: "expected cell path".into(),
994                    span: path.span().unwrap_or(*span),
995                })
996            }
997        }
998        Instruction::UpsertCellPath {
999            src_dst,
1000            path,
1001            new_value,
1002        } => {
1003            let mut data = ctx.take_reg(*src_dst).body;
1004            let metadata = data.take_metadata();
1005            // Change the span because we're modifying it
1006            let mut value = data.into_value(*span)?;
1007            let path = ctx.take_reg(*path);
1008            let new_value = ctx.collect_reg(*new_value, *span)?;
1009            if let PipelineData::Value(Value::CellPath { val: path, .. }, _) = path.body {
1010                value.upsert_data_at_cell_path(&path.members, new_value)?;
1011                ctx.put_reg(
1012                    *src_dst,
1013                    PipelineExecutionData::from(value.into_pipeline_data_with_metadata(metadata)),
1014                );
1015                Ok(Continue)
1016            } else if let PipelineData::Value(Value::Error { error, .. }, _) = path.body {
1017                Err(*error)
1018            } else {
1019                Err(ShellError::TypeMismatch {
1020                    err_message: "expected cell path".into(),
1021                    span: path.span().unwrap_or(*span),
1022                })
1023            }
1024        }
1025        Instruction::UpdateVarCellPath {
1026            var_id,
1027            cell_path,
1028            new_value,
1029        } => {
1030            let new_val = ctx.collect_reg(*new_value, *span)?;
1031            let path = ctx.take_reg(*cell_path);
1032            if let PipelineData::Value(Value::CellPath { val: path, .. }, _) = path.body {
1033                let new_val = if nu_experimental::ENFORCE_RUNTIME_ANNOTATIONS.get() {
1034                    let variable = ctx.engine_state.get_var(*var_id);
1035                    let expected_ty = variable.ty.follow_cell_path(&path.members);
1036                    if let Some(expected_ty) = expected_ty {
1037                        check_assignment_type(new_val, &expected_ty, *span)?
1038                    } else {
1039                        new_val
1040                    }
1041                } else {
1042                    new_val
1043                };
1044                ctx.stack
1045                    .upsert_var_cell_path(*var_id, &path.members, new_val, *span)?;
1046                Ok(Continue)
1047            } else if let PipelineData::Value(Value::Error { error, .. }, _) = path.body {
1048                Err(*error)
1049            } else {
1050                Err(ShellError::TypeMismatch {
1051                    err_message: "expected cell path".into(),
1052                    span: path.span().unwrap_or(*span),
1053                })
1054            }
1055        }
1056        Instruction::Jump { index } => Ok(Branch(*index)),
1057        Instruction::BranchIf { cond, index } => {
1058            let data = ctx.take_reg(*cond);
1059            let data_span = data.span();
1060            let val = match data.body {
1061                PipelineData::Value(Value::Bool { val, .. }, _) => val,
1062                PipelineData::Value(Value::Error { error, .. }, _) => {
1063                    return Err(*error);
1064                }
1065                _ => {
1066                    return Err(ShellError::TypeMismatch {
1067                        err_message: "expected bool".into(),
1068                        span: data_span.unwrap_or(*span),
1069                    });
1070                }
1071            };
1072            if val {
1073                Ok(Branch(*index))
1074            } else {
1075                Ok(Continue)
1076            }
1077        }
1078        Instruction::BranchIfEmpty { src, index } => {
1079            let is_empty = matches!(
1080                ctx.borrow_reg(*src),
1081                PipelineData::Empty | PipelineData::Value(Value::Nothing { .. }, _)
1082            );
1083
1084            if is_empty {
1085                Ok(Branch(*index))
1086            } else {
1087                Ok(Continue)
1088            }
1089        }
1090        Instruction::Match {
1091            pattern,
1092            src,
1093            index,
1094        } => {
1095            let value = ctx.clone_reg_value(*src, *span)?;
1096            ctx.matches.clear();
1097            if pattern.match_value(&value, &mut ctx.matches) {
1098                // Match succeeded: set variables and branch
1099                for (var_id, match_value) in ctx.matches.drain(..) {
1100                    ctx.stack.add_var(var_id, match_value);
1101                }
1102                Ok(Branch(*index))
1103            } else {
1104                // Failed to match, put back original value
1105                ctx.matches.clear();
1106                Ok(Continue)
1107            }
1108        }
1109        Instruction::CheckMatchGuard { src } => {
1110            if matches!(
1111                ctx.borrow_reg(*src),
1112                PipelineData::Value(Value::Bool { .. }, _)
1113            ) {
1114                Ok(Continue)
1115            } else {
1116                Err(ShellError::MatchGuardNotBool { span: *span })
1117            }
1118        }
1119        Instruction::Iterate {
1120            dst,
1121            stream,
1122            end_index,
1123        } => eval_iterate(ctx, *dst, *stream, *end_index, *span),
1124        Instruction::OnError { index } => {
1125            ctx.stack.error_handlers.push(ErrorHandler {
1126                handler_index: *index,
1127                error_register: None,
1128            });
1129            Ok(Continue)
1130        }
1131        Instruction::OnErrorInto { index, dst } => {
1132            ctx.stack.error_handlers.push(ErrorHandler {
1133                handler_index: *index,
1134                error_register: Some(*dst),
1135            });
1136            Ok(Continue)
1137        }
1138        Instruction::Finally { index } => {
1139            ctx.stack.finally_run_handlers.push(ErrorHandler {
1140                handler_index: *index,
1141                error_register: None,
1142            });
1143            Ok(Continue)
1144        }
1145        Instruction::FinallyInto { index, dst } => {
1146            ctx.stack.finally_run_handlers.push(ErrorHandler {
1147                handler_index: *index,
1148                error_register: Some(*dst),
1149            });
1150            Ok(Continue)
1151        }
1152        Instruction::PopErrorHandler => {
1153            ctx.stack.error_handlers.pop(ctx.error_handler_base);
1154            Ok(Continue)
1155        }
1156        Instruction::PopFinallyRun => {
1157            ctx.stack.finally_run_handlers.pop(ctx.finally_handler_base);
1158            Ok(Continue)
1159        }
1160        Instruction::ReturnEarly { src } => Ok(InstructionResult::ReturnEarly(*src)),
1161        Instruction::Return { src } => Ok(Return(*src)),
1162    }
1163}
1164
1165/// Load a literal value into a register
1166fn load_literal(
1167    ctx: &mut EvalContext<'_>,
1168    dst: RegId,
1169    lit: &Literal,
1170    span: Span,
1171) -> Result<InstructionResult, ShellError> {
1172    // `Literal::Empty` represents "no pipeline input" and should produce
1173    // `PipelineData::Empty`. This is distinct from `Literal::Nothing` which
1174    // represents the `null` value and should produce `PipelineData::Value(Value::Nothing)`.
1175    // Some commands (like `metadata`) distinguish between these when deciding
1176    // whether positional args are allowed.
1177    if matches!(lit, Literal::Empty) {
1178        ctx.put_reg(dst, PipelineExecutionData::empty());
1179    } else {
1180        let value = literal_value(ctx, lit, span)?;
1181        ctx.put_reg(
1182            dst,
1183            PipelineExecutionData::from(PipelineData::value(value, None)),
1184        );
1185    }
1186    Ok(InstructionResult::Continue)
1187}
1188
1189fn literal_value(
1190    ctx: &mut EvalContext<'_>,
1191    lit: &Literal,
1192    span: Span,
1193) -> Result<Value, ShellError> {
1194    Ok(match lit {
1195        Literal::Bool(b) => Value::bool(*b, span),
1196        Literal::Int(i) => Value::int(*i, span),
1197        Literal::Float(f) => Value::float(*f, span),
1198        Literal::Filesize(q) => Value::filesize(*q, span),
1199        Literal::Duration(q) => Value::duration(*q, span),
1200        Literal::Binary(bin) => Value::binary(&ctx.data[*bin], span),
1201        Literal::Block(block_id) | Literal::RowCondition(block_id) | Literal::Closure(block_id) => {
1202            let block = ctx.engine_state.get_block(*block_id);
1203            let captures = block
1204                .captures
1205                .iter()
1206                .map(|(var_id, span)| get_var(ctx, *var_id, *span).map(|val| (*var_id, val)))
1207                .collect::<Result<Vec<_>, ShellError>>()?;
1208            Value::closure(
1209                Closure {
1210                    block_id: *block_id,
1211                    captures,
1212                },
1213                span,
1214            )
1215        }
1216        Literal::Range {
1217            start,
1218            step,
1219            end,
1220            inclusion,
1221        } => {
1222            let start = ctx.collect_reg(*start, span)?;
1223            let step = ctx.collect_reg(*step, span)?;
1224            let end = ctx.collect_reg(*end, span)?;
1225            let range = Range::new(start, step, end, *inclusion, span)?;
1226            Value::range(range, span)
1227        }
1228        Literal::List { capacity } => Value::list(Vec::with_capacity(*capacity), span),
1229        Literal::Record { capacity } => Value::record(Record::with_capacity(*capacity), span),
1230        Literal::Filepath {
1231            val: path,
1232            no_expand,
1233        } => {
1234            let path = ctx.get_str(*path, span)?;
1235            if *no_expand {
1236                Value::string(path, span)
1237            } else {
1238                let path = expand_path(path, true);
1239                Value::string(path.to_string_lossy(), span)
1240            }
1241        }
1242        Literal::Directory {
1243            val: path,
1244            no_expand,
1245        } => {
1246            let path = ctx.get_str(*path, span)?;
1247            if path == "-" {
1248                Value::string("-", span)
1249            } else if *no_expand {
1250                Value::string(path, span)
1251            } else {
1252                let path = expand_path(path, true);
1253                Value::string(path.to_string_lossy(), span)
1254            }
1255        }
1256        Literal::GlobPattern { val, no_expand } => {
1257            Value::glob(ctx.get_str(*val, span)?, *no_expand, span)
1258        }
1259        Literal::String(s) => Value::string(ctx.get_str(*s, span)?, span),
1260        Literal::RawString(s) => Value::string(ctx.get_str(*s, span)?, span),
1261        Literal::CellPath(path) => Value::cell_path(CellPath::clone(path), span),
1262        Literal::Date(dt) => Value::date(**dt, span),
1263        Literal::Nothing => Value::nothing(span),
1264        // Empty is handled specially in load_literal and should never reach here
1265        Literal::Empty => Value::nothing(span),
1266    })
1267}
1268
1269fn binary_op(
1270    ctx: &mut EvalContext<'_>,
1271    lhs_dst: RegId,
1272    op: &Operator,
1273    rhs: RegId,
1274    span: Span,
1275) -> Result<InstructionResult, ShellError> {
1276    let lhs_val = ctx.collect_reg(lhs_dst, span)?;
1277    let rhs_val = ctx.collect_reg(rhs, span)?;
1278
1279    // Handle binary op errors early
1280    if let Value::Error { error, .. } = lhs_val {
1281        return Err(*error);
1282    }
1283    if let Value::Error { error, .. } = rhs_val {
1284        return Err(*error);
1285    }
1286
1287    // We only have access to one span here, but the generated code usually adds a `span`
1288    // instruction to set the output span to the right span.
1289    let op_span = span;
1290
1291    let result = match op {
1292        Operator::Comparison(cmp) => match cmp {
1293            Comparison::Equal => lhs_val.eq(op_span, &rhs_val, span)?,
1294            Comparison::NotEqual => lhs_val.ne(op_span, &rhs_val, span)?,
1295            Comparison::LessThan => lhs_val.lt(op_span, &rhs_val, span)?,
1296            Comparison::GreaterThan => lhs_val.gt(op_span, &rhs_val, span)?,
1297            Comparison::LessThanOrEqual => lhs_val.lte(op_span, &rhs_val, span)?,
1298            Comparison::GreaterThanOrEqual => lhs_val.gte(op_span, &rhs_val, span)?,
1299            Comparison::RegexMatch => {
1300                lhs_val.regex_match(ctx.engine_state, op_span, &rhs_val, false, span)?
1301            }
1302            Comparison::NotRegexMatch => {
1303                lhs_val.regex_match(ctx.engine_state, op_span, &rhs_val, true, span)?
1304            }
1305            Comparison::In => lhs_val.r#in(op_span, &rhs_val, span)?,
1306            Comparison::NotIn => lhs_val.not_in(op_span, &rhs_val, span)?,
1307            Comparison::Has => lhs_val.has(op_span, &rhs_val, span)?,
1308            Comparison::NotHas => lhs_val.not_has(op_span, &rhs_val, span)?,
1309            Comparison::StartsWith => lhs_val.starts_with(op_span, &rhs_val, span)?,
1310            Comparison::NotStartsWith => lhs_val.not_starts_with(op_span, &rhs_val, span)?,
1311            Comparison::EndsWith => lhs_val.ends_with(op_span, &rhs_val, span)?,
1312            Comparison::NotEndsWith => lhs_val.not_ends_with(op_span, &rhs_val, span)?,
1313        },
1314        Operator::Math(mat) => match mat {
1315            Math::Add => lhs_val.add(op_span, &rhs_val, span)?,
1316            Math::Subtract => lhs_val.sub(op_span, &rhs_val, span)?,
1317            Math::Multiply => lhs_val.mul(op_span, &rhs_val, span)?,
1318            Math::Divide => lhs_val.div(op_span, &rhs_val, span)?,
1319            Math::FloorDivide => lhs_val.floor_div(op_span, &rhs_val, span)?,
1320            Math::Modulo => lhs_val.modulo(op_span, &rhs_val, span)?,
1321            Math::Pow => lhs_val.pow(op_span, &rhs_val, span)?,
1322            Math::Concatenate => lhs_val.concat(op_span, &rhs_val, span)?,
1323        },
1324        Operator::Boolean(bl) => match bl {
1325            Boolean::Or => lhs_val.or(op_span, &rhs_val, span)?,
1326            Boolean::Xor => lhs_val.xor(op_span, &rhs_val, span)?,
1327            Boolean::And => lhs_val.and(op_span, &rhs_val, span)?,
1328        },
1329        Operator::Bits(bit) => match bit {
1330            Bits::BitOr => lhs_val.bit_or(op_span, &rhs_val, span)?,
1331            Bits::BitXor => lhs_val.bit_xor(op_span, &rhs_val, span)?,
1332            Bits::BitAnd => lhs_val.bit_and(op_span, &rhs_val, span)?,
1333            Bits::ShiftLeft => lhs_val.bit_shl(op_span, &rhs_val, span)?,
1334            Bits::ShiftRight => lhs_val.bit_shr(op_span, &rhs_val, span)?,
1335        },
1336        Operator::Assignment(_asg) => {
1337            return Err(ShellError::IrEvalError {
1338                msg: "can't eval assignment with the `binary-op` instruction".into(),
1339                span: Some(span),
1340            });
1341        }
1342    };
1343
1344    ctx.put_reg(
1345        lhs_dst,
1346        PipelineExecutionData::from(PipelineData::value(result, None)),
1347    );
1348
1349    Ok(InstructionResult::Continue)
1350}
1351
1352/// Evaluate a call
1353fn eval_call<D: DebugContext>(
1354    ctx: &mut EvalContext<'_>,
1355    decl_id: DeclId,
1356    head: Span,
1357    mut input: PipelineData,
1358) -> Result<PipelineData, ShellError> {
1359    let EvalContext {
1360        engine_state,
1361        stack: caller_stack,
1362        args_base,
1363        redirect_out,
1364        redirect_err,
1365        ..
1366    } = ctx;
1367
1368    let args_len = caller_stack.arguments.get_len(*args_base);
1369    let decl = engine_state.get_decl(decl_id);
1370    // Commands such as `ignore --stderr` need errors as pipeline values so they can decide
1371    // whether to suppress or rethrow.
1372    let stderr_pipe_separate = matches!(
1373        redirect_err.as_ref(),
1374        Some(Redirection::Pipe(OutDest::PipeSeparate))
1375    );
1376
1377    // Set up redirect modes
1378    let mut caller_stack = caller_stack.push_redirection(redirect_out.take(), redirect_err.take());
1379
1380    let result = (|| {
1381        if let Some(block_id) = decl.block_id() {
1382            // If the decl is a custom command
1383            let block = engine_state.get_block(block_id);
1384
1385            // check types after acquiring block to avoid unnecessarily cloning Signature
1386            check_input_types(&input, &block.signature, head)?;
1387
1388            // Set up a callee stack with the captures and move arguments from the stack into variables
1389            let mut callee_stack = caller_stack.gather_captures(engine_state, &block.captures);
1390
1391            gather_arguments(
1392                engine_state,
1393                block,
1394                &mut caller_stack,
1395                &mut callee_stack,
1396                *args_base,
1397                args_len,
1398                head,
1399            )?;
1400
1401            // Snapshot the call's return destination onto the callee stack. Intermediate
1402            // expressions in the body may temporarily set OutDest::Value (e.g. `if (…)`);
1403            // Stack::is_stdout_redirected / `is-redirected` read this frame instead.
1404            // See Stack::with_invocation_stdout for details.
1405            let mut callee_stack =
1406                callee_stack.with_invocation_stdout(caller_stack.stdout().clone());
1407
1408            // Add one to the recursion count, so we don't recurse too deep. Stack overflows are not
1409            // recoverable in Rust.
1410            callee_stack.recursion_count += 1;
1411
1412            let result =
1413                eval_block_with_early_return::<D>(engine_state, &mut callee_stack, block, input)
1414                    .map(|p| p.body);
1415
1416            // Move environment variables back into the caller stack scope if requested to do so
1417            if block.redirect_env {
1418                redirect_env(engine_state, &mut caller_stack, &callee_stack);
1419            }
1420
1421            result
1422        } else {
1423            // `ignore` intentionally handles upstream error values at command level.
1424            // Skip early input-error propagation for the built-in `ignore` command so
1425            // `run()` can apply `--stderr`/`--show-errors` semantics.
1426            let allow_error_input = matches!(input, PipelineData::Value(Value::Error { .. }, ..))
1427                && engine_state
1428                    .find_decl(b"ignore", &[])
1429                    .is_some_and(|ignore_decl_id| ignore_decl_id == decl_id);
1430            if !allow_error_input {
1431                check_input_types(&input, &decl.signature(), head)?;
1432            }
1433            // FIXME: precalculate this and save it somewhere
1434            let span = Span::merge_many(
1435                std::iter::once(head).chain(
1436                    caller_stack
1437                        .arguments
1438                        .get_args(*args_base, args_len)
1439                        .iter()
1440                        .flat_map(|arg| arg.span()),
1441                ),
1442            );
1443
1444            let call = Call {
1445                decl_id,
1446                head,
1447                span,
1448                args_base: *args_base,
1449                args_len,
1450            };
1451
1452            // Make sure that iterating value itself can be interrupted.
1453            // e.g: 0..inf | to md
1454            if let PipelineData::Value(v, ..) = &mut input {
1455                v.inject_signals(engine_state);
1456            }
1457            // Run the call
1458            decl.run(engine_state, &mut caller_stack, &(&call).into(), input)
1459        }
1460    })();
1461
1462    drop(caller_stack);
1463
1464    // Important that this runs, to reset state post-call:
1465    ctx.stack.arguments.leave_frame(ctx.args_base);
1466    ctx.redirect_out = None;
1467    ctx.redirect_err = None;
1468
1469    match result {
1470        Err(err) if stderr_pipe_separate => Ok(PipelineData::Value(Value::error(err, head), None)),
1471        result => result,
1472    }
1473}
1474
1475fn find_named_var_id(
1476    sig: &Signature,
1477    name: &[u8],
1478    short: &[u8],
1479    span: Span,
1480) -> Result<VarId, ShellError> {
1481    sig.named
1482        .iter()
1483        .find(|n| match (n.long_name(), n.short) {
1484            (Some(long), _) => long.as_bytes() == name,
1485            // Short-only flag: match on the short character
1486            (None, Some(s)) => s.encode_utf8(&mut [0; 4]).as_bytes() == short,
1487            (None, None) => false,
1488        })
1489        .ok_or_else(|| ShellError::IrEvalError {
1490            msg: format!(
1491                "block does not have an argument named `{}`",
1492                String::from_utf8_lossy(name)
1493            ),
1494            span: Some(span),
1495        })
1496        .and_then(|flag| expect_named_var_id(flag, span))
1497}
1498
1499fn expect_named_var_id(arg: &Flag, span: Span) -> Result<VarId, ShellError> {
1500    arg.var_id.ok_or_else(|| ShellError::IrEvalError {
1501        msg: format!(
1502            "block signature is missing var id for named arg `{}`",
1503            arg.long
1504        ),
1505        span: Some(span),
1506    })
1507}
1508
1509fn expect_positional_var_id(arg: &PositionalArg, span: Span) -> Result<VarId, ShellError> {
1510    arg.var_id.ok_or_else(|| ShellError::IrEvalError {
1511        msg: format!(
1512            "block signature is missing var id for positional arg `{}`",
1513            arg.name
1514        ),
1515        span: Some(span),
1516    })
1517}
1518
1519/// Move arguments from the stack into variables for a custom command
1520fn gather_arguments(
1521    engine_state: &EngineState,
1522    block: &Block,
1523    caller_stack: &mut Stack,
1524    callee_stack: &mut Stack,
1525    args_base: usize,
1526    args_len: usize,
1527    call_head: Span,
1528) -> Result<(), ShellError> {
1529    let mut positional_iter = block
1530        .signature
1531        .required_positional
1532        .iter()
1533        .map(|p| (p, true))
1534        .chain(
1535            block
1536                .signature
1537                .optional_positional
1538                .iter()
1539                .map(|p| (p, false)),
1540        );
1541
1542    // Arguments that didn't get consumed by required/optional
1543    let mut rest = vec![];
1544    let mut rest_span: Option<Span> = None;
1545
1546    // If the rest param uses ExternalArgument shape (untyped `def --wrapped` or `known extern`),
1547    // tilde and ndots in bare glob values should be expanded, matching `run-external` behavior so
1548    // that `$args | to nuon` shows expanded paths (e.g. `/home/user`) rather than `~`.
1549    // We detect this via `allows_unknown_args`, which is set for all `def --wrapped` and
1550    // `known extern` commands, and only affects `Value::Glob` values (explicit `[...rest: string]`
1551    // produces `Value::String`, not `Value::Glob`, so those are unaffected).
1552    let expand_glob_args = block.signature.allows_unknown_args;
1553
1554    // If we encounter a spread, all further positionals should go to rest
1555    let mut always_spread = false;
1556
1557    for arg in caller_stack.arguments.drain_args(args_base, args_len) {
1558        match arg {
1559            Argument::Positional { span, val, .. } => {
1560                // Don't check next positional arg if we encountered a spread previously
1561                let next = (!always_spread).then(|| positional_iter.next()).flatten();
1562                if let Some((positional_arg, required)) = next {
1563                    let var_id = expect_positional_var_id(positional_arg, span)?;
1564                    if required {
1565                        // By checking the type of the bound variable rather than converting the
1566                        // SyntaxShape here, we might be able to save some allocations and effort
1567                        let variable = engine_state.get_var(var_id);
1568                        check_type(&val, &variable.ty)?;
1569                    }
1570                    callee_stack.add_var(var_id, val);
1571                } else {
1572                    rest_span = Some(rest_span.map_or(val.span(), |s| s.append(val.span())));
1573                    let val = if expand_glob_args {
1574                        expand_external_glob_arg(val)
1575                    } else {
1576                        val
1577                    };
1578                    rest.push(val);
1579                }
1580            }
1581            Argument::Spread {
1582                vals,
1583                span: spread_span,
1584                ..
1585            } => match vals {
1586                Value::List { vals, .. } => {
1587                    rest.extend(vals);
1588                    rest_span = Some(rest_span.map_or(spread_span, |s| s.append(spread_span)));
1589                    always_spread = true;
1590                }
1591                Value::Nothing { .. } => {
1592                    rest_span = Some(rest_span.map_or(spread_span, |s| s.append(spread_span)));
1593                    always_spread = true;
1594                }
1595                Value::Error { error, .. } => return Err(*error),
1596                _ => return Err(ShellError::CannotSpreadAsList { span: vals.span() }),
1597            },
1598            Argument::Flag {
1599                data,
1600                name,
1601                short,
1602                span,
1603            } => {
1604                let var_id = find_named_var_id(&block.signature, &data[name], &data[short], span)?;
1605                callee_stack.add_var(var_id, Value::bool(true, span))
1606            }
1607            Argument::Named {
1608                data,
1609                name,
1610                short,
1611                span,
1612                val,
1613                ..
1614            } => {
1615                let var_id = find_named_var_id(&block.signature, &data[name], &data[short], span)?;
1616                callee_stack.add_var(var_id, val)
1617            }
1618            Argument::ParserInfo { .. } => (),
1619        }
1620    }
1621
1622    // Add the collected rest of the arguments if a spread argument exists
1623    if let Some(rest_arg) = &block.signature.rest_positional {
1624        let rest_span = rest_span.unwrap_or(call_head);
1625        let var_id = expect_positional_var_id(rest_arg, rest_span)?;
1626        callee_stack.add_var(var_id, Value::list(rest, rest_span));
1627    }
1628
1629    // Check for arguments that haven't yet been set and set them to their defaults
1630    for (positional_arg, _) in positional_iter {
1631        let var_id = expect_positional_var_id(positional_arg, call_head)?;
1632        callee_stack.add_var(
1633            var_id,
1634            positional_arg
1635                .default_value
1636                .clone()
1637                .unwrap_or(Value::nothing(call_head)),
1638        );
1639    }
1640
1641    for named_arg in &block.signature.named {
1642        if let Some(var_id) = named_arg.var_id {
1643            // For named arguments, we do this check by looking to see if the variable was set yet on
1644            // the stack. This assumes that the stack's variables was previously empty, but that's a
1645            // fair assumption for a brand new callee stack.
1646            if !callee_stack.vars.iter().any(|(id, _)| *id == var_id) {
1647                let val = if named_arg.arg.is_none() {
1648                    Value::bool(false, call_head)
1649                } else if let Some(value) = &named_arg.default_value {
1650                    value.clone()
1651                } else {
1652                    Value::nothing(call_head)
1653                };
1654                callee_stack.add_var(var_id, val);
1655            }
1656        }
1657    }
1658
1659    Ok(())
1660}
1661
1662/// Type check helper. Produces `CantConvert` error if `val` is not compatible with `ty`.
1663fn check_type(val: &Value, ty: &Type) -> Result<(), ShellError> {
1664    match val {
1665        Value::Error { error, .. } => Err(*error.clone()),
1666        _ if val.is_assignable_to(ty) => Ok(()),
1667        _ => Err(ShellError::CantConvert {
1668            to_type: ty.to_string(),
1669            from_type: val.get_type().to_string(),
1670            span: val.span(),
1671            help: None,
1672        }),
1673    }
1674}
1675
1676/// Type check and convert value for assignment.
1677fn check_assignment_type(
1678    val: Value,
1679    target_ty: &Type,
1680    assignment_span: Span,
1681) -> Result<Value, ShellError> {
1682    match val {
1683        Value::Error { error, .. } => Err(*error),
1684        _ if val.is_assignable_to(target_ty) => Ok(val), // No conversion needed, but compatible
1685        _ => {
1686            let expected = target_ty.to_string();
1687            let actual = val.get_type().to_string();
1688
1689            let mut err = LabeledError::new("Type mismatch.");
1690            err = err.with_code("nu::shell::type_mismatch");
1691
1692            // Some values, like `$env.CMD_DURATION_MS`, are generated internally and don't have
1693            // spans that are relevant to users.
1694            // We avoid incorrect error labels by checking for that here.
1695            if !(val.span() == Span::unknown() || val.span() == Span::test_data()) {
1696                err = err.with_label(format!("the value is a {actual}"), val.span());
1697            }
1698
1699            err = err.with_label(
1700                format!("expected {expected}, got {actual}"),
1701                assignment_span,
1702            );
1703
1704            Err(ShellError::LabeledError(err.into()))
1705        }
1706    }
1707}
1708
1709/// Type check pipeline input against command's input types
1710fn check_input_types(
1711    input: &PipelineData,
1712    signature: &Signature,
1713    head: Span,
1714) -> Result<(), ShellError> {
1715    let io_types = &signature.input_output_types;
1716
1717    // If a command doesn't have any input/output types, then treat command input type as any
1718    if io_types.is_empty() {
1719        return Ok(());
1720    }
1721
1722    // If a command only has a nothing input type, then allow any input data
1723    if io_types.iter().all(|(intype, _)| intype == &Type::Nothing) {
1724        return Ok(());
1725    }
1726
1727    match input {
1728        // early return error directly if detected
1729        PipelineData::Value(Value::Error { error, .. }, ..) => return Err(*error.clone()),
1730        // bypass run-time typechecking for custom types
1731        PipelineData::Value(Value::Custom { .. }, ..) => return Ok(()),
1732        _ => (),
1733    }
1734
1735    // Check if the input type is compatible with *any* of the command's possible input types
1736    if io_types
1737        .iter()
1738        .any(|(command_type, _)| input.is_assignable_to(command_type))
1739    {
1740        return Ok(());
1741    }
1742
1743    let input_types: Vec<Type> = io_types.iter().map(|(input, _)| input.clone()).collect();
1744    let expected_string = combined_type_string(&input_types, "and");
1745
1746    match (input, expected_string) {
1747        (PipelineData::Empty, _) => Err(ShellError::PipelineEmpty { dst_span: head }),
1748        (_, Some(expected_string)) => Err(ShellError::OnlySupportsThisInputType {
1749            exp_input_type: expected_string,
1750            wrong_type: input.get_type().to_string(),
1751            dst_span: head,
1752            src_span: input.span().unwrap_or(head),
1753        }),
1754        // expected_string didn't generate properly, so we can't show the proper error
1755        (_, None) => Err(ShellError::NushellFailed {
1756            msg: "Command input type strings is empty, despite being non-zero earlier".to_string(),
1757        }),
1758    }
1759}
1760
1761/// Get variable from [`Stack`] or [`EngineState`]
1762fn get_var(ctx: &mut EvalContext<'_>, var_id: VarId, span: Span) -> Result<Value, ShellError> {
1763    match var_id {
1764        // $env
1765        ENV_VARIABLE_ID => {
1766            let env_vars = ctx.stack.get_env_vars(ctx.engine_state);
1767            let env_columns = env_vars.keys();
1768            let env_values = env_vars.values();
1769
1770            let mut pairs = env_columns
1771                .map(|x| x.to_string())
1772                .zip(env_values.cloned())
1773                .collect::<Vec<(String, Value)>>();
1774
1775            pairs.sort_by(|a, b| a.0.cmp(&b.0));
1776
1777            Ok(Value::record(pairs.into_iter().collect(), span))
1778        }
1779        id if id == nu_protocol::LAST_VARIABLE_ID => {
1780            // Truncation warning is deferred until after print (see evaluate_source).
1781            ctx.stack.defer_last_result_truncation_warning();
1782            ctx.stack.get_var(var_id, span)
1783        }
1784        _ => ctx.stack.get_var(var_id, span).or_else(|err| {
1785            // $nu is handled by getting constant
1786            if let Some(const_val) = ctx.engine_state.get_constant(var_id).cloned() {
1787                Ok(const_val.with_span(span))
1788            } else {
1789                Err(err)
1790            }
1791        }),
1792    }
1793}
1794
1795/// Get an environment variable (case-insensitive lookup is handled by EnvName)
1796fn get_env_var<'a>(ctx: &'a mut EvalContext<'_>, key: &str) -> Option<&'a Value> {
1797    // Read scopes in order
1798    for overlays in ctx
1799        .stack
1800        .env_vars
1801        .iter()
1802        .rev()
1803        .chain(std::iter::once(&ctx.engine_state.env_vars))
1804    {
1805        // Read overlays in order
1806        for overlay_name in ctx.stack.active_overlays.iter().rev() {
1807            let Some(map) = overlays.get(overlay_name) else {
1808                // Skip if overlay doesn't exist in this scope
1809                continue;
1810            };
1811            let hidden = ctx.stack.env_hidden.get(overlay_name);
1812            let is_hidden = |key: &EnvName| hidden.is_some_and(|hidden| hidden.contains(key));
1813
1814            if let Some(val) = map
1815                // Check for exact match (now case-insensitive due to EnvName)
1816                .get(&EnvName::from(key))
1817                // Skip when encountering an overlay where the key is hidden
1818                .filter(|_| !is_hidden(&EnvName::from(key)))
1819            {
1820                return Some(val);
1821            }
1822        }
1823    }
1824    // Not found
1825    None
1826}
1827
1828/// Get the existing name of an environment variable (case-insensitive lookup is handled by EnvName).
1829/// This is used to implement case preservation of environment variables, so that changing an
1830/// environment variable that already exists always uses the same case.
1831fn get_env_var_name<'a>(ctx: &mut EvalContext<'_>, key: &'a str) -> Cow<'a, str> {
1832    // Read scopes in order
1833    ctx.stack
1834        .env_vars
1835        .iter()
1836        .rev()
1837        .chain(std::iter::once(&ctx.engine_state.env_vars))
1838        .flat_map(|overlays| {
1839            // Read overlays in order
1840            ctx.stack
1841                .active_overlays
1842                .iter()
1843                .rev()
1844                .filter_map(|name| overlays.get(name))
1845        })
1846        .find_map(|map| {
1847            // Check if it exists (case-insensitive due to EnvName)
1848            if map.contains_key(&EnvName::from(key)) {
1849                // Find the existing key to preserve its case
1850                map.keys()
1851                    .find(|k| k.as_str().eq_ignore_case(key))
1852                    .map(|k| Cow::Owned(k.as_str().to_owned()))
1853            } else {
1854                None
1855            }
1856        })
1857        // didn't exist, use the provided key
1858        .unwrap_or(Cow::Borrowed(key))
1859}
1860
1861/// Helper to collect values into [`PipelineData`], preserving original span and metadata
1862///
1863/// The metadata is removed if it is the file data source, as that's just meant to mark streams.
1864///
1865/// It doesn't check pipefail if `ignore_error` is true.
1866fn collect(
1867    pipe: PipelineExecutionData,
1868    fallback_span: Span,
1869    #[cfg(feature = "os")] ignore_error: bool,
1870) -> Result<PipelineData, ShellError> {
1871    let mut data = pipe.body;
1872    let span = data.span().unwrap_or(fallback_span);
1873    let metadata = data.take_metadata().and_then(|m| m.for_collect());
1874    #[cfg(feature = "os")]
1875    if nu_experimental::PIPE_FAIL.get() && !ignore_error {
1876        check_exit_status_future(pipe.exit)?;
1877    }
1878    let value = data.into_value(span)?;
1879    Ok(PipelineData::value(value, metadata))
1880}
1881
1882/// Helper for drain behavior.
1883fn drain(
1884    ctx: &mut EvalContext<'_>,
1885    data: PipelineExecutionData,
1886) -> Result<InstructionResult, ShellError> {
1887    use self::InstructionResult::*;
1888
1889    match data.body {
1890        PipelineData::ByteStream(stream, ..) => {
1891            let span = stream.span();
1892            let callback_spans = stream.get_caller_spans().clone();
1893            if let Err(mut err) = stream.drain() {
1894                ctx.stack.set_last_error(&err);
1895                if callback_spans.is_empty() {
1896                    return Err(err);
1897                } else {
1898                    for s in callback_spans {
1899                        err = ShellError::EvalBlockWithInput {
1900                            span: s,
1901                            sources: vec![err],
1902                        }
1903                    }
1904                    return Err(err);
1905                }
1906            } else {
1907                ctx.stack.set_last_exit_code(0, span);
1908            }
1909        }
1910        PipelineData::ListStream(stream, ..) => {
1911            let callback_spans = stream.get_caller_spans().clone();
1912            if let Err(mut err) = stream.drain() {
1913                if callback_spans.is_empty() {
1914                    return Err(err);
1915                } else {
1916                    for s in callback_spans {
1917                        err = ShellError::EvalBlockWithInput {
1918                            span: s,
1919                            sources: vec![err],
1920                        }
1921                    }
1922                    return Err(err);
1923                }
1924            }
1925        }
1926        PipelineData::Value(..) | PipelineData::Empty => {}
1927    }
1928
1929    let pipefail = nu_experimental::PIPE_FAIL.get();
1930    if !pipefail {
1931        return Ok(Continue);
1932    }
1933    #[cfg(feature = "os")]
1934    {
1935        check_exit_status_future(data.exit).map(|_| Continue)
1936    }
1937    #[cfg(not(feature = "os"))]
1938    Ok(Continue)
1939}
1940
1941/// Helper for drainIfEnd behavior
1942fn drain_if_end(
1943    ctx: &mut EvalContext<'_>,
1944    data: PipelineExecutionData,
1945) -> Result<PipelineData, ShellError> {
1946    let stack = &mut ctx
1947        .stack
1948        .push_redirection(ctx.redirect_out.clone(), ctx.redirect_err.clone());
1949    let result = data.body.drain_to_out_dests(ctx.engine_state, stack)?;
1950
1951    let pipefail = nu_experimental::PIPE_FAIL.get();
1952    if !pipefail {
1953        return Ok(result);
1954    }
1955    #[cfg(feature = "os")]
1956    {
1957        check_exit_status_future(data.exit).map(|_| result)
1958    }
1959    #[cfg(not(feature = "os"))]
1960    Ok(result)
1961}
1962
1963enum RedirectionStream {
1964    Out,
1965    Err,
1966}
1967
1968/// Open a file for redirection
1969fn open_file(ctx: &EvalContext<'_>, path: &Value, append: bool) -> Result<Arc<File>, ShellError> {
1970    let path_expanded =
1971        expand_path_with(path.as_str()?, ctx.engine_state.cwd(Some(ctx.stack))?, true);
1972    let mut options = File::options();
1973    if append {
1974        options.append(true);
1975    } else {
1976        options.write(true).truncate(true);
1977    }
1978    let file = options
1979        .create(true)
1980        .open(&path_expanded)
1981        .map_err(|err| IoError::new(err, path.span(), path_expanded))?;
1982    Ok(Arc::new(file))
1983}
1984
1985/// Set up a [`Redirection`] from a [`RedirectMode`]
1986fn eval_redirection(
1987    ctx: &mut EvalContext<'_>,
1988    mode: &RedirectMode,
1989    span: Span,
1990    which: RedirectionStream,
1991) -> Result<Option<Redirection>, ShellError> {
1992    match mode {
1993        RedirectMode::Pipe => Ok(Some(Redirection::Pipe(OutDest::Pipe))),
1994        RedirectMode::PipeSeparate => Ok(Some(Redirection::Pipe(OutDest::PipeSeparate))),
1995        RedirectMode::Value => Ok(Some(Redirection::Pipe(OutDest::Value))),
1996        RedirectMode::Null => Ok(Some(Redirection::Pipe(OutDest::Null))),
1997        RedirectMode::Inherit => Ok(Some(Redirection::Pipe(OutDest::Inherit))),
1998        RedirectMode::Print => Ok(Some(Redirection::Pipe(OutDest::Print))),
1999        RedirectMode::File { file_num } => {
2000            let file = ctx
2001                .files
2002                .get(*file_num as usize)
2003                .cloned()
2004                .flatten()
2005                .ok_or_else(|| ShellError::IrEvalError {
2006                    msg: format!("Tried to redirect to file #{file_num}, but it is not open"),
2007                    span: Some(span),
2008                })?;
2009            Ok(Some(Redirection::File(file)))
2010        }
2011        RedirectMode::Caller => Ok(match which {
2012            RedirectionStream::Out => ctx.stack.pipe_stdout().cloned().map(Redirection::Pipe),
2013            RedirectionStream::Err => ctx.stack.pipe_stderr().cloned().map(Redirection::Pipe),
2014        }),
2015    }
2016}
2017
2018/// Do an `iterate` instruction. This can be called repeatedly to get more values from an iterable
2019fn eval_iterate(
2020    ctx: &mut EvalContext<'_>,
2021    dst: RegId,
2022    stream: RegId,
2023    end_index: usize,
2024    span: Span,
2025) -> Result<InstructionResult, ShellError> {
2026    let mut data = ctx.take_reg(stream);
2027    if let PipelineData::ListStream(list_stream, _) = &mut data.body {
2028        // Modify the stream, taking one value off, and branching if it's empty
2029        if let Some(val) = list_stream.next_value() {
2030            ctx.put_reg(dst, PipelineExecutionData::from(val.into_pipeline_data()));
2031            ctx.put_reg(stream, data); // put the stream back so it can be iterated on again
2032            Ok(InstructionResult::Continue)
2033        } else {
2034            ctx.put_reg(dst, PipelineExecutionData::empty());
2035            Ok(InstructionResult::Branch(end_index))
2036        }
2037    } else {
2038        // Convert the PipelineData to an iterator, and wrap it in a ListStream so it can be
2039        // iterated on
2040        let metadata = data.body.take_metadata();
2041        let span = data.span().unwrap_or(span);
2042        ctx.put_reg(
2043            stream,
2044            PipelineExecutionData::from(PipelineData::list_stream(
2045                ListStream::new(data.body.into_iter(), span, Signals::EMPTY),
2046                metadata,
2047            )),
2048        );
2049        eval_iterate(ctx, dst, stream, end_index, span)
2050    }
2051}
2052
2053/// Redirect environment from the callee stack to the caller stack
2054fn redirect_env(engine_state: &EngineState, caller_stack: &mut Stack, callee_stack: &Stack) {
2055    // TODO: make this more efficient
2056    // Grab all environment variables from the callee
2057    let caller_env_vars = caller_stack.get_env_var_names(engine_state);
2058
2059    // remove env vars that are present in the caller but not in the callee
2060    // (the callee hid them)
2061    for var in caller_env_vars.iter() {
2062        if !callee_stack.has_env_var(engine_state, var) {
2063            caller_stack.hide_env_var(engine_state, var);
2064        }
2065    }
2066
2067    // add new env vars from callee to caller
2068    for (var, value) in callee_stack.get_stack_env_vars() {
2069        caller_stack.add_env_var(var, value);
2070    }
2071
2072    // set config to callee config, to capture any updates to that
2073    caller_stack.config.clone_from(&callee_stack.config);
2074}