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}