Skip to main content

nu_engine/
scope.rs

1//! Helpers for the `scope` family of commands (`scope variables`, `scope commands`, …).
2//!
3//! # How “what’s in scope” is collected
4//!
5//! Permanent (global) bindings live on [`EngineState`] overlays. Nested parse scopes throw
6//! away their name maps on `exit_scope`, so two additional mechanisms recover locals:
7//!
8//! 1. **Variables** — each [`Variable`](nu_protocol::engine::Variable) may store its `name`.
9//!    [`ScopeData::collect_vars`] builds a name→id map from permanent overlays, then overwrites
10//!    with stack-resident VarIds (outer→inner so the live binding wins). Non-const entries
11//!    without a stack value are skipped (supports `unlet`). Permanent names without
12//!    `Variable.name` still appear via overlays.
13//!
14//! 2. **Commands / aliases / externs / modules** — parse snapshots
15//!    [`ScopeBindings`](nu_protocol::engine::ScopeBindings) onto [`Block`](nu_protocol::ast::Block).
16//!    At runtime:
17//!    - Whole blocks (closures, custom commands) push bindings on
18//!      [`Stack::active_scope_bindings`] in `eval_ir_block`.
19//!    - Keyword bodies inlined into parent IR record
20//!      [`ScopeRegion`](nu_protocol::ir::ScopeRegion)s; `scope` includes regions that contain
21//!      the current instruction index.
22//!
23//!    [`ScopeData::populate_decls`] / [`ScopeData::populate_modules`] merge permanent overlays,
24//!    then active whole-block bindings, then matching IR regions.
25//!
26//! [`scope engine-stats`](crate) intentionally reports only engine-wide counts and ignores locals.
27
28use nu_protocol::{
29    CommandWideCompleter, DeclId, ModuleId, Signature, Span, Type, Value, VarId,
30    ast::Expr,
31    engine::{Command, CommandType, EngineState, ScopeBindings, Stack, Visibility},
32    record,
33};
34use std::{cmp::Ordering, collections::HashMap};
35
36/// Collects name→id maps for the `scope` subcommands.
37///
38/// Call `populate_*` before the matching `collect_*` (except variables: collection is
39/// self-contained; `populate_vars` is a no-op retained only for call-site uniformity).
40pub struct ScopeData<'e, 's> {
41    engine_state: &'e EngineState,
42    stack: &'s Stack,
43    decls_map: HashMap<Vec<u8>, DeclId>,
44    modules_map: HashMap<Vec<u8>, ModuleId>,
45    visibility: Visibility,
46}
47
48impl<'e, 's> ScopeData<'e, 's> {
49    pub fn new(engine_state: &'e EngineState, stack: &'s Stack) -> Self {
50        Self {
51            engine_state,
52            stack,
53            decls_map: HashMap::new(),
54            modules_map: HashMap::new(),
55            visibility: Visibility::new(),
56        }
57    }
58
59    /// No-op retained so all `scope` commands share the same populate-then-collect pattern.
60    /// Variable listing is implemented entirely in [`Self::collect_vars`].
61    pub fn populate_vars(&mut self) {}
62
63    // decls include all commands, i.e., normal commands, aliases, and externals
64    pub fn populate_decls(&mut self) {
65        let bindings = self.collect_local_and_global_bindings();
66        self.decls_map = bindings.decls;
67        self.visibility = bindings.visibility;
68    }
69
70    pub fn populate_modules(&mut self) {
71        let bindings = self.collect_local_and_global_bindings();
72        self.modules_map = bindings.modules;
73    }
74
75    /// Permanent overlays, then whole-block active bindings, then IR regions covering the PC.
76    fn collect_local_and_global_bindings(&self) -> ScopeBindings {
77        let mut bindings = ScopeBindings::default();
78        for overlay_frame in self.engine_state.active_overlays(&[]) {
79            bindings.extend_from_overlay(overlay_frame);
80        }
81        for local in &self.stack.active_scope_bindings {
82            bindings.extend_from_bindings(local);
83        }
84        if let Some(pc) = self.stack.ir_instruction_index {
85            // Regions are recorded outer→inner by construction; later extends win on name clash.
86            for region in &self.stack.ir_scope_regions {
87                if region.contains(pc) {
88                    bindings.extend_from_bindings(&region.bindings);
89                }
90            }
91        }
92        bindings
93    }
94
95    /// List variables currently nameable at this stack depth (local ∪ global).
96    ///
97    /// Permanent overlay names are the baseline (global scope). Stack VarIds with
98    /// [`Variable::name`] overwrite so locals and shadowed `let` bindings report the live
99    /// binding. Values are read through the stack parent chain (capture stacks keep the caller
100    /// as parent) so outer/`let` globals remain visible inside `do`/closures. Entries removed
101    /// with `unlet` are omitted.
102    pub fn collect_vars(&self, span: Span) -> Vec<Value> {
103        let mut name_to_id: HashMap<Vec<u8>, VarId> = HashMap::new();
104
105        for overlay_frame in self.engine_state.active_overlays(&[]) {
106            for (name, var_id) in &overlay_frame.vars {
107                name_to_id.insert(name.clone(), *var_id);
108            }
109        }
110
111        // Outer → inner so innermost same-name binding wins.
112        for var_id in stack_var_ids(self.stack) {
113            if let Some(name) = &self.engine_state.get_var(var_id).name {
114                name_to_id.insert(name.clone(), var_id);
115            }
116        }
117
118        let mut vars = vec![];
119
120        for (var_name, var_id) in &name_to_id {
121            if is_unlet(self.stack, *var_id) {
122                continue;
123            }
124
125            let var = self.engine_state.get_var(*var_id);
126            let var_type = Value::string(var.ty.to_string(), span);
127            let is_const = Value::bool(var.const_val.is_some(), span);
128
129            let var_value_result = self.stack.get_var(*var_id, span);
130
131            // Prefer stack (including parent chain) value, then const, else nothing so global
132            // names still appear when not captured into the current closure frame.
133            if var_value_result.is_err() && var.const_val.is_none() {
134                // Name is in permanent overlays (global) or only on stack with no value yet.
135                // Keep listing overlay globals; skip pure stack placeholders without a value.
136                let in_permanent_overlay = self
137                    .engine_state
138                    .active_overlays(&[])
139                    .any(|overlay| overlay.vars.values().any(|id| *id == *var_id));
140                if !in_permanent_overlay {
141                    continue;
142                }
143            }
144
145            let var_value = var_value_result
146                .ok()
147                .or(var.const_val.clone())
148                .unwrap_or(Value::nothing(span));
149
150            let var_id_val = Value::int(var_id.get() as i64, span);
151            let memory_size = Value::int(var_value.memory_size() as i64, span);
152
153            vars.push(Value::record(
154                record! {
155                    "name" => Value::string(String::from_utf8_lossy(var_name).to_string(), span),
156                    "type" => var_type,
157                    "value" => var_value,
158                    "is_const" => is_const,
159                    "var_id" => var_id_val,
160                    "mem_size" => memory_size,
161                },
162                span,
163            ));
164        }
165
166        sort_rows(&mut vars);
167        vars
168    }
169
170    pub fn collect_commands(&self, span: Span) -> Vec<Value> {
171        let mut commands = vec![];
172
173        for (command_name, decl_id) in &self.decls_map {
174            if self.visibility.is_decl_id_visible(decl_id)
175                && !self.engine_state.get_decl(*decl_id).is_alias()
176            {
177                let command_name = String::from_utf8_lossy(command_name);
178                let decl = self.engine_state.get_decl(*decl_id);
179                let signature = decl.signature();
180
181                let examples = decl
182                    .examples()
183                    .into_iter()
184                    .map(|x| {
185                        Value::record(
186                            record! {
187                                "description" => Value::string(x.description, span),
188                                "example" => Value::string(x.example, span),
189                                "result" => x.result.unwrap_or(Value::nothing(span)).with_span(span),
190                            },
191                            span,
192                        )
193                    })
194                    .collect();
195
196                let attributes = decl
197                    .attributes()
198                    .into_iter()
199                    .map(|(name, value)| {
200                        Value::record(
201                            record! {
202                                "name" => Value::string(name, span),
203                                "value" => value,
204                            },
205                            span,
206                        )
207                    })
208                    .collect();
209
210                let deprecations = decl
211                    .deprecation_info()
212                    .into_iter()
213                    .map(|entry| entry.into_value(&command_name, span))
214                    .collect();
215
216                let record = record! {
217                    "name" => Value::string(command_name, span),
218                    "category" => Value::string(signature.category.to_string(), span),
219                    "signatures" => self.collect_signatures(&signature, span),
220                    "description" => Value::string(decl.description(), span),
221                    "examples" => Value::list(examples, span),
222                    "attributes" => Value::list(attributes, span),
223                    "type" => Value::string(decl.command_type().to_string(), span),
224                    "is_sub" => Value::bool(decl.is_sub(), span),
225                    "is_const" => Value::bool(decl.is_const(), span),
226                    "creates_scope" => Value::bool(signature.creates_scope, span),
227                    "extra_description" => Value::string(decl.extra_description(), span),
228                    "search_terms" => Value::string(decl.search_terms().join(", "), span),
229                    "complete" => match signature.complete {
230                        Some(CommandWideCompleter::Command(decl_id)) => Value::int(decl_id.get() as i64, span),
231                        Some(CommandWideCompleter::External) => Value::string("external", span),
232                        None => Value::nothing(span),
233                    },
234                    "deprecation_info" => Value::list(deprecations, span),
235                    "decl_id" => Value::int(decl_id.get() as i64, span),
236                };
237
238                commands.push(Value::record(record, span))
239            }
240        }
241
242        sort_rows(&mut commands);
243
244        commands
245    }
246
247    fn collect_signatures(&self, signature: &Signature, span: Span) -> Value {
248        let mut sigs = signature
249            .input_output_types
250            .iter()
251            .map(|(input_type, output_type)| {
252                (
253                    input_type.to_shape().to_string(),
254                    Value::list(
255                        self.collect_signature_entries(input_type, output_type, signature, span),
256                        span,
257                    ),
258                )
259            })
260            .collect::<Vec<(String, Value)>>();
261
262        // Until we allow custom commands to have input and output types, let's just
263        // make them Type::Any Type::Any so they can show up in our `scope commands`
264        // a little bit better. If sigs is empty, we're pretty sure that we're dealing
265        // with a custom command.
266        if sigs.is_empty() {
267            let any_type = &Type::Any;
268            sigs.push((
269                any_type.to_shape().to_string(),
270                Value::list(
271                    self.collect_signature_entries(any_type, any_type, signature, span),
272                    span,
273                ),
274            ));
275        }
276        sigs.sort_unstable_by(|(k1, _), (k2, _)| k1.cmp(k2));
277        // For most commands, input types are not repeated in
278        // `input_output_types`, i.e. each input type has only one associated
279        // output type. Furthermore, we want this to always be true. However,
280        // there are currently some exceptions, such as `hash sha256` which
281        // takes in string but may output string or binary depending on the
282        // presence of the --binary flag. In such cases, the "special case"
283        // signature usually comes later in the input_output_types, so this will
284        // remove them from the record.
285        sigs.dedup_by(|(k1, _), (k2, _)| k1 == k2);
286        Value::record(sigs.into_iter().collect(), span)
287    }
288
289    fn collect_signature_entries(
290        &self,
291        input_type: &Type,
292        output_type: &Type,
293        signature: &Signature,
294        span: Span,
295    ) -> Vec<Value> {
296        let mut sig_records = vec![];
297
298        // input
299        sig_records.push(Value::record(
300            record! {
301                "parameter_name" => Value::nothing(span),
302                "parameter_type" => Value::string("input", span),
303                "syntax_shape" => Value::string(input_type.to_shape().to_string(), span),
304                "is_optional" => Value::bool(false, span),
305                "short_flag" => Value::nothing(span),
306                "description" => Value::nothing(span),
307                "completion" => Value::nothing(span),
308                "parameter_default" => Value::nothing(span),
309            },
310            span,
311        ));
312
313        // required_positional
314        for req in &signature.required_positional {
315            let completion = req
316                .completion
317                .as_ref()
318                .map(|compl| compl.to_value(self.engine_state, span))
319                .unwrap_or(Value::nothing(span));
320
321            sig_records.push(Value::record(
322                record! {
323                    "parameter_name" => Value::string(&req.name, span),
324                    "parameter_type" => Value::string("positional", span),
325                    "syntax_shape" => Value::string(req.shape.to_string(), span),
326                    "is_optional" => Value::bool(false, span),
327                    "short_flag" => Value::nothing(span),
328                    "description" => Value::string(&req.desc, span),
329                    "completion" => completion,
330                    "parameter_default" => Value::nothing(span),
331                },
332                span,
333            ));
334        }
335
336        // optional_positional
337        for opt in &signature.optional_positional {
338            let completion = opt
339                .completion
340                .as_ref()
341                .map(|compl| compl.to_value(self.engine_state, span))
342                .unwrap_or(Value::nothing(span));
343
344            let default = if let Some(val) = &opt.default_value {
345                val.clone()
346            } else {
347                Value::nothing(span)
348            };
349
350            sig_records.push(Value::record(
351                record! {
352                    "parameter_name" => Value::string(&opt.name, span),
353                    "parameter_type" => Value::string("positional", span),
354                    "syntax_shape" => Value::string(opt.shape.to_string(), span),
355                    "is_optional" => Value::bool(true, span),
356                    "short_flag" => Value::nothing(span),
357                    "description" => Value::string(&opt.desc, span),
358                    "completion" => completion,
359                    "parameter_default" => default,
360                },
361                span,
362            ));
363        }
364
365        // rest_positional
366        if let Some(rest) = &signature.rest_positional {
367            let name = if rest.name == "rest" { "" } else { &rest.name };
368            let completion = rest
369                .completion
370                .as_ref()
371                .map(|compl| compl.to_value(self.engine_state, span))
372                .unwrap_or(Value::nothing(span));
373
374            sig_records.push(Value::record(
375                record! {
376                    "parameter_name" => Value::string(name, span),
377                    "parameter_type" => Value::string("rest", span),
378                    "syntax_shape" => Value::string(rest.shape.to_string(), span),
379                    "is_optional" => Value::bool(true, span),
380                    "short_flag" => Value::nothing(span),
381                    "description" => Value::string(&rest.desc, span),
382                    "completion" => completion,
383                    // rest_positional does have default, but parser prohibits specifying it?!
384                    "parameter_default" => Value::nothing(span),
385                },
386                span,
387            ));
388        }
389
390        // named flags
391        for named in &signature.named {
392            let flag_type;
393
394            // Skip the help flag
395            if named.long == "help" {
396                continue;
397            }
398
399            let completion = named
400                .completion
401                .as_ref()
402                .map(|compl| compl.to_value(self.engine_state, span))
403                .unwrap_or(Value::nothing(span));
404
405            let shape = if let Some(arg) = &named.arg {
406                flag_type = Value::string("named", span);
407                Value::string(arg.to_string(), span)
408            } else {
409                flag_type = Value::string("switch", span);
410                Value::nothing(span)
411            };
412
413            let short_flag = if let Some(c) = named.short {
414                Value::string(c, span)
415            } else {
416                Value::nothing(span)
417            };
418
419            let default = if let Some(val) = &named.default_value {
420                val.clone()
421            } else {
422                Value::nothing(span)
423            };
424
425            sig_records.push(Value::record(
426                record! {
427                    "parameter_name" => Value::string(&named.long, span),
428                    "parameter_type" => flag_type,
429                    "syntax_shape" => shape,
430                    "is_optional" => Value::bool(!named.required, span),
431                    "short_flag" => short_flag,
432                    "description" => Value::string(&named.desc, span),
433                    "completion" => completion,
434                    "parameter_default" => default,
435                },
436                span,
437            ));
438        }
439
440        // output
441        sig_records.push(Value::record(
442            record! {
443                "parameter_name" => Value::nothing(span),
444                "parameter_type" => Value::string("output", span),
445                "syntax_shape" => Value::string(output_type.to_shape().to_string(), span),
446                "is_optional" => Value::bool(false, span),
447                "short_flag" => Value::nothing(span),
448                "description" => Value::nothing(span),
449                "completion" => Value::nothing(span),
450                "parameter_default" => Value::nothing(span),
451            },
452            span,
453        ));
454
455        sig_records
456    }
457
458    pub fn collect_externs(&self, span: Span) -> Vec<Value> {
459        let mut externals = vec![];
460
461        for (command_name, decl_id) in &self.decls_map {
462            let decl = self.engine_state.get_decl(*decl_id);
463
464            if decl.is_known_external() {
465                let record = record! {
466                    "name" => Value::string(String::from_utf8_lossy(command_name), span),
467                    "description" => Value::string(decl.description(), span),
468                    "decl_id" => Value::int(decl_id.get() as i64, span),
469                };
470
471                externals.push(Value::record(record, span))
472            }
473        }
474
475        sort_rows(&mut externals);
476        externals
477    }
478
479    pub fn collect_aliases(&self, span: Span) -> Vec<Value> {
480        let mut aliases = vec![];
481
482        for (decl_name, decl_id) in &self.decls_map {
483            if self.visibility.is_decl_id_visible(decl_id) {
484                let decl = self.engine_state.get_decl(*decl_id);
485                if let Some(alias) = decl.as_alias() {
486                    let aliased_decl_id = if let Expr::Call(wrapped_call) = &alias.wrapped_call.expr
487                    {
488                        Value::int(wrapped_call.decl_id.get() as i64, span)
489                    } else {
490                        Value::nothing(span)
491                    };
492
493                    let expansion = String::from_utf8_lossy(
494                        self.engine_state.get_span_contents(alias.wrapped_call.span),
495                    );
496
497                    aliases.push(Value::record(
498                        record! {
499                            "name" => Value::string(String::from_utf8_lossy(decl_name), span),
500                            "expansion" => Value::string(expansion, span),
501                            "description" => Value::string(alias.description(), span),
502                            "decl_id" => Value::int(decl_id.get() as i64, span),
503                            "aliased_decl_id" => aliased_decl_id,
504                        },
505                        span,
506                    ));
507                }
508            }
509        }
510
511        sort_rows(&mut aliases);
512        aliases
513    }
514
515    fn collect_module(&self, module_name: &[u8], module_id: &ModuleId, span: Span) -> Value {
516        let module = self.engine_state.get_module(*module_id);
517
518        let all_decls = module.decls();
519
520        let mut export_commands: Vec<Value> = all_decls
521            .iter()
522            .filter_map(|(name_bytes, decl_id)| {
523                let decl = self.engine_state.get_decl(*decl_id);
524
525                if !decl.is_alias() && !decl.is_known_external() {
526                    Some(Value::record(
527                        record! {
528                            "name" => Value::string(String::from_utf8_lossy(name_bytes), span),
529                            "decl_id" => Value::int(decl_id.get() as i64, span),
530                        },
531                        span,
532                    ))
533                } else {
534                    None
535                }
536            })
537            .collect();
538
539        let mut export_aliases: Vec<Value> = all_decls
540            .iter()
541            .filter_map(|(name_bytes, decl_id)| {
542                let decl = self.engine_state.get_decl(*decl_id);
543
544                if decl.is_alias() {
545                    Some(Value::record(
546                        record! {
547                            "name" => Value::string(String::from_utf8_lossy(name_bytes), span),
548                            "decl_id" => Value::int(decl_id.get() as i64, span),
549                        },
550                        span,
551                    ))
552                } else {
553                    None
554                }
555            })
556            .collect();
557
558        let mut export_externs: Vec<Value> = all_decls
559            .iter()
560            .filter_map(|(name_bytes, decl_id)| {
561                let decl = self.engine_state.get_decl(*decl_id);
562
563                if decl.is_known_external() {
564                    Some(Value::record(
565                        record! {
566                            "name" => Value::string(String::from_utf8_lossy(name_bytes), span),
567                            "decl_id" => Value::int(decl_id.get() as i64, span),
568                        },
569                        span,
570                    ))
571                } else {
572                    None
573                }
574            })
575            .collect();
576
577        let mut export_submodules: Vec<Value> = module
578            .submodules()
579            .iter()
580            .map(|(name_bytes, submodule_id)| self.collect_module(name_bytes, submodule_id, span))
581            .collect();
582
583        let mut export_consts: Vec<Value> = module
584            .consts()
585            .iter()
586            .map(|(name_bytes, var_id)| {
587                Value::record(
588                    record! {
589                        "name" => Value::string(String::from_utf8_lossy(name_bytes), span),
590                        "type" => Value::string(self.engine_state.get_var(*var_id).ty.to_string(), span),
591                        "var_id" => Value::int(var_id.get() as i64, span),
592                    },
593                    span,
594                )
595            })
596            .collect();
597
598        sort_rows(&mut export_commands);
599        sort_rows(&mut export_aliases);
600        sort_rows(&mut export_externs);
601        sort_rows(&mut export_submodules);
602        sort_rows(&mut export_consts);
603
604        let (module_desc, module_extra_desc) = self
605            .engine_state
606            .build_module_desc(*module_id)
607            .unwrap_or_default();
608
609        Value::record(
610            record! {
611                "name" => Value::string(String::from_utf8_lossy(module_name), span),
612                "commands" => Value::list(export_commands, span),
613                "aliases" => Value::list(export_aliases, span),
614                "externs" => Value::list(export_externs, span),
615                "submodules" => Value::list(export_submodules, span),
616                "constants" => Value::list(export_consts, span),
617                "has_env_block" => Value::bool(module.env_block.is_some(), span),
618                "description" => Value::string(module_desc, span),
619                "extra_description" => Value::string(module_extra_desc, span),
620                "module_id" => Value::int(module_id.get() as i64, span),
621                "file" => Value::string(module.file.clone().map_or("unknown".to_string(), |(p, _)| p.path().to_string_lossy().to_string()), span),
622            },
623            span,
624        )
625    }
626
627    pub fn collect_modules(&self, span: Span) -> Vec<Value> {
628        let mut modules = vec![];
629
630        for (module_name, module_id) in &self.modules_map {
631            modules.push(self.collect_module(module_name, module_id, span));
632        }
633
634        modules.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
635        modules
636    }
637
638    pub fn collect_engine_state(&self, span: Span) -> Value {
639        let num_env_vars = self
640            .engine_state
641            .env_vars
642            .values()
643            .map(|overlay| overlay.len() as i64)
644            .sum();
645
646        let config = self.stack.get_config(self.engine_state);
647        let last_result = Value::record(
648            record! {
649                "name" => Value::string(
650                    format!("${}", nu_protocol::LAST_RESULT_VAR_NAME),
651                    span,
652                ),
653                "size_limit" => Value::filesize(config.max_last_result_size, span),
654                "memory_size" => Value::filesize(
655                    nu_protocol::Filesize::new(self.stack.last_result_memory_size() as i64),
656                    span,
657                ),
658                "truncated" => Value::bool(self.stack.last_result_was_truncated(), span),
659                "has_metadata" => Value::bool(
660                    self.stack.last_result_metadata().is_some(),
661                    span,
662                ),
663            },
664            span,
665        );
666
667        Value::record(
668            record! {
669                "source_bytes" => Value::int(self.engine_state.next_span_start() as i64, span),
670                "num_vars" => Value::int(self.engine_state.num_vars() as i64, span),
671                "num_decls" => Value::int(self.engine_state.num_decls() as i64, span),
672                "num_blocks" => Value::int(self.engine_state.num_blocks() as i64, span),
673                "num_modules" => Value::int(self.engine_state.num_modules() as i64, span),
674                "num_env_vars" => Value::int(num_env_vars, span),
675                "last_result" => last_result,
676            },
677            span,
678        )
679    }
680}
681
682/// Collect VarIds present on the stack (parents first, then current frame).
683///
684/// Mirrors [`Stack`] lookup: walk parents first (skipping `parent_deletions`), then append
685/// current-frame vars. Same-name shadowing is resolved later when building the name→id map
686/// in [`ScopeData::collect_vars`].
687fn stack_var_ids(stack: &Stack) -> Vec<VarId> {
688    let mut ids = Vec::new();
689    collect_stack_var_ids(stack, &mut ids);
690    ids
691}
692
693fn collect_stack_var_ids(stack: &Stack, ids: &mut Vec<VarId>) {
694    if let Some(parent) = &stack.parent_stack {
695        collect_stack_var_ids(parent, ids);
696        ids.retain(|id| !stack.parent_deletions.contains(id));
697    }
698    // `remove_var` already drops entries from `vars`; no need to consult `deletions`.
699    for (var_id, _) in &stack.vars {
700        ids.push(*var_id);
701    }
702}
703
704/// True if `unlet` removed this variable on this stack or any parent.
705fn is_unlet(stack: &Stack, var_id: VarId) -> bool {
706    let mut current = Some(stack);
707    while let Some(s) = current {
708        if s.deletions.contains(&var_id) || s.parent_deletions.contains(&var_id) {
709            return true;
710        }
711        current = s.parent_stack.as_deref();
712    }
713    false
714}
715
716fn sort_rows(decls: &mut [Value]) {
717    decls.sort_by(|a, b| match (a, b) {
718        (Value::Record { val: rec_a, .. }, Value::Record { val: rec_b, .. }) => {
719            // Comparing the first value from the record
720            // It is expected that the first value is the name of the entry (command, module, alias, etc.)
721            match (rec_a.values().next(), rec_b.values().next()) {
722                (Some(val_a), Some(val_b)) => match (val_a, val_b) {
723                    (Value::String { val: str_a, .. }, Value::String { val: str_b, .. }) => {
724                        str_a.cmp(str_b)
725                    }
726                    _ => Ordering::Equal,
727                },
728                _ => Ordering::Equal,
729            }
730        }
731        _ => Ordering::Equal,
732    });
733}
734
735/// Find the first declaration with `CommandType::Builtin` whose name matches `name`,
736/// scanning from the most-recently registered declaration backwards.
737///
738/// This mirrors the static `%name` parser behavior: `%` always resolves to a built-in,
739/// even when a custom declaration shadows the same name in the current scope.
740pub fn find_builtin_decl(engine_state: &EngineState, name: &str) -> Option<DeclId> {
741    for idx in (0..engine_state.num_decls()).rev() {
742        let decl_id = DeclId::new(idx);
743        let decl = engine_state.get_decl(decl_id);
744        if decl.command_type() == CommandType::Builtin && decl.name() == name {
745            return Some(decl_id);
746        }
747    }
748    None
749}