Skip to main content

nu_engine/compile/
mod.rs

1use nu_protocol::{
2    CompileError, IntoSpanned, RegId, Span,
3    ast::{Block, Expr, Pipeline, PipelineRedirection, RedirectionSource, RedirectionTarget},
4    engine::StateWorkingSet,
5    ir::{Instruction, IrBlock, RedirectMode, ScopeRegion},
6};
7
8mod builder;
9mod call;
10mod expression;
11mod keyword;
12mod operator;
13mod redirect;
14
15use builder::BlockBuilder;
16use call::*;
17use expression::compile_expression;
18use operator::*;
19use redirect::*;
20
21const BLOCK_INPUT: RegId = RegId::new(0);
22
23/// Compile Nushell pipeline abstract syntax tree (AST) to internal representation (IR) instructions
24/// for evaluation.
25pub fn compile(working_set: &StateWorkingSet, block: &Block) -> Result<IrBlock, CompileError> {
26    let mut builder = BlockBuilder::new(block.span);
27
28    let span = block.span.unwrap_or(Span::unknown());
29
30    // Top-level: no scope region — `eval_ir_block` pushes this block's `scope_bindings`.
31    compile_block(
32        working_set,
33        &mut builder,
34        block,
35        false,
36        RedirectModes::caller(span),
37        Some(BLOCK_INPUT),
38        BLOCK_INPUT,
39    )?;
40
41    // A complete block has to end with a `return`
42    builder.push(Instruction::Return { src: BLOCK_INPUT }.into_spanned(span))?;
43
44    builder.finish()
45}
46
47/// Compiles a [`Block`] in-place into an IR block. This can be used in a nested manner, for example
48/// by [`compile_if()`][keyword::compile_if], where the instructions for the blocks for the if/else
49/// are inlined into the top-level IR block.
50///
51/// When `record_scope_region` is true and the block has parse-time `scope_bindings`, records a
52/// [`ScopeRegion`] covering the inlined instructions so `scope` can see those locals by program
53/// counter (keyword bodies never enter `eval_ir_block`). Pass `false` for the outer block from
54/// [`compile`] (bindings activated at eval entry instead).
55fn compile_block(
56    working_set: &StateWorkingSet,
57    builder: &mut BlockBuilder,
58    block: &Block,
59    record_scope_region: bool,
60    redirect_modes: RedirectModes,
61    in_reg: Option<RegId>,
62    out_reg: RegId,
63) -> Result<(), CompileError> {
64    let span = block.span.unwrap_or(Span::unknown());
65    let region_start = builder.instructions.len();
66
67    let mut redirect_modes = Some(redirect_modes);
68    if !block.pipelines.is_empty() {
69        let last_index = block.pipelines.len() - 1;
70        for (index, pipeline) in block.pipelines.iter().enumerate() {
71            compile_pipeline(
72                working_set,
73                builder,
74                pipeline,
75                span,
76                // the redirect mode only applies to the last pipeline.
77                if index == last_index {
78                    redirect_modes
79                        .take()
80                        .expect("should only take redirect_modes once")
81                } else {
82                    RedirectModes::default()
83                },
84                // input is only passed to the first pipeline.
85                if index == 0 { in_reg } else { None },
86                out_reg,
87            )?;
88
89            if index != last_index {
90                // Explicitly drain the out reg after each non-final pipeline, because that's how
91                // the semicolon functions.
92                if builder.is_allocated(out_reg) {
93                    builder.push(Instruction::Drain { src: out_reg }.into_spanned(span))?;
94                }
95                builder.load_empty(out_reg)?;
96            }
97        }
98    } else if in_reg.is_none() {
99        builder.load_empty(out_reg)?;
100    }
101
102    if record_scope_region && let Some(bindings) = &block.scope_bindings {
103        let region_end = builder.instructions.len();
104        // Empty inlined bodies produce no instructions; still record a zero-width region
105        // is useless for PC matching, so only store non-empty ranges.
106        if region_start < region_end {
107            builder.scope_regions.push(ScopeRegion {
108                start: region_start,
109                end: region_end,
110                bindings: bindings.clone(),
111            });
112        }
113    }
114
115    Ok(())
116}
117
118fn compile_pipeline(
119    working_set: &StateWorkingSet,
120    builder: &mut BlockBuilder,
121    pipeline: &Pipeline,
122    fallback_span: Span,
123    redirect_modes: RedirectModes,
124    in_reg: Option<RegId>,
125    out_reg: RegId,
126) -> Result<(), CompileError> {
127    let mut iter = pipeline.elements.iter().peekable();
128    let mut in_reg = in_reg;
129    let mut redirect_modes = Some(redirect_modes);
130    while let Some(element) = iter.next() {
131        let span = element.pipe.unwrap_or(fallback_span);
132
133        // We have to get the redirection mode from either the explicit redirection in the pipeline
134        // element, or from the next expression if it's specified there. If this is the last
135        // element, then it's from whatever is passed in as the mode to use.
136
137        let next_redirect_modes = if let Some(next_element) = iter.peek() {
138            let mut modes = redirect_modes_of_expression(working_set, &next_element.expr, span)?;
139
140            // If there's a next element with no inherent redirection we always pipe out *unless*
141            // this is a single redirection of stderr to pipe (e>|)
142            if modes.out.is_none()
143                && !matches!(
144                    element.redirection,
145                    Some(PipelineRedirection::Single {
146                        source: RedirectionSource::Stderr,
147                        target: RedirectionTarget::Pipe { .. }
148                    })
149                )
150            {
151                let pipe_span = next_element.pipe.unwrap_or(next_element.expr.span);
152                modes.out = Some(RedirectMode::Pipe.into_spanned(pipe_span));
153            }
154
155            modes
156        } else {
157            redirect_modes
158                .take()
159                .expect("should only take redirect_modes once")
160        };
161
162        let spec_redirect_modes = match &element.redirection {
163            Some(PipelineRedirection::Single { source, target }) => {
164                let mode = redirection_target_to_mode(working_set, builder, target)?;
165                match source {
166                    RedirectionSource::Stdout => RedirectModes {
167                        out: Some(mode),
168                        err: None,
169                    },
170                    RedirectionSource::Stderr => RedirectModes {
171                        out: None,
172                        err: Some(mode),
173                    },
174                    RedirectionSource::StdoutAndStderr => RedirectModes {
175                        out: Some(mode),
176                        err: Some(mode),
177                    },
178                }
179            }
180            Some(PipelineRedirection::Separate { out, err }) => {
181                // In this case, out and err must not both be Pipe
182                assert!(
183                    !matches!(
184                        (out, err),
185                        (
186                            RedirectionTarget::Pipe { .. },
187                            RedirectionTarget::Pipe { .. }
188                        )
189                    ),
190                    "for Separate redirection, out and err targets must not both be Pipe"
191                );
192                let out = redirection_target_to_mode(working_set, builder, out)?;
193                let err = redirection_target_to_mode(working_set, builder, err)?;
194                RedirectModes {
195                    out: Some(out),
196                    err: Some(err),
197                }
198            }
199            None => RedirectModes {
200                out: None,
201                err: None,
202            },
203        };
204
205        let redirect_modes = RedirectModes {
206            out: spec_redirect_modes.out.or(next_redirect_modes.out),
207            err: spec_redirect_modes.err.or(next_redirect_modes.err),
208        };
209
210        compile_expression(
211            working_set,
212            builder,
213            &element.expr,
214            redirect_modes.clone(),
215            in_reg,
216            out_reg,
217        )?;
218
219        // Only clean up the redirection if current element is NOT
220        // a nested eval expression, since this already cleans it.
221        if !has_nested_eval_expr(&element.expr.expr) {
222            // Clean up the redirection
223            finish_redirection(builder, redirect_modes, out_reg)?;
224        }
225
226        // The next pipeline element takes input from this output
227        in_reg = Some(out_reg);
228    }
229    Ok(())
230}
231
232fn has_nested_eval_expr(expr: &Expr) -> bool {
233    is_subexpression(expr) || is_block_call(expr)
234}
235
236fn is_block_call(expr: &Expr) -> bool {
237    match expr {
238        Expr::Call(inner) => inner
239            .arguments
240            .iter()
241            .any(|arg| matches!(arg.expr().map(|e| &e.expr), Some(Expr::Block(..)))),
242        _ => false,
243    }
244}
245
246fn is_subexpression(expr: &Expr) -> bool {
247    match expr {
248        Expr::FullCellPath(inner) => {
249            matches!(&inner.head.expr, &Expr::Subexpression(..))
250        }
251        Expr::Subexpression(..) => true,
252        _ => false,
253    }
254}