Skip to main content

nu_protocol/engine/
engine_state.rs

1use crate::{
2    BlockId, Config, DeclId, FileId, GetSpan, Handlers, HistoryConfig, JobId, Module, ModuleId,
3    OverlayId, ShellError, SignalAction, Signals, Signature, Span, SpanId, Type, Value, VarId,
4    VirtualPathId,
5    ast::{Block, Expr},
6    debugger::{Debugger, NoopDebugger},
7    engine::{
8        CachedFile, Command, DEFAULT_OVERLAY_NAME, EnvName, EnvVars, OverlayFrame, PromptState,
9        ScopeFrame, Stack, StateDelta, Variable, VisibilityStack,
10        description::{Doccomments, build_desc},
11    },
12    eval_const::create_nu_constant,
13    report_error::ReportLog,
14    shell_error::{generic::GenericError, io::IoError},
15};
16use fancy_regex::Regex;
17use lru::LruCache;
18use nu_config::NushellConfigDirs;
19use nu_path::AbsolutePathBuf;
20use nu_utils::time::Instant;
21use std::{
22    collections::{HashMap, HashSet},
23    num::NonZeroUsize,
24    path::PathBuf,
25    sync::{
26        Arc, Mutex, MutexGuard, PoisonError,
27        atomic::{AtomicBool, AtomicU32, Ordering},
28        mpsc::Sender,
29        mpsc::channel,
30    },
31    time::Duration,
32};
33
34type PoisonDebuggerError<'a> = PoisonError<MutexGuard<'a, Box<dyn Debugger>>>;
35
36#[cfg(feature = "plugin")]
37use crate::{PluginRegistryFile, PluginRegistryItem, RegisteredPlugin};
38
39use super::{CurrentJob, Jobs, Mail, Mailbox, ThreadJob};
40
41/// Configure whether the current working directory may be updated when [`EngineState::merge_env`]
42/// is called.
43///
44/// During testing, this is causing issues, so this may disable it.
45pub static UPDATE_CWD: AtomicBool = AtomicBool::new(true);
46
47#[derive(Clone, Debug)]
48pub enum VirtualPath {
49    File(FileId),
50    Dir(Vec<VirtualPathId>),
51}
52
53#[derive(Debug, Default)]
54pub struct ReplState {
55    pub buffer: String,
56    // A byte position, as `EditCommand::MoveToPosition` is also a byte position
57    pub cursor_pos: usize,
58    /// Immediately accept the buffer on the next loop.
59    pub accept: bool,
60}
61
62#[derive(Debug)]
63pub struct IsDebugging(AtomicBool);
64
65impl IsDebugging {
66    pub fn new(val: bool) -> Self {
67        IsDebugging(AtomicBool::new(val))
68    }
69}
70
71impl Clone for IsDebugging {
72    fn clone(&self) -> Self {
73        IsDebugging(AtomicBool::new(self.0.load(Ordering::Relaxed)))
74    }
75}
76
77/// The core global engine state. This includes all global definitions as well as any global state that
78/// will persist for the whole session.
79///
80/// Declarations, variables, blocks, and other forms of data are held in the global state and referenced
81/// elsewhere using their IDs. These IDs are simply their index into the global state. This allows us to
82/// more easily handle creating blocks, binding variables and callsites, and more, because each of these
83/// will refer to the corresponding IDs rather than their definitions directly. At runtime, this means
84/// less copying and smaller structures.
85///
86/// Many of the larger objects in this structure are stored within `Arc` to decrease the cost of
87/// cloning `EngineState`. While `Arc`s are generally immutable, they can be modified using
88/// `Arc::make_mut`, which automatically clones to a new allocation if there are other copies of
89/// the `Arc` already in use, but will let us modify the `Arc` directly if we have the only
90/// reference to it.
91///
92/// Note that the runtime stack is not part of this global state. Runtime stacks are handled differently,
93/// but they also rely on using IDs rather than full definitions.
94#[derive(Clone, derive_more::Debug)]
95pub struct EngineState {
96    files: Vec<CachedFile>,
97    pub(super) virtual_paths: Vec<(String, VirtualPath)>,
98    vars: Vec<Variable>,
99    #[debug("{:?}", decls.iter().map(|c| c.name()).collect::<Vec<_>>())]
100    decls: Arc<Vec<Box<dyn Command + 'static>>>,
101    // The Vec is wrapped in Arc so that if we don't need to modify the list, we can just clone
102    // the reference and not have to clone each individual Arc inside. These lists can be
103    // especially long, so it helps
104    #[debug("{:?}", blocks.iter().map(|b| &b.signature.name))]
105    pub(super) blocks: Arc<Vec<Arc<Block>>>,
106    #[debug("{:?}", modules.iter().map(|m| String::from_utf8_lossy(&m.name)))]
107    pub(super) modules: Arc<Vec<Arc<Module>>>,
108    pub spans: Vec<Span>,
109    doccomments: Doccomments,
110    pub scope: ScopeFrame,
111    signals: Signals,
112    pub signal_handlers: Option<Handlers>,
113    pub env_vars: Arc<EnvVars>,
114    pub previous_env_vars: Arc<HashMap<EnvName, Value>>,
115    /// Live config. Replace it with [`Self::set_config`] so [`Self::config_epoch`] stays in sync.
116    pub config: Arc<Config>,
117    /// Incremented when [`Self::config`] is replaced (`set_config`, `merge_env`).
118    config_epoch: u64,
119    pub pipeline_externals_state: Arc<(AtomicU32, AtomicU32)>,
120    pub repl_state: Arc<Mutex<ReplState>>,
121    /// Shared source of truth for the interactive prompt's rendered content. The
122    /// REPL fills it each cycle from `$env.PROMPT_COMMAND` and friends; a
123    /// background job's `commandline set-prompt` overwrites individual segments
124    /// and triggers an in-place repaint. Shared with every job that clones this
125    /// engine state.
126    pub prompt_state: Arc<PromptState>,
127    pub table_decl_id: Option<DeclId>,
128    #[cfg(feature = "plugin")]
129    pub plugin_path: Option<PathBuf>,
130    #[cfg(feature = "plugin")]
131    #[debug("{:?}", plugins.iter().map(|rp| rp.identity().name()).collect::<Vec<_>>())]
132    plugins: Vec<Arc<dyn RegisteredPlugin>>,
133    /// Resolved configuration directories and file paths.
134    ///
135    /// Populated once at startup by `nu_config::resolve_paths()` in `main.rs`.
136    /// All downstream code (config-file loading, `$nu` constant generation,
137    /// history backend, etc.) reads from this struct.
138    pub config_dirs: NushellConfigDirs,
139
140    pub history_enabled: bool,
141    pub history_session_id: i64,
142    /// Whether the startup-only `$env.config.history.*` options are locked from further
143    /// changes (currently `path`, `max_size`, `file_format`, `isolation`).
144    ///
145    /// Set to `true` once the REPL has finished initializing reedline's history backend.
146    /// After that point, changing any of these options would have no effect on the live
147    /// history, so attempts to mutate them are rejected with an error instead of being
148    /// silently ignored.
149    pub history_locked_after_startup: bool,
150    // Path to the file Nushell is currently evaluating, or None if we're in an interactive session.
151    pub file: Option<PathBuf>,
152    pub regex_cache: Arc<Mutex<LruCache<String, Regex>>>,
153    pub is_interactive: bool,
154    /// When true with [`Self::is_interactive`], REPL user-line evaluation may store `$ans`.
155    ///
156    /// Set only around true REPL command evaluation (`do_run_cmd`), not config/env/banner
157    /// startup, so startup scripts do not overwrite interactive last-result.
158    pub capture_repl_last_result: bool,
159    pub is_login: bool,
160    pub is_lsp: bool,
161    pub is_mcp: bool,
162    /// Running as the Debug Adapter Protocol server (`nu --dap`). Like
163    /// `is_lsp`/`is_mcp`, this means stdout is a protocol stream, so anything
164    /// that would print to it must go to stderr instead.
165    pub is_dap: bool,
166    /// When startup began: the top of `main` for the `nu` binary, otherwise when this engine
167    /// was created. See [`EngineState::startup_time`].
168    startup_start: Instant,
169    /// How long startup took, once [`EngineState::finish_startup`] has been called.
170    startup_time: Option<Duration>,
171    is_debugging: IsDebugging,
172    pub debugger: Arc<Mutex<Box<dyn Debugger>>>,
173    pub report_log: Arc<Mutex<ReportLog>>,
174
175    pub jobs: Arc<Mutex<Jobs>>,
176
177    // The job being executed with this engine state, or None if main thread
178    pub current_job: CurrentJob,
179
180    pub root_job_sender: Sender<Mail>,
181
182    // When there are background jobs running, the interactive behavior of `exit` changes depending on
183    // the value of this flag:
184    // - if this is false, then a warning about running jobs is shown and `exit` enables this flag
185    // - if this is true, then `exit` will `std::process::exit`
186    //
187    // This ensures that running exit twice will terminate the program correctly
188    pub exit_warning_given: Arc<AtomicBool>,
189}
190
191// The max number of compiled regexes to keep around in a LRU cache, arbitrarily chosen
192const REGEX_CACHE_SIZE: usize = 100; // must be nonzero, otherwise will panic
193
194pub const NU_VARIABLE_ID: VarId = VarId::new(0);
195pub const IN_VARIABLE_ID: VarId = VarId::new(1);
196pub const ENV_VARIABLE_ID: VarId = VarId::new(2);
197/// Interactive last-result special variable.
198///
199/// The user-facing name is [`LAST_RESULT_VAR_NAME`] (e.g. `$ans`). Change that constant
200/// if the public name should differ; keep this ID stable.
201pub const LAST_VARIABLE_ID: VarId = VarId::new(3);
202/// Identifier for the last-result special variable **without** the `$` sigil.
203///
204/// Change this single constant to rename the binding site-wide (e.g. `"ans"` → `$ans`).
205/// The name is reserved (cannot be rebound with `let` / `mut` / `const`). Not user-configurable.
206///
207/// With a positive `max_last_result_size`, `$ans` is `{ last, exit_code, duration, command }`.
208/// With size `0`, `$ans` is `{ exit_code, duration, command }` (`last` omitted). `command` is
209/// the exact last REPL source (same buffer reedline stores in history). Resolved from
210/// the stack's last-result slot, not from captured locals — keep capture-discovery
211/// `>` checks in sync with this being the last special var id.
212pub const LAST_RESULT_VAR_NAME: &str = "ans";
213// NOTE: If you add more specials after LAST_VARIABLE_ID, update capture discovery
214// (`var_id > LAST_VARIABLE_ID`) and any other special-id thresholds.
215
216// The first span is unknown span
217pub const UNKNOWN_SPAN_ID: SpanId = SpanId::new(0);
218
219impl EngineState {
220    pub fn new() -> Self {
221        let (send, recv) = channel::<Mail>();
222
223        Self {
224            files: vec![],
225            virtual_paths: vec![],
226            vars: vec![
227                Variable::new(Span::new(0, 0), Type::Any, false),
228                Variable::new(Span::new(0, 0), Type::Any, false),
229                Variable::new(Span::new(0, 0), Type::Any, false),
230                Variable::new(Span::new(0, 0), Type::Any, false),
231                Variable::new(Span::new(0, 0), Type::Any, false),
232            ],
233            decls: Arc::new(vec![]),
234            blocks: Arc::new(vec![]),
235            modules: Arc::new(vec![Arc::new(Module::new(
236                DEFAULT_OVERLAY_NAME.as_bytes().to_vec(),
237            ))]),
238            spans: vec![Span::unknown()],
239            doccomments: Doccomments::new(),
240            // make sure we have some default overlay:
241            scope: ScopeFrame::with_empty_overlay(
242                DEFAULT_OVERLAY_NAME.as_bytes().to_vec(),
243                ModuleId::new(0),
244                false,
245            ),
246            signal_handlers: None,
247            signals: Signals::empty(),
248            env_vars: Arc::new(
249                [(DEFAULT_OVERLAY_NAME.to_string(), HashMap::new())]
250                    .into_iter()
251                    .collect(),
252            ),
253            previous_env_vars: Arc::new(HashMap::new()),
254            config: Arc::new(Config::default()),
255            config_epoch: 0,
256            pipeline_externals_state: Arc::new((AtomicU32::new(0), AtomicU32::new(0))),
257            repl_state: Arc::new(Mutex::new(ReplState {
258                buffer: "".to_string(),
259                cursor_pos: 0,
260                accept: false,
261            })),
262            prompt_state: Arc::new(PromptState::new()),
263            table_decl_id: None,
264            #[cfg(feature = "plugin")]
265            plugin_path: None,
266            #[cfg(feature = "plugin")]
267            plugins: vec![],
268            config_dirs: NushellConfigDirs::empty(),
269            history_enabled: true,
270            history_session_id: 0,
271            history_locked_after_startup: false,
272            file: None,
273            regex_cache: Arc::new(Mutex::new(LruCache::new(
274                NonZeroUsize::new(REGEX_CACHE_SIZE).expect("tried to create cache of size zero"),
275            ))),
276            is_interactive: false,
277            capture_repl_last_result: false,
278            is_login: false,
279            is_lsp: false,
280            is_mcp: false,
281            is_dap: false,
282            startup_start: Instant::now(),
283            startup_time: None,
284            is_debugging: IsDebugging::new(false),
285            debugger: Arc::new(Mutex::new(Box::new(NoopDebugger))),
286            report_log: Arc::default(),
287            jobs: Arc::new(Mutex::new(Jobs::default())),
288            current_job: CurrentJob {
289                id: JobId::new(0),
290                background_thread_job: None,
291                mailbox: Arc::new(Mutex::new(Mailbox::new(recv))),
292            },
293            root_job_sender: send,
294            exit_warning_given: Arc::new(AtomicBool::new(false)),
295        }
296    }
297
298    pub fn signals(&self) -> &Signals {
299        &self.signals
300    }
301
302    /// Return a compiled regex, reusing the process-wide LRU cache when possible.
303    ///
304    /// On lock contention (or a poisoned mutex), compiles without touching the cache.
305    pub fn get_cached_regex(&self, pattern: &str) -> Result<Regex, fancy_regex::Error> {
306        match self.regex_cache.try_lock() {
307            Ok(mut cache) => cache
308                .try_get_or_insert_ref(pattern, || Regex::new(pattern))
309                .cloned(),
310            Err(_) => Regex::new(pattern),
311        }
312    }
313
314    /// Compile `pattern` via [`Self::get_cached_regex`], mapping failures to
315    /// [`ShellError::InvalidValue`] at `span`.
316    pub fn compile_regex(&self, pattern: &str, span: Span) -> Result<Regex, ShellError> {
317        self.get_cached_regex(pattern)
318            .map_err(|err| invalid_regex_value(pattern, err, span))
319    }
320
321    pub fn reset_signals(&mut self) {
322        self.signals.reset();
323        if let Some(ref handlers) = self.signal_handlers {
324            handlers.run(SignalAction::Reset);
325        }
326    }
327
328    pub fn set_signals(&mut self, signals: Signals) {
329        self.signals = signals;
330    }
331
332    /// Merges a `StateDelta` onto the current state. These deltas come from a system, like the parser, that
333    /// creates a new set of definitions and visible symbols in the current scope. We make this transactional
334    /// as there are times when we want to run the parser and immediately throw away the results (namely:
335    /// syntax highlighting and completions).
336    ///
337    /// When we want to preserve what the parser has created, we can take its output (the `StateDelta`) and
338    /// use this function to merge it into the global state.
339    pub fn merge_delta(&mut self, mut delta: StateDelta) -> Result<(), ShellError> {
340        // Take the mutable reference and extend the permanent state from the working set
341        self.files.extend(delta.files);
342        self.virtual_paths.extend(delta.virtual_paths);
343        self.vars.extend(delta.vars);
344        self.spans.extend(delta.spans);
345        self.doccomments.merge_with(delta.doccomments);
346
347        // Avoid potentially cloning the Arcs if we aren't adding anything
348        if !delta.decls.is_empty() {
349            Arc::make_mut(&mut self.decls).extend(delta.decls);
350        }
351        if !delta.blocks.is_empty() {
352            Arc::make_mut(&mut self.blocks).extend(delta.blocks);
353        }
354        if !delta.modules.is_empty() {
355            Arc::make_mut(&mut self.modules).extend(delta.modules);
356        }
357
358        let first = delta.scope.remove(0);
359
360        for (delta_name, delta_overlay) in first.clone().overlays {
361            if let Some((_, existing_overlay)) = self
362                .scope
363                .overlays
364                .iter_mut()
365                .find(|(name, _)| name == &delta_name)
366            {
367                // Updating existing overlay
368                for item in delta_overlay.decls.into_iter() {
369                    existing_overlay.decls.insert(item.0, item.1);
370                }
371                for item in delta_overlay.vars.into_iter() {
372                    existing_overlay.insert_variable(item.0, item.1);
373                }
374                for item in delta_overlay.modules.into_iter() {
375                    existing_overlay.modules.insert(item.0, item.1);
376                }
377
378                existing_overlay
379                    .visibility
380                    .merge_with(delta_overlay.visibility);
381            } else {
382                // New overlay was added to the delta
383                self.scope.overlays.push((delta_name, delta_overlay));
384            }
385        }
386
387        let mut activated_ids = self.translate_overlay_ids(&first);
388
389        let mut removed_ids = vec![];
390
391        for name in &first.removed_overlays {
392            if let Some(overlay_id) = self.find_overlay(name) {
393                removed_ids.push(overlay_id);
394            }
395        }
396
397        // Remove overlays removed in delta
398        self.scope
399            .active_overlays
400            .retain(|id| !removed_ids.contains(id));
401
402        // Move overlays activated in the delta to be first
403        self.scope
404            .active_overlays
405            .retain(|id| !activated_ids.contains(id));
406        self.scope.active_overlays.append(&mut activated_ids);
407
408        #[cfg(feature = "plugin")]
409        if !delta.plugins.is_empty() {
410            for plugin in std::mem::take(&mut delta.plugins) {
411                // Connect plugins to the signal handlers
412                if let Some(handlers) = &self.signal_handlers {
413                    plugin.clone().configure_signal_handler(handlers)?;
414                }
415
416                // Replace plugins that overlap in identity.
417                if let Some(existing) = self
418                    .plugins
419                    .iter_mut()
420                    .find(|p| p.identity().name() == plugin.identity().name())
421                {
422                    // Stop the existing plugin, so that the new plugin definitely takes over
423                    existing.stop()?;
424                    *existing = plugin;
425                } else {
426                    self.plugins.push(plugin);
427                }
428            }
429        }
430
431        #[cfg(feature = "plugin")]
432        if !delta.plugin_registry_items.is_empty() {
433            // Update the plugin file with the new signatures.
434            if self.plugin_path.is_some() {
435                self.update_plugin_file(std::mem::take(&mut delta.plugin_registry_items))?;
436            }
437        }
438
439        Ok(())
440    }
441
442    /// Merge the environment from the runtime Stack into the engine state
443    pub fn merge_env(&mut self, stack: &mut Stack) -> Result<(), ShellError> {
444        for mut scope in stack.env_vars.drain(..) {
445            for (overlay_name, mut env) in Arc::make_mut(&mut scope).drain() {
446                if let Some(env_vars) = Arc::make_mut(&mut self.env_vars).get_mut(&overlay_name) {
447                    // Updating existing overlay
448                    env_vars.extend(env.drain());
449                } else {
450                    // Pushing a new overlay
451                    Arc::make_mut(&mut self.env_vars).insert(overlay_name, env);
452                }
453            }
454        }
455
456        if UPDATE_CWD.load(Ordering::Relaxed) {
457            let cwd = self.cwd(Some(stack))?;
458            std::env::set_current_dir(cwd)
459                .map_err(|err| IoError::new_internal(err, "Could not set current dir"))?;
460        }
461
462        if let Some(config) = stack.config.take() {
463            // If config was updated in the stack, replace it.
464            self.replace_config(config);
465
466            // Make plugin GC config changes take effect immediately.
467            #[cfg(feature = "plugin")]
468            self.update_plugin_gc_configs(&self.config.plugin_gc);
469        }
470
471        Ok(())
472    }
473
474    /// Clean up unused variables from a Stack to prevent memory leaks.
475    /// This removes variables that are no longer referenced by any overlay or alias.
476    pub fn cleanup_stack_variables(&mut self, stack: &mut Stack) {
477        let mut shadowed_vars = HashSet::new();
478        for (_, frame) in self.scope.overlays.iter_mut() {
479            shadowed_vars.extend(frame.shadowed_vars.drain(..));
480        }
481
482        if shadowed_vars.is_empty() {
483            return;
484        }
485
486        // Collect VarIds still referenced by alias definitions so we don't
487        // remove them — doing so would cause a VariableNotFoundAtRuntime error
488        // when the alias is invoked after the variable has been re-bound.
489        let mut alias_var_ids = HashSet::new();
490        for decl in self.decls.iter() {
491            if let Some(alias) = decl.as_alias() {
492                collect_alias_var_ids(&alias.wrapped_call, &mut alias_var_ids);
493            }
494        }
495
496        // Remove variables from stack that are shadowed and not referenced by any alias
497        stack.vars.retain(|(var_id, _)| {
498            !shadowed_vars.contains(var_id) || alias_var_ids.contains(var_id)
499        });
500    }
501
502    pub fn active_overlay_ids<'a, 'b>(
503        &'b self,
504        removed_overlays: &'a [Vec<u8>],
505    ) -> impl DoubleEndedIterator<Item = &'b OverlayId> + 'a
506    where
507        'b: 'a,
508    {
509        self.scope.active_overlays.iter().filter(|id| {
510            !removed_overlays
511                .iter()
512                .any(|name| name == self.get_overlay_name(**id))
513        })
514    }
515
516    pub fn active_overlays<'a, 'b>(
517        &'b self,
518        removed_overlays: &'a [Vec<u8>],
519    ) -> impl DoubleEndedIterator<Item = &'b OverlayFrame> + 'a
520    where
521        'b: 'a,
522    {
523        self.active_overlay_ids(removed_overlays)
524            .map(|id| self.get_overlay(*id))
525    }
526
527    pub fn active_overlay_names<'a, 'b>(
528        &'b self,
529        removed_overlays: &'a [Vec<u8>],
530    ) -> impl DoubleEndedIterator<Item = &'b [u8]> + 'a
531    where
532        'b: 'a,
533    {
534        self.active_overlay_ids(removed_overlays)
535            .map(|id| self.get_overlay_name(*id))
536    }
537
538    /// Translate overlay IDs from other to IDs in self
539    fn translate_overlay_ids(&self, other: &ScopeFrame) -> Vec<OverlayId> {
540        let other_names = other.active_overlays.iter().map(|other_id| {
541            &other
542                .overlays
543                .get(other_id.get())
544                .expect("internal error: missing overlay")
545                .0
546        });
547
548        other_names
549            .map(|other_name| {
550                self.find_overlay(other_name)
551                    .expect("internal error: missing overlay")
552            })
553            .collect()
554    }
555
556    pub fn last_overlay_name(&self, removed_overlays: &[Vec<u8>]) -> &[u8] {
557        self.active_overlay_names(removed_overlays)
558            .last()
559            .expect("internal error: no active overlays")
560    }
561
562    pub fn last_overlay(&self, removed_overlays: &[Vec<u8>]) -> &OverlayFrame {
563        self.active_overlay_ids(removed_overlays)
564            .last()
565            .map(|id| self.get_overlay(*id))
566            .expect("internal error: no active overlays")
567    }
568
569    pub fn get_overlay_name(&self, overlay_id: OverlayId) -> &[u8] {
570        &self
571            .scope
572            .overlays
573            .get(overlay_id.get())
574            .expect("internal error: missing overlay")
575            .0
576    }
577
578    pub fn get_overlay(&self, overlay_id: OverlayId) -> &OverlayFrame {
579        &self
580            .scope
581            .overlays
582            .get(overlay_id.get())
583            .expect("internal error: missing overlay")
584            .1
585    }
586
587    pub fn render_env_vars(&self) -> HashMap<&str, &Value> {
588        let mut result: HashMap<&str, &Value> = HashMap::new();
589
590        for overlay_name in self.active_overlay_names(&[]) {
591            let name = String::from_utf8_lossy(overlay_name);
592            if let Some(env_vars) = self.env_vars.get(name.as_ref()) {
593                result.extend(env_vars.iter().map(|(k, v)| (k.as_str(), v)));
594            }
595        }
596
597        result
598    }
599
600    pub fn add_env_var(&mut self, name: String, val: Value) {
601        let overlay_name = String::from_utf8_lossy(self.last_overlay_name(&[])).to_string();
602
603        if let Some(env_vars) = Arc::make_mut(&mut self.env_vars).get_mut(&overlay_name) {
604            env_vars.insert(EnvName::from(name), val);
605        } else {
606            Arc::make_mut(&mut self.env_vars).insert(
607                overlay_name,
608                [(EnvName::from(name), val)].into_iter().collect(),
609            );
610        }
611    }
612
613    pub fn get_env_var(&self, name: &str) -> Option<&Value> {
614        for overlay_id in self.scope.active_overlays.iter().rev() {
615            let overlay_name = String::from_utf8_lossy(self.get_overlay_name(*overlay_id));
616            if let Some(env_vars) = self.env_vars.get(overlay_name.as_ref())
617                && let Some(val) = env_vars.get(&EnvName::from(name))
618            {
619                return Some(val);
620            }
621        }
622
623        None
624    }
625
626    #[cfg(feature = "plugin")]
627    pub fn plugins(&self) -> &[Arc<dyn RegisteredPlugin>] {
628        &self.plugins
629    }
630
631    #[cfg(feature = "plugin")]
632    fn update_plugin_file(&self, updated_items: Vec<PluginRegistryItem>) -> Result<(), ShellError> {
633        // Updating the signatures plugin file with the added signatures
634        use std::fs::File;
635
636        let plugin_path = self.plugin_path.as_ref().ok_or_else(|| {
637            ShellError::Generic(
638                GenericError::new_internal("Plugin file path not set", "")
639                    .with_help("you may be running nu with --no-config-file"),
640            )
641        })?;
642
643        // Read the current contents of the plugin file if it exists
644        let mut contents = match File::open(plugin_path.as_path()) {
645            Ok(mut plugin_file) => PluginRegistryFile::read_from(&mut plugin_file, None),
646            Err(err) => {
647                if err.kind() == std::io::ErrorKind::NotFound {
648                    Ok(PluginRegistryFile::default())
649                } else {
650                    Err(ShellError::Io(IoError::new_internal_with_path(
651                        err,
652                        "Failed to open plugin file",
653                        PathBuf::from(plugin_path),
654                    )))
655                }
656            }
657        }?;
658
659        // Update the given signatures
660        for item in updated_items {
661            contents.upsert_plugin(item);
662        }
663
664        // Write it to the same path
665        let plugin_file = File::create(plugin_path.as_path()).map_err(|err| {
666            IoError::new_internal_with_path(
667                err,
668                "Failed to write plugin file",
669                PathBuf::from(plugin_path),
670            )
671        })?;
672
673        contents.write_to(plugin_file, None)
674    }
675
676    /// Update plugins with new garbage collection config
677    #[cfg(feature = "plugin")]
678    fn update_plugin_gc_configs(&self, plugin_gc: &crate::PluginGcConfigs) {
679        for plugin in &self.plugins {
680            plugin.set_gc_config(plugin_gc.get(plugin.identity().name()));
681        }
682    }
683
684    pub fn num_files(&self) -> usize {
685        self.files.len()
686    }
687
688    pub fn num_virtual_paths(&self) -> usize {
689        self.virtual_paths.len()
690    }
691
692    pub fn num_vars(&self) -> usize {
693        self.vars.len()
694    }
695
696    pub fn num_decls(&self) -> usize {
697        self.decls.len()
698    }
699
700    pub fn num_blocks(&self) -> usize {
701        self.blocks.len()
702    }
703
704    pub fn num_modules(&self) -> usize {
705        self.modules.len()
706    }
707
708    pub fn num_spans(&self) -> usize {
709        self.spans.len()
710    }
711    pub fn print_vars(&self) {
712        for var in self.vars.iter().enumerate() {
713            println!("var{}: {:?}", var.0, var.1);
714        }
715    }
716
717    pub fn print_decls(&self) {
718        for decl in self.decls.iter().enumerate() {
719            println!("decl{}: {:?}", decl.0, decl.1.signature());
720        }
721    }
722
723    pub fn print_blocks(&self) {
724        for block in self.blocks.iter().enumerate() {
725            println!("block{}: {:?}", block.0, block.1);
726        }
727    }
728
729    pub fn print_contents(&self) {
730        for cached_file in self.files.iter() {
731            let string = String::from_utf8_lossy(&cached_file.content);
732            println!("{string}");
733        }
734    }
735
736    /// Find the [`DeclId`](crate::DeclId) corresponding to a declaration with `name`.
737    ///
738    /// Searches within active overlays, and filtering out overlays in `removed_overlays`.
739    pub fn find_decl(&self, name: &[u8], removed_overlays: &[Vec<u8>]) -> Option<DeclId> {
740        let mut visibility = VisibilityStack::default();
741
742        for overlay_frame in self.active_overlays(removed_overlays).rev() {
743            visibility.push(&overlay_frame.visibility);
744
745            if let Some(decl_id) = overlay_frame.get_decl(name)
746                && visibility.is_decl_id_visible(&decl_id)
747            {
748                return Some(decl_id);
749            }
750        }
751
752        None
753    }
754
755    /// Find the name of the declaration corresponding to `decl_id`.
756    ///
757    /// Searches within active overlays, and filtering out overlays in `removed_overlays`.
758    pub fn find_decl_name(&self, decl_id: DeclId, removed_overlays: &[Vec<u8>]) -> Option<&[u8]> {
759        let mut visibility = VisibilityStack::default();
760
761        for overlay_frame in self.active_overlays(removed_overlays).rev() {
762            visibility.push(&overlay_frame.visibility);
763
764            if visibility.is_decl_id_visible(&decl_id) {
765                for (name, id) in overlay_frame.decls.iter() {
766                    if id == &decl_id {
767                        return Some(name);
768                    }
769                }
770            }
771        }
772
773        None
774    }
775
776    /// Find the [`OverlayId`](crate::OverlayId) corresponding to `name`.
777    ///
778    /// Searches all overlays, not just active overlays. To search only in active overlays, use [`find_active_overlay`](EngineState::find_active_overlay)
779    pub fn find_overlay(&self, name: &[u8]) -> Option<OverlayId> {
780        self.scope.find_overlay(name)
781    }
782
783    /// Find the [`OverlayId`](crate::OverlayId) of the active overlay corresponding to `name`.
784    ///
785    /// Searches only active overlays. To search in all overlays, use [`find_overlay`](EngineState::find_active_overlay)
786    pub fn find_active_overlay(&self, name: &[u8]) -> Option<OverlayId> {
787        self.scope.find_active_overlay(name)
788    }
789
790    /// Find the [`ModuleId`](crate::ModuleId) corresponding to `name`.
791    ///
792    /// Searches within active overlays, and filtering out overlays in `removed_overlays`.
793    pub fn find_module(&self, name: &[u8], removed_overlays: &[Vec<u8>]) -> Option<ModuleId> {
794        for overlay_frame in self.active_overlays(removed_overlays).rev() {
795            if let Some(module_id) = overlay_frame.modules.get(name) {
796                return Some(*module_id);
797            }
798        }
799
800        None
801    }
802
803    pub fn get_module_comments(&self, module_id: ModuleId) -> Option<&[Span]> {
804        self.doccomments.get_module_comments(module_id)
805    }
806
807    #[cfg(feature = "plugin")]
808    pub fn plugin_decls(&self) -> impl Iterator<Item = &Box<dyn Command + 'static>> {
809        let mut unique_plugin_decls = HashMap::new();
810
811        // Make sure there are no duplicate decls: Newer one overwrites the older one
812        for decl in self.decls.iter().filter(|d| d.is_plugin()) {
813            unique_plugin_decls.insert(decl.name(), decl);
814        }
815
816        let mut plugin_decls: Vec<(&str, &Box<dyn Command>)> =
817            unique_plugin_decls.into_iter().collect();
818
819        // Sort the plugins by name so we don't end up with a random plugin file each time
820        plugin_decls.sort_by(|a, b| a.0.cmp(b.0));
821        plugin_decls.into_iter().map(|(_, decl)| decl)
822    }
823
824    pub fn which_module_has_decl(
825        &self,
826        decl_name: &[u8],
827        removed_overlays: &[Vec<u8>],
828    ) -> Option<&[u8]> {
829        for overlay_frame in self.active_overlays(removed_overlays).rev() {
830            for (module_name, module_id) in overlay_frame.modules.iter() {
831                let module = self.get_module(*module_id);
832                if module.has_decl(decl_name) {
833                    return Some(module_name);
834                }
835            }
836        }
837
838        None
839    }
840
841    /// Apply a function to all commands. The function accepts a command name and its DeclId
842    pub fn traverse_commands(&self, mut f: impl FnMut(&[u8], DeclId)) {
843        for overlay_frame in self.active_overlays(&[]).rev() {
844            for (name, decl_id) in &overlay_frame.decls {
845                if overlay_frame.visibility.is_decl_id_visible(decl_id) {
846                    f(name, *decl_id);
847                }
848            }
849        }
850    }
851
852    pub fn get_span_contents(&self, span: Span) -> &[u8] {
853        self.try_get_file_contents(span).unwrap_or(&[0u8; 0])
854    }
855
856    pub fn try_get_file_contents(&self, span: Span) -> Option<&[u8]> {
857        self.files.iter().find_map(|file| {
858            if file.covered_span.contains_span(span) {
859                let start = span.start - file.covered_span.start;
860                let end = span.end - file.covered_span.start;
861                Some(&file.content[start..end])
862            } else {
863                None
864            }
865        })
866    }
867
868    /// If the span's content starts with the given prefix, return two subspans
869    /// corresponding to this prefix, and the rest of the content.
870    pub fn span_match_prefix(&self, span: Span, prefix: &[u8]) -> Option<(Span, Span)> {
871        let contents = self.get_span_contents(span);
872
873        if contents.starts_with(prefix) {
874            span.split_at(prefix.len())
875        } else {
876            None
877        }
878    }
879
880    /// If the span's content ends with the given postfix, return two subspans
881    /// corresponding to the rest of the content, and this postfix.
882    pub fn span_match_postfix(&self, span: Span, prefix: &[u8]) -> Option<(Span, Span)> {
883        let contents = self.get_span_contents(span);
884
885        if contents.ends_with(prefix) {
886            span.split_at(span.len() - prefix.len())
887        } else {
888            None
889        }
890    }
891
892    /// Get the global config from the engine state.
893    ///
894    /// Use [`Stack::get_config()`] instead whenever the `Stack` is available, as it takes into
895    /// account local changes to `$env.config`.
896    pub fn get_config(&self) -> &Arc<Config> {
897        &self.config
898    }
899
900    /// Identity of the current config object. Changes when `$env.config` is rewritten.
901    pub fn config_epoch(&self) -> u64 {
902        self.config_epoch
903    }
904
905    fn replace_config(&mut self, conf: Arc<Config>) {
906        self.config_epoch = self.config_epoch.wrapping_add(1);
907        self.config = conf;
908    }
909
910    pub fn set_config(&mut self, conf: impl Into<Arc<Config>>) {
911        let conf = conf.into();
912
913        #[cfg(feature = "plugin")]
914        if conf.plugin_gc != self.config.plugin_gc {
915            // Make plugin GC config changes take effect immediately.
916            self.update_plugin_gc_configs(&conf.plugin_gc);
917        }
918
919        self.replace_config(conf);
920    }
921
922    /// Fetch the configuration for a plugin
923    ///
924    /// The `plugin` must match the registered name of a plugin.  For `plugin add
925    /// nu_plugin_example` the plugin name to use will be `"example"`
926    pub fn get_plugin_config(&self, plugin: &str) -> Option<&Value> {
927        self.config.plugins.get(plugin)
928    }
929
930    /// Returns the configuration settings for command history or `None` if history is disabled
931    pub fn history_config(&self) -> Option<HistoryConfig> {
932        self.history_enabled.then(|| self.config.history.clone())
933    }
934
935    /// Resolve the history file path using the already-resolved config dirs.
936    ///
937    /// Returns `None` when history is disabled or the history path is set to
938    /// [`HistoryPath::Disabled`].
939    pub fn history_path(&self) -> Option<std::path::PathBuf> {
940        self.history_config()?
941            .file_path(&self.config_dirs.config_home)
942    }
943
944    pub fn get_var(&self, var_id: VarId) -> &Variable {
945        self.vars
946            .get(var_id.get())
947            .expect("internal error: missing variable")
948    }
949
950    pub fn get_constant(&self, var_id: VarId) -> Option<&Value> {
951        let var = self.get_var(var_id);
952        var.const_val.as_ref()
953    }
954
955    pub fn generate_nu_constant(&mut self) {
956        self.vars[NU_VARIABLE_ID.get()].const_val = Some(create_nu_constant(self, Span::unknown()));
957    }
958
959    pub fn get_decl(&self, decl_id: DeclId) -> &dyn Command {
960        self.decls
961            .get(decl_id.get())
962            .expect("internal error: missing declaration")
963            .as_ref()
964    }
965
966    /// Get all commands within scope, sorted by the commands' names
967    pub fn get_decls_sorted(&self, include_hidden: bool) -> Vec<(Vec<u8>, DeclId)> {
968        let mut decls_map = HashMap::new();
969
970        for overlay_frame in self.active_overlays(&[]) {
971            let new_decls = if include_hidden {
972                overlay_frame.decls.clone()
973            } else {
974                overlay_frame
975                    .decls
976                    .clone()
977                    .into_iter()
978                    .filter(|(_, id)| overlay_frame.visibility.is_decl_id_visible(id))
979                    .collect()
980            };
981
982            decls_map.extend(new_decls);
983        }
984
985        let mut decls: Vec<(Vec<u8>, DeclId)> = decls_map.into_iter().collect();
986
987        decls.sort_by(|a, b| a.0.cmp(&b.0));
988        decls
989    }
990
991    pub fn get_signature(&self, decl: &dyn Command) -> Signature {
992        if let Some(block_id) = decl.block_id() {
993            *self.blocks[block_id.get()].signature.clone()
994        } else {
995            decl.signature()
996        }
997    }
998
999    /// Get signatures of all commands within scope with their decl ids.
1000    pub fn get_signatures_and_declids(&self, include_hidden: bool) -> Vec<(Signature, DeclId)> {
1001        self.get_decls_sorted(include_hidden)
1002            .into_iter()
1003            .map(|(_, id)| {
1004                let decl = self.get_decl(id);
1005
1006                (self.get_signature(decl).update_from_command(decl), id)
1007            })
1008            .collect()
1009    }
1010
1011    pub fn get_block(&self, block_id: BlockId) -> &Arc<Block> {
1012        self.blocks
1013            .get(block_id.get())
1014            .expect("internal error: missing block")
1015    }
1016
1017    /// Optionally get a block by id, if it exists
1018    ///
1019    /// Prefer to use [`.get_block()`](Self::get_block) in most cases - `BlockId`s that don't exist
1020    /// are normally a compiler error. This only exists to stop plugins from crashing the engine if
1021    /// they send us something invalid.
1022    pub fn try_get_block(&self, block_id: BlockId) -> Option<&Arc<Block>> {
1023        self.blocks.get(block_id.get())
1024    }
1025
1026    pub fn get_module(&self, module_id: ModuleId) -> &Module {
1027        self.modules
1028            .get(module_id.get())
1029            .expect("internal error: missing module")
1030    }
1031
1032    pub fn get_virtual_path(&self, virtual_path_id: VirtualPathId) -> &(String, VirtualPath) {
1033        self.virtual_paths
1034            .get(virtual_path_id.get())
1035            .expect("internal error: missing virtual path")
1036    }
1037
1038    pub fn next_span_start(&self) -> usize {
1039        if let Some(cached_file) = self.files.last() {
1040            cached_file.covered_span.end
1041        } else {
1042            0
1043        }
1044    }
1045
1046    pub fn files(
1047        &self,
1048    ) -> impl DoubleEndedIterator<Item = &CachedFile> + ExactSizeIterator<Item = &CachedFile> {
1049        self.files.iter()
1050    }
1051
1052    pub fn add_file(&mut self, filename: Arc<str>, content: Arc<[u8]>) -> FileId {
1053        let next_span_start = self.next_span_start();
1054        let next_span_end = next_span_start + content.len();
1055
1056        let covered_span = Span::new(next_span_start, next_span_end);
1057
1058        self.files.push(CachedFile {
1059            name: filename,
1060            content,
1061            covered_span,
1062        });
1063
1064        FileId::new(self.num_files() - 1)
1065    }
1066
1067    pub fn build_desc(&self, spans: &[Span]) -> (String, String) {
1068        let comment_lines: Vec<&[u8]> = spans
1069            .iter()
1070            .map(|span| self.get_span_contents(*span))
1071            .collect();
1072        build_desc(&comment_lines)
1073    }
1074
1075    pub fn build_module_desc(&self, module_id: ModuleId) -> Option<(String, String)> {
1076        self.get_module_comments(module_id)
1077            .map(|comment_spans| self.build_desc(comment_spans))
1078    }
1079
1080    /// Returns the current working directory, which is guaranteed to be canonicalized.
1081    ///
1082    /// Returns an empty String if $env.PWD doesn't exist.
1083    #[deprecated(since = "0.92.3", note = "please use `EngineState::cwd()` instead")]
1084    pub fn current_work_dir(&self) -> String {
1085        self.cwd(None)
1086            .map(|path| path.to_string_lossy().to_string())
1087            .unwrap_or_default()
1088    }
1089
1090    /// Returns the current working directory, which is guaranteed to be an
1091    /// absolute path without trailing slashes (unless it's the root path), but
1092    /// might contain symlink components.
1093    ///
1094    /// If `stack` is supplied, also considers modifications to the working
1095    /// directory on the stack that have yet to be merged into the engine state.
1096    pub fn cwd(&self, stack: Option<&Stack>) -> Result<AbsolutePathBuf, ShellError> {
1097        // Helper function to create a simple generic error.
1098        fn error(msg: &str, cwd: impl AsRef<nu_path::Path>) -> ShellError {
1099            ShellError::Generic(
1100                GenericError::new_internal(
1101                    msg.to_string(),
1102                    format!("$env.PWD = {}", cwd.as_ref().display()),
1103                )
1104                .with_help("Use `cd` to reset $env.PWD into a good state"),
1105            )
1106        }
1107
1108        // Retrieve $env.PWD from the stack or the engine state.
1109        let pwd = if let Some(stack) = stack {
1110            stack.get_env_var(self, "PWD")
1111        } else {
1112            self.get_env_var("PWD")
1113        };
1114
1115        let pwd = pwd.ok_or_else(|| error("$env.PWD not found", ""))?;
1116
1117        if let Ok(pwd) = pwd.as_str() {
1118            let path = AbsolutePathBuf::try_from(pwd)
1119                .map_err(|_| error("$env.PWD is not an absolute path", pwd))?;
1120
1121            // Technically, a root path counts as "having trailing slashes", but
1122            // for the purpose of PWD, a root path is acceptable.
1123            if path.parent().is_some() && nu_path::has_trailing_slash(path.as_ref()) {
1124                Err(error("$env.PWD contains trailing slashes", &path))
1125            } else if !path.exists() {
1126                Err(error("$env.PWD points to a non-existent directory", &path))
1127            } else if !path.is_dir() {
1128                Err(error("$env.PWD points to a non-directory", &path))
1129            } else {
1130                Ok(path)
1131            }
1132        } else {
1133            Err(error("$env.PWD is not a string", format!("{pwd:?}")))
1134        }
1135    }
1136
1137    /// Like `EngineState::cwd()`, but returns a String instead of a PathBuf for convenience.
1138    pub fn cwd_as_string(&self, stack: Option<&Stack>) -> Result<String, ShellError> {
1139        let cwd = self.cwd(stack)?;
1140        cwd.into_os_string()
1141            .into_string()
1142            .map_err(|err| ShellError::NonUtf8Custom {
1143                msg: format!("The current working directory is not a valid utf-8 string: {err:?}"),
1144                span: Span::unknown(),
1145            })
1146    }
1147
1148    // TODO: see if we can completely get rid of this
1149    pub fn get_file_contents(&self) -> &[CachedFile] {
1150        &self.files
1151    }
1152
1153    /// How long startup took (`$nu.startup-time`), or how long it has taken so far while it is
1154    /// still running, for example while the config files are being evaluated.
1155    ///
1156    /// Like the timers in bash and zsh, startup is measured from inside the shell, not from
1157    /// process creation, so it leaves out the time the operating system spends starting the
1158    /// executable, and `exec nu` does not count the program it replaced.
1159    pub fn startup_time(&self) -> Duration {
1160        self.startup_time
1161            .unwrap_or_else(|| self.startup_start.elapsed())
1162    }
1163
1164    /// Sets when startup began. `nu` calls this with the time it reads at the top of `main`.
1165    pub fn set_startup_start(&mut self, startup_start: Instant) {
1166        self.startup_start = startup_start;
1167    }
1168
1169    /// Ends startup: from now on [`EngineState::startup_time`] stays at the time it took, and
1170    /// later calls keep the first value. Regenerates `$nu` so it shows that time, along with any
1171    /// other changes made during startup. Returns the startup time.
1172    pub fn finish_startup(&mut self) -> Duration {
1173        let startup_time = *self
1174            .startup_time
1175            .get_or_insert_with(|| self.startup_start.elapsed());
1176        self.generate_nu_constant();
1177        startup_time
1178    }
1179
1180    pub fn activate_debugger(
1181        &self,
1182        debugger: Box<dyn Debugger>,
1183    ) -> Result<(), PoisonDebuggerError<'_>> {
1184        let mut locked_debugger = self.debugger.lock()?;
1185        *locked_debugger = debugger;
1186        locked_debugger.activate();
1187        self.is_debugging.0.store(true, Ordering::Relaxed);
1188        Ok(())
1189    }
1190
1191    pub fn deactivate_debugger(&self) -> Result<Box<dyn Debugger>, PoisonDebuggerError<'_>> {
1192        let mut locked_debugger = self.debugger.lock()?;
1193        locked_debugger.deactivate();
1194        let ret = std::mem::replace(&mut *locked_debugger, Box::new(NoopDebugger));
1195        self.is_debugging.0.store(false, Ordering::Relaxed);
1196        Ok(ret)
1197    }
1198
1199    pub fn is_debugging(&self) -> bool {
1200        self.is_debugging.0.load(Ordering::Relaxed)
1201    }
1202
1203    pub fn recover_from_panic(&mut self) {
1204        if Mutex::is_poisoned(&self.repl_state) {
1205            self.repl_state = Arc::new(Mutex::new(ReplState {
1206                buffer: "".to_string(),
1207                cursor_pos: 0,
1208                accept: false,
1209            }));
1210        }
1211        if Mutex::is_poisoned(&self.jobs) {
1212            self.jobs = Arc::new(Mutex::new(Jobs::default()));
1213        }
1214        if Mutex::is_poisoned(&self.regex_cache) {
1215            self.regex_cache = Arc::new(Mutex::new(LruCache::new(
1216                NonZeroUsize::new(REGEX_CACHE_SIZE).expect("tried to create cache of size zero"),
1217            )));
1218        }
1219    }
1220
1221    /// Reset mutable per-session state after cloning a shared engine template.
1222    pub fn make_session_state_unique(&mut self) {
1223        let (send, recv) = channel();
1224
1225        self.pipeline_externals_state = Arc::new((AtomicU32::new(0), AtomicU32::new(0)));
1226        self.repl_state = Default::default();
1227        self.report_log = Default::default();
1228        self.prompt_state = Arc::new(PromptState::new());
1229        self.jobs = Default::default();
1230        self.current_job = CurrentJob {
1231            id: JobId::new(0),
1232            background_thread_job: None,
1233            mailbox: Arc::new(Mutex::new(Mailbox::new(recv))),
1234        };
1235        self.root_job_sender = send;
1236        self.exit_warning_given = Default::default();
1237        self.regex_cache = Arc::new(Mutex::new(LruCache::new(
1238            NonZeroUsize::new(REGEX_CACHE_SIZE).expect("tried to create cache of size zero"),
1239        )));
1240        self.is_debugging = IsDebugging::new(false);
1241        self.debugger = Arc::new(Mutex::new(Box::new(NoopDebugger)));
1242    }
1243
1244    /// Add new span and return its ID
1245    pub fn add_span(&mut self, span: Span) -> SpanId {
1246        self.spans.push(span);
1247        SpanId::new(self.num_spans() - 1)
1248    }
1249
1250    /// Find ID of a span (should be avoided if possible)
1251    pub fn find_span_id(&self, span: Span) -> Option<SpanId> {
1252        self.spans
1253            .iter()
1254            .position(|sp| sp == &span)
1255            .map(SpanId::new)
1256    }
1257
1258    // Determines whether the current state is being held by a background job
1259    pub fn is_background_job(&self) -> bool {
1260        self.current_job.background_thread_job.is_some()
1261    }
1262
1263    // Gets the thread job entry
1264    pub fn current_thread_job(&self) -> Option<&ThreadJob> {
1265        self.current_job.background_thread_job.as_ref()
1266    }
1267}
1268
1269/// Collect all `VarId`s referenced by `Expr::Var` nodes in `expr`.
1270/// This is used by [`EngineState::cleanup_stack_variables`] to avoid removing
1271/// variables that are still referenced by alias definitions.
1272fn collect_alias_var_ids(expr: &crate::ast::Expression, var_ids: &mut HashSet<VarId>) {
1273    let mut queue = vec![expr];
1274
1275    while let Some(e) = queue.pop() {
1276        match &e.expr {
1277            Expr::Var(id) => {
1278                var_ids.insert(*id);
1279            }
1280            Expr::Call(call) => {
1281                for arg in &call.arguments {
1282                    if let Some(sub_expr) = arg.expr() {
1283                        queue.push(sub_expr);
1284                    }
1285                }
1286            }
1287            Expr::ExternalCall(head, args) => {
1288                queue.push(head);
1289                for arg in args.iter() {
1290                    queue.push(arg.expr());
1291                }
1292            }
1293            Expr::FullCellPath(fcp) => queue.push(&fcp.head),
1294            _ => {}
1295        }
1296    }
1297}
1298
1299impl GetSpan for &EngineState {
1300    /// Get existing span
1301    fn get_span(&self, span_id: SpanId) -> Span {
1302        *self
1303            .spans
1304            .get(span_id.get())
1305            .expect("internal error: missing span")
1306    }
1307}
1308
1309impl Default for EngineState {
1310    fn default() -> Self {
1311        Self::new()
1312    }
1313}
1314
1315/// Map a fancy-regex compile failure to [`ShellError::InvalidValue`].
1316///
1317/// Shared by [`EngineState::compile_regex`] and call sites that compile with
1318/// options that bypass the LRU cache (for example a custom backtrack limit).
1319pub fn invalid_regex_value(pattern: &str, err: fancy_regex::Error, span: Span) -> ShellError {
1320    ShellError::InvalidValue {
1321        valid: "a valid regular expression".into(),
1322        actual: format!("'{pattern}' ({err})"),
1323        span,
1324    }
1325}
1326
1327#[cfg(test)]
1328mod engine_state_tests {
1329    use crate::engine::StateWorkingSet;
1330    use std::str::{Utf8Error, from_utf8};
1331
1332    use super::*;
1333
1334    #[test]
1335    fn add_file_gives_id() {
1336        let engine_state = EngineState::new();
1337        let mut engine_state = StateWorkingSet::new(&engine_state);
1338        let id = engine_state.add_file("test.nu", &[]);
1339
1340        assert_eq!(id, FileId::new(0));
1341    }
1342
1343    #[test]
1344    fn get_cached_regex_reuses_compiled_patterns() {
1345        let engine_state = EngineState::new();
1346        let pattern = r"[^\w]+";
1347
1348        let first = engine_state
1349            .get_cached_regex(pattern)
1350            .expect("pattern should compile");
1351        assert!(first.is_match("!!!").unwrap_or(false));
1352
1353        {
1354            let cache = engine_state.regex_cache.lock().expect("cache lock");
1355            assert_eq!(cache.len(), 1);
1356            assert!(cache.peek(pattern).is_some());
1357        }
1358
1359        let second = engine_state
1360            .get_cached_regex(pattern)
1361            .expect("pattern should compile from cache");
1362        assert!(second.is_match("!!!").unwrap_or(false));
1363
1364        {
1365            let cache = engine_state.regex_cache.lock().expect("cache lock");
1366            assert_eq!(cache.len(), 1, "second lookup should not grow the cache");
1367        }
1368    }
1369
1370    #[test]
1371    fn get_cached_regex_does_not_store_invalid_patterns() {
1372        let engine_state = EngineState::new();
1373
1374        assert!(engine_state.get_cached_regex("(").is_err());
1375
1376        let cache = engine_state.regex_cache.lock().expect("cache lock");
1377        assert_eq!(cache.len(), 0);
1378    }
1379
1380    #[test]
1381    fn add_file_gives_id_including_parent() {
1382        let mut engine_state = EngineState::new();
1383        let parent_id = engine_state.add_file("test.nu".into(), Arc::new([]));
1384
1385        let mut working_set = StateWorkingSet::new(&engine_state);
1386        let working_set_id = working_set.add_file("child.nu", &[]);
1387
1388        assert_eq!(parent_id, FileId::new(0));
1389        assert_eq!(working_set_id, FileId::new(1));
1390    }
1391
1392    #[test]
1393    fn merge_states() -> Result<(), ShellError> {
1394        let mut engine_state = EngineState::new();
1395        engine_state.add_file("test.nu".into(), Arc::new([]));
1396
1397        let delta = {
1398            let mut working_set = StateWorkingSet::new(&engine_state);
1399            let _ = working_set.add_file("child.nu", &[]);
1400            working_set.render()
1401        };
1402
1403        engine_state.merge_delta(delta)?;
1404
1405        assert_eq!(engine_state.num_files(), 2);
1406        assert_eq!(&*engine_state.files[0].name, "test.nu");
1407        assert_eq!(&*engine_state.files[1].name, "child.nu");
1408
1409        Ok(())
1410    }
1411
1412    #[test]
1413    fn list_variables() -> Result<(), Utf8Error> {
1414        let varname = "something";
1415        let varname_with_sigil = "$".to_owned() + varname;
1416        let engine_state = EngineState::new();
1417        let mut working_set = StateWorkingSet::new(&engine_state);
1418        working_set.add_variable(
1419            varname.as_bytes().into(),
1420            Span { start: 0, end: 1 },
1421            Type::Int,
1422            false,
1423        );
1424        let variables = working_set
1425            .list_variables()
1426            .into_iter()
1427            .map(from_utf8)
1428            .collect::<Result<Vec<&str>, Utf8Error>>()?;
1429        assert_eq!(variables, vec![varname_with_sigil]);
1430        Ok(())
1431    }
1432
1433    #[test]
1434    fn get_plugin_config() {
1435        let mut engine_state = EngineState::new();
1436
1437        assert!(
1438            engine_state.get_plugin_config("example").is_none(),
1439            "Unexpected plugin configuration"
1440        );
1441
1442        let mut plugins = HashMap::new();
1443        plugins.insert("example".into(), Value::string("value", Span::test_data()));
1444
1445        let mut config = Config::clone(engine_state.get_config());
1446        config.plugins = plugins;
1447
1448        engine_state.set_config(config);
1449
1450        assert!(
1451            engine_state.get_plugin_config("example").is_some(),
1452            "Plugin configuration not found"
1453        );
1454    }
1455}
1456
1457#[cfg(test)]
1458mod test_cwd {
1459    //! Here're the test cases we need to cover:
1460    //!
1461    //! `EngineState::cwd()` computes the result from `self.env_vars["PWD"]` and
1462    //! optionally `stack.env_vars["PWD"]`.
1463    //!
1464    //! PWD may be unset in either `env_vars`.
1465    //! PWD should NOT be an empty string.
1466    //! PWD should NOT be a non-string value.
1467    //! PWD should NOT be a relative path.
1468    //! PWD should NOT contain trailing slashes.
1469    //! PWD may point to a directory or a symlink to directory.
1470    //! PWD should NOT point to a file or a symlink to file.
1471    //! PWD should NOT point to non-existent entities in the filesystem.
1472
1473    use crate::{
1474        Value,
1475        engine::{EngineState, Stack},
1476    };
1477    use nu_path::{AbsolutePath, Path, assert_path_eq};
1478    use tempfile::{NamedTempFile, TempDir};
1479
1480    /// Creates a symlink. Works on both Unix and Windows.
1481    #[cfg(any(unix, windows))]
1482    fn symlink(
1483        original: impl AsRef<AbsolutePath>,
1484        link: impl AsRef<AbsolutePath>,
1485    ) -> std::io::Result<()> {
1486        let original = original.as_ref();
1487        let link = link.as_ref();
1488
1489        #[cfg(unix)]
1490        {
1491            std::os::unix::fs::symlink(original, link)
1492        }
1493        #[cfg(windows)]
1494        {
1495            if original.is_dir() {
1496                std::os::windows::fs::symlink_dir(original, link)
1497            } else {
1498                std::os::windows::fs::symlink_file(original, link)
1499            }
1500        }
1501    }
1502
1503    /// Create an engine state initialized with the given PWD.
1504    fn engine_state_with_pwd(path: impl AsRef<Path>) -> EngineState {
1505        let mut engine_state = EngineState::new();
1506        engine_state.add_env_var(
1507            "PWD".into(),
1508            Value::test_string(path.as_ref().to_str().unwrap()),
1509        );
1510        engine_state
1511    }
1512
1513    /// Create a stack initialized with the given PWD.
1514    fn stack_with_pwd(path: impl AsRef<Path>) -> Stack {
1515        let mut stack = Stack::new();
1516        stack.add_env_var(
1517            "PWD".into(),
1518            Value::test_string(path.as_ref().to_str().unwrap()),
1519        );
1520        stack
1521    }
1522
1523    #[test]
1524    fn pwd_not_set() {
1525        let engine_state = EngineState::new();
1526        engine_state.cwd(None).unwrap_err();
1527    }
1528
1529    #[test]
1530    fn pwd_is_empty_string() {
1531        let engine_state = engine_state_with_pwd("");
1532        engine_state.cwd(None).unwrap_err();
1533    }
1534
1535    #[test]
1536    fn pwd_is_non_string_value() {
1537        let mut engine_state = EngineState::new();
1538        engine_state.add_env_var("PWD".into(), Value::test_glob("*"));
1539        engine_state.cwd(None).unwrap_err();
1540    }
1541
1542    #[test]
1543    fn pwd_is_relative_path() {
1544        let engine_state = engine_state_with_pwd("./foo");
1545
1546        engine_state.cwd(None).unwrap_err();
1547    }
1548
1549    #[test]
1550    fn pwd_has_trailing_slash() {
1551        let dir = TempDir::new().unwrap();
1552        let engine_state = engine_state_with_pwd(dir.path().join(""));
1553
1554        engine_state.cwd(None).unwrap_err();
1555    }
1556
1557    #[test]
1558    fn pwd_points_to_root() {
1559        #[cfg(windows)]
1560        let root = Path::new(r"C:\");
1561        #[cfg(not(windows))]
1562        let root = Path::new("/");
1563
1564        let engine_state = engine_state_with_pwd(root);
1565        let cwd = engine_state.cwd(None).unwrap();
1566        assert_path_eq!(cwd, root);
1567    }
1568
1569    #[test]
1570    fn pwd_points_to_normal_file() {
1571        let file = NamedTempFile::new().unwrap();
1572        let engine_state = engine_state_with_pwd(file.path());
1573
1574        engine_state.cwd(None).unwrap_err();
1575    }
1576
1577    #[test]
1578    fn pwd_points_to_normal_directory() {
1579        let dir = TempDir::new().unwrap();
1580        let engine_state = engine_state_with_pwd(dir.path());
1581
1582        let cwd = engine_state.cwd(None).unwrap();
1583        assert_path_eq!(cwd, dir.path());
1584    }
1585
1586    #[test]
1587    fn pwd_points_to_symlink_to_file() {
1588        let file = NamedTempFile::new().unwrap();
1589        let temp_file = AbsolutePath::try_new(file.path()).unwrap();
1590        let dir = TempDir::new().unwrap();
1591        let temp = AbsolutePath::try_new(dir.path()).unwrap();
1592
1593        let link = temp.join("link");
1594        symlink(temp_file, &link).unwrap();
1595        let engine_state = engine_state_with_pwd(&link);
1596
1597        engine_state.cwd(None).unwrap_err();
1598    }
1599
1600    #[test]
1601    fn pwd_points_to_symlink_to_directory() {
1602        let dir = TempDir::new().unwrap();
1603        let temp = AbsolutePath::try_new(dir.path()).unwrap();
1604
1605        let link = temp.join("link");
1606        symlink(temp, &link).unwrap();
1607        let engine_state = engine_state_with_pwd(&link);
1608
1609        let cwd = engine_state.cwd(None).unwrap();
1610        assert_path_eq!(cwd, link);
1611    }
1612
1613    #[test]
1614    fn pwd_points_to_broken_symlink() {
1615        let dir = TempDir::new().unwrap();
1616        let temp = AbsolutePath::try_new(dir.path()).unwrap();
1617        let other_dir = TempDir::new().unwrap();
1618        let other_temp = AbsolutePath::try_new(other_dir.path()).unwrap();
1619
1620        let link = temp.join("link");
1621        symlink(other_temp, &link).unwrap();
1622        let engine_state = engine_state_with_pwd(&link);
1623
1624        drop(other_dir);
1625        engine_state.cwd(None).unwrap_err();
1626    }
1627
1628    #[test]
1629    fn pwd_points_to_nonexistent_entity() {
1630        let engine_state = engine_state_with_pwd(TempDir::new().unwrap().path());
1631
1632        engine_state.cwd(None).unwrap_err();
1633    }
1634
1635    #[test]
1636    fn stack_pwd_not_set() {
1637        let dir = TempDir::new().unwrap();
1638        let engine_state = engine_state_with_pwd(dir.path());
1639        let stack = Stack::new();
1640
1641        let cwd = engine_state.cwd(Some(&stack)).unwrap();
1642        assert_eq!(cwd, dir.path());
1643    }
1644
1645    #[test]
1646    fn stack_pwd_is_empty_string() {
1647        let dir = TempDir::new().unwrap();
1648        let engine_state = engine_state_with_pwd(dir.path());
1649        let stack = stack_with_pwd("");
1650
1651        engine_state.cwd(Some(&stack)).unwrap_err();
1652    }
1653
1654    #[test]
1655    fn stack_pwd_points_to_normal_directory() {
1656        let dir1 = TempDir::new().unwrap();
1657        let dir2 = TempDir::new().unwrap();
1658        let engine_state = engine_state_with_pwd(dir1.path());
1659        let stack = stack_with_pwd(dir2.path());
1660
1661        let cwd = engine_state.cwd(Some(&stack)).unwrap();
1662        assert_path_eq!(cwd, dir2.path());
1663    }
1664
1665    #[test]
1666    fn stack_pwd_points_to_normal_directory_with_symlink_components() {
1667        let dir = TempDir::new().unwrap();
1668        let temp = AbsolutePath::try_new(dir.path()).unwrap();
1669
1670        // `/tmp/dir/link` points to `/tmp/dir`, then we set PWD to `/tmp/dir/link/foo`
1671        let link = temp.join("link");
1672        symlink(temp, &link).unwrap();
1673        let foo = link.join("foo");
1674        std::fs::create_dir(temp.join("foo")).unwrap();
1675        let engine_state = EngineState::new();
1676        let stack = stack_with_pwd(&foo);
1677
1678        let cwd = engine_state.cwd(Some(&stack)).unwrap();
1679        assert_path_eq!(cwd, foo);
1680    }
1681}