zsh/extensions/subexp_cleanup.rs
1//! Rust-only cleanup guard for the transient `__subexp_arr_*` array
2//! temporaries that `paramsubst` (src/ported/subst.rs) stashes in the
3//! global paramtab while expanding an array sub-expression
4//! (`${(s: :)$(cmd)}`, `${${(s: :)v}[N]}`, …).
5//!
6//! In zsh C the sub-expression array lives in the per-paramsubst
7//! `Value` struct (`v.isarr = 1; aval = …`) and never touches the
8//! parameter hash, so it is invisible to `typeset -p`. zshrs reuses
9//! the canonical paramtab as scratch so the existing splat / subscript
10//! / filter code can resolve the temp through the normal var-lookup
11//! paths — but without cleanup that leaks `__subexp_arr_N` into
12//! user-visible state (it shows up in `typeset -p`, in `${(k)parameters}`,
13//! etc.). This guard evicts every temp its owning `paramsubst` call
14//! created when that call returns, on ALL paths — including the many
15//! early error returns and the recursive nested-`${…}` returns —
16//! restoring the C-visible behaviour that nothing leaks.
17//!
18//! This is deliberately NOT a `src/ported` helper: there is no zsh C
19//! function it corresponds to. It is pure zshrs bookkeeping that exists
20//! only because the port reuses the global paramtab as scratch, so it
21//! lives under `src/extensions` per the project's Rust-only-code rule.
22
23/// Tracks the `__subexp_arr_*` temp array names created during a single
24/// `paramsubst` invocation and evicts them from the global paramtab on
25/// `Drop` (i.e. when the invocation returns, on every code path).
26pub struct SubexpTempGuard {
27 names: Vec<String>,
28}
29
30impl SubexpTempGuard {
31 #[inline]
32 pub fn new() -> Self {
33 Self { names: Vec::new() }
34 }
35
36 /// Register a temp array name for eviction when this guard drops.
37 #[inline]
38 pub fn track(&mut self, name: String) {
39 self.names.push(name);
40 }
41}
42
43impl Default for SubexpTempGuard {
44 #[inline]
45 fn default() -> Self {
46 Self::new()
47 }
48}
49
50impl Drop for SubexpTempGuard {
51 fn drop(&mut self) {
52 if self.names.is_empty() {
53 return;
54 }
55 // Direct paramtab eviction (not `unsetparam`): these temps are
56 // plain `PM_ARRAY` nodes with no gsu / special-unset hooks, so a
57 // straight `remove` is both correct and free of side effects.
58 if let Ok(mut tab) = crate::ported::params::paramtab().write() {
59 for n in &self.names {
60 tab.remove(n);
61 }
62 }
63 }
64}