zsh/ported/hashtable.rs
1//! Hash table implementations - port of hashtable.c
2//!
3//! Provides hash tables for commands, shell functions, reserved words, aliases,
4//! and history. The four tables whose iteration order is user-visible
5//! (`cmdnamtab`, `shfunctab`, `reswdtab`, `aliastab`/`sufaliastab`)
6//! store their nodes in `hashtable_nodes`, a port of the node-storage
7//! half of C's `struct hashtable` (`Src/zsh.h:1175-1235`), so
8//! `${(k)commands}` / `${(k)functions}` / `$reswords` /
9//! `${(k)aliases}` come out in C's bucket-walk order.
10//!
11//! `cmdnam_table` / `shfunc_table` / `reswd_table` / `alias_table` are
12//! Rust-side typed wrappers. C uses one polymorphic `struct hashtable`
13//! (`Src/zsh.h:1175-1235`) with function-pointer callbacks per table
14//! kind; the canonical Rust port of that struct lives at
15//! `zsh_h.rs:532`. These typed wrappers add fields that aren't part
16//! of `struct hashtable` (e.g. `cmdnam_table` carries
17//! `path_checked_index` + `path` + `hash_executables_only` for the
18//! `PATH`-walk fast-rehash that C tracks via the file-scope
19//! `pathchecked` / `hashed_anything` statics in `Src/hashtable.c`).
20//! Lowercase naming reflects that the wrappers are co-located with
21//! the canonical `hashtable` rather than mirror it 1:1.
22
23#![allow(non_camel_case_types)]
24
25use crate::compat::zgetcwd;
26use crate::hist::{hashchar, hist_ring};
27use crate::jobs::getsigidx;
28use crate::ported::hist::{hist_ignore_all_dups, histlinect, histremovedups, up_histent};
29use crate::ported::pattern::{patcompile, pattry};
30use crate::ported::signals::removetrap;
31use crate::ported::utils::scriptfilename_get;
32use crate::ported::zsh_h::{
33 alias, options, BANG_TOK, CASE, COPROC, DINBRACK, DOLOOP, DONE, ELIF, ELSE, ESAC, FI, FOR,
34 FOREACH, FUNC, IF, INBRACE_TOK, NOCORRECT, OUTBRACE_TOK, PAT_HEAPDUP, REPEAT, SELECT, THEN,
35 TIME, TYPESET, UNTIL, WHILE, ZEND,
36};
37use crate::signals::{settrap, unsettrap};
38use crate::text::{getpermtext, zoutputtab};
39use crate::utils::{nicezputs, quotedzputs, unmetafy_str, xsymlink, zputs, ztrcmp, zwarn};
40use crate::zsh_h::{
41 cmdnam, hashnode, hashtable, HashNode, HashTable, reswd, shfunc, ALIAS_GLOBAL, ALIAS_SUFFIX, DISABLED, EF_RUN,
42 HASHED, HIST_DUP, HIST_FOREIGN, HIST_MAKEUNIQUE, HIST_TMPSTORE, PM_CUR_FPATH, PM_KSHSTORED,
43 PM_LOADDIR, PM_TAGGED, PM_TAGGED_LOCAL, PM_UNALIASED, PM_UNDEFINED, PM_ZSHSTORED, PRINT_LIST,
44 PRINT_NAMEONLY, PRINT_WHENCE_CSH, PRINT_WHENCE_FUNCDEF, PRINT_WHENCE_SIMPLE,
45 PRINT_WHENCE_VERBOSE, PRINT_WHENCE_WORD, ZSIG_FUNC,
46};
47use std::collections::HashMap;
48use std::fs;
49use std::io;
50use std::os::unix::fs::PermissionsExt;
51use std::path::PathBuf;
52use std::sync::atomic::Ordering;
53
54/// Generic hash function (zsh's hasher)
55/// Compute the canonical zsh hash for a string.
56/// Port of `hasher(const char *str)` from Src/hashtable.c:86 — uses the same
57// Generic hash function // c:86
58/// `hash * 33 + char` polynomial the C source uses for every
59/// HashTable lookup.
60pub fn hasher(str: &str) -> u32 {
61 // c:86
62 let mut hashval: u32 = 0;
63 for c in str.bytes() {
64 hashval = hashval.wrapping_add(hashval.wrapping_shl(5).wrapping_add(c as u32));
65 }
66 hashval
67}
68
69/// Port of the node-storage half of `struct hashtable`
70/// (`Src/zsh.h:1175-1235`: `HashNode *nodes; int hsize; int ct;`) as
71/// `Src/hashtable.c` actually drives it — an OPEN-HASHED BUCKET ARRAY:
72///
73/// * bucket index is `ht->hash(nam) % ht->hsize` (c:176/236/260/280);
74/// * a new key goes to the FRONT of its chain (c:194-196 for an empty
75/// bucket, c:214-215 otherwise);
76/// * replacing an existing key keeps that key's POSITION in the chain
77/// (c:187-203 `replacing:` — `hn->next = hp->next`);
78/// * once `ct >= hsize * 2` the table quadruples and every node is
79/// re-added in old-traversal order (c:183/219 → `expandhashtable`
80/// c:458-482);
81/// * an unsorted scan walks bucket 0..hsize-1, each chain head→tail
82/// (`scanmatchtable` c:420-434).
83///
84/// That walk order is OBSERVABLE: `${(k)functions}` /
85/// `${(k)commands}` / `compadd -k <assoc>` all emit it verbatim
86/// (`Src/Modules/parameter.c:480-481` loops `shfunctab->nodes[i]`
87/// directly), and `join_clines` is a non-commutative fold, so the
88/// order matches are added in decides the common prefix `compadd -k`
89/// produces.
90///
91/// The port previously stood a `std::collections::HashMap` in for the
92/// bucket array. That is not merely a different order — `RandomState`
93/// re-seeds per process, so `print -rl -- ${(k)functions}` returned a
94/// DIFFERENT order on every run of the same binary against the same
95/// 46761-function table, and any completion enumerating the table was
96/// nondeterministic.
97///
98/// !!! WARNING: RUST-ONLY HELPER !!!
99/// C reaches these fields through one polymorphic `struct hashtable`
100/// with `hash`/`cmpnodes`/`addnode`/`getnode` function pointers. Rust
101/// has no equivalent of the `HashNode`-as-first-member downcast, so the
102/// storage is a generic struct and the typed wrappers below embed it.
103/// Every method is named for the `Src/hashtable.c` function it ports.
104#[derive(Debug, Clone)]
105pub struct hashtable_nodes<T> {
106 /// `HashNode *nodes` (`Src/zsh.h:1177`) — `hsize` chains. Index 0 of
107 /// a chain is the head, i.e. the most recently inserted key.
108 nodes: Vec<Vec<(String, T)>>,
109 /// `int hsize` (`Src/zsh.h:1179`) — number of buckets.
110 hsize: usize,
111 /// `int ct` (`Src/zsh.h:1181`) — number of live nodes.
112 ct: usize,
113}
114
115impl<T> hashtable_nodes<T> {
116 /// Port of `newhashtable(int size, …)` from `Src/hashtable.c:100` —
117 /// `zshcalloc(size * sizeof(HashNode))` + `hsize = size` + `ct = 0`.
118 pub fn newhashtable(size: usize) -> Self {
119 // c:100
120 let size = size.max(1); // c:115 — a 0-bucket table can't be indexed
121 let mut nodes = Vec::with_capacity(size);
122 nodes.resize_with(size, Vec::new); // c:116
123 Self {
124 nodes,
125 hsize: size, // c:117
126 ct: 0, // c:118
127 }
128 }
129
130 /// Port of `expandhashtable(HashTable ht)` from `Src/hashtable.c:458`.
131 /// Quadruples `hsize`, zeroes `ct`, and re-adds every node walking the
132 /// OLD table in traversal order (bucket 0..osize-1, chain head→tail)
133 /// so the new chains come out in C's exact order.
134 fn expandhashtable(&mut self) {
135 // c:458
136 let osize = self.hsize; // c:463
137 let onodes = std::mem::take(&mut self.nodes); // c:464
138 self.hsize = osize * 4; // c:466
139 self.nodes = Vec::with_capacity(self.hsize);
140 self.nodes.resize_with(self.hsize, Vec::new); // c:467
141 self.ct = 0; // c:468
142 // c:471-476 — `for (i = 0, ha = onodes; i < osize; i++, ha++)
143 // for (hn = *ha; hn;) { hp = hn->next;
144 // ht->addnode(ht, hn->nam, hn); hn = hp; }`
145 for bucket in onodes {
146 for (nam, node) in bucket {
147 let hashval = hasher(&nam) as usize % self.hsize; // c:176
148 self.nodes[hashval].insert(0, (nam, node)); // c:214-215
149 self.ct += 1; // c:219 (the expand test can't re-fire here)
150 }
151 }
152 }
153
154 /// Port of `addhashnode2(HashTable ht, char *nam, void *nodeptr)` from
155 /// `Src/hashtable.c:168` — inserts, returning the displaced node.
156 pub fn addhashnode2(&mut self, nam: &str, nodeptr: T) -> Option<T> {
157 // c:168
158 let hashval = hasher(nam) as usize % self.hsize; // c:176
159 // c:186-206 — an existing key is replaced IN PLACE, keeping its
160 // position in the chain; ct does not move and no expand fires.
161 if let Some(pos) = self.nodes[hashval].iter().position(|(k, _)| k == nam) {
162 let old = std::mem::replace(&mut self.nodes[hashval][pos], (nam.to_string(), nodeptr));
163 return Some(old.1); // c:203
164 }
165 // c:193-196 / c:214-215 — otherwise the new node goes to the FRONT.
166 self.nodes[hashval].insert(0, (nam.to_string(), nodeptr));
167 self.ct += 1;
168 if self.ct >= self.hsize * 2 {
169 // c:183 / c:219
170 self.expandhashtable(); // c:184 / c:220
171 }
172 None
173 }
174
175 /// Port of `gethashnode2(HashTable ht, const char *nam)` from
176 /// `Src/hashtable.c:255` — lookup WITHOUT the DISABLED filter.
177 pub fn gethashnode2(&self, nam: &str) -> Option<&T> {
178 // c:255
179 let hashval = hasher(nam) as usize % self.hsize; // c:260
180 self.nodes[hashval]
181 .iter()
182 .find(|(k, _)| k == nam) // c:262-265
183 .map(|(_, v)| v)
184 }
185
186 /// Mutable companion of [`hashtable_nodes::gethashnode2`]. C mutates
187 /// straight through the returned `HashNode` pointer; Rust needs the
188 /// separate borrow.
189 pub fn get_mut(&mut self, nam: &str) -> Option<&mut T> {
190 // c:255
191 let hashval = hasher(nam) as usize % self.hsize;
192 self.nodes[hashval]
193 .iter_mut()
194 .find(|(k, _)| k == nam)
195 .map(|(_, v)| v)
196 }
197
198 /// Port of `removehashnode(HashTable ht, const char *nam)` from
199 /// `Src/hashtable.c:275` — unlinks the node from its chain and
200 /// decrements `ct`.
201 pub fn removehashnode(&mut self, nam: &str) -> Option<T> {
202 // c:275
203 let hashval = hasher(nam) as usize % self.hsize; // c:280
204 let pos = self.nodes[hashval].iter().position(|(k, _)| k == nam)?;
205 self.ct -= 1; // c:294
206 Some(self.nodes[hashval].remove(pos).1)
207 }
208
209 /// Port of `emptyhashtable(HashTable ht)` from `Src/hashtable.c:517`
210 /// (`resizehashtable(ht, ht->hsize)`) — frees every node, keeps `hsize`.
211 pub fn emptyhashtable(&mut self) {
212 // c:517
213 for bucket in self.nodes.iter_mut() {
214 bucket.clear(); // c:490-497
215 }
216 self.ct = 0; // c:509
217 }
218
219 /// C's `ht->ct` (`Src/zsh.h:1181`).
220 pub fn len(&self) -> usize {
221 self.ct
222 }
223
224 /// `ht->ct == 0`.
225 pub fn is_empty(&self) -> bool {
226 self.ct == 0
227 }
228
229 /// The unsorted scan order of `scanmatchtable` (`Src/hashtable.c:420-434`):
230 /// bucket 0..hsize-1, each chain head→tail. This IS the order zsh's
231 /// `${(k)assoc}` / `compadd -k` emit.
232 pub fn iter(&self) -> impl Iterator<Item = (&String, &T)> {
233 // c:420-434
234 self.nodes.iter().flatten().map(|(k, v)| (k, v))
235 }
236
237 /// Mutable companion of [`hashtable_nodes::iter`] — same
238 /// `scanmatchtable` walk (`Src/hashtable.c:420-434`); C hands the
239 /// scan function a writable `HashNode`, Rust needs the separate
240 /// borrow.
241 pub fn iter_mut(&mut self) -> impl Iterator<Item = (&String, &mut T)> {
242 // c:420-434
243 self.nodes.iter_mut().flatten().map(|(k, v)| (&*k, v))
244 }
245
246 /// The names in `scanmatchtable` order (`Src/hashtable.c:420-434`) —
247 /// C's scan reads `hn->nam` off each node in the same walk.
248 pub fn keys(&self) -> impl Iterator<Item = &String> {
249 // c:420-434
250 self.nodes.iter().flatten().map(|(k, _)| k)
251 }
252
253 /// The nodes in `scanmatchtable` order (`Src/hashtable.c:420-434`).
254 pub fn values(&self) -> impl Iterator<Item = &T> {
255 // c:420-434
256 self.nodes.iter().flatten().map(|(_, v)| v)
257 }
258
259 /// Mutable companion of [`hashtable_nodes::values`]
260 /// (`Src/hashtable.c:420-434`).
261 pub fn values_mut(&mut self) -> impl Iterator<Item = &mut T> {
262 // c:420-434
263 self.nodes.iter_mut().flatten().map(|(_, v)| v)
264 }
265
266 /// `ht->getnode2(ht, nam)` (`Src/hashtable.c:255` `gethashnode2`) —
267 /// map-shaped alias so tables converted from a `HashMap` keep their
268 /// call sites. Same O(1) bucket hash + short-chain walk as C.
269 pub fn get(&self, nam: &str) -> Option<&T> {
270 // c:255
271 self.gethashnode2(nam)
272 }
273
274 /// `ht->getnode2(ht, nam)` != NULL (`Src/hashtable.c:255`).
275 pub fn contains_key(&self, nam: &str) -> bool {
276 // c:255
277 self.gethashnode2(nam).is_some()
278 }
279
280 /// `ht->addnode(ht, ztrdup(nam), node)` (`Src/hashtable.c:157`
281 /// `addhashnode` → `addhashnode2` at `c:168`) — returns the
282 /// displaced node instead of running `freenode`.
283 pub fn insert(&mut self, nam: String, nodeptr: T) -> Option<T> {
284 // c:157 / c:168
285 self.addhashnode2(&nam, nodeptr)
286 }
287
288 /// `ht->removenode(ht, nam)` (`Src/hashtable.c:275`
289 /// `removehashnode`).
290 pub fn remove(&mut self, nam: &str) -> Option<T> {
291 // c:275
292 self.removehashnode(nam)
293 }
294
295 /// `ht->emptytable(ht)` (`Src/hashtable.c:517` `emptyhashtable`).
296 pub fn clear(&mut self) {
297 // c:517
298 self.emptyhashtable();
299 }
300
301 /// The `scanmatchtable` walk (`Src/hashtable.c:420-434`) with the
302 /// C `freenode` arm taken for every node the predicate rejects.
303 /// Chain order of the survivors is preserved exactly, which is what
304 /// makes this different from rebuilding the table.
305 pub fn retain<F: FnMut(&String, &mut T) -> bool>(&mut self, mut f: F) {
306 // c:420-434
307 let mut ct = 0usize;
308 for bucket in self.nodes.iter_mut() {
309 bucket.retain_mut(|(k, v)| f(k, v));
310 ct += bucket.len();
311 }
312 self.ct = ct; // c:294 — one decrement per unlinked node
313 }
314}
315
316impl<T> Default for hashtable_nodes<T> {
317 /// `newparamtable`/`newmoduletable` fall back to `size = 17` when
318 /// handed 0 (`Src/params.c:541-542`, and `Src/module.c:1602`
319 /// creates 17-bucket sub-tables); use that as the neutral default
320 /// for `#[derive(Default)]` containers.
321 fn default() -> Self {
322 // c:541-542
323 Self::newhashtable(17)
324 }
325}
326
327impl<T, Q: ?Sized + std::borrow::Borrow<str>> std::ops::Index<&Q> for hashtable_nodes<T> {
328 type Output = T;
329 /// `ht->getnode2(ht, nam)` with C's "caller already checked" contract
330 /// (`Src/hashtable.c:255`) — panics on a missing name, like the
331 /// `HashMap` indexing it replaces.
332 fn index(&self, nam: &Q) -> &T {
333 // c:255
334 let nam = nam.borrow();
335 self.gethashnode2(nam)
336 .unwrap_or_else(|| panic!("no hash node named {nam}"))
337 }
338}
339
340impl<'a, T> IntoIterator for &'a hashtable_nodes<T> {
341 type Item = (&'a String, &'a T);
342 type IntoIter = std::iter::Map<
343 std::iter::Flatten<std::slice::Iter<'a, Vec<(String, T)>>>,
344 fn(&'a (String, T)) -> (&'a String, &'a T),
345 >;
346 /// `for (k, v) in &table` — the `scanmatchtable` walk
347 /// (`Src/hashtable.c:420-434`).
348 fn into_iter(self) -> Self::IntoIter {
349 // c:420-434
350 self.nodes
351 .iter()
352 .flatten()
353 .map(|(k, v): &(String, T)| (k, v))
354 }
355}
356
357// ===========================================================
358// Direct ports of the generic `HashTable` lifecycle / mutation /
359// printer routines from Src/hashtable.c. The Rust port stores
360// command/alias/reswd/shfunc tables as `HashMap`-backed wrappers
361// (above), so most of these are free-fn shims for ABI/name
362// parity. Callers in the Rust executor reach the live state via
363// the typed table structs (`alias_table`, `shfunc_table`, etc.).
364// ===========================================================
365
366/// Port of `newhashtable(int size, UNUSED(char const *name), UNUSED(PrintTableStats printinfo))` from `Src/hashtable.c:100`.
367///
368/// C `zshcalloc`s the header (every function-pointer slot NULL), allocates
369/// `size` zeroed bucket heads and sets `hsize = size`, `ct = 0`,
370/// `scantab = NULL`. The caller (e.g. `newzstyletable`, `newparamtable`)
371/// then installs its own `hash`/`addnode`/`getnode`/... slots.
372// Get a new hash table // c:100
373/// WARNING: param names don't match C — Rust=(size, name) vs C=(size, name, printinfo)
374pub fn newhashtable(size: i32, name: &str) -> HashTable {
375 // c:100
376 let _ = name; // c:100 UNUSED(name) outside ZSH_HASH_DEBUG
377 let mut nodes: Vec<Option<HashNode>> = Vec::new();
378 nodes.resize_with(size.max(0) as usize, || None); // c:121 zshcalloc(size * sizeof(HashNode))
379 Box::new(hashtable {
380 hsize: size, // c:122
381 ct: 0, // c:123
382 nodes, // c:121
383 tmpdata: 0,
384 hash: None,
385 emptytable: None,
386 filltable: None,
387 cmpnodes: None,
388 addnode: None,
389 getnode: None,
390 getnode2: None,
391 removenode: None,
392 disablenode: None,
393 enablenode: None,
394 freenode: None,
395 printnode: None,
396 scantab: None, // c:125
397 })
398}
399
400/// The `Src/hashtable.c` generic table methods, ported over the C-shaped
401/// [`hashtable`] header (`Src/zsh.h:1200-1222`): bucket array of
402/// `HashNode` chains plus the function-pointer slots. Each is an
403/// associated fn with no receiver so that `hashtable::addhashnode` etc.
404/// coerce to the `AddNodeFunc`/`GetNodeFunc`/... slot types, the way
405/// `ht->addnode = addhashnode;` assigns the C function.
406///
407/// C's `void *nodeptr` is `usize` here (`AddNodeFunc`): the address of a
408/// `Box<hashnode>` obtained with `Box::into_raw`.
409impl hashtable {
410 /// Port of `addhashnode(HashTable ht, char *nam, void *nodeptr)` from `Src/hashtable.c:157`.
411 pub fn addhashnode(ht: &mut hashtable, nam: String, nodeptr: usize) {
412 // c:157
413 let oldnode = hashtable::addhashnode2(ht, nam, nodeptr); // c:159
414 if let Some(oldnode) = oldnode {
415 // c:160
416 if let Some(freenode) = ht.freenode {
417 freenode(oldnode); // c:161
418 }
419 }
420 }
421
422 /// Port of `addhashnode2(HashTable ht, char *nam, void *nodeptr)` from `Src/hashtable.c:168`.
423 /// Adds a node, returning the old node on replacement. A replaced key keeps
424 /// its chain position (c:187-203); a new key goes to the front (c:214-215).
425 pub fn addhashnode2(ht: &mut hashtable, nam: String, nodeptr: usize) -> Option<HashNode> {
426 // c:168
427 // c:174 — `hn = (HashNode) nodeptr; hn->nam = nam;`
428 // SAFETY: callers pass `Box::into_raw(Box<hashnode>) as usize`.
429 let mut hn: HashNode = unsafe { Box::from_raw(nodeptr as *mut hashnode) };
430 hn.nam = nam;
431
432 let hash = ht.hash.expect("hashtable.hash unset");
433 let cmpnodes = ht.cmpnodes.expect("hashtable.cmpnodes unset");
434 let hashval = (hash(&hn.nam) % ht.hsize as u32) as usize; // c:177
435
436 // Detach the chain so it can be walked and re-linked.
437 let mut chain: Vec<HashNode> = Vec::new();
438 let mut cur = ht.nodes[hashval].take(); // c:178 hp = ht->nodes[hashval]
439 while let Some(mut n) = cur {
440 cur = n.next.take();
441 chain.push(n);
442 }
443
444 // c:182-209 — first-node and rest-of-chain key checks.
445 let old = match chain.iter().position(|n| cmpnodes(&n.nam, &hn.nam) == 0) {
446 Some(i) => Some(std::mem::replace(&mut chain[i], hn)), // c:187-203 replacing:
447 None => {
448 chain.insert(0, hn); // c:214-215
449 None
450 }
451 };
452
453 let mut head: Option<HashNode> = None;
454 for mut n in chain.into_iter().rev() {
455 n.next = head;
456 head = Some(n);
457 }
458 ht.nodes[hashval] = head;
459
460 if old.is_none() {
461 ht.ct += 1; // c:218
462 if ht.ct >= ht.hsize * 2 {
463 // c:219
464 hashtable::expandhashtable(ht); // c:220
465 }
466 }
467 old // c:203 / c:221
468 }
469
470 /// Port of `gethashnode2(HashTable ht, const char *nam)` from `Src/hashtable.c:255`.
471 /// Looks the key up ignoring the DISABLED flag; the returned node is a copy of
472 /// the chain entry (`next` cleared).
473 pub fn gethashnode2(ht: &hashtable, nam: &str) -> Option<HashNode> {
474 // c:255
475 let hash = ht.hash?;
476 let cmpnodes = ht.cmpnodes?;
477 let hashval = (hash(nam) % ht.hsize as u32) as usize; // c:260
478 let mut hp = ht.nodes.get(hashval)?.as_ref(); // c:261
479 while let Some(n) = hp {
480 if cmpnodes(&n.nam, nam) == 0 {
481 // c:262
482 return Some(Box::new(hashnode {
483 next: None,
484 nam: n.nam.clone(),
485 flags: n.flags,
486 })); // c:263
487 }
488 hp = n.next.as_ref(); // c:261
489 }
490 None // c:265
491 }
492
493 /// Port of `removehashnode(HashTable ht, const char *nam)` from `Src/hashtable.c:275`.
494 pub fn removehashnode(ht: &mut hashtable, nam: &str) -> Option<HashNode> {
495 // c:275
496 let hash = ht.hash?;
497 let cmpnodes = ht.cmpnodes?;
498 let hashval = (hash(nam) % ht.hsize as u32) as usize; // c:280
499
500 let mut chain: Vec<HashNode> = Vec::new();
501 let mut cur = ht.nodes.get_mut(hashval)?.take(); // c:281 hp = ht->nodes[hashval]
502 while let Some(mut n) = cur {
503 cur = n.next.take();
504 chain.push(n);
505 }
506 let removed = chain
507 .iter()
508 .position(|n| cmpnodes(&n.nam, nam) == 0) // c:285 / c:301
509 .map(|i| chain.remove(i));
510
511 let mut head: Option<HashNode> = None;
512 for mut n in chain.into_iter().rev() {
513 n.next = head;
514 head = Some(n);
515 }
516 ht.nodes[hashval] = head;
517
518 if removed.is_some() {
519 ht.ct -= 1; // c:289 gotit: ht->ct--
520 }
521 removed // c:299 / c:312
522 }
523
524 /// Port of `expandhashtable(HashTable ht)` from `Src/hashtable.c:458`.
525 /// Quadruples `hsize` and re-adds every node via `ht->addnode`, walking the
526 /// old table in bucket order.
527 pub fn expandhashtable(ht: &mut hashtable) {
528 // c:458
529 let osize = ht.hsize; // c:463
530 let onodes = std::mem::take(&mut ht.nodes); // c:464
531
532 ht.hsize = osize * 4; // c:466
533 ht.nodes.resize_with(ht.hsize as usize, || None); // c:467
534 ht.ct = 0; // c:468
535
536 let addnode = ht.addnode.expect("hashtable.addnode unset");
537 for ha in onodes {
538 // c:471
539 let mut hn = ha; // c:472
540 while let Some(mut n) = hn {
541 hn = n.next.take(); // c:473 hp = hn->next
542 let nam = n.nam.clone();
543 addnode(ht, nam, Box::into_raw(n) as usize); // c:474
544 }
545 }
546 }
547
548 /// Port of `resizehashtable(HashTable ht, int newsize)` from `Src/hashtable.c:486`.
549 /// Frees every node through `ht->freenode`, then re-zeroes (or reallocates) the
550 /// bucket array.
551 pub fn resizehashtable(ht: &mut hashtable, newsize: i32) {
552 // c:486
553 let freenode = ht.freenode;
554 for ha in ht.nodes.iter_mut() {
555 // c:493
556 let mut hn = ha.take();
557 while let Some(mut n) = hn {
558 hn = n.next.take(); // c:495 hp = hn->next
559 if let Some(freenode) = freenode {
560 freenode(n); // c:496
561 }
562 }
563 }
564 // c:502-511 — new size: reallocate; same size: re-zero.
565 ht.nodes.clear();
566 ht.nodes.resize_with(newsize.max(0) as usize, || None);
567 ht.hsize = newsize;
568 ht.ct = 0; // c:513
569 }
570
571 /// Port of `emptyhashtable(HashTable ht)` from `Src/hashtable.c:519`.
572 pub fn emptyhashtable(ht: &mut hashtable) {
573 // c:519
574 let hsize = ht.hsize;
575 hashtable::resizehashtable(ht, hsize); // c:521
576 }
577}
578
579/// Port of `deletehashtable(HashTable ht)` from `Src/hashtable.c:129`.
580///
581/// C frees every node via `emptytable` then frees the header.
582/// Rust port: `Drop` runs the equivalent on the typed table when
583/// it falls out of scope. The free fn here calls clear on the
584/// passed map for C name parity at call sites that explicitly
585/// invoke deletehashtable.
586pub fn deletehashtable<T>(ht: &mut HashMap<String, T>) {
587 // c:129
588 ht.clear();
589}
590
591// `cmdnam` struct + impl deleted — Rust-only duplicate of canonical
592// `crate::ported::zsh_h::cmdnam` (zsh.h:1301-1308). C struct:
593//
594// struct cmdnam {
595// struct hashnode node;
596// union {
597// char **name; /* HASHED off: full $PATH array (u.name) */
598// char *cmd; /* HASHED on: resolved abs path (u.cmd) */
599// } u;
600// };
601//
602// The Rust-only version had a flat `name, flags, path: PathBuf,
603// dir_index` shape that lost the hashnode embedding and the
604// name/cmd union (the C source uses `flags & HASHED` to dispatch
605// which arm holds the value). Type alias surfaces the canonical
606// struct directly; the previous `path: PathBuf` becomes
607// `cmd: Option<String>` and `dir_index: Option<usize>` becomes
608// `name: Option<Vec<String>>` (the full PATH-segment slice the
609// command would be looked up against).
610// c:1301
611
612/// Port of `addhashnode(HashTable ht, char *nam, void *nodeptr)` from `Src/hashtable.c:157`.
613///
614/// C body:
615/// ```c
616/// HashNode oldnode = addhashnode2(ht, nam, nodeptr);
617/// if (oldnode) ht->freenode(oldnode);
618/// ```
619///
620/// Generic insert that drops the previous value at `nam` (Rust's
621// is now greater than twice the number of hash values, // c:157
622// the table is then expanded. // c:157
623/// `HashMap::insert` returns the old value; dropping it runs the
624/// equivalent of `freenode`). For typed table-specific entry
625/// shapes use the table's own `add()` method.
626// Add a node to a hash table, returning the old node on replacement. // c:168
627/// `addhashnode` — see implementation.
628pub fn addhashnode<T>(ht: &mut HashMap<String, T>, nam: &str, value: T) {
629 // c:157
630 ht.insert(nam.to_string(), value);
631}
632
633// Add a node to a hash table, returning the old node on replacement. // c:168
634/// Port of `addhashnode2(HashTable ht, char *nam, void *nodeptr)` from `Src/hashtable.c:168`.
635///
636/// C body inserts and returns the OLD node (instead of freeing
637/// it via the freenode callback). Rust HashMap::insert already
638/// has this shape — return the displaced value.
639pub fn addhashnode2<T>(ht: &mut HashMap<String, T>, nam: &str, nodeptr: T) -> Option<T> {
640 // c:168
641 ht.insert(nam.to_string(), nodeptr)
642}
643
644/// Port of `gethashnode(HashTable ht, const char *nam)` from `Src/hashtable.c:231`.
645///
646/// C body returns NULL if the entry has the DISABLED flag set;
647// the hashnode. If the node is DISABLED // c:231
648// or isn't found, it returns NULL // c:231
649/// otherwise returns the node. Generic lookup helper — `T` must
650/// expose its DISABLED flag via the [`HashNodeFlags`] trait so
651/// the disabled filter applies.
652/// WARNING: param names don't match C — Rust=(nam) vs C=(ht, nam)
653pub fn gethashnode<'a, T: HashNodeFlags>(
654 // c:231
655 ht: &'a HashMap<String, T>,
656 nam: &str,
657) -> Option<&'a T> {
658 ht.get(nam).filter(|t| !t.is_disabled())
659}
660
661impl cmdnam_table {
662 /// `new` — see implementation.
663 pub fn new() -> Self {
664 Self {
665 // hashtable.c:603 — `cmdnamtab = newhashtable(201, "cmdnamtab", NULL)`.
666 // The bucket count is observable: `${(k)commands}` and
667 // `compadd -k commands` emit the raw bucket walk.
668 table: crate::cow_map::CowArc::new(hashtable_nodes::newhashtable(201)), // c:603
669 path_checked_index: 0,
670 path: Vec::new(),
671 hash_executables_only: false,
672 }
673 }
674 /// `snapshot` — the table as the parent had it, for an in-process
675 /// subshell to hand back at its end. Same contract and the same C
676 /// reasoning as `alias_table::snapshot`: the forked child's `hash`,
677 /// `unhash`, `hash -r` and `commands[x]=…` write a private copy.
678 ///
679 /// O(1) for the table (`CowArc`). `path` is copied, but nothing sets
680 /// it (`set_path` has no callers), so it is always empty.
681 ///
682 /// !!! RUST-ONLY METHOD — no C counterpart (C forks) !!!
683 pub fn snapshot(&self) -> cmdnam_table {
684 self.clone()
685 }
686 /// `restore` — put back a table taken by `snapshot`.
687 ///
688 /// !!! RUST-ONLY METHOD — no C counterpart (C forks) !!!
689 pub fn restore(&mut self, snap: cmdnam_table) {
690 *self = snap;
691 }
692 /// `set_path` — see implementation.
693 pub fn set_path(&mut self, path: Vec<String>) {
694 self.path = path;
695 self.path_checked_index = 0;
696 }
697 /// `set_hash_executables_only` — see implementation.
698 pub fn set_hash_executables_only(&mut self, value: bool) {
699 self.hash_executables_only = value;
700 }
701 /// `add` — `addhashnode` (`Src/hashtable.c:157`).
702 pub fn add(&mut self, cmd: cmdnam) {
703 let nam = cmd.node.nam.clone();
704 let _ = self.table.addhashnode2(&nam, cmd); // c:168
705 }
706 /// `get` — `gethashnode` (`Src/hashtable.c:231`).
707 pub fn get(&self, name: &str) -> Option<&cmdnam> {
708 self.table
709 .gethashnode2(name)
710 .filter(|c| (c.node.flags & DISABLED as i32) == 0) // c:239
711 }
712 /// `get_including_disabled` — `gethashnode2` (`Src/hashtable.c:255`).
713 pub fn get_including_disabled(&self, name: &str) -> Option<&cmdnam> {
714 self.table.gethashnode2(name) // c:255
715 }
716 /// `remove` — `removehashnode` (`Src/hashtable.c:275`).
717 pub fn remove(&mut self, name: &str) -> Option<cmdnam> {
718 self.table.removehashnode(name) // c:275
719 }
720 /// `clear` — `emptyhashtable` (`Src/hashtable.c:517`).
721 pub fn clear(&mut self) {
722 self.table.emptyhashtable(); // c:517
723 self.path_checked_index = 0;
724 }
725 /// `len` — see implementation.
726 pub fn len(&self) -> usize {
727 self.table.len()
728 }
729 /// `is_empty` — see implementation.
730 pub fn is_empty(&self) -> bool {
731 self.table.is_empty()
732 }
733
734 /// Hash all commands in a directory
735 pub fn hash_dir(&mut self, dir: &str, dir_index: usize) {
736 if dir.starts_with('.') || dir.is_empty() {
737 return;
738 }
739
740 let Ok(entries) = fs::read_dir(dir) else {
741 return;
742 };
743
744 for entry in entries.flatten() {
745 let Ok(name) = entry.file_name().into_string() else {
746 continue;
747 };
748
749 if self.table.gethashnode2(&name).is_some() {
750 continue;
751 }
752
753 let should_add = if self.hash_executables_only {
754 // Inline of the deleted is_executable helper.
755 let path = entry.path();
756 #[cfg(unix)]
757 {
758 path.metadata()
759 .map(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)
760 .unwrap_or(false)
761 }
762 #[cfg(not(unix))]
763 {
764 path.is_file()
765 }
766 } else {
767 true
768 };
769
770 if should_add {
771 // C `cn->u.name = pathchecked;` at hashtable.c:712 —
772 // the unhashed entry carries the PATH-array slice it
773 // would scan. Rust port: snapshot the single PATH
774 // segment at `dir_index` so lookup later resolves
775 // the path. Older Rust-only code stored just the
776 // index; canonical port stores the actual segment.
777 let segment = self
778 .path
779 .get(dir_index)
780 .cloned()
781 .unwrap_or_else(|| dir.to_string());
782 let _ = self
783 .table
784 .addhashnode2(&name, cmdnam_unhashed(&name, vec![segment]));
785 // c:168
786 }
787 }
788 }
789
790 /// Fill table from PATH
791 pub fn fill(&mut self) {
792 for i in self.path_checked_index..self.path.len() {
793 let dir = self.path[i].clone();
794 self.hash_dir(&dir, i);
795 }
796 self.path_checked_index = self.path.len();
797 }
798
799 /// Iterate over all entries
800 pub fn iter(&self) -> impl Iterator<Item = (&String, &cmdnam)> {
801 self.table.iter()
802 }
803
804 /// Get full path for a command. Mirrors C's
805 /// `findcmd(name, 1, 0)` lookup via cmdnamtab (Src/exec.c:5260).
806 pub fn get_full_path(&self, name: &str) -> Option<PathBuf> {
807 let cmd = self.table.gethashnode2(name)?;
808 if (cmd.node.flags & DISABLED as i32) != 0 {
809 return None;
810 }
811 // HASHED branch: cn->u.cmd holds the resolved path.
812 if (cmd.node.flags & HASHED as i32) != 0 {
813 if let Some(ref s) = cmd.cmd {
814 return Some(PathBuf::from(s));
815 }
816 }
817 // Unhashed branch: cn->u.name holds PATH segments to scan.
818 if let Some(ref segs) = cmd.name {
819 if let Some(seg) = segs.first() {
820 let mut path = PathBuf::from(seg);
821 path.push(name);
822 return Some(path);
823 }
824 }
825 None
826 }
827}
828
829impl Default for cmdnam_table {
830 fn default() -> Self {
831 Self::new()
832 }
833}
834
835// `shfunc` struct + impl deleted — Rust-only duplicate of canonical
836// `crate::ported::zsh_h::shfunc` (zsh.h:1316-1325). Canonical:
837//
838// struct shfunc {
839// struct hashnode node;
840// char *filename;
841// zlong lineno;
842// Eprog funcdef;
843// Eprog redir;
844// Emulation_options sticky;
845// };
846//
847// Canonical was extended with a Rust-only `body: Option<String>`
848// field (deferred-compile source text) so callers using the old
849// `shfunc.body` access continue working. Type alias surfaces
850// canonical as `shfunc`; helpers below build instances with the
851// hashnode literal pre-populated.
852
853/// Port of `gethashnode2(HashTable ht, const char *nam)` from `Src/hashtable.c:255`.
854///
855/// Same as gethashnode but bypasses the DISABLED filter.
856pub fn gethashnode2<'a, T>(ht: &'a HashMap<String, T>, nam: &str) -> Option<&'a T> {
857 // c:255
858 ht.get(nam)
859}
860
861/// Port of `removehashnode(HashTable ht, const char *nam)` from `Src/hashtable.c:275`.
862///
863// table and returns a pointer to it. If there // c:275
864// is no such node, then it returns NULL // c:275
865/// C body removes the node from the bucket chain and returns the
866/// removed pointer (or NULL). Rust `HashMap::remove` has the
867/// matching shape.
868pub fn removehashnode<T>(ht: &mut HashMap<String, T>, nam: &str) -> Option<T> {
869 // c:275
870 ht.remove(nam)
871}
872
873/// Port of `disablehashnode(HashNode hn, UNUSED(int flags))` from `Src/hashtable.c:323`.
874///
875/// C body: `hn->flags |= DISABLED;`. Generic helper that flips
876/// the DISABLED bit on the named entry via [`HashNodeFlags`].
877pub fn disablehashnode<T: HashNodeFlags>(hn: &mut HashMap<String, T>, flags: &str) -> bool {
878 hn.get_mut(flags)
879 .map(|node| {
880 node.set_disabled(true);
881 true
882 })
883 .unwrap_or(false) // c:323
884}
885
886impl shfunc_table {
887 /// `new` — `shfunctab = newhashtable(7, "shfunctab", NULL)`
888 /// (`Src/hashtable.c:814`). The initial bucket count is part of the
889 /// observable scan order, so it must be C's 7 and grow only through
890 /// `expandhashtable`'s x4 rule.
891 pub fn new() -> Self {
892 Self {
893 table: std::sync::Arc::new(hashtable_nodes::newhashtable(7)), // hashtable.c:814
894 }
895 }
896 /// `snapshot` — clone the whole bucket array for subshell
897 /// save/restore. Used by `subshell_begin` to capture the parent's
898 /// function set before the subshell body runs, so `subshell_end`
899 /// can restore it (matches C fork-copy semantics at
900 /// `Src/exec.c::entersubsh`).
901 ///
902 /// This captures the TABLE, not a `HashMap` of its entries: the scan
903 /// order is part of the state (`Src/Modules/parameter.c:480-481`
904 /// walks `shfunctab->nodes[i]` directly), and rebuilding a bucket
905 /// array from an unordered map would reshuffle `${(k)functions}`
906 /// after every `( … )` / `$( … )`.
907 ///
908 /// O(1): the bucket array is `Arc`-shared (see the `table` field),
909 /// so this is a refcount bump. The deep copy is deferred to the
910 /// first `&mut self` method the subshell body calls, which goes
911 /// through `Arc::make_mut`. A body that never defines, removes,
912 /// disables or autoloads a function never pays for one.
913 pub fn snapshot(&self) -> std::sync::Arc<shfunc_table> {
914 std::sync::Arc::new(self.clone())
915 }
916 /// `restore` — replace the internal table with a saved snapshot.
917 /// Called by `subshell_end` after the subshell body completes.
918 /// Takes the `Arc`-shared snapshot stored in `SubshellSnapshot`;
919 /// unwraps in place when uniquely owned (the common case), else
920 /// clones out of the shared handle.
921 pub fn restore(&mut self, snap: std::sync::Arc<shfunc_table>) {
922 *self = std::sync::Arc::try_unwrap(snap).unwrap_or_else(|arc| (*arc).clone());
923 }
924 /// Formerly pre-sized the backing `HashMap`. C has no such call and
925 /// CANNOT have one: `hsize` only ever moves through
926 /// `expandhashtable`'s x4 steps (`Src/hashtable.c:466`), and the
927 /// bucket count is what `hasher(nam) % hsize` — hence the whole scan
928 /// order — is computed against. Pre-sizing to the batch size would
929 /// put every name in a different bucket than zsh does. Kept as a
930 /// no-op so the `compinit` call site (which is a Rust-only
931 /// optimisation) still compiles.
932 pub fn reserve(&mut self, _additional: usize) {}
933
934 /// `add` — `addhashnode2` (`Src/hashtable.c:168`) with the displaced
935 /// node handed back to the caller instead of `freenode`d.
936 pub fn add(&mut self, func: shfunc) -> Option<shfunc> {
937 let name = func.node.nam.clone();
938 std::sync::Arc::make_mut(&mut self.table)
939 .addhashnode2(&name, Box::new(func)) // c:168
940 .map(|b| *b)
941 }
942 /// `get` — `gethashnode` (`Src/hashtable.c:231`): DISABLED nodes read
943 /// as absent.
944 pub fn get(&self, name: &str) -> Option<&shfunc> {
945 self.table
946 .gethashnode2(name) // c:236-241
947 .map(|b| b.as_ref())
948 .filter(|f| (f.node.flags & DISABLED as i32) == 0) // c:239
949 }
950 /// `get_including_disabled` — `gethashnode2` (`Src/hashtable.c:255`).
951 pub fn get_including_disabled(&self, name: &str) -> Option<&shfunc> {
952 self.table.gethashnode2(name).map(|b| b.as_ref()) // c:255
953 }
954 /// `get_mut` — see implementation.
955 pub fn get_mut(&mut self, name: &str) -> Option<&mut shfunc> {
956 std::sync::Arc::make_mut(&mut self.table)
957 .get_mut(name)
958 .map(|b| b.as_mut())
959 .filter(|f| (f.node.flags & DISABLED as i32) == 0)
960 }
961 /// `remove` — `removehashnode` (`Src/hashtable.c:275`).
962 pub fn remove(&mut self, name: &str) -> Option<shfunc> {
963 std::sync::Arc::make_mut(&mut self.table)
964 .removehashnode(name)
965 .map(|b| *b) // c:275
966 }
967 /// `contains_key` — see implementation.
968 pub fn contains_key(&self, name: &str) -> bool {
969 self.table.gethashnode2(name).is_some()
970 }
971
972 /// Port of C's `HashTable.addnode` GSU function pointer
973 /// (`Src/zsh.h:281+`). Takes a `*mut shfunc` (typedef `Shfunc`)
974 /// previously obtained via `Box::into_raw` — reclaims ownership
975 /// into the table by name. After this call, the caller's `shf`
976 /// pointer is INVALIDATED in the Rust ownership sense; subsequent
977 /// reads must go through `getnode(name)` to get a fresh pointer.
978 /// In practice, C code re-uses the same `shf` pointer because the
979 /// Box stays at the same heap address — we keep that semantic by
980 /// boxing-on-heap. Replaces any prior entry with the same name
981 /// (matching C `addnode`'s overwrite-and-free-old behavior).
982 pub fn addnode(&mut self, shf: *mut shfunc) {
983 if shf.is_null() {
984 return;
985 }
986 let boxed = unsafe { Box::from_raw(shf) };
987 let name = boxed.node.nam.clone();
988 let _ = std::sync::Arc::make_mut(&mut self.table).addhashnode2(&name, boxed); // c:168
989 }
990
991 /// Port of C's `HashTable.getnode` GSU. Returns the raw `Shfunc`
992 /// pointer (typedef `*mut shfunc`) or null if missing or disabled.
993 /// Pointer stays valid as long as the underlying `Box<shfunc>`
994 /// lives in the table (i.e. until `remove`/`addnode`-overwrite).
995 ///
996 /// NOTE (copy-on-write): the bucket array is `Arc`-shared while a
997 /// subshell snapshot is live, so writing THROUGH this pointer would
998 /// bypass `Arc::make_mut` and mutate the snapshot too. It has no
999 /// callers — it is a name-parity anchor — and any future caller
1000 /// must take `&mut` through `get_mut`/`add` instead.
1001 pub fn getnode(&self, name: &str) -> *mut shfunc {
1002 self.table
1003 .gethashnode2(name)
1004 .filter(|b| (b.node.flags & DISABLED as i32) == 0)
1005 .map(|b| b.as_ref() as *const shfunc as *mut shfunc)
1006 .unwrap_or(std::ptr::null_mut())
1007 }
1008
1009 /// Port of C's `HashTable.getnode2` GSU — same as `getnode` but
1010 /// returns disabled nodes too. Used by `unhash`/`enable -f` paths.
1011 pub fn getnode2(&self, name: &str) -> *mut shfunc {
1012 self.table
1013 .gethashnode2(name)
1014 .map(|b| b.as_ref() as *const shfunc as *mut shfunc)
1015 .unwrap_or(std::ptr::null_mut())
1016 }
1017 /// `disable` — see implementation.
1018 pub fn disable(&mut self, name: &str) -> bool {
1019 if let Some(func) = std::sync::Arc::make_mut(&mut self.table).get_mut(name) {
1020 func.node.flags |= DISABLED as i32;
1021 true
1022 } else {
1023 false
1024 }
1025 }
1026 /// `enable` — see implementation.
1027 pub fn enable(&mut self, name: &str) -> bool {
1028 if let Some(func) = std::sync::Arc::make_mut(&mut self.table).get_mut(name) {
1029 func.node.flags &= !(DISABLED as i32);
1030 true
1031 } else {
1032 false
1033 }
1034 }
1035 /// `len` — see implementation.
1036 pub fn len(&self) -> usize {
1037 self.table.len()
1038 }
1039 /// `is_empty` — see implementation.
1040 pub fn is_empty(&self) -> bool {
1041 self.table.is_empty()
1042 }
1043 /// `iter` — see implementation.
1044 pub fn iter(&self) -> impl Iterator<Item = (&String, &shfunc)> {
1045 self.table.iter().map(|(k, b)| (k, b.as_ref()))
1046 }
1047 /// `iter_sorted` — see implementation.
1048 pub fn iter_sorted(&self) -> Vec<(&String, &shfunc)> {
1049 let mut entries: Vec<(&String, &shfunc)> =
1050 self.table.iter().map(|(k, b)| (k, b.as_ref())).collect();
1051 entries.sort_by(|a, b| a.0.cmp(b.0));
1052 entries
1053 }
1054 /// `clear` — `emptyhashtable` (`Src/hashtable.c:517`): frees every
1055 /// node but keeps the bucket count.
1056 pub fn clear(&mut self) {
1057 std::sync::Arc::make_mut(&mut self.table).emptyhashtable(); // c:517
1058 }
1059}
1060
1061impl Default for shfunc_table {
1062 fn default() -> Self {
1063 Self::new()
1064 }
1065}
1066
1067// `reswdToken` enum deleted — Rust-only enum duplicating the
1068// canonical `lextok` i32 token constants already in zsh_h.rs
1069// (BANG_TOK/DINBRACK/INBRACE_TOK/OUTBRACE_TOK/CASE/COPROC/DOLOOP
1070// /DONE/ELIF/ELSE/ZEND/ESAC/FI/FOR/FOREACH/FUNC/IF/NOCORRECT/
1071// REPEAT/SELECT/THEN/TIME/UNTIL/WHILE/TYPESET at zsh.h:345-371).
1072// reswd.token now stores the raw i32 lextok matching C `struct
1073// reswd { HashNode node; int token; }` at zsh.h:1246-1249.
1074
1075// `reswd` struct + impl deleted — Rust-only duplicate of canonical
1076// `crate::ported::zsh_h::reswd` (zsh.h:1246-1249). The canonical
1077// has `node: hashnode { nam, flags, next }` + `token: i32`; the
1078// Rust-only had `name, flags: u32, token: i32` (missing the
1079// hashnode embedding). Type alias surfaces the canonical struct
1080// to in-file callers and external imports.
1081// c:1246
1082
1083/// Public copy of the canonical `reswds[]` table from
1084/// `Src/hashtable.c:1076-1108`. Each entry is `(name, lextok)`; the
1085/// token identifies which grammar production the word triggers.
1086///
1087/// Callers outside the hashtable (LSP reflection dump, IntelliJ
1088/// inventory) iterate this directly so they don't have to take the
1089/// `reswdtab` lock or duplicate the list. Filtering: entries with
1090/// `token == TYPESET` are declaration commands (local / typeset /
1091/// declare / export / readonly / integer / float) — they're aliased
1092/// to `typeset` at the grammar level but really live as builtins, so
1093/// a "reserved word" inventory should exclude them.
1094pub const RESWDS: &[(&str, i32)] = &[
1095 ("!", BANG_TOK),
1096 ("[[", DINBRACK),
1097 ("{", INBRACE_TOK),
1098 ("}", OUTBRACE_TOK),
1099 ("case", CASE),
1100 ("coproc", COPROC),
1101 ("declare", TYPESET),
1102 ("do", DOLOOP),
1103 ("done", DONE),
1104 ("elif", ELIF),
1105 ("else", ELSE),
1106 ("end", ZEND),
1107 ("esac", ESAC),
1108 ("export", TYPESET),
1109 ("fi", FI),
1110 ("float", TYPESET),
1111 ("for", FOR),
1112 ("foreach", FOREACH),
1113 ("function", FUNC),
1114 ("if", IF),
1115 ("integer", TYPESET),
1116 ("local", TYPESET),
1117 ("nocorrect", NOCORRECT),
1118 ("readonly", TYPESET),
1119 ("repeat", REPEAT),
1120 ("select", SELECT),
1121 ("then", THEN),
1122 ("time", TIME),
1123 ("typeset", TYPESET),
1124 ("until", UNTIL),
1125 ("while", WHILE),
1126];
1127
1128/// Port of `enablehashnode(HashNode hn, UNUSED(int flags))` from `Src/hashtable.c:332`.
1129///
1130/// C body: `hn->flags &= ~DISABLED;`. Inverse of [`disablehashnode`].
1131pub fn enablehashnode<T: HashNodeFlags>(hn: &mut HashMap<String, T>, flags: &str) -> bool {
1132 hn.get_mut(flags)
1133 .map(|node| {
1134 node.set_disabled(false);
1135 true
1136 })
1137 .unwrap_or(false) // c:332
1138}
1139
1140impl reswd_table {
1141 /// `new` — port of `createreswdtable()` (`Src/hashtable.c:1120`):
1142 /// `newhashtable(23, "reswdtab", NULL)` (`c:1124`) followed by
1143 /// `for (rw = reswds; rw->node.nam; rw++) reswdtab->addnode(...)`
1144 /// (`c:1138-1139`), which walks the static `reswds[]` array in
1145 /// declaration order.
1146 pub fn new() -> Self {
1147 // c:1124 — `reswdtab = newhashtable(23, "reswdtab", NULL);`
1148 let mut table = hashtable_nodes::newhashtable(23);
1149
1150 // Direct port of `static struct reswd reswds[]` at
1151 // Src/hashtable.c:1076-1108. Token IDs are the lextok
1152 // constants from zsh_h.rs (zsh.h:345-371).
1153 //
1154 // Same list is exposed via the public `RESWDS` const below so
1155 // callers outside this module (LSP reflection dump, IntelliJ
1156 // tool-window inventory) can enumerate reserved words without
1157 // taking the table lock.
1158 let words: [(&str, i32); 31] = [
1159 // c:1076
1160 ("!", BANG_TOK), // c:1077
1161 ("[[", DINBRACK), // c:1078
1162 ("{", INBRACE_TOK), // c:1079
1163 ("}", OUTBRACE_TOK), // c:1080
1164 ("case", CASE), // c:1081
1165 ("coproc", COPROC), // c:1082
1166 ("declare", TYPESET), // c:1083
1167 ("do", DOLOOP), // c:1084
1168 ("done", DONE), // c:1085
1169 ("elif", ELIF), // c:1086
1170 ("else", ELSE), // c:1087
1171 ("end", ZEND), // c:1088
1172 ("esac", ESAC), // c:1089
1173 ("export", TYPESET), // c:1090
1174 ("fi", FI), // c:1091
1175 ("float", TYPESET), // c:1092
1176 ("for", FOR), // c:1093
1177 ("foreach", FOREACH), // c:1094
1178 ("function", FUNC), // c:1095
1179 ("if", IF), // c:1096
1180 ("integer", TYPESET), // c:1097
1181 ("local", TYPESET), // c:1098
1182 ("nocorrect", NOCORRECT), // c:1099
1183 ("readonly", TYPESET), // c:1100
1184 ("repeat", REPEAT), // c:1101
1185 ("select", SELECT), // c:1102
1186 ("then", THEN), // c:1103
1187 ("time", TIME), // c:1104
1188 ("typeset", TYPESET), // c:1105
1189 ("until", UNTIL), // c:1106
1190 ("while", WHILE), // c:1107
1191 ];
1192 // Sanity: the local `words` array and the public `RESWDS` const
1193 // below MUST stay in sync — both are direct ports of the same
1194 // upstream `reswds[]` table at Src/hashtable.c:1076-1108.
1195 debug_assert_eq!(words.len(), RESWDS.len());
1196
1197 for (name, token) in words {
1198 // Direct struct literal — canonical `reswd` has
1199 // `node: hashnode` (zsh.h:1246) so we build the
1200 // embedded hashnode inline. Mirrors C `{{NULL,
1201 // "if", 0}, IF}` at hashtable.c:1077+.
1202 // c:1138-1139 — `reswdtab->addnode(reswdtab, rw->node.nam, rw)`
1203 table.addhashnode2(
1204 name,
1205 reswd {
1206 node: hashnode {
1207 next: None,
1208 nam: name.to_string(),
1209 flags: 0,
1210 },
1211 token,
1212 },
1213 );
1214 }
1215
1216 Self { table }
1217 }
1218 /// `get` — `gethashnode` (`Src/hashtable.c:245`), the lookup that
1219 /// skips `DISABLED` nodes (`c:253`); `createreswdtable` wires
1220 /// `reswdtab->getnode = gethashnode` (`c:1131`).
1221 pub fn get(&self, name: &str) -> Option<&reswd> {
1222 // c:245
1223 self.table
1224 .gethashnode2(name)
1225 .filter(|r| (r.node.flags & DISABLED as i32) == 0)
1226 }
1227 /// `get_including_disabled` — `gethashnode2`
1228 /// (`Src/hashtable.c:255`), wired as `reswdtab->getnode2`
1229 /// (`c:1132`).
1230 pub fn get_including_disabled(&self, name: &str) -> Option<&reswd> {
1231 self.table.gethashnode2(name) // c:255
1232 }
1233 /// `disable` — see implementation.
1234 pub fn disable(&mut self, name: &str) -> bool {
1235 if let Some(rw) = self.table.get_mut(name) {
1236 rw.node.flags |= DISABLED as i32;
1237 true
1238 } else {
1239 false
1240 }
1241 }
1242 /// `enable` — see implementation.
1243 pub fn enable(&mut self, name: &str) -> bool {
1244 if let Some(rw) = self.table.get_mut(name) {
1245 rw.node.flags &= !(DISABLED as i32);
1246 true
1247 } else {
1248 false
1249 }
1250 }
1251 /// `is_reserved` — see implementation.
1252 pub fn is_reserved(&self, name: &str) -> bool {
1253 self.get(name).is_some()
1254 }
1255 /// `iter` — the raw bucket walk `getreswords`
1256 /// (`Src/Modules/parameter.c:877-880`) does over
1257 /// `reswdtab->nodes[]`, which is what `$reswords` /
1258 /// `$dis_reswords` expose.
1259 pub fn iter(&self) -> impl Iterator<Item = (&String, &reswd)> {
1260 self.table.iter() // c:420-434
1261 }
1262 /// Port of `addhashnode(HashTable ht, char *nam, void *nodeptr)`
1263 /// from `Src/hashtable.c:157`. C stores `nodeptr` under `nam` and
1264 /// frees any node it displaces (`ht->freenode(oldnode)`, c:161).
1265 /// Here the map replaces the entry and the displaced `reswd` is
1266 /// dropped, matching C's freenode semantics. Runtime companion to
1267 /// the seed-only `new()`; param_private's `setup_` uses it to
1268 /// register `private` as a TYPESET reserved word at module boot
1269 /// (param_private.c:687 `reswdtab->addnode(reswdtab, ...)`).
1270 pub fn insert(&mut self, name: &str, rw: reswd) {
1271 // c:157 addhashnode → c:159 addhashnode2 sets hn->nam = nam
1272 self.table.addhashnode2(name, rw);
1273 }
1274 /// Port of `removehashnode(HashTable ht, const char *nam)` from
1275 /// `Src/hashtable.c:275`. Unlinks the node keyed by `nam` and
1276 /// returns it (C returns the removed `HashNode`, or NULL when the
1277 /// key is absent — c:283). param_private's teardown uses it to
1278 /// unregister the `private` reserved word (param_private.c:722
1279 /// `removehashnode(reswdtab, "private")`).
1280 pub fn remove(&mut self, name: &str) -> Option<reswd> {
1281 // c:275
1282 self.table.removehashnode(name)
1283 }
1284}
1285
1286impl Default for reswd_table {
1287 fn default() -> Self {
1288 Self::new()
1289 }
1290}
1291
1292// `crate::ported::zsh_h::alias` struct + impl deleted — Rust-only duplicate of canonical
1293// `crate::ported::zsh_h::alias` (zsh.h:1253-1257). The canonical
1294// has `node: hashnode { nam, flags, next }` embedded (c:1254) +
1295// `text: String` (c:1255) + `inuse: i32` (c:1256); the Rust-only
1296// had a flat `name: String, flags: u32, text: String, inuse: i32`
1297// (missing the hashnode embedding).
1298
1299/// Port of `static int hnamcmp(const void *ap, const void *bp)`
1300/// from `Src/hashtable.c:341-346`. C body:
1301/// ```c
1302/// HashNode a = *(HashNode *)ap;
1303/// HashNode b = *(HashNode *)bp;
1304/// return ztrcmp(a->nam, b->nam);
1305/// ```
1306///
1307/// `ztrcmp` is a META-AWARE compare that XORs Meta-escaped bytes
1308/// with 32 before comparing (Src/utils.c:5106). The previous Rust
1309/// port used `str::cmp` which does naive byte-wise lexicographic
1310/// compare — for Meta-encoded hash-table keys this sorts them
1311/// incorrectly (Meta byte 0x83 sorts AFTER ASCII printable but the
1312/// real underlying byte 0x83^32=0xa3 should compare as a high byte).
1313///
1314/// Route through the canonical `crate::ported::utils::ztrcmp` so
1315/// `functions`, `alias`, etc. sort their key listings the same way
1316/// C does for Meta-encoded names.
1317pub fn hnamcmp(ap: &str, bp: &str) -> std::cmp::Ordering {
1318 ztrcmp(ap, bp) // c:345
1319}
1320
1321/// Port of `scanmatchtable(HashTable ht, Patprog pprog, int sorted, int flags1, int flags2, ScanFunc scanfunc, int scanflags)` from `Src/hashtable.c:373`.
1322///
1323/// C body walks every node calling `func(node, scanflags)` if
1324/// the node satisfies (a) optional pattern match, (b) `flags1`
1325/// require-at-least-one, (c) `flags2` require-none-of. The
1326/// `sorted` flag pre-sorts entries before scanning.
1327///
1328/// Rust port: same shape with closure callback. Returns the
1329/// match count.
1330/// WARNING: param names don't match C — Rust=() vs C=(ht, pprog, sorted, flags1, flags2, scanfunc, scanflags)
1331pub fn scanmatchtable<T: HashNodeFlags, F: FnMut(&str, &T)>(
1332 ht: &HashMap<String, T>,
1333 pattern: Option<&str>,
1334 sorted: bool,
1335 flags1: u32,
1336 flags2: u32,
1337 mut func: F,
1338) -> i32 {
1339 let mut entries: Vec<(&String, &T)> = ht.iter().collect();
1340 if sorted {
1341 // c:400 — `qsort(hnsorttab, ct, sizeof(HashNode), hnamcmp);`
1342 // hnamcmp routes through Meta-aware ztrcmp. The previous Rust
1343 // port used `str::cmp` (naive byte-wise) which sorts Meta-
1344 // encoded hash keys incorrectly. Use the canonical hnamcmp
1345 // to match C's qsort comparator exactly.
1346 entries.sort_by(|a, b| hnamcmp(a.0, b.0)); // c:400
1347 }
1348 let mut match_count = 0;
1349 for (name, node) in entries {
1350 if let Some(p) = pattern {
1351 if !simple_glob_match(p, name) {
1352 continue;
1353 }
1354 }
1355 let f = node.flags();
1356 if flags1 != 0 && (f & flags1) == 0 {
1357 continue;
1358 }
1359 if flags2 != 0 && (f & flags2) != 0 {
1360 continue;
1361 }
1362 func(name, node);
1363 match_count += 1;
1364 }
1365 match_count
1366}
1367
1368impl alias_table {
1369 /// `new` — `newhashtable(23, "aliastab", NULL)` +
1370 /// `createaliastable(aliastab)` (`Src/hashtable.c:1210-1212`),
1371 /// without the two default aliases. `sufaliastab` is created at
1372 /// `hsize = 11` (`Src/hashtable.c:1221`) so `sufaliastab_lock()`
1373 /// builds its table inline rather than going through here.
1374 pub fn new() -> Self {
1375 Self {
1376 table: crate::cow_map::CowArc::new(hashtable_nodes::newhashtable(23)), // c:1210
1377 }
1378 }
1379 /// `with_defaults` — `new()` plus the two aliases
1380 /// `createaliastables()` installs (`Src/hashtable.c:1215-1216`).
1381 /// They are added FIRST, before any user alias, so the chain heads
1382 /// come out in C's order.
1383 pub fn with_defaults() -> Self {
1384 let mut table = Self::new();
1385 // ZSHRS-ONLY. `run-help` / `which-command` are ZSH's default
1386 // aliases. A drop-in starts with the set ITS shell defines:
1387 // bash, dash and /bin/sh define none at all, ksh93 defines 19
1388 // (`r`, `functions`, `integer`, …) and mksh 11. Measured with
1389 // `<shell> -c alias` in an empty environment.
1390 if let Some(defaults) = crate::extensions::emulation_startup::default_aliases() {
1391 for (name, text) in defaults {
1392 table.add(createaliasnode(name, text, 0));
1393 }
1394 return table;
1395 }
1396 // C addaliasnode(aliastab, "run-help", createaliasnode("man", 0));
1397 // at hashtable.c:1215-1216.
1398 table.add(createaliasnode("run-help", "man", 0)); // c:1215
1399 table.add(createaliasnode("which-command", "whence", 0)); // c:1216
1400 table
1401 }
1402 /// `snapshot` — the table as the parent had it, for a subshell to
1403 /// hand back at its end.
1404 ///
1405 /// C never needs this: `getoutput` and `( … )` fork
1406 /// (`Src/exec.c:4816`, `c:2880`) and `entersubsh` (`c:1123-1261`)
1407 /// leaves `aliastab`/`sufaliastab` alone, so the child mutates a
1408 /// private copy that dies with it. zshrs runs both forms in process,
1409 /// so the parent's table is taken here and put back by `restore`.
1410 ///
1411 /// O(1): the bucket array is `CowArc`-shared. The deep copy happens
1412 /// only if the body adds, removes, disables or enables an alias.
1413 /// Expanding one does not count — `alias::inuse` is atomic.
1414 ///
1415 /// !!! RUST-ONLY METHOD — no C counterpart (C forks) !!!
1416 pub fn snapshot(&self) -> alias_table {
1417 alias_table {
1418 table: self.table.clone(),
1419 }
1420 }
1421 /// `restore` — put back a table taken by `snapshot`, bucket order
1422 /// and all (`${(k)aliases}` walks the buckets directly).
1423 ///
1424 /// !!! RUST-ONLY METHOD — no C counterpart (C forks) !!!
1425 pub fn restore(&mut self, snap: alias_table) {
1426 self.table = snap.table;
1427 }
1428 /// `add` — `addhashnode2` (`Src/hashtable.c:168`). C's `addnode`
1429 /// for this table is `addhashnode` (`c:1194`), which is
1430 /// `addhashnode2` + `freenode(oldnode)` (`c:157-162`); returning
1431 /// the displaced node lets the caller do the freeing, and dropping
1432 /// it is `freealiasnode` (`c:1243`).
1433 pub fn add(&mut self, alias: alias) -> Option<alias> {
1434 // c:157 / c:168
1435 let nam = alias.node.nam.clone(); // c:177 — `hn->nam = nam`
1436 self.table.addhashnode2(&nam, alias)
1437 }
1438 /// `get` — `gethashnode` (`Src/hashtable.c:245`), i.e. the lookup
1439 /// that skips `DISABLED` nodes (`c:253`).
1440 pub fn get(&self, name: &str) -> Option<&alias> {
1441 // c:245
1442 self.table
1443 .gethashnode2(name)
1444 .filter(|a| (a.node.flags & DISABLED as i32) == 0)
1445 }
1446 /// `get_including_disabled` — `gethashnode2`
1447 /// (`Src/hashtable.c:255`), the lookup WITHOUT the DISABLED filter.
1448 pub fn get_including_disabled(&self, name: &str) -> Option<&alias> {
1449 self.table.gethashnode2(name) // c:255
1450 }
1451 /// `get_mut` — mutable `gethashnode` (`Src/hashtable.c:245`); C
1452 /// mutates straight through the returned `HashNode` pointer.
1453 pub fn get_mut(&mut self, name: &str) -> Option<&mut alias> {
1454 // c:245
1455 self.table
1456 .get_mut(name)
1457 .filter(|a| (a.node.flags & DISABLED as i32) == 0)
1458 }
1459 /// `remove` — `removehashnode` (`Src/hashtable.c:275`).
1460 pub fn remove(&mut self, name: &str) -> Option<alias> {
1461 self.table.removehashnode(name) // c:275
1462 }
1463 /// `disable` — see implementation.
1464 pub fn disable(&mut self, name: &str) -> bool {
1465 if let Some(alias) = self.table.get_mut(name) {
1466 alias.node.flags |= DISABLED as i32;
1467 true
1468 } else {
1469 false
1470 }
1471 }
1472 /// `enable` — see implementation.
1473 pub fn enable(&mut self, name: &str) -> bool {
1474 if let Some(alias) = self.table.get_mut(name) {
1475 alias.node.flags &= !(DISABLED as i32);
1476 true
1477 } else {
1478 false
1479 }
1480 }
1481 /// `len` — see implementation.
1482 pub fn len(&self) -> usize {
1483 self.table.len()
1484 }
1485 /// `is_empty` — see implementation.
1486 pub fn is_empty(&self) -> bool {
1487 self.table.is_empty()
1488 }
1489 /// `clear` — `emptyhashtable` (`Src/hashtable.c:517`), which is
1490 /// `resizehashtable(ht, ht->hsize)`: every node freed, `hsize` kept.
1491 pub fn clear(&mut self) {
1492 self.table.emptyhashtable(); // c:517
1493 }
1494 /// `iter` — the unsorted `scanmatchtable` walk
1495 /// (`Src/hashtable.c:420-434`): bucket 0..hsize-1, each chain
1496 /// head→tail. This IS the order `${(k)aliases}` / `${(k)galiases}`
1497 /// / `${(k)saliases}` emit via `scanpmraliases` and friends
1498 /// (`Src/Modules/parameter.c:2005-2047`).
1499 pub fn iter(&self) -> impl Iterator<Item = (&String, &alias)> {
1500 self.table.iter() // c:420-434
1501 }
1502 /// `iter_sorted` — the `sorted` arm of `scanmatchtable`
1503 /// (`Src/hashtable.c:395-401`), which `qsort`s the collected nodes
1504 /// with `hnamcmp`.
1505 pub fn iter_sorted(&self) -> Vec<(&String, &alias)> {
1506 let mut entries: Vec<_> = self.table.iter().collect();
1507 entries.sort_by(|a, b| a.0.cmp(b.0)); // c:400
1508 entries
1509 }
1510}
1511
1512impl Default for alias_table {
1513 fn default() -> Self {
1514 Self::new()
1515 }
1516}
1517
1518/// Port of `scanhashtable(HashTable ht, int sorted, int flags1, int flags2, ScanFunc scanfunc, int scanflags)` from `Src/hashtable.c:446`.
1519///
1520/// C body delegates to `scanmatchtable` with `pprog = NULL`. Rust
1521/// port does the same.
1522/// WARNING: param names don't match C — Rust=() vs C=(ht, sorted, flags1, flags2, scanfunc, scanflags)
1523pub fn scanhashtable<T: HashNodeFlags, F: FnMut(&str, &T)>(
1524 ht: &HashMap<String, T>,
1525 sorted: bool,
1526 flags1: u32,
1527 flags2: u32,
1528 func: F,
1529) -> i32 {
1530 scanmatchtable(ht, None, sorted, flags1, flags2, func)
1531}
1532
1533/// Port of `expandhashtable(HashTable ht)` from `Src/hashtable.c:458`.
1534///
1535/// C grows the bucket array when load factor exceeds threshold.
1536/// Rust HashMap rehashes automatically — calling reserve on the
1537/// passed map gives the closest equivalent.
1538/// Rust idiom replacement: `HashMap::reserve` covers the C
1539/// `growhashtable` bucket-realloc + rehash loop.
1540pub fn expandhashtable<T>(ht: &mut HashMap<String, T>) {
1541 let want = ht.len() * 2;
1542 ht.reserve(want.saturating_sub(ht.capacity()));
1543}
1544
1545/// Port of `resizehashtable(HashTable ht, int newsize)` from `Src/hashtable.c:486`.
1546///
1547/// C reallocates buckets to a specific size. Rust HashMap reserves
1548/// capacity to ensure at least `newsize` entries fit without rehash.
1549/// Rust idiom replacement: `HashMap::reserve(need)` covers the C
1550/// `realloc(hsize * sizeof(HashNode))` + rehash dance.
1551pub fn resizehashtable<T>(ht: &mut HashMap<String, T>, newsize: i32) {
1552 let need = newsize.max(0) as usize;
1553 if need > ht.capacity() {
1554 ht.reserve(need - ht.capacity());
1555 }
1556}
1557
1558// Generic method to empty a hash table // c:519
1559/// Port of `emptyhashtable(HashTable ht)` from `Src/hashtable.c:519`.
1560///
1561/// C body: `resizehashtable(ht, ht->hsize);` — drop all nodes
1562/// while keeping the bucket array. Rust HashMap::clear preserves
1563/// capacity, matching the semantic.
1564pub fn emptyhashtable<T>(ht: &mut HashMap<String, T>) {
1565 // c:519
1566 ht.clear();
1567}
1568
1569// Print info about hash table // c:527
1570/// Port of `printhashtabinfo(HashTable ht)` from `Src/hashtable.c:78`.
1571///
1572/// C body prints chain-length distribution stats for hash-table
1573/// debug analysis (under ZSH_HASH_DEBUG). Rust HashMap doesn't
1574/// expose chain-length info; emit count + capacity which is the
1575/// equivalent visibility.
1576/// Rust idiom replacement: HashMap's open addressing doesn't expose
1577/// chain length, so we emit name+capacity+len — the equivalent
1578/// visibility under Rust's std::collections backend.
1579/// WARNING: param names don't match C — Rust=(name, ht) vs C=(ht)
1580pub fn printhashtabinfo<T>(name: &str, ht: &HashMap<String, T>) -> String {
1581 // c:78
1582 format!(
1583 "name of table : {}\nsize of nodes[] : {}\nnumber of nodes : {}",
1584 name,
1585 ht.capacity(),
1586 ht.len()
1587 )
1588}
1589
1590/// Port of `bin_hashinfo(UNUSED(char *nam), UNUSED(char **args), UNUSED(Options ops), UNUSED(int func))` from `Src/hashtable.c:566`.
1591///
1592/// C iterates all registered hashtables (cmdnamtab, shfunctab,
1593/// aliastab, etc.) and emits stats for each. Rust port walks the
1594/// known-singleton tables.
1595pub fn bin_hashinfo(
1596 _nam: &str,
1597 _args: &[String], // c:566
1598 _ops: &options,
1599 _func: i32,
1600) -> i32 {
1601 let banner = "----------------------------------------------------";
1602 println!("{}", banner);
1603 {
1604 let tab = cmdnamtab_lock().read().expect("cmdnamtab poisoned");
1605 println!("name of table : cmdnamtab");
1606 println!("number of nodes : {}", tab.len());
1607 }
1608 println!("{}", banner);
1609 {
1610 let tab = shfunctab_lock().read().expect("shfunctab poisoned");
1611 println!("name of table : shfunctab");
1612 println!("number of nodes : {}", tab.len());
1613 }
1614 println!("{}", banner);
1615 {
1616 let tab = aliastab_lock().read().expect("aliastab poisoned");
1617 println!("name of table : aliastab");
1618 println!("number of nodes : {}", tab.len());
1619 }
1620 println!("{}", banner);
1621 0
1622}
1623
1624// Old fake `dircache_lock(Mutex<HashMap<String, i32>>)` deleted —
1625// wrong shape (C uses `struct dircache_entry { name, refs }` not
1626// `HashMap<String, i32>`). Canonical port lives earlier in this
1627// file at the `dircache_entry` struct + `dircache_lock` accessor
1628// returning `Mutex<Vec<dircache_entry>>`.
1629
1630/// Port of `createcmdnamtable()` from `Src/hashtable.c:601`.
1631///
1632/// C body sets up the cmdnamtab GSU vtable (hash, addnode,
1633/// removenode, freenode, printnode = printcmdnamnode). Rust port
1634/// just touches the singleton to ensure it's initialised.
1635pub fn createcmdnamtable() {
1636 let _ = cmdnamtab_lock();
1637}
1638
1639/// Port of `emptycmdnamtable(HashTable ht)` from `Src/hashtable.c:623`.
1640///
1641/// C body:
1642/// ```c
1643/// emptyhashtable(ht);
1644/// pathchecked = path;
1645/// ```
1646///
1647/// Drops every PATH cache entry (used by `hash -r`) and resets
1648/// the per-PATH-entry "checked" cursor so subsequent lookups
1649/// re-scan from the start.
1650/// WARNING: param names don't match C — Rust=() vs C=(ht)
1651pub fn emptycmdnamtable() {
1652 // c:1015 — `emptyhashtable(ht);`
1653 cmdnamtab_lock()
1654 .write()
1655 .expect("cmdnamtab poisoned")
1656 .clear();
1657 // c:1016 — `pathchecked = path;`. Resetting the cursor here (not in
1658 // each caller) is what C does: every caller that empties the table
1659 // must also allow a subsequent `fillcmdnamtable` to re-walk PATH
1660 // from the start. Without this, emptying the table (e.g. a `PATH`
1661 // reassignment) left `pathchecked` exhausted, so the next
1662 // `${(k)commands}` / `compadd -k commands` scan refilled nothing.
1663 pathchecked.store(0, std::sync::atomic::Ordering::SeqCst);
1664}
1665
1666/// Port of `hashdir(char **dirp)` from `Src/hashtable.c:634`.
1667///
1668/// C body opendir's the directory, reads each entry, and adds
1669/// any executable to `cmdnamtab` (skipping names already present
1670/// from earlier PATH entries). Rust port routes through
1671/// `cmdnam_table::hash_dir`.
1672/// Rust idiom replacement: pure delegation to `hash_dir` on the
1673/// typed `CmdNamTable`; the C opendir/readdir/executable-test loop
1674/// lives there with `fs::read_dir` + `is_executable_via_metadata`.
1675/// WARNING: param names don't match C — Rust=(dir, dir_index) vs C=(dirp)
1676pub fn hashdir(dir: &str, dir_index: usize) {
1677 cmdnamtab_lock()
1678 .write()
1679 .expect("cmdnamtab poisoned")
1680 .hash_dir(dir, dir_index);
1681}
1682
1683/// Port of `fillcmdnamtable(UNUSED(HashTable ht))` from `Src/hashtable.c:712`.
1684///
1685/// C body:
1686/// ```c
1687/// for (pq = pathchecked; *pq; pq++) hashdir(pq);
1688/// pathchecked = pq;
1689/// ```
1690///
1691/// Walks every PATH entry calling `hashdir` for each. The
1692/// `pathchecked` cursor is updated so subsequent calls don't
1693/// re-walk PATH entries that were already scanned.
1694/// WARNING: param names don't match C — Rust=(path) vs C=(ht)
1695pub fn fillcmdnamtable(path: &[String]) {
1696 // c:716 — `for (pq = pathchecked; *pq; pq++) hashdir(pq);`. Start
1697 // from the cursor, NOT index 0: dirs already walked by an earlier
1698 // fill (or by `hashcmd`, which bumps `pathchecked`) must not be
1699 // re-scanned. Re-filling from 0 on every call made
1700 // `${(k)commands}` / `compadd -k commands` return the entire PATH
1701 // even when `pathchecked` was exhausted and the table had been
1702 // emptied — diverging from zsh, whose scan yields the current
1703 // (possibly empty) table. Symptom: `l<TAB>` listed all 230 PATH
1704 // commands vs zsh's 5 builtins.
1705 use std::sync::atomic::Ordering;
1706 let from = pathchecked.load(Ordering::SeqCst);
1707 if from < path.len() {
1708 let mut tab = cmdnamtab_lock().write().expect("cmdnamtab poisoned");
1709 for idx in from..path.len() {
1710 tab.hash_dir(&path[idx], idx);
1711 }
1712 }
1713 // c:719 — `pathchecked = pq;` — cursor advances to the end.
1714 pathchecked.store(path.len(), Ordering::SeqCst);
1715}
1716
1717/// Port of `freecmdnamnode(HashNode hn)` from `Src/hashtable.c:724`.
1718///
1719/// C body frees the entry's name + (if HASHED) cached path. Rust
1720/// port: drop runs both when the entry is removed from the table.
1721/// This helper performs the removal to trigger Drop.
1722pub fn freecmdnamnode(hn: &str) {
1723 cmdnamtab_lock()
1724 .write()
1725 .expect("cmdnamtab poisoned")
1726 .remove(hn);
1727}
1728
1729/// Port of `printcmdnamnode(HashNode hn, int printflags)` from `Src/hashtable.c:739`.
1730///
1731/// Emits one cmdnamtab entry for `hash` / `whence`. Each branch
1732/// returns; PRINT_LIST falls through to the tail that emits
1733/// `quotedzputs(nam) '=' quotedzputs(u.cmd|*u.name '/' nam) '\n'`.
1734pub fn printcmdnamnode(hn: &cmdnam, printflags: i32) {
1735 // c:741 — `Cmdnam cn = (Cmdnam) hn;` — Rust types give us cmdnam.
1736
1737 // c:743-747 — PRINT_WHENCE_WORD branch.
1738 if (printflags & PRINT_WHENCE_WORD) != 0 {
1739 // c:744-745 — `printf("%s: %s\n", nam, HASHED ? "hashed" : "command");`
1740 let kind = if (hn.node.flags & HASHED as i32) != 0 {
1741 "hashed"
1742 } else {
1743 "command"
1744 };
1745 println!("{}: {}", hn.node.nam, kind); // c:744
1746 return; // c:746
1747 }
1748
1749 // c:749-760 — PRINT_WHENCE_CSH | PRINT_WHENCE_SIMPLE branch.
1750 if (printflags & (PRINT_WHENCE_CSH | PRINT_WHENCE_SIMPLE)) != 0 {
1751 let mut so = io::stdout();
1752 if (hn.node.flags & HASHED as i32) != 0 {
1753 // c:750
1754 // c:751-752 — `zputs(u.cmd, stdout); putchar('\n');`
1755 if let Some(cmd) = &hn.cmd {
1756 let _ = zputs(cmd, &mut so); // c:751
1757 }
1758 println!(); // c:752
1759 } else {
1760 // c:753
1761 // c:754-757 — `zputs(*u.name); putchar('/'); zputs(nam); putchar('\n');`
1762 if let Some(name_arr) = &hn.name {
1763 if let Some(first) = name_arr.first() {
1764 let _ = zputs(first, &mut so); // c:754
1765 }
1766 }
1767 print!("/"); // c:755
1768 let _ = zputs(&hn.node.nam, &mut so); // c:756
1769 println!(); // c:757
1770 }
1771 return; // c:759
1772 }
1773
1774 // c:762-777 — PRINT_WHENCE_VERBOSE branch.
1775 if (printflags & PRINT_WHENCE_VERBOSE) != 0 {
1776 let mut so = io::stdout();
1777 if (hn.node.flags & HASHED as i32) != 0 {
1778 // c:763
1779 // c:764-767 — `nicezputs(nam); printf(" is hashed to "); nicezputs(u.cmd); putchar('\n');`
1780 let _ = nicezputs(&hn.node.nam, &mut so); // c:764
1781 print!(" is hashed to "); // c:765
1782 if let Some(cmd) = &hn.cmd {
1783 let _ = nicezputs(cmd, &mut so); // c:766
1784 }
1785 println!(); // c:767
1786 } else {
1787 // c:768
1788 // c:769-774 — `nicezputs(nam); printf(" is "); nicezputs(*u.name); putchar('/'); nicezputs(nam); putchar('\n');`
1789 let _ = nicezputs(&hn.node.nam, &mut so); // c:769
1790 print!(" is "); // c:770
1791 if let Some(name_arr) = &hn.name {
1792 if let Some(first) = name_arr.first() {
1793 let _ = nicezputs(first, &mut so); // c:771
1794 }
1795 }
1796 print!("/"); // c:772
1797 let _ = nicezputs(&hn.node.nam, &mut so); // c:773
1798 println!(); // c:774
1799 }
1800 return; // c:776
1801 }
1802
1803 // c:779-784 — PRINT_LIST prefix block; falls through to the tail.
1804 if (printflags & PRINT_LIST) != 0 {
1805 // c:779
1806 print!("hash "); // c:780
1807 // c:782-783 — `-- ` for names starting with `-`.
1808 if hn.node.nam.starts_with('-') {
1809 // c:782
1810 print!("-- "); // c:783
1811 }
1812 }
1813
1814 // c:786-798 — common tail. HASHED uses u.cmd, !HASHED splices first
1815 // u.name PATH segment + '/' + nam.
1816 // ZSHRS-ONLY: bash prints `hits<TAB>path` rows under a header and
1817 // dash prints the bare path; only the Korn shells and zsh use the
1818 // `name=path` form below. See `emulation_output::hash_entry`.
1819 {
1820 let resolved = if (hn.node.flags & HASHED as i32) != 0 {
1821 hn.cmd.clone().unwrap_or_default()
1822 } else {
1823 let dir = hn
1824 .name
1825 .as_ref()
1826 .and_then(|a| a.first().cloned())
1827 .unwrap_or_default();
1828 format!("{dir}/{}", hn.node.nam)
1829 };
1830 if let Some(row) =
1831 crate::extensions::emulation_output::hash_entry(&hn.node.nam, &resolved)
1832 {
1833 println!("{row}");
1834 return;
1835 }
1836 }
1837 if (hn.node.flags & HASHED as i32) != 0 {
1838 // c:786
1839 let _ = io::Write::write_all(&mut io::stdout(), &unmetafy_str("edzputs(&hn.node.nam))); // c:787
1840 print!("="); // c:788
1841 if let Some(cmd) = &hn.cmd {
1842 let _ = io::Write::write_all(&mut io::stdout(), &unmetafy_str("edzputs(cmd))); // c:789
1843 }
1844 println!(); // c:790
1845 } else {
1846 // c:791
1847 let _ = io::Write::write_all(&mut io::stdout(), &unmetafy_str("edzputs(&hn.node.nam))); // c:792
1848 print!("="); // c:793
1849 if let Some(name_arr) = &hn.name {
1850 if let Some(first) = name_arr.first() {
1851 let _ = io::Write::write_all(&mut io::stdout(), &unmetafy_str("edzputs(first))); // c:794
1852 }
1853 }
1854 print!("/"); // c:795
1855 let _ = io::Write::write_all(&mut io::stdout(), &unmetafy_str("edzputs(&hn.node.nam))); // c:796
1856 println!(); // c:797
1857 }
1858}
1859
1860/// Port of `createshfunctable()` from `Src/hashtable.c:812`.
1861///
1862/// C body:
1863/// ```c
1864/// shfunctab = newhashtable(7, "shfunctab", NULL);
1865/// shfunctab->hash = hasher;
1866/// shfunctab->cmpnodes = strcmp;
1867/// shfunctab->addnode = addhashnode;
1868/// shfunctab->getnode = gethashnode;
1869/// shfunctab->getnode2 = gethashnode2;
1870/// shfunctab->removenode = removeshfuncnode;
1871/// shfunctab->disablenode = disableshfuncnode;
1872/// shfunctab->enablenode = enableshfuncnode;
1873/// shfunctab->freenode = freeshfuncnode;
1874/// shfunctab->printnode = printshfuncnode;
1875/// ```
1876///
1877/// Rust port: idempotent — touching the OnceLock initialises the
1878/// singleton on first call. The GSU function-pointer assignments
1879/// from C are encoded as the free-fn names below (each callable
1880/// directly without a vtable lookup).
1881pub fn createshfunctable() {
1882 let _ = shfunctab_lock();
1883}
1884
1885/// Port of `removeshfuncnode(UNUSED(HashTable ht), const char *nam)` from `Src/hashtable.c:836`.
1886///
1887/// C body:
1888/// ```c
1889/// if (!strncmp(nam, "TRAP", 4) && (sigidx = getsigidx(nam + 4)) != -1)
1890/// hn = removetrap(sigidx);
1891/// else
1892/// hn = removehashnode(shfunctab, nam);
1893/// return hn;
1894/// ```
1895///
1896/// Drops the named function from `shfunctab`. If the name is a
1897/// `TRAP<sig>` form, also clears the trap via signals.rs.
1898/// Returns the removed function (or None if absent).
1899/// WARNING: param names don't match C — Rust=(nam) vs C=(ht, nam)
1900pub fn removeshfuncnode(nam: &str) -> Option<shfunc> {
1901 // c:841-844 — the two arms are EXCLUSIVE:
1902 // if (!strncmp(nam, "TRAP", 4) && (sigidx = getsigidx(nam + 4)) != -1)
1903 // hn = removetrap(sigidx);
1904 // else
1905 // hn = removehashnode(shfunctab, nam);
1906 // The Rust port ran BOTH: `removetrap` already pulls the TRAP<SIG>
1907 // node out of shfunctab (signals.rs c:832-841), so the follow-up
1908 // `remove` returned None and the caller (`bin_unhash`, c:4405) read
1909 // that as "no such hash table element". `unfunction TRAPZERR` then
1910 // reported an error AND — because the second remove ran outside
1911 // removetrap's dosavetrap window — the localtraps restore had
1912 // nothing to put back (C03traps:13,14).
1913 if let Some(sig_part) = nam.strip_prefix("TRAP") {
1914 // c:841
1915 if let Some(sig) = getsigidx(sig_part) {
1916 return removetrap(sig); // c:842
1917 }
1918 }
1919 // c:844 — `hn = removehashnode(shfunctab, nam);`
1920 shfunctab_lock()
1921 .write()
1922 .expect("shfunctab poisoned")
1923 .remove(nam)
1924}
1925
1926/// Port of `disableshfuncnode(HashNode hn, UNUSED(int flags))` from `Src/hashtable.c:855`.
1927///
1928/// C body:
1929/// ```c
1930/// hn->flags |= DISABLED;
1931/// if (!strncmp(hn->nam, "TRAP", 4)) {
1932/// int sigidx = getsigidx(hn->nam + 4);
1933/// if (sigidx != -1) {
1934/// sigtrapped[sigidx] &= ~ZSIG_FUNC;
1935/// unsettrap(sigidx);
1936/// }
1937/// }
1938/// ```
1939///
1940/// Sets the DISABLED flag on the function entry; for TRAP*
1941/// functions, also unsettraps the corresponding signal so the
1942/// shell stops invoking the (now-disabled) trap.
1943/// WARNING: param names don't match C — Rust=(hn) vs C=(hn, flags)
1944pub fn disableshfuncnode(hn: &str) {
1945 {
1946 let mut tab = shfunctab_lock().write().expect("shfunctab poisoned");
1947 tab.disable(hn);
1948 }
1949 if let Some(sig_part) = hn.strip_prefix("TRAP") {
1950 if let Some(sig) = getsigidx(sig_part) {
1951 unsettrap(sig);
1952 }
1953 }
1954}
1955
1956/// Port of `enableshfuncnode(HashNode hn, UNUSED(int flags))` from `Src/hashtable.c:873`.
1957///
1958/// C body:
1959/// ```c
1960/// shf->node.flags &= ~DISABLED;
1961/// if (!strncmp(shf->node.nam, "TRAP", 4)) {
1962/// int sigidx = getsigidx(shf->node.nam + 4);
1963/// if (sigidx != -1) settrap(sigidx, NULL, ZSIG_FUNC);
1964/// }
1965/// ```
1966///
1967/// Clears the DISABLED flag; for TRAP* functions, re-installs
1968/// the signal handler with `ZSIG_FUNC` semantics so the shell
1969/// dispatches the trap function on the next signal delivery.
1970/// WARNING: param names don't match C — Rust=(hn) vs C=(hn, flags)
1971pub fn enableshfuncnode(hn: &str) {
1972 {
1973 let mut tab = shfunctab_lock().write().expect("shfunctab poisoned");
1974 tab.enable(hn);
1975 }
1976 if let Some(sig_part) = hn.strip_prefix("TRAP") {
1977 if let Some(sig) = getsigidx(sig_part) {
1978 // c:882 — `settrap(sigidx, NULL, ZSIG_FUNC)`. The TRAPxxx
1979 // function body resolves through shfunctab at dispatch
1980 // (`gettrapnode`), not via the trap arrays directly.
1981 let _ = settrap(sig, None, ZSIG_FUNC);
1982 // c:Src/signals.c::settrap → unsettrap → removetrap also
1983 // clears any previously-registered string-form trap for
1984 // the same signal (single-slot sigtrapped[] array). The
1985 // zshrs port stores string-form bodies in a separate
1986 // `traps_table` HashMap that `removetrap` doesn't touch,
1987 // so the string body survives the function-form
1988 // registration and BOTH fire on the next signal. Drop
1989 // the string-form entry here so dotrap's
1990 // `traps_table` fallback doesn't double-dispatch. Bug
1991 // #541 in docs/BUGS.md.
1992 if let Ok(mut t) = crate::ported::builtin::traps_table().lock() {
1993 t.remove(sig_part);
1994 }
1995 }
1996 }
1997}
1998
1999/// Port of `freeshfuncnode(HashNode hn)` from `Src/hashtable.c:888`.
2000///
2001/// C body frees the function name, body Eprog, redir Eprog,
2002/// filename string, and sticky options struct. Rust port: drop
2003/// runs all of this when the entry is removed; this helper just
2004/// removes from the table to trigger the drop chain.
2005/// Rust idiom replacement: `HashMap::remove` triggers the `Box<T>`
2006/// drop cascade — same teardown as the C zfree chain, automated.
2007pub fn freeshfuncnode(hn: &str) {
2008 shfunctab_lock()
2009 .write()
2010 .expect("shfunctab poisoned")
2011 .remove(hn);
2012}
2013
2014/// Port of `printshfuncnode(HashNode hn, int printflags)` from `Src/hashtable.c:914`.
2015///
2016/// Emits one shfunctab entry for `functions` / `whence` / `typeset -f`.
2017/// PRINT_NAMEONLY and the PRINT_WHENCE_* variants return early; the
2018/// default body emits the full re-parseable `name () { body }` form
2019/// including autoload-stub, traced markers, and trailing redirections.
2020pub fn printshfuncnode(hn: &shfunc, printflags: i32) {
2021 // c:916 — `Shfunc f = (Shfunc) hn;` — Rust types give us shfunc.
2022 // c:917 — `char *t = 0;` — declared but only used by the funcdef/redir
2023 // branches; Rust scope-locals the `t` binding inside each branch.
2024
2025 // c:919-925 — PRINT_NAMEONLY (or PRINT_WHENCE_SIMPLE without FUNCDEF):
2026 // `zputs(nam); putchar('\n'); return;`
2027 if (printflags & PRINT_NAMEONLY) != 0
2028 || ((printflags & PRINT_WHENCE_SIMPLE) != 0 && (printflags & PRINT_WHENCE_FUNCDEF) == 0)
2029 {
2030 let mut so = io::stdout();
2031 let _ = zputs(&hn.node.nam, &mut so); // c:922
2032 println!(); // c:923
2033 return; // c:924
2034 }
2035
2036 // c:927-944 — PRINT_WHENCE_VERBOSE | PRINT_WHENCE_WORD (without FUNCDEF):
2037 // nicezputs(nam) ":" function | " is an autoload shell function" | " is a shell function"
2038 // [" from " quotedzputs(filename) [(PM_LOADDIR) "/" quotedzputs(nam)]] '\n'
2039 if (printflags & (PRINT_WHENCE_VERBOSE | PRINT_WHENCE_WORD)) != 0
2040 && (printflags & PRINT_WHENCE_FUNCDEF) == 0
2041 {
2042 let mut so = io::stdout();
2043 let _ = nicezputs(&hn.node.nam, &mut so); // c:929
2044 // c:930-933 — printf one of three strings via nested ternary.
2045 let msg = if (printflags & PRINT_WHENCE_WORD) != 0 {
2046 ": function" // c:930
2047 } else if (hn.node.flags & PM_UNDEFINED as i32) != 0 {
2048 " is an autoload shell function" // c:932
2049 } else {
2050 " is a shell function" // c:933
2051 };
2052 print!("{}", msg);
2053 // c:934-941 — verbose-with-filename suffix.
2054 if (printflags & PRINT_WHENCE_VERBOSE) != 0 {
2055 if let Some(filename) = &hn.filename {
2056 // c:934
2057 print!(" from "); // c:935
2058 let _ = io::Write::write_all(&mut io::stdout(), &unmetafy_str("edzputs(filename))); // c:936
2059 if (hn.node.flags & PM_LOADDIR as i32) != 0 {
2060 // c:937
2061 print!("/"); // c:938
2062 let _ = io::Write::write_all(&mut io::stdout(), &unmetafy_str("edzputs(&hn.node.nam))); // c:939
2063 }
2064 }
2065 }
2066 println!(); // c:942
2067 return; // c:943
2068 }
2069
2070 // !!! EMULATION-ONLY (no C counterpart) !!! bash's `declare -f` / `typeset
2071 // -f` print the `NAME () \n{ \n body\n}` layout that `type` uses.
2072 if hn.body.is_some()
2073 && matches!(
2074 crate::extensions::emulation_startup::personality(),
2075 crate::extensions::emulation_startup::Personality::Bash
2076 )
2077 {
2078 print!(
2079 "{}",
2080 crate::dash_mode::bash_function_listing(&hn.node.nam, hn.body.as_deref())
2081 );
2082 return;
2083 }
2084
2085 // c:946 — `quotedzputs(nam, stdout);`
2086 let _ = io::Write::write_all(&mut io::stdout(), &unmetafy_str("edzputs(&hn.node.nam)));
2087
2088 // c:947-987 — funcdef-present branch (or PM_UNDEFINED stub) vs empty `() { }`.
2089 // RUST-ONLY EXTENSION: zshrs's shfunc carries a raw `body: Option<String>`
2090 // alongside (or instead of) the compiled `funcdef: Eprog` (see
2091 // `shfunc` doc at zsh_h.rs:670). The fusevm compile path stores the
2092 // body source there (parse.rs:7118 + parse.rs:1787) but never builds
2093 // a C-shaped Eprog — `funcdef` stays None. C zsh's getpermtext walks
2094 // the wordcode-Eprog; in zshrs we fall back to `body` text directly
2095 // when funcdef is absent. Without this, `functions NAME` prints
2096 // `f () { }` for every user-defined function.
2097 //
2098 // c:947 tests `f->funcdef`, which every DEFINED function has — an empty
2099 // `f() {}` holds an empty Eprog and lists as `f () {\n\t\n}`. Only a
2100 // node with no program at all takes the c:986 ` () { }` arm, so an
2101 // empty `body` still counts as present.
2102 let has_body_source = hn.body.is_some();
2103 if hn.funcdef.is_some() || has_body_source || (hn.node.flags & PM_UNDEFINED as i32) != 0 {
2104 // c:947
2105 print!(" () {{\n"); // c:948
2106 let _ = zoutputtab(&mut io::stdout()); // c:949
2107 // c:950-954 — `# undefined` marker or getpermtext body.
2108 let mut t: Option<String>;
2109 if (hn.node.flags & PM_UNDEFINED as i32) != 0 {
2110 // c:950
2111 println!(
2112 "{} undefined",
2113 hashchar.load(Ordering::Relaxed) as u8 as char
2114 ); // c:951
2115 let _ = zoutputtab(&mut io::stdout()); // c:952
2116 t = None;
2117 } else if let Some(fd) = hn.funcdef.as_ref() {
2118 // c:953
2119 t = Some(getpermtext(fd.clone(), None, 1)); // c:954
2120 } else {
2121 // Rust-only fallback: emit `body` text directly. C's
2122 // getpermtext walks the wordcode-Eprog and emits
2123 // canonicalized statement-per-line text. We don't have
2124 // the Eprog, so we normalize the captured raw body
2125 // source: strip a leading `{` + ws, trailing `}` + ws,
2126 // and trailing `;` before the `}`. These appear when
2127 // par_simple's body_argv path (parse.rs:7041) reuses
2128 // the raw input slice that still includes the framing
2129 // braces; par_funcdef's brace path strips them but the
2130 // short-form `name() { body }` path can capture the
2131 // closing `}` because cmdpos vs. cmdpos confusion
2132 // makes the `}` lex as STRING_LEX, missing the
2133 // OUTBRACE_TOK arm at parse.rs:7113.
2134 // c:Src/text.c gettext2 — C zsh re-emits function bodies
2135 // from parsed wordcode (`getpermtext`) with `\n\t` between
2136 // sibling statements AND recursive indenting of nested
2137 // function definitions. zshrs stores raw source (no Eprog
2138 // for shfunc bodies); the closure below applies the same
2139 // canonicalization at print time.
2140 // Bug #197 (top-level statements) + #124 (nested fns) in
2141 // docs/BUGS.md.
2142 //
2143 // canonicalize: walk char-by-char tracking quote state +
2144 // brace/paren depth. At brace_depth == 0:
2145 // - top-level `;` (or `; `) becomes `\n\t` * (depth+1)
2146 // - `name() {` or `name () {` opens a nested fn def;
2147 // emit `name () {\n` then recurse on the body until
2148 // the matching `}` with depth+1, then `\n\t` * depth
2149 // + `}`.
2150 let canonicalize_body = |source: &str| -> String {
2151 fn fmt_body(s: &str, depth: usize, lead: bool) -> String {
2152 let chars: Vec<char> = s.chars().collect();
2153 let mut out = String::with_capacity(s.len());
2154 let mut in_sq = false;
2155 let mut in_dq = false;
2156 let mut brace_depth: i32 = 0;
2157 let mut paren_depth: i32 = 0;
2158 let stmt_indent = "\t".repeat(depth);
2159 let mut i = 0;
2160 // NOTE: caller (the funcdef emit at hashtable.rs:1364)
2161 // already wrote one leading `\t` via zoutputtab. Don't
2162 // double-indent the first statement. With `lead`
2163 // (recursive nested-fn body), we DO need the leading
2164 // indent because the caller writes `name () {\n` then
2165 // recurses without a prior tab.
2166 if lead && !chars.is_empty() {
2167 out.push_str(&stmt_indent);
2168 }
2169 while i < chars.len() {
2170 let c = chars[i];
2171 if !in_sq && !in_dq && c == '\\' && i + 1 < chars.len() {
2172 out.push(c);
2173 out.push(chars[i + 1]);
2174 i += 2;
2175 continue;
2176 }
2177 if !in_dq && c == '\'' {
2178 in_sq = !in_sq;
2179 out.push(c);
2180 i += 1;
2181 continue;
2182 }
2183 if !in_sq && c == '"' {
2184 in_dq = !in_dq;
2185 out.push(c);
2186 i += 1;
2187 continue;
2188 }
2189 // Detect nested fn-def pattern at depth 0:
2190 // `name() {...}` or `name () {...}` (and
2191 // optional `function `-keyword form). Only
2192 // when in_sq/in_dq == false and brace/paren
2193 // depth == 0.
2194 if !in_sq && !in_dq && brace_depth == 0 && paren_depth == 0 {
2195 // c:Src/text.c gettext2 WC_CASE arm (~520) —
2196 // C re-emits case statements from wordcode as
2197 // case W in
2198 // (p | q) body ;;
2199 // esac
2200 // with `;;`/`;&`/`;|` per WC_CASE_TYPE. The
2201 // generic `;`-break below ATE the `;;` (it
2202 // skips runs of `;`), so `functions f`
2203 // displayed case bodies without terminators
2204 // and with `;&` mangled to `&`. Re-render the
2205 // whole case..esac region case-aware.
2206 if out.is_empty() || out.ends_with('\n') || out.ends_with('\t') {
2207 if let Some((next_i, txt)) = try_render_case(&chars, i, depth) {
2208 out.push_str(&txt);
2209 i = next_i;
2210 // Consume trailing `;` + ws; emit a
2211 // statement break if more follows.
2212 while i < chars.len()
2213 && (chars[i] == ' '
2214 || chars[i] == '\t'
2215 || chars[i] == '\n'
2216 || chars[i] == ';')
2217 {
2218 i += 1;
2219 }
2220 if i < chars.len() {
2221 out.push('\n');
2222 out.push_str(&stmt_indent);
2223 }
2224 continue;
2225 }
2226 }
2227 // Try to match `<ident>\s*\(\s*\)\s*\{` at
2228 // current position, OR `function\s+<ident>...{`.
2229 let fn_start = try_match_fn_def(&chars, i);
2230 if let Some((header_end, name_str)) = fn_start {
2231 // Find matching `}` for the body.
2232 let body_open = header_end; // index just after `{`
2233 let body_close = find_matching_brace(&chars, body_open - 1);
2234 if let Some(close_idx) = body_close {
2235 let body_src: String =
2236 chars[body_open..close_idx].iter().collect();
2237 let body_trim = body_src
2238 .trim_start_matches(|c: char| c.is_whitespace())
2239 .trim_end_matches(|c: char| c.is_whitespace() || c == ';')
2240 .to_string();
2241 out.push_str(&name_str);
2242 out.push_str(" () {\n");
2243 out.push_str(&fmt_body(&body_trim, depth + 1, true));
2244 out.push('\n');
2245 out.push_str(&stmt_indent);
2246 out.push('}');
2247 i = close_idx + 1;
2248 // Consume trailing `;` and ws.
2249 let saved = i;
2250 while i < chars.len()
2251 && (chars[i] == ' ' || chars[i] == '\t' || chars[i] == ';')
2252 {
2253 i += 1;
2254 }
2255 if saved != i || i < chars.len() {
2256 // More content follows — emit
2257 // statement break.
2258 if i < chars.len() {
2259 out.push('\n');
2260 out.push_str(&stmt_indent);
2261 }
2262 }
2263 continue;
2264 }
2265 }
2266 }
2267 if !in_sq && !in_dq {
2268 match c {
2269 '{' => brace_depth += 1,
2270 '}' => brace_depth = (brace_depth - 1).max(0),
2271 '(' => paren_depth += 1,
2272 ')' => paren_depth = (paren_depth - 1).max(0),
2273 _ => {}
2274 }
2275 }
2276 if !in_sq && !in_dq && brace_depth == 0 && paren_depth == 0 && c == ';' {
2277 i += 1;
2278 while i < chars.len()
2279 && (chars[i] == ' ' || chars[i] == '\t' || chars[i] == ';')
2280 {
2281 i += 1;
2282 }
2283 if i < chars.len() {
2284 out.push('\n');
2285 out.push_str(&stmt_indent);
2286 }
2287 continue;
2288 }
2289 out.push(c);
2290 i += 1;
2291 }
2292 out
2293 }
2294 fn is_ident_byte(b: u8) -> bool {
2295 b == b'_' || b.is_ascii_alphanumeric()
2296 }
2297 fn try_match_fn_def(chars: &[char], start: usize) -> Option<(usize, String)> {
2298 // Skip leading `function ` keyword (optional).
2299 let mut i = start;
2300 let _function_prefix = {
2301 let rest: String = chars[i..].iter().collect();
2302 if rest.starts_with("function ") || rest.starts_with("function\t") {
2303 i += "function".len();
2304 while i < chars.len() && (chars[i] == ' ' || chars[i] == '\t') {
2305 i += 1;
2306 }
2307 true
2308 } else {
2309 false
2310 }
2311 };
2312 // Match identifier.
2313 let name_start = i;
2314 while i < chars.len() && is_ident_byte(chars[i] as u8) {
2315 i += 1;
2316 }
2317 if i == name_start {
2318 return None;
2319 }
2320 let name: String = chars[name_start..i].iter().collect();
2321 // Skip optional whitespace.
2322 while i < chars.len() && (chars[i] == ' ' || chars[i] == '\t') {
2323 i += 1;
2324 }
2325 // Require `()` for the non-`function`-keyword form;
2326 // C zsh accepts `function name { ... }` without parens.
2327 if i < chars.len() && chars[i] == '(' {
2328 i += 1;
2329 while i < chars.len() && (chars[i] == ' ' || chars[i] == '\t') {
2330 i += 1;
2331 }
2332 if i >= chars.len() || chars[i] != ')' {
2333 return None;
2334 }
2335 i += 1;
2336 } else if !_function_prefix {
2337 return None;
2338 }
2339 // Skip ws + `{`.
2340 while i < chars.len() && (chars[i] == ' ' || chars[i] == '\t') {
2341 i += 1;
2342 }
2343 if i >= chars.len() || chars[i] != '{' {
2344 return None;
2345 }
2346 Some((i + 1, name))
2347 }
2348 /// Keyword match at `i` with word boundaries on both
2349 /// sides (start/ws/`;` before; end/ws/`;` after).
2350 fn matches_kw(chars: &[char], i: usize, kw: &str) -> bool {
2351 let kl = kw.len();
2352 if i + kl > chars.len() {
2353 return false;
2354 }
2355 if !chars[i..i + kl].iter().copied().eq(kw.chars()) {
2356 return false;
2357 }
2358 let before_ok = i == 0 || chars[i - 1].is_whitespace() || chars[i - 1] == ';';
2359 let after_ok = i + kl == chars.len()
2360 || chars[i + kl].is_whitespace()
2361 || chars[i + kl] == ';';
2362 before_ok && after_ok
2363 }
2364 /// Split a case-pattern alternation on top-level `|`
2365 /// (quote/paren aware), trimming each alternative —
2366 /// `a|b` → ["a", "b"], rendered `(a | b)` like
2367 /// C:Src/text.c gettext2's `taddstr(" | ")` walk.
2368 fn split_top_bar(pat: &str) -> Vec<String> {
2369 let chars: Vec<char> = pat.chars().collect();
2370 let mut alts = Vec::new();
2371 let mut cur = String::new();
2372 let (mut in_sq, mut in_dq) = (false, false);
2373 let mut pd = 0i32;
2374 let mut i = 0;
2375 while i < chars.len() {
2376 let c = chars[i];
2377 if !in_sq && !in_dq && c == '\\' && i + 1 < chars.len() {
2378 cur.push(c);
2379 cur.push(chars[i + 1]);
2380 i += 2;
2381 continue;
2382 }
2383 if !in_dq && c == '\'' {
2384 in_sq = !in_sq;
2385 } else if !in_sq && c == '"' {
2386 in_dq = !in_dq;
2387 } else if !in_sq && !in_dq {
2388 match c {
2389 '(' => pd += 1,
2390 ')' => pd = (pd - 1).max(0),
2391 '|' if pd == 0 => {
2392 alts.push(cur.trim().to_string());
2393 cur.clear();
2394 i += 1;
2395 continue;
2396 }
2397 _ => {}
2398 }
2399 }
2400 cur.push(c);
2401 i += 1;
2402 }
2403 alts.push(cur.trim().to_string());
2404 alts
2405 }
2406 /// Case-aware re-render of one `case W in … esac`
2407 /// region starting at `start` (which must point at the
2408 /// `case` keyword). Returns (index-after-`esac`,
2409 /// rendered text) or None when the region doesn't
2410 /// parse (caller falls back to generic emission).
2411 /// Output shape mirrors C:Src/text.c gettext2 WC_CASE:
2412 /// case W in
2413 /// \t(p | q) body ;;
2414 /// esac
2415 /// with arm bodies recursively formatted at depth+2
2416 /// (continuation statements land one level deeper than
2417 /// the arm line, matching zsh 5.9 output).
2418 fn try_render_case(
2419 chars: &[char],
2420 start: usize,
2421 depth: usize,
2422 ) -> Option<(usize, String)> {
2423 let n = chars.len();
2424 let mut i = start;
2425 if !matches_kw(chars, i, "case") {
2426 return None;
2427 }
2428 i += 4;
2429 if i >= n || !chars[i].is_whitespace() {
2430 return None;
2431 }
2432 while i < n && chars[i].is_whitespace() {
2433 i += 1;
2434 }
2435 // Scrutinee word: scan to unquoted whitespace.
2436 let word_start = i;
2437 let (mut in_sq, mut in_dq) = (false, false);
2438 while i < n {
2439 let c = chars[i];
2440 if !in_sq && !in_dq && c == '\\' && i + 1 < n {
2441 i += 2;
2442 continue;
2443 }
2444 if !in_dq && c == '\'' {
2445 in_sq = !in_sq;
2446 } else if !in_sq && c == '"' {
2447 in_dq = !in_dq;
2448 } else if !in_sq && !in_dq && c.is_whitespace() {
2449 break;
2450 }
2451 i += 1;
2452 }
2453 if i == word_start || in_sq || in_dq {
2454 return None;
2455 }
2456 let word: String = chars[word_start..i].iter().collect();
2457 while i < n && chars[i].is_whitespace() {
2458 i += 1;
2459 }
2460 if !matches_kw(chars, i, "in") {
2461 return None;
2462 }
2463 i += 2;
2464 let indent_case = "\t".repeat(depth);
2465 let indent_arm = "\t".repeat(depth + 1);
2466 let mut rendered = format!("case {} in", word);
2467 loop {
2468 while i < n && chars[i].is_whitespace() {
2469 i += 1;
2470 }
2471 if i >= n {
2472 return None; // unterminated — bail out
2473 }
2474 if matches_kw(chars, i, "esac") {
2475 i += 4;
2476 break;
2477 }
2478 // Pattern: optional leading `(`, alts to `)`.
2479 if chars[i] == '(' {
2480 i += 1;
2481 }
2482 let pat_start = i;
2483 let (mut in_sq, mut in_dq) = (false, false);
2484 let mut pd = 0i32;
2485 while i < n {
2486 let c = chars[i];
2487 if !in_sq && !in_dq && c == '\\' && i + 1 < n {
2488 i += 2;
2489 continue;
2490 }
2491 if !in_dq && c == '\'' {
2492 in_sq = !in_sq;
2493 } else if !in_sq && c == '"' {
2494 in_dq = !in_dq;
2495 } else if !in_sq && !in_dq {
2496 if c == '(' {
2497 pd += 1;
2498 } else if c == ')' {
2499 if pd == 0 {
2500 break;
2501 }
2502 pd -= 1;
2503 }
2504 }
2505 i += 1;
2506 }
2507 if i >= n || chars[i] != ')' {
2508 return None;
2509 }
2510 let pat_src: String = chars[pat_start..i].iter().collect();
2511 i += 1; // consume `)`
2512 while i < n && (chars[i] == ' ' || chars[i] == '\t') {
2513 i += 1;
2514 }
2515 // Body: scan to `;;`/`;&`/`;|` or this case's
2516 // `esac` (last arm without terminator), quote/
2517 // paren/brace aware, nested case…esac tracked.
2518 let body_start = i;
2519 let mut body_end = i;
2520 let mut term: Option<&str> = None;
2521 let (mut in_sq, mut in_dq) = (false, false);
2522 let (mut bd, mut pd) = (0i32, 0i32);
2523 let mut nested_case = 0i32;
2524 loop {
2525 if i >= n {
2526 return None; // unterminated — bail out
2527 }
2528 let c = chars[i];
2529 if !in_sq && !in_dq && c == '\\' && i + 1 < n {
2530 i += 2;
2531 continue;
2532 }
2533 if !in_dq && c == '\'' {
2534 in_sq = !in_sq;
2535 i += 1;
2536 continue;
2537 }
2538 if !in_sq && c == '"' {
2539 in_dq = !in_dq;
2540 i += 1;
2541 continue;
2542 }
2543 if in_sq || in_dq {
2544 i += 1;
2545 continue;
2546 }
2547 match c {
2548 '{' => bd += 1,
2549 '}' => bd = (bd - 1).max(0),
2550 '(' => pd += 1,
2551 ')' => pd = (pd - 1).max(0),
2552 _ => {}
2553 }
2554 if bd == 0 && pd == 0 {
2555 if matches_kw(chars, i, "case") {
2556 nested_case += 1;
2557 i += 4;
2558 continue;
2559 }
2560 if matches_kw(chars, i, "esac") {
2561 if nested_case == 0 {
2562 body_end = i;
2563 break;
2564 }
2565 nested_case -= 1;
2566 i += 4;
2567 continue;
2568 }
2569 if nested_case == 0
2570 && c == ';'
2571 && i + 1 < n
2572 && matches!(chars[i + 1], ';' | '&' | '|')
2573 {
2574 body_end = i;
2575 term = Some(match chars[i + 1] {
2576 ';' => ";;",
2577 '&' => ";&",
2578 _ => ";|",
2579 });
2580 i += 2;
2581 break;
2582 }
2583 }
2584 i += 1;
2585 }
2586 let body_src: String = chars[body_start..body_end].iter().collect();
2587 let body_trim = body_src.trim().trim_end_matches(';').trim_end();
2588 rendered.push('\n');
2589 rendered.push_str(&indent_arm);
2590 rendered.push('(');
2591 rendered.push_str(&split_top_bar(&pat_src).join(" | "));
2592 rendered.push_str(") ");
2593 rendered.push_str(&fmt_body(body_trim, depth + 2, false));
2594 if let Some(t) = term {
2595 rendered.push(' ');
2596 rendered.push_str(t);
2597 }
2598 }
2599 rendered.push('\n');
2600 rendered.push_str(&indent_case);
2601 rendered.push_str("esac");
2602 Some((i, rendered))
2603 }
2604 fn find_matching_brace(chars: &[char], open: usize) -> Option<usize> {
2605 let mut depth = 1i32;
2606 let mut in_sq = false;
2607 let mut in_dq = false;
2608 let mut j = open + 1;
2609 while j < chars.len() {
2610 let c = chars[j];
2611 if !in_sq && !in_dq && c == '\\' && j + 1 < chars.len() {
2612 j += 2;
2613 continue;
2614 }
2615 if !in_dq && c == '\'' {
2616 in_sq = !in_sq;
2617 } else if !in_sq && c == '"' {
2618 in_dq = !in_dq;
2619 } else if !in_sq && !in_dq {
2620 if c == '{' {
2621 depth += 1;
2622 } else if c == '}' {
2623 depth -= 1;
2624 if depth == 0 {
2625 return Some(j);
2626 }
2627 }
2628 }
2629 j += 1;
2630 }
2631 None
2632 }
2633 let mut s = source.trim().to_string();
2634 if s.starts_with('{') {
2635 s = s[1..].trim_start().to_string();
2636 }
2637 if s.ends_with('}') {
2638 s.pop();
2639 s = s.trim_end().to_string();
2640 }
2641 if s.ends_with(';') {
2642 s.pop();
2643 s = s.trim_end().to_string();
2644 }
2645 fmt_body(&s, 1, false)
2646 };
2647 // c:954 — `t = getpermtext(fd, NULL, 1);`. C holds the body as
2648 // compiled wordcode and renders it back to source with
2649 // getpermtext, which is what produces zsh's canonical layout:
2650 // `do`/`then` on their own line with the body indented under
2651 // them, `(` and `)` broken onto separate lines, an `always`
2652 // block re-emitted as `{ … } always { … }`, and a trailing
2653 // space after every assignment (taddassign, c:203-204).
2654 //
2655 // zshrs only has an Eprog for zwc-loaded functions (the
2656 // `hn.funcdef` branch above); shell-defined ones keep their raw
2657 // source, which is why this branch existed. Re-parse that source
2658 // and render it through the SAME deparser rather than
2659 // re-deriving getpermtext's formatting rules by hand —
2660 // canonicalize_body reproduced the flat cases but not the
2661 // indenting ones, and mangled `always` blocks into
2662 // `print x } always { print y`.
2663 //
2664 // The source is parsed as-is. hn.body arrives with the framing
2665 // `{ }` of `name() { … }` ALREADY stripped, so removing a
2666 // leading `{` here would eat the braces of a body whose first
2667 // command is itself a brace group (`f() { { print x } }`) and
2668 // would leave an always-block body unbalanced. That
2669 // unconditional strip is exactly why canonicalize_body below
2670 // rendered `f() { { print x } always { print y } }` as
2671 // `print x } always { print y`.
2672 //
2673 // parse_string is the wordcode parser, whose coverage is
2674 // narrower than the AST parser that executes these bodies, so
2675 // fall back to canonicalize_body when it can't take the source
2676 // rather than losing the listing entirely.
2677 //
2678 // That re-parse is a SECOND lex of text C never lexes twice, so
2679 // every lexer-time option the deparse depends on has to be put
2680 // back the way it was when the function was defined, or the same
2681 // function prints two different ways across a `setopt`. RCQUOTES
2682 // is the one that does it: an adjacent quote pair inside a quoted
2683 // word lexes as one literal quote (c:Src/lex.c:1328) rather than
2684 // as two delimiters, so `f() { local v='it''s' }` came back as
2685 // `local v='it's'` once RCQUOTES was set — docs/BUGS.md #1105.
2686 // `funcdef_lex_pin` restores the value recorded when the
2687 // definition installed, and pins nothing when this body has no
2688 // record.
2689 let deparse_body = |source: &str| -> String {
2690 let _pin = crate::vm_helper::funcdef_lex_pin(&hn.node.nam, source);
2691 match crate::ported::exec::parse_string(source.trim(), 1) {
2692 Some(p) => crate::ported::text::getpermtext(Box::new(p), None, 1), // c:954
2693 None => canonicalize_body(source),
2694 }
2695 };
2696 t = hn.body.clone().map(|s| deparse_body(&s));
2697 }
2698 // c:955-958 — PM_TAGGED | PM_TAGGED_LOCAL → `# traced` marker.
2699 if (hn.node.flags & (PM_TAGGED | PM_TAGGED_LOCAL) as i32) != 0 {
2700 println!("{} traced", hashchar.load(Ordering::Relaxed) as u8 as char); // c:956
2701 let _ = zoutputtab(&mut io::stdout()); // c:957
2702 }
2703 // c:959-983 — no funcdef text → autoload stub; else emit text.
2704 if t.is_none() {
2705 // c:959
2706 // c:960-964 — `fopt = "UtTkzc"; flgs[] = { PM_UNALIASED, PM_TAGGED,
2707 // PM_TAGGED_LOCAL, PM_KSHSTORED, PM_ZSHSTORED, PM_CUR_FPATH, 0 };`
2708 let fopt: &[u8] = b"UtTkzc"; // c:960
2709 let flgs: [u32; 6] = [
2710 // c:961-964
2711 PM_UNALIASED,
2712 PM_TAGGED,
2713 PM_TAGGED_LOCAL,
2714 PM_KSHSTORED,
2715 PM_ZSHSTORED,
2716 PM_CUR_FPATH,
2717 ];
2718 let mut so = io::stdout();
2719 let _ = zputs("builtin autoload -X", &mut so); // c:967
2720 // c:968-969 — emit each fopt char whose flag is set.
2721 for fl in 0..fopt.len() {
2722 // c:968
2723 if (hn.node.flags & flgs[fl] as i32) != 0 {
2724 // c:969
2725 print!("{}", fopt[fl] as char); // c:969
2726 }
2727 }
2728 // c:970-973 — PM_LOADDIR with filename → ' ' + zputs(filename).
2729 if let Some(filename) = &hn.filename {
2730 if (hn.node.flags & PM_LOADDIR as i32) != 0 {
2731 // c:970
2732 print!(" "); // c:971
2733 let _ = zputs(filename, &mut so); // c:972
2734 }
2735 }
2736 } else {
2737 // c:974
2738 // c:975 — `zputs(t, stdout);`
2739 let body = t.take().unwrap();
2740 let mut so = io::stdout();
2741 let _ = zputs(&body, &mut so); // c:975
2742 // c:977-982 — funcdef.flags & EF_RUN → run-time suffix.
2743 let ef_run = hn
2744 .funcdef
2745 .as_ref()
2746 .map(|fd| (fd.flags & EF_RUN) != 0)
2747 .unwrap_or(false);
2748 if ef_run {
2749 // c:977
2750 println!(); // c:978
2751 let _ = zoutputtab(&mut io::stdout()); // c:979
2752 let _ = io::Write::write_all(&mut io::stdout(), &unmetafy_str("edzputs(&hn.node.nam))); // c:980
2753 print!(" \"$@\""); // c:981
2754 }
2755 }
2756 print!("\n}}"); // c:984
2757 } else {
2758 // c:985
2759 print!(" () {{ }}"); // c:986
2760 }
2761 // c:988-994 — redir present → emit its text.
2762 if let Some(redir) = &hn.redir {
2763 // c:988
2764 let t = getpermtext(redir.clone(), None, 1); // c:989
2765 if !t.is_empty() {
2766 // c:990
2767 let mut so = io::stdout();
2768 let _ = zputs(&t, &mut so); // c:991
2769 }
2770 } else if let Some(t) = &hn.redir_text {
2771 // RUST-ONLY twin of the branch above, exactly parallel to the
2772 // `body`-instead-of-`funcdef` fallback at c:947: the fusevm
2773 // definition path registers the redirection list as already-rendered
2774 // text rather than a second Eprog, so there is nothing for
2775 // `getpermtext` to walk. Without this, `functions f` / `which f`
2776 // dropped the `} > out` tail that C prints at c:991.
2777 if !t.is_empty() {
2778 let mut so = io::stdout();
2779 let _ = zputs(t, &mut so); // c:991
2780 }
2781 }
2782
2783 println!(); // c:996
2784}
2785
2786/// Port of `scanmatchshfunc(Patprog pprog, int sorted, int flags1, int flags2, ScanFunc scanfunc, int scanflags, int expand)` from `Src/hashtable.c:1013`.
2787///
2788/// C body iterates `shfunctab` and calls `func(node)` on every
2789/// entry whose name matches the compiled pattern `pprog`. Rust
2790/// port walks the singleton with a closure callback.
2791///
2792/// Returns the count of matched entries (mirrors C's int return).
2793/// WARNING: param names don't match C — Rust=(pattern, func) vs C=(pprog, sorted, flags1, flags2, scanfunc, scanflags, expand)
2794pub fn scanmatchshfunc<F>(pattern: Option<&str>, mut func: F) -> i32
2795where
2796 F: FnMut(&str, &shfunc),
2797{
2798 let tab = shfunctab_lock().read().expect("shfunctab poisoned");
2799 let mut count = 0;
2800 // c:Src/hashtable.c:1031 scanshfunc(sorted=1, …) — the `sorted`
2801 // flag is set on every internal caller (bin_functions's no-arg
2802 // listing, etc.), so scan walks entries in sorted order via
2803 // hnamcmp (byte-wise ASCII compare). The HashMap iter order is
2804 // arbitrary; collect + sort for parity.
2805 let mut entries: Vec<_> = tab.iter().collect();
2806 entries.sort_by(|(a, _), (b, _)| a.cmp(b));
2807 for (name, entry) in entries {
2808 let matches = match pattern {
2809 None => true,
2810 Some(p) => simple_glob_match(p, name),
2811 };
2812 if matches {
2813 func(name, entry);
2814 count += 1;
2815 }
2816 }
2817 count
2818}
2819
2820/// Port of `scanshfunc(int sorted, int flags1, int flags2, ScanFunc scanfunc, int scanflags, int expand)` from `Src/hashtable.c:1031`.
2821///
2822/// C body walks every `shfunctab` entry calling `func(node, flags)`.
2823/// Rust port delegates to scanmatchshfunc with no pattern.
2824/// WARNING: param names don't match C — Rust=(func) vs C=(sorted, flags1, flags2, scanfunc, scanflags, expand)
2825pub fn scanshfunc<F>(func: F) -> i32
2826where
2827 F: FnMut(&str, &shfunc),
2828{
2829 scanmatchshfunc(None, func)
2830}
2831
2832/// Port of `printshfuncexpand(HashNode hn, int printflags, int expand)` from `Src/hashtable.c:1042`.
2833///
2834/// C body:
2835/// ```c
2836/// int save_expand;
2837/// save_expand = text_expand_tabs;
2838/// text_expand_tabs = expand;
2839/// shfunctab->printnode(hn, printflags);
2840/// text_expand_tabs = save_expand;
2841/// ```
2842///
2843/// Briefly toggles `text_expand_tabs` around the printnode call so
2844/// the body indentation comes out either tab- or space-formatted
2845/// per the caller's `expand` arg.
2846pub fn printshfuncexpand(hn: &shfunc, printflags: i32, expand: i32) {
2847 // c:1044 — `int save_expand;`
2848 let save_expand: i32; // c:1044
2849 // c:1046 — `save_expand = text_expand_tabs;`
2850 save_expand = crate::text::TEXT_EXPAND_TABS.load(Ordering::Relaxed); // c:1046
2851 // c:1047 — `text_expand_tabs = expand;`
2852 crate::text::TEXT_EXPAND_TABS.store(expand, Ordering::Relaxed); // c:1047
2853 // c:1048 — `shfunctab->printnode(hn, printflags);`
2854 printshfuncnode(hn, printflags); // c:1048
2855 // c:1049 — `text_expand_tabs = save_expand;`
2856 crate::text::TEXT_EXPAND_TABS.store(save_expand, Ordering::Relaxed); // c:1049
2857}
2858
2859/// Port of `getshfuncfile(shfunc shf)` from `Src/hashtable.c:1059`.
2860///
2861/// C body (verbatim):
2862/// if (shf->node.flags & PM_LOADDIR) {
2863/// return zhtricat(shf->filename, "/", shf->node.nam);
2864/// } else if (shf->filename) {
2865/// return dupstring(shf->filename);
2866/// } else {
2867/// return NULL;
2868/// }
2869///
2870/// PM_LOADDIR is set when zsh loaded the function via fpath
2871/// directory autoload (the common `autoload -Uz` path): in that
2872/// case `filename` is the DIRECTORY and we must append `/name` to
2873/// produce the actual source file. Prior Rust port skipped the
2874/// PM_LOADDIR branch, so `${functions_source[my_autoload]}`
2875/// returned the fpath dir (e.g. `/usr/share/zsh/5.9/functions`)
2876/// instead of the real path (`.../functions/my_autoload`).
2877/// NOTE ON THE SIGNATURE: C takes the resolved node —
2878/// `getshfuncfile(Shfunc shf)` — so callers that already hold it (c:549
2879/// `pm->u.str = getshfuncfile(shf)`, c:589 `pm.u.str =
2880/// getshfuncfile((Shfunc)hn)`) reach the filename with no lookup at all.
2881/// This port is keyed by NAME, so those callers pay a second shfunctab
2882/// lock + hash. Callers on a whole-map path therefore inline the c:1061-1063
2883/// body against the node they already resolved rather than calling here.
2884pub fn getshfuncfile(shf: &str) -> Option<String> {
2885 let tab = shfunctab_lock().read().expect("shfunctab poisoned");
2886 let f = tab.get_including_disabled(shf)?;
2887 let filename = f.filename.as_ref()?;
2888 // c:1061 — PM_LOADDIR: `zhtricat(shf->filename, "/", shf->node.nam)`
2889 if (f.node.flags as u32 & crate::ported::zsh_h::PM_LOADDIR) != 0 {
2890 Some(format!("{}/{}", filename, f.node.nam))
2891 } else {
2892 // c:1063 — `dupstring(shf->filename)`
2893 Some(filename.clone())
2894 }
2895}
2896
2897/// Port of `createreswdtable()` from `Src/hashtable.c:1120`.
2898///
2899/// C body wires up the reswdtab GSU vtable then iterates the
2900/// static `reswds` array calling `addnode` for each. Rust port:
2901/// touches the singleton (which seeds the table from the static
2902/// word list in `reswd_table::new`).
2903pub fn createreswdtable() {
2904 let _ = reswdtab_lock();
2905}
2906
2907/// Port of `printreswdnode(HashNode hn, int printflags)` from `Src/hashtable.c:1147`.
2908///
2909/// C body:
2910/// ```c
2911/// Reswd rw = (Reswd) hn;
2912/// if (printflags & PRINT_WHENCE_WORD) {
2913/// printf("%s: reserved\n", rw->node.nam);
2914/// return;
2915/// }
2916/// if (printflags & PRINT_WHENCE_CSH) {
2917/// printf("%s: shell reserved word\n", rw->node.nam);
2918/// return;
2919/// }
2920/// if (printflags & PRINT_WHENCE_VERBOSE) {
2921/// printf("%s is a reserved word\n", rw->node.nam);
2922/// return;
2923/// }
2924/// /* default is name only */
2925/// printf("%s\n", rw->node.nam);
2926/// ```
2927pub fn printreswdnode(hn: &reswd, printflags: i32) {
2928 // c:1149 — `Reswd rw = (Reswd) hn;` — Rust types already give us reswd.
2929 // c:1151-1154 — PRINT_WHENCE_WORD branch.
2930 if (printflags & PRINT_WHENCE_WORD) != 0 {
2931 println!("{}: reserved", hn.node.nam); // c:1152
2932 return; // c:1153
2933 }
2934 // c:1156-1159 — PRINT_WHENCE_CSH branch.
2935 if (printflags & PRINT_WHENCE_CSH) != 0 {
2936 println!("{}: shell reserved word", hn.node.nam); // c:1157
2937 return; // c:1158
2938 }
2939 // c:1161-1164 — PRINT_WHENCE_VERBOSE branch.
2940 if (printflags & PRINT_WHENCE_VERBOSE) != 0 {
2941 println!("{} is a reserved word", hn.node.nam); // c:1162
2942 return; // c:1163
2943 }
2944 // c:1166-1167 — default: name only.
2945 println!("{}", hn.node.nam); // c:1167
2946}
2947
2948/// Port of `void createaliastable(HashTable ht)` from `Src/hashtable.c:1186`.
2949/// ```c
2950/// void
2951/// createaliastable(HashTable ht)
2952/// {
2953/// ht->hash = hasher;
2954/// ht->emptytable = NULL;
2955/// ht->filltable = NULL;
2956/// ht->cmpnodes = strcmp;
2957/// ht->addnode = addhashnode;
2958/// ht->getnode = gethashnode;
2959/// ht->getnode2 = gethashnode2;
2960/// ht->removenode = removehashnode;
2961/// ht->disablenode = disablehashnode;
2962/// ht->enablenode = enablehashnode;
2963/// ht->freenode = freealiasnode;
2964/// ht->printnode = printaliasnode;
2965/// }
2966/// ```
2967/// The Rust `hashtable.addnode/.getnode/.removenode/.disablenode/.enablenode/
2968/// .freenode/.printnode` function-pointer types take untyped HashNode
2969/// arguments. The generic Rust helpers (`addhashnode<T>`/`gethashnode<T>`/
2970/// etc.) take typed `&mut HashMap<String, T>` so they can't directly
2971/// satisfy the untyped slot signature; downstream consumers of `aliastab`
2972/// dispatch through `aliastab_lock()` (the typed wrapper) instead of
2973/// the C-style slot. Mirror the C structure verbatim: assign every slot
2974/// either to the matching adapter or `None`, with each line citing the
2975/// matching c:NNN.
2976pub fn createaliastable(ht: &mut hashtable) {
2977 // c:1188
2978 fn cmpnodes_strcmp(a: &str, b: &str) -> i32 {
2979 // c:1193 strcmp
2980 a.cmp(b) as i32
2981 }
2982 ht.hash = Some(hasher); // c:1190
2983 ht.emptytable = None; // c:1191
2984 ht.filltable = None; // c:1192
2985 ht.cmpnodes = Some(cmpnodes_strcmp); // c:1193
2986 // c:1194-1201 — addnode/getnode/getnode2/removenode/disablenode/
2987 // enablenode/freenode/printnode: their C signatures are `void(*)(
2988 // HashTable, char *, void *)` / `HashNode(*)(HashTable, char *)` /
2989 // ... — they take untyped `void *`. The typed Rust helpers
2990 // (`addhashnode<T>(ht: &mut HashMap<String, T>, ...)`) can't be
2991 // coerced through the `fn(&mut hashtable, String, usize)` slot
2992 // shape without per-value-type trampoline closures. Leave the
2993 // slots `None`; the typed dispatch through `aliastab_lock` is the
2994 // canonical Rust path for this table.
2995 ht.addnode = None; // c:1194 addhashnode
2996 ht.getnode = None; // c:1195 gethashnode
2997 ht.getnode2 = None; // c:1196 gethashnode2
2998 ht.removenode = None; // c:1197 removehashnode
2999 ht.disablenode = None; // c:1198 disablehashnode
3000 ht.enablenode = None; // c:1199 enablehashnode
3001 ht.freenode = None; // c:1200 freealiasnode
3002 ht.printnode = None; // c:1201 printaliasnode
3003}
3004
3005/// Trait exposing the DISABLED flag on a hash-node value.
3006///
3007/// Implemented for the per-table value types so the generic ops
3008/// (`gethashnode`/`disablehashnode`/etc.) can filter / mutate
3009/// without per-table dispatch. Mirrors C's `HashNode->flags`
3010/// field which every node struct embeds via the `struct hashnode`
3011/// header.
3012pub trait HashNodeFlags {
3013 fn flags(&self) -> u32;
3014 fn set_disabled(&mut self, disabled: bool);
3015 fn is_disabled(&self) -> bool {
3016 self.flags() & (DISABLED as u32) != 0
3017 }
3018}
3019
3020impl HashNodeFlags for alias {
3021 fn flags(&self) -> u32 {
3022 self.node.flags as u32
3023 }
3024 fn set_disabled(&mut self, disabled: bool) {
3025 if disabled {
3026 self.node.flags |= DISABLED as i32;
3027 } else {
3028 self.node.flags &= !(DISABLED as i32);
3029 }
3030 }
3031}
3032
3033impl HashNodeFlags for shfunc {
3034 fn flags(&self) -> u32 {
3035 self.node.flags as u32
3036 }
3037 fn set_disabled(&mut self, disabled: bool) {
3038 if disabled {
3039 self.node.flags |= DISABLED as i32;
3040 } else {
3041 self.node.flags &= !(DISABLED as i32);
3042 }
3043 }
3044}
3045
3046impl HashNodeFlags for cmdnam {
3047 fn flags(&self) -> u32 {
3048 self.node.flags as u32
3049 }
3050 fn set_disabled(&mut self, disabled: bool) {
3051 if disabled {
3052 self.node.flags |= DISABLED as i32;
3053 } else {
3054 self.node.flags &= !(DISABLED as i32);
3055 }
3056 }
3057}
3058
3059impl HashNodeFlags for reswd {
3060 fn flags(&self) -> u32 {
3061 self.node.flags as u32
3062 }
3063 fn set_disabled(&mut self, disabled: bool) {
3064 if disabled {
3065 self.node.flags |= DISABLED as i32;
3066 } else {
3067 self.node.flags &= !(DISABLED as i32);
3068 }
3069 }
3070}
3071
3072/// Port of `createaliastables()` from `Src/hashtable.c:1206`.
3073///
3074/// C body (lines 1206-1224):
3075/// ```c
3076/// aliastab = newhashtable(23, "aliastab", NULL);
3077/// createaliastable(aliastab);
3078/// aliastab->addnode(aliastab, ztrdup("run-help"), createaliasnode(ztrdup("man"), 0));
3079/// aliastab->addnode(aliastab, ztrdup("which-command"), createaliasnode(ztrdup("whence"), 0));
3080/// sufaliastab = newhashtable(11, "sufaliastab", NULL);
3081/// createaliastable(sufaliastab);
3082/// ```
3083///
3084/// The OnceLock-backed `aliastab_lock()` / `sufaliastab_lock()`
3085/// stand in for `newhashtable(...)` + `createaliastable(...)` — they
3086/// lazy-init the underlying maps on first access.
3087pub fn createaliastables() {
3088 // c:1206 — newhashtable(23, "aliastab", NULL)
3089 // c:1212 — createaliastable(aliastab)
3090 let mut tab = aliastab_lock().write().expect("aliastab poisoned");
3091 // ZSHRS-ONLY. Same gate as `with_defaults`: a drop-in gets ITS
3092 // shell's default aliases, not zsh's two. This function runs during
3093 // init, AFTER the binary has selected the personality, so without the
3094 // gate it re-added `run-help` / `which-command` on top of the set
3095 // installed at table-creation time — `--bash` listed two aliases bash
3096 // does not have, and `--ksh` listed 21 where ksh has 19.
3097 if let Some(defaults) = crate::extensions::emulation_startup::default_aliases() {
3098 tab.clear();
3099 for (name, text) in defaults {
3100 tab.add(createaliasnode(name, text, 0));
3101 }
3102 drop(tab);
3103 let _ = sufaliastab_lock();
3104 return;
3105 }
3106 // c:1215 — `aliastab->addnode(aliastab, ztrdup("run-help"),
3107 // createaliasnode(ztrdup("man"), 0));`
3108 tab.add(createaliasnode("run-help", "man", 0)); // c:1215
3109 // c:1216 — `aliastab->addnode(aliastab, ztrdup("which-command"),
3110 // createaliasnode(ztrdup("whence"), 0));`
3111 tab.add(createaliasnode("which-command", "whence", 0)); // c:1216
3112 drop(tab);
3113 // c:1221 — newhashtable(11, "sufaliastab", NULL)
3114 // c:1223 — createaliastable(sufaliastab)
3115 let _ = sufaliastab_lock();
3116}
3117// c:1253
3118
3119/// Build an alias node with the canonical `alias` shape.
3120/// Mirrors C `addaliasnode(aliastab, name, createaliasnode(text, flags))`
3121/// at hashtable.c:1230 — caller-side bundle for the
3122/// hashnode+text+flags inline-build.
3123pub fn createaliasnode(name: &str, text: &str, flags: u32) -> alias {
3124 // c:1230
3125 alias {
3126 node: hashnode {
3127 next: None,
3128 nam: name.to_string(),
3129 flags: flags as i32,
3130 },
3131 text: text.to_string(),
3132 inuse: std::sync::atomic::AtomicI32::new(0),
3133 }
3134}
3135
3136/// Port of `createaliasnode(char *txt, int flags)` from `Src/hashtable.c:1230`.
3137///
3138/// C body:
3139/// ```c
3140/// al = zshcalloc(sizeof *al);
3141/// al->node.flags = flags;
3142/// al->text = txt;
3143/// al->inuse = 0;
3144/// return al;
3145/// ```
3146// Duplicate `createaliasnode` removed — canonical port is at the
3147// earlier definition (matches C hashtable.c:1230).
3148
3149/// Port of `freealiasnode(HashNode hn)` from `Src/hashtable.c:1243`.
3150///
3151/// C body frees the name + text strings + alias struct. Rust
3152/// port: drop runs the same when the crate::ported::zsh_h::alias is removed from its
3153/// table. This helper triggers the drop.
3154pub fn freealiasnode(hn: &str) {
3155 let mut tab = aliastab_lock().write().expect("aliastab poisoned");
3156 tab.remove(hn);
3157}
3158
3159/// Port of `printaliasnode(HashNode hn, int printflags)` from `Src/hashtable.c:1256`.
3160///
3161/// Emits `whence`-style output for one alias with PRINT_NAMEONLY /
3162/// PRINT_WHENCE_WORD / PRINT_WHENCE_SIMPLE / PRINT_WHENCE_CSH /
3163/// PRINT_WHENCE_VERBOSE / PRINT_LIST flag dispatch. PRINT_LIST falls
3164/// through to the tail `quotedzputs(nam) '=' quotedzputs(text) '\n'`;
3165/// every other branch returns early.
3166pub fn printaliasnode(hn: &alias, printflags: i32) {
3167 // c:1258 — `Alias a = (Alias) hn;` — Rust types already give us alias.
3168
3169 // c:1260-1264 — PRINT_NAMEONLY branch.
3170 if (printflags & PRINT_NAMEONLY) != 0 {
3171 let mut so = io::stdout();
3172 let _ = zputs(&hn.node.nam, &mut so); // c:1261
3173 println!(); // c:1262
3174 return; // c:1263
3175 }
3176
3177 // c:1266-1274 — PRINT_WHENCE_WORD branch.
3178 if (printflags & PRINT_WHENCE_WORD) != 0 {
3179 if (hn.node.flags & ALIAS_SUFFIX as i32) != 0 {
3180 println!("{}: suffix alias", hn.node.nam); // c:1268
3181 } else if (hn.node.flags & ALIAS_GLOBAL as i32) != 0 {
3182 println!("{}: global alias", hn.node.nam); // c:1270
3183 } else {
3184 println!("{}: alias", hn.node.nam); // c:1272
3185 }
3186 return; // c:1273
3187 }
3188
3189 // c:1276-1280 — PRINT_WHENCE_SIMPLE branch.
3190 if (printflags & PRINT_WHENCE_SIMPLE) != 0 {
3191 let mut so = io::stdout();
3192 let _ = zputs(&hn.text, &mut so); // c:1277
3193 println!(); // c:1278
3194 return; // c:1279
3195 }
3196
3197 // c:1282-1293 — PRINT_WHENCE_CSH branch.
3198 if (printflags & PRINT_WHENCE_CSH) != 0 {
3199 let mut so = io::stdout();
3200 let _ = nicezputs(&hn.node.nam, &mut so); // c:1283
3201 print!(": "); // c:1284
3202 if (hn.node.flags & ALIAS_SUFFIX as i32) != 0 {
3203 print!("suffix "); // c:1286
3204 } else if (hn.node.flags & ALIAS_GLOBAL as i32) != 0 {
3205 print!("globally "); // c:1288
3206 }
3207 print!("aliased to "); // c:1289
3208 let _ = nicezputs(&hn.text, &mut so); // c:1290
3209 println!(); // c:1291
3210 return; // c:1292
3211 }
3212
3213 // c:1295-1308 — PRINT_WHENCE_VERBOSE branch.
3214 if (printflags & PRINT_WHENCE_VERBOSE) != 0 {
3215 let mut so = io::stdout();
3216 let _ = nicezputs(&hn.node.nam, &mut so); // c:1296
3217 if crate::dash_mode::bash_mode() {
3218 // !!! BASH-MODE !!! bash(1) `type`: "NAME is aliased to `TEXT'".
3219 print!(" is aliased to `");
3220 let _ = nicezputs(&hn.text, &mut so);
3221 println!("'");
3222 return;
3223 }
3224 print!(" is a"); // c:1297
3225 if (hn.node.flags & ALIAS_SUFFIX as i32) != 0 {
3226 print!(" suffix"); // c:1299
3227 } else if (hn.node.flags & ALIAS_GLOBAL as i32) != 0 {
3228 print!(" global"); // c:1301
3229 } else {
3230 print!("n"); // c:1303
3231 }
3232 print!(" alias for "); // c:1304
3233 if crate::dash_mode::korn_mode() {
3234 // !!! KORN-MODE (no C counterpart) !!! ksh93 and mksh word the text the
3235 // way their `alias` listing does: `a is an alias for 'ls -l'`.
3236 let quoted = if crate::dash_mode::ksh93_mode() {
3237 crate::dash_mode::ksh93_printf_q(&hn.text)
3238 } else {
3239 quotedzputs(&hn.text)
3240 };
3241 let _ = io::Write::write_all(&mut so, &unmetafy_str("ed));
3242 } else {
3243 let _ = nicezputs(&hn.text, &mut so); // c:1305
3244 }
3245 println!(); // c:1306
3246 return; // c:1307
3247 }
3248
3249 // ZSHRS-ONLY, no C counterpart. bash's `alias` listing always emits
3250 // the reusable `alias NAME='value'` form with the value single-quoted
3251 // — `alias a=1` lists as `alias a='1'` — where zsh's plain listing is
3252 // bare `a=1` and only `alias -L` adds the prefix. So a `--bash` shell
3253 // printed `ll='ls -alF'` where bash prints `alias ll='ls -alF'`.
3254 // Applies to the plain listing only; the whence/nameonly forms above
3255 // have already returned.
3256 if crate::extensions::dash_mode::bash_mode() {
3257 println!(
3258 "alias {}={}",
3259 hn.node.nam,
3260 crate::extensions::emulation_startup::single_quote(&hn.text)
3261 );
3262 return;
3263 }
3264 // dash single-quotes the value ALWAYS (`alias yy=1` lists as
3265 // `yy='1'`), where ksh, mksh and zsh quote only when the value needs
3266 // it. No `alias ` prefix, unlike bash.
3267 if crate::extensions::emulation_output::alias_always_quotes() {
3268 println!(
3269 "{}={}",
3270 hn.node.nam,
3271 crate::extensions::emulation_startup::single_quote(&hn.text)
3272 );
3273 return;
3274 }
3275
3276 // c:1310-1330 — PRINT_LIST prefix block (falls through to the
3277 // tail quotedzputs body below; default-no-flags also reaches the
3278 // tail by skipping this block).
3279 if (printflags & PRINT_LIST) != 0 {
3280 // c:1312-1316 — Fast fail on `=` in name (unrepresentable
3281 // `alias name=...` round-trip).
3282 if hn.node.nam.contains('=') {
3283 // c:1313
3284 zwarn(&format!(
3285 "invalid alias '{}' encountered while printing aliases",
3286 hn.node.nam
3287 ));
3288 return; // c:1316
3289 }
3290 print!("alias "); // c:1320
3291 if (hn.node.flags & ALIAS_SUFFIX as i32) != 0 {
3292 // c:1321
3293 print!("-s "); // c:1322
3294 } else if (hn.node.flags & ALIAS_GLOBAL as i32) != 0 {
3295 // c:1323
3296 print!("-g "); // c:1324
3297 }
3298 // c:1326-1329 — `-- ` so a name starting with `-`/`+` isn't
3299 // interpreted as an option when the listing is re-executed.
3300 if hn.node.nam.starts_with('-') || hn.node.nam.starts_with('+') {
3301 // c:1328
3302 print!("-- "); // c:1329
3303 }
3304 }
3305
3306 // c:1332-1336 — common tail: quotedzputs(nam) '=' quotedzputs(text) '\n'.
3307 let _ = io::Write::write_all(&mut io::stdout(), &unmetafy_str("edzputs(&hn.node.nam))); // c:1332
3308 print!("="); // c:1333
3309 // ksh93 words the value the way its `printf %q` does (`$'it\'s'`).
3310 let value = if crate::dash_mode::ksh93_mode() {
3311 crate::dash_mode::ksh93_printf_q(&hn.text)
3312 } else {
3313 quotedzputs(&hn.text)
3314 };
3315 let _ = io::Write::write_all(&mut io::stdout(), &unmetafy_str(&value)); // c:1334
3316 println!(); // c:1336
3317}
3318
3319/// Port of `createhisttable()` from `Src/hashtable.c:1345`.
3320///
3321/// C body wires up the histtab GSU vtable with `histhasher` /
3322/// `histstrcmp` / `addhistnode` etc. Rust port: touches the
3323/// singleton to initialise. The HashMap-keyed-by-string model
3324/// is much simpler than C's per-bucket chain; the entries hold
3325/// (history event-id) values keyed by command-text.
3326pub fn createhisttable() {
3327 let _ = histtab_lock();
3328}
3329
3330/// History-specific hash function (normalizes whitespace).
3331/// Port of `histhasher(const char *str)` from `Src/hashtable.c:1365`.
3332///
3333/// C body uses `inblank(*str)` (canonical typtab predicate at
3334/// `Src/ztype.h:50` — NARROW blank: space/tab ONLY, not newline,
3335/// definitely NOT broad Unicode whitespace). The Rust port previously
3336/// used `c.is_whitespace()` which is the Unicode-broad set including
3337/// CR/FF/VT/NBSP — every line of zsh history containing one of those
3338/// bytes hashed to a different bucket than C would have.
3339///
3340/// Faithful: matches `inblank` exactly (`c:50` — `space + tab`).
3341pub fn histhasher(s: &str) -> u32 {
3342 // c:1365
3343 // c:50 — `inblank(c)` = `c == ' ' || c == '\t'`. NOT `\n`, NOT broad.
3344 #[inline]
3345 fn is_inblank_narrow(c: char) -> bool {
3346 c == ' ' || c == '\t'
3347 }
3348
3349 let mut hashval: u32 = 0;
3350 let mut chars = s.chars().peekable();
3351
3352 // c:1369 — `while (inblank(*str)) str++;` skip leading blanks.
3353 while let Some(&c) = chars.peek() {
3354 if is_inblank_narrow(c) {
3355 chars.next();
3356 } else {
3357 break;
3358 }
3359 }
3360
3361 // c:1371 — main mix loop.
3362 while let Some(c) = chars.next() {
3363 if is_inblank_narrow(c) {
3364 // c:1373 — `do str++; while (inblank(*str));` collapse runs.
3365 while let Some(&next) = chars.peek() {
3366 if is_inblank_narrow(next) {
3367 chars.next();
3368 } else {
3369 break;
3370 }
3371 }
3372 // c:1374-1375 — `if (*str) hashval += (hashval << 5) + ' ';`
3373 if chars.peek().is_some() {
3374 hashval = hashval.wrapping_add(hashval.wrapping_shl(5).wrapping_add(' ' as u32));
3375 }
3376 } else {
3377 // c:1377 — `hashval += (hashval << 5) + *(unsigned char *)str++;`
3378 hashval = hashval.wrapping_add(hashval.wrapping_shl(5).wrapping_add(c as u32));
3379 }
3380 }
3381 hashval
3382}
3383
3384/// Port of `emptyhisttable(HashTable ht)` from `Src/hashtable.c:1385`.
3385///
3386/// C body:
3387/// ```c
3388/// emptyhashtable(ht);
3389/// if (hist_ring) histremovedups();
3390/// ```
3391/// WARNING: param names don't match C — Rust=() vs C=(ht)
3392pub fn emptyhisttable() {
3393 // c:1385 — `emptyhashtable(ht)` — clear the lookup table.
3394 histtab_lock().write().expect("histtab poisoned").clear();
3395 // c:1386 — `if (hist_ring) histremovedups();` — prune dup-flagged
3396 // entries from the history ring.
3397 let has_ring = !hist_ring.lock().unwrap().is_empty();
3398 if has_ring {
3399 histremovedups(); // c:1386
3400 }
3401}
3402
3403/// Compare strings with normalized whitespace (for history).
3404/// Port of `histstrcmp(const char *str1, const char *str2)` from
3405/// `Src/hashtable.c:1396`.
3406///
3407/// C body uses `inblank(*str)` everywhere (`Src/ztype.h:50` — NARROW
3408/// space/tab only). The previous Rust port used `c.is_whitespace()`
3409/// (broad Unicode set including CR/FF/VT/NBSP), which would silently
3410/// fold history lines that C considers distinct (e.g. lines that
3411/// contain NBSP would dedupe against lines with no NBSP).
3412///
3413/// C signature is 2-arg: it reads `isset(HISTREDUCEBLANKS)` directly.
3414/// Rust port passes `reduce_blanks` as an explicit 3rd arg to keep
3415/// the option read out of this leaf fn (call sites at hist.c thread
3416/// the option from the parent scope).
3417pub fn histstrcmp(s1: &str, s2: &str, reduce_blanks: bool) -> std::cmp::Ordering {
3418 // c:1396
3419 // c:50 — `inblank(c)` = `c == ' ' || c == '\t'`. NOT newline, NOT broad.
3420 #[inline]
3421 fn is_inblank_narrow(c: char) -> bool {
3422 c == ' ' || c == '\t'
3423 }
3424
3425 // c:1398-1399 — skip leading inblank in both strings.
3426 let s1 = s1.trim_start_matches(is_inblank_narrow);
3427 let s2 = s2.trim_start_matches(is_inblank_narrow);
3428
3429 // c:1405 — HISTREDUCEBLANKS short-circuit to raw strcmp.
3430 if reduce_blanks {
3431 return s1.cmp(s2);
3432 }
3433
3434 let mut c1 = s1.chars().peekable();
3435 let mut c2 = s2.chars().peekable();
3436
3437 // c:1408 — `while (*str1 && *str2) { ... }` then `return *str1 - *str2;`.
3438 loop {
3439 let ch1 = c1.peek().copied();
3440 let ch2 = c2.peek().copied();
3441
3442 match (ch1, ch2) {
3443 (None, None) => return std::cmp::Ordering::Equal, // c:1421 — both NUL
3444 (None, Some(c)) => {
3445 // c:1421 — *str1=0 - *str2; left shorter (Less) unless str2
3446 // is all-inblank residue.
3447 if is_inblank_narrow(c) {
3448 while c2.peek().copied().map(is_inblank_narrow).unwrap_or(false) {
3449 c2.next();
3450 }
3451 if c2.peek().is_none() {
3452 return std::cmp::Ordering::Equal;
3453 }
3454 }
3455 return std::cmp::Ordering::Less;
3456 }
3457 (Some(c), None) => {
3458 if is_inblank_narrow(c) {
3459 while c1.peek().copied().map(is_inblank_narrow).unwrap_or(false) {
3460 c1.next();
3461 }
3462 if c1.peek().is_none() {
3463 return std::cmp::Ordering::Equal;
3464 }
3465 }
3466 return std::cmp::Ordering::Greater;
3467 }
3468 (Some(ch1), Some(ch2)) => {
3469 let ws1 = is_inblank_narrow(ch1);
3470 let ws2 = is_inblank_narrow(ch2);
3471
3472 if ws1 && ws2 {
3473 // c:1411-1413 — collapse both runs.
3474 while c1.peek().copied().map(is_inblank_narrow).unwrap_or(false) {
3475 c1.next();
3476 }
3477 while c2.peek().copied().map(is_inblank_narrow).unwrap_or(false) {
3478 c2.next();
3479 }
3480 } else if ws1 {
3481 // c:1410 — `if (!inblank(*str2)) break;` → mismatch.
3482 while c1.peek().copied().map(is_inblank_narrow).unwrap_or(false) {
3483 c1.next();
3484 }
3485 if c1.peek().is_none() {
3486 return std::cmp::Ordering::Less;
3487 }
3488 return std::cmp::Ordering::Less;
3489 } else if ws2 {
3490 while c2.peek().copied().map(is_inblank_narrow).unwrap_or(false) {
3491 c2.next();
3492 }
3493 if c2.peek().is_none() {
3494 return std::cmp::Ordering::Greater;
3495 }
3496 return std::cmp::Ordering::Greater;
3497 } else if ch1 != ch2 {
3498 return ch1.cmp(&ch2); // c:1417 — *str1 - *str2
3499 } else {
3500 c1.next();
3501 c2.next();
3502 }
3503 }
3504 }
3505 }
3506}
3507
3508/// Port of `addhistnode(HashTable ht, char *nam, void *nodeptr)` from `Src/hashtable.c:1427`.
3509///
3510/// C body:
3511/// ```c
3512/// HashNode oldnode = addhashnode2(ht, nam, nodeptr);
3513/// Histent he = (Histent)nodeptr;
3514/// if (oldnode && oldnode != (HashNode)nodeptr) {
3515/// if (he->node.flags & HIST_MAKEUNIQUE
3516/// || (he->node.flags & HIST_FOREIGN && (Histent)oldnode == he->up)) {
3517/// (void) addhashnode2(ht, oldnode->nam, oldnode); /* restore hash */
3518/// he->node.flags |= HIST_DUP;
3519/// he->node.flags &= ~HIST_MAKEUNIQUE;
3520/// } else {
3521/// oldnode->flags |= HIST_DUP;
3522/// if (hist_ignore_all_dups)
3523/// freehistnode(oldnode); /* Remove the old dup */
3524/// }
3525/// } else
3526/// he->node.flags &= ~HIST_MAKEUNIQUE;
3527/// ```
3528///
3529/// The Rust `histtab` is keyed by command text → event id, so
3530/// `addhashnode2` maps to `HashMap::insert` (returns the displaced
3531/// event). The new node `he` and the displaced `oldnode` are located
3532/// in `hist_ring` by their `histnum`; their `node.flags` are the same
3533/// `HIST_*` fields the C node carries.
3534///
3535/// NOTE: the caller must NOT hold the `hist_ring` lock across this
3536/// call — `addhistnode` re-locks the ring to read/mutate node flags.
3537/// WARNING: param names don't match C — Rust=(nam, event_id) vs C=(ht, nam, nodeptr)
3538pub fn addhistnode(nam: &str, event_id: i32) -> Option<i32> {
3539 // c:1429 — `HashNode oldnode = addhashnode2(ht, nam, nodeptr);`
3540 let oldnode = histtab_lock()
3541 .write()
3542 .expect("histtab poisoned")
3543 .insert(nam.to_string(), event_id);
3544
3545 // c:1431 — `if (oldnode && oldnode != (HashNode)nodeptr)`
3546 if let Some(old_event) = oldnode {
3547 if old_event != event_id {
3548 // `he->node.flags` — flags of the newly inserted node. C reads
3549 // `he->node.flags` directly off the pointer; the Rust ring is a
3550 // `Vec` keyed by `histnum`, so locate the entry by event id.
3551 let he_flags = hist_ring
3552 .lock()
3553 .unwrap()
3554 .iter()
3555 .find(|h| h.histnum == event_id as i64)
3556 .map(|h| h.node.flags)
3557 .unwrap_or(0);
3558 // c:1433 — `(Histent)oldnode == he->up` (the entry directly
3559 // above `he` in the ring is the one being displaced).
3560 let up_is_old = up_histent(event_id as i64) == Some(old_event as i64);
3561 if (he_flags & HIST_MAKEUNIQUE as i32) != 0
3562 || ((he_flags & HIST_FOREIGN as i32) != 0 && up_is_old)
3563 {
3564 // c:1434 — `addhashnode2(ht, oldnode->nam, oldnode);`
3565 // Restore the hash so `nam` maps back to the old event
3566 // (same command text, so the key is unchanged).
3567 histtab_lock()
3568 .write()
3569 .expect("histtab poisoned")
3570 .insert(nam.to_string(), old_event);
3571 // c:1435-1436 — mark `he` a dup, clear make-unique.
3572 if let Some(h) = hist_ring
3573 .lock()
3574 .unwrap()
3575 .iter_mut()
3576 .find(|h| h.histnum == event_id as i64)
3577 {
3578 h.node.flags = (h.node.flags | HIST_DUP as i32) & !(HIST_MAKEUNIQUE as i32);
3579 }
3580 } else {
3581 // c:1439 — `oldnode->flags |= HIST_DUP;`
3582 if let Some(h) = hist_ring
3583 .lock()
3584 .unwrap()
3585 .iter_mut()
3586 .find(|h| h.histnum == old_event as i64)
3587 {
3588 h.node.flags |= HIST_DUP as i32;
3589 }
3590 // c:1440-1441 — `if (hist_ignore_all_dups) freehistnode(oldnode);`
3591 // C's `freehistnode` == `freehistdata(oldnode, 1); zfree(oldnode)`;
3592 // the ported `freehistdata(idx, 1)` unlinks the old node from
3593 // the ring (the Rust equivalent of `zfree`) and — because the
3594 // node is now HIST_DUP-flagged — skips removing the hash entry
3595 // that already points at the new node (c:1466 guard).
3596 if hist_ignore_all_dups.load(Ordering::SeqCst) != 0 {
3597 let idx = hist_ring
3598 .lock()
3599 .unwrap()
3600 .iter()
3601 .position(|h| h.histnum == old_event as i64);
3602 if let Some(idx) = idx {
3603 freehistdata(idx, 1);
3604 }
3605 }
3606 }
3607 return oldnode;
3608 }
3609 }
3610 // c:1445 — `he->node.flags &= ~HIST_MAKEUNIQUE;`
3611 if let Some(h) = hist_ring
3612 .lock()
3613 .unwrap()
3614 .iter_mut()
3615 .find(|h| h.histnum == event_id as i64)
3616 {
3617 h.node.flags &= !(HIST_MAKEUNIQUE as i32);
3618 }
3619 oldnode
3620}
3621
3622/// Port of `freehistnode(HashNode nodeptr)` from `Src/hashtable.c:1450`.
3623///
3624/// C body: `freehistdata((Histent)nodeptr, 1); zfree(nodeptr, ...);`
3625/// Rust port: removes from the lookup table — drop runs the
3626/// equivalent of zfree.
3627pub fn freehistnode(nodeptr: &str) {
3628 histtab_lock()
3629 .write()
3630 .expect("histtab poisoned")
3631 .remove(nodeptr);
3632}
3633
3634/// Port of `freehistdata(Histent he, int unlink)` from `Src/hashtable.c:1458`.
3635///
3636/// C body: removes the named entry from `histtab` (unless flagged
3637/// HIST_DUP/HIST_TMPSTORE), frees the command + word-array fields,
3638/// and if `unlink` re-links the ring around `he` and decrements
3639/// `histlinect`. Rust port indexes into `hist_ring` (Vec replaces C's
3640/// doubly-linked list); the up/down relink collapses to `Vec::remove`.
3641/// WARNING: param names don't match C — Rust=(idx, unlink) vs C=(he, unlink)
3642pub fn freehistdata(idx: usize, unlink: i32) {
3643 // c:1458
3644 let mut ring = hist_ring.lock().unwrap();
3645 let he = match ring.get(idx) {
3646 Some(h) => h,
3647 None => return,
3648 }; // c:1461 if (!he) return
3649 let nam = he.node.nam.clone();
3650 let flags = he.node.flags as u32;
3651 let histnum = he.histnum as i32;
3652 if (flags & (HIST_DUP | HIST_TMPSTORE)) == 0 {
3653 // c:1467
3654 let mut tab = histtab_lock().write().expect("histtab poisoned"); // c:1468 removehashnode(histtab, ...)
3655 // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
3656 // C removes by name: every non-DUP entry is the hashed node for its
3657 // text. readhistfile here does not hash file entries, so an evicted
3658 // file entry can share its text with a newer interactive entry that
3659 // owns the key. Remove the key only when it points at `he`.
3660 if tab.get(&nam) == Some(&histnum) {
3661 tab.remove(&nam);
3662 }
3663 }
3664 // c:1471-1473 — `zsfree(name); if (nwords) zfree(words, ...)`. Rust
3665 // String/Vec drop handles both; only the unlink step needs explicit
3666 // ring mutation.
3667 if unlink != 0 {
3668 // c:1475
3669 ring.remove(idx); // c:1477-1483 unlink up/down
3670 let new_ct = ring.len() as i64;
3671 drop(ring);
3672 histlinect.store(new_ct, Ordering::SeqCst);
3673 // c:1477 --histlinect
3674 }
3675}
3676
3677/// Port of `dircache_set(char **name, char *value)` from `Src/hashtable.c:1537`.
3678///
3679/// C body manages a refcounted directory-name cache:
3680/// - `value == NULL` → decrement refs on `*name`, free if zero,
3681/// set `*name = NULL`.
3682/// - `value != NULL` → search for an existing entry, bump refs,
3683/// else allocate a new slot.
3684///
3685/// Rust port: routes through dircache_lock() with refcount-by-
3686/// HashMap-value (i32). Add/remove via the (name, value) pair.
3687pub fn dircache_set(name: &mut Option<String>, value: Option<&str>) {
3688 // c:1537
3689 let mut cache = dircache_lock().lock().expect("dircache poisoned");
3690
3691 if value.is_none() {
3692 // c:1541
3693 // c:1542-1543 — `if (!*name) return;`
3694 let key = match name.as_deref() {
3695 None => return, // c:1543
3696 Some(s) => s.to_string(),
3697 };
3698 // c:1544-1548 — `if (!dircache_size) { zsfree(*name); *name = NULL; return; }`
3699 if cache.is_empty() {
3700 // c:1544
3701 *name = None; // c:1546
3702 return; // c:1547
3703 }
3704 // c:1550-1582 — scan cache, decrement matching entry's refs;
3705 // on refs==0, drop the entry. Rust keys by string equality
3706 // since we don't share the C pointer-identity used at c:1553.
3707 if let Some(idx) = cache.iter().position(|e| e.name == key) {
3708 // c:1550
3709 cache[idx].refs -= 1; // c:1555
3710 if cache[idx].refs == 0 {
3711 // c:1556
3712 cache.remove(idx); // c:1558-1577 collapsed
3713 DIRCACHE_LASTENTRY.store(usize::MAX, Ordering::SeqCst); // c:1564/1577
3714 }
3715 *name = None; // c:1579
3716 return; // c:1580
3717 }
3718 // c:1583-1584 — `zsfree(*name); *name = NULL;`
3719 *name = None; // c:1584
3720 } else {
3721 // c:1585
3722 let mut v = value.unwrap().to_string();
3723 // c:1590-1594 — absolute-path normalization for relative input.
3724 if !v.starts_with('/') {
3725 // c:1590
3726 let cwd = zgetcwd(); // c:1591 zgetcwd
3727 v = format!("{}/{}", cwd, v); // c:1591 zhtricat
3728 if let Some(resolved) = xsymlink(&v) {
3729 // c:1593 xsymlink(..., 1)
3730 v = resolved; // c:1593
3731 } // c:1593
3732 }
3733 // c:1602-1606 — `dircache_lastentry` fast-path: same path as last.
3734 let last_idx = DIRCACHE_LASTENTRY.load(Ordering::SeqCst);
3735 if last_idx != usize::MAX && last_idx < cache.len() && cache[last_idx].name == v {
3736 *name = Some(cache[last_idx].name.clone()); // c:1604
3737 cache[last_idx].refs += 1; // c:1605
3738 return; // c:1606
3739 }
3740 // c:1607-1610 — empty-cache: allocate first entry.
3741 if cache.is_empty() {
3742 // c:1607
3743 cache.push(dircache_entry {
3744 name: v.clone(),
3745 refs: 1,
3746 }); // c:1609-1610
3747 DIRCACHE_LASTENTRY.store(0usize, Ordering::SeqCst);
3748 *name = Some(v);
3749 return;
3750 }
3751 // c:1611-1619 — scan for existing entry, bump refs.
3752 if let Some(idx) = cache.iter().position(|e| e.name == v) {
3753 // c:1612-1614
3754 *name = Some(cache[idx].name.clone()); // c:1615
3755 cache[idx].refs += 1; // c:1616
3756 DIRCACHE_LASTENTRY.store(idx, Ordering::SeqCst);
3757 return;
3758 }
3759 // c:1620+ — push new entry.
3760 cache.push(dircache_entry {
3761 name: v.clone(),
3762 refs: 1,
3763 });
3764 let new_idx = cache.len() - 1;
3765 DIRCACHE_LASTENTRY.store(new_idx, Ordering::SeqCst);
3766 *name = Some(v);
3767 }
3768}
3769
3770// `DIRCACHE_LASTENTRY` already declared below at hashtable.rs:1849
3771// as `AtomicUsize` (`usize::MAX` sentinel). Reuse that — the new
3772// body above adapts via i32 cast.
3773
3774// `SuffixAliasTable` type alias deleted — Rust-only convenience.
3775// C has no `SuffixAliasTable`; the same generic `HashTable` powers
3776// both `aliastab` and `sufaliastab` (declared identically at
3777// hashtable.c:1177-1182). Callers can use `alias_table` directly
3778// for both. (When the canonical HashTable substrate is wired,
3779// both will share the same generic type.)
3780
3781/// Port of `struct dircache_entry` from `Src/hashtable.c:1503-1509`.
3782///
3783/// C body:
3784/// ```c
3785/// struct dircache_entry {
3786/// char *name; /* Name of directory in cache */
3787/// int refs; /* Number of references to it */
3788/// };
3789/// ```
3790#[allow(non_camel_case_types)]
3791#[derive(Debug, Clone)]
3792pub struct dircache_entry {
3793 // c:1503
3794 pub name: String, // c:1506
3795 pub refs: i32, // c:1508
3796}
3797
3798/// Command name hash table
3799// hash table containing external commands // c:587
3800#[derive(Debug, Clone)]
3801/// `$cmdtab` table of cached executable lookups.
3802/// Port of `cmdnamtab` from Src/hashtable.c — `createcmdnamtable()`
3803/// (line 601), `emptycmdnamtable()` (line 623), and `hashdir()`
3804/// (line 634) drive populate/clear/fill cycles.
3805/// **NOT C-FAITHFUL — Rust-only typed wrapper around HashMap.**
3806/// C uses the generic `HashTable` struct (zsh.h:1530 / zsh_h.rs:535)
3807/// with per-table GSU callback fn pointers (`hash`/`addnode`/
3808/// `getnode`/`removenode`/`freenode`/`printnode`/`scantab`). Each
3809/// per-table accessor (`cmdnamtab_lock`, `shfunctab_lock`, etc.)
3810/// returns a `Mutex<HashTable>` instance with the appropriate
3811/// callbacks wired. When the generic-HashTable substrate lands,
3812/// cmdnam_table/shfunc_table/reswd_table/alias_table get deleted
3813/// in favor of typed views over the shared `HashTable` storage.
3814pub struct cmdnam_table {
3815 /// `table` field — C's `cmdnamtab` bucket array, `hsize = 201`
3816 /// (`Src/hashtable.c:603`). Was a `std::collections::HashMap`,
3817 /// whose per-process-seeded order made `${(k)commands)` /
3818 /// `compadd -k commands` differ on every run.
3819 ///
3820 /// COPY-ON-WRITE, like `alias_table::table`: `$( … )` snapshots it on
3821 /// every substitution (see `snapshot`), and a filled table holds an
3822 /// entry per executable on `$PATH` — thousands.
3823 table: crate::cow_map::CowArc<hashtable_nodes<cmdnam>>,
3824 /// `path_checked_index` field.
3825 path_checked_index: usize,
3826 /// `path` field.
3827 path: Vec<String>,
3828 /// `hash_executables_only` field.
3829 hash_executables_only: bool,
3830}
3831
3832// `impl shfunc` deleted — methods replaced with inline flag checks
3833// (`(shf.node.flags & FLAG as i32) != 0`) at callers, mirroring
3834// C's idiom. Constructors `shfunc_with_body` / `shfunc_autoload`
3835// above replace `shfunc::with_body` / `::autoload` / `::new`.
3836
3837/// Shell function hash table
3838// hash table containing the shell functions // c:805
3839#[derive(Debug, Clone)]
3840/// `$shfunctab` shell function table.
3841/// Port of the `shfunctab` HashTable Src/hashtable.c builds —
3842/// `printshfuncnode` / `freeshfuncnode` (Src/builtin.c) hang off
3843/// the same shape.
3844/// Faithful port of C's `HashTable shfunctab` (Src/zsh.h, declared
3845/// `mod_export HashTable shfunctab`). Stores `Box<shfunc>` so that
3846/// raw `*mut shfunc` handed to C-style call sites stays stable
3847/// across map rehashes — mirrors C's `HashNode` semantics where
3848/// the table owns the heap allocation and hands out pointers.
3849/// Owned-value accessors (`add`, `get`, `get_mut`) coexist with
3850/// C-faithful pointer accessors (`addnode`, `getnode`) so both
3851/// the Rust-idiomatic bytecode function-def path
3852/// (`fusevm_bridge.rs:8378`) and the C-style `bin_functions`
3853/// port (`builtin.rs:3689+`) write to the same canonical table.
3854pub struct shfunc_table {
3855 /// `table` field — the `nodes`/`hsize`/`ct` bucket array C's
3856 /// `shfunctab` is, created at `hsize = 7` (`Src/hashtable.c:814`
3857 /// `shfunctab = newhashtable(7, "shfunctab", NULL)`).
3858 ///
3859 /// It used to be a `std::collections::HashMap`, whose iteration
3860 /// order is neither C's bucket walk nor even stable across runs
3861 /// (`RandomState` re-seeds per process). `${(k)functions}` and
3862 /// `compadd -k functions` read this order straight out
3863 /// (`Src/Modules/parameter.c:480-481`), so both were random.
3864 ///
3865 /// COPY-ON-WRITE. The bucket array sits behind an `Arc` so that
3866 /// `snapshot()` — which `$( … )` and `( … )` run on EVERY
3867 /// substitution — is a refcount bump instead of a deep clone of
3868 /// every `Box<shfunc>` in the table. On a shell with ~47k loaded
3869 /// functions the deep clone was ~47% of the cost of entering a
3870 /// command substitution.
3871 ///
3872 /// Every `&mut self` method below goes through `Arc::make_mut`, so
3873 /// the FIRST write after a snapshot pays the clone and the parent's
3874 /// snapshot keeps the pre-write bucket array. That is exactly the
3875 /// old semantics — a subshell that only READS functions (the
3876 /// overwhelmingly common case) now pays nothing.
3877 table: std::sync::Arc<hashtable_nodes<Box<shfunc>>>,
3878}
3879
3880/// Reserved word hash table
3881#[derive(Debug)]
3882/// `$reswdtab` reserved-word table.
3883// hash table containing the reserved words // c:1111
3884/// Port of the `reswdtab` HashTable from Src/hashtable.c — used
3885/// by Src/lex.c to recognize keywords like `if`/`while`/`do`.
3886/// **NOT C-FAITHFUL — Rust-only typed wrapper.** See WARNING on
3887/// `cmdnam_table` for the canonical-port direction.
3888pub struct reswd_table {
3889 /// `table` field — C's `reswdtab` bucket array, `hsize = 23`
3890 /// (`Src/hashtable.c:1124` `reswdtab = newhashtable(23, "reswdtab",
3891 /// NULL)`).
3892 ///
3893 /// Was a `std::collections::HashMap`, whose per-process-seeded
3894 /// order made `$reswords` / `$dis_reswords` random. `getreswords`
3895 /// (`Src/Modules/parameter.c:871-886`) walks the raw bucket array
3896 /// (`for (i = 0; i < reswdtab->hsize; i++) for (hn =
3897 /// reswdtab->nodes[i]; hn; hn = hn->next)`), so those parameters
3898 /// expose C's chain order directly.
3899 table: hashtable_nodes<reswd>,
3900}
3901
3902/// crate::ported::zsh_h::alias hash table
3903#[derive(Debug)]
3904/// `$aliastab` alias hash.
3905/// Port of the `aliastab` HashTable from Src/hashtable.c —
3906// hash table containing the aliases // c:1174
3907/// `bin_alias()` (Src/builtin.c) drives every mutation. Suffix
3908/// aliases live in a separate `sufaliastab` instance.
3909/// **NOT C-FAITHFUL — Rust-only typed wrapper.** See WARNING on
3910/// `cmdnam_table` for the canonical-port direction.
3911pub struct alias_table {
3912 /// `table` field — C's `aliastab` bucket array, `hsize = 23`
3913 /// (`Src/hashtable.c:1210` `aliastab = newhashtable(23, "aliastab",
3914 /// NULL)`); the `sufaliastab` instance is built with `hsize = 11`
3915 /// (`Src/hashtable.c:1221`).
3916 ///
3917 /// Was an `indexmap::IndexMap`, chosen on the theory that
3918 /// insertion order was "closer to zsh's observed behavior". That
3919 /// reasoning was wrong: zsh emits alias keys in the `scanhashtable`
3920 /// bucket walk (`Src/hashtable.c:420-434`) that `scanpmraliases` /
3921 /// `scanpmgaliases` / `scanpmsaliases` drive
3922 /// (`Src/Modules/parameter.c:2005-2047`), i.e. bucket
3923 /// `hasher(nam) % hsize` ascending, each chain walked head→tail
3924 /// with the most recently added key at the head (`c:214-215`).
3925 /// That is neither insertion order nor sorted order, so
3926 /// `${(k)aliases}` diverged from zsh for every alias set.
3927 ///
3928 /// COPY-ON-WRITE, for the same reason as `shfunc_table::table`:
3929 /// `$( … )` snapshots both alias tables on every substitution (see
3930 /// `snapshot`), and a user table runs to thousands of aliases.
3931 table: crate::cow_map::CowArc<hashtable_nodes<alias>>,
3932}
3933
3934// Mirrors C's file-statics at hashtable.c:1517:
3935// `static struct dircache_entry *dircache, *dircache_lastentry;`
3936// `static int dircache_size;`
3937// Rust port keeps the cache as a `Mutex<Vec<dircache_entry>>` plus
3938// a lastentry index. dircache_size is implicit (Vec::len()).
3939static DIRCACHE_INNER: std::sync::OnceLock<std::sync::Mutex<Vec<dircache_entry>>> =
3940 std::sync::OnceLock::new();
3941static DIRCACHE_LASTENTRY: std::sync::atomic::AtomicUsize = // c:1517
3942 std::sync::atomic::AtomicUsize::new(usize::MAX); // sentinel "no last"
3943
3944/// Build a hashed `cmdnam` carrying a resolved path. Mirrors C's
3945/// inline `cn->u.cmd = ztrdup(path); cn->node.flags = HASHED;` at
3946/// hashtable.c:704.
3947pub fn cmdnam_hashed(name: &str, path: &str) -> cmdnam {
3948 // c:704 idiom
3949 cmdnam {
3950 node: hashnode {
3951 next: None,
3952 nam: name.to_string(),
3953 flags: HASHED as i32,
3954 },
3955 name: None,
3956 cmd: Some(path.to_string()),
3957 }
3958}
3959
3960/// Build an unhashed `cmdnam` whose lookup will scan
3961/// `path_segments`. Mirrors C's `cn->u.name = pathchecked;
3962/// cn->node.flags = 0;` at hashtable.c:712.
3963pub fn cmdnam_unhashed(name: &str, path_segments: Vec<String>) -> cmdnam {
3964 // c:712 idiom
3965 cmdnam {
3966 node: hashnode {
3967 next: None,
3968 nam: name.to_string(),
3969 flags: 0,
3970 },
3971 name: Some(path_segments),
3972 cmd: None,
3973 }
3974}
3975
3976/// Build a `shfunc` for the lazy-compile path with body source text.
3977/// Mirrors C's `shfunctab->addnode(shfunctab, ztrdup(name), shf)`
3978/// after callers populate `shf->funcdef = parse_subst_string(body)`.
3979pub fn shfunc_with_body(name: &str, body: &str) -> shfunc {
3980 // c:824 idiom
3981 // c:Src/exec.c:5383 — `ztrdup(scriptfilename)`. zsh tags every
3982 // shfunc with the script it was defined in so `whence -v fn`
3983 // and `type fn` can print `is a shell function from <script>`.
3984 // For `-c '...'` invocations zsh sets scriptfilename to "zsh".
3985 // Without this seed, fusevm-compiled functions all had
3986 // filename=None and `type fn` lost the "from <script>" suffix.
3987 shfunc {
3988 node: hashnode {
3989 next: None,
3990 nam: name.to_string(),
3991 flags: 0,
3992 },
3993 filename: scriptfilename_get(),
3994 lineno: 0,
3995 funcdef: None,
3996 redir: None,
3997 sticky: None,
3998 body: Some(body.to_string()),
3999 redir_text: None,
4000 }
4001}
4002
4003/// Build an autoload-marker `shfunc`. Mirrors C's
4004/// `createshfunc(name); shf->node.flags = PM_UNDEFINED;` at
4005/// hashtable.c:829.
4006pub fn shfunc_autoload(name: &str) -> shfunc {
4007 // c:829 idiom
4008 shfunc {
4009 node: hashnode {
4010 next: None,
4011 nam: name.to_string(),
4012 flags: PM_UNDEFINED as i32,
4013 },
4014 filename: None,
4015 lineno: 0,
4016 funcdef: None,
4017 redir: None,
4018 sticky: None,
4019 body: None,
4020 redir_text: None,
4021 }
4022}
4023
4024// -----------------------------------------------------------
4025// cmdnamtab / aliastab / sufaliastab / reswdtab / histtab
4026// global singletons. Match C's `mod_export HashTable cmdnamtab;`
4027// (hashtable.c:594) and friends. Each is lazily initialised on
4028// first access.
4029// -----------------------------------------------------------
4030
4031// hash table containing external commands // c:587
4032/// Singleton accessor for the global `cmdnamtab`.
4033/// Mirrors C's `mod_export HashTable cmdnamtab` (hashtable.c:594).
4034/// Per PORT_PLAN.md Phase 3 (bucket-2, read-mostly): the PATH cache
4035/// is read on every command resolution but mutated only by `hash`,
4036/// `rehash`, or `path` reassignment. `RwLock` lets parallel command
4037/// lookups proceed without serialising on a single mutex. Holder
4038/// accessor keeps the `_lock` suffix for source-stability (call
4039/// sites use `.read()`/`.write()` directly).
4040pub fn cmdnamtab_lock() -> &'static std::sync::RwLock<cmdnam_table> {
4041 // c:594
4042 static CMDNAMTAB: std::sync::OnceLock<std::sync::RwLock<cmdnam_table>> =
4043 std::sync::OnceLock::new();
4044 CMDNAMTAB.get_or_init(|| std::sync::RwLock::new(cmdnam_table::new()))
4045}
4046
4047/// Port of `mod_export char **pathchecked;` from `Src/hashtable.c:595`.
4048///
4049/// Cursor into the `$path` array tracking how far the PATH-hash-on-
4050/// first-use machinery has walked. Bumped by `hashcmd` (exec.c:1042)
4051/// after each successful lookup so subsequent `hashdir` calls only
4052/// scan entries we haven't already cached.
4053///
4054/// C uses `char **pathchecked` (pointer into the `path[]` array); the
4055/// Rust port stores an index since `$path` lives in paramtab and is
4056/// re-fetched on each access. Reset to 0 by `path` reassignment per
4057/// `Src/hashtable.c:618`.
4058pub static pathchecked: std::sync::atomic::AtomicUsize = // c:595
4059 std::sync::atomic::AtomicUsize::new(0);
4060
4061// hash table containing the aliases // c:1174
4062/// Singleton accessor for the global `aliastab`.
4063/// Mirrors C's `mod_export HashTable aliastab` (hashtable.c:1186).
4064/// Bucket-2 read-mostly: aliases are looked up on every command word,
4065/// mutated only by `alias`/`unalias`. `RwLock` per PORT_PLAN.md.
4066pub fn aliastab_lock() -> &'static std::sync::RwLock<alias_table> {
4067 // c:1186
4068 static ALIASTAB: std::sync::OnceLock<std::sync::RwLock<alias_table>> =
4069 std::sync::OnceLock::new();
4070 ALIASTAB.get_or_init(|| std::sync::RwLock::new(alias_table::with_defaults()))
4071}
4072
4073/// Singleton accessor for the global `sufaliastab`.
4074/// Mirrors C's `mod_export HashTable sufaliastab` (hashtable.c:1187).
4075/// Bucket-2 read-mostly: same rationale as `aliastab`.
4076pub fn sufaliastab_lock() -> &'static std::sync::RwLock<alias_table> {
4077 static SUFALIASTAB: std::sync::OnceLock<std::sync::RwLock<alias_table>> =
4078 std::sync::OnceLock::new();
4079 // c:1221 — `sufaliastab = newhashtable(11, "sufaliastab", NULL);`
4080 // "Table for suffix aliases --- make this smaller" (c:1219). The
4081 // smaller `hsize` gives a DIFFERENT bucket walk than `aliastab`'s
4082 // 23, so `${(k)saliases}` order depends on getting this exact
4083 // number. Built inline rather than via `alias_table::new()` (which
4084 // is `aliastab`'s 23) because a second named constructor would be
4085 // a Rust-only fn with no C counterpart.
4086 SUFALIASTAB.get_or_init(|| {
4087 std::sync::RwLock::new(alias_table {
4088 table: crate::cow_map::CowArc::new(hashtable_nodes::newhashtable(11)), // c:1221
4089 })
4090 })
4091}
4092
4093// hash table containing the reserved words // c:1111
4094/// Singleton accessor for the global `reswdtab`.
4095/// Mirrors C's `HashTable reswdtab` (hashtable.c, file-scope).
4096/// Bucket-2 read-mostly (effectively read-only post-init): every
4097/// command word is checked against reserved words; the table is
4098/// populated once at startup. `RwLock` per PORT_PLAN.md.
4099pub fn reswdtab_lock() -> &'static std::sync::RwLock<reswd_table> {
4100 // c:1115
4101 static reswdTAB: std::sync::OnceLock<std::sync::RwLock<reswd_table>> =
4102 std::sync::OnceLock::new();
4103 reswdTAB.get_or_init(|| std::sync::RwLock::new(reswd_table::new()))
4104}
4105
4106/// Singleton accessor for the global `histtab` (history events).
4107/// Mirrors C's `HashTable histtab` (hashtable.c:1340).
4108pub fn histtab_lock() -> &'static std::sync::RwLock<HashMap<String, i32>> {
4109 static HISTTAB: std::sync::OnceLock<std::sync::RwLock<HashMap<String, i32>>> =
4110 std::sync::OnceLock::new();
4111 HISTTAB.get_or_init(|| std::sync::RwLock::new(HashMap::new()))
4112}
4113
4114// ===========================================================
4115// shfunctab — the global shell-function table.
4116//
4117// Port of `mod_export HashTable shfunctab` from
4118// `Src/hashtable.c:808` and the GSU callbacks built around it
4119// (`createshfunctable` and the `*shfuncnode` family).
4120//
4121// C zsh dispatches every `function f() { … }` definition,
4122// `unfunction`, `disable -f`, `enable -f`, `whence`, and trap-
4123// function lookup through `shfunctab`. zshrs uses a singleton
4124// `OnceLock<Mutex<shfunc_table>>` exposed via `shfunctab_lock()`
4125// so the GSU-style C names below can mutate it without taking a
4126// `ShellExecutor` parameter (matching the C signatures, where
4127// the table is global).
4128// ===========================================================
4129
4130/// Singleton accessor for the global `shfunctab`.
4131/// Mirrors C's `mod_export HashTable shfunctab` (hashtable.c:808).
4132/// Lazily initialised on first access. Bucket-2 read-mostly: shell
4133/// functions are looked up on every function-call dispatch, mutated
4134/// only by `function f()` / `unfunction` / `autoload`. `RwLock`
4135/// per PORT_PLAN.md.
4136pub fn shfunctab_lock() -> &'static std::sync::RwLock<shfunc_table> {
4137 // c:808
4138 static shfuncTAB: std::sync::OnceLock<std::sync::RwLock<shfunc_table>> =
4139 std::sync::OnceLock::new();
4140 shfuncTAB.get_or_init(|| std::sync::RwLock::new(shfunc_table::new()))
4141}
4142
4143/// Glob-style match for hashtable scan callers. Direct port of C's
4144/// `pattry(pprog, hn->nam)` at `Src/hashtable.c:412` / `c:431` —
4145/// `scanmatchtable` compiles the caller's pattern once into a
4146/// `Patprog` and tests every node's name against it. zshrs's
4147/// `patmatch(pattern, text)` (pattern.rs:1561) does the
4148/// `patcompile + pattry` pair in one call, so we route through
4149/// it directly.
4150///
4151/// Previously this was an ad-hoc 30-line recursive matcher that
4152/// only handled `*` and `?` — char classes (`[abc]`), numeric
4153/// ranges (`<1-9>`), recursive globs, and the rest of zsh's
4154/// extended-glob set silently fell through. Now uses the
4155/// canonical engine.
4156fn simple_glob_match(pattern: &str, name: &str) -> bool {
4157 // c:hashtable.c:412 — `scanmatchtable` callers pass a compiled
4158 // `Patprog`; this helper inlines the compile+match since callers
4159 // here have only the raw pattern string.
4160 patcompile(
4161 &{
4162 let mut __pat_tok = (pattern).to_string();
4163 crate::ported::glob::tokenize(&mut __pat_tok);
4164 __pat_tok
4165 },
4166 PAT_HEAPDUP as i32,
4167 None,
4168 )
4169 .map_or(false, |p| pattry(&p, name))
4170}
4171
4172// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4173// ─── RUST-ONLY ACCESSORS ───
4174//
4175// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
4176// RwLock<T>>` globals declared above. C zsh uses direct global
4177// access; Rust needs these wrappers because `OnceLock::get_or_init`
4178// is the only way to lazily construct shared state. These ported sit
4179// here so the body of this file reads in C source order without
4180// the accessor wrappers interleaved between real port ported.
4181// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4182
4183// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4184// ─── RUST-ONLY ACCESSORS ───
4185//
4186// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
4187// RwLock<T>>` globals declared above. C zsh uses direct global
4188// access; Rust needs these wrappers because `OnceLock::get_or_init`
4189// is the only way to lazily construct shared state. These ported sit
4190// here so the body of this file reads in C source order without
4191// the accessor wrappers interleaved between real port ported.
4192// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4193
4194/// Singleton accessor for the `dircache` file-static at
4195/// `Src/hashtable.c:1517`.
4196pub fn dircache_lock() -> &'static std::sync::Mutex<Vec<dircache_entry>> {
4197 DIRCACHE_INNER.get_or_init(|| std::sync::Mutex::new(Vec::new()))
4198}
4199
4200#[cfg(test)]
4201mod tests {
4202 use std::cmp::Ordering;
4203
4204 use super::*;
4205
4206 /// Complexity pin for the `$( … )` alias snapshot. Taking it must not
4207 /// copy the table, and EXPANDING an alias inside the body — which
4208 /// flips `inuse` on every use (`Src/lex.c:1929`, `Src/input.c:773`) —
4209 /// must not copy it either. Only a real definition pays, once, and
4210 /// `restore` then drops the body's table whole.
4211 #[test]
4212 fn alias_snapshot_is_shared_until_a_definition_splits_it() {
4213 let mut live = alias_table::new();
4214 for i in 0..2000 {
4215 live.add(createaliasnode(&format!("a{i}"), "print", 0));
4216 }
4217 let snap = live.snapshot();
4218 assert!(crate::cow_map::CowArc::ptr_eq(&live.table, &snap.table));
4219
4220 // What the lexer does for every expansion.
4221 let a = live.get("a7").unwrap();
4222 a.inuse.store(1, std::sync::atomic::Ordering::Relaxed);
4223 a.inuse.store(0, std::sync::atomic::Ordering::Relaxed);
4224 assert!(
4225 crate::cow_map::CowArc::ptr_eq(&live.table, &snap.table),
4226 "expanding an alias split the table"
4227 );
4228
4229 live.add(createaliasnode("inner", "echo", 0));
4230 live.remove("a1");
4231 assert!(!crate::cow_map::CowArc::ptr_eq(&live.table, &snap.table));
4232 assert!(snap.get("inner").is_none() && snap.get("a1").is_some());
4233
4234 live.restore(snap);
4235 assert!(live.get("inner").is_none());
4236 assert!(live.get("a1").is_some());
4237 assert_eq!(live.len(), 2000);
4238 }
4239
4240 /// Same pin for `cmdnamtab`: a snapshot shares the (possibly
4241 /// thousands-strong, after a `$PATH` fill) bucket array.
4242 #[test]
4243 fn cmdnam_snapshot_is_shared_until_a_hash_splits_it() {
4244 let mut live = cmdnam_table::new();
4245 live.add(cmdnam_hashed("ls", "/bin/ls"));
4246 let snap = live.snapshot();
4247 assert!(crate::cow_map::CowArc::ptr_eq(&live.table, &snap.table));
4248 assert!(live.get("ls").is_some());
4249 assert!(crate::cow_map::CowArc::ptr_eq(&live.table, &snap.table));
4250
4251 live.add(cmdnam_hashed("zq", "/bin/echo"));
4252 assert!(snap.get("zq").is_none());
4253 live.restore(snap);
4254 assert!(live.get("zq").is_none() && live.get("ls").is_some());
4255 }
4256
4257 #[test]
4258 fn test_hasher() {
4259 let _g = crate::test_util::global_state_lock();
4260 assert_eq!(hasher(""), 0);
4261 assert_ne!(hasher("test"), 0);
4262 assert_eq!(hasher("test"), hasher("test"));
4263 assert_ne!(hasher("test"), hasher("Test"));
4264 }
4265
4266 /// Pin `hnamcmp` to its canonical C body at `Src/hashtable.c:341-346`:
4267 /// must route through `ztrcmp` (META-AWARE compare), not naive
4268 /// `str::cmp`. The previous Rust port used byte-wise cmp which
4269 /// sorts Meta-encoded keys incorrectly.
4270 #[test]
4271 fn hnamcmp_uses_ztrcmp_meta_aware_compare() {
4272 let _g = crate::test_util::global_state_lock();
4273 // Plain ASCII: same as str::cmp.
4274 assert_eq!(hnamcmp("apple", "banana"), Ordering::Less);
4275 assert_eq!(hnamcmp("banana", "apple"), Ordering::Greater);
4276 assert_eq!(hnamcmp("equal", "equal"), Ordering::Equal);
4277
4278 // Empty string sorts before non-empty.
4279 assert_eq!(hnamcmp("", "a"), Ordering::Less);
4280 assert_eq!(hnamcmp("a", ""), Ordering::Greater);
4281
4282 // Meta-encoded byte: 0x83 0x41 → real 0x61 ('a'). The
4283 // Meta-aware ztrcmp treats `\x83\x41` as 'a' for compare
4284 // purposes; naive str::cmp would compare 0x83 vs 0x61 (so
4285 // "\x83\x41" would sort AFTER 'a'). Verify the Meta-aware
4286 // path: the encoded "a" should compare equal-ish to "a".
4287 // Construct via unsafe bytes since 0x83 isn't valid UTF-8
4288 // alone — Rust ztrcmp operates on bytes.
4289 let meta_a_bytes: Vec<u8> = vec![0x83, 0x41]; // Meta + 'A'^32 = 'a'
4290 let meta_a = unsafe { std::str::from_utf8_unchecked(&meta_a_bytes) };
4291 // Real "a" (0x61) vs encoded "a" (0x83 0x41): ztrcmp resolves
4292 // both to 0x61 at the first position → Equal. But ztrcmp also
4293 // takes into account end-of-string, so encoded "a" is longer
4294 // by one byte unstripped. The C ztrcmp loop skips matching
4295 // prefix; here the first bytes differ (0x61 vs 0x83), so it
4296 // resolves c1=0x61, c2=(0x41^32)=0x61 → Equal. Verify.
4297 assert_eq!(
4298 hnamcmp("a", meta_a),
4299 Ordering::Equal,
4300 "c:345 — Meta-encoded 'a' (0x83 0x41) compares equal to real 'a'"
4301 );
4302 }
4303
4304 #[test]
4305 fn test_histhasher() {
4306 let _g = crate::test_util::global_state_lock();
4307 assert_eq!(histhasher(" hello world "), histhasher("hello world"));
4308 assert_ne!(histhasher("hello world"), histhasher("helloworld"));
4309 }
4310
4311 /// `Src/hashtable.c:1365-1380` — `histhasher` uses `inblank(*str)`
4312 /// per `Src/ztype.h:50`: NARROW space/tab only. The previous Rust
4313 /// port used `c.is_whitespace()` (broad Unicode) which would have
4314 /// silently rehashed any history line containing CR/FF/VT/NBSP.
4315 /// Pin the narrow-inblank semantics:
4316 /// * Multi-space/tab runs collapse to a single ' ' bucket-mix.
4317 /// * Newlines are NOT collapsed (newline is not inblank per c:50).
4318 /// * NBSP / CR are NOT treated as inblank.
4319 #[test]
4320 fn histhasher_inblank_is_narrow_space_tab_only() {
4321 let _g = crate::test_util::global_state_lock();
4322 // c:1369 — leading inblank stripped; multiple equivalent forms hash same.
4323 assert_eq!(
4324 histhasher("\t hello"),
4325 histhasher("hello"),
4326 "c:1369 — leading space+tab stripped before mixing"
4327 );
4328 // c:1373 — runs of inblank collapse to a single ' '.
4329 assert_eq!(
4330 histhasher("a \t b"),
4331 histhasher("a b"),
4332 "c:1373 — interior inblank runs collapse to single space"
4333 );
4334
4335 // Newline is NOT inblank per c:50; it must hash as itself, not collapse.
4336 assert_ne!(
4337 histhasher("a\nb"),
4338 histhasher("a b"),
4339 "c:50 — newline is NOT inblank; hashes as its own char"
4340 );
4341 // CR is NOT inblank.
4342 assert_ne!(
4343 histhasher("a\rb"),
4344 histhasher("ab"),
4345 "CR not in inblank; must mix as a character, not collapse"
4346 );
4347 // NBSP (0xA0) is NOT inblank (it's broad Unicode whitespace
4348 // but NOT in C's narrow typtab class).
4349 assert_ne!(
4350 histhasher("a\u{00A0}b"),
4351 histhasher("ab"),
4352 "NBSP not in inblank; must mix as a character, not collapse"
4353 );
4354 }
4355
4356 #[test]
4357 fn test_histstrcmp() {
4358 let _g = crate::test_util::global_state_lock();
4359 assert_eq!(
4360 histstrcmp(" hello world ", "hello world", false),
4361 Ordering::Equal
4362 );
4363 assert_eq!(
4364 histstrcmp("hello world", "hello world", true),
4365 Ordering::Equal
4366 );
4367 }
4368
4369 /// `Src/hashtable.c:1396-1421` — `histstrcmp` uses `inblank(*str)`
4370 /// (NARROW space/tab only per `Src/ztype.h:50`). The previous Rust
4371 /// port used `c.is_whitespace()` (broad Unicode) which silently
4372 /// folded history lines that C considers distinct.
4373 /// Pin narrow-inblank semantics.
4374 #[test]
4375 fn histstrcmp_inblank_is_narrow_space_tab_only() {
4376 let _g = crate::test_util::global_state_lock();
4377 // c:1411-1413 — runs of inblank collapse to a single boundary.
4378 assert_eq!(
4379 histstrcmp("hello\tworld", "hello world", false),
4380 Ordering::Equal,
4381 "c:1411-1413 — tab and space both inblank; mixed runs equal"
4382 );
4383 // Newline is NOT inblank per c:50 → string mismatch.
4384 assert_ne!(
4385 histstrcmp("hello\nworld", "hello world", false),
4386 Ordering::Equal,
4387 "c:50 — newline is NOT inblank; must be treated as ordinary char"
4388 );
4389 // CR is NOT inblank.
4390 assert_ne!(
4391 histstrcmp("hello\rworld", "hello world", false),
4392 Ordering::Equal,
4393 "CR not in inblank; not collapsed with space"
4394 );
4395 // NBSP is NOT inblank (broad Unicode whitespace, NOT typtab).
4396 assert_ne!(
4397 histstrcmp("hello\u{00A0}world", "hello world", false),
4398 Ordering::Equal,
4399 "NBSP not in inblank; not collapsed"
4400 );
4401 // c:1405 — HISTREDUCEBLANKS short-circuits to raw cmp.
4402 // With reduce_blanks=true the multi-space form is NOT collapsed.
4403 assert_ne!(
4404 histstrcmp("hello world", "hello world", true),
4405 Ordering::Equal,
4406 "c:1405 — HISTREDUCEBLANKS=true → strcmp; runs do NOT collapse"
4407 );
4408 }
4409
4410 /// `Src/hashtable.c:1398-1399` — leading inblank is stripped from
4411 /// both sides BEFORE comparison. So `" cmd"` and `"\tcmd"` are
4412 /// equal. Trailing inblank (per the loop behavior, c:1421
4413 /// `*str1 - *str2` reaches 0 when one side runs out) is also
4414 /// folded: trailing run on one side vs end on the other returns
4415 /// Equal via the (Some, None) inblank-collapse branch.
4416 #[test]
4417 fn histstrcmp_strips_leading_and_trailing_inblank() {
4418 let _g = crate::test_util::global_state_lock();
4419 assert_eq!(
4420 histstrcmp(" cmd", "\tcmd", false),
4421 Ordering::Equal,
4422 "c:1398-1399 — leading inblank skipped (both kinds)"
4423 );
4424 assert_eq!(
4425 histstrcmp("cmd ", "cmd", false),
4426 Ordering::Equal,
4427 "c:1421 — trailing inblank on left collapses to end-equal"
4428 );
4429 assert_eq!(
4430 histstrcmp("cmd", "cmd\t\t", false),
4431 Ordering::Equal,
4432 "c:1421 — trailing inblank on right collapses to end-equal"
4433 );
4434 }
4435
4436 #[test]
4437 fn test_cmdnam_table() {
4438 let _g = crate::test_util::global_state_lock();
4439 let mut table = cmdnam_table::new();
4440 table.add(cmdnam_hashed("ls", "/bin/ls"));
4441
4442 assert!(table.get("ls").is_some());
4443 assert!(table.get("nonexistent").is_none());
4444
4445 let ls = table.get("ls").unwrap();
4446 assert_ne!((ls.node.flags & HASHED as i32), 0);
4447 assert_eq!((ls.node.flags & DISABLED as i32), 0);
4448 }
4449
4450 #[test]
4451 fn test_shfunc_table() {
4452 let _g = crate::test_util::global_state_lock();
4453 let mut table = shfunc_table::new();
4454 table.add(shfunc_with_body("myfunc", "echo hello"));
4455 table.add(shfunc_autoload("lazy"));
4456
4457 assert!(table.get("myfunc").is_some());
4458 assert_eq!(
4459 (table.get("myfunc").unwrap().node.flags & PM_UNDEFINED as i32),
4460 0
4461 );
4462 assert_ne!(
4463 (table.get("lazy").unwrap().node.flags & PM_UNDEFINED as i32),
4464 0
4465 );
4466
4467 table.disable("myfunc");
4468 assert!(table.get("myfunc").is_none());
4469 assert!(table.get_including_disabled("myfunc").is_some());
4470
4471 table.enable("myfunc");
4472 assert!(table.get("myfunc").is_some());
4473 }
4474
4475 #[test]
4476 fn test_reswd_table() {
4477 let _g = crate::test_util::global_state_lock();
4478 let table = reswd_table::new();
4479
4480 assert!(table.is_reserved("if"));
4481 assert!(table.is_reserved("while"));
4482 assert!(table.is_reserved("[["));
4483 assert!(!table.is_reserved("notreserved"));
4484
4485 let if_rw = table.get("if").unwrap();
4486 assert_eq!(if_rw.token, IF);
4487 }
4488
4489 #[test]
4490 fn test_alias_table() {
4491 let _g = crate::test_util::global_state_lock();
4492 let mut table = alias_table::with_defaults();
4493
4494 assert!(table.get("run-help").is_some());
4495 assert_eq!(table.get("run-help").unwrap().text, "man");
4496
4497 table.add(createaliasnode("G", "| grep", ALIAS_GLOBAL as u32));
4498 let g = table.get("G").unwrap();
4499 assert_ne!((g.node.flags & ALIAS_GLOBAL as i32), 0);
4500
4501 table.add(createaliasnode("pdf", "zathura", ALIAS_SUFFIX as u32));
4502 let p = table.get("pdf").unwrap();
4503 assert_ne!((p.node.flags & ALIAS_SUFFIX as i32), 0);
4504
4505 table.disable("G");
4506 assert!(table.get("G").is_none());
4507 }
4508
4509 #[test]
4510 fn test_dir_cache() {
4511 let _g = crate::test_util::global_state_lock();
4512 // Smoke-test the canonical `dircache` file-static at
4513 // hashtable.c:1517 — the cache lives in a global Mutex
4514 // matching C semantics. Each test gets a fresh slice via
4515 // a unique-name marker so parallel tests don't collide.
4516 let cache = dircache_lock();
4517 {
4518 let mut g = cache.lock().unwrap();
4519 g.clear();
4520 g.push(dircache_entry {
4521 name: "/usr/share/zsh".into(),
4522 refs: 1,
4523 });
4524 g.push(dircache_entry {
4525 name: "/usr/share/zsh".into(),
4526 refs: 1,
4527 });
4528 // Dedupe-by-refs is the C semantic: get_or_insert bumps
4529 // refs on an existing entry. Verify the data shape.
4530 assert_eq!(g.len(), 2);
4531 assert_eq!(g[0].refs, 1);
4532 }
4533 }
4534
4535 // -------------------------------------------------------------
4536 // Tests for the global shfunctab singleton & GSU callbacks.
4537 //
4538 // Tests are serialised via shfuncTAB_TEST_LOCK because they
4539 // mutate the process-wide singleton.
4540 // -------------------------------------------------------------
4541
4542 static shfuncTAB_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
4543
4544 fn fresh_shfunctab() {
4545 let mut tab = shfunctab_lock().write().expect("shfunctab poisoned");
4546 tab.clear();
4547 }
4548
4549 #[test]
4550 fn test_createshfunctable_idempotent() {
4551 let _g = crate::test_util::global_state_lock();
4552 let _g = shfuncTAB_TEST_LOCK.lock();
4553 createshfunctable();
4554 createshfunctable();
4555 // Singleton handle stable across calls.
4556 let h1 = shfunctab_lock() as *const _;
4557 let h2 = shfunctab_lock() as *const _;
4558 assert_eq!(h1, h2);
4559 }
4560
4561 #[test]
4562 fn test_shfunctab_add_get_remove() {
4563 let _g = crate::test_util::global_state_lock();
4564 let _g = shfuncTAB_TEST_LOCK.lock();
4565 fresh_shfunctab();
4566 {
4567 let mut tab = shfunctab_lock().write().unwrap();
4568 tab.add(shfunc_with_body("greet", "echo hello"));
4569 }
4570 {
4571 let tab = shfunctab_lock().read().unwrap();
4572 assert!(tab.get("greet").is_some());
4573 assert_eq!(
4574 tab.get("greet").unwrap().body.as_deref(),
4575 Some("echo hello")
4576 );
4577 }
4578 let removed = removeshfuncnode("greet");
4579 assert!(removed.is_some());
4580 assert!(shfunctab_lock().read().unwrap().get("greet").is_none());
4581 }
4582
4583 #[test]
4584 fn test_shfunctab_disable_enable() {
4585 let _g = crate::test_util::global_state_lock();
4586 let _g = shfuncTAB_TEST_LOCK.lock();
4587 fresh_shfunctab();
4588 {
4589 let mut tab = shfunctab_lock().write().unwrap();
4590 tab.add(shfunc_with_body("f", "true"));
4591 }
4592 disableshfuncnode("f");
4593 // get() filters disabled; get_including_disabled doesn't.
4594 {
4595 let tab = shfunctab_lock().read().unwrap();
4596 assert!(tab.get("f").is_none());
4597 assert!(tab.get_including_disabled("f").is_some());
4598 }
4599 enableshfuncnode("f");
4600 assert!(shfunctab_lock().read().unwrap().get("f").is_some());
4601 removeshfuncnode("f");
4602 }
4603
4604 #[test]
4605 fn test_simple_glob_match() {
4606 let _g = crate::test_util::global_state_lock();
4607 assert!(simple_glob_match("foo", "foo"));
4608 assert!(!simple_glob_match("foo", "bar"));
4609 assert!(simple_glob_match("f*", "foo"));
4610 assert!(simple_glob_match("f*", "f"));
4611 assert!(simple_glob_match("*o", "foo"));
4612 assert!(simple_glob_match("*", ""));
4613 assert!(simple_glob_match("?oo", "foo"));
4614 assert!(!simple_glob_match("?oo", "fo"));
4615 assert!(simple_glob_match("f*o", "frogspawn-suo"));
4616 }
4617
4618 #[test]
4619 fn test_scanmatchshfunc_matches_pattern() {
4620 let _g = crate::test_util::global_state_lock();
4621 let _g = shfuncTAB_TEST_LOCK.lock();
4622 fresh_shfunctab();
4623 {
4624 let mut tab = shfunctab_lock().write().unwrap();
4625 tab.add(shfunc_with_body("foo", "echo a"));
4626 tab.add(shfunc_with_body("foobar", "echo b"));
4627 tab.add(shfunc_with_body("baz", "echo c"));
4628 }
4629 let mut matched: Vec<String> = Vec::new();
4630 let count = scanmatchshfunc(Some("foo*"), |name, _| matched.push(name.to_string()));
4631 assert_eq!(count, 2);
4632 matched.sort();
4633 assert_eq!(matched, vec!["foo".to_string(), "foobar".to_string()]);
4634 // No-pattern walks all.
4635 let total = scanshfunc(|_, _| {});
4636 assert_eq!(total, 3);
4637 fresh_shfunctab();
4638 }
4639
4640 #[test]
4641 fn test_getshfuncfile_returns_filename() {
4642 let _g = crate::test_util::global_state_lock();
4643 let _g = shfuncTAB_TEST_LOCK.lock();
4644 fresh_shfunctab();
4645 {
4646 let mut tab = shfunctab_lock().write().unwrap();
4647 let mut f = shfunc_with_body("f", "true");
4648 f.filename = Some("/tmp/zshrs-ported/f".to_string());
4649 tab.add(f);
4650 }
4651 assert_eq!(getshfuncfile("f"), Some("/tmp/zshrs-ported/f".to_string()));
4652 assert_eq!(getshfuncfile("nonexistent"), None);
4653 fresh_shfunctab();
4654 }
4655
4656 // -------------------------------------------------------------
4657 // Generic hashtable ops + per-table singletons.
4658 // -------------------------------------------------------------
4659
4660 #[test]
4661 fn test_generic_addhashnode_displaces_old() {
4662 let _g = crate::test_util::global_state_lock();
4663 let mut ht: HashMap<String, alias> = HashMap::new();
4664 addhashnode(&mut ht, "x", createaliasnode("x", "echo a", 0));
4665 let old = addhashnode2(&mut ht, "x", createaliasnode("x", "echo b", 0));
4666 assert!(old.is_some());
4667 assert_eq!(old.unwrap().text, "echo a");
4668 assert_eq!(gethashnode2(&ht, "x").unwrap().text, "echo b");
4669 }
4670
4671 #[test]
4672 fn test_generic_disable_filters_get() {
4673 let _g = crate::test_util::global_state_lock();
4674 let mut ht: HashMap<String, alias> = HashMap::new();
4675 ht.insert("a".to_string(), createaliasnode("a", "1", 0));
4676 assert!(gethashnode(&ht, "a").is_some());
4677 disablehashnode(&mut ht, "a");
4678 // gethashnode filters disabled, gethashnode2 doesn't.
4679 assert!(gethashnode(&ht, "a").is_none());
4680 assert!(gethashnode2(&ht, "a").is_some());
4681 enablehashnode(&mut ht, "a");
4682 assert!(gethashnode(&ht, "a").is_some());
4683 }
4684
4685 #[test]
4686 fn test_scanmatchtable_pattern_and_count() {
4687 let _g = crate::test_util::global_state_lock();
4688 let mut ht: HashMap<String, alias> = HashMap::new();
4689 ht.insert("foo".to_string(), createaliasnode("foo", "1", 0));
4690 ht.insert("foobar".to_string(), createaliasnode("foobar", "2", 0));
4691 ht.insert("baz".to_string(), createaliasnode("baz", "3", 0));
4692 let mut hits: Vec<String> = Vec::new();
4693 let count = scanmatchtable(&ht, Some("foo*"), true, 0, 0, |n, _| {
4694 hits.push(n.to_string())
4695 });
4696 assert_eq!(count, 2);
4697 // Sorted output guaranteed when sorted=true.
4698 assert_eq!(hits, vec!["foo".to_string(), "foobar".to_string()]);
4699 }
4700
4701 #[test]
4702 fn test_emptyhashtable_clears() {
4703 let _g = crate::test_util::global_state_lock();
4704 let mut ht: HashMap<String, alias> = HashMap::new();
4705 ht.insert("a".to_string(), createaliasnode("a", "1", 0));
4706 ht.insert("b".to_string(), createaliasnode("b", "2", 0));
4707 assert_eq!(ht.len(), 2);
4708 emptyhashtable(&mut ht);
4709 assert_eq!(ht.len(), 0);
4710 }
4711
4712 #[test]
4713 fn test_resizehashtable_reserves_capacity() {
4714 let _g = crate::test_util::global_state_lock();
4715 let mut ht: HashMap<String, i32> = HashMap::new();
4716 let initial_cap = ht.capacity();
4717 resizehashtable(&mut ht, 200);
4718 assert!(ht.capacity() >= 200);
4719 assert!(ht.capacity() >= initial_cap);
4720 }
4721
4722 #[test]
4723 fn test_aliastab_singleton_has_defaults() {
4724 let _g = crate::test_util::global_state_lock();
4725 let tab = aliastab_lock().read().unwrap();
4726 // createaliastables seeds run-help and which-command.
4727 assert!(tab.get_including_disabled("run-help").is_some());
4728 assert!(tab.get_including_disabled("which-command").is_some());
4729 }
4730
4731 #[test]
4732 fn test_createaliasnode_sets_flags() {
4733 let _g = crate::test_util::global_state_lock();
4734 let a = createaliasnode("foo", "echo bar", ALIAS_GLOBAL as u32);
4735 assert_eq!(a.node.nam, "foo");
4736 assert_eq!(a.text, "echo bar");
4737 assert_ne!((a.node.flags & ALIAS_GLOBAL as i32), 0);
4738 }
4739
4740 #[test]
4741 fn test_printaliasnode_smoke() {
4742 // printaliasnode writes directly to stdout (matches C's void
4743 // return / writes-to-stdout signature). The behavioural parity
4744 // assertions live in `tests/builtin_c_parity.rs::alias_builtin`,
4745 // which compares against `/bin/zsh -fc 'alias gst'` byte-for-byte.
4746 // This unit test just exercises every flag branch to make sure
4747 // none panics / borrows incorrectly.
4748 let _g = crate::test_util::global_state_lock();
4749 let a = createaliasnode("ll", "ls -la", 0);
4750 printaliasnode(&a, PRINT_NAMEONLY);
4751 printaliasnode(&a, PRINT_WHENCE_WORD);
4752 printaliasnode(&a, PRINT_WHENCE_SIMPLE);
4753 printaliasnode(&a, PRINT_WHENCE_CSH);
4754 printaliasnode(&a, PRINT_WHENCE_VERBOSE);
4755 printaliasnode(&a, PRINT_LIST);
4756 printaliasnode(&a, 0);
4757 }
4758
4759 #[test]
4760 fn test_printreswdnode_smoke() {
4761 // printreswdnode writes directly to stdout (matches C's void
4762 // return / write-to-stdout signature at hashtable.c:1147).
4763 // Smoke-test every flag branch to make sure none panics.
4764 let _g = crate::test_util::global_state_lock();
4765 let table = reswd_table::new();
4766 let if_rw = table.get("if").unwrap();
4767 printreswdnode(if_rw, PRINT_WHENCE_WORD);
4768 printreswdnode(if_rw, PRINT_WHENCE_CSH);
4769 printreswdnode(if_rw, PRINT_WHENCE_VERBOSE);
4770 printreswdnode(if_rw, 0);
4771 }
4772
4773 #[test]
4774 fn test_addhistnode_displaces_old() {
4775 let _g = crate::test_util::global_state_lock();
4776 emptyhisttable();
4777 assert_eq!(addhistnode("ls -la", 1), None);
4778 let old = addhistnode("ls -la", 5);
4779 assert_eq!(old, Some(1));
4780 emptyhisttable();
4781 }
4782
4783 #[test]
4784 fn test_freecmdnamnode_removes() {
4785 let _g = crate::test_util::global_state_lock();
4786 emptycmdnamtable();
4787 {
4788 let mut tab = cmdnamtab_lock().write().unwrap();
4789 tab.add(cmdnam_unhashed("ls", vec!["/bin".to_string()]));
4790 }
4791 assert!(cmdnamtab_lock().read().unwrap().get("ls").is_some());
4792 freecmdnamnode("ls");
4793 assert!(cmdnamtab_lock().read().unwrap().get("ls").is_none());
4794 }
4795
4796 #[test]
4797 fn test_dircache_set_refcounts() {
4798 let _g = crate::test_util::global_state_lock();
4799 // Refcount add → entries grow.
4800 let mut k: Option<String> = None;
4801 dircache_set(&mut k, Some("/usr/bin"));
4802 let mut k2: Option<String> = None;
4803 dircache_set(&mut k2, Some("/usr/bin"));
4804 let cache_size = dircache_lock().lock().unwrap().len();
4805 assert!(cache_size >= 1);
4806 }
4807
4808 /// c:1230 — `createaliasnode(name, text, flags)` builds an crate::ported::zsh_h::alias
4809 /// with the text field populated. Regression that drops `text`
4810 /// would silently install aliases that expand to nothing.
4811 #[test]
4812 fn createaliasnode_round_trips_name_and_text() {
4813 let _g = crate::test_util::global_state_lock();
4814 let a = createaliasnode("ls-color", "ls --color=auto", 0);
4815 assert_eq!(a.text, "ls --color=auto");
4816 assert_eq!(a.node.nam, "ls-color");
4817 }
4818
4819 // ─── alias-creation zsh-corpus pins ────────────────────────────
4820
4821 /// `createaliasnode` round-trips name+text+flags=0 (regular alias).
4822 #[test]
4823 fn alias_corpus_create_regular_alias() {
4824 let _g = crate::test_util::global_state_lock();
4825 let a = createaliasnode("ll", "ls -la", 0);
4826 assert_eq!(a.node.nam, "ll");
4827 assert_eq!(a.text, "ls -la");
4828 // Regular alias = no GLOBAL/SUFFIX flags.
4829 let f = a.node.flags as i32;
4830 assert_eq!(
4831 f & (ALIAS_GLOBAL | ALIAS_SUFFIX),
4832 0,
4833 "regular alias has no GLOBAL/SUFFIX bits"
4834 );
4835 }
4836
4837 /// `createaliasnode` with ALIAS_GLOBAL flag sets the global bit.
4838 #[test]
4839 fn alias_corpus_create_global_alias_carries_flag() {
4840 let _g = crate::test_util::global_state_lock();
4841 let a = createaliasnode("G", "global text", ALIAS_GLOBAL as u32);
4842 let f = a.node.flags as i32;
4843 assert_ne!(f & ALIAS_GLOBAL, 0, "ALIAS_GLOBAL set");
4844 }
4845
4846 /// `createaliasnode` with ALIAS_SUFFIX flag sets the suffix bit.
4847 #[test]
4848 fn alias_corpus_create_suffix_alias_carries_flag() {
4849 let _g = crate::test_util::global_state_lock();
4850 let a = createaliasnode("S", "suffix text", ALIAS_SUFFIX as u32);
4851 let f = a.node.flags as i32;
4852 assert_ne!(f & ALIAS_SUFFIX, 0, "ALIAS_SUFFIX set");
4853 }
4854
4855 /// Empty text is preserved (zsh allows zero-length alias expansion).
4856 #[test]
4857 fn alias_corpus_create_empty_text_preserved() {
4858 let _g = crate::test_util::global_state_lock();
4859 let a = createaliasnode("noop", "", 0);
4860 assert_eq!(a.text, "");
4861 }
4862
4863 /// Alias text may contain spaces — preserved as-is.
4864 #[test]
4865 fn alias_corpus_create_multi_word_text_preserved() {
4866 let _g = crate::test_util::global_state_lock();
4867 let a = createaliasnode("rmf", "rm -rf --no-preserve-root", 0);
4868 assert_eq!(a.text, "rm -rf --no-preserve-root");
4869 }
4870
4871 /// `aliastab_lock` initialises with the two default aliases
4872 /// `run-help` and `which-command` per hashtable.c:1215-1216.
4873 /// A regression here breaks zsh's documented default behaviour
4874 /// where `run-help` resolves to `man` after `autoload -U run-help`.
4875 #[test]
4876 fn aliastab_seeds_run_help_and_which_command_defaults() {
4877 let _g = crate::test_util::global_state_lock();
4878 createaliastables();
4879 let tab = aliastab_lock().read().expect("aliastab poisoned");
4880 assert!(tab.get("run-help").is_some(), "run-help default missing");
4881 assert!(
4882 tab.get("which-command").is_some(),
4883 "which-command default missing"
4884 );
4885 }
4886
4887 /// c:86 — `hasher` is the canonical zsh string hash. Same input
4888 /// MUST produce same output (basic determinism); different inputs
4889 /// SHOULD produce different outputs (no pathological collisions
4890 /// for single-char-different strings). The wrapping_add chain in
4891 /// the impl makes this a Bernstein-style hash; verify it's stable.
4892 #[test]
4893 fn hasher_is_deterministic_across_calls() {
4894 let _g = crate::test_util::global_state_lock();
4895 assert_eq!(hasher("foo"), hasher("foo"));
4896 assert_eq!(hasher(""), hasher(""));
4897 // Common shell names should not collide trivially.
4898 assert_ne!(hasher("ls"), hasher("cd"));
4899 assert_ne!(hasher("foo"), hasher("bar"));
4900 }
4901
4902 /// c:86 — empty input hashes to 0 (the seed value). A regression
4903 /// changing the seed would invalidate every persisted hash + cause
4904 /// silent rebuild storms in the cache layer.
4905 #[test]
4906 fn hasher_empty_string_hashes_to_zero() {
4907 let _g = crate::test_util::global_state_lock();
4908 assert_eq!(hasher(""), 0);
4909 }
4910
4911 /// c:86 — single-byte input `c` hashes to `c as u32` exactly
4912 /// (the loop runs once: hashval = 0 + 0<<5 + c = c). Pins the
4913 /// canonical first-iteration formula.
4914 #[test]
4915 fn hasher_single_byte_equals_byte_value() {
4916 let _g = crate::test_util::global_state_lock();
4917 assert_eq!(hasher("a"), b'a' as u32);
4918 assert_eq!(hasher("Z"), b'Z' as u32);
4919 assert_eq!(hasher("0"), b'0' as u32);
4920 }
4921
4922 /// `Src/hashtable.c:90-91` — `hashval += (hashval << 5) + c`
4923 /// simplifies to `hashval = hashval*33 + c` (the Bernstein
4924 /// hash variant). Pin the exact two-byte formula so a refactor
4925 /// to a different polynomial (e.g. FNV / djb2 / siphash) fails
4926 /// loudly. Regression here invalidates every cached fpath/hash
4927 /// digest stored on disk.
4928 #[test]
4929 fn hasher_two_byte_matches_bernstein_polynomial() {
4930 let _g = crate::test_util::global_state_lock();
4931 // For "ab": h0=0; h1 = 0 + (0<<5) + 'a' = 97; h2 = 97 + (97<<5) + 'b' = 97 + 3104 + 98 = 3299.
4932 assert_eq!(
4933 hasher("ab"),
4934 97u32
4935 .wrapping_add(97u32.wrapping_shl(5))
4936 .wrapping_add(b'b' as u32)
4937 );
4938 assert_eq!(hasher("ab"), 3299);
4939 // Pin the exact value for "ls" — a name we'll lookup billions of times.
4940 let ls_expected = {
4941 let mut h: u32 = 0;
4942 for &c in b"ls" {
4943 h = h.wrapping_add(h.wrapping_shl(5)).wrapping_add(c as u32);
4944 }
4945 h
4946 };
4947 assert_eq!(hasher("ls"), ls_expected);
4948 }
4949
4950 /// c:86 — hasher must NOT mix in encoding/locale state — the
4951 /// algorithm is byte-by-byte. Multi-byte UTF-8 like 'é' (0xC3 0xA9)
4952 /// hashes the two bytes independently. Pin so a regression that
4953 /// uses chars instead of bytes (which would aggregate the two
4954 /// bytes into one codepoint) fails.
4955 #[test]
4956 fn hasher_processes_utf8_bytes_not_codepoints() {
4957 let _g = crate::test_util::global_state_lock();
4958 // 'é' UTF-8 = 0xC3 0xA9 — two bytes.
4959 let expected = {
4960 let mut h: u32 = 0;
4961 for &c in &[0xC3u8, 0xA9u8] {
4962 h = h.wrapping_add(h.wrapping_shl(5)).wrapping_add(c as u32);
4963 }
4964 h
4965 };
4966 assert_eq!(
4967 hasher("é"),
4968 expected,
4969 "c:90 — `*(unsigned char *) str++` reads BYTES, not codepoints"
4970 );
4971 }
4972
4973 /// c:157 — `addhashnode` inserts; `gethashnode2` reads back.
4974 /// Round-trip MUST yield the value just inserted. Regression
4975 /// returning None on a present key would break every command-
4976 /// table lookup.
4977 #[test]
4978 fn addhashnode_then_gethashnode2_round_trips() {
4979 let _g = crate::test_util::global_state_lock();
4980 let mut h: HashMap<String, i32> = HashMap::new();
4981 addhashnode(&mut h, "key1", 42);
4982 assert_eq!(gethashnode2(&h, "key1"), Some(&42));
4983 assert_eq!(gethashnode2(&h, "missing"), None);
4984 }
4985
4986 /// c:275 — `removehashnode` returns Some(value) when present and
4987 /// drops the entry. Subsequent lookup MUST miss. Regression
4988 /// returning Some without removing would let callers think they
4989 /// removed when they actually didn't.
4990 #[test]
4991 fn removehashnode_returns_value_and_drops_entry() {
4992 let _g = crate::test_util::global_state_lock();
4993 let mut h: HashMap<String, String> = HashMap::new();
4994 addhashnode(&mut h, "key1", "val".to_string());
4995 let removed = removehashnode(&mut h, "key1");
4996 assert_eq!(removed.as_deref(), Some("val"));
4997 assert!(
4998 gethashnode2(&h, "key1").is_none(),
4999 "after removehashnode, lookup must miss"
5000 );
5001 }
5002
5003 /// c:275 — `removehashnode` on a missing key returns None and
5004 /// doesn't mutate the table. A regression where it errors or
5005 /// inserts a sentinel would break `unalias missing` (which is
5006 /// supposed to fail-soft).
5007 #[test]
5008 fn removehashnode_missing_key_returns_none() {
5009 let _g = crate::test_util::global_state_lock();
5010 let mut h: HashMap<String, i32> = HashMap::new();
5011 addhashnode(&mut h, "k1", 1);
5012 let len_before = h.len();
5013 assert!(removehashnode(&mut h, "missing").is_none());
5014 assert_eq!(h.len(), len_before, "missing-key remove must not mutate");
5015 }
5016
5017 // ─── zsh-corpus pins: hashtable add/get/remove ──────────────────
5018
5019 /// `addhashnode2` returns None on first insert.
5020 #[test]
5021 fn hashtable_corpus_add_new_returns_none() {
5022 let mut h: HashMap<String, i32> = HashMap::new();
5023 assert!(addhashnode2(&mut h, "fresh", 7).is_none());
5024 assert_eq!(gethashnode2(&h, "fresh"), Some(&7));
5025 }
5026
5027 /// `addhashnode2` on an existing key returns the OLD value.
5028 #[test]
5029 fn hashtable_corpus_add_existing_returns_previous_value() {
5030 let mut h: HashMap<String, i32> = HashMap::new();
5031 addhashnode2(&mut h, "k", 1);
5032 let prev = addhashnode2(&mut h, "k", 2);
5033 assert_eq!(prev, Some(1), "old value returned on replace");
5034 assert_eq!(gethashnode2(&h, "k"), Some(&2), "new value installed");
5035 }
5036
5037 /// `gethashnode2` on missing key returns None.
5038 #[test]
5039 fn hashtable_corpus_get_missing_returns_none() {
5040 let h: HashMap<String, i32> = HashMap::new();
5041 assert!(gethashnode2(&h, "anything").is_none());
5042 }
5043
5044 /// `newhashtable` allocates `size` empty buckets and no function slots (c:100-126).
5045 #[test]
5046 fn hashtable_corpus_newhashtable_allocates_buckets() {
5047 let ht = newhashtable(64, "myht");
5048 assert_eq!(ht.hsize, 64);
5049 assert_eq!(ht.ct, 0);
5050 assert_eq!(ht.nodes.len(), 64);
5051 assert!(ht.addnode.is_none() && ht.hash.is_none());
5052 }
5053
5054 /// Round-trip with many distinct keys.
5055 #[test]
5056 fn hashtable_corpus_many_keys_round_trip() {
5057 let mut h: HashMap<String, i32> = HashMap::new();
5058 for i in 0..100 {
5059 addhashnode(&mut h, &format!("k{i}"), i);
5060 }
5061 for i in 0..100 {
5062 assert_eq!(gethashnode2(&h, &format!("k{i}")), Some(&i));
5063 }
5064 assert_eq!(h.len(), 100);
5065 }
5066
5067 /// `removehashnode` followed by `gethashnode2` shows missing.
5068 #[test]
5069 fn hashtable_corpus_remove_then_get_is_none() {
5070 let mut h: HashMap<String, String> = HashMap::new();
5071 addhashnode(&mut h, "x", "value".into());
5072 let _ = removehashnode(&mut h, "x");
5073 assert!(gethashnode2(&h, "x").is_none());
5074 }
5075
5076 // ═══════════════════════════════════════════════════════════════════
5077 // Additional C-parity tests for Src/hashtable.c hasher + hnamcmp +
5078 // generic add/remove primitives.
5079 // ═══════════════════════════════════════════════════════════════════
5080
5081 /// c:86 — `hasher` of empty string returns 0 (no bytes contribute).
5082 #[test]
5083 fn hasher_empty_string_returns_zero() {
5084 assert_eq!(hasher(""), 0, "no bytes → hash 0");
5085 }
5086
5087 /// c:86 — `hasher` is deterministic: same input → same output.
5088 #[test]
5089 fn hasher_is_deterministic() {
5090 let h1 = hasher("test_string");
5091 let h2 = hasher("test_string");
5092 assert_eq!(h1, h2, "hasher must be deterministic");
5093 }
5094
5095 /// c:86 — `hasher` differentiates between different strings
5096 /// (no trivial collisions on common inputs).
5097 #[test]
5098 fn hasher_distinguishes_common_strings() {
5099 assert_ne!(hasher("foo"), hasher("bar"));
5100 assert_ne!(hasher("a"), hasher("b"));
5101 assert_ne!(hasher("test"), hasher("Test"), "case-sensitive");
5102 }
5103
5104 /// c:86 — hash of single char "a" matches the formula
5105 /// `0 + (0<<5) + 'a' = 0x61` (verifies inline formula).
5106 #[test]
5107 fn hasher_single_char_matches_formula() {
5108 let h = hasher("a");
5109 assert_eq!(h, b'a' as u32, "single char 'a' → 0x61");
5110 let h = hasher("0");
5111 assert_eq!(h, b'0' as u32, "single char '0' → 0x30");
5112 }
5113
5114 /// c:86 — hasher of "ab": h=0 → h=0+(0<<5)+'a'=0x61
5115 /// → h=0x61+(0x61<<5)+'b' = 0x61 + 0xC20 + 0x62 = 0xCE3.
5116 #[test]
5117 fn hasher_two_char_matches_formula() {
5118 let h = hasher("ab");
5119 let expected: u32 = 0u32
5120 .wrapping_add(0u32.wrapping_shl(5))
5121 .wrapping_add(b'a' as u32);
5122 let expected = expected
5123 .wrapping_add(expected.wrapping_shl(5))
5124 .wrapping_add(b'b' as u32);
5125 assert_eq!(h, expected, "two-char formula must match");
5126 }
5127
5128 /// c:86 — uses wrapping arithmetic so long strings don't panic.
5129 #[test]
5130 fn hasher_long_string_does_not_panic() {
5131 let s = "a".repeat(10_000);
5132 let _ = hasher(&s);
5133 }
5134
5135 /// c:345 — `hnamcmp("abc", "abc")` returns Equal.
5136 #[test]
5137 fn hnamcmp_equal_strings_return_equal() {
5138 assert_eq!(hnamcmp("abc", "abc"), std::cmp::Ordering::Equal);
5139 assert_eq!(hnamcmp("", ""), std::cmp::Ordering::Equal);
5140 }
5141
5142 /// c:345 — `hnamcmp` orders lexicographically.
5143 #[test]
5144 fn hnamcmp_lex_order() {
5145 assert_eq!(hnamcmp("abc", "abd"), std::cmp::Ordering::Less);
5146 assert_eq!(hnamcmp("abd", "abc"), std::cmp::Ordering::Greater);
5147 }
5148
5149 /// c:345 — empty string sorts before any non-empty string.
5150 #[test]
5151 fn hnamcmp_empty_sorts_first() {
5152 assert_eq!(hnamcmp("", "x"), std::cmp::Ordering::Less);
5153 assert_eq!(hnamcmp("x", ""), std::cmp::Ordering::Greater);
5154 }
5155
5156 /// `emptyhashtable` drops all entries.
5157 #[test]
5158 fn emptyhashtable_clears_all_entries() {
5159 let mut h: HashMap<String, i32> = HashMap::new();
5160 h.insert("a".to_string(), 1);
5161 h.insert("b".to_string(), 2);
5162 h.insert("c".to_string(), 3);
5163 emptyhashtable(&mut h);
5164 assert!(h.is_empty(), "all entries dropped after emptyhashtable");
5165 }
5166
5167 /// `deletehashtable` clears the map (Rust semantics).
5168 #[test]
5169 fn deletehashtable_clears_all_entries() {
5170 let mut h: HashMap<String, i32> = HashMap::new();
5171 h.insert("x".to_string(), 42);
5172 deletehashtable(&mut h);
5173 assert!(h.is_empty());
5174 }
5175
5176 /// `removehashnode` on missing key returns None (no panic).
5177 #[test]
5178 fn removehashnode_missing_returns_none() {
5179 let mut h: HashMap<String, i32> = HashMap::new();
5180 let prev = removehashnode(&mut h, "never_there");
5181 assert!(prev.is_none(), "remove of missing key → None");
5182 }
5183
5184 /// `addhashnode` overwriting existing key drops old value silently.
5185 #[test]
5186 fn addhashnode_overwrite_does_not_panic() {
5187 let mut h: HashMap<String, String> = HashMap::new();
5188 addhashnode(&mut h, "k", "first".into());
5189 addhashnode(&mut h, "k", "second".into());
5190 assert_eq!(gethashnode2(&h, "k"), Some(&"second".to_string()));
5191 }
5192
5193 // ═══════════════════════════════════════════════════════════════════
5194 // Additional C-parity tests for Src/hashtable.c
5195 // c:55 hasher / c:85 newhashtable / c:97 deletehashtable /
5196 // c:150 addhashnode2 / c:343 gethashnode2 / c:355 removehashnode /
5197 // c:715 hnamcmp / c:876 expandhashtable / c:887 resizehashtable /
5198 // c:916 printhashtabinfo
5199 // ═══════════════════════════════════════════════════════════════════
5200
5201 /// c:55 — `hasher("")` empty string returns u32 (type pin).
5202 #[test]
5203 fn hasher_empty_returns_u32_type() {
5204 let _: u32 = hasher("");
5205 }
5206
5207 /// c:55 — `hasher` is pure.
5208 #[test]
5209 fn hasher_is_pure_full_sweep() {
5210 for s in ["", "a", "abc", "hello world", "日本"] {
5211 let first = hasher(s);
5212 for _ in 0..5 {
5213 assert_eq!(hasher(s), first, "hasher({:?}) must be pure", s);
5214 }
5215 }
5216 }
5217
5218 /// c:100 — `newhashtable(0, "")` returns a `HashTable` with no buckets.
5219 #[test]
5220 fn newhashtable_returns_hashtable_type() {
5221 let ht: HashTable = newhashtable(0, "");
5222 assert_eq!(ht.hsize, 0);
5223 }
5224
5225 /// c:97 — `deletehashtable` on empty table is safe no-op.
5226 #[test]
5227 fn deletehashtable_empty_no_panic() {
5228 let mut empty: HashMap<String, String> = HashMap::new();
5229 deletehashtable(&mut empty);
5230 assert!(empty.is_empty(), "still empty after delete");
5231 }
5232
5233 /// c:150 — `addhashnode2` returns Option<T> (replaced value).
5234 #[test]
5235 fn addhashnode2_returns_option_type() {
5236 let mut h: HashMap<String, i32> = HashMap::new();
5237 let _: Option<i32> = addhashnode2(&mut h, "k", 1);
5238 }
5239
5240 /// c:150 — `addhashnode2` first insert returns None.
5241 #[test]
5242 fn addhashnode2_first_insert_returns_none() {
5243 let mut h: HashMap<String, i32> = HashMap::new();
5244 let r = addhashnode2(&mut h, "k", 42);
5245 assert!(r.is_none(), "first insert → None (no replacement)");
5246 }
5247
5248 /// c:150 — `addhashnode2` overwrite returns Some(old).
5249 #[test]
5250 fn addhashnode2_overwrite_returns_some_old() {
5251 let mut h: HashMap<String, i32> = HashMap::new();
5252 addhashnode2(&mut h, "k", 1);
5253 let r = addhashnode2(&mut h, "k", 2);
5254 assert_eq!(r, Some(1), "overwrite returns previous value");
5255 }
5256
5257 /// c:343 — `gethashnode2(empty, _)` returns None.
5258 #[test]
5259 fn gethashnode2_empty_table_returns_none() {
5260 let h: HashMap<String, String> = HashMap::new();
5261 assert!(gethashnode2(&h, "anything").is_none());
5262 }
5263
5264 /// c:355 — `removehashnode(empty, _)` returns None.
5265 #[test]
5266 fn removehashnode_empty_table_returns_none() {
5267 let mut h: HashMap<String, String> = HashMap::new();
5268 assert!(removehashnode(&mut h, "anything").is_none());
5269 }
5270
5271 /// c:715 — `hnamcmp` is antisymmetric.
5272 #[test]
5273 fn hnamcmp_antisymmetric() {
5274 use std::cmp::Ordering;
5275 for (a, b) in [("a", "b"), ("abc", "xyz"), ("", "x")] {
5276 let ab = hnamcmp(a, b);
5277 let ba = hnamcmp(b, a);
5278 assert_eq!(
5279 ab.reverse(),
5280 ba,
5281 "hnamcmp must be antisymmetric for ({:?}, {:?})",
5282 a,
5283 b
5284 );
5285 // ab cannot be Equal AND ba Equal unless both Equal
5286 if ab == Ordering::Equal {
5287 assert_eq!(ba, Ordering::Equal);
5288 }
5289 }
5290 }
5291
5292 /// c:887 — `resizehashtable` with same size is no-op.
5293 #[test]
5294 fn resizehashtable_same_size_no_panic() {
5295 let mut h: HashMap<String, i32> = HashMap::new();
5296 h.insert("a".to_string(), 1);
5297 h.insert("b".to_string(), 2);
5298 resizehashtable(&mut h, 2);
5299 assert_eq!(h.len(), 2, "entries preserved after same-size resize");
5300 }
5301
5302 /// c:876 — `expandhashtable` is idempotent.
5303 #[test]
5304 fn expandhashtable_idempotent() {
5305 let mut h: HashMap<String, i32> = HashMap::new();
5306 h.insert("a".to_string(), 1);
5307 for _ in 0..5 {
5308 expandhashtable(&mut h);
5309 }
5310 assert_eq!(h.get("a"), Some(&1), "value preserved across expansions");
5311 }
5312
5313 /// c:916 — `printhashtabinfo("", empty)` returns String type.
5314 #[test]
5315 fn printhashtabinfo_returns_string_type() {
5316 let empty: HashMap<String, String> = HashMap::new();
5317 let _: String = printhashtabinfo("test", &empty);
5318 }
5319
5320 // ═══════════════════════════════════════════════════════════════════
5321 // Additional C-parity tests for Src/hashtable.c
5322 // c:55 hasher / c:139 addhashnode / c:343 gethashnode2 / c:355 removehashnode /
5323 // c:715 hnamcmp / c:900 emptyhashtable / c:916 printhashtabinfo
5324 // ═══════════════════════════════════════════════════════════════════
5325
5326 /// c:55 — `hasher` returns u32 (compile-time pin).
5327 #[test]
5328 fn hasher_returns_u32_type() {
5329 let _: u32 = hasher("anything");
5330 }
5331
5332 /// c:55 — `hasher` is deterministic (same input → same hash, alt).
5333 #[test]
5334 fn hasher_is_deterministic_alt() {
5335 for s in ["", "x", "abc", "longer input", "日本"] {
5336 let first = hasher(s);
5337 for _ in 0..5 {
5338 assert_eq!(hasher(s), first, "hasher({:?}) must be pure", s);
5339 }
5340 }
5341 }
5342
5343 /// c:55 — `hasher` distinguishes simple distinct inputs (sanity:
5344 /// not a constant hash).
5345 #[test]
5346 fn hasher_distinguishes_distinct_inputs() {
5347 let h_a = hasher("a");
5348 let h_b = hasher("b");
5349 let h_z = hasher("z");
5350 // At least two of three must differ (proves non-constant).
5351 let distinct = (h_a != h_b) || (h_b != h_z) || (h_a != h_z);
5352 assert!(
5353 distinct,
5354 "hasher must distinguish distinct inputs; got {} {} {}",
5355 h_a, h_b, h_z
5356 );
5357 }
5358
5359 /// c:139 — `addhashnode` followed by gethashnode2 retrieves entry.
5360 #[test]
5361 fn addhashnode_then_gethashnode2_retrieves_entry() {
5362 let mut h: HashMap<String, String> = HashMap::new();
5363 addhashnode(&mut h, "key", "value".to_string());
5364 let v = gethashnode2(&h, "key");
5365 assert_eq!(
5366 v,
5367 Some(&"value".to_string()),
5368 "add then get must round-trip"
5369 );
5370 }
5371
5372 /// c:355 — `removehashnode` after add returns Some(value).
5373 #[test]
5374 fn removehashnode_after_add_returns_some() {
5375 let mut h: HashMap<String, String> = HashMap::new();
5376 addhashnode(&mut h, "k", "v".to_string());
5377 let removed = removehashnode(&mut h, "k");
5378 assert_eq!(
5379 removed,
5380 Some("v".to_string()),
5381 "remove returns the removed value"
5382 );
5383 assert!(h.is_empty(), "table empty after remove");
5384 }
5385
5386 /// c:355 — `removehashnode` twice returns Some then None.
5387 #[test]
5388 fn removehashnode_twice_returns_some_then_none() {
5389 let mut h: HashMap<String, i32> = HashMap::new();
5390 addhashnode(&mut h, "k", 42);
5391 let first = removehashnode(&mut h, "k");
5392 let second = removehashnode(&mut h, "k");
5393 assert!(first.is_some());
5394 assert!(second.is_none(), "second remove of same key returns None");
5395 }
5396
5397 /// c:715 — `hnamcmp(x, x)` returns Equal (reflexive).
5398 #[test]
5399 fn hnamcmp_reflexive() {
5400 use std::cmp::Ordering;
5401 for s in ["", "a", "hello", "long string here"] {
5402 assert_eq!(
5403 hnamcmp(s, s),
5404 Ordering::Equal,
5405 "hnamcmp({:?}, {:?}) must be Equal",
5406 s,
5407 s
5408 );
5409 }
5410 }
5411
5412 /// c:900 — `emptyhashtable` actually drops all entries.
5413 #[test]
5414 fn emptyhashtable_drops_all_entries() {
5415 let mut h: HashMap<String, i32> = HashMap::new();
5416 for i in 0..10 {
5417 addhashnode(&mut h, &format!("k_{}", i), i);
5418 }
5419 assert_eq!(h.len(), 10);
5420 emptyhashtable(&mut h);
5421 assert_eq!(h.len(), 0, "empty must clear all entries");
5422 }
5423
5424 /// c:916 — `printhashtabinfo` for empty table returns non-empty
5425 /// String (must contain at least the table name).
5426 #[test]
5427 fn printhashtabinfo_empty_table_non_empty_output() {
5428 let empty: HashMap<String, String> = HashMap::new();
5429 let r = printhashtabinfo("my_table_name", &empty);
5430 assert!(
5431 !r.is_empty(),
5432 "printhashtabinfo must produce non-empty output even for empty table"
5433 );
5434 }
5435
5436 /// c:97 — `deletehashtable` empties + safe.
5437 #[test]
5438 fn deletehashtable_empties_table() {
5439 let mut h: HashMap<String, i32> = HashMap::new();
5440 addhashnode(&mut h, "k", 1);
5441 deletehashtable(&mut h);
5442 assert!(h.is_empty(), "delete must empty the table");
5443 }
5444
5445 /// c:100 — the generic table methods round-trip nodes through the
5446 /// function-pointer slots, replace in place and grow at `ct >= 2*hsize`.
5447 #[test]
5448 fn hashtable_methods_add_get_remove_expand() {
5449 let mut ht = newhashtable(2, "t");
5450 ht.hash = Some(hasher);
5451 ht.cmpnodes = Some(|a, b| a.cmp(b) as i32);
5452 ht.addnode = Some(hashtable::addhashnode);
5453 ht.freenode = Some(|hn| drop(hn));
5454 let mk = |flags: i32| {
5455 Box::into_raw(Box::new(hashnode {
5456 next: None,
5457 nam: String::new(),
5458 flags,
5459 })) as usize
5460 };
5461 for i in 0..10 {
5462 hashtable::addhashnode(&mut ht, format!("k{i}"), mk(i));
5463 }
5464 assert_eq!(ht.ct, 10);
5465 assert!(ht.hsize > 2, "expandhashtable quadrupled hsize");
5466 hashtable::addhashnode(&mut ht, "k3".to_string(), mk(99));
5467 assert_eq!(ht.ct, 10, "replacement does not change ct");
5468 assert_eq!(hashtable::gethashnode2(&ht, "k3").unwrap().flags, 99);
5469 assert!(hashtable::removehashnode(&mut ht, "k3").is_some());
5470 assert!(hashtable::gethashnode2(&ht, "k3").is_none());
5471 assert_eq!(ht.ct, 9);
5472 hashtable::emptyhashtable(&mut ht);
5473 assert_eq!(ht.ct, 0);
5474 }
5475
5476 /// c:954 — printshfuncnode renders a function body with
5477 /// `getpermtext(fd, NULL, 1)`, so `functions f` prints CANONICAL text
5478 /// rather than the source as typed. zshrs keeps raw source for
5479 /// shell-defined functions, so its listing path re-parses and renders
5480 /// through that same deparser; these are the shapes where the layout is
5481 /// an actual decision rather than a passthrough.
5482 ///
5483 /// The `always` case is the one that matters most: the previous
5484 /// hand-rolled canonicalization emitted `print x } always { print y`,
5485 /// which is not merely mis-indented, it no longer parses — and
5486 /// `functions` output is meant to be re-readable by the shell.
5487 ///
5488 /// Pins the deparse itself rather than printshfuncnode's stdout, so it
5489 /// doesn't depend on capturing print! output. Indent 1 matches C, which
5490 /// writes one tab via zoutputtab (c:949) before calling getpermtext.
5491 #[test]
5492 fn function_body_deparses_to_canonical_layout() {
5493 let _g = crate::test_util::global_state_lock();
5494 for (body, want) in [
5495 // `do` gets its own line; the body indents beneath it.
5496 (
5497 "for i in 1 2; do print $i; done",
5498 "for i in 1 2\n\tdo\n\t\tprint $i\n\tdone",
5499 ),
5500 // `(` and `)` break onto their own lines.
5501 ("(print s)", "(\n\t\tprint s\n\t)"),
5502 // taddassign appends a trailing space after the value
5503 // (c:Src/text.c:203-204) and nothing backs it off.
5504 ("g=inner", "g=inner "),
5505 // The shape the emulation broke.
5506 (
5507 "{ print x } always { print y }",
5508 "{\n\t\tprint x\n\t} always {\n\t\tprint y\n\t}",
5509 ),
5510 ] {
5511 let prog = crate::ported::exec::parse_string(body, 1)
5512 .unwrap_or_else(|| panic!("body must parse for the listing path: {body:?}"));
5513 let got = crate::ported::text::getpermtext(Box::new(prog), None, 1);
5514 assert_eq!(got, want, "c:954 deparse of {body:?}");
5515 }
5516 }
5517}