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