1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
//! Bash sparse-array support — a Rust-only extension with NO zsh C
//! counterpart.
//!
//! zsh (and zshrs) arrays are DENSE `Vec<String>`: `a[5]=q` on a 3-element
//! array pads indices 3,4 with empty strings, so `${#a[@]}` is 6. bash
//! arrays are SPARSE maps: the same op leaves indices {0,1,2,5}, so
//! `${#a[@]}` is 4, `${!a[@]}` is `0 1 2 5`, and `unset a[1]` removes index
//! 1 leaving {0,2}.
//!
//! Rather than change the dense `Vec<String>` representation the whole
//! param system (and `--zsh`, the primary target) depends on, this tracks
//! the "holes" — the indices that are padding artifacts or were `unset`,
//! and therefore NOT real elements — in a side table keyed by array name.
//! It is consulted only in `--bash` mode; `${#}`/`${!}`/`${@}` subtract the
//! holes, subscript-assign/append/full-reassign maintain them, and `unset
//! a[i]` marks a hole. Everything else keeps operating on the dense Vec.
use std::cell::RefCell;
use std::collections::{BTreeSet, HashMap};
thread_local! {
/// name → set of HOLE indices (padding / unset slots that are not real
/// elements). Absent name ⇒ fully dense (no holes).
static HOLES: RefCell<HashMap<String, BTreeSet<usize>>> = RefCell::new(HashMap::new());
}
/// Clear all hole tracking for `name` — the array became fully dense
/// (a plain `a=(...)` reassign, `unset a`, or a non-bash mutation).
pub fn clear(name: &str) {
HOLES.with(|h| {
h.borrow_mut().remove(name);
});
}
/// Record that a subscript assignment set index `i`, padding the dense Vec
/// from `old_len` up to `i`. Indices `old_len..i` become holes; `i` itself
/// is a real element (removed from the hole set).
pub fn note_subscript_set(name: &str, old_len: usize, i: usize) {
HOLES.with(|h| {
let mut map = h.borrow_mut();
let set = map.entry(name.to_string()).or_default();
if i >= old_len {
for k in old_len..i {
set.insert(k);
}
}
set.remove(&i);
if set.is_empty() {
map.remove(name);
}
});
}
/// Mark index `i` a hole (an `unset a[i]` that leaves a gap rather than a
/// dense empty). Returns nothing; callers keep the dense Vec's slot as "".
pub fn note_unset(name: &str, i: usize) {
HOLES.with(|h| {
h.borrow_mut()
.entry(name.to_string())
.or_default()
.insert(i);
});
}
/// Record every slot of a freshly-written `0..len` array that the literal
/// did NOT name as a hole.
///
/// An explicit-index array literal is sparse in bash: `a=([2]=x [5]=y)`
/// holds indices {2,5}, not a dense 0..5 with empties. The dense `Vec`
/// underneath has already been written by the caller's setter; this marks
/// the slots between the named ones, which is what `${a[@]}`, `${!a[@]}`
/// and `${#a[@]}` consult.
///
/// Shared by all three literal paths — the `a=(…)` builtin, `declare -a`'s
/// bracket form, and `declare -a`'s Marker-triad form — so they cannot
/// drift apart the way the first two did: the triad form had no marking at
/// all, and `declare -a a=([5]=x [10]=y)` came out dense with eleven
/// elements where bash reports two.
pub fn note_holes_outside(name: &str, explicit: &BTreeSet<usize>, len: usize) {
for i in 0..len {
if !explicit.contains(&i) {
note_unset(name, i);
}
}
}
/// Drop any hole indices >= `new_len` — the array shrank (truncation), so
/// trailing holes no longer exist.
pub fn truncate(name: &str, new_len: usize) {
HOLES.with(|h| {
let mut map = h.borrow_mut();
if let Some(set) = map.get_mut(name) {
set.retain(|&k| k < new_len);
if set.is_empty() {
map.remove(name);
}
}
});
}
/// The hole indices for `name` (empty if dense).
pub fn holes(name: &str) -> BTreeSet<usize> {
HOLES.with(|h| h.borrow().get(name).cloned().unwrap_or_default())
}
/// True if `name` has any tracked holes.
pub fn has_holes(name: &str) -> bool {
HOLES.with(|h| h.borrow().get(name).map_or(false, |s| !s.is_empty()))
}
/// The live (non-hole) indices of a dense Vec of length `len` for `name`,
/// in ascending order. With no holes this is simply `0..len`.
pub fn live_indices(name: &str, len: usize) -> Vec<usize> {
let hs = holes(name);
(0..len).filter(|k| !hs.contains(k)).collect()
}
/// The live element COUNT for a dense Vec of length `dense_len` — `dense_len`
/// minus the holes below it. Inherently a no-op (`== dense_len`) outside
/// `--bash` mode, where no holes are ever recorded. Backs `${#a[@]}`.
pub fn live_len(name: &str, dense_len: usize) -> usize {
HOLES.with(|h| match h.borrow().get(name) {
Some(set) => dense_len - set.iter().filter(|&&k| k < dense_len).count(),
None => dense_len,
})
}
/// Filter a dense whole-array Vec down to its live elements, dropping the
/// hole slots (padding / `unset` gaps). Order-preserving. No-op outside
/// `--bash` (no holes tracked). Backs the `${a[@]}` / `${a[*]}` splat.
pub fn compact(name: &str, dense: Vec<String>) -> Vec<String> {
let hs = holes(name);
if hs.is_empty() {
return dense;
}
dense
.into_iter()
.enumerate()
.filter(|(k, _)| !hs.contains(k))
.map(|(_, v)| v)
.collect()
}