Skip to main content

libperl_rs/
stash.rs

1//! Stash (package symbol table) traversal — enumerate the subs of a
2//! package tree the way `%Foo::` looks in Perl. Step 2's north-star
3//! walker (`docs/plan/README.md` §4 Step 2.3), rebuilt from
4//! libperl-proto0's `eg/stash_walker0.rs` / examples `105`-`107` on
5//! the new newtype layer.
6//!
7//! ```no_run
8//! # use libperl_rs::*;
9//! # let mut perl = Perl::new();
10//! let mut walker = StashWalker::new(&perl);
11//! walker.walk("main", &mut |e| {
12//!     println!("{}::{} from {:?}", e.package, e.name, e.cv.file());
13//! });
14//! ```
15
16use std::collections::HashSet;
17
18use crate::{Cv, Gv, Perl, SvKind};
19
20/// One named sub found in a stash: `package` is the normalized
21/// package path (`"main"`, `"Foo::Bar"`), `name` the symbol name
22/// inside it. `gv` is the glob the CV hangs off, or `None` when the
23/// stash slot held a bare code reference instead of a full glob (the
24/// sub-ref-in-stash optimisation).
25pub struct SubEntry {
26    pub package: String,
27    pub name: String,
28    pub cv: Cv,
29    pub gv: Option<Gv>,
30}
31
32/// Recursive stash walker with cycle protection. Reusable across
33/// multiple [`walk`](StashWalker::walk) calls — the seen-set persists,
34/// so each package is visited at most once per walker.
35pub struct StashWalker<'p> {
36    perl: &'p Perl,
37    seen: HashSet<String>,
38}
39
40impl<'p> StashWalker<'p> {
41    pub fn new(perl: &'p Perl) -> Self {
42        StashWalker {
43            perl,
44            seen: HashSet::new(),
45        }
46    }
47
48    /// Walk the package `pack` (typically `"main"`) and, recursively,
49    /// every sub-package reachable from it, calling `emit` for each
50    /// named sub. Symbols other than subs (scalars, arrays, ...) are
51    /// skipped.
52    pub fn walk(&mut self, pack: &str, emit: &mut dyn FnMut(&SubEntry)) {
53        if !self.seen.insert(pack.to_string()) {
54            return;
55        }
56        let Some(stash) = self.perl.gv_stashpv(pack, 0) else {
57            return;
58        };
59        for (key, val) in stash.iter(self.perl) {
60            let name = String::from_utf8_lossy(key).into_owned();
61            match val.kind() {
62                // `ref $Foo::{bar} eq 'CODE'` — the slot holds a bare
63                // sub ref (constant / stub optimisation), no glob.
64                SvKind::Ref(target) => {
65                    if let SvKind::Code(cv) = target.kind() {
66                        emit(&SubEntry {
67                            package: pack.to_string(),
68                            name,
69                            cv,
70                            gv: None,
71                        });
72                    }
73                }
74                SvKind::Glob(gv) => {
75                    if let Some(cv) = gv.cv() {
76                        emit(&SubEntry {
77                            package: pack.to_string(),
78                            name: name.clone(),
79                            cv,
80                            gv: Some(gv),
81                        });
82                    }
83                    // A trailing-`::` glob is a sub-package: recurse.
84                    if let Some(base) = name.strip_suffix("::") {
85                        if !base.is_empty() {
86                            let child = if pack == "main" {
87                                base.to_string()
88                            } else {
89                                format!("{pack}::{base}")
90                            };
91                            self.walk(&child, emit);
92                        }
93                    }
94                }
95                _ => {}
96            }
97        }
98    }
99}