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