Skip to main content

nu_parser/
parse_captures_compile.rs

1use crate::{
2    lex::lex, parse_helpers::PERCENT_FORCED_BUILTIN_PARSER_INFO, parse_pipelines::parse_block,
3};
4use log::trace;
5use nu_protocol::{
6    BlockId, IN_VARIABLE_ID, LAST_VARIABLE_ID, ParseError, Span, Type, VarId, ast::*,
7    engine::StateWorkingSet,
8};
9use std::{collections::HashMap, sync::Arc};
10
11pub fn compile_block(working_set: &mut StateWorkingSet<'_>, block: &mut Block) {
12    if !working_set.parse_errors.is_empty() {
13        // This means there might be a bug in the parser, since calling this function while parse
14        // errors are present is a logic error. However, it's not fatal and it's best to continue
15        // without doing anything.
16        log::error!("compile_block called with parse errors");
17        return;
18    }
19
20    match nu_engine::compile(working_set, block) {
21        Ok(ir_block) => {
22            block.ir_block = Some(ir_block);
23        }
24        Err(err) => working_set.compile_errors.push(err),
25    }
26}
27
28pub fn compile_block_with_id(working_set: &mut StateWorkingSet<'_>, block_id: BlockId) {
29    if !working_set.parse_errors.is_empty() {
30        // This means there might be a bug in the parser, since calling this function while parse
31        // errors are present is a logic error. However, it's not fatal and it's best to continue
32        // without doing anything.
33        log::error!("compile_block_with_id called with parse errors");
34        return;
35    }
36
37    match nu_engine::compile(working_set, working_set.get_block(block_id)) {
38        Ok(ir_block) => {
39            working_set.get_block_mut(block_id).ir_block = Some(ir_block);
40        }
41        Err(err) => {
42            working_set.compile_errors.push(err);
43        }
44    };
45}
46
47pub fn discover_captures_in_closure(
48    working_set: &StateWorkingSet,
49    block: &Block,
50    seen: &mut Vec<VarId>,
51    seen_blocks: &mut HashMap<BlockId, Vec<(VarId, Span)>>,
52    output: &mut Vec<(VarId, Span)>,
53) -> Result<(), ParseError> {
54    for flag in &block.signature.named {
55        if let Some(var_id) = flag.var_id {
56            seen.push(var_id);
57        }
58    }
59
60    for positional in &block.signature.required_positional {
61        if let Some(var_id) = positional.var_id {
62            seen.push(var_id);
63        }
64    }
65    for positional in &block.signature.optional_positional {
66        if let Some(var_id) = positional.var_id {
67            seen.push(var_id);
68        }
69    }
70    if let Some(positional) = &block.signature.rest_positional
71        && let Some(var_id) = positional.var_id
72    {
73        seen.push(var_id);
74    }
75
76    for pipeline in &block.pipelines {
77        discover_captures_in_pipeline(working_set, pipeline, seen, seen_blocks, output)?;
78    }
79
80    Ok(())
81}
82
83fn discover_captures_in_pipeline(
84    working_set: &StateWorkingSet,
85    pipeline: &Pipeline,
86    seen: &mut Vec<VarId>,
87    seen_blocks: &mut HashMap<BlockId, Vec<(VarId, Span)>>,
88    output: &mut Vec<(VarId, Span)>,
89) -> Result<(), ParseError> {
90    for element in &pipeline.elements {
91        discover_captures_in_pipeline_element(working_set, element, seen, seen_blocks, output)?;
92    }
93
94    Ok(())
95}
96
97pub fn discover_captures_in_pipeline_element(
98    working_set: &StateWorkingSet,
99    element: &PipelineElement,
100    seen: &mut Vec<VarId>,
101    seen_blocks: &mut HashMap<BlockId, Vec<(VarId, Span)>>,
102    output: &mut Vec<(VarId, Span)>,
103) -> Result<(), ParseError> {
104    discover_captures_in_expr(working_set, &element.expr, seen, seen_blocks, output)?;
105
106    if let Some(redirection) = element.redirection.as_ref() {
107        match redirection {
108            PipelineRedirection::Single { target, .. } => {
109                if let Some(expr) = target.expr() {
110                    discover_captures_in_expr(working_set, expr, seen, seen_blocks, output)?;
111                }
112            }
113            PipelineRedirection::Separate { out, err } => {
114                if let Some(expr) = out.expr() {
115                    discover_captures_in_expr(working_set, expr, seen, seen_blocks, output)?;
116                }
117                if let Some(expr) = err.expr() {
118                    discover_captures_in_expr(working_set, expr, seen, seen_blocks, output)?;
119                }
120            }
121        }
122    }
123
124    Ok(())
125}
126
127pub fn discover_captures_in_pattern(pattern: &MatchPattern, seen: &mut Vec<VarId>) {
128    match &pattern.pattern {
129        Pattern::Variable(var_id) => seen.push(*var_id),
130        Pattern::List(items) => {
131            for item in items {
132                discover_captures_in_pattern(item, seen)
133            }
134        }
135        Pattern::Record(items) => {
136            for item in items {
137                discover_captures_in_pattern(&item.1, seen)
138            }
139        }
140        Pattern::Or(patterns) => {
141            for pattern in patterns {
142                discover_captures_in_pattern(pattern, seen)
143            }
144        }
145        Pattern::Rest(var_id) => seen.push(*var_id),
146        Pattern::Expression(_)
147        | Pattern::Value(_)
148        | Pattern::IgnoreValue
149        | Pattern::IgnoreRest
150        | Pattern::Garbage => {}
151    }
152}
153
154pub fn discover_captures_in_expr(
155    working_set: &StateWorkingSet,
156    expr: &Expression,
157    seen: &mut Vec<VarId>,
158    seen_blocks: &mut HashMap<BlockId, Vec<(VarId, Span)>>,
159    output: &mut Vec<(VarId, Span)>,
160) -> Result<(), ParseError> {
161    match &expr.expr {
162        Expr::AttributeBlock(ab) => {
163            discover_captures_in_expr(working_set, &ab.item, seen, seen_blocks, output)?;
164        }
165        Expr::BinaryOp(lhs, _, rhs) => {
166            discover_captures_in_expr(working_set, lhs, seen, seen_blocks, output)?;
167            discover_captures_in_expr(working_set, rhs, seen, seen_blocks, output)?;
168        }
169        Expr::UnaryNot(expr) => {
170            discover_captures_in_expr(working_set, expr, seen, seen_blocks, output)?;
171        }
172        Expr::Closure(block_id) => {
173            let block = working_set.get_block(*block_id);
174            let results = {
175                let mut seen = vec![];
176                let mut results = vec![];
177
178                discover_captures_in_closure(
179                    working_set,
180                    block,
181                    &mut seen,
182                    seen_blocks,
183                    &mut results,
184                )?;
185
186                for (var_id, span) in results.iter() {
187                    if !seen.contains(var_id)
188                        && let Some(variable) = working_set.get_variable_if_possible(*var_id)
189                        && variable.mutable
190                    {
191                        return Err(ParseError::CaptureOfMutableVar(*span));
192                    }
193                }
194
195                results
196            };
197            seen_blocks.insert(*block_id, results.clone());
198            for (var_id, span) in results.into_iter() {
199                if !seen.contains(&var_id) {
200                    output.push((var_id, span))
201                }
202            }
203        }
204        Expr::Block(block_id) => {
205            let block = working_set.get_block(*block_id);
206            // FIXME: is this correct?
207            let results = {
208                let mut seen = vec![];
209                let mut results = vec![];
210                discover_captures_in_closure(
211                    working_set,
212                    block,
213                    &mut seen,
214                    seen_blocks,
215                    &mut results,
216                )?;
217                results
218            };
219
220            seen_blocks.insert(*block_id, results.clone());
221            for (var_id, span) in results.into_iter() {
222                if !seen.contains(&var_id) {
223                    output.push((var_id, span))
224                }
225            }
226        }
227        Expr::Binary(_) => {}
228        Expr::Bool(_) => {}
229        Expr::Call(call) => {
230            if let Some(head_expr) = call.parser_info.get(PERCENT_FORCED_BUILTIN_PARSER_INFO) {
231                discover_captures_in_expr(working_set, head_expr, seen, seen_blocks, output)?;
232            } else {
233                let decl = working_set.get_decl(call.decl_id);
234                if let Some(block_id) = decl.block_id() {
235                    match seen_blocks.get(&block_id) {
236                        Some(capture_list) => {
237                            // Push captures onto the outer closure that aren't created by that outer closure
238                            for capture in capture_list {
239                                if !seen.contains(&capture.0) {
240                                    output.push(*capture);
241                                }
242                            }
243                        }
244                        None => {
245                            let block = working_set.get_block(block_id);
246                            if !block.captures.is_empty() {
247                                for (capture, span) in &block.captures {
248                                    if !seen.contains(capture) {
249                                        output.push((*capture, *span));
250                                    }
251                                }
252                            } else {
253                                let result = {
254                                    let mut seen = vec![];
255                                    seen_blocks.insert(block_id, vec![]);
256
257                                    let mut result = vec![];
258                                    discover_captures_in_closure(
259                                        working_set,
260                                        block,
261                                        &mut seen,
262                                        seen_blocks,
263                                        &mut result,
264                                    )?;
265
266                                    result
267                                };
268                                // Push captures onto the outer closure that aren't created by that outer closure
269                                for capture in &result {
270                                    if !seen.contains(&capture.0) {
271                                        output.push(*capture);
272                                    }
273                                }
274
275                                seen_blocks.insert(block_id, result);
276                            }
277                        }
278                    }
279                }
280            }
281
282            for arg in &call.arguments {
283                match arg {
284                    Argument::Named(named) => {
285                        if let Some(arg) = &named.2 {
286                            discover_captures_in_expr(working_set, arg, seen, seen_blocks, output)?;
287                        }
288                    }
289                    Argument::Positional(expr)
290                    | Argument::Unknown(expr)
291                    | Argument::Spread(expr) => {
292                        discover_captures_in_expr(working_set, expr, seen, seen_blocks, output)?;
293                    }
294                }
295            }
296        }
297        Expr::CellPath(_) => {}
298        Expr::DateTime(_) => {}
299        Expr::ExternalCall(head, args) => {
300            discover_captures_in_expr(working_set, head, seen, seen_blocks, output)?;
301
302            for ExternalArgument::Regular(expr) | ExternalArgument::Spread(expr) in args.as_ref() {
303                discover_captures_in_expr(working_set, expr, seen, seen_blocks, output)?;
304            }
305        }
306        Expr::Filepath(_, _) => {}
307        Expr::Directory(_, _) => {}
308        Expr::Float(_) => {}
309        Expr::FullCellPath(cell_path) => {
310            discover_captures_in_expr(working_set, &cell_path.head, seen, seen_blocks, output)?;
311        }
312        Expr::ImportPattern(_) => {}
313        Expr::Overlay(_) => {}
314        Expr::Garbage => {}
315        Expr::Nothing => {}
316        Expr::GlobPattern(_, _) => {}
317        Expr::Int(_) => {}
318        Expr::Keyword(kw) => {
319            discover_captures_in_expr(working_set, &kw.expr, seen, seen_blocks, output)?;
320        }
321        Expr::List(list) => {
322            for item in list {
323                discover_captures_in_expr(working_set, item.expr(), seen, seen_blocks, output)?;
324            }
325        }
326        Expr::Operator(_) => {}
327        Expr::Range(range) => {
328            if let Some(from) = &range.from {
329                discover_captures_in_expr(working_set, from, seen, seen_blocks, output)?;
330            }
331            if let Some(next) = &range.next {
332                discover_captures_in_expr(working_set, next, seen, seen_blocks, output)?;
333            }
334            if let Some(to) = &range.to {
335                discover_captures_in_expr(working_set, to, seen, seen_blocks, output)?;
336            }
337        }
338        Expr::Record(items) => {
339            for item in items {
340                match item {
341                    RecordItem::Pair(field_name, field_value) => {
342                        discover_captures_in_expr(
343                            working_set,
344                            field_name,
345                            seen,
346                            seen_blocks,
347                            output,
348                        )?;
349                        discover_captures_in_expr(
350                            working_set,
351                            field_value,
352                            seen,
353                            seen_blocks,
354                            output,
355                        )?;
356                    }
357                    RecordItem::Spread(_, record) => {
358                        discover_captures_in_expr(working_set, record, seen, seen_blocks, output)?;
359                    }
360                }
361            }
362        }
363        Expr::Signature(sig) => {
364            // Something with a declaration, similar to a var decl, will introduce more VarIds into the stack at eval
365            for pos in &sig.required_positional {
366                if let Some(var_id) = pos.var_id {
367                    seen.push(var_id);
368                }
369            }
370            for pos in &sig.optional_positional {
371                if let Some(var_id) = pos.var_id {
372                    seen.push(var_id);
373                }
374            }
375            if let Some(rest) = &sig.rest_positional
376                && let Some(var_id) = rest.var_id
377            {
378                seen.push(var_id);
379            }
380            for named in &sig.named {
381                if let Some(var_id) = named.var_id {
382                    seen.push(var_id);
383                }
384            }
385        }
386        Expr::String(_) => {}
387        Expr::RawString(_) => {}
388        Expr::StringInterpolation(exprs) | Expr::GlobInterpolation(exprs, _) => {
389            for expr in exprs {
390                discover_captures_in_expr(working_set, expr, seen, seen_blocks, output)?;
391            }
392        }
393        Expr::MatchBlock(match_block) => {
394            for match_ in match_block {
395                discover_captures_in_pattern(&match_.0, seen);
396                discover_captures_in_expr(working_set, &match_.1, seen, seen_blocks, output)?;
397            }
398        }
399        Expr::Collect(var_id, expr) => {
400            seen.push(*var_id);
401            discover_captures_in_expr(working_set, expr, seen, seen_blocks, output)?
402        }
403        Expr::RowCondition(block_id) | Expr::Subexpression(block_id) => {
404            let block = working_set.get_block(*block_id);
405
406            let results = {
407                let mut results = vec![];
408                let mut seen = vec![];
409                discover_captures_in_closure(
410                    working_set,
411                    block,
412                    &mut seen,
413                    seen_blocks,
414                    &mut results,
415                )?;
416                results
417            };
418
419            seen_blocks.insert(*block_id, results.clone());
420            for (var_id, span) in results.into_iter() {
421                if !seen.contains(&var_id) {
422                    output.push((var_id, span))
423                }
424            }
425        }
426        Expr::Table(table) => {
427            for header in table.columns.as_ref() {
428                discover_captures_in_expr(working_set, header, seen, seen_blocks, output)?;
429            }
430            for row in table.rows.as_ref() {
431                for cell in row.as_ref() {
432                    discover_captures_in_expr(working_set, cell, seen, seen_blocks, output)?;
433                }
434            }
435        }
436        Expr::ValueWithUnit(value) => {
437            discover_captures_in_expr(working_set, &value.expr, seen, seen_blocks, output)?;
438        }
439        Expr::Var(var_id) => {
440            // Capture user locals only. Specials `$nu` / `$env` / `$ans` resolve from
441            // engine/stack state (not stack vars); `$in` is the exception and is captured.
442            // Threshold is the last special id (`LAST_VARIABLE_ID`); keep in sync with
443            // engine_state specials when adding more.
444            if (*var_id > LAST_VARIABLE_ID || *var_id == IN_VARIABLE_ID) && !seen.contains(var_id) {
445                output.push((*var_id, expr.span));
446            }
447        }
448        Expr::VarDecl(var_id) => {
449            seen.push(*var_id);
450        }
451    }
452    Ok(())
453}
454
455pub(crate) fn wrap_redirection_with_collect(
456    working_set: &mut StateWorkingSet,
457    target: RedirectionTarget,
458) -> RedirectionTarget {
459    match target {
460        RedirectionTarget::File { expr, append, span } => RedirectionTarget::File {
461            expr: wrap_expr_with_collect(working_set, expr, None),
462            span,
463            append,
464        },
465        RedirectionTarget::Pipe { span } => RedirectionTarget::Pipe { span },
466    }
467}
468
469pub(crate) fn wrap_element_with_collect(
470    working_set: &mut StateWorkingSet,
471    element: PipelineElement,
472    input_type: Option<&Type>,
473) -> PipelineElement {
474    PipelineElement {
475        pipe: element.pipe,
476        expr: wrap_expr_with_collect(working_set, element.expr, input_type),
477        redirection: element.redirection.map(|r| match r {
478            PipelineRedirection::Single { source, target } => PipelineRedirection::Single {
479                source,
480                target: wrap_redirection_with_collect(working_set, target),
481            },
482            PipelineRedirection::Separate { out, err } => PipelineRedirection::Separate {
483                out: wrap_redirection_with_collect(working_set, out),
484                err: wrap_redirection_with_collect(working_set, err),
485            },
486        }),
487    }
488}
489
490pub(crate) fn wrap_expr_with_collect(
491    working_set: &mut StateWorkingSet,
492    mut expr: Expression,
493    input_type: Option<&Type>,
494) -> Expression {
495    let span = expr.span;
496
497    // IN_VARIABLE_ID should get replaced with a unique variable, so that we don't have to
498    // execute as a closure
499    let var_id = working_set.add_variable(
500        b"$in".into(),
501        Span::new(span.start, span.start),
502        input_type.cloned().unwrap_or(Type::Any),
503        false,
504    );
505    expr.replace_in_variable(working_set, var_id);
506
507    // Bind the custom `$in` variable for that particular expression
508    let ty = expr.ty.clone();
509    Expression::new(
510        working_set,
511        Expr::Collect(var_id, Box::new(expr)),
512        span,
513        // We can expect it to have the same result type
514        ty,
515    )
516}
517
518pub fn parse(
519    working_set: &mut StateWorkingSet,
520    fname: Option<&str>,
521    contents: &[u8],
522    scoped: bool,
523) -> Arc<Block> {
524    parse_with_block_cache(working_set, fname, contents, scoped, true)
525}
526
527/// Like [`parse`], but never returns a span-matched cached block.
528///
529/// Required for `source` / `source-env`: free variables rebind to new VarIds
530/// when `let`/`mut` are re-declared, so a block cached by span alone is stale.
531/// See https://github.com/nushell/nushell/issues/18515
532pub fn parse_fresh(
533    working_set: &mut StateWorkingSet,
534    fname: Option<&str>,
535    contents: &[u8],
536    scoped: bool,
537) -> Arc<Block> {
538    parse_with_block_cache(working_set, fname, contents, scoped, false)
539}
540
541fn parse_with_block_cache(
542    working_set: &mut StateWorkingSet,
543    fname: Option<&str>,
544    contents: &[u8],
545    scoped: bool,
546    use_block_cache: bool,
547) -> Arc<Block> {
548    trace!("parse");
549
550    let file_id = {
551        let fname = fname.map(nu_path::expand_to_real_path);
552        let fname = fname.as_deref().map(|p| p.to_string_lossy());
553        let name = fname.as_deref().unwrap_or("source");
554        working_set.add_file(name, contents)
555    };
556
557    let new_span = working_set.get_span_for_file(file_id);
558
559    // Reuse a previously-parsed block with the same span when safe (e.g. LSP).
560    // Callers that need fresh free-variable bindings use `parse_fresh`.
561    if use_block_cache && let Some(block) = working_set.find_block_by_span(new_span) {
562        return block;
563    }
564    let mut output = {
565        let (output, err) = lex(contents, new_span.start, &[], &[], false);
566        if let Some(err) = err {
567            working_set.error(err)
568        }
569
570        Arc::new(parse_block(
571            working_set,
572            &output,
573            new_span,
574            scoped,
575            false,
576            None,
577        ))
578    };
579
580    // Top level `Block`s are compiled eagerly, as they don't have a parent which would cause them
581    // to be compiled later.
582    if working_set.parse_errors.is_empty() {
583        compile_block(working_set, Arc::make_mut(&mut output));
584    }
585
586    let mut seen = vec![];
587    let mut seen_blocks = HashMap::new();
588
589    let mut captures = vec![];
590    match discover_captures_in_closure(
591        working_set,
592        &output,
593        &mut seen,
594        &mut seen_blocks,
595        &mut captures,
596    ) {
597        Ok(_) => {
598            Arc::make_mut(&mut output).captures = captures;
599        }
600        Err(err) => working_set.error(err),
601    }
602
603    // Also check other blocks that might have been imported
604    let mut errors = vec![];
605    for (block_idx, block) in working_set.delta.blocks.iter().enumerate() {
606        let block_id = block_idx + working_set.permanent_state.num_blocks();
607        let block_id = BlockId::new(block_id);
608
609        if !seen_blocks.contains_key(&block_id) {
610            let mut captures = vec![];
611
612            match discover_captures_in_closure(
613                working_set,
614                block,
615                &mut seen,
616                &mut seen_blocks,
617                &mut captures,
618            ) {
619                Ok(_) => {
620                    seen_blocks.insert(block_id, captures);
621                }
622                Err(err) => {
623                    errors.push(err);
624                }
625            }
626        }
627    }
628    for err in errors {
629        working_set.error(err)
630    }
631
632    for (block_id, captures) in seen_blocks.into_iter() {
633        // In theory, we should only be updating captures where we have new information
634        // the only place where this is possible would be blocks that are newly created
635        // by our working set delta. If we ever tried to modify the permanent state, we'd
636        // panic (again, in theory, this shouldn't be possible)
637        let block = working_set.get_block(block_id);
638        let block_captures_empty = block.captures.is_empty();
639        // need to check block_id >= working_set.permanent_state.num_blocks()
640        // to avoid mutate a block that is in the permanent state.
641        // this can happened if user defines a function with recursive call
642        // and pipe a variable to the command, e.g:
643        // def px [] { if true { 42 } else { px } };    # the block px is saved in permanent state.
644        // let x = 3
645        // $x | px
646        // If we don't guard for `block_id`, it will change captures of `px`, which is
647        // already saved in permanent state
648        if !captures.is_empty()
649            && block_captures_empty
650            && block_id.get() >= working_set.permanent_state.num_blocks()
651        {
652            let block = working_set.get_block_mut(block_id);
653            block.captures = captures;
654        }
655    }
656
657    output
658}