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