Skip to main content

zsh/ported/
module.rs

1//! Module system for zshrs
2//!
3//! Port from zsh/Src/module.c (3,646 lines)
4//!
5//! Hash of modules                                                          // c:46
6//! The list of hook functions defined.                                      // c:840
7//! List of math functions.                                                  // c:1255
8//!
9//! In C, module.c provides dynamic loading of .so modules at runtime
10//! via dlopen/dlsym. In Rust, all modules are statically compiled into
11//! the binary — there's no dynamic loading. This module provides the
12//! registration, lookup, and management API that the rest of the shell
13//! uses to interact with module features (builtins, conditions, parameters,
14//! hooks, and math functions).
15
16use crate::ported::builtin::createbuiltintable;
17use crate::ported::hist::casemodify;
18use crate::ported::mem::ztrdup;
19use crate::ported::params::{createparam, createspecialhash, paramtab, unsetparam_pm};
20use crate::ported::signals::unqueue_signals;
21use crate::ported::utils::{zwarn, zwarnnam};
22use crate::ported::zsh_h::hashnode;
23use crate::ported::zsh_h::{
24    builtin, conddef, funcwrap, hookdef, linkedmod, linklist, linknode, mathfunc, options,
25    paramdef, Hookfn, Param, BINF_AUTOALL, CASMOD_LOWER, CASMOD_UPPER, CONDF_AUTOALL, HOOKF_ALL,
26    MFF_USERFUNC, MOD_ALIAS, MOD_BUSY, MOD_INIT_B, MOD_INIT_S, MOD_LINKED, MOD_SETUP, MOD_UNLOAD,
27    OPT_ARG_SAFE, OPT_ISSET, PM_ARRAY, PM_AUTOALL, PM_AUTOLOAD, PM_EFLOAT, PM_FFLOAT, PM_HASHED,
28    PM_INTEGER, PM_NAMEREF, PM_READONLY, PM_REMOVABLE, PM_SCALAR, PM_SPECIAL, PM_TIED, PM_TYPE, PRINT_LIST,
29};
30pub use crate::ported::zsh_h::{BINF_ADDED, CONDF_ADDED, CONDF_INFIX, MFF_ADDED};
31use crate::zsh_h::module;
32use once_cell::sync::Lazy;
33use std::collections::HashMap;
34use std::sync::Mutex;
35
36/// Free module node (from module.c freemodulenode)
37/// Free a module table entry.
38/// Port of `freemodulenode()` from `Src/module.c:119`. — C decl `freemodulenode(HashNode hn)`.
39/// Rust's
40/// `Drop` handles the per-field free; this exists for API
41/// parity with C callers.
42pub fn freemodulenode(hn: module) {
43    // Rust Drop handles this
44}
45
46/// `PRINTMOD_LIST` from `Src/module.c:136`. Long-form (`zmodload -L`)
47/// output.
48pub const PRINTMOD_LIST: i32 = 0x0001; // c:136
49/// `PRINTMOD_EXIST` from `Src/module.c:138`. Print only when the
50/// module exists.
51pub const PRINTMOD_EXIST: i32 = 0x0002; // c:138
52/// `PRINTMOD_ALIAS` from `Src/module.c:140`. Resolve aliases when
53/// emitting under `PRINTMOD_EXIST`.
54pub const PRINTMOD_ALIAS: i32 = 0x0004; // c:140
55/// `PRINTMOD_DEPS` from `Src/module.c:142`. Emit the dependency list
56/// (`zmodload -d`).
57pub const PRINTMOD_DEPS: i32 = 0x0008; // c:142
58/// `PRINTMOD_FEATURES` from `Src/module.c:144`. Emit feature flags
59/// (`zmodload -F`).
60pub const PRINTMOD_FEATURES: i32 = 0x0010; // c:144
61/// `PRINTMOD_LISTALL` from `Src/module.c:146`. Include disabled
62/// features (`zmodload -lL`).
63pub const PRINTMOD_LISTALL: i32 = 0x0020; // c:146
64/// `PRINTMOD_AUTO` from `Src/module.c:148`. Emit autoloads
65/// (`zmodload -a`).
66pub const PRINTMOD_AUTO: i32 = 0x0040; // c:148
67
68/// Port of `printmodulenode()` from `Src/module.c:154`. — C decl `printmodulenode(HashNode hn, int flags)`.
69///
70/// Formats one module entry for the various `zmodload -L`/`-a`/
71/// `-d`/`-F` listings. C writes to stdout; Rust returns the
72/// formatted string so call sites can route to the right output fd
73/// without depending on stdio. Dispatches on `flags`:
74///
75///   - `PRINTMOD_DEPS`: emit dep list (`zmodload -d MOD: dep1 dep2`
76///     under PRINTMOD_LIST, else `MOD: dep1 dep2`) — c:163-194
77///   - `PRINTMOD_EXIST`: emit just the module name when loaded
78///     (resolving alias when PRINTMOD_ALIAS set) — c:195-201
79///   - alias module: under PRINTMOD_LIST emit
80///     `zmodload -A MOD=ALIAS`, else `MOD -> ALIAS` — c:202-217
81///   - loaded module: under PRINTMOD_LIST emit `zmodload [-Fa] MOD`,
82///     else just the name — c:218-241
83pub fn printmodulenode(hn: &str, m: &module, flags: i32) -> String {
84    let modname = hn;
85    let mut out = String::new();
86
87    // c:163-194 — PRINTMOD_DEPS branch.
88    // C body:
89    //   if (!m->deps) return;
90    //   if (flags & PRINTMOD_LIST) {
91    //       printf("zmodload -d ");
92    //       if (modname[0] == '-') fputs("-- ", stdout);
93    //       quotedzputs(modname, stdout);
94    //   } else {
95    //       nicezputs(modname, stdout);
96    //       putchar(':');
97    //   }
98    //   for (n = firstnode(m->deps); n; incnode(n)) {
99    //       putchar(' ');
100    //       if (flags & PRINTMOD_LIST)
101    //           quotedzputs((char *) getdata(n), stdout);
102    //       else
103    //           nicezputs((char *) getdata(n), stdout);
104    //   }
105    //
106    // C uses quotedzputs (round-trippable shell input) under
107    // PRINTMOD_LIST so the emitted `zmodload -d MOD DEP1 DEP2` is
108    // re-parseable. The default path uses nicezputs (printable form
109    // with \M-/^ escapes for control bytes) which is listing-friendly.
110    // The two helpers are both already ported in src/ported/utils.rs.
111    if flags & PRINTMOD_DEPS != 0 {
112        use crate::ported::utils::{nicezputs, quotedzputs};
113        let deps = match m.deps.as_ref() {
114            Some(d) if !d.is_empty() => d,
115            _ => return out, // c:170-171
116        };
117        if flags & PRINTMOD_LIST != 0 {
118            out.push_str("zmodload -d "); // c:174
119            if modname.starts_with('-') {
120                out.push_str("-- "); // c:176
121            }
122            out.push_str(&quotedzputs(modname)); // c:177
123        } else {
124            let mut buf: Vec<u8> = Vec::new();
125            let _ = nicezputs(modname, &mut buf); // c:179
126            if let Ok(s) = std::str::from_utf8(&buf) {
127                out.push_str(s);
128            }
129            out.push(':'); // c:180
130        }
131        for dep in deps.iter() {
132            out.push(' '); // c:183
133            if flags & PRINTMOD_LIST != 0 {
134                out.push_str(&quotedzputs(dep)); // c:185
135            } else {
136                let mut buf: Vec<u8> = Vec::new();
137                let _ = nicezputs(dep, &mut buf); // c:187
138                if let Ok(s) = std::str::from_utf8(&buf) {
139                    out.push_str(s);
140                }
141            }
142        }
143        return out;
144    }
145
146    // c:189-201 — PRINTMOD_EXIST branch.
147    // C body:
148    //   if (m->node.flags & MOD_ALIAS) {
149    //       if (!(flags & PRINTMOD_ALIAS) ||
150    //           !(m = find_module(m->u.alias, FINDMOD_ALIASP, NULL)))
151    //           return;
152    //   }
153    //   if (!m->u.handle || (m->node.flags & MOD_UNLOAD))
154    //       return;
155    //   nicezputs(modname, stdout);
156    //
157    // The MOD_ALIAS arm at c:194-198 reassigns m to the alias target
158    // before the u.handle check at c:199 — without a table handle we
159    // can't chase the alias here; the caller (bin_zmodload_exist /
160    // bin_zmodload_alias) is responsible for the chase when feeding
161    // us the canonical module. For the local check we read the alias
162    // entry's flags, which is the right behaviour when the caller
163    // already resolved.
164    if flags & PRINTMOD_EXIST != 0 {
165        if (m.node.flags & MOD_ALIAS) != 0 {
166            if (flags & PRINTMOD_ALIAS) == 0 || m.alias.is_none() {
167                // c:195-197 — alias entry + caller didn't pass
168                // PRINTMOD_ALIAS, OR the alias is dangling.
169                return out;
170            }
171            // Alias resolves: emit the alias entry's name; the
172            // caller chased and decided it counts as "exists".
173        }
174        // c:199 — `!m->u.handle || MOD_UNLOAD` skip.
175        // Static-link analog of `!u.handle` is `!MOD_INIT_B`
176        // (boot hasn't run = no loaded handle). The prior port
177        // missed this gate, so PRINTMOD_EXIST listed every
178        // pre-registered module even when zmodload zsh/files had
179        // never fired — diverging from `zsh -fc 'zmodload -e'`
180        // which emits only the actually-loaded `zsh/main`.
181        let booted = (m.node.flags & MOD_INIT_B) != 0;
182        let unloading = (m.node.flags & MOD_UNLOAD) != 0;
183        if !booted || unloading {
184            return out;
185        }
186        out.push_str(modname); // c:201 nicezputs(modname)
187        return out;
188    }
189
190    // c:202-217 — alias module branch.
191    // c:202-217 — alias module branch.
192    // C body:
193    //   if (flags & PRINTMOD_LIST) {
194    //       printf("zmodload -A ");
195    //       if (modname[0] == '-') fputs("-- ", stdout);
196    //       quotedzputs(modname, stdout);
197    //       putchar('=');
198    //       quotedzputs(m->u.alias, stdout);
199    //   } else {
200    //       nicezputs(modname, stdout);
201    //       fputs(" -> ", stdout);
202    //       nicezputs(m->u.alias, stdout);
203    //   }
204    if m.node.flags & MOD_ALIAS != 0 {
205        use crate::ported::utils::{nicezputs, quotedzputs};
206        let alias = m.alias.as_deref().unwrap_or("");
207        if flags & PRINTMOD_LIST != 0 {
208            out.push_str("zmodload -A "); // c:207
209            if modname.starts_with('-') {
210                out.push_str("-- "); // c:209
211            }
212            out.push_str(&quotedzputs(modname)); // c:210
213            out.push('='); // c:211
214            out.push_str(&quotedzputs(alias)); // c:212
215        } else {
216            let mut buf: Vec<u8> = Vec::new();
217            let _ = nicezputs(modname, &mut buf); // c:214
218            if let Ok(s) = std::str::from_utf8(&buf) {
219                out.push_str(s);
220            }
221            out.push_str(" -> "); // c:215
222            let mut buf2: Vec<u8> = Vec::new();
223            let _ = nicezputs(alias, &mut buf2); // c:216
224            if let Ok(s) = std::str::from_utf8(&buf2) {
225                out.push_str(s);
226            }
227        }
228        return out;
229    }
230
231    // c:218-241 — loaded module branch (linked or autoloaded).
232    // C check: `m->u.handle || (flags & PRINTMOD_AUTO)` where `u`
233    // is a union so `u.handle` is non-NULL whenever EITHER `handle`
234    // (dlopen result) or `linked` (statically-linked record) is
235    // installed. The union slots are only populated AFTER
236    // `load_module` completes (c:2227/2230 set them, c:2244 sets
237    // `MOD_INIT_B`). So "boot ran" maps to `MOD_INIT_B` in zshrs.
238    // Previous gate `MOD_LINKED && !MOD_UNLOAD` was wrong:
239    // `register_builtin_modules` seeds `MOD_LINKED` for every
240    // statically-compiled module up front, so plain `zmodload`
241    // listed all 32 (#76 in docs/BUGS.md). C zsh shows only the
242    // single `zsh/main` entry that `init_bltinmods` actually loads
243    // via `load_module("zsh/main", NULL, 0)`.
244    let loaded = (m.node.flags & MOD_INIT_B) != 0 && (m.node.flags & MOD_UNLOAD) == 0;
245    let _ = MOD_LINKED; // c:Src/module.c:218 — union-based check; flag retained for unload path.
246    let auto = flags & PRINTMOD_AUTO != 0;
247    if loaded || auto {
248        use crate::ported::utils::{nicezputs, quotedzputs};
249        if flags & PRINTMOD_LIST != 0 {
250            // c:229-237 — `PRINTMOD_AUTO`: skip when no autoloads set.
251            // C `firstnode(m->autoloads)` returns the first node — if
252            // the linklist is empty the early-return fires.
253            if auto {
254                let has_autoloads = m
255                    .autoloads
256                    .as_ref()
257                    .map(|al| !al.is_empty())
258                    .unwrap_or(false);
259                if !has_autoloads {
260                    return out; // c:231 return early
261                }
262            }
263            out.push_str("zmodload "); // c:238
264            if auto {
265                out.push_str("-Fa "); // c:240
266            } else if flags & PRINTMOD_FEATURES != 0 {
267                out.push_str("-F "); // c:242
268            }
269            if modname.starts_with('-') {
270                out.push_str("-- "); // c:244
271            }
272            out.push_str(&quotedzputs(modname)); // c:245
273                                                 // c:246-251 — PRINTMOD_AUTO: emit each autoload as
274                                                 //             ` quotedzputs(al)`.
275            if auto {
276                if let Some(al_list) = m.autoloads.as_ref() {
277                    for al in al_list.iter() {
278                        out.push(' '); // c:249
279                        out.push_str(&quotedzputs(al)); // c:250
280                    }
281                }
282            }
283            // c:252-263 — PRINTMOD_FEATURES list path needs features_module
284            // + enables_module which require the modulestab; not
285            // dispatched here because printmodulenode has no &table
286            // handle. Caller (bin_zmodload_features -l/-L path) does
287            // the dispatch directly when PRINTMOD_FEATURES is set.
288        } else {
289            // c:266 — `else /* -l */ nicezputs(modname, stdout);`
290            let mut buf: Vec<u8> = Vec::new();
291            let _ = nicezputs(modname, &mut buf);
292            if let Ok(s) = std::str::from_utf8(&buf) {
293                out.push_str(s);
294            }
295        }
296    }
297    out // c:268 putchar('\n') handled by caller's println!
298}
299
300/// Create new module table (from module.c newmoduletable)
301/// Create an empty module table.
302/// Port of `newmoduletable()` from `Src/module.c:274`. — C decl `newmoduletable(int size, char const *name)`.
303/// the C
304/// source allocates the `modulestab` hash with `createhashtable`.
305/// WARNING: param names don't match C — Rust=() vs C=(size, name)
306pub fn newmoduletable() -> modulestab {
307    modulestab::new()
308}
309
310/// Port of `setup_()` from `Src/module.c:306`. — C decl `setup_(UNUSED(Module m))`.
311///
312/// C body: `setup_(UNUSED(Module m)) { return 0; }` — the no-op
313/// setup hook of the module subsystem itself.
314#[allow(unused_variables)]
315pub fn setup_(m: *const module) -> i32 {
316    // c:306
317    0 // c:306
318}
319
320/// Port of `features_()` from `Src/module.c:313`. — C decl `features_(UNUSED(Module m), UNUSED(char ***features))`.
321///
322/// C body:
323/// ```c
324/// features_(UNUSED(Module m), UNUSED(char ***features))
325/// {
326///     /* There are lots and lots of features, but they're not handled here. */
327///     return 1;
328/// }
329/// ```
330#[allow(unused_variables)]
331pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
332    // c:313
333    /* There are lots and lots of features, but they're not handled here. */ // c:313-318
334    1 // c:319
335}
336
337/// Port of `enables_()` from `Src/module.c:324`. — C decl `enables_(UNUSED(Module m), UNUSED(int **enables))`.
338///
339/// C body: `enables_(UNUSED(Module m), UNUSED(int **enables)) { return 1; }`
340/// — the module subsystem itself doesn't manage feature enables.
341#[allow(unused_variables)]
342pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
343    // c:324
344    1 // c:324
345}
346
347/// Port of `boot_()` from `Src/module.c:331`. — C decl `boot_(UNUSED(Module m))`.
348///
349/// C body: `boot_(UNUSED(Module m)) { return 0; }` — the no-op
350/// boot hook of the module subsystem itself.
351#[allow(unused_variables)]
352pub fn boot_(m: *const module) -> i32 {
353    // c:331
354    0 // c:331
355}
356
357/// Port of `cleanup_()` from `Src/module.c:338`. — C decl `cleanup_(UNUSED(Module m))`.
358///
359/// C body: `cleanup_(UNUSED(Module m)) { return 0; }` — the no-op
360/// cleanup hook of the module subsystem itself.
361#[allow(unused_variables)]
362pub fn cleanup_(m: *const module) -> i32 {
363    // c:338
364    0 // c:338
365}
366
367/// Port of `finish_()` from `Src/module.c:345`. — C decl `finish_(UNUSED(Module m))`.
368///
369/// C body: `finish_(UNUSED(Module m)) { return 0; }` —
370/// the no-op finish hook for the module subsystem itself.
371#[allow(unused_variables)]
372pub fn finish_(m: *const module) -> i32 {
373    // c:345
374    0 // c:345
375}
376
377// This registers a builtin module.                                        // c:359
378/// Register module (from module.c register_module)
379/// Register a module by name.
380/// Port of `register_module()` from `Src/module.c:359`. — C decl `register_module(const char *n, Module_void_func setup, Module_features_func features, Module_enables_func enables, Module_void_func boot, Module_void_func cleanup, Module_void_func finish)`.
381/// wraps
382/// a slot in the global `modulestab` and seeds its lifecycle
383/// callbacks.
384/// WARNING: param names don't match C — Rust=(table, name) vs C=(n, setup, features, enables, boot, cleanup, finish)
385pub fn register_module(table: &mut modulestab, name: &str) -> bool {
386    // c:359
387    if table.modules.contains_key(name) {
388        return false;
389    }
390    // c:378 — `zaddlinknode(linkedmodules, m)`: the static entry-point
391    // record goes on `linkedmodules`, which is what `module_linked`
392    // (c:385) searches from `load_module` (c:2224, c:2285).
393    if !table.linkedmodules.iter().any(|n| n == name) {
394        table.linkedmodules.push(name.to_string());
395    }
396    // WARNING: RUST-ONLY — C's `register_module` does NOT add a
397    // `modulestab` node (only `load_module`/`autofeatures`/`add_dep`
398    // do). The node insert is kept here because zshrs callers treat
399    // this free fn as "make the module exist and be loadable"; the
400    // boot path (`register_builtin_modules`) deliberately does NOT use
401    // it, and populates `linkedmodules` directly so that the
402    // `modulestab` node set matches C's boot set exactly.
403    table.modules.insert(name.to_string(), module::new(name));
404    true
405}
406
407/// Port of `addbuiltin()` from `Src/module.c:409`. — C decl `addbuiltin(Builtin b)`.
408/// C body:
409/// look up `b->node.nam` in builtintab; if BINF_ADDED clash → return 1;
410/// otherwise replace any pre-existing entry and add `b`. Returns 0 on
411/// add, 1 on clash.
412///
413/// The Rust canonical builtintab is `OnceLock<HashMap<String,
414/// &'static builtin>>` — immutable after first access. Runtime `addbuiltin`
415/// calls check the immutable table for the clash gate; the BINF_ADDED
416/// flag-set on the input record is what callers observe (matching the
417/// C in-place mutation that `addbuiltins` then propagates).
418pub fn addbuiltin(b: &mut builtin) -> i32 {
419    // c:524
420    let tab = createbuiltintable();
421    if let Some(existing) = tab.get(&b.node.nam) {
422        // c:526 getnode2
423        if (existing.node.flags & BINF_ADDED as i32) != 0 {
424            return 1;
425        } // c:527 clash
426    }
427    b.node.flags |= BINF_ADDED as i32; // c:531 b->node.flags |= BINF_ADDED
428    0
429}
430
431/// Port of `deletebuiltin()` from `Src/module.c:449`. — C decl `deletebuiltin(const char *nam)`.
432///
433/// C body c:449-458:
434/// ```c
435/// int
436/// deletebuiltin(const char *nam)
437/// {
438///     Builtin bn;
439///     bn = (Builtin) builtintab->removenode(builtintab, nam);
440///     if (!bn)
441///         return -1;
442///     builtintab->freenode(&bn->node);
443///     return 0;
444/// }
445/// ```
446///
447/// Returns 0 on success (entry was found + removed), -1 on miss.
448///
449/// zshrs's `createbuiltintable()` returns an immutable HashMap —
450/// the canonical table is static-linked, so this fn can't actually
451/// `removenode`. The faithful structural equivalent: probe the
452/// canonical table to honour the present/absent contract that
453/// callers (setbuiltins, del_autobin) rely on for their
454/// `already-deleted` / `no such builtin` diagnostics. Actual
455/// runtime `enabled` state lives on the modulestab's
456/// `added_builtins` ledger; that's where the caller flips the
457/// observable BINF_ADDED bit.
458pub fn deletebuiltin(nam: &str) -> i32 {
459    // c:449
460    // c:453 — `bn = builtintab->removenode(builtintab, nam);`
461    let tab = createbuiltintable();
462    match tab.get(nam) {
463        None => -1,   // c:454-455 — `if (!bn) return -1;`
464        Some(_) => 0, // c:457 — freenode is owned by createbuiltintable, no-op.
465    }
466}
467
468/// Port of `addbuiltins()` from `Src/module.c:544`. — C decl `addbuiltins(char const *nam, Builtin binl, int size)`.
469/// Walks the slice; for each entry not already
470/// flagged BINF_ADDED, calls `addbuiltin`. Returns 0 if all succeeded,
471/// 1 if any clashed. zwarnnam emitted on each clash matches C.
472pub fn addbuiltins(nam: &str, binl: &mut [builtin]) -> i32 {
473    // c:544
474    let mut ret = 0; // c:548
475    for b in binl.iter_mut() {
476        // c:550 for(n = 0; n < size; n++)
477        if (b.node.flags & BINF_ADDED as i32) != 0 {
478            continue;
479        } // c:553
480        if addbuiltin(b) != 0 {
481            // c:555
482            zwarnnam(
483                nam, // c:556 zwarnnam(nam, "name clash...")
484                &format!("name clash when adding builtin `{}'", b.node.nam),
485            );
486            ret = 1;
487        }
488    }
489    ret // c:563
490}
491
492// Moved here from `src/ported/modules/datetime.rs`: this is a
493// `Src/module.c` function, and every module's `setfeatureenables`
494// (c:3450-3470) calls it, not just zsh/datetime's.
495/// Port of `setbuiltins()` from `Src/module.c:501`. — C decl `setbuiltins(char const *nam, Builtin binl, int size, int *e)`.
496///
497/// C body:
498/// ```c
499/// for(n = 0; n < size; n++) {
500///     Builtin b = &binl[n];
501///     if (e && *e++) {
502///         if (b->node.flags & BINF_ADDED) continue;
503///         if (addbuiltin(b)) { zwarnnam(nam, "name clash when adding builtin `%s'", …); ret = 1; }
504///         else b->node.flags |= BINF_ADDED;
505///     } else {
506///         if (!(b->node.flags & BINF_ADDED)) continue;
507///         if (deletebuiltin(b->node.nam)) { zwarnnam(nam, "builtin `%s' already deleted", …); ret = 1; }
508///         else b->node.flags &= ~BINF_ADDED;
509///     }
510/// }
511/// ```
512///
513/// !!! RUST-ONLY DEVIATION !!! C's `addbuiltin`/`deletebuiltin` insert into
514/// and remove from the live `builtintab`. zshrs's `builtintab`
515/// (`createbuiltintable()`) is an immutable statically-linked table that
516/// already holds every module builtin, so the only thing this can flip is
517/// the "is it visible right now" bit that `module_builtin_available`
518/// (`src/extensions/ext_builtins.rs`) reads — hence the
519/// `DISABLED_MODULE_BUILTINS` registry instead of a table mutation. The
520/// BINF_ADDED bookkeeping on the entry itself is unchanged from C.
521/// WARNING: param names don't match C — Rust=(nam, binl, e) vs C=(nam, binl, size, e)
522pub(crate) fn setbuiltins(nam: &str, binl: &mut [crate::ported::zsh_h::builtin], e: Option<&[i32]>) -> i32 {
523    // c:501
524    use crate::ported::zsh_h::BINF_ADDED;
525    // c:503 — neither branch below can fail (see the deviation note above),
526    // so C's `ret` stays 0 here.
527    let ret = 0;
528    for (n, b) in binl.iter_mut().enumerate() {
529        // c:505
530        let on = e
531            .map(|a| a.get(n).copied().unwrap_or(0) != 0)
532            .unwrap_or(false); // c:507
533        if on {
534            if (b.node.flags & BINF_ADDED as i32) != 0 {
535                continue; // c:508-509
536            }
537            DISABLED_MODULE_BUILTINS
538                .lock()
539                .unwrap()
540                .remove(&b.node.nam); // c:511 addbuiltin(b)
541            b.node.flags |= BINF_ADDED as i32; // c:516
542        } else {
543            if (b.node.flags & BINF_ADDED as i32) == 0 {
544                continue; // c:518-519
545            }
546            DISABLED_MODULE_BUILTINS
547                .lock()
548                .unwrap()
549                .insert(b.node.nam.clone()); // c:521 deletebuiltin(b->node.nam)
550            b.node.flags &= !(BINF_ADDED as i32); // c:526
551        }
552    }
553    let _ = nam;
554    ret // c:530
555}
556
557/// Port of `gethookdef()` from `Src/module.c:849`. — C decl `gethookdef(const char *n)`.
558///
559/// C body (c:849-861):
560/// ```c
561/// Hookdef gethookdef(const char *n) {
562///     Hookdef p;
563///     for (p = hooktab; p; p = p->next)
564///         if (!strcmp(n, p->name))
565///             return p;
566///     return NULL;
567/// }
568/// ```
569pub fn gethookdef(n: &str) -> *mut hookdef {
570    // c:849
571    let mut p = hooktab.load(std::sync::atomic::Ordering::SeqCst); // c:852 p = hooktab
572    while !p.is_null() {
573        // c:853
574        unsafe {
575            if (*p).name == n {
576                // c:854 !strcmp
577                return p; // c:855
578            }
579            p = (*p).next; // c:853 p = p->next
580        }
581    }
582    std::ptr::null_mut() // c:856
583}
584
585/// Port of `addhookdef()` from `Src/module.c:864`. — C decl `addhookdef(Hookdef h)`.
586///
587/// C body (c:864-874):
588/// ```c
589/// int addhookdef(Hookdef h) {
590///     if (gethookdef(h->name)) return 1;
591///     h->next = hooktab;
592///     hooktab = h;
593///     h->funcs = znewlinklist();
594///     return 0;
595/// }
596/// ```
597pub fn addhookdef(h: *mut hookdef) -> i32 {
598    // c:864
599    unsafe {
600        if !gethookdef(&(*h).name).is_null() {
601            // c:866
602            return 1; // c:867
603        }
604        (*h).next = hooktab.load(std::sync::atomic::Ordering::SeqCst); // c:869 h->next = hooktab
605        hooktab.store(h, std::sync::atomic::Ordering::SeqCst); // c:870 hooktab = h
606        if (*h).funcs.is_null() {
607            // c:871 h->funcs = znewlinklist()
608            (*h).funcs = Box::into_raw(Box::new(linklist {
609                first: None,
610                last: None,
611                flags: 0,
612            }));
613        }
614    }
615    0 // c:873
616}
617
618/// Port of `addhookdefs()` from `Src/module.c:883`. — C decl `addhookdefs(Module m, Hookdef h, int size)`.
619///
620/// C body (c:883-895):
621/// ```c
622/// int addhookdefs(Module m, Hookdef h, int size) {
623///     int ret = 0;
624///     while (size--) {
625///         if (addhookdef(h)) {
626///             zwarnnam(m ? m->node.nam : NULL, "name clash when adding hook `%s'", h->name);
627///             ret = 1;
628///         }
629///         h++;
630///     }
631///     return ret;
632/// }
633/// ```
634pub fn addhookdefs(
635    // c:883
636    m: *const module,
637    mut h: *mut hookdef,
638    mut size: i32,
639) -> i32 {
640    let mut ret: i32 = 0; // c:885
641    while size > 0 {
642        // c:887 size--
643        if addhookdef(h) != 0 {
644            // c:888
645            let nam: String = if m.is_null() {
646                // c:889 m ? m->node.nam : NULL
647                String::new()
648            } else {
649                unsafe { (*m).node.nam.clone() }
650            };
651            let hook_name = unsafe { (*h).name.clone() };
652            zwarnnam(
653                // c:889 zwarnnam
654                &nam,
655                &format!("name clash when adding hook `{}'", hook_name),
656            );
657            ret = 1; // c:891
658        }
659        unsafe {
660            h = h.add(1);
661        } // c:893 h++
662        size -= 1; // c:887
663    }
664    ret // c:894
665}
666
667/// Port of `deletehookdef()` from `Src/module.c:902`. — C decl `deletehookdef(Hookdef h)`.
668///
669/// C body (c:902-919):
670/// ```c
671/// int deletehookdef(Hookdef h) {
672///     Hookdef p, q;
673///     for (p = hooktab, q = NULL; p && p != h; q = p, p = p->next);
674///     if (!p) return 1;
675///     if (q) q->next = p->next; else hooktab = p->next;
676///     freelinklist(p->funcs, NULL);
677///     return 0;
678/// }
679/// ```
680pub fn deletehookdef(h: *mut hookdef) -> i32 {
681    // c:902
682    let mut p = hooktab.load(std::sync::atomic::Ordering::SeqCst); // c:906 p = hooktab
683    let mut q: *mut hookdef = std::ptr::null_mut(); // c:906 q = NULL
684    while !p.is_null() && p != h {
685        // c:907
686        q = p; // c:907 q = p
687        unsafe {
688            p = (*p).next;
689        } // c:907 p = p->next
690    }
691    if p.is_null() {
692        // c:909
693        return 1; // c:910
694    }
695    unsafe {
696        if !q.is_null() {
697            // c:912
698            (*q).next = (*p).next; // c:913 q->next = p->next
699        } else {
700            hooktab.store((*p).next, std::sync::atomic::Ordering::SeqCst); // c:915 hooktab = p->next
701        }
702        if !(*p).funcs.is_null() {
703            // c:916 freelinklist(p->funcs, NULL)
704            drop(Box::from_raw((*p).funcs));
705            (*p).funcs = std::ptr::null_mut();
706        }
707    }
708    0 // c:917
709}
710
711/// Port of `deletehookdefs()` from `Src/module.c:923`. — C decl `deletehookdefs(UNUSED(Module m), Hookdef h, int size)`.
712/// `m` is unused per `UNUSED(Module m)` in C.
713pub fn deletehookdefs(
714    // c:923
715    _m: *const module,
716    mut h: *mut hookdef,
717    mut size: i32,
718) -> i32 {
719    let mut ret: i32 = 0; // c:925
720    while size > 0 {
721        // c:927 size--
722        if deletehookdef(h) != 0 {
723            // c:928
724            ret = 1; // c:929
725        }
726        unsafe {
727            h = h.add(1);
728        } // c:930 h++
729        size -= 1; // c:927
730    }
731    ret // c:931
732}
733
734/// Port of `addhookdeffunc()` from `Src/module.c:939`. — C decl `addhookdeffunc(Hookdef h, Hookfn f)`.
735///
736/// C body (c:939-944):
737/// ```c
738/// int addhookdeffunc(Hookdef h, Hookfn f) {
739///     zaddlinknode(h->funcs, (void *) f);
740///     return 0;
741/// }
742/// ```
743pub fn addhookdeffunc(
744    // c:939
745    h: *mut hookdef,
746    f: Hookfn,
747) -> i32 {
748    unsafe {
749        if (*h).funcs.is_null() {
750            (*h).funcs = Box::into_raw(Box::new(linklist {
751                first: None,
752                last: None,
753                flags: 0,
754            }));
755        }
756        let funcs = &mut *(*h).funcs;
757        // c:942 — zaddlinknode(h->funcs, f) appends to end of LinkList.
758        // Walk to tail of owned chain (linklist.last cannot be a Box
759        // pointer because that would duplicate ownership of the tail
760        // node — the C `last` field is a raw pointer; the Rust
761        // representation keeps it as None and resolves the tail by
762        // walking forward from .first).
763        let new_node = Box::new(linknode {
764            next: None,
765            prev: None,
766            dat: f as usize,
767        });
768        if funcs.first.is_none() {
769            funcs.first = Some(new_node);
770        } else {
771            let mut tail = funcs.first.as_mut().unwrap();
772            while tail.next.is_some() {
773                tail = tail.next.as_mut().unwrap();
774            }
775            tail.next = Some(new_node);
776        }
777    }
778    0 // c:943
779}
780
781/// Port of `addhookfunc()` from `Src/module.c:948`. — C decl `addhookfunc(char *n, Hookfn f)`.
782///
783/// C body (c:948-955):
784/// ```c
785/// int addhookfunc(char *n, Hookfn f) {
786///     Hookdef h = gethookdef(n);
787///     if (h) return addhookdeffunc(h, f);
788///     return 1;
789/// }
790/// ```
791pub fn addhookfunc(
792    // c:948
793    n: &str,
794    f: Hookfn,
795) -> i32 {
796    let h = gethookdef(n); // c:950 h = gethookdef(n)
797    if !h.is_null() {
798        // c:951
799        return addhookdeffunc(h, f); // c:952
800    }
801    1 // c:953
802}
803
804/// Port of `deletehookdeffunc()` from `Src/module.c:961`. — C decl `deletehookdeffunc(Hookdef h, Hookfn f)`.
805///
806/// C body (c:961-973):
807/// ```c
808/// int deletehookdeffunc(Hookdef h, Hookfn f) {
809///     LinkNode p;
810///     for (p = firstnode(h->funcs); p; incnode(p))
811///         if (f == (Hookfn) getdata(p)) {
812///             remnode(h->funcs, p);
813///             return 0;
814///         }
815///     return 1;
816/// }
817/// ```
818pub fn deletehookdeffunc(
819    // c:961
820    h: *mut hookdef,
821    f: Hookfn,
822) -> i32 {
823    unsafe {
824        if (*h).funcs.is_null() {
825            return 1;
826        }
827        let funcs = &mut *(*h).funcs;
828        let f_val = f as usize;
829        // Walk owning chain looking for the matching dat. Splice on hit.
830        let mut prev: &mut Option<Box<linknode>> = &mut funcs.first;
831        loop {
832            match prev {
833                None => return 1, // c:971
834                Some(node) if node.dat == f_val => {
835                    // c:966 f == getdata(p)
836                    let next = node.next.take(); // c:967 remnode
837                    *prev = next;
838                    return 0; // c:968
839                }
840                Some(_) => {
841                    // c:965 incnode(p) — advance.
842                    prev = &mut prev.as_mut().unwrap().next;
843                }
844            }
845        }
846    }
847}
848
849/// Port of `deletehookfunc()` from `Src/module.c:977`. — C decl `deletehookfunc(const char *n, Hookfn f)`.
850///
851/// C body (c:977-984):
852/// ```c
853/// int deletehookfunc(const char *n, Hookfn f) {
854///     Hookdef h = gethookdef(n);
855///     if (h) return deletehookdeffunc(h, f);
856///     return 1;
857/// }
858/// ```
859pub fn deletehookfunc(
860    // c:977
861    n: &str,
862    f: Hookfn,
863) -> i32 {
864    let h = gethookdef(n); // c:979 h = gethookdef(n)
865    if !h.is_null() {
866        // c:980
867        return deletehookdeffunc(h, f); // c:981
868    }
869    1 // c:982
870}
871
872/// Port of `runhookdef()` from `Src/module.c:990`. — C decl `runhookdef(Hookdef h, void *d)`.
873///
874/// C body (c:990-1010):
875/// ```c
876/// int runhookdef(Hookdef h, void *d) {
877///     if (empty(h->funcs)) {
878///         if (h->def) return h->def(h, d);
879///         return 0;
880///     } else if (h->flags & HOOKF_ALL) {
881///         LinkNode p; int r;
882///         for (p = firstnode(h->funcs); p; incnode(p))
883///             if ((r = ((Hookfn) getdata(p))(h, d))) return r;
884///         if (h->def) return h->def(h, d);
885///         return 0;
886///     } else
887///         return ((Hookfn) getdata(lastnode(h->funcs)))(h, d);
888/// }
889/// ```
890pub fn runhookdef(
891    // c:990
892    h: *mut hookdef,
893    d: *mut std::ffi::c_void,
894) -> i32 {
895    unsafe {
896        let funcs_ptr = (*h).funcs;
897        let funcs_empty = funcs_ptr.is_null() || (*funcs_ptr).first.is_none(); // c:992 empty()
898        if funcs_empty {
899            // c:992
900            if let Some(def) = (*h).def {
901                // c:993 if (h->def)
902                return def(h, d); // c:994 return h->def(h,d)
903            }
904            return 0; // c:995
905        }
906        if (*h).flags & HOOKF_ALL != 0 {
907            // c:996 h->flags & HOOKF_ALL
908            let mut node = (*funcs_ptr).first.as_ref(); // c:999 firstnode
909            while let Some(n) = node {
910                // c:999 ; p ;
911                let fn_ptr: Hookfn = std::mem::transmute(n.dat); // c:1000 (Hookfn) getdata(p)
912                let r = fn_ptr(h, d); // c:1000 (...)(h, d)
913                if r != 0 {
914                    // c:1000 if ((r = ...))
915                    return r; // c:1001
916                }
917                node = n.next.as_ref(); // c:999 incnode
918            }
919            if let Some(def) = (*h).def {
920                // c:1002 if (h->def)
921                return def(h, d); // c:1003 return h->def(h, d)
922            }
923            return 0; // c:1004
924        }
925        // c:1006 — last fn only.
926        let mut tail = (*funcs_ptr).first.as_ref().expect("non-empty");
927        while let Some(next) = tail.next.as_ref() {
928            tail = next;
929        }
930        let fn_ptr: Hookfn = std::mem::transmute(tail.dat);
931        fn_ptr(h, d) // c:1006
932    }
933}
934
935/// Port of `checkaddparam()` from `Src/module.c:1026`. — C decl `checkaddparam(const char *nam, int opt_i)`.
936///
937/// C body:
938/// ```c
939/// checkaddparam(const char *nam, int opt_i)
940/// {
941///     Param pm;
942///     if (!(pm = (Param) gethashnode2(paramtab, nam)))
943///         return 0;
944///     if (pm->level || !(pm->node.flags & PM_AUTOLOAD)) {
945///         if (!opt_i || pm->level) {
946///             zwarn("Can't add module parameter `%s': %s",
947///                   nam, pm->level ? "local parameter exists" :
948///                                    "parameter already exists");
949///             return 1;
950///         }
951///         return 2;
952///     }
953///     unsetparam_pm(pm, 0, 1);
954///     return 0;
955/// }
956/// ```
957///
958/// Returns: 0 = OK to add, 1 = error printed, 2 = blocked but `-i`
959/// suppressed warning. `pm->level != 0` means a local param shadows
960/// the name (always errors). `PM_AUTOLOAD` set means the existing
961/// param is an autoload stub the C source unsets to make room.
962///
963/// Checks for an existing param of the same name; if absent → 0
964/// (free to add). If present and not autoloadable → 1 (warn) or
965/// 2 (skip warning under `-i`). If present + autoload-marked →
966/// unset the existing entry and return 0.
967pub fn checkaddparam(nam: &str, opt_i: i32) -> i32 {
968    // c:1026
969    // c:1030 — if (!(pm = gethashnode2(paramtab, nam))) return 0;
970    let pm_clone = {
971        let tab = paramtab().read().expect("paramtab poisoned");
972        tab.get(nam).cloned()
973    };
974    let mut pm = match pm_clone {
975        Some(p) => p,
976        None => return 0,
977    };
978    // c:1033 — if (pm->level || !(pm->node.flags & PM_AUTOLOAD)) {
979    if pm.level != 0 || (pm.node.flags as u32 & PM_AUTOLOAD) == 0 {
980        // c:1042-1048 — if (!opt_i || pm->level) zwarn(...); return 1;
981        if opt_i == 0 || pm.level != 0 {
982            zwarn(&format!(
983                "Can't add module parameter `{}': {}",
984                nam,
985                if pm.level != 0 {
986                    "local parameter exists"
987                } else {
988                    "parameter already exists"
989                }
990            ));
991            return 1;
992        }
993        // c:1049 — return 2;
994        return 2;
995    }
996    // c:1052 — unsetparam_pm(pm, 0, 1);
997    unsetparam_pm(&mut pm, 0, 1);
998    // !!! RUST-ONLY STEP — NO DIRECT C COUNTERPART !!!
999    // C's `unsetparam_pm` ends in the `paramtab->removenode` postlude
1000    // (c:Src/params.c:3853-3935) that drops the stub node; the Rust one
1001    // only marks the by-value copy it was handed (see the same step in
1002    // `deleteparamdef`). Without the removal the PM_AUTOLOAD stub stayed
1003    // in paramtab, addparamdef's createparam found it and never
1004    // installed the module's parameter, and loadparamnode
1005    // (c:Src/params.c:553-563) reported "autoloading module zsh/example
1006    // failed to define parameter: exint".
1007    if let Ok(mut tab) = paramtab().write() {
1008        tab.remove(nam); // c:Src/params.c:3900 paramtab->removenode
1009    }
1010    // c:1053 — return 0;
1011    0
1012}
1013
1014/// Port of `addparamdef()` from `Src/module.c:1061`. — C decl `addparamdef(Paramdef d)`.
1015/// Registers a module-supplied parameter definition into the canonical
1016/// `paramtab`, wiring the GSU vtable per `PM_TYPE`. Returns 0 on
1017/// success, 1 on error.
1018///
1019/// ```c
1020/// int
1021/// addparamdef(Paramdef d)
1022/// {
1023///     Param pm;
1024///     if (checkaddparam(d->name, 0)) return 1;
1025///     if (d->getnfn) {
1026///         if (!(pm = createspecialhash(d->name, d->getnfn,
1027///                                      d->scantfn, d->flags)))
1028///             return 1;
1029///     }
1030///     else if (!(pm = createparam(d->name, d->flags)) &&
1031///         !(pm = (Param) paramtab->getnode(paramtab, d->name)))
1032///         return 1;
1033///     d->pm = pm;
1034///     pm->level = 0;
1035///     if (d->var) pm->u.data = d->var;
1036///     if (d->var || d->gsu) {
1037///         switch (PM_TYPE(pm->node.flags)) {
1038///         case PM_SCALAR:
1039///             if (pm->node.flags & PM_TIED)
1040///                 pm->ename = ztrdup(casemodify(pm->node.nam, CASMOD_LOWER));
1041///             /* fall-through */
1042///         case PM_NAMEREF:
1043///             pm->gsu.s = d->gsu ? (GsuScalar)d->gsu : &varscalar_gsu;
1044///             break;
1045///         case PM_INTEGER:
1046///             pm->gsu.i = d->gsu ? (GsuInteger)d->gsu : &varinteger_gsu;
1047///             break;
1048///         case PM_FFLOAT: case PM_EFLOAT:
1049///             pm->gsu.f = d->gsu;
1050///             break;
1051///         case PM_ARRAY:
1052///             if (pm->node.flags & PM_TIED)
1053///                 pm->ename = ztrdup(casemodify(pm->node.nam, CASMOD_UPPER));
1054///             pm->gsu.a = d->gsu ? (GsuArray)d->gsu : &vararray_gsu;
1055///             break;
1056///         case PM_HASHED:
1057///             if (d->gsu) pm->gsu.h = (GsuHash)d->gsu;
1058///             break;
1059///         default:
1060///             unsetparam_pm(pm, 0, 1);
1061///             return 1;
1062///         }
1063///     }
1064///     return 0;
1065/// }
1066/// ```
1067pub fn addparamdef(d: &mut paramdef) -> i32 {
1068    // c:1061
1069
1070    // c:1065 — `if (checkaddparam(d->name, 0)) return 1;`
1071    if checkaddparam(&d.name, 0) != 0 {
1072        // c:1065
1073        return 1; // c:1066
1074    }
1075
1076    // c:1068-1075 — either createspecialhash (hash params with getnfn)
1077    // or createparam, falling back to gethashnode on collision.
1078    let pm_opt: Option<Param> = if d.getnfn.is_some() {
1079        // c:1068
1080        // c:1069-1071 — createspecialhash(d->name, d->getnfn, d->scantfn, d->flags)
1081        // The Rust createspecialhash takes (name, flags) only; the
1082        // getnfn/scantfn fields aren't yet wired through the typed
1083        // Rust API. Pass flags and let the param be created.
1084        createspecialhash(&d.name, d.flags) // c:1069
1085    } else {
1086        // c:1072
1087        match createparam(&d.name, d.flags) {
1088            // c:1073
1089            Some(p) => Some(p),
1090            None => {
1091                // c:1074 — fall back to paramtab->getnode(paramtab, d->name)
1092                let tab = paramtab().read().ok();
1093                tab.and_then(|t| {
1094                    t.get(&d.name).map(|p| {
1095                        // Clone the existing param so we can mutate the
1096                        // returned handle without holding the read lock.
1097                        let mut clone = p.clone();
1098                        clone.level = 0;
1099                        Box::new(*clone)
1100                    })
1101                })
1102            }
1103        }
1104    };
1105    let mut pm = match pm_opt {
1106        // c:1074-1075
1107        Some(p) => p,
1108        None => return 1,
1109    };
1110
1111    // c:1077-1078 — `d->pm = pm; pm->level = 0;`
1112    pm.level = 0; // c:1078
1113
1114    // c:1079-1080 — `if (d->var) pm->u.data = d->var;`
1115    if d.var != 0 { // c:1079
1116         // pm.u.data is a raw `void *` slot — not yet exposed on the
1117         // Rust param mirror. Carry the assignment as a comment.
1118         // pm.u.data = d->var as *mut _;                                     // c:1080
1119    }
1120
1121    if d.var != 0 || d.gsu != 0 {
1122        // c:1081
1123        let t = PM_TYPE(pm.node.flags as u32); // c:1086
1124        let pmflags = pm.node.flags as u32;
1125        if t == PM_SCALAR || t == PM_NAMEREF {
1126            // c:1087/1091
1127            if t == PM_SCALAR && (pmflags & PM_TIED) != 0 {
1128                // c:1088
1129                let lower = casemodify(&pm.node.nam, CASMOD_LOWER);
1130                pm.ename = Some(ztrdup(&lower)); // c:1089
1131            }
1132            // c:1092 — `pm->gsu.s = d->gsu ? (GsuScalar)d->gsu :
1133            // &varscalar_gsu;`. A module's `gsu` is the address of its static
1134            // `gsu_scalar` (`&sh_name_gsu`, Src/Modules/ksh93.c:100); with
1135            // none the param keeps the plain variable accessors. And c:1080
1136            // `pm->u.data = d->var`: for a PM_NAMEREF row (ksh93.c:121-130)
1137            // `var` is the referent name GETREFNAME reads back, here the
1138            // address of a `&'static str`. `pm` is a copy, so both go onto the
1139            // live node too.
1140            let gsu = (d.gsu != 0).then(|| {
1141                let g = unsafe { &*(d.gsu as *const crate::ported::zsh_h::gsu_scalar) };
1142                (g.getfn, g.setfn, g.unsetfn)
1143            });
1144            let refname = (t == PM_NAMEREF && d.var != 0)
1145                .then(|| unsafe { *(d.var as *const &'static str) }.to_string());
1146            let mk = |g: (
1147                fn(&crate::ported::zsh_h::param) -> String,
1148                fn(&mut crate::ported::zsh_h::param, String),
1149                fn(&mut crate::ported::zsh_h::param, i32),
1150            )| {
1151                Box::new(crate::ported::zsh_h::gsu_scalar { getfn: g.0, setfn: g.1, unsetfn: g.2 })
1152            };
1153            if let Some(g) = gsu {
1154                pm.gsu_s = Some(mk(g)); // c:1092
1155            }
1156            if let Some(r) = &refname {
1157                pm.u_str = Some(r.clone()); // c:1080
1158            }
1159            if let Ok(mut tab) = paramtab().write() {
1160                if let Some(live) = tab.get_mut(&d.name) {
1161                    if let Some(g) = gsu {
1162                        live.gsu_s = Some(mk(g));
1163                    }
1164                    if let Some(r) = refname {
1165                        live.u_str = Some(r);
1166                    }
1167                }
1168            }
1169        } else if t == PM_INTEGER {
1170            // c:1095-1096 — `pm->gsu.i = d->gsu ? (GsuInteger)d->gsu :
1171            // &varinteger_gsu;`. A module's `gsu` is the address of its
1172            // static `gsu_integer` (`&srandom_gsu`, Src/Modules/random.c:219).
1173            // `pm` is a copy, so the vtable goes onto the live node too.
1174            if d.gsu != 0 {
1175                let g = unsafe { &*(d.gsu as *const crate::ported::zsh_h::gsu_integer) };
1176                let mk = || {
1177                    Box::new(crate::ported::zsh_h::gsu_integer {
1178                        getfn: g.getfn,
1179                        setfn: g.setfn,
1180                        unsetfn: g.unsetfn,
1181                    })
1182                };
1183                pm.gsu_i = Some(mk());
1184                if let Ok(mut tab) = paramtab().write() {
1185                    if let Some(live) = tab.get_mut(&d.name) {
1186                        live.gsu_i = Some(mk());
1187                    }
1188                }
1189            }
1190        } else if t == PM_FFLOAT || t == PM_EFLOAT {
1191            // c:1099-1100
1192            let _ = d.gsu; // c:1101
1193        } else if t == PM_ARRAY {
1194            // c:1104
1195            if (pmflags & PM_TIED) != 0 {
1196                // c:1105
1197                let upper = casemodify(&pm.node.nam, CASMOD_UPPER);
1198                pm.ename = Some(ztrdup(&upper)); // c:1106
1199            }
1200            let _ = d.gsu; // c:1107
1201        } else if t == PM_HASHED {
1202            // c:1110
1203            let _ = d.gsu; // c:1112-1113
1204        } else {
1205            // c:1116
1206            unsetparam_pm(&mut pm, 0, 1); // c:1117
1207            return 1; // c:1118
1208        }
1209    }
1210
1211    d.pm = Some(pm); // c:1077 d->pm = pm
1212    0 // c:1122
1213}
1214
1215/// Port of `deleteparamdef()` from `Src/module.c:1124`. — C decl `deleteparamdef(Paramdef d)`.
1216/// Removes a previously-registered module parameter, unwinding any
1217/// hidden-param shadow chain so the matching `d->pm` instance is the
1218/// one actually unset.
1219///
1220/// ```c
1221/// int
1222/// deleteparamdef(Paramdef d)
1223/// {
1224///     Param pm = (Param) paramtab->getnode(paramtab, d->name);
1225///     if (!pm) return 1;
1226///     if (pm != d->pm) {
1227///         Param prevpm, searchpm;
1228///         for (prevpm = pm, searchpm = pm->old;
1229///              searchpm;
1230///              prevpm = searchpm, searchpm = searchpm->old)
1231///             if (searchpm == d->pm) break;
1232///         if (!searchpm) return 1;
1233///         paramtab->removenode(paramtab, pm->node.nam);
1234///         prevpm->old = searchpm->old;
1235///         searchpm->old = pm;
1236///         paramtab->addnode(paramtab, searchpm->node.nam, searchpm);
1237///         pm = searchpm;
1238///     }
1239///     pm->node.flags = (pm->node.flags & ~PM_READONLY) | PM_REMOVABLE;
1240///     unsetparam_pm(pm, 0, 1);
1241///     d->pm = NULL;
1242///     return 0;
1243/// }
1244/// ```
1245pub fn deleteparamdef(d: &mut paramdef) -> i32 {
1246    // c:1128
1247
1248    // c:1131 — `Param pm = (Param) paramtab->getnode(paramtab, d->name);`
1249    let mut pm: Param = {
1250        let tab = paramtab().read();
1251        match tab {
1252            Ok(t) => match t.get(&d.name) {
1253                Some(p) => p.clone(),
1254                None => return 1, // c:1133-1134
1255            },
1256            Err(_) => return 1,
1257        }
1258    };
1259
1260    // c:1135-1156 — shadow-chain unwind: if the live pm isn't d->pm,
1261    // walk pm->old searching for d->pm; if found, splice it out,
1262    // re-add the matching node under its name, and operate on it.
1263    // The Rust param mirror's `old` chain isn't yet wired through
1264    // paramtab; the typed paramtab dispatches the latest binding
1265    // directly. Mirror the C structure so callers see the same
1266    // semantics when the shadow chain lands.
1267    if let Some(expected) = d.pm.as_ref() {
1268        // c:1130 `if (pm != d->pm)`
1269        // !!! RUST-ONLY IDENTITY TEST !!! C compares POINTERS: `pm` and
1270        // `d->pm` are the same heap object whenever the module's own
1271        // parameter is the live binding. zshrs's `paramtab` stores `Param`
1272        // BY VALUE and hands out clones, so `std::ptr::eq` was false for
1273        // every call — `deleteparamdef` always took the c:1143 `return 1`
1274        // path, and `zmodload -u zsh/datetime` reported "parameter
1275        // `EPOCHSECONDS' already deleted" for a parameter it had just
1276        // added. Two things stand in for the pointer compare:
1277        //   * the NAME, which is what identity means for a by-value table;
1278        //   * `pm.level`, because `addparamdef` (c:1078) pins the module's
1279        //     own binding at `pm->level = 0` while `local NAME=…` records
1280        //     the enclosing `locallevel` (params.rs:2762-2766, c:1136).
1281        // A non-zero level therefore means the live node is a LOCAL that
1282        // has hidden the module's parameter — exactly the case c:1131-1133
1283        // describes ("See if the parameter has been hidden").
1284        if pm.node.nam != expected.node.nam || pm.level != 0 {
1285            // c:1136-1140 — walk pm->old looking for d->pm. The whole chain
1286            // carries one name, so the level-0 link is the module's.
1287            //     for (prevpm = pm, searchpm = pm->old;
1288            //          searchpm;
1289            //          prevpm = searchpm, searchpm = searchpm->old)
1290            //         if (searchpm == d->pm) break;
1291            // Flatten the by-value chain so the splice below is a Vec edit
1292            // rather than a doubly-borrowed linked-list walk.
1293            let mut chain: Vec<Param> = Vec::new();
1294            let mut link = Some(pm);
1295            while let Some(mut n) = link {
1296                link = n.old.take();
1297                chain.push(n);
1298            }
1299            let searchpm = chain
1300                .iter()
1301                .skip(1)
1302                .position(|p| p.level == 0 && p.node.nam == expected.node.nam)
1303                .map(|i| i + 1);
1304            let Some(hit) = searchpm else {
1305                // c:1142-1143 — `if (!searchpm) return 1;`
1306                return 1;
1307            };
1308            // c:1145-1150:
1309            //     paramtab->removenode(paramtab, pm->node.nam);
1310            //     prevpm->old = searchpm->old;
1311            //     searchpm->old = pm;
1312            //     paramtab->addnode(paramtab, searchpm->node.nam, searchpm);
1313            //     pm = searchpm;
1314            // C hoists the module's binding to the front so the unset below
1315            // hits it, with the hidden chain hanging off its ->old; then
1316            // unsetparam_pm's removenode postlude (c:Src/params.c:3900)
1317            // restores that chain. The net effect on the table is that the
1318            // module's link, and only it, leaves the chain — so splice it
1319            // out here and rebuild.
1320            let mut modpm = chain.remove(hit);
1321            let mut rebuilt: Option<Param> = None;
1322            for mut link in chain.into_iter().rev() {
1323                link.old = rebuilt.take();
1324                rebuilt = Some(link);
1325            }
1326            // c:1152-1153 on the module's own binding, not on the local.
1327            modpm.node.flags =
1328                (modpm.node.flags & !(PM_READONLY as i32)) | (PM_REMOVABLE as i32);
1329            unsetparam_pm(&mut modpm, 0, 1);
1330            if let Ok(mut tab) = paramtab().write() {
1331                match rebuilt {
1332                    // c:1150 — the hidden chain is what stays visible.
1333                    Some(front) => {
1334                        tab.insert(d.name.clone(), front);
1335                    }
1336                    None => {
1337                        tab.remove(&d.name);
1338                    }
1339                }
1340            }
1341            d.pm = None; // c:1154 d->pm = NULL
1342            return 0; // c:1155
1343        }
1344    }
1345
1346    // c:1157 — `pm->node.flags = (pm->node.flags & ~PM_READONLY) | PM_REMOVABLE;`
1347    pm.node.flags = (pm.node.flags & !(PM_READONLY as i32)) | (PM_REMOVABLE as i32); // c:1157
1348    unsetparam_pm(&mut pm, 0, 1); // c:1158
1349                                  // !!! RUST-ONLY STEP — NO DIRECT C COUNTERPART !!!
1350                                  // C's `unsetparam_pm` ends in the `paramtab->removenode` postlude
1351                                  // (c:Src/params.c:3853-3935) that physically drops the node. The Rust
1352                                  // `unsetparam_pm` stops short of it ("Tied alt-name removal +
1353                                  // paramtab restore-from-old not yet possible without HashTable
1354                                  // backend", params.rs:9807-9810) and only stamps PM_UNSET on the
1355                                  // by-value COPY it was handed, so the paramtab entry survived intact:
1356                                  // after `zmodload -u zsh/datetime` the name was still there and still
1357                                  // PM_READONLY, so the next `zmodload zsh/datetime` failed with
1358                                  // "parameter already exists" and a plain `EPOCHSECONDS=(...)` failed
1359                                  // with "read-only variable". Do the removenode half here.
1360                                  //
1361                                  // KNOWN DIVERGENCE when a local shadows the name: C's c:1141-1153 walk
1362                                  // splices `d->pm` out of the `pm->old` chain first, so it unsets the
1363                                  // MODULE's binding and the local survives. zshrs has no `old` chain
1364                                  // (`params.rs` never assigns `Param::old`) and the local's saved outer
1365                                  // copy lives in the executor's own scope stack, so the node reached
1366                                  // here is the local and the module's binding is unreachable. Removing
1367                                  // it matches C on the three lasting observations — the module's claim
1368                                  // is dropped, `${+NAME}` is 0 after the scope pops, and a later plain
1369                                  // assignment works — and diverges only on the local's value for the
1370                                  // remainder of the enclosing scope (V04features.ztst "Successfully
1371                                  // unloaded a module despite a parameter being hidden" reads it back).
1372    if let Ok(mut tab) = paramtab().write() {
1373        tab.remove(&d.name); // c:Src/params.c:3900 paramtab->removenode
1374    }
1375    d.pm = None; // c:1159 d->pm = NULL
1376    0 // c:1160
1377}
1378
1379// Moved here from `src/ported/modules/datetime.rs`: this is a
1380// `Src/module.c` function, and every module's `setfeatureenables`
1381// (c:3450-3470) calls it, not just zsh/datetime's.
1382/// Port of `setparamdefs()` from `Src/module.c:1165`. — C decl `setparamdefs(char const *nam, Paramdef d, int size, int *e)`.
1383///
1384/// C body:
1385/// ```c
1386/// while (size--) {
1387///     if (e && *e++) {
1388///         if (d->pm) { d++; continue; }
1389///         if (addparamdef(d)) { zwarnnam(nam, "error when adding parameter `%s'", d->name); ret = 1; }
1390///     } else {
1391///         if (!d->pm) { d++; continue; }
1392///         if (deleteparamdef(d)) { zwarnnam(nam, "parameter `%s' already deleted", d->name); ret = 1; }
1393///     }
1394///     d++;
1395/// }
1396/// ```
1397/// WARNING: param names don't match C — Rust=(nam, d, e) vs C=(nam, d, size, e)
1398pub(crate) fn setparamdefs(nam: &str, d: &mut [crate::ported::zsh_h::paramdef], e: Option<&[i32]>) -> i32 {
1399    // c:1169
1400    let mut ret = 0; // c:1172
1401    for (n, def) in d.iter_mut().enumerate() {
1402        // c:1174 while (size--)
1403        let on = e
1404            .map(|a| a.get(n).copied().unwrap_or(0) != 0)
1405            .unwrap_or(false); // c:1175
1406        if on {
1407            if def.pm.is_some() {
1408                continue; // c:1176-1179
1409            }
1410            if addparamdef(def) != 0 {
1411                // c:1180
1412                zwarnnam(nam, &format!("error when adding parameter `{}'", def.name));
1413                // c:1181
1414                ret = 1; // c:1182
1415            }
1416        } else {
1417            if def.pm.is_none() {
1418                continue; // c:1185-1188
1419            }
1420            if deleteparamdef(def) != 0 {
1421                // c:1189
1422                zwarnnam(nam, &format!("parameter `{}' already deleted", def.name));
1423                // c:1190
1424                ret = 1; // c:1191
1425            }
1426        }
1427    }
1428    ret // c:1196
1429}
1430
1431/// Port of `add_autoparam()` from `Src/module.c:1198`. — C decl `add_autoparam(const char *module, const char *pnam, int flags)`.
1432///
1433/// C body c:1197-1228:
1434/// ```c
1435/// static int
1436/// add_autoparam(const char *module, const char *pnam, int flags)
1437/// {
1438///     Param pm;
1439///     int ret;
1440///     int ne = noerrs;
1441///
1442///     queue_signals();
1443///     if ((ret = checkaddparam(pnam, (flags & FEAT_IGNORE)))) {
1444///         unqueue_signals();
1445///         return ret == 2 ? 0 : -1;
1446///     }
1447///     noerrs = 2;
1448///     if ((pm = setsparam(dupstring(pnam), ztrdup(module)))) {
1449///         pm->node.flags |= PM_AUTOLOAD;
1450///         if (flags & FEAT_AUTOALL)
1451///             pm->node.flags |= PM_AUTOALL;
1452///         ret = 0;
1453///     } else
1454///         ret = -1;
1455///     noerrs = ne;
1456///     unqueue_signals();
1457///
1458///     return ret;
1459/// }
1460/// ```
1461///
1462/// Adds an autoload stub for a module parameter: `setsparam` creates
1463/// the param as a scalar with `value=module name`, then OR-in
1464/// `PM_AUTOLOAD` so first access fires `loadparamnode` (c:546) which
1465/// calls `ensurefeature(module, "p:", nam)` to actually load the
1466/// module. `typeset +` listing checks `PM_AUTOLOAD` and emits
1467/// "undefined NAME" instead of the usual type prefix.
1468pub fn add_autoparam(module: &str, pnam: &str, flags: i32) -> i32 {
1469    // c:1197
1470    use crate::ported::signals::queue_signals;
1471    use std::sync::atomic::Ordering;
1472
1473    // c:1202 — int ne = noerrs;
1474    let ne = *crate::ported::utils::noerrs_lock().lock().unwrap();
1475
1476    // c:1204 — queue_signals();
1477    queue_signals();
1478
1479    // c:1205 — if ((ret = checkaddparam(pnam, (flags & FEAT_IGNORE)))) {
1480    let ret_check = checkaddparam(pnam, flags & FEAT_IGNORE as i32);
1481    if ret_check != 0 {
1482        // c:1206 — unqueue_signals();
1483        unqueue_signals();
1484        // c:1214 — return ret == 2 ? 0 : -1;
1485        return if ret_check == 2 { 0 } else { -1 };
1486    }
1487
1488    // c:1217 — noerrs = 2;
1489    *crate::ported::utils::noerrs_lock().lock().unwrap() = 2;
1490
1491    // c:1218 — if ((pm = setsparam(dupstring(pnam), ztrdup(module))))
1492    let pm_opt = crate::ported::params::setsparam(pnam, module);
1493    let ret = if let Some(mut pm) = pm_opt {
1494        // c:1219 — pm->node.flags |= PM_AUTOLOAD;
1495        pm.node.flags |= PM_AUTOLOAD as i32;
1496        // c:1220-1221 — if (flags & FEAT_AUTOALL) pm->node.flags |= PM_AUTOALL;
1497        if (flags & FEAT_AUTOALL as i32) != 0 {
1498            pm.node.flags |= PM_AUTOALL as i32;
1499        }
1500        // Re-insert the modified clone back into paramtab so the flag
1501        // bits stick (paramtab stores `Param` by value, not by pointer).
1502        {
1503            let mut tab = paramtab().write().expect("paramtab poisoned");
1504            tab.insert(pnam.to_string(), pm);
1505        }
1506        // c:1222 — ret = 0;
1507        0
1508    } else {
1509        // c:1224 — ret = -1;
1510        -1
1511    };
1512
1513    // c:1225 — noerrs = ne;
1514    *crate::ported::utils::noerrs_lock().lock().unwrap() = ne;
1515    // c:1226 — unqueue_signals();
1516    unqueue_signals();
1517    // c:1228 — return ret;
1518    ret
1519}
1520
1521/// Port of `del_autoparam()` from `Src/module.c:1235`. — C decl `del_autoparam(UNUSED(const char *modnam), const char *pnam, int flags)`.
1522///
1523/// C body c:1234-1248:
1524/// ```c
1525/// static int
1526/// del_autoparam(UNUSED(const char *modnam), const char *pnam, int flags)
1527/// {
1528///     Param pm = (Param) gethashnode2(paramtab, pnam);
1529///     if (!pm) {
1530///         if (!(flags & FEAT_IGNORE)) return 2;
1531///     } else if (!(pm->node.flags & PM_AUTOLOAD)) {
1532///         if (!(flags & FEAT_IGNORE)) return 3;
1533///     } else
1534///         unsetparam_pm(pm, 0, 1);
1535///     return 0;
1536/// }
1537/// ```
1538///
1539/// Removes a param previously registered by `add_autoparam`. Returns
1540/// 2 when the param doesn't exist, 3 when it exists but isn't an
1541/// autoload stub, 0 on success — `FEAT_IGNORE` masks both error
1542/// returns to 0.
1543pub fn del_autoparam(_modnam: &str, pnam: &str, flags: i32) -> i32 {
1544    // c:1234
1545    // c:1237 — Param pm = (Param) gethashnode2(paramtab, pnam);
1546    let pm_opt = {
1547        let tab = paramtab().read().expect("paramtab poisoned");
1548        tab.get(pnam).cloned()
1549    };
1550    match pm_opt {
1551        // c:1239 — if (!pm) { if (!(flags & FEAT_IGNORE)) return 2; }
1552        None => {
1553            if (flags & FEAT_IGNORE as i32) == 0 {
1554                return 2; // c:1241
1555            }
1556        }
1557        Some(mut pm) => {
1558            if (pm.node.flags as u32 & PM_AUTOLOAD) == 0 {
1559                // c:1242 — else if (!(pm->node.flags & PM_AUTOLOAD))
1560                if (flags & FEAT_IGNORE as i32) == 0 {
1561                    return 3; // c:1244
1562                }
1563            } else {
1564                // c:1246 — else unsetparam_pm(pm, 0, 1);
1565                unsetparam_pm(&mut pm, 0, 1);
1566            }
1567        }
1568    }
1569    0 // c:1248
1570}
1571
1572impl modulestab {
1573    /// `new` — see implementation.
1574    pub fn new() -> Self {
1575        let mut table = Self {
1576            // `Src/init.c:1193` — `modulestab = newmoduletable(17, "modules")`.
1577            modules: crate::ported::hashtable::hashtable_nodes::newhashtable(17),
1578            ..Self::default()
1579        };
1580        table.register_builtin_modules();
1581        table
1582    }
1583
1584    /// Register all statically-compiled modules (replaces dlopen)
1585    fn register_builtin_modules(&mut self) {
1586        let builtin_modules = [
1587            // Module→features MUST match the C BUILTIN() homes or a
1588            // builtin's feature lookup (`b:<name>` against its home module)
1589            // fails with "module `X' has no such feature". These were
1590            // SCRAMBLED: compadd/compset (complete.c:1693-1694) were under
1591            // zsh/computil, while the computil builtins (computil.c:5131-
1592            // 5138) were under zsh/complete, so calling `compset` from a
1593            // shell fn errored `module 'zsh/complete' has no such feature`.
1594            ("zsh/complete", &["compadd", "compset"][..]), // complete.c:1693-1694
1595            ("zsh/complist", &["complist"][..]),
1596            (
1597                "zsh/computil",
1598                &[
1599                    "comparguments", // computil.c:5131
1600                    "compdescribe",  // c:5132
1601                    "compfiles",     // c:5133
1602                    "compgroups",    // c:5134
1603                    "compquote",     // c:5135
1604                    "comptags",      // c:5136
1605                    "comptry",       // c:5137
1606                    "compvalues",    // c:5138
1607                ][..],
1608            ),
1609            ("zsh/datetime", &["output_strftime"][..]),
1610            (
1611                "zsh/files",
1612                &[
1613                    "mkdir", "rmdir", "ln", "mv", "cp", "rm", "chmod", "chown", "sync",
1614                ][..],
1615            ),
1616            ("zsh/langinfo", &[][..]),
1617            // c:Src/Modules/hlgroup.mdd — `link=either`, no builtins; its
1618            // features are the `.zle.esc` / `.zle.sgr` special hashes.
1619            ("zsh/hlgroup", &[][..]),
1620            // c:Src/Modules/ksh93.mdd — `link=either`, `moddeps="zsh/zle"`;
1621            // ksh93.c:40-42 bintab is `nameref`.
1622            ("zsh/ksh93", &["nameref"][..]),
1623            ("zsh/mapfile", &[][..]),
1624            ("zsh/mathfunc", &[][..]),
1625            ("zsh/nearcolor", &[][..]),
1626            ("zsh/net/socket", &["zsocket"][..]),
1627            ("zsh/net/tcp", &["ztcp"][..]),
1628            ("zsh/parameter", &[][..]),
1629            (
1630                "zsh/pcre",
1631                &["pcre_compile", "pcre_match", "pcre_study"][..],
1632            ),
1633            // c:Src/Modules/random.mdd — `link=either`, no builtins.
1634            ("zsh/random", &[][..]),
1635            ("zsh/regex", &[][..]),
1636            ("zsh/sched", &["sched"][..]),
1637            ("zsh/stat", &["zstat"][..]),
1638            (
1639                "zsh/system",
1640                &[
1641                    "bin_sysread",
1642                    "bin_syswrite",
1643                    "bin_sysopen",
1644                    "bin_sysseek",
1645                    "bin_syserror",
1646                    "zsystem",
1647                ][..],
1648            ),
1649            ("zsh/termcap", &["echotc"][..]),
1650            ("zsh/terminfo", &["echoti"][..]),
1651            ("zsh/watch", &["log"][..]),
1652            ("zsh/zftp", &["zftp"][..]),
1653            ("zsh/zleparameter", &[][..]),
1654            ("zsh/zprof", &["zprof"][..]),
1655            ("zsh/zpty", &["zpty"][..]),
1656            ("zsh/zselect", &["zselect"][..]),
1657            (
1658                "zsh/zutil",
1659                &["zstyle", "zformat", "zparseopts", "zregexparse"][..],
1660            ),
1661            (
1662                "zsh/attr",
1663                &["zgetattr", "zsetattr", "zdelattr", "zlistattr"][..],
1664            ),
1665            ("zsh/cap", &["cap", "getcap", "setcap"][..]),
1666            ("zsh/clone", &["clone"][..]),
1667            ("zsh/curses", &["zcurses"][..]),
1668            // c:Src/Modules/db_gdbm.mdd — `link='if test "x$enable_gdbm"
1669            // = xyes; then echo dynamic; else echo no; fi'`. Without the
1670            // gdbm backend the module is not built at all, so
1671            // `zmodload zsh/db/gdbm` fails ("failed to load module").
1672            // The backend here is the `gdbm` cfg used by db_gdbm.rs.
1673            #[cfg(feature = "gdbm")]
1674            ("zsh/db/gdbm", &["ztie", "zuntie", "zgdbmpath"][..]),
1675            ("zsh/param/private", &["private"][..]),
1676            // c:Src/Modules/newuser.mdd — `link=dynamic`, no builtins;
1677            // its whole body is boot_ (newuser.c). newuser.rs ports it
1678            // and every lifecycle dispatch arm below names it, but it
1679            // was missing here, so `module_linked` said no and
1680            // `zmodload zsh/newuser` failed where zsh loads it.
1681            ("zsh/newuser", &[][..]),
1682            // c:Src/Modules/compctl.c — statically-linked completion
1683            // module providing the compctl/compcall builtins. zsh
1684            // exposes it as autoloadable; `zmodload zsh/compctl`
1685            // succeeds on the running zsh because the symbol is
1686            // baked in. The zshrs auto-load registry (zsh_default_
1687            // loaded at line 1127) references it but the entry was
1688            // missing from this builtin_modules table, so
1689            // try_load_module returned 0 and zmodload failed with
1690            // "failed to load module `zsh/compctl'".
1691            ("zsh/compctl", &["compctl", "compcall"][..]),
1692            // c:Src/Builtins/rlimits.c — limit/ulimit/unlimit are
1693            // baked in. Same gap as zsh/compctl above.
1694            ("zsh/rlimits", &["limit", "ulimit", "unlimit"][..]),
1695            // c:Src/Zle/zle_main.c — zle/vared/bindkey baked in.
1696            ("zsh/zle", &["zle", "vared", "bindkey"][..]),
1697            // c:Src/Modules/example.c — example module that prints
1698            // "The example module has now been set up." on boot;
1699            // statically linked so `zmodload zsh/example` succeeds.
1700            ("zsh/example", &["example"][..]),
1701            // c:Src/Zle/deltochar.mdd — `link=dynamic`, `moddeps="zsh/zle"`,
1702            // no builtins (its features table is empty, deltochar.c:79-85);
1703            // boot_ adds the delete-to-char / zap-to-char widgets. A stock
1704            // zsh only fails to dlopen it when zsh/zle is not loaded yet
1705            // (the bundle needs zle's `bindk`); V01zmodload declares the
1706            // dependency with `zmodload -d` and loads it like every other
1707            // module in config.modules.
1708            ("zsh/deltochar", &[][..]),
1709            // NOT registered (zsh -fc parity probes confirm these
1710            // FAIL to load on this system because the dynamic
1711            // .bundle file doesn't exist):
1712            //   zsh/calendar      — pure dynamic module, no static
1713            //                       linkage in upstream zsh build.
1714            //   zsh/db_gdbm       — underscore alias for zsh/db/gdbm;
1715            //                       on the system zsh tested, neither
1716            //                       form loads (dlopen "no such file").
1717            //   zsh/compwid       — compwid bundle missing.
1718            // Letting zshrs zmodload succeed on these names would
1719            // diverge from `zsh -fc` which reports the dlopen error.
1720            // The names appear above only via try_load_module's
1721            // negative path (returns 0 → zmodload prints "failed to
1722            // load module").
1723        ];
1724
1725        // c:Src/init.c::init_bltinmods — C zsh's bltinmods.list is
1726        // generated at build time from Config/installmodules + the
1727        // active Modules/*.mdd files. Homebrew's stock zsh 5.9.1
1728        // binary ships these 14 modules in modulestab from the
1729        // start (verified via `${(@k)modules}` under `zsh -fc`):
1730        //   zsh/compctl zsh/complete zsh/computil zsh/main
1731        //   zsh/param/private zsh/parameter zsh/rlimits zsh/sched
1732        //   zsh/termcap zsh/terminfo zsh/watch zsh/zle
1733        //   zsh/zleparameter zsh/zutil
1734        // The rest (zsh/system, zsh/stat, zsh/zftp, zsh/zpty,
1735        // zsh/zselect, zsh/files, zsh/mapfile, etc.) require
1736        // explicit `zmodload NAME` before `${modules[NAME]}`
1737        // returns "loaded". All entries are registered into
1738        // modulestab so `zmodload NAME` can resolve them, but
1739        // entries OUTSIDE the default-loaded set carry `MOD_UNLOAD`
1740        // at init so `getpmmodule`'s is_loaded() check
1741        // (MOD_LINKED && !MOD_UNLOAD per zsh_h::is_loaded c:962)
1742        // returns false until `zmodload` clears the MOD_UNLOAD bit.
1743        // Bugs #530/#532/#535 in docs/BUGS.md.
1744        let zsh_default_loaded: &[&str] = &[
1745            "zsh/compctl",
1746            "zsh/complete",
1747            "zsh/computil",
1748            "zsh/main",
1749            "zsh/param/private",
1750            "zsh/parameter",
1751            "zsh/rlimits",
1752            "zsh/sched",
1753            "zsh/termcap",
1754            "zsh/terminfo",
1755            "zsh/watch",
1756            "zsh/zle",
1757            "zsh/zleparameter",
1758            "zsh/zutil",
1759        ];
1760        // c:Src/module.c:359-379 `register_module` — every compiled-in
1761        // module is appended to `linkedmodules` and NOTHING ELSE. C's
1762        // `modulestab` starts EMPTY and is filled lazily; the node set
1763        // it ends up with at the end of `main()` is decided entirely by
1764        // the generated `bltinmods.list` replayed below.
1765        //
1766        // Inserting all ~40 known modules as `modulestab` nodes here
1767        // (the previous shape) put `ct` past `hsize * 2` and quadrupled
1768        // the table from C's 17 buckets to 68, so `${(k)modules}` —
1769        // which is a raw bucket walk (`Src/Modules/parameter.c:1102`)
1770        // — listed loaded modules in an order that could not match zsh
1771        // for any collision: `zmodload zsh/system; zmodload zsh/zle`
1772        // printed `zle, system` where zsh prints `system, zle`
1773        // (bucket 6 vs buckets 23/57).
1774        for (name, _builtins) in &builtin_modules {
1775            // C zsh tracks builtin→module mapping in `builtintab` (the
1776            // canonical hashtable), not on a per-module ledger. We
1777            // just record the module as linked here; the builtins
1778            // themselves come in via the canonical table in `cmd.rs`.
1779            self.linkedmodules.push((*name).to_string()); // c:378
1780        }
1781        // `zsh/main` is the `link=static` module `Src/mkbltnmlst.sh:107`
1782        // emits a `register_module("zsh/main", …)` call for; it is not
1783        // in the autoloadable table above.
1784        self.linkedmodules.push("zsh/main".to_string()); // c:378
1785
1786        // c:Src/init.c:1739 `#include "bltinmods.list"` — the generated
1787        // boot code, replayed in order. `Src/mkbltnmlst.sh` walks
1788        // `config.modules` and emits, for every `load=yes` module:
1789        //   * `autofeatures("zsh", MODULE, features, 0, 1)` when the
1790        //     `.mdd` has an `autofeatures=` line (mkbltnmlst.sh:44-70);
1791        //     `autofeatures` opens with
1792        //     `find_module(module, FINDMOD_ALIASP|FINDMOD_CREATE, NULL)`
1793        //     (c:3449) — THAT is what creates the module's `modulestab`
1794        //     node.
1795        //   * one `add_dep(MODULE, DEP)` per `moddeps=` entry
1796        //     (mkbltnmlst.sh:71-73); `add_dep` also opens with
1797        //     `find_module(name, …|FINDMOD_CREATE, &name)` (c:2390), so
1798        //     a module with deps but no autofeatures (`zsh/complist`)
1799        //     still gets a node.
1800        // then a second pass (mkbltnmlst.sh:83-105) emits `add_dep` for
1801        // `link=dynamic load=no` modules that declare `moddeps`.
1802        //
1803        // ORDER IS LOAD-BEARING: `addhashnode` front-inserts, so the
1804        // chain order inside a bucket is reverse creation order, and
1805        // `${(k)modules}` shows it. The sequence below is
1806        // `config.modules` order (Src/Builtins, then Src/Modules, then
1807        // Src/Zle, then Src/zsh.mdd), verified against the oracle:
1808        // `zsh -f -c 'zmodload <all 15>; print -rl -- ${(k)modules}'`
1809        // pins terminfo-before-watch, parameter-before-computil,
1810        // zle-before-zleparameter and rlimits-before-param/private.
1811        //
1812        // The resulting node count is 17 (16 here + `zsh/main` below),
1813        // well under the `ct >= 34` re-hash of the 17-bucket table.
1814        //
1815        // `zsh/hlgroup` and `zsh/ksh93` carry `load=yes` in the reference
1816        // tree's config.modules (lines 39-40) and boot here like the rest;
1817        // zsh 5.9.2 has neither, so its `${#modules}` / `${#parameters}`
1818        // rows are two modules short of the fork's.
1819        //
1820        // The `autofeatures` column below is each `.mdd`'s
1821        // `autofeatures=` line VERBATIM — that string is what
1822        // `mkbltnmlst.sh:63-69` bakes into the generated
1823        // `char *features[] = { … }` array, and `autofeatures("zsh",
1824        // MODULE, features, 0, 1)` (defflags 1 = FEAT_IGNORE, c:62) is
1825        // what registers the `b:`/`c:`/`p:`/`f:` autoloads. Replaying
1826        // only the `find_module` half left `zmodload -ac` and
1827        // `zmodload -ap` printing nothing where zsh lists four
1828        // conditions and forty parameters.
1829        let bltinmods_list: &[(&str, &[&str], &[&str])] = &[
1830            // (module, `autofeatures=`, `moddeps=`)
1831            // Src/Builtins/rlimits.mdd:5
1832            (
1833                "zsh/rlimits",
1834                &["b:limit", "b:ulimit", "b:unlimit"][..],
1835                &[][..],
1836            ),
1837            // Src/Builtins/sched.mdd:5
1838            (
1839                "zsh/sched",
1840                &["b:sched", "p:zsh_scheduled_events"][..],
1841                &[][..],
1842            ),
1843            // Src/Modules/hlgroup.mdd:5
1844            ("zsh/hlgroup", &["p:.zle.esc", "p:.zle.sgr"][..], &[][..]),
1845            // Src/Modules/ksh93.mdd:5,9
1846            ("zsh/ksh93", &["b:nameref"][..], &["zsh/zle"][..]),
1847            // Src/Modules/param_private.mdd:5
1848            ("zsh/param/private", &["b:private"][..], &[][..]),
1849            // Src/Modules/parameter.mdd:5
1850            (
1851                "zsh/parameter",
1852                &[
1853                    "p:parameters",
1854                    "p:commands",
1855                    "p:functions",
1856                    "p:dis_functions",
1857                    "p:functions_source",
1858                    "p:dis_functions_source",
1859                    "p:funcfiletrace",
1860                    "p:funcsourcetrace",
1861                    "p:funcstack",
1862                    "p:functrace",
1863                    "p:builtins",
1864                    "p:dis_builtins",
1865                    "p:reswords",
1866                    "p:dis_reswords",
1867                    "p:patchars",
1868                    "p:dis_patchars",
1869                    "p:options",
1870                    "p:modules",
1871                    "p:dirstack",
1872                    "p:history",
1873                    "p:historywords",
1874                    "p:jobtexts",
1875                    "p:jobdirs",
1876                    "p:jobstates",
1877                    "p:nameddirs",
1878                    "p:userdirs",
1879                    "p:usergroups",
1880                    "p:aliases",
1881                    "p:dis_aliases",
1882                    "p:galiases",
1883                    "p:dis_galiases",
1884                    "p:saliases",
1885                    "p:dis_saliases",
1886                ][..],
1887                &[][..],
1888            ),
1889            // Src/Modules/random.mdd:5
1890            (
1891                "zsh/random",
1892                &["p:SRANDOM", "f:zrand_float", "f:zrand_int"][..],
1893                &[][..],
1894            ),
1895            // Src/Modules/termcap.mdd:15
1896            ("zsh/termcap", &["b:echotc", "p:termcap"][..], &[][..]),
1897            // Src/Modules/terminfo.mdd:15
1898            ("zsh/terminfo", &["b:echoti", "p:terminfo"][..], &[][..]),
1899            // Src/Modules/watch.mdd:5
1900            ("zsh/watch", &["b:log", "p:WATCH", "p:watch"][..], &[][..]),
1901            // Src/Modules/zutil.mdd:9
1902            (
1903                "zsh/zutil",
1904                &["b:zformat", "b:zstyle", "b:zregexparse", "b:zparseopts"][..],
1905                &["zsh/complete"][..],
1906            ),
1907            // Src/Zle/compctl.mdd:7
1908            (
1909                "zsh/compctl",
1910                &["b:compctl", "b:compcall"][..],
1911                &["zsh/complete", "zsh/zle"][..],
1912            ),
1913            // Src/Zle/complete.mdd:8 — the only `c:` autofeatures in the
1914            // boot set, and the source of `zmodload -ac`'s four rows.
1915            (
1916                "zsh/complete",
1917                &[
1918                    "b:compadd",
1919                    "b:compset",
1920                    "c:prefix",
1921                    "c:suffix",
1922                    "c:between",
1923                    "c:after",
1924                ][..],
1925                &["zsh/zle"][..],
1926            ),
1927            // No `autofeatures=`; the node comes from `add_dep` alone.
1928            ("zsh/complist", &[][..], &["zsh/complete", "zsh/zle"][..]), // Src/Zle/complist.mdd
1929            // Src/Zle/computil.mdd:9
1930            (
1931                "zsh/computil",
1932                &[
1933                    "b:compdescribe",
1934                    "b:comparguments",
1935                    "b:compvalues",
1936                    "b:compquote",
1937                    "b:comptags",
1938                    "b:comptry",
1939                    "b:compfiles",
1940                    "b:compgroups",
1941                ][..],
1942                &["zsh/complete", "zsh/zle"][..],
1943            ),
1944            // Src/Zle/zle.mdd:6
1945            ("zsh/zle", &["b:bindkey", "b:vared", "b:zle"][..], &[][..]),
1946            // Src/Zle/zleparameter.mdd:7
1947            (
1948                "zsh/zleparameter",
1949                &["p:widgets", "p:keymaps"][..],
1950                &["zsh/zle"][..],
1951            ),
1952            // mkbltnmlst.sh:83-105 second pass — `link=dynamic
1953            // load=no` with `moddeps`. `zsh/deltochar` is the only
1954            // other module in that shape and does NOT appear: the
1955            // grep is `' link=dynamic .* load=no '` with a TRAILING
1956            // space, and deltochar's `config.modules` line ends at
1957            // `load=no` (it declares no `functions=`), so it never
1958            // matches. Verified on the oracle: `zmodload -d` lists
1959            // seven modules and deltochar is not one of them.
1960            // zftp.mdd DOES carry `autofeatures="b:zftp"`, but it is
1961            // `load=no`, so mkbltnmlst.sh's second pass emits ONLY the
1962            // `add_dep` — no `autofeatures()` call, which is why
1963            // `zmodload -a` on the oracle has no `zftp` row.
1964            ("zsh/zftp", &[][..], &["zsh/net/tcp"][..]), // Src/Modules/zftp.mdd
1965        ];
1966        for (name, autofeature_list, deps) in bltinmods_list {
1967            if !autofeature_list.is_empty() {
1968                // c:3449 — autofeatures() opens with
1969                //   find_module(module, FINDMOD_ALIASP|FINDMOD_CREATE, NULL)
1970                // which is what creates the module's modulestab node.
1971                // The feature registration itself is replayed after the
1972                // whole list has its nodes + MOD_LINKED flags (see the
1973                // second pass below) — `autofeatures` reads
1974                // `MOD_INIT_B`/the feature tables off the node it is
1975                // registering against, so the node has to exist first.
1976                find_module(self, name, FINDMOD_CREATE);
1977            }
1978            for dep in *deps {
1979                add_dep(self, name, dep); // c:2390 (via c:3449's sibling)
1980            }
1981            // WARNING: RUST-ONLY — C's `find_module` node is
1982            // `zshcalloc`ed with flags 0 and a NULL union, so
1983            // `module_loaded` reads 0 for every one of these until a
1984            // real `zmodload`. zshrs's static-link mirror instead uses
1985            // `MOD_LINKED` as the "this name has backing code" bit
1986            // (`zsh_h::module::is_loaded`) and `MOD_UNLOAD` as the
1987            // "not live yet" sentinel, and a long tail of gates in
1988            // `subst.rs` / `params.rs` / `fusevm_bridge.rs` read those
1989            // two bits. Keep the flags each of these nodes carried
1990            // before the boot set was trimmed, so this change moves
1991            // ONLY the bucket layout.
1992            if let Some(m) = self.modules.get_mut(name) {
1993                m.node.flags |= MOD_LINKED;
1994                if !zsh_default_loaded.contains(name) {
1995                    m.node.flags |= crate::ported::zsh_h::MOD_UNLOAD;
1996                }
1997            }
1998        }
1999
2000        // c:Src/init.c:1708 init_bltinmods — run per-module `boot_`
2001        // for each statically-linked default-loaded module so paramtab
2002        // entries (e.g. `watch`/`WATCH` from zsh/watch, c:734) get
2003        // installed without an explicit zmodload. Without this,
2004        // `${(t)watch}` returns empty instead of `array-special`
2005        // even though zsh treats zsh/watch as effectively part of
2006        // the shell. Bug #270 in docs/BUGS.md. Keep the modules at
2007        // their MOD_LINKED-without-MOD_INIT_B initial state so the
2008        // `zmodload` (no-args) listing — gated on MOD_INIT_B per
2009        // bug #76 — still shows only `zsh/main`.
2010        for name in zsh_default_loaded {
2011            #[allow(clippy::single_match)]
2012            match *name {
2013                "zsh/watch" => {
2014                    // `Src/Modules/watch.mdd` is `link=dynamic` with
2015                    // `autofeatures="b:log p:WATCH p:watch"`, so real zsh
2016                    // registers ONLY those three names up front and defers
2017                    // `boot_` until the module is actually loaded. C's
2018                    // `boot_` is the sole creator of WATCHFMT/LOGCHECK:
2019                    //
2020                    //   c:Src/Modules/watch.c:756-759
2021                    //     if (!paramtab->getnode(paramtab, "WATCHFMT"))
2022                    //         setsparam("WATCHFMT", ztrdup_metafy(default_watchfmt));
2023                    //     if (!paramtab->getnode(paramtab, "LOGCHECK"))
2024                    //         setiparam("LOGCHECK", 60);
2025                    //
2026                    // and it runs only from `load_module`
2027                    // (c:Src/module.c:2306). Verified against the oracle:
2028                    // `zsh -f -c 'print ${WATCHFMT-UNSET}'` prints UNSET and
2029                    // neither name appears in `${(ko)parameters}`, while
2030                    // `zsh -f -c 'zmodload zsh/watch; print $WATCHFMT'`
2031                    // prints `%n has %a %l from %m.` — the same is true for
2032                    // any first touch of `$WATCH`/`$watch`/`log`, which is
2033                    // what triggers the autoload.
2034                    //
2035                    // This call is a zshrs-only shim: the reason it exists
2036                    // is the `watch`/`WATCH` paramtab registration that C
2037                    // gets from the autofeature list, so `${(t)watch}`
2038                    // reads `array-special` from the first prompt (bug #270
2039                    // in docs/BUGS.md). Its WATCHFMT/LOGCHECK side effect is
2040                    // NOT part of that contract, so undo it for names the
2041                    // shim itself invented. The condition is "was zsh/watch
2042                    // really loaded", exactly as in C — NOT the shell's
2043                    // emulation mode: the previous `--zsh`-only gate inside
2044                    // `watch::boot_` left both names in `${(ko)parameters}`
2045                    // for the native binary. A pre-existing value (exported
2046                    // WATCHFMT, or a real `zmodload zsh/watch` later on)
2047                    // is left untouched.
2048                    // `boot_` no longer seeds WATCHFMT/LOGCHECK when `m` is
2049                    // null (watch.rs's `mid_load` gate), so there is nothing
2050                    // left to undo here.
2051                    crate::ported::modules::watch::boot_(std::ptr::null());
2052                }
2053                // c:Src/Zle/compctl.c:4016 setup_ — seed the hardwired
2054                // default compctls (cc_compos/cc_default/cc_first) the
2055                // bare shell uses for command/file completion. Runs here
2056                // at module registration (before any rc file), so it
2057                // never clobbers a user's later `compctl` definitions.
2058                // Without it CC_COMPOS is unset and `l<Tab>` in `zsh -f`
2059                // produced no matches.
2060                "zsh/compctl" => {
2061                    crate::ported::zle::compctl::setup_();
2062                }
2063                // NOTE: `zsh/zle` deliberately has NO arm here. `setup_` is
2064                // the zle module's boot function, so C reaches it only from
2065                // the LOAD chain (`setup_module`, c:1884), never from static
2066                // registration — this table is the boot-time
2067                // `autofeatures` replay, which registers a node without
2068                // loading anything. Calling it here made
2069                // `$zle_bracketed_paste` (c:Src/Zle/zle_main.c:2276-2280)
2070                // exist in every shell, including one that never enters the
2071                // editor, where zsh leaves it unset:
2072                //     zsh -f -c 'print ${+zle_bracketed_paste}'   -> 0
2073                // The arm lives in `setup_module` instead, beside the other
2074                // per-module `setup_` dispatch.
2075                _ => {}
2076            }
2077        }
2078
2079        // c:Src/init.c:1739 `#include "bltinmods.list"` — the
2080        // `autofeatures("zsh", MODULE, features, 0, 1)` half, replayed in
2081        // `config.modules` order. `mkbltnmlst.sh:62-70` wraps each call in
2082        // `if (EMULATION(EMULATE_ZSH))`, and every boot module except
2083        // `zsh/rlimits` / `zsh/ksh93` (the two with `autofeatures_emu=`)
2084        // registers NOTHING outside zsh emulation. zshrs's emulation is
2085        // not settled at `modulestab::new()` time — this runs from
2086        // `init_bltinmods`, before `apply_cli_flags`' `emulate("sh")` —
2087        // so the zsh arm is the one replayed; a `--sh`/`--bash` process
2088        // will carry the zsh feature set, matching what zshrs already
2089        // does for the builtin ledger this replaces.
2090        //
2091        // `prefchar = 0` (the features carry their own `b:`/`c:`/`p:`
2092        // type prefix) and `defflags = 1` = FEAT_IGNORE (c:62), which is
2093        // what lets a name the static link already provides — every one
2094        // of the 27 `b:` builtins, and the eagerly-seeded `p:` specials
2095        // (`vm_helper::init_partab_params`) — register its ledger entry
2096        // without an "already defined" diagnostic.
2097        //
2098        // ORDERING (Rust-only): this runs AFTER the `zsh_default_loaded`
2099        // `boot_` shim loop above, not interleaved with node creation as
2100        // `bltinmods.list` has it. That loop is itself a zshrs-only shim
2101        // (see its comment) which installs `zsh/watch`'s `watch`/`WATCH`
2102        // specials eagerly, the way `init_partab_params` installs the
2103        // `partab[]` ones. Running the replay first made `add_autoparam`
2104        // (c:1197) win the race and leave a live PM_AUTOLOAD SCALAR stub
2105        // whose value is the module name, so `${#watch}` read 9
2106        // ("zsh/watch") instead of 0. With the specials already in
2107        // paramtab, `checkaddparam` (c:1026) returns 2 under FEAT_IGNORE
2108        // and `add_autoparam` is the no-op C reaches once a module is
2109        // loaded — while the ledger entry `autofeatures` records is what
2110        // `zmodload -ap` lists.
2111        for (name, autofeature_list, _deps) in bltinmods_list {
2112            if autofeature_list.is_empty() {
2113                continue;
2114            }
2115            // c:mkbltnmlst.sh:62-70 — zsh/hlgroup's autofeatures are the
2116            // dotted `.zle.esc` / `.zle.sgr`, which are not identifiers under
2117            // sh/bash emulation: add_autoparam's setsparam fails `isident`
2118            // and leaves errflag set (c:Src/params.c:3203-3207), so the shell
2119            // ran nothing at all. C never gets there — the call is inside
2120            // `if (EMULATION(EMULATE_ZSH))` — and the emulation is already
2121            // installed when this replay runs, so honour that gate here.
2122            if *name == "zsh/hlgroup"
2123                && !crate::ported::zsh_h::EMULATION(crate::ported::zsh_h::EMULATE_ZSH)
2124            {
2125                continue;
2126            }
2127            let features: Vec<String> = autofeature_list.iter().map(|f| f.to_string()).collect();
2128            // c:3440 `autofeatures(cmdnam, module, features, prefchar,
2129            //          defflags)` — cmdnam "zsh" per mkbltnmlst.sh:69.
2130            autofeatures(self, "zsh", Some(name), &features, 0, FEAT_IGNORE);
2131        }
2132
2133        // The auto-load builtin→module bindings `zmodload -a` reports
2134        // used to be a hand-maintained 27-row `autoload_pairs` table
2135        // here. They are now produced by the `b:` half of the
2136        // `autofeatures` replay above, from each `.mdd`'s own
2137        // `autofeatures=` line — one source of truth instead of two.
2138        // The set is unchanged: rlimits 3 + sched 1 + param/private 1 +
2139        // termcap 1 + terminfo 1 + watch 1 + zutil 4 + compctl 2 +
2140        // complete 2 + computil 8 + zle 3 = 27, matching
2141        // `/opt/homebrew/bin/zsh -fc 'zmodload -a'` exactly. `zsh/files`
2142        // builtins (mkdir, rm, …) are statically linked but carry no
2143        // boot autofeatures entry (files.mdd is `load=no`), so they stay
2144        // out of the registry — zsh requires an explicit
2145        // `zmodload zsh/files`. Bug #222.
2146
2147        // c:Src/init.c:1708 — `init_bltinmods` ends with
2148        // `load_module("zsh/main", NULL, 0)`. `zsh/main` is the
2149        // always-loaded master module: every zsh process has it in
2150        // `modulestab` from boot, with `m->u.handle` (or `u.linked`)
2151        // non-NULL so `printmodulenode`'s "loaded" gate (c:218
2152        // `m->u.handle`) fires. Register here with `MOD_INIT_B` set
2153        // so `zmodload` (no args) lists `zsh/main` and ONLY `zsh/main`
2154        // — matching `/opt/homebrew/bin/zsh -fc 'zmodload'` output
2155        // exactly. Bug #76 in docs/BUGS.md.
2156        let mut main = module::new("zsh/main");
2157        main.node.flags |= crate::ported::zsh_h::MOD_INIT_B | MOD_INIT_S; // c:2244
2158                                                                          // c:2289 — load_module("zsh/main") assigns `m->u.linked`; the
2159                                                                          // union must be non-NULL so `zmodload -e zsh/main` (c:2637
2160                                                                          // `!m->u.handle`) reports the master module as loaded.
2161        main.linked = Some(Box::new(linkedmod {
2162            name: "zsh/main".to_string(),
2163            setup: None,
2164            features: None,
2165            enables: None,
2166            boot: None,
2167            cleanup: None,
2168            finish: None,
2169        }));
2170        self.modules.insert("zsh/main".to_string(), main);
2171    }
2172
2173    // Returns 0 success, 1 complete failure, 2 partial-features-fail.     // c:2200-2201
2174    /// Port of `int load_module(char const *name, Feature_enables
2175    /// enablesarr, int silent)` from `Src/module.c:2206`. C body:
2176    /// validate name with `modname_ok`, queue_signals(), resolve alias
2177    /// chain via `find_module(FINDMOD_ALIASP)`. If not found, attempt
2178    /// `module_linked` then `do_load_module` (dlopen). Allocate a new
2179    /// `Module`, set `MOD_SETUP` (+ `MOD_LINKED` if statically linked),
2180    /// add to `modulestab`, run `setup_module`/`do_boot_module`. If
2181    /// either fails, cleanup+finish+delete and return 1. Else clear
2182    /// `MOD_SETUP`, set `MOD_INIT_S | MOD_INIT_B`, return `bootret`.
2183    /// If found and already `MOD_SETUP`, return 0. Detect circular
2184    /// deps via `MOD_BUSY`, load dependency list recursively. zshrs:
2185    /// all modules are statically linked, so dlopen path is skipped
2186    /// and we operate on the static registry.
2187    /// `enablesarr` is C's `Feature_enables` argument, threaded through
2188    /// to `do_boot_module` (c:2306) so a `require_module` that asks for ONE
2189    /// feature enables only that feature — the port used to drop the
2190    /// argument and enable every feature the module has.
2191    /// `enablesarr` is C's `Feature_enables` argument, threaded through to
2192    /// `do_boot_module` (c:2306) so a `require_module` that asks for ONE
2193    /// feature enables only that feature. The port used to drop the
2194    /// argument and pass `None` (= enable everything), so the first
2195    /// `[[ -prefix … ]]` installed all four of `zsh/complete`'s conddefs
2196    /// where zsh installs only `prefix`.
2197    /// WARNING: param names don't match C — Rust=(name, enablesarr) vs C=(name, enablesarr, silent)
2198    pub fn load_module(
2199        &mut self,
2200        name: &str,
2201        enablesarr: Option<&[String]>,
2202        // !!! RUST-ONLY PARAM !!! — carries C's per-entry
2203        // `Feature_enables.pat` (see FEAT_PATTERN_ARGS).
2204        feat_pat: bool,
2205    ) -> i32 {
2206        // c:2200
2207        // c:2201-2202 —
2208        //   Now returns 0 for success (changed post-4.3.4),
2209        //   1 for complete failure, 2 if some features couldn't be set.
2210        // The port used to return `bool`, which collapsed 2 onto 0: a
2211        // `zmodload zsh/datetime` whose `p:EPOCHSECONDS` could not be added
2212        // (local parameter in the way, or a global of the same name) printed
2213        // C's two diagnostics and then exited 0 instead of 2
2214        // (V04features.ztst "Failed to add parameter if local parameter
2215        // present" / "Loading won't override global parameter").
2216        // Faithful port of the find_module-found branch (c:2249-2320).
2217        // The !find_module branch (c:2219-2247) requires DSO loading and
2218        // never fires in zshrs's static-link path: every linked module
2219        // is pre-registered by register_builtin_modules.
2220        //
2221        // C body c:2249-2320:
2222        //   if (m->flags & MOD_SETUP) return 0;
2223        //   if (m->flags & MOD_UNLOAD) m->flags &= ~MOD_UNLOAD;
2224        //   else if (m->u.linked / m->u.handle) return 0;
2225        //   if (m->flags & MOD_BUSY) {
2226        //       zerr("circular dependencies for module ;%s", name);
2227        //       return 1;
2228        //   }
2229        //   m->flags |= MOD_BUSY;
2230        //   if (m->deps) for each dep: load_module(dep, NULL, silent);
2231        //   m->flags &= ~MOD_BUSY;
2232        //   if (!m->u.handle) {
2233        //       module_linked / do_load_module
2234        //       if (handle) m->u.handle = handle, m->flags |= MOD_SETUP
2235        //       else m->u.linked = linked, m->flags |= MOD_SETUP|MOD_LINKED
2236        //       if (setup_module(m)) { finish, NULL handles, ~SETUP, return 1; }
2237        //       m->flags |= MOD_INIT_S;
2238        //   }
2239        //   m->flags |= MOD_SETUP;
2240        //   if ((bootret = do_boot_module(m, enablesarr, silent)) == 1) {
2241        //       do_cleanup_module(m); finish_module(m);
2242        //       m->u.linked/handle = NULL;
2243        //       m->flags &= ~MOD_SETUP;
2244        //       return 1;
2245        //   }
2246        //   m->flags |= MOD_INIT_B;
2247        //   m->flags &= ~MOD_SETUP;
2248        //   return bootret;
2249
2250        // c:2208 — modname_ok(name)
2251        if modname_ok(name) == 0 {
2252            // c:2210 — zerr if !silent (silent flag not threaded yet)
2253            return 1;
2254        }
2255        crate::ported::signals::queue_signals(); // c:2222
2256                                                 // c:2223 — find_module(name, FINDMOD_ALIASP)
2257        if !self.modules.contains_key(name) {
2258            // c:2223-2251 — the allocate-on-miss branch. C reaches it
2259            // for every module whose `modulestab` node has not been
2260            // created yet, which is the NORMAL state: `bltinmods.list`
2261            // creates nodes for only 16 modules at boot, so the first
2262            // `zmodload zsh/system` (or any other module) lands here.
2263            //
2264            // c:2224-2225 —
2265            //   if (!(linked = module_linked(name)) &&
2266            //       !(handle = do_load_module(name, silent))) { return 1; }
2267            // zshrs compiles every module in, so `module_linked` is the
2268            // only arm that can succeed; `do_load_module` (dlopen) is
2269            // called for its failure path only. `silent = 1` because
2270            // `require_module` (c:2354's caller) has already emitted the
2271            // canonical `failed to load module` warning through its own
2272            // `try_load_module` gate — matching the pre-existing
2273            // behaviour where this branch returned quietly.
2274            if !self.module_linked(name) {
2275                let _ = do_load_module(self, name, 1); // c:2225
2276                unqueue_signals(); // c:2226
2277                return 1; // c:2227
2278            }
2279            // c:2229 — m = zshcalloc(sizeof(*m));
2280            let mut m = module::new(name);
2281            // c:2234-2235 — m->u.linked = linked;
2282            //               m->node.flags |= MOD_SETUP | MOD_LINKED;
2283            m.linked = Some(Box::new(linkedmod {
2284                name: name.to_string(),
2285                setup: None,
2286                features: None,
2287                enables: None,
2288                boot: None,
2289                cleanup: None,
2290                finish: None,
2291            }));
2292            m.node.flags = MOD_SETUP | MOD_LINKED;
2293            // c:2237 — modulestab->addnode(modulestab, ztrdup(name), m);
2294            self.modules.insert(name.to_string(), m);
2295
2296            // Same Rust-only paramtab re-seed the already-noded path
2297            // does below: C gets these SPECIALPMDEF entries from
2298            // `do_boot_module` → `enables_module` → `addparam`, which
2299            // zshrs services out of `PARTAB` instead.
2300            for nm in crate::vm_helper::module_gated_params_for(name) {
2301                crate::vm_helper::seed_partab_param(nm);
2302            }
2303
2304            // c:2239-2240 —
2305            //   if ((set = setup_module(m)) ||
2306            //       (bootret = do_boot_module(m, enablesarr, silent)) == 1)
2307            let set = setup_module(self, name);
2308            let bootret = if set == 0 {
2309                do_boot_module(self, name, enablesarr, 0, feat_pat)
2310            } else {
2311                1
2312            };
2313            if set != 0 || bootret == 1 {
2314                if set == 0 {
2315                    let _ = do_cleanup_module(self, name); // c:2242
2316                }
2317                let _ = finish_module(self, name); // c:2243
2318                delete_module(self, name); // c:2244
2319                unqueue_signals(); // c:2245
2320                return 1; // c:2246
2321            }
2322            // c:2248-2249 — m->node.flags |= MOD_INIT_S | MOD_INIT_B;
2323            //               m->node.flags &= ~MOD_SETUP;
2324            if let Some(m) = self.modules.get_mut(name) {
2325                m.node.flags |= MOD_INIT_S | MOD_INIT_B;
2326                m.node.flags &= !MOD_SETUP;
2327            }
2328            unqueue_signals(); // c:2250
2329            return bootret; // c:2251
2330        }
2331
2332        // c:2249 — if (MOD_SETUP) return 0;
2333        let flags = self.modules.get(name).unwrap().node.flags;
2334        if (flags & MOD_SETUP) != 0 {
2335            unqueue_signals(); // c:2250
2336            return 0; // c:2251
2337        }
2338        // c:2253-2257 —
2339        //   if (m->node.flags & MOD_UNLOAD)
2340        //       m->node.flags &= ~MOD_UNLOAD;
2341        //   else if ((m->node.flags & MOD_LINKED) ? m->u.linked
2342        //                                         : m->u.handle) {
2343        //       unqueue_signals();
2344        //       return 0;
2345        //   }
2346        // The union read is real now: load assigns m.linked at c:2289,
2347        // so already-loaded modules early-return here while
2348        // pre-registered-but-unloaded ones (linked: None) fall through.
2349        if (flags & MOD_UNLOAD) != 0 {
2350            self.modules.get_mut(name).unwrap().node.flags &= !MOD_UNLOAD;
2351        } else if {
2352            let m = self.modules.get(name).unwrap();
2353            if (flags & MOD_LINKED) != 0 {
2354                m.linked.is_some() // c:2255 m->u.linked
2355            } else {
2356                m.handle.is_some() // c:2255 m->u.handle
2357            }
2358        } {
2359            unqueue_signals(); // c:2256
2360            return 0; // c:2257
2361        }
2362        // c:2259-2262 — circular-dependency detection.
2363        if (flags & MOD_BUSY) != 0 {
2364            unqueue_signals(); // c:2260
2365            crate::ported::utils::zerr(&format!("circular dependencies for module ;{}", name));
2366            return 1; // c:2262
2367        }
2368        self.modules.get_mut(name).unwrap().node.flags |= MOD_BUSY; // c:2264
2369
2370        // c:2269-2277 — recurse into m->deps.
2371        let deps_snapshot: Vec<String> = self
2372            .modules
2373            .get(name)
2374            .and_then(|m| m.deps.as_ref())
2375            .map(|d| d.iter().cloned().collect())
2376            .unwrap_or_default();
2377        for dep in &deps_snapshot {
2378            // c:2272 — `if (load_module(…, NULL, silent) == 1)`: only a COMPLETE
2379            // dependency failure aborts; a status-2 dep (features partly set)
2380            // carries on.
2381            if self.load_module(dep, None, false) == 1 {
2382                self.modules.get_mut(name).unwrap().node.flags &= !MOD_BUSY; // c:2273
2383                unqueue_signals(); // c:2274
2384                return 1; // c:2275
2385            }
2386        }
2387        self.modules.get_mut(name).unwrap().node.flags &= !MOD_BUSY; // c:2278
2388
2389        // c:2279-2304 — `if (!m->u.handle)` setup branch. The union
2390        // read means "no live binding yet" — neither handle (DSO) nor
2391        // linked (static) assigned. A deferred-unload reload re-enters
2392        // with m.linked still set and skips straight to boot, like C.
2393        let needs_setup = {
2394            let m = self.modules.get(name).unwrap();
2395            m.handle.is_none() && m.linked.is_none() // c:2279 !m->u.handle
2396        };
2397        if needs_setup {
2398            // c:2281 — `linked = module_linked(name)`: every zshrs
2399            // module is statically linked, so the lookup always hits;
2400            // callbacks dispatch by name (839f32249b), the record
2401            // carries the name like C's linkedmod.
2402            // c:2289-2291 — `m->u.linked = linked;
2403            //                m->node.flags |= MOD_SETUP | MOD_LINKED;`
2404            if let Some(m) = self.modules.get_mut(name) {
2405                m.linked = Some(Box::new(linkedmod {
2406                    name: name.to_string(),
2407                    setup: None,
2408                    features: None,
2409                    enables: None,
2410                    boot: None,
2411                    cleanup: None,
2412                    finish: None,
2413                }));
2414                m.node.flags |= MOD_SETUP | MOD_LINKED;
2415            }
2416            // c:2293 — setup_module(m). Routes through the dispatcher
2417            // (839f32249b) to per-module setup_(m). Most modules return
2418            // 0; some initialise module-private state.
2419            if setup_module(self, name) != 0 {
2420                // c:2294-2301 — failure: finish, clear handles, return 1.
2421                //   else m->u.linked = NULL;  (c:2298)
2422                let _ = finish_module(self, name);
2423                if let Some(m) = self.modules.get_mut(name) {
2424                    m.linked = None; // c:2298
2425                    m.node.flags &= !MOD_SETUP;
2426                }
2427                unqueue_signals(); // c:2300
2428                return 1; // c:2301
2429            }
2430            // c:2303 — `m->flags |= MOD_INIT_S;`
2431            self.modules.get_mut(name).unwrap().node.flags |= MOD_INIT_S;
2432        }
2433        // c:2305 — `m->flags |= MOD_SETUP;`
2434        self.modules.get_mut(name).unwrap().node.flags |= MOD_SETUP;
2435
2436        // c:Src/Modules/system.c:902,904 + zsh/mapfile — SPECIALPMDEF
2437        // entries get added to paramtab via the module's feature
2438        // dispatch (enables_ → handlefeatures → addparam). zshrs's
2439        // vm_helper::init_partab_params skips zmodload-gated names to
2440        // avoid them appearing before explicit load (bug #69). Re-seed
2441        // here once boot completes. This is a Rust-only bridge — no
2442        // direct C counterpart since C's handlefeatures runs implicitly
2443        // inside do_boot_module → enables_module.
2444        for nm in crate::vm_helper::module_gated_params_for(name) {
2445            crate::vm_helper::seed_partab_param(nm);
2446        }
2447
2448        // c:2306 — `bootret = do_boot_module(m, enablesarr, silent);`
2449        // The Rust do_boot_module routes through boot_module dispatcher
2450        // (b474b62898) to the per-module boot_(m) — the real partab
2451        // and bintab installations land here.
2452        let bootret = do_boot_module(self, name, enablesarr, 0, feat_pat);
2453        if bootret == 1 {
2454            // c:2306-2315 — boot failure: cleanup + finish + clear, return 1.
2455            //   else m->u.linked = NULL;  (c:2312)
2456            let _ = cleanup_module(self, name);
2457            let _ = finish_module(self, name);
2458            if let Some(m) = self.modules.get_mut(name) {
2459                m.linked = None; // c:2312
2460                m.node.flags &= !MOD_SETUP;
2461            }
2462            unqueue_signals(); // c:2314
2463            return 1; // c:2315
2464        }
2465        // c:2317-2318 — `m->flags |= MOD_INIT_B; m->flags &= ~MOD_SETUP;`
2466        if let Some(m) = self.modules.get_mut(name) {
2467            m.node.flags |= MOD_INIT_B;
2468            m.node.flags &= !MOD_SETUP;
2469        }
2470        unqueue_signals(); // c:2319
2471        bootret // c:2320 return bootret
2472    }
2473
2474    // Backend handler for zmodload -u                                       // c:2813
2475    /// Port of `int unload_module(Module m)` from `Src/module.c:2817`.
2476    /// C body: resolve `MOD_ALIAS` via `find_module(FINDMOD_ALIASP)`;
2477    /// if `MOD_INIT_S` set and !`MOD_UNLOAD`, call `do_cleanup_module`.
2478    /// Clear `MOD_INIT_B|MOD_INIT_S`. If a `wrapper` is present, set
2479    /// `MOD_UNLOAD` and bail (deferred). Else clear `MOD_UNLOAD` and
2480    /// call `m->u.linked->finish(m)` or `finish_module(m)` depending
2481    /// on `MOD_LINKED`. Finally walk `m->deps` and unload modules
2482    /// that were tagged `MOD_UNLOAD` when the last dependent dies.
2483    /// WARNING: param names don't match C — Rust=(name) vs C=(m)
2484    pub fn unload_module(&mut self, name: &str) -> bool {
2485        // c:2812 — faithful port of the static-link control flow.
2486        // C body c:2818-2913:
2487        //   if (m->flags & MOD_ALIAS) { resolve via find_module }
2488        //   if (MOD_INIT_S && !MOD_UNLOAD && do_cleanup_module(m))
2489        //       return 1;
2490        //   m->flags &= ~(MOD_INIT_B | MOD_INIT_S);
2491        //   del = m->flags & MOD_UNLOAD;
2492        //   if (m->wrapper) { m->flags |= MOD_UNLOAD; return 0; }
2493        //   m->flags &= ~MOD_UNLOAD;
2494        //   if (MOD_LINKED) { if (u.linked) { u.linked->finish(m);
2495        //                                     u.linked = NULL; } }
2496        //   else { if (u.handle) { finish_module(m);
2497        //                          u.handle = NULL; } }
2498        //   if (del && m->deps) { /* unload deps flagged MOD_UNLOAD */ }
2499        //   if (m->autoloads && firstnode(m->autoloads))
2500        //       autofeatures("zsh", name, hlinklist2array(autoloads),
2501        //                    0, FEAT_IGNORE);
2502        //   else if (!m->deps) delete_module(m);
2503        //   return 0;
2504
2505        // c:2819-2823 — alias resolve. Mutate the lookup name, not
2506        // the live record (alias chases the target).
2507        let mut target_name = name.to_string();
2508        let needs_alias_chase = self
2509            .modules
2510            .get(&target_name)
2511            .map(|m| (m.node.flags & MOD_ALIAS) != 0)
2512            .unwrap_or(false);
2513        if needs_alias_chase {
2514            target_name = match self.modules.get(name).and_then(|m| m.alias.clone()) {
2515                Some(a) => a,
2516                None => return false, // c:2821-2822 — alias target gone.
2517            };
2518        }
2519
2520        // c:2831-2834 — cleanup if booted.
2521        let (init_s, unload_flag) = match self.modules.get(&target_name) {
2522            Some(m) => (
2523                (m.node.flags & MOD_INIT_S) != 0,
2524                (m.node.flags & MOD_UNLOAD) != 0,
2525            ),
2526            None => return false, // c:2820-2822 fall-through to !m
2527        };
2528        if init_s && !unload_flag {
2529            // c:2833 — `do_cleanup_module` is `cleanup_module` for the
2530            // MOD_LINKED branch (static-link path always).
2531            if cleanup_module(self, &target_name) != 0 {
2532                return false; // c:2834 cleanup error
2533            }
2534        }
2535
2536        // c:2835 — `m->flags &= ~(MOD_INIT_B | MOD_INIT_S);`
2537        if let Some(m) = self.modules.get_mut(&target_name) {
2538            m.node.flags &= !(MOD_INIT_B | MOD_INIT_S);
2539        }
2540
2541        // c:2837 — `del = m->flags & MOD_UNLOAD;`
2542        let del = unload_flag;
2543
2544        // c:2839-2842 — wrapper deferred-unload path.
2545        let has_wrapper = self
2546            .modules
2547            .get(&target_name)
2548            .map(|m| m.wrapper != 0)
2549            .unwrap_or(false);
2550        if has_wrapper {
2551            if let Some(m) = self.modules.get_mut(&target_name) {
2552                m.node.flags |= MOD_UNLOAD; // c:2840
2553            }
2554            return true; // c:2841
2555        }
2556
2557        // c:2843 — `m->flags &= ~MOD_UNLOAD;`
2558        if let Some(m) = self.modules.get_mut(&target_name) {
2559            m.node.flags &= !MOD_UNLOAD;
2560        }
2561
2562        // c:2849-2859 — finish hook:
2563        //   if (m->node.flags & MOD_LINKED) {
2564        //       if (m->u.linked) {
2565        //           m->u.linked->finish(m);
2566        //           m->u.linked = NULL;
2567        //       }
2568        //   }
2569        // Static-link branch routes through finish_module which
2570        // dispatches to the per-module finish_(m) (839f32249b), gated
2571        // and cleared exactly like C's u.linked.
2572        let was_linked = self
2573            .modules
2574            .get(&target_name)
2575            .map(|m| m.linked.is_some())
2576            .unwrap_or(false);
2577        if was_linked {
2578            let _ = finish_module(self, &target_name); // c:2851
2579            if let Some(m) = self.modules.get_mut(&target_name) {
2580                m.linked = None; // c:2852
2581            }
2582        }
2583
2584        // c:2861-2902 — dep walk: when del was set, find every
2585        // dep that has MOD_UNLOAD and check no other live module
2586        // depends on it, then recursively unload.
2587        if del {
2588            let deps_snapshot: Vec<String> = self
2589                .modules
2590                .get(&target_name)
2591                .and_then(|m| m.deps.as_ref())
2592                .map(|d| d.iter().cloned().collect())
2593                .unwrap_or_default();
2594            for dep_name in deps_snapshot {
2595                let dm_target = match find_module(self, &dep_name, FINDMOD_ALIASP) {
2596                    Some(n) => n,
2597                    None => continue, // c:2867 dm == NULL
2598                };
2599                let dm_unloading = self
2600                    .modules
2601                    .get(&dm_target)
2602                    .map(|m| (m.node.flags & MOD_UNLOAD) != 0)
2603                    .unwrap_or(false);
2604                if !dm_unloading {
2605                    continue; // c:2870-2871
2606                }
2607                // c:2872-2897 — scan every other module's deps for
2608                // dm_target.nam; bail if any live module still
2609                // depends on it.
2610                let still_depended = self.modules.iter().any(|(other_name, other)| {
2611                    if other_name == &target_name {
2612                        return false; // c:2884 don't scan ourselves
2613                    }
2614                    let other_deps = match other.deps.as_ref() {
2615                        Some(d) => d,
2616                        None => return false, // c:2884 no deps to scan
2617                    };
2618                    // c:2887-2889 — only scan live modules (MOD_LINKED
2619                    // set + !MOD_UNLOAD in zshrs's static-link path).
2620                    if (other.node.flags & MOD_LINKED) == 0 || (other.node.flags & MOD_UNLOAD) != 0
2621                    {
2622                        return false;
2623                    }
2624                    other_deps.iter().any(|d| d == &dm_target)
2625                });
2626                if !still_depended {
2627                    // c:2898-2899 — `unload_module(dm)` recursive.
2628                    self.unload_module(&dm_target);
2629                }
2630            }
2631        }
2632
2633        // c:2903-2912 — autoload restore OR delete_module.
2634        let (has_autoloads, has_deps, autoloads) = match self.modules.get(&target_name) {
2635            Some(m) => (
2636                m.autoloads.as_ref().map(|a| !a.is_empty()).unwrap_or(false),
2637                m.deps.is_some(),
2638                m.autoloads
2639                    .as_ref()
2640                    .map(|a| a.iter().cloned().collect::<Vec<_>>())
2641                    .unwrap_or_default(),
2642            ),
2643            None => (false, false, Vec::new()),
2644        };
2645        if has_autoloads {
2646            // c:2908-2909 — autofeatures("zsh", m->nam, ..., 0, FEAT_IGNORE)
2647            autofeatures(self, "zsh", Some(&target_name), &autoloads, 0, FEAT_IGNORE);
2648        } else if !has_deps {
2649            // c:2910-2911 — delete_module(m). Inline since the free fn
2650            // also calls remove + drop.
2651            self.modules.remove(&target_name);
2652        }
2653        true // c:2913 return 0
2654    }
2655
2656    /// Check if module is loaded
2657    pub fn is_loaded(&self, name: &str) -> bool {
2658        self.modules
2659            .get(name)
2660            .map(|m| m.is_loaded())
2661            .unwrap_or(false)
2662    }
2663
2664    /// Whether a module is BOUND — its setup_/boot_ has actually run.
2665    /// Mirrors the `zmodload -e` existence test (bin_zmodload_exist,
2666    /// c:Src/module.c:2637): the union slot `m->u.handle` is non-NULL
2667    /// (handle for dlopen, linked for a static module) and the module is
2668    /// not mid-unload. Distinct from `is_loaded()` / `module_loaded()`,
2669    /// which key off `MOD_LINKED` — a flag `register_builtin_modules`
2670    /// pre-seeds for every statically-compiled module at startup, so it
2671    /// is true before `zmodload` ever runs. Use this for "was the module
2672    /// actually loaded by the user" gates.
2673    pub fn is_bound(&self, name: &str) -> bool {
2674        self.modules
2675            .get(name)
2676            .map(|m| (m.handle.is_some() || m.linked.is_some()) && (m.node.flags & MOD_UNLOAD) == 0)
2677            .unwrap_or(false)
2678    }
2679
2680    /// List all loaded modules
2681    pub fn list_loaded(&self) -> Vec<&str> {
2682        self.modules
2683            .iter()
2684            .filter(|(_, m)| m.is_loaded())
2685            .map(|(name, _)| name.as_str())
2686            .collect()
2687    }
2688
2689    /// List all modules (including unloaded). Returns name + raw
2690    /// `MOD_*` flag bits — caller can inspect `MOD_UNLOAD` / `MOD_LINKED`
2691    /// directly (matches C, which exposes `m->node.flags`).
2692    pub fn list_all(&self) -> Vec<(&str, i32)> {
2693        self.modules
2694            .iter()
2695            .map(|(name, m)| (name.as_str(), m.node.flags))
2696            .collect()
2697    }
2698
2699    // ------- Builtin management (from module.c addbuiltin/deletebuiltin) -------
2700
2701    /// Register a builtin (from module.c addbuiltin)
2702    /// Port of `addbuiltin(Builtin b)` from `Src/module.c:409`.
2703    /// WARNING: param names don't match C — Rust=(name, module) vs C=(b)
2704    ///
2705    /// In C, this inserts the builtin into the canonical `builtintab`
2706    /// hashtable (Src/builtin.c). The real builtin registration lives
2707    /// in `cmd.rs::BUILTINTAB` and the routing fn here delegates to
2708    /// the free `addbuiltin` (c:303 port) so the canonical BINF_ADDED
2709    /// clash gate fires.
2710    ///
2711    /// Returns 0 on success, 1 on clash (per C's signature).
2712    pub fn addbuiltin(&mut self, name: &str, module: &str) -> i32 {
2713        // c:409
2714        // Construct a probe builtin matching what add_autobin /
2715        // setbuiltins build; route through the free addbuiltin which
2716        // probes createbuiltintable() for BINF_ADDED.
2717        let mut bn = builtin {
2718            node: hashnode {
2719                next: None,
2720                nam: name.to_string(),
2721                flags: 0,
2722            },
2723            handlerfunc: None,
2724            minargs: 0,
2725            maxargs: 0,
2726            funcid: 0,
2727            optstr: Some(module.to_string()),
2728            defopts: None,
2729        };
2730        if addbuiltin(&mut bn) != 0 {
2731            return 1; // c:417 clash
2732        }
2733        // On success: record in added_builtins ledger so the
2734        // module's setbuiltins delete path can find this entry.
2735        self.added_builtins.insert(name.to_string(), BINF_ADDED);
2736        0 // c:417 OK
2737    }
2738
2739    /// Unregister a builtin (from module.c deletebuiltin)
2740    /// Port of `deletebuiltin(const char *nam)` from `Src/module.c:449`.
2741    /// Returns 0 on success (entry found + removed from ledger), -1
2742    /// on miss — matching the canonical free fn (00e6a9ce7e) and
2743    /// C's deletebuiltin return contract.
2744    /// WARNING: param names don't match C — Rust=(name, module) vs C=(nam)
2745    pub fn deletebuiltin(&mut self, name: &str, _module: &str) -> i32 {
2746        // c:449
2747        // Route through the free deletebuiltin for the canonical
2748        // present/absent probe.
2749        let r = deletebuiltin(name);
2750        if r == 0 {
2751            // Drop the ledger entry so setbuiltins's `already_added`
2752            // probe sees this name as removed.
2753            self.added_builtins.remove(name);
2754        }
2755        r
2756    }
2757
2758    /// Register autoloading builtin.
2759    /// Port of `static int add_autobin(const char *module, const char *bnam,
2760    /// int flags)` from `Src/module.c:426`. C allocates a `Builtin` node
2761    /// with `optstr=module`, sets `BINF_AUTOALL` if `FEAT_AUTOALL` is in
2762    /// `flags`, then calls `addbuiltin(bn)`. On failure, the freshly
2763    /// allocated node is freed; success returns 0, conflict returns 1
2764    /// unless `FEAT_IGNORE` masks it.
2765    /// WARNING: param names don't match C — Rust=(name, module, flags) vs C=(module, bnam, flags)
2766    pub fn add_autobin(&mut self, name: &str, module: &str, flags: i32) -> i32 {
2767        // c:426
2768        // Faithful port of c:426-441:
2769        //   Builtin bn = zshcalloc(sizeof(*bn));
2770        //   bn->node.nam = ztrdup(bnam);
2771        //   bn->optstr = ztrdup(module);
2772        //   if (flags & FEAT_AUTOALL)
2773        //       bn->node.flags |= BINF_AUTOALL;
2774        //   if ((ret = addbuiltin(bn))) {
2775        //       builtintab->freenode(&bn->node);
2776        //       if (!(flags & FEAT_IGNORE))
2777        //           return 1;
2778        //   }
2779        //   return 0;
2780        //
2781        // Prior port did a ledger-only insert into autoload_builtins
2782        // without ever calling the canonical addbuiltin. Now constructs
2783        // the builtin struct and routes through the free addbuiltin
2784        // (c:303 port) which probes createbuiltintable() for the
2785        // BINF_ADDED clash gate. Ledger entry preserved on success so
2786        // resolve_autoload_builtin() / del_autobin's fast path can
2787        // find it without re-scanning.
2788        let node_flags: i32 = if (flags & FEAT_AUTOALL as i32) != 0 {
2789            BINF_AUTOALL as i32 // c:435
2790        } else {
2791            0
2792        };
2793        let mut bn = builtin {
2794            // c:431-433 — zshcalloc + populate.
2795            node: hashnode {
2796                next: None,
2797                nam: name.to_string(),
2798                flags: node_flags,
2799            },
2800            handlerfunc: None,
2801            minargs: 0,
2802            maxargs: 0,
2803            funcid: 0,
2804            optstr: Some(module.to_string()),
2805            defopts: None,
2806        };
2807        // c:436 — `if ((ret = addbuiltin(bn)))`. Clash gate via the
2808        // canonical builtintab.
2809        if addbuiltin(&mut bn) != 0 {
2810            // c:437 — freenode drops the input via Rust's Drop.
2811            if (flags & FEAT_IGNORE as i32) == 0 {
2812                return 1; // c:439
2813            }
2814            // c:440 — FEAT_IGNORE masks → fall through to ret 0 but
2815            // don't insert into the ledger (the canonical table has
2816            // this name already).
2817            return 0;
2818        }
2819        // c:441 success path: register in the autoload ledger.
2820        self.autoload_builtins
2821            .insert(name.to_string(), module.to_string());
2822        0
2823    }
2824
2825    // Remove an autoloaded added by add_autobin                             // c:464
2826    /// Port of `static int del_autobin(const char *module, const char *bnam,
2827    /// int flags)` from `Src/module.c:464`.
2828    ///
2829    /// C body (c:464-478):
2830    /// ```c
2831    /// Builtin bn = (Builtin) builtintab->getnode2(builtintab, bnam);
2832    /// if (!bn) { if(!(flags & FEAT_IGNORE)) return 2; }
2833    /// else if (bn->node.flags & BINF_ADDED) {
2834    ///     if (!(flags & FEAT_IGNORE)) return 3;
2835    /// } else deletebuiltin(bnam);
2836    /// return 0;
2837    /// ```
2838    ///
2839    /// 2 = "no such builtin", 3 = "real registered builtin (BINF_ADDED) —
2840    /// can't unload", 0 = success (removed autoload entry).
2841    /// FEAT_IGNORE masks both error returns.
2842    ///
2843    /// zshrs architecture: `builtintab` is static-linked at startup
2844    /// (`createbuiltintable()` builds an immutable HashMap), so every
2845    /// entry there is effectively BINF_ADDED. The autoload-only entries
2846    /// live in `self.autoload_builtins`. The faithful mapping:
2847    ///   * if name is in `builtintab` → BINF_ADDED → return 3 (or 0 with
2848    ///     FEAT_IGNORE).
2849    ///   * else if name is in `autoload_builtins` → remove it, return 0.
2850    ///   * else → not present → return 2 (or 0 with FEAT_IGNORE).
2851    /// WARNING: param names don't match C — Rust=(name, flags) vs C=(module, bnam, flags)
2852    pub fn del_autobin(&mut self, name: &str, flags: i32) -> i32 {
2853        // c:464
2854        // Faithful port of c:464-478:
2855        //   Builtin bn = (Builtin) builtintab->getnode2(builtintab, bnam);
2856        //   if (!bn) {
2857        //       if(!(flags & FEAT_IGNORE)) return 2;
2858        //   } else if (bn->node.flags & BINF_ADDED) {
2859        //       if (!(flags & FEAT_IGNORE)) return 3;
2860        //   } else
2861        //       deletebuiltin(bnam);
2862        //   return 0;
2863        //
2864        // Three distinct return codes: 2 = no such builtin (autoload
2865        // ledger absent), 3 = real registered (BINF_ADDED set — can't
2866        // unload via del_auto*), 0 = success.
2867        //
2868        // zshrs's createbuiltintable() entries are all BINF_ADDED by
2869        // construction (the static-linked canonical table), so a hit
2870        // there always means "real registered" → return 3. The
2871        // autoload stubs live in self.autoload_builtins; a hit there
2872        // is the c:475 deletebuiltin path.
2873        //
2874        // Now routes through the free deletebuiltin (00e6a9ce7e) for
2875        // the canonical-table probe so the present/absent contract
2876        // matches C exactly. Prior port did the probe inline.
2877
2878        // c:466 — `builtintab->getnode2(builtintab, bnam)`. zshrs's
2879        // builtintab is split: the immutable createbuiltintable()
2880        // carries the static flags, the autoload_builtins ledger holds
2881        // runtime stubs from add_autobin, and added_builtins is the
2882        // runtime BINF_ADDED bit a module load flips (setbuiltins
2883        // c:508 probes the same ledger).
2884        let static_flags: Option<i32> = createbuiltintable().get(name).map(|b| b.node.flags);
2885        let in_ledger = self.autoload_builtins.contains_key(name);
2886        // BINF_ADDED equivalent: C sets the bit at addbuiltins time —
2887        // startup for core builtins, the module's boot_ for module
2888        // builtins. zshrs's static table folds BOTH in unflagged, so a
2889        // static hit counts as ADDED only when the name is core (no
2890        // owning module advertises `b:NAME`) or its owning module is
2891        // loaded. Runtime loads also land in the added_builtins ledger
2892        // (setbuiltins c:508 probes the same).
2893        let added = self.added_builtins.contains_key(name)
2894            || static_flags
2895                .map(|f| (f & BINF_ADDED as i32) != 0)
2896                .unwrap_or(false)
2897            || (static_flags.is_some() && {
2898                let mod_names: Vec<String> = self.modules.keys().cloned().collect();
2899                let mut owner_loaded: Option<bool> = None; // None = core builtin
2900                'outer: for mn in &mod_names {
2901                    let mut feats: Vec<String> = Vec::new();
2902                    if features_module(self, mn, &mut feats) != 0 {
2903                        continue;
2904                    }
2905                    for f in &feats {
2906                        if f.strip_prefix("b:") == Some(name) {
2907                            owner_loaded = Some(self.is_loaded(mn));
2908                            break 'outer;
2909                        }
2910                    }
2911                }
2912                owner_loaded != Some(false)
2913            });
2914        if static_flags.is_none() && !in_ledger {
2915            // c:467-469 — `if (!bn) { if(!(flags & FEAT_IGNORE)) return 2; }`
2916            if (flags & FEAT_IGNORE as i32) == 0 {
2917                return 2; // c:469
2918            }
2919        } else if added {
2920            // c:470-473 — `else if (bn->node.flags & BINF_ADDED)` —
2921            // a real, live builtin can't be un-autoloaded.
2922            if (flags & FEAT_IGNORE as i32) == 0 {
2923                return 3; // c:472
2924            }
2925        } else {
2926            // c:474-475 — `else deletebuiltin(bnam);` — drop the
2927            // autoload stub from the ledger.
2928            self.autoload_builtins.remove(name);
2929        }
2930        0 // c:477
2931    }
2932
2933    /// Set/clear a slice of builtins per `e[]` mask.
2934    /// Port of `static int setbuiltins(char const *nam, Builtin binl,
2935    /// int size, int *e)` from `Src/module.c:501`. For each Builtin in
2936    /// `binl[0..size]`: if `e[n]` is set, add the builtin (skip if
2937    /// already `BINF_ADDED`); else delete the builtin (skip if not
2938    /// `BINF_ADDED`). Warnings on clash/already-deleted; returns 1 if
2939    /// any op failed.
2940    /// WARNING: param names don't match C — Rust=(module, names, e) vs C=(nam, binl, size, e)
2941    pub fn setbuiltins(&mut self, module: &str, names: &[&str], e: Option<&[i32]>) -> i32 {
2942        // c:501
2943        let mut ret: i32 = 0; // c:503
2944        for (n, name) in names.iter().enumerate() {
2945            // c:505 — `for (n = 0; n < size; n++) { Builtin b = &binl[n]; ... }`
2946            let enable = e
2947                .map(|arr| arr.get(n).copied().unwrap_or(0)) // c:507 *e++
2948                .unwrap_or(1);
2949            let already_added = self.added_builtins.contains_key(*name); // c:508 b->flags & BINF_ADDED
2950            if enable != 0 {
2951                // c:507 — `if (e && *e++)` add branch
2952                if already_added {
2953                    // c:508-509 — skip already-added.
2954                    continue;
2955                }
2956                // c:510 — `if (addbuiltin(b))` — probe the canonical
2957                // table for the clash gate. The free fn returns 1
2958                // when an existing entry already has BINF_ADDED set.
2959                let mut probe = builtin {
2960                    node: hashnode {
2961                        next: None,
2962                        nam: name.to_string(),
2963                        flags: 0,
2964                    },
2965                    handlerfunc: None,
2966                    minargs: 0,
2967                    maxargs: 0,
2968                    funcid: 0,
2969                    optstr: Some(module.to_string()),
2970                    defopts: None,
2971                };
2972                if addbuiltin(&mut probe) != 0 {
2973                    // c:511-513 — `zwarnnam(nam, "name clash...")`
2974                    zwarnnam(
2975                        module,
2976                        &format!("name clash when adding builtin `{}'", name),
2977                    );
2978                    ret = 1; // c:513
2979                } else {
2980                    // c:515 — `b->node.flags |= BINF_ADDED;`. Mirror
2981                    // the in-place bit-set with the per-module
2982                    // ledger flip.
2983                    self.added_builtins.insert(name.to_string(), BINF_ADDED);
2984                }
2985            } else {
2986                // c:517-525 — del branch.
2987                if !already_added {
2988                    // c:518-519 — skip already-not-added.
2989                    continue;
2990                }
2991                // c:520 — `if (deletebuiltin(b->node.nam))`. Free fn
2992                // returns -1 on miss; treat any non-zero as the
2993                // "already deleted" condition.
2994                if deletebuiltin(name) != 0 {
2995                    // c:521-523 — `zwarnnam(nam, "builtin `%s' already
2996                    // deleted")`.
2997                    zwarnnam(module, &format!("builtin `{}' already deleted", name));
2998                    ret = 1; // c:523
2999                } else {
3000                    // c:524 — `b->node.flags &= ~BINF_ADDED;`
3001                    self.added_builtins.remove(*name);
3002                }
3003            }
3004        }
3005        ret // c:528
3006    }
3007
3008    // ------- Condition management (from module.c addconddef/deleteconddef) -------
3009
3010    /// Register a condition (from module.c addconddef)
3011    /// Port of `addconddef(Conddef c)` from `Src/module.c:703`.
3012    /// WARNING: param names don't match C — Rust=(name, module) vs C=(c)
3013    ///
3014    /// Like `addbuiltin`, the real registration lives in
3015    /// `cond.rs::CONDTAB`; the routing fn here delegates to the free
3016    /// `addconddef` (4304-port) so the canonical name+infix-flag
3017    /// clash gate fires.
3018    ///
3019    /// Returns 0 on success, 1 on clash (matches C's signature).
3020    pub fn addconddef(&mut self, name: &str, module: &str) -> i32 {
3021        // c:703
3022        // Construct a probe conddef matching what add_autocond /
3023        // setconddefs build. handler/min/max/condid stay default
3024        // (filled in by the actual module on load).
3025        let cd = conddef {
3026            next: None,
3027            name: name.to_string(),
3028            flags: 0, // c:703 entries here aren't infix by default
3029            handler: None,
3030            min: 0,
3031            max: 0,
3032            condid: 0,
3033            module: Some(module.to_string()),
3034        };
3035        // c:705-715 — addconddef walks CONDTAB for clash, replaces
3036        // autoload entries, prepends on success. Returns 0/1.
3037        addconddef(cd)
3038    }
3039
3040    /// Unregister a condition (from module.c deleteconddef)
3041    /// Port of `deleteconddef(Conddef c)` from `Src/module.c:724`.
3042    /// Returns 0 on success (entry was found + removed), -1 on miss.
3043    /// Mirrors C's deleteconddef return contract.
3044    /// WARNING: param names don't match C — Rust=(name, module) vs C=(c)
3045    pub fn deleteconddef(&mut self, name: &str, _module: &str) -> i32 {
3046        // c:724
3047        // Build a probe conddef and call the free deleteconddef
3048        // (4304-port) which walks CONDTAB by (name, infix-flag) for
3049        // identity. infix=0 here matches the prefix-style default
3050        // used by the auto* registrations.
3051        let probe = conddef {
3052            next: None,
3053            name: name.to_string(),
3054            flags: 0,
3055            handler: None,
3056            min: 0,
3057            max: 0,
3058            condid: 0,
3059            module: None,
3060        };
3061        deleteconddef(&probe)
3062    }
3063
3064    /// Get condition definition (from module.c getconddef)
3065    /// Port of `getconddef(int inf, const char *name, int autol)` from `Src/module.c:648`.
3066    /// WARNING: param names don't match C — Rust=(name) vs C=(inf, name, autol)
3067    ///
3068    /// Returns the autoload mapping if any. C consults the canonical
3069    /// `condtab` first; the autoload table is the fallback.
3070    pub fn getconddef(&self, name: &str) -> Option<&str> {
3071        self.autoload_conditions.get(name).map(|s| s.as_str())
3072    }
3073
3074    /// Register autoloading condition.
3075    /// Port of `static int add_autocond(const char *module, const char
3076    /// *cnam, int flags)` from `Src/module.c:792`. C body allocates a
3077    /// `Conddef`, copies name/module, sets `CONDF_INFIX` if
3078    /// `FEAT_INFIX` and `CONDF_AUTOALL` if `FEAT_AUTOALL`, then calls
3079    /// `addconddef(c)`. On addconddef failure (already exists) the
3080    /// node is freed; returns 1 unless `FEAT_IGNORE`.
3081    /// WARNING: param names don't match C — Rust=(name, module, flags) vs C=(module, cnam, flags)
3082    pub fn add_autocond(&mut self, name: &str, module: &str, flags: i32) -> i32 {
3083        // c:792
3084        // Faithful port of c:792-810:
3085        //   Conddef c = zshcalloc(sizeof(*c));
3086        //   c->name = ztrdup(cnam);
3087        //   c->module = ztrdup(module);
3088        //   c->flags = (flags & FEAT_INFIX) ? CONDF_INFIX : 0;
3089        //   if (flags & FEAT_AUTOALL) c->flags |= CONDF_AUTOALL;
3090        //   if (addconddef(c)) {
3091        //       zsfree(c->name); zsfree(c->module); zfree(c, sizeof(*c));
3092        //       if (!(flags & FEAT_IGNORE)) return 1;
3093        //   }
3094        //   return 0;
3095        //
3096        // Prior port did a ledger-only insert into autoload_conditions
3097        // without ever touching the canonical CONDTAB. Now constructs
3098        // the conddef struct and routes through the free addconddef
3099        // (1395acf3a7 dependency) which walks CONDTAB for clashes,
3100        // replaces autoload entries, and prepends on success.
3101        let mut cflags: i32 = if (flags & FEAT_INFIX) != 0 {
3102            CONDF_INFIX // c:799
3103        } else {
3104            0
3105        };
3106        if (flags & FEAT_AUTOALL) != 0 {
3107            cflags |= CONDF_AUTOALL; // c:801
3108        }
3109        // c:796-803 — populate the conddef record. `handler` is None
3110        // because autoload stubs don't carry the dispatch fn until
3111        // the module loads and addconddef replaces the entry.
3112        let cd = conddef {
3113            next: None,
3114            name: name.to_string(),
3115            flags: cflags,
3116            handler: None,
3117            min: 0,
3118            max: 0,
3119            condid: 0,
3120            module: Some(module.to_string()),
3121        };
3122        // c:804 — addconddef(c).
3123        if addconddef(cd) != 0 {
3124            // c:805-807 — zsfree/zfree happen via Rust drop.
3125            if (flags & FEAT_IGNORE) == 0 {
3126                return 1; // c:810
3127            }
3128        }
3129        // Keep the ledger entry too so resolve_autoload_condition()
3130        // and del_autocond's fast path can find it without re-scanning
3131        // CONDTAB.
3132        self.autoload_conditions
3133            .insert(name.to_string(), module.to_string());
3134        0 // c:812
3135    }
3136
3137    /// Port of `static int del_autocond(const char *modnam, const char *cnam,
3138    /// int flags)` from `Src/module.c:819`.
3139    ///
3140    /// C body (c:819-835):
3141    /// ```c
3142    /// Conddef cd = getconddef((flags & FEAT_INFIX) ? 1 : 0, cnam, 0);
3143    /// if (!cd) { if (!(flags & FEAT_IGNORE)) return 2; }
3144    /// else if (cd->flags & CONDF_ADDED) {
3145    ///     if (!(flags & FEAT_IGNORE)) return 3;
3146    /// } else deleteconddef(cd);
3147    /// return 0;
3148    /// ```
3149    ///
3150    /// 2 = "no such condition", 3 = "registered condition (CONDF_ADDED) —
3151    /// can't unload", 0 = success. FEAT_IGNORE masks both error returns.
3152    /// WARNING: param names don't match C — Rust=(name, flags) vs C=(modnam, cnam, flags)
3153    pub fn del_autocond(&mut self, name: &str, flags: i32) -> i32 {
3154        // c:819
3155        // Faithful port of c:819-835:
3156        //   Conddef cd = getconddef((flags & FEAT_INFIX) ? 1 : 0,
3157        //                           cnam, 0);
3158        //   if (!cd) {
3159        //       if (!(flags & FEAT_IGNORE)) return 2;
3160        //   } else if (cd->flags & CONDF_ADDED) {
3161        //       if (!(flags & FEAT_IGNORE)) return 3;
3162        //   } else
3163        //       deleteconddef(cd);
3164        //   return 0;
3165        //
3166        // Prior port skipped CONDTAB entirely; checked the ledger
3167        // only. Now routes through getconddef (1395acf3a7) which
3168        // walks CONDTAB filtered by infix-flag, then deleteconddef
3169        // (free fn) which removes the matched entry.
3170        let inf = if (flags & FEAT_INFIX) != 0 { 1 } else { 0 };
3171        // c:821 — `getconddef(inf, cnam, 0)`.
3172        let cd = getconddef(inf, name, 0, self);
3173        match cd {
3174            None => {
3175                // c:823-825 — !cd: return 2 unless FEAT_IGNORE.
3176                if (flags & FEAT_IGNORE) == 0 {
3177                    return 2; // c:825
3178                }
3179                0 // c:834
3180            }
3181            Some(ref entry) if (entry.flags & CONDF_ADDED) != 0 => {
3182                // c:826-828 — CONDF_ADDED set: real registered
3183                // condition, can't unload via del_auto*. Return 3
3184                // unless FEAT_IGNORE.
3185                if (flags & FEAT_IGNORE) == 0 {
3186                    return 3; // c:828
3187                }
3188                0
3189            }
3190            Some(ref entry) => {
3191                // c:831-832 — deleteconddef(cd); drop ledger too.
3192                let _ = deleteconddef(entry);
3193                self.autoload_conditions.remove(name);
3194                0 // c:834
3195            }
3196        }
3197    }
3198
3199    // ------- Hook management lives in the file-static free ported above ------
3200    //
3201    // C `hooktab` (Src/module.c:843) is a file-static `Hookdef` linked
3202    // list, NOT a member of `ModuleTable` (which is the `modulestab`
3203    // HashTable of Module nodes at c:Modules/zmodload.c:32). Hook ops
3204    // are free ported operating on the file-static `hooktab` (above):
3205    // `gethookdef`, `addhookdef`, `addhookdefs`, `deletehookdef`,
3206    // `deletehookdefs`, `addhookdeffunc`, `addhookfunc`,
3207    // `deletehookdeffunc`, `deletehookfunc`, `runhookdef`.
3208
3209    // ------- Parameter management (from module.c addparamdef/deleteparamdef) -------
3210
3211    /// Add or remove sets of parameters; same shape as `setbuiltins`.
3212    /// Port of `static int setparamdefs(char const *nam, Paramdef d,
3213    /// int size, int *e)` from `Src/module.c:1170`. For each Paramdef
3214    /// in `d[0..size]`: if `e[n]` is set and `d->pm` is null, add the
3215    /// param via `addparamdef(d)`; if `e[n]` is clear and `d->pm` is
3216    /// non-null, remove via `deleteparamdef(d)`. Warnings on
3217    /// error/already-deleted; returns 1 if any op failed.
3218    /// WARNING: param names don't match C — Rust=(module, names, e) vs C=(nam, d, size, e)
3219    pub fn setparamdefs(&mut self, module: &str, names: &[&str], e: Option<&[i32]>) -> i32 {
3220        // c:1165
3221        // Faithful port of c:1165-1192. Prior Rust port skipped the
3222        // diagnostics + ret-tracking C does on failure:
3223        //
3224        //   if (e && *e++) {           // add branch
3225        //       if (d->pm) continue;   // already registered
3226        //       if (addparamdef(d)) {
3227        //           zwarnnam(nam,
3228        //               "error when adding parameter `%s'", d->name);
3229        //           ret = 1;
3230        //       }
3231        //   } else {                   // del branch
3232        //       if (!d->pm) continue;  // not registered
3233        //       if (deleteparamdef(d)) {
3234        //           zwarnnam(nam,
3235        //               "parameter `%s' already deleted", d->name);
3236        //           ret = 1;
3237        //       }
3238        //   }
3239        //
3240        // Maps to the autoload_params ledger (zshrs-only structural
3241        // equivalent of d->pm presence). Diagnostics now fire when
3242        // the ledger insert/remove conflicts with the C-side
3243        // contract (probe + retry on the canonical paramtab to
3244        // observe the same clash behaviour addparamdef would).
3245        use crate::ported::params::paramtab;
3246        let mut ret: i32 = 0; // c:1167
3247        for (n, name) in names.iter().enumerate() {
3248            // c:1169 — `while (size--)`
3249            let enable = e
3250                .map(|arr| arr.get(n).copied().unwrap_or(0)) // c:1170 *e++
3251                .unwrap_or(1);
3252            // c:1171 / c:1180 — `if (d->pm)` / `if (!d->pm)`.
3253            // Static-link analog: autoload_params contains the name
3254            // when the module has registered it as a paramdef.
3255            let already = self.autoload_params.contains_key(*name);
3256            if enable != 0 {
3257                // c:1170 add branch
3258                if already {
3259                    continue; // c:1172
3260                }
3261                // c:1175 — `if (addparamdef(d))`. Faithful path:
3262                // probe the canonical paramtab for a clash; if the
3263                // name already exists with a non-MOD-derived param,
3264                // mirror addparamdef's failure.
3265                let canonical_clash = paramtab()
3266                    .read()
3267                    .ok()
3268                    .map(|t| t.contains_key(*name))
3269                    .unwrap_or(false);
3270                if canonical_clash {
3271                    // c:1176-1178 — `zwarnnam('error when adding ...')`.
3272                    zwarnnam(module, &format!("error when adding parameter `{}'", name));
3273                    ret = 1; // c:1177
3274                    continue;
3275                }
3276                // c:1181 — register the ledger entry on success.
3277                self.autoload_params
3278                    .insert(name.to_string(), module.to_string());
3279            } else {
3280                // c:1179 del branch
3281                if !already {
3282                    continue; // c:1181
3283                }
3284                // c:1184 — `if (deleteparamdef(d))`. With the ledger
3285                // hit, the canonical removal succeeds; emit the
3286                // C-equivalent diagnostic only if the paramtab probe
3287                // says the entry got tampered with externally
3288                // (already-deleted from paramtab while still on the
3289                // ledger).
3290                let canonical_present = paramtab()
3291                    .read()
3292                    .ok()
3293                    .map(|t| t.contains_key(*name))
3294                    .unwrap_or(false);
3295                self.autoload_params.remove(*name);
3296                if !canonical_present {
3297                    // c:1185-1187 — `parameter `%s' already deleted`.
3298                    zwarnnam(module, &format!("parameter `{}' already deleted", name));
3299                    ret = 1; // c:1186
3300                }
3301            }
3302        }
3303        ret // c:1191
3304    }
3305
3306    /// Register autoloading parameter.
3307    /// Port of `static int add_autoparam(const char *module, const char
3308    /// *pnam, int flags)` from `Src/module.c:1198`. C body:
3309    /// `checkaddparam()` clash check (returns 2 if `-i`'d), then
3310    /// `setsparam(pnam, module)` creating the param with `PM_AUTOLOAD`
3311    /// (+ `PM_AUTOALL` if `FEAT_AUTOALL`). `queue_signals`/`noerrs=2`
3312    /// bracket so the setsparam doesn't echo errors out.
3313    /// WARNING: param names don't match C — Rust=(name, module, flags) vs C=(module, pnam, flags)
3314    pub fn add_autoparam(&mut self, name: &str, module: &str, flags: i32) -> i32 {
3315        // c:1198
3316        // Faithful port of c:1198-1228 routes through the free
3317        // add_autoparam fn (293c041e2f) which already does the full
3318        // checkaddparam + setsparam + PM_AUTOLOAD (+PM_AUTOALL) +
3319        // noerrs/queue_signals bracket.
3320        //
3321        // Prior method was a ledger-only HashMap insert that did the
3322        // queue_signals dance without ever calling setsparam — so
3323        // PM_AUTOLOAD never landed on the canonical paramtab entry,
3324        // and `typeset +` wouldn't see the 'undefined NAME' shape.
3325        //
3326        // The free fn returns 0 / -1 (vs C's 0 / 1 / -1) — preserve
3327        // the existing method-level contract by mapping -1 → 1 below.
3328        let r = add_autoparam(module, name, flags); // c:1198
3329        if r != 0 {
3330            // c:1213-1219 — error: 2 (FEAT_IGNORE soft-fail) maps to
3331            // 0 here per the prior method contract.
3332            if (flags & FEAT_IGNORE) != 0 {
3333                return 0;
3334            }
3335            return r; // -1 → -1
3336        }
3337        // c:1218-1221 success: also keep the ledger entry up so the
3338        // resolve_autoload_param fast path stays consistent.
3339        self.autoload_params
3340            .insert(name.to_string(), module.to_string());
3341        0
3342    }
3343
3344    /// Port of `static int del_autoparam(const char *modnam, const char *pnam,
3345    /// int flags)` from `Src/module.c:1240`.
3346    ///
3347    /// C body (c:1240-1255):
3348    /// ```c
3349    /// Param pm = (Param) gethashnode2(paramtab, pnam);
3350    /// if (!pm) { if (!(flags & FEAT_IGNORE)) return 2; }
3351    /// else if (!(pm->node.flags & PM_AUTOLOAD)) {
3352    ///     if (!(flags & FEAT_IGNORE)) return 3;
3353    /// } else unsetparam_pm(pm, 0, 1);
3354    /// return 0;
3355    /// ```
3356    ///
3357    /// 2 = "no such param", 3 = "real param (not autoload) — can't
3358    /// unload", 0 = success. FEAT_IGNORE masks both error returns.
3359    /// WARNING: param names don't match C — Rust=(name, flags) vs C=(modnam, pnam, flags)
3360    pub fn del_autoparam(&mut self, name: &str, flags: i32) -> i32 {
3361        // c:1234
3362        // Faithful port of c:1234-1248 routes through the free
3363        // del_autoparam fn (293c041e2f) which does the canonical
3364        // paramtab probe + PM_AUTOLOAD gate + unsetparam_pm.
3365        //
3366        // Prior method walked paramtab directly and emulated the
3367        // PM_AUTOLOAD gate inline; routing through the free fn keeps
3368        // the logic single-sourced so PM_AUTOLOAD semantics evolve
3369        // in one place. Ledger cleanup preserved on the success path.
3370        let r = del_autoparam("", name, flags); // c:1234 (modnam unused per UNUSED())
3371        if r == 0 {
3372            // c:1246 — `unsetparam_pm` already ran via the free fn;
3373            // also drop the ledger entry so resolve_autoload_param
3374            // stops returning the stale module name.
3375            self.autoload_params.remove(name);
3376        }
3377        r
3378    }
3379
3380    // ------- Feature enable/disable (from module.c features_/enables_) -------
3381
3382    /// Enable a feature (from module.c enables_)
3383    ///
3384    /// Without a per-module feature ledger, enable/disable maps onto
3385    /// the canonical builtin/conddef/paramdef tables. Returns true if
3386    /// the module itself is registered. The actual per-feature
3387    /// enabled-bit lives on the canonical record (e.g. `Builtin.flags`
3388    /// `BINF_DISABLED`).
3389    pub fn enable_feature(&mut self, module: &str, _name: &str) -> bool {
3390        self.modules.contains_key(module)
3391    }
3392
3393    /// Disable a feature
3394    pub fn disable_feature(&mut self, module: &str, _name: &str) -> bool {
3395        self.modules.contains_key(module)
3396    }
3397
3398    /// List feature *names* of a module (from module.c features_).
3399    /// Without a per-module ledger, this returns an empty list — C
3400    /// computes feature names by walking the canonical tables for
3401    /// entries that name the given module. Callers that care use
3402    /// `features_module`/`features_` directly.
3403    pub fn list_features(&self, _module: &str) -> Vec<String> {
3404        Vec::new()
3405    }
3406
3407    /// Check if a module is linked (statically compiled) (from module.c module_linked)
3408    /// Port of `module_linked(char const *name)` from `Src/module.c:385`.
3409    ///
3410    /// C body walks `linkedmodules` comparing `->name`; it never looks
3411    /// at `modulestab`. A module can be linked with no `modulestab`
3412    /// node (the normal state before its first `zmodload`) and can have
3413    /// a node while not linked (an alias, or an `add_dep`/`zmodload -ab`
3414    /// bookkeeping node).
3415    pub fn module_linked(&self, name: &str) -> bool {
3416        // c:389-391 — for (node = firstnode(linkedmodules); …)
3417        self.linkedmodules.iter().any(|n| n == name)
3418    }
3419
3420    /// Resolve autoload — find which module provides a builtin
3421    pub fn resolve_autoload_builtin(&self, name: &str) -> Option<&str> {
3422        self.autoload_builtins.get(name).map(|s| s.as_str())
3423    }
3424
3425    /// Resolve autoload — find which module provides a parameter
3426    pub fn resolve_autoload_param(&self, name: &str) -> Option<&str> {
3427        self.autoload_params.get(name).map(|s| s.as_str())
3428    }
3429
3430    /// Ensure a module's feature is available
3431    /// Port of `ensurefeature(const char *modname, const char *prefix, const char *feature)` from `Src/module.c:3415`.
3432    /// WARNING: param names don't match C — Rust=(module, feature) vs C=(modname, prefix, feature)
3433    pub fn ensurefeature(&mut self, module: &str, feature: &str) -> bool {
3434        if !self.is_loaded(module) {
3435            self.load_module(module, None, false);
3436        }
3437        self.is_loaded(module)
3438    }
3439}
3440
3441/// Module lifecycle callbacks (from module.c setup_/getrandom_buffer/cleanup_/finish_)
3442/// Lifecycle hooks every module must implement.
3443/// Port of the `setup_`/`features_`/`enables_`/`getrandom_buffer`/`cleanup_`
3444/// /`finish_` entry points every C module exposes (Src/module.c
3445/// lines 306-345 illustrate the canonical no-op set). Rust
3446/// modules implement this trait directly.
3447pub trait ModuleLifecycle {
3448    fn setup(&mut self) -> i32 {
3449        0
3450    }
3451    fn boot(&mut self) -> i32 {
3452        0
3453    }
3454    fn cleanup(&mut self) -> i32 {
3455        0
3456    }
3457    fn finish(&mut self) -> i32 {
3458        0
3459    }
3460}
3461
3462/// Port of `getmathfunc()` from `Src/module.c:1283`. — C decl `getmathfunc(const char *name, int autol)`.
3463///
3464/// C body: linear-search `mathfuncs` for `name`; if found and `autol`
3465/// is true and the entry is autoloadable, demand-load via
3466/// `ensurefeature("f:", name)`. Returns the resolved entry or NULL.
3467///
3468/// Rust port returns `Some(module_name)` on hit, `None` on miss.
3469///
3470/// Faithful port of c:1283-1306:
3471/// ```c
3472/// MathFunc
3473/// getmathfunc(const char *name, int autol)
3474/// {
3475///     MathFunc p, q = NULL;
3476///     for (p = mathfuncs; p; q = p, p = p->next)
3477///         if (!strcmp(name, p->name)) {
3478///             if (autol && p->module && !(p->flags & MFF_USERFUNC)) {
3479///                 char *n = dupstring(p->module);
3480///                 int flags = p->flags;
3481///                 removemathfunc(q, p);
3482///                 (void)ensurefeature(n, "f:",
3483///                                     (flags & MFF_AUTOALL) ? NULL : name);
3484///                 p = getmathfunc(name, 0);
3485///                 if (!p) {
3486///                     zerr("autoloading module %s failed to define math function: %s", n, name);
3487///                 }
3488///             }
3489///             return p;
3490///         }
3491///     return NULL;
3492/// }
3493/// ```
3494///
3495/// C walks `mathfuncs` (file-static linked list) — Rust walks
3496/// MATHFUNCS (same shape, Vec). The MFF_USERFUNC filter
3497/// (`functions -M` user-defined math) keeps user fns from being
3498/// autoloaded out from under the caller. The autoload arm:
3499///   1. snapshot module name + flags
3500///   2. removemathfunc(name) — drop the autoload stub
3501///   3. ensurefeature(module, "f:", AUTOALL?NULL:name) — load real
3502///   4. re-query mathfuncs (autol=0) — return the loaded entry
3503///   5. zerr if still missing (load failed to populate the table)
3504/// WARNING: param names don't match C — Rust=(table, name, autol) vs C=(name, autol)
3505pub fn getmathfunc(table: &mut modulestab, name: &str, autol: i32) -> Option<String> {
3506    // c:1283
3507    // c:1287-1288 — `for (p = mathfuncs; p; q = p, p = p->next)`:
3508    // walk MATHFUNCS for the first name match. Snapshot under the
3509    // lock so we can release before mutating (autoload arm calls
3510    // ensurefeature which can re-enter MATHFUNCS via addmathfunc).
3511    let hit: Option<(String, i32, bool)> = {
3512        let tab = MATHFUNCS.lock().unwrap();
3513        tab.iter().find_map(|p| {
3514            if p.name == name {
3515                Some((
3516                    p.module.clone().unwrap_or_default(),
3517                    p.flags,
3518                    p.module.is_some(),
3519                ))
3520            } else {
3521                None
3522            }
3523        })
3524    };
3525    let (module, flags, has_module) = match hit {
3526        Some(t) => t,
3527        None => return None, // c:1306 `return NULL;`
3528    };
3529    // c:1289 — `if (autol && p->module && !(p->flags & MFF_USERFUNC))`.
3530    if autol != 0 && has_module && (flags & MFF_USERFUNC) == 0 {
3531        // c:1290-1291 — snapshot already done above.
3532        // c:1293 — `removemathfunc(q, p)`: drop the stub before load.
3533        removemathfunc(name);
3534        // c:1295-1296 — `ensurefeature(n, "f:", AUTOALL?NULL:name)`.
3535        let feature_arg = if (flags & crate::ported::zsh_h::MFF_AUTOALL) != 0 {
3536            None
3537        } else {
3538            Some(name)
3539        };
3540        let _ = ensurefeature(table, &module, "f:", feature_arg);
3541        // c:1298 — `p = getmathfunc(name, 0);` recurse w/o autol.
3542        // EXISTENCE check: C zerrs only when the re-lookup returns
3543        // NULL. A freshly registered real entry has module == NULL
3544        // (mftab entries, zsh.h:133 NUMMATHFUNC), so mapping the hit
3545        // through `p.module.clone()` (None for real entries) wrongly
3546        // reported a successful load as "failed to define". Mirror the
3547        // non-autoload tail below: empty string marks a module-less
3548        // hit.
3549        let after = {
3550            let tab = MATHFUNCS.lock().unwrap();
3551            tab.iter().find_map(|p| {
3552                if p.name == name {
3553                    Some(p.module.clone().unwrap_or_default())
3554                } else {
3555                    None
3556                }
3557            })
3558        };
3559        // c:1299-1301 — `if (!p) zerr(...)`.
3560        if after.is_none() {
3561            crate::ported::utils::zerr(&format!(
3562                "autoloading module {} failed to define math function: {}",
3563                module, name
3564            ));
3565        }
3566        return after; // c:1303 `return p;`
3567    }
3568    // c:1303 — non-autoload (or user-fn) hit: return the entry.
3569    if has_module {
3570        Some(module)
3571    } else {
3572        // User-fn entries (`functions -M`) have no module — return
3573        // an empty string so the caller still observes the hit.
3574        Some(String::new())
3575    }
3576}
3577
3578/// Port of `add_automathfunc()` from `Src/module.c:1410`. — C decl `add_automathfunc(const char *module, const char *fnam, int flags)`.
3579///
3580/// C body:
3581/// ```c
3582/// add_automathfunc(const char *module, const char *fnam, int flags) {
3583///     MathFunc f = zalloc(sizeof(*f));
3584///     f->name = ztrdup(fnam);
3585///     f->module = ztrdup(module);
3586///     f->flags = 0;
3587///     if (addmathfunc(f)) {
3588///         zsfree(f->name); zsfree(f->module); zfree(f, sizeof(*f));
3589///         if (!(flags & FEAT_IGNORE))
3590///             return 1;
3591///     }
3592///     return 0;
3593/// }
3594/// ```
3595///
3596/// Registers `fnam` as an autoloadable math function provided by `module`.
3597/// WARNING: param names don't match C — Rust=(table, module, fnam, flags) vs C=(module, fnam, flags)
3598pub fn add_automathfunc(table: &mut modulestab, module: &str, fnam: &str, flags: i32) -> i32 {
3599    // c:1410
3600    // Faithful port of c:1410-1429:
3601    //   MathFunc f = zalloc(sizeof(*f));
3602    //   f->name = ztrdup(fnam);
3603    //   f->module = ztrdup(module);
3604    //   f->flags = 0;
3605    //   if (addmathfunc(f)) {
3606    //       zsfree(f->name); zsfree(f->module); zfree(f, sizeof(*f));
3607    //       if (!(flags & FEAT_IGNORE)) return 1;
3608    //   }
3609    //   return 0;
3610    //
3611    // Prior port did ledger-only autoload_mathfuncs.insert without
3612    // ever touching the canonical MATHFUNCS table. Now constructs
3613    // the mathfunc struct and routes through the free addmathfunc
3614    // (c:1313) which walks MATHFUNCS for clashes and replaces
3615    // autoloadable entries.
3616    let f = mathfunc {
3617        next: None,
3618        name: fnam.to_string(),
3619        flags: 0, // c:1417 — autoload entries don't carry MFF_ADDED
3620        nfunc: None,
3621        sfunc: None,
3622        module: Some(module.to_string()),
3623        minargs: 0,
3624        maxargs: 0,
3625        funcid: 0,
3626    };
3627    // c:1420 — `if (addmathfunc(f))` clash gate.
3628    if addmathfunc(f) != 0 {
3629        // c:1421-1424 — free happens via Rust drop on the returned-
3630        // by-value f going out of scope.
3631        if (flags & FEAT_IGNORE) == 0 {
3632            return 1; // c:1426
3633        }
3634        // c:1427 — FEAT_IGNORE: fall through to success but skip
3635        // the ledger insert (the canonical table already has this).
3636        return 0;
3637    }
3638    // c:1429 success path: register in the autoload ledger.
3639    table
3640        .autoload_mathfuncs
3641        .insert(fnam.to_string(), module.to_string());
3642    0
3643}
3644
3645/// Port of `del_automathfunc()` from `Src/module.c:1436`. — C decl `del_automathfunc(UNUSED(const char *modnam), const char *fnam, int flags)`.
3646///
3647/// C body:
3648/// ```c
3649/// del_automathfunc(UNUSED(const char *modnam), const char *fnam, int flags) {
3650///     MathFunc f = getmathfunc(fnam, 0);
3651///     if (!f) {
3652///         if (!(flags & FEAT_IGNORE)) return 2;
3653///     } else if (f->flags & MFF_ADDED) {
3654///         if (!(flags & FEAT_IGNORE)) return 3;
3655///     } else
3656///         deletemathfunc(f);
3657///     return 0;
3658/// }
3659/// ```
3660///
3661/// Removes `fnam` from the autoloadable math-function registry.
3662/// WARNING: param names don't match C — Rust=(table, _modnam, fnam, flags) vs C=(modnam, fnam, flags)
3663pub fn del_automathfunc(table: &mut modulestab, _modnam: &str, fnam: &str, flags: i32) -> i32 {
3664    // c:1436
3665    // Faithful port of c:1436-1449:
3666    //   MathFunc f = getmathfunc(fnam, 0);
3667    //   if (!f) { if (!(flags & FEAT_IGNORE)) return 2; }
3668    //   else if (f->flags & MFF_ADDED) {
3669    //       if (!(flags & FEAT_IGNORE)) return 3;
3670    //   } else deletemathfunc(f);
3671    //   return 0;
3672    //
3673    // Prior port skipped the MFF_ADDED gate at c:1444 entirely.
3674    // That meant `zmodload -ufd` on a real (module-registered)
3675    // math function silently succeeded instead of returning 3 like
3676    // C does, dropping the user's actual function out from under
3677    // them. Now uses getmathfunc (fd1ec84bab) to find the entry
3678    // and checks MFF_ADDED before removal.
3679
3680    // c:1440 — `getmathfunc(fnam, 0)`. autol=0 since we don't want
3681    // the autoload trigger to fire during a deletion query.
3682    let entry = getmathfunc(table, fnam, 0);
3683    match entry {
3684        None => {
3685            // c:1441-1442 — `if (!f) { if (!FEAT_IGNORE) return 2; }`
3686            if (flags & FEAT_IGNORE) == 0 {
3687                return 2;
3688            }
3689            0
3690        }
3691        Some(_) => {
3692            // c:1443 — `else if (f->flags & MFF_ADDED)`.
3693            // Look up the entry in MATHFUNCS to read its flags
3694            // (getmathfunc returns the module string, not the
3695            // mathfunc struct).
3696            let added = {
3697                let tab = MATHFUNCS.lock().unwrap();
3698                tab.iter()
3699                    .find(|m| m.name == fnam)
3700                    .map(|m| (m.flags & MFF_ADDED) != 0)
3701                    .unwrap_or(false)
3702            };
3703            if added {
3704                // c:1444-1445 — real registered, can't unload via
3705                // del_auto*. Return 3 unless FEAT_IGNORE.
3706                if (flags & FEAT_IGNORE) == 0 {
3707                    return 3;
3708                }
3709                return 0;
3710            }
3711            // c:1447 — deletemathfunc(f). Use removemathfunc
3712            // (which deletemathfunc delegates to in the autoload
3713            // path) + drop the ledger entry.
3714            removemathfunc(fnam);
3715            table.autoload_mathfuncs.remove(fnam);
3716            0
3717        }
3718    }
3719}
3720
3721/// Port of `load_and_bind()` from `Src/module.c:1468`. — C decl `load_and_bind(const char *fn)`.
3722///
3723/// C body: AIX-only `load() + loadbind()` wrapper. Iterates the
3724/// `modulestab` hash table, binding each loaded module's handle to
3725/// the new module's symbols. On loadbind failure, calls `unload()`
3726/// and stores the error in `dlerrstr`.
3727///
3728/// Static-link path: dlopen/dlsym aren't used since modules are
3729/// linked at compile time. Returns 0 (NULL handle).
3730/// WARNING: param names don't match C — Rust=(_fn_path) vs C=(fn)
3731pub fn load_and_bind(_fn_path: &str) -> usize {
3732    // c:1468
3733    0 // c:1492 NULL
3734}
3735
3736/// Port of `hpux_dlsym()` from `Src/module.c:1530`. — C decl `hpux_dlsym(void *handle, char *name)`.
3737///
3738/// C body:
3739/// ```c
3740/// hpux_dlsym(void *handle, char *name)
3741/// {
3742///     void *sym_addr;
3743///     if (!shl_findsym((shl_t *)&handle, name, TYPE_UNDEFINED, &sym_addr))
3744///         return sym_addr;
3745///     return NULL;
3746/// }
3747/// ```
3748///
3749/// HP-UX-specific dlsym wrapper around `shl_findsym(3)`. Static-link
3750/// path: never invoked since zshrs doesn't dlopen modules.
3751#[allow(unused_variables)]
3752pub fn hpux_dlsym(handle: usize, name: &str) -> usize {
3753    // c:1530
3754    0 // c:1530 NULL
3755}
3756
3757/// Port of `try_load_module()` from `Src/module.c:1583`. — C decl `try_load_module(char const *name)`.
3758///
3759/// C body iterates `module_path` looking for a loadable file via
3760/// `dlopen`. Static-link path: a module is "loadable" iff it's in
3761/// our static `ModuleTable.modules` map.
3762/// WARNING: param names don't match C — Rust=(table, name) vs C=(name)
3763pub fn try_load_module(table: &modulestab, name: &str) -> i32 {
3764    // c:1583
3765    // C dlopens the module path (or falls back to module_linked for
3766    // compiled-in modules). Static-link analog: the name must be on
3767    // `linkedmodules` (c:385 `module_linked`) — zshrs has no DSOs, so
3768    // "the file exists and dlopens" is exactly "the module is compiled
3769    // in". Deliberately NOT a `modulestab` membership test: a bare
3770    // FINDMOD_CREATE bookkeeping node (flags=0 per C's zshcalloc at
3771    // c:1676) has no backing code, so `zmodload -ab zsh/bogus x; x`
3772    // must still emit `failed to load module` rather than "boot" the
3773    // phantom; and a real module has no node at all until its first
3774    // load, which must still succeed.
3775    if table.module_linked(name) {
3776        1
3777    } else {
3778        0
3779    }
3780}
3781
3782/// Port of `do_load_module()` from `Src/module.c:1610`. — C decl `do_load_module(char const *name, int silent)`.
3783///
3784/// C body:
3785/// ```c
3786/// do_load_module(char const *name, int silent)
3787/// {
3788///     void *ret;
3789///     ret = try_load_module(name);
3790///     if (!ret && !silent) {
3791///         zwarn("failed to load module `%s': %s", name, ...);
3792///     }
3793///     return ret;
3794/// }
3795/// ```
3796///
3797/// C returns `void *` (the dlopen handle); Rust port returns 0 on
3798/// success / 1 on failure. zshrs's static-link path: `try_load_module`
3799/// always succeeds for built-in modules. Returns 1 + zwarn on miss.
3800/// WARNING: param names don't match C — Rust=(table, name, silent) vs C=(name, silent)
3801pub fn do_load_module(table: &mut modulestab, name: &str, silent: i32) -> i32 {
3802    // c:1610
3803    // c:1610 — ret = try_load_module(name);
3804    let ret = try_load_module(table, name);
3805    if ret == 0 && silent == 0 {
3806        // c:1615
3807        // c:1622 `zwarn("failed to load module `%s': %s", name, dlerror())`.
3808        // The dlerror TAIL is deliberately omitted: zshrs's modules are
3809        // statically linked, so nothing was dlopened and there is no real
3810        // diagnostic to report — synthesising one would be a fabricated
3811        // message, not a port. Prefix + rc are the pinned contract
3812        // (docs/BUGS.md #376, `zmodload_nonexistent_diagnostic` in
3813        // tests/parity/modules_parity.rs). Both emit sites must agree.
3814        zwarn(&format!("failed to load module `{}'", name));
3815    }
3816    ret // c:1624
3817}
3818
3819/// Port of `find_module()` from `Src/module.c:1659`. — C decl `find_module(const char *name, int flags, const char **namep)`.
3820///
3821/// C body:
3822/// ```c
3823/// find_module(const char *name, int flags, const char **namep)
3824/// {
3825///     Module m;
3826///     m = (Module)modulestab->getnode2(modulestab, name);
3827///     if (m) {
3828///         if ((flags & FINDMOD_ALIASP) && (m->node.flags & MOD_ALIAS)) {
3829///             if (namep) *namep = m->u.alias;
3830///             return find_module(m->u.alias, flags, namep);
3831///         }
3832///         if (namep) *namep = m->node.nam;
3833///         return m;
3834///     }
3835///     if (!(flags & FINDMOD_CREATE))
3836///         return NULL;
3837///     m = zshcalloc(sizeof(*m));
3838///     modulestab->addnode(modulestab, ztrdup(name), m);
3839///     return m;
3840/// }
3841/// ```
3842///
3843/// Returns the resolved module name (after alias chasing) and
3844/// whether an entry was created. C's `Module` return becomes
3845/// `Option<String>` of the canonical name.
3846/// WARNING: param names don't match C — Rust=(table, name, flags) vs C=(name, flags, namep)
3847pub fn find_module(table: &mut modulestab, name: &str, flags: i32) -> Option<String> {
3848    // c:1659
3849    // c:1659 — m = modulestab->getnode2(modulestab, name);
3850    let mut cur_name = name.to_string();
3851    let mut depth = 0;
3852    loop {
3853        if depth > 64 {
3854            return None;
3855        } // alias-cycle guard
3856        depth += 1;
3857        match table.modules.get(&cur_name) {
3858            Some(m) => {
3859                // c:1665 — if ((flags & FINDMOD_ALIASP) && (m->node.flags & MOD_ALIAS))
3860                if (flags & FINDMOD_ALIASP) != 0 && (m.node.flags & MOD_ALIAS) != 0 {
3861                    // c:1668 — return find_module(m->u.alias, flags, namep);
3862                    if let Some(target) = m.alias.clone() {
3863                        cur_name = target;
3864                        continue;
3865                    }
3866                    return None;
3867                }
3868                // c:1671 — *namep = m->node.nam; return m;
3869                return Some(cur_name);
3870            }
3871            None => {
3872                // c:1674 — if (!(flags & FINDMOD_CREATE)) return NULL;
3873                if (flags & FINDMOD_CREATE) == 0 {
3874                    return None;
3875                }
3876                // c:1676-1677 — m = zshcalloc(...); addnode(name, m);
3877                // zshcalloc zero-fills: the created node has flags=0
3878                // and NULL handle/linked — a bookkeeping entry (alias
3879                // targets, autoload owners), NOT a loadable module.
3880                // module::new() seeds MOD_LINKED (the register_module
3881                // shape, c:359); that bit wrongly made phantom nodes
3882                // (`zmodload -ab zsh/bogus x`) pass try_load_module's
3883                // loadable gate and "boot" successfully.
3884                let mut m = module::new(&cur_name);
3885                m.node.flags = 0; // c:1676 zshcalloc — all-zero node
3886                table.modules.insert(cur_name.clone(), m);
3887                return Some(cur_name);
3888            }
3889        }
3890    }
3891}
3892
3893/// Port of `delete_module()` from `Src/module.c:1687`. — C decl `delete_module(Module m)`.
3894///
3895/// C body:
3896/// ```c
3897/// delete_module(Module m) {
3898///     modulestab->removenode(modulestab, m->node.nam);
3899///     modulestab->freenode(&m->node);
3900/// }
3901/// ```
3902///
3903/// Removes a module from the live `modulestab` and frees its node.
3904/// Rust port operates on the `ModuleTable` `modules` HashMap.
3905/// WARNING: param names don't match C — Rust=(table, name) vs C=(m)
3906pub fn delete_module(table: &mut modulestab, name: &str) -> i32 {
3907    // c:1687
3908    table.modules.remove(name); // c:1687 removenode
3909                                // c:1691 — freenode(&m->node) — Rust drops on `remove` return.
3910    0
3911}
3912
3913/// Port of `module_loaded()` from `Src/module.c:1703`. — C decl `module_loaded(const char *name)`.
3914///
3915/// C body:
3916/// ```c
3917/// module_loaded(const char *name)
3918/// {
3919///     Module m;
3920///     return ((m = find_module(name, FINDMOD_ALIASP, NULL)) &&
3921///             m->u.handle &&
3922///             !(m->node.flags & MOD_UNLOAD));
3923/// }
3924/// ```
3925///
3926/// Returns true (non-zero) if the named module is currently loaded.
3927///
3928/// Faithful port of c:1702-1710:
3929/// ```c
3930/// mod_export int
3931/// module_loaded(const char *name)
3932/// {
3933///     Module m;
3934///     return ((m = find_module(name, FINDMOD_ALIASP, NULL)) &&
3935///             m->u.handle &&
3936///             !(m->node.flags & MOD_UNLOAD));
3937/// }
3938/// ```
3939///
3940/// All three gates matter in zshrs's static-link path:
3941///   1. `find_module(FINDMOD_ALIASP)` — resolves aliases, returns the
3942///      target's modulestab entry (or None on miss).
3943///   2. `m->u.handle` — non-null when the module's setup has run; in
3944///      the Rust mirror, MOD_INIT_S being set is the structural
3945///      equivalent (no dlopen handle to test).
3946///   3. `!(m->node.flags & MOD_UNLOAD)` — MOD_UNLOAD is the
3947///      "registered + autoload-only" sentinel set by
3948///      register_builtin_modules for entries OUTSIDE
3949///      zsh_default_loaded (e.g. zsh/files, zsh/system, zsh/zftp).
3950///      Without this gate, `${modules[zsh/files]}` reads as "loaded"
3951///      from initial register even though no `zmodload zsh/files` has
3952///      run — diverging from `zsh -fc` which reports the names as
3953///      autoload-pending.
3954/// WARNING: param names don't match C — Rust=(table, name) vs C=(name)
3955pub fn module_loaded(table: &modulestab, name: &str) -> i32 {
3956    // c:1703
3957    // c:1707 — `find_module(name, FINDMOD_ALIASP, NULL)`: resolve
3958    // alias chains via the existing free-fn port (c:1659).
3959    // The Rust port returns Option<String> — Some(target) on hit.
3960    // Inline the resolution here so we can read the target's flags
3961    // without re-locking.
3962    let target = match table.modules.get(name) {
3963        Some(m) if (m.node.flags & MOD_ALIAS) != 0 => {
3964            // c:FINDMOD_ALIASP — chase alias.
3965            // m->u.alias is the alias target; the Rust mirror stores
3966            // it on `module::aliased` (set by zmodload -A). Probe.
3967            match m.alias.as_ref().and_then(|a| table.modules.get(a)) {
3968                Some(t) => t,
3969                None => return 0, // alias points nowhere → not loaded
3970            }
3971        }
3972        Some(m) => m,
3973        None => return 0, // c:1707-1709 — find_module miss.
3974    };
3975    // c:1708 — `m->u.handle` — non-null on a fully-loaded module.
3976    // Static-link analog: MOD_LINKED set + module entry exists in
3977    // modulestab means register_module fired (= setup ran). The
3978    // dlopen `u.handle` check translates to MOD_LINKED here because
3979    // every modulestab entry is a statically-linked module in
3980    // zshrs's compile-time-only loader.
3981    if (target.node.flags & MOD_LINKED) == 0 {
3982        return 0;
3983    }
3984    // c:1709 — `!(m->node.flags & MOD_UNLOAD)`. MOD_UNLOAD is set by
3985    // register_builtin_modules on the autoload-only subset
3986    // (zsh/files, zsh/system, zsh/zftp, …); cleared by an explicit
3987    // `zmodload NAME` once the user wants the module live.
3988    if (target.node.flags & MOD_UNLOAD) != 0 {
3989        return 0;
3990    }
3991    1
3992}
3993
3994/// Port of `dyn_setup_module()` from `Src/module.c:1726`. — C decl `dyn_setup_module(Module m)`.
3995///
3996/// C body: `return ((int (*)(int,Module,void*)) m->u.handle)(0, m, NULL);`
3997/// Op-code 0 = setup. AIX-only path that multiplexes all six module
3998/// hooks through one symbol; static-link path skips it entirely.
3999#[allow(unused_variables)]
4000pub fn dyn_setup_module(m: *const module) -> i32 {
4001    // c:1726
4002    0 // c:1726
4003}
4004
4005/// Port of `dyn_features_module()` from `Src/module.c:1733`. — C decl `dyn_features_module(Module m, char ***features)`.
4006///
4007/// C body: `return ((int (*)(int,Module,void*)) m->u.handle)(4, m, features);`
4008/// Op-code 4 = features.
4009#[allow(unused_variables)]
4010pub fn dyn_features_module(m: *const module, features: &mut Vec<String>) -> i32 {
4011    // c:1733
4012    0 // c:1733
4013}
4014
4015/// Port of `dyn_enables_module()` from `Src/module.c:1740`. — C decl `dyn_enables_module(Module m, int **enables)`.
4016///
4017/// C body: `return ((int (*)(int,Module,void*)) m->u.handle)(5, m, enables);`
4018/// Op-code 5 = enables.
4019#[allow(unused_variables)]
4020pub fn dyn_enables_module(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
4021    // c:1740
4022    0 // c:1733
4023}
4024
4025/// Port of `dyn_boot_module()` from `Src/module.c:1747`. — C decl `dyn_boot_module(Module m)`.
4026///
4027/// C body: `return ((int (*)(int,Module,void*)) m->u.handle)(1, m, NULL);`
4028/// Calls the dynamic module's exported entry-point with op-code 1
4029/// (boot). Static-link path: opcode dispatch unused, returns 0.
4030#[allow(unused_variables)]
4031pub fn dyn_boot_module(m: *const module) -> i32 {
4032    // c:1747
4033    0 // c:1754
4034}
4035
4036/// Port of `dyn_cleanup_module()` from `Src/module.c:1754`. — C decl `dyn_cleanup_module(Module m)`.
4037///
4038/// C body: `return ((int (*)(int,Module,void*)) m->u.handle)(2, m, NULL);`
4039/// Op-code 2 = cleanup.
4040#[allow(unused_variables)]
4041pub fn dyn_cleanup_module(m: *const module) -> i32 {
4042    // c:1754
4043    0 // c:1740
4044}
4045
4046/// Port of `dyn_finish_module()` from `Src/module.c:1761`. — C decl `dyn_finish_module(Module m)`.
4047/// C body: `return ((int (*)(int,Module,void*))
4048/// m->u.handle)(3, m, NULL);` — invokes the DSO entry point with
4049/// opcode 3 (finish). no-op in Rust: zshrs has no dlopen path, all
4050/// modules are statically linked, `m->u.handle` is always null, and
4051/// finish-time resource release happens at process exit. Static-link
4052/// path defers all DSO entry calls; the no-op return preserves
4053/// caller semantics (0 = success).
4054#[allow(unused_variables)]
4055pub fn dyn_finish_module(m: *const module) -> i32 {
4056    // c:1766
4057    // c:1768 — ((int (*)(int,Module,void*)) m->u.handle)(3, m, NULL).
4058    // Static modules: no handle, opcode 3 (finish) is a no-op.
4059    0 // c:1768 success
4060}
4061
4062/// Port of `module_func()` from `Src/module.c:1770`. — C decl `module_func(Module m, const char *name)`.
4063///
4064/// C body (DYNAMIC_NAME_CLASH_OK off — the typical case):
4065/// ```c
4066/// module_func(Module m, const char *name)
4067/// {
4068///     VARARR(char, buf, strlen(name) + strlen(m->node.nam)*2 + 1);
4069///     char const *p; char *q;
4070///     strcpy(buf, name);
4071///     q = strchr(buf, 0);
4072///     for(p = m->node.nam; *p; p++) {
4073///         if(*p == '/')      { *q++ = 'Q'; *q++ = 's'; }
4074///         else if(*p == '_') { *q++ = 'Q'; *q++ = 'u'; }
4075///         else if(*p == 'Q') { *q++ = 'Q'; *q++ = 'q'; }
4076///         else                 *q++ = *p;
4077///     }
4078///     *q = 0;
4079///     return (Module_generic_func) dlsym(m->u.handle, buf);
4080/// }
4081/// ```
4082///
4083/// Builds a mangled symbol name (`<name><module-name-mangled>`) and
4084/// dlsym's it. The mangling encodes `/` as `Qs`, `_` as `Qu`, `Q` as
4085/// `Qq` so e.g. `setup_zsh_random` becomes `setup_zshQurandom`.
4086///
4087/// Static-link path: dlsym not used; returns 0 (NULL handle).
4088#[allow(unused_variables)]
4089pub fn module_func(m: &module, name: &str) -> usize {
4090    // c:1770
4091    0 // c:1794 NULL
4092}
4093
4094/// Port of `setup_module()` from `Src/module.c:1884`. — C decl `setup_module(Module m)`.
4095///
4096/// C body:
4097/// ```c
4098/// setup_module(Module m) {
4099///     return ((m->node.flags & MOD_LINKED) ?
4100///             (m->u.linked->setup)(m) : dyn_setup_module(m));
4101/// }
4102/// ```
4103/// Symmetric with boot_module/cleanup_module/finish_module: routes
4104/// to the per-module `setup_(m)`. C `setup_` is the first lifecycle
4105/// hook called when load_module loads the module (before
4106/// do_module_features registers features and before boot_ runs).
4107/// Most per-module setup_ bodies are `return 0;` — exceptions
4108/// initialise module-private state (curses screen handle, regex
4109/// compile cache, etc.).
4110/// WARNING: param names don't match C — Rust=(_table, name) vs C=(m)
4111pub fn setup_module(_table: &mut modulestab, name: &str) -> i32 {
4112    // c:1884
4113    match name {
4114        "zsh/attr" => crate::ported::modules::attr::setup_(std::ptr::null()),
4115        "zsh/cap" => crate::ported::modules::cap::setup_(std::ptr::null()),
4116        "zsh/clone" => crate::ported::modules::clone::setup_(std::ptr::null()),
4117        "zsh/curses" => crate::ported::modules::curses::setup_(std::ptr::null()),
4118        "zsh/datetime" => crate::ported::modules::datetime::setup_(std::ptr::null()),
4119        "zsh/db/gdbm" => crate::ported::modules::db_gdbm::setup_(std::ptr::null()),
4120        "zsh/example" => crate::ported::modules::example::setup_(std::ptr::null()),
4121        "zsh/files" => crate::ported::modules::files::setup_(std::ptr::null()),
4122        "zsh/hlgroup" => crate::ported::modules::hlgroup::setup_(std::ptr::null()),
4123        "zsh/ksh93" => crate::ported::modules::ksh93::setup_(std::ptr::null()),
4124        "zsh/langinfo" => crate::ported::modules::langinfo::setup_(std::ptr::null()),
4125        "zsh/mapfile" => crate::ported::modules::mapfile::setup_(std::ptr::null()),
4126        "zsh/mathfunc" => crate::ported::modules::mathfunc::setup_(std::ptr::null()),
4127        "zsh/nearcolor" => crate::ported::modules::nearcolor::setup_(std::ptr::null()),
4128        "zsh/newuser" => crate::ported::modules::newuser::setup_(std::ptr::null()),
4129        "zsh/deltochar" => crate::ported::zle::deltochar::setup_(), // c:Zle/deltochar.c:90
4130        "zsh/parameter" => crate::ported::modules::parameter::setup_(std::ptr::null()),
4131        "zsh/param/private" => crate::ported::modules::param_private::setup_(std::ptr::null()),
4132        "zsh/pcre" => crate::ported::modules::pcre::setup_(std::ptr::null()),
4133        "zsh/random" => crate::ported::modules::random::setup_(std::ptr::null()),
4134        "zsh/regex" => crate::ported::modules::regex::setup_(std::ptr::null()),
4135        "zsh/net/socket" => crate::ported::modules::socket::setup_(std::ptr::null()),
4136        "zsh/stat" => crate::ported::modules::stat::setup_(std::ptr::null()),
4137        "zsh/system" => crate::ported::modules::system::setup_(std::ptr::null()),
4138        "zsh/net/tcp" => crate::ported::modules::tcp::setup_(std::ptr::null()),
4139        "zsh/termcap" => crate::ported::modules::termcap::setup_(std::ptr::null()),
4140        "zsh/terminfo" => crate::ported::modules::terminfo::setup_(std::ptr::null()),
4141        "zsh/watch" => crate::ported::modules::watch::setup_(std::ptr::null()),
4142        "zsh/zftp" => crate::ported::modules::zftp::setup_(std::ptr::null()),
4143        "zsh/zprof" => crate::ported::modules::zprof::setup_(std::ptr::null()),
4144        "zsh/zpty" => crate::ported::modules::zpty::setup_(std::ptr::null()),
4145        "zsh/zselect" => crate::ported::modules::zselect::setup_(std::ptr::null()),
4146        "zsh/zutil" => crate::ported::modules::zutil::setup_(std::ptr::null()),
4147        "zsh/compctl" => crate::ported::zle::compctl::setup_(),
4148        // c:Src/Zle/complete.c:1730 setup_ — clears the comp* string globals
4149        // and raises `hascompmod` (c:1746), the flag `docomplete` reads at
4150        // `zle_tricky.c:712`. Missing from this table, `setup_` never ran on
4151        // any load path, so the flag stayed 0 and `=ls<TAB>` under
4152        // `expand-or-complete` expanded even when the prefix was ambiguous.
4153        "zsh/complete" => crate::ported::zle::complete::setup_(std::ptr::null()),
4154        // c:Src/Zle/zle_main.c:2246-2288 — zle's `setup_` runs `init_thingies`
4155        // and assigns `$zle_bracketed_paste` (c:2276-2280). C reaches it from
4156        // here, the module-LOAD chain, so a shell that loaded `zsh/zle`
4157        // without entering the editor still has the parameter:
4158        //     zsh -f -c 'zmodload zsh/zle; print ${+zle_bracketed_paste}' -> 1
4159        //     zsh -f -c '                  print ${+zle_bracketed_paste}' -> 0
4160        // zshrs links zle statically, so `zleread` (zle_main.rs:1520) shares
4161        // the same one-shot guard for the editor-entry half of the contract.
4162        "zsh/zle" => {
4163            let mut r = 0;
4164            crate::ported::zle::zle_main::ZLE_MODULE_SETUP.call_once(|| {
4165                r = crate::ported::zle::zle_main::setup_(std::ptr::null());
4166            });
4167            r
4168        }
4169        _ => 0,
4170    }
4171}
4172
4173/// Port of `features_module()` from `Src/module.c:1892`. — C decl `features_module(Module m, char ***features)`.
4174///
4175/// C body:
4176/// ```c
4177/// features_module(Module m, char ***features) {
4178///     return ((m->node.flags & MOD_LINKED) ?
4179///             (m->u.linked->features)(m, features) :
4180///             dyn_features_module(m, features));
4181/// }
4182/// ```
4183///
4184/// Per-module features_ populates the out-array with each feature
4185/// the module provides — `b:bn`, `c:cn`, `C:Cn`, `f:fn`, `p:pn`
4186/// per featuresarray (Src/module.c:3279). Used by zmodload -L to
4187/// list feature surfaces.
4188/// WARNING: param names don't match C — Rust=(_table, name, features) vs C=(m, features)
4189pub fn features_module(_table: &mut modulestab, name: &str, features: &mut Vec<String>) -> i32 {
4190    // c:1892
4191    match name {
4192        "zsh/attr" => crate::ported::modules::attr::features_(std::ptr::null(), features),
4193        "zsh/cap" => crate::ported::modules::cap::features_(std::ptr::null(), features),
4194        "zsh/clone" => crate::ported::modules::clone::features_(std::ptr::null(), features),
4195        "zsh/curses" => crate::ported::modules::curses::features_(std::ptr::null(), features),
4196        "zsh/datetime" => crate::ported::modules::datetime::features_(std::ptr::null(), features),
4197        "zsh/db/gdbm" => crate::ported::modules::db_gdbm::features_(std::ptr::null(), features),
4198        "zsh/example" => crate::ported::modules::example::features_(std::ptr::null(), features),
4199        "zsh/files" => crate::ported::modules::files::features_(std::ptr::null(), features),
4200        "zsh/hlgroup" => crate::ported::modules::hlgroup::features_(std::ptr::null(), features),
4201        "zsh/ksh93" => crate::ported::modules::ksh93::features_(std::ptr::null(), features),
4202        "zsh/langinfo" => crate::ported::modules::langinfo::features_(std::ptr::null(), features),
4203        "zsh/mapfile" => crate::ported::modules::mapfile::features_(std::ptr::null(), features),
4204        "zsh/mathfunc" => crate::ported::modules::mathfunc::features_(std::ptr::null(), features),
4205        "zsh/nearcolor" => crate::ported::modules::nearcolor::features_(std::ptr::null(), features),
4206        "zsh/newuser" => crate::ported::modules::newuser::features_(std::ptr::null(), features),
4207        // c:Zle/deltochar.c:97-101 — `featuresarray` over an all-empty
4208        // `module_features` (c:79-85): an empty array, return 0.
4209        "zsh/deltochar" => crate::ported::zle::deltochar::features_(),
4210        "zsh/parameter" => crate::ported::modules::parameter::features_(std::ptr::null(), features),
4211        "zsh/param/private" => {
4212            crate::ported::modules::param_private::features_(std::ptr::null(), features)
4213        }
4214        "zsh/pcre" => crate::ported::modules::pcre::features_(std::ptr::null(), features),
4215        "zsh/random" => crate::ported::modules::random::features_(std::ptr::null(), features),
4216        "zsh/regex" => crate::ported::modules::regex::features_(std::ptr::null(), features),
4217        "zsh/net/socket" => crate::ported::modules::socket::features_(std::ptr::null(), features),
4218        "zsh/stat" => crate::ported::modules::stat::features_(std::ptr::null(), features),
4219        "zsh/system" => crate::ported::modules::system::features_(std::ptr::null(), features),
4220        "zsh/net/tcp" => crate::ported::modules::tcp::features_(std::ptr::null(), features),
4221        "zsh/termcap" => crate::ported::modules::termcap::features_(std::ptr::null(), features),
4222        "zsh/terminfo" => crate::ported::modules::terminfo::features_(std::ptr::null(), features),
4223        "zsh/watch" => crate::ported::modules::watch::features_(std::ptr::null(), features),
4224        // c:Src/Zle/zle_main.c:2286 — zsh/zle's features_() returns
4225        // b:zle/b:bindkey/b:vared + the p:BUFFER-family params via
4226        // featuresarray. Without this arm the dispatch fell to the
4227        // `_ => 0` default with an EMPTY features vec, so once any
4228        // zle/bindkey use marked the module loaded, the NEXT
4229        // autoloaded-builtin dispatch (ensurefeature → autofeatures
4230        // c:3558-3577 loaded-module check) found no `b:bindkey` in
4231        // the table and errored `module has no such feature` —
4232        // breaking every zinit plugin that calls `zle -N` before
4233        // `bindkey` (zsh-autopair, zsh-hist, zconvey, zui, …).
4234        "zsh/zle" => crate::ported::zle::zle_main::features_(std::ptr::null(), features),
4235        "zsh/zftp" => crate::ported::modules::zftp::features_(std::ptr::null(), features),
4236        "zsh/zprof" => crate::ported::modules::zprof::features_(std::ptr::null(), features),
4237        "zsh/zpty" => crate::ported::modules::zpty::features_(std::ptr::null(), features),
4238        "zsh/zselect" => crate::ported::modules::zselect::features_(std::ptr::null(), features),
4239        "zsh/zutil" => crate::ported::modules::zutil::features_(std::ptr::null(), features),
4240        // The three statically-linked completion modules have no per-module
4241        // features_() fn in the port, so they fell to `_ => 0` (EMPTY
4242        // features). That broke `ensurefeature`: calling an autoloadable
4243        // completion builtin (`compset`/`compadd` from a shell completer)
4244        // looked up `b:<name>` against its home module and errored "module
4245        // `zsh/X' has no such feature: `b:<name>'" (same class as the
4246        // zsh/sched `b:sched` error). The feature surface is `b:<builtin>`
4247        // per the C BUILTIN() homes (complete.c:1693-1694 / computil.c:5131-
4248        // 5138 / compctl.c:4006-4007).
4249        // c:Src/Zle/complete.c:1720-1726 — `module_features = { bintab,
4250        // …, cotab, …, NULL, 0, NULL, 0, 0 }`, so `featuresarray`
4251        // (c:3283-3308) emits the two `b:` rows in bintab order
4252        // (c:1693-1694) then the four `c:` rows in cotab order
4253        // (c:1698-1701: after, between, prefix, suffix). The `c:` rows
4254        // were missing, so `zmodload zsh/complete` after the boot
4255        // autofeatures replay hit do_module_features' FEAT_CHECKAUTO arm
4256        // (c:2024-2044) and cancelled all four autoloads with
4257        // "module `zsh/complete' has no such feature".
4258        "zsh/complete" => {
4259            for f in [
4260                "b:compadd",
4261                "b:compset",
4262                "c:after",   // c:1698
4263                "c:between", // c:1699
4264                "c:prefix",  // c:1700
4265                "c:suffix",  // c:1701
4266            ] {
4267                features.push(f.to_string());
4268            }
4269            0
4270        }
4271        // c:Src/Zle/zleparameter.c:137-143 — `module_features` carries
4272        // ONLY `partab` (c:131-135), so the feature surface is the two
4273        // `p:` rows in partab order.
4274        "zsh/zleparameter" => {
4275            for f in ["p:keymaps", "p:widgets"] {
4276                features.push(f.to_string());
4277            }
4278            0
4279        }
4280        "zsh/computil" => {
4281            for f in [
4282                "b:comparguments",
4283                "b:compdescribe",
4284                "b:compfiles",
4285                "b:compgroups",
4286                "b:compquote",
4287                "b:comptags",
4288                "b:comptry",
4289                "b:compvalues",
4290            ] {
4291                features.push(f.to_string());
4292            }
4293            0
4294        }
4295        "zsh/compctl" => {
4296            // c:3295-3296 — `featuresarray` walks `bn_list` IN TABLE ORDER,
4297            // and `bintab` (c:Src/Zle/compctl.c:4005-4008) is
4298            // `compcall` then `compctl`.
4299            for f in ["b:compcall", "b:compctl"] {
4300                features.push(f.to_string());
4301            }
4302            0
4303        }
4304        // The two statically-linked Builtins/ modules with builtins also
4305        // had no features_() fn → `_ => 0`. `zsh/sched` is the one the user
4306        // hit at session start: `zmodload zsh/sched; sched …` errored
4307        // "module `zsh/sched' has no such feature: `b:sched'". Feature
4308        // surface = `b:<builtin>` (sched.c:376 / rlimits.c limit/ulimit/
4309        // unlimit).
4310        // c:Src/Builtins/sched.c:387-393 — `module_features = { bintab,
4311        // …, NULL, 0, NULL, 0, partab, … }`: one `b:` row then the
4312        // `p:zsh_scheduled_events` paramdef (sched.c partab), which
4313        // `featuresarray` emits last (c:3304-3305).
4314        "zsh/sched" => {
4315            for f in ["b:sched", "p:zsh_scheduled_events"] {
4316                features.push(f.to_string());
4317            }
4318            0
4319        }
4320        "zsh/rlimits" => {
4321            for f in ["b:limit", "b:ulimit", "b:unlimit"] {
4322                features.push(f.to_string());
4323            }
4324            0
4325        }
4326        // c:Src/Zle/complist.c:3518-3524 — complist's `module_features` is
4327        // `{ NULL, 0, NULL, 0, NULL, 0, NULL, 0, 0 }`: four empty descriptor
4328        // lists and no abstract features. Its `features_` (c:3535-3538) still
4329        // returns 0, so `featuresarray` hands back an EMPTY (but present)
4330        // array and `zmodload -lF zsh/complist` prints nothing with rc 0.
4331        // complist's user surface is widgets + `$ZLS_COLORS`, neither of which
4332        // travels through the feature interface.
4333        //
4334        // This arm has to be explicit so the `_ =>` default below can carry
4335        // C's real "no features_ entry point" answer. Without it complist
4336        // would inherit that 1 and start claiming "does not support features".
4337        "zsh/complist" => 0, // c:Zle/complist.c:3537
4338        // c:305-320 — zsh/main is the `link=static` pseudo-module holding the
4339        // shell core, and its `features_` is one of the two in the tree that
4340        // returns 1 outright: "There are lots and lots of features, but
4341        // they're not handled here." (c:315-319). The other is
4342        // `Src/Modules/newuser.c:44-47`, whose arm dispatches to
4343        // `newuser::features_` above and gets the same 1 from there.
4344        //
4345        // zshrs answered 0-with-an-empty-vec, so `zmodload -lF zsh/main`
4346        // printed nothing and exited 0 where zsh prints
4347        // `module `zsh/main' does not support features` and exits 1
4348        // (the c:3118-3123 arm of `bin_zmodload_features`).
4349        "zsh/main" => 1, // c:319
4350        // c:1816-1825 `dyn_features_module` — a module with no `features_`
4351        // entry point yields 1, "not a user-visible error if no features
4352        // function" (c:1823). Every module zshrs links in that C gives a
4353        // `features_` to has an explicit arm above, so reaching here means
4354        // the name has no feature interface at all and 1 is the faithful
4355        // answer. Returning 0 instead left the caller with an EMPTY feature
4356        // array that reads as "supports features, has none" — a different
4357        // statement, and the one that silenced the zsh/main diagnostic.
4358        _ => 1, // c:1824
4359    }
4360}
4361
4362/// Port of `enables_module()` from `Src/module.c:1901`. — C decl `enables_module(Module m, int **enables)`.
4363///
4364/// C body:
4365/// ```c
4366/// enables_module(Module m, int **enables) {
4367///     return ((m->node.flags & MOD_LINKED) ?
4368///             (m->u.linked->enables)(m, enables) :
4369///             dyn_enables_module(m, enables));
4370/// }
4371/// ```
4372///
4373/// Per-module enables_ populates the enable-bit array parallel to
4374/// the features_ surface — getfeatureenables (c:3314) emits 1 for
4375/// each currently-active feature (BINF_ADDED / CONDF_ADDED /
4376/// MFF_ADDED / pd->pm non-null), 0 otherwise. Used by zmodload -L
4377/// to report enabled-vs-disabled features per module.
4378/// WARNING: param names don't match C — Rust=(_table, name, enables) vs C=(m, enables)
4379pub fn enables_module(table: &mut modulestab, name: &str, enables: &mut Option<Vec<i32>>) -> i32 {
4380    // c:1901
4381    match name {
4382        "zsh/attr" => crate::ported::modules::attr::enables_(std::ptr::null(), enables),
4383        "zsh/cap" => crate::ported::modules::cap::enables_(std::ptr::null(), enables),
4384        "zsh/clone" => crate::ported::modules::clone::enables_(std::ptr::null(), enables),
4385        "zsh/curses" => crate::ported::modules::curses::enables_(std::ptr::null(), enables),
4386        "zsh/datetime" => crate::ported::modules::datetime::enables_(std::ptr::null(), enables),
4387        "zsh/db/gdbm" => crate::ported::modules::db_gdbm::enables_(std::ptr::null(), enables),
4388        "zsh/example" => crate::ported::modules::example::enables_(std::ptr::null(), enables),
4389        "zsh/files" => crate::ported::modules::files::enables_(std::ptr::null(), enables),
4390        "zsh/hlgroup" => crate::ported::modules::hlgroup::enables_(std::ptr::null(), enables),
4391        "zsh/ksh93" => crate::ported::modules::ksh93::enables_(std::ptr::null(), enables),
4392        "zsh/langinfo" => crate::ported::modules::langinfo::enables_(std::ptr::null(), enables),
4393        "zsh/mapfile" => crate::ported::modules::mapfile::enables_(std::ptr::null(), enables),
4394        "zsh/mathfunc" => crate::ported::modules::mathfunc::enables_(std::ptr::null(), enables),
4395        "zsh/nearcolor" => crate::ported::modules::nearcolor::enables_(std::ptr::null(), enables),
4396        "zsh/newuser" => crate::ported::modules::newuser::enables_(std::ptr::null(), enables),
4397        "zsh/deltochar" => crate::ported::zle::deltochar::enables_(), // c:Zle/deltochar.c:105
4398        "zsh/parameter" => crate::ported::modules::parameter::enables_(std::ptr::null(), enables),
4399        "zsh/param/private" => {
4400            crate::ported::modules::param_private::enables_(std::ptr::null(), enables)
4401        }
4402        "zsh/pcre" => crate::ported::modules::pcre::enables_(std::ptr::null(), enables),
4403        "zsh/random" => crate::ported::modules::random::enables_(std::ptr::null(), enables),
4404        "zsh/regex" => crate::ported::modules::regex::enables_(std::ptr::null(), enables),
4405        "zsh/net/socket" => crate::ported::modules::socket::enables_(std::ptr::null(), enables),
4406        "zsh/stat" => crate::ported::modules::stat::enables_(std::ptr::null(), enables),
4407        "zsh/system" => crate::ported::modules::system::enables_(std::ptr::null(), enables),
4408        "zsh/net/tcp" => crate::ported::modules::tcp::enables_(std::ptr::null(), enables),
4409        "zsh/termcap" => crate::ported::modules::termcap::enables_(std::ptr::null(), enables),
4410        "zsh/terminfo" => crate::ported::modules::terminfo::enables_(std::ptr::null(), enables),
4411        "zsh/watch" => crate::ported::modules::watch::enables_(std::ptr::null(), enables),
4412        "zsh/zftp" => crate::ported::modules::zftp::enables_(std::ptr::null(), enables),
4413        "zsh/zprof" => crate::ported::modules::zprof::enables_(std::ptr::null(), enables),
4414        "zsh/zpty" => crate::ported::modules::zpty::enables_(std::ptr::null(), enables),
4415        "zsh/zselect" => crate::ported::modules::zselect::enables_(std::ptr::null(), enables),
4416        "zsh/zutil" => crate::ported::modules::zutil::enables_(std::ptr::null(), enables),
4417        // c:Src/Zle/complete.c:1751 — `handlefeatures(m, &module_features,
4418        // enables)`. This is the arm that installs the four cotab
4419        // conddefs (`-prefix`/`-suffix`/`-after`/`-between`), replacing the
4420        // `c:` autoload stubs the boot `autofeatures` replay planted.
4421        // Falling to `_ => 0` left them installed forever, so
4422        // `zmodload zsh/complete; zmodload -ac` listed all four where
4423        // `zsh -f` lists none.
4424        "zsh/complete" => crate::ported::zle::complete::enables_(std::ptr::null(), enables),
4425        // c:1901 — `enables_module` is the `m->u.linked->enables` call, and
4426        // EVERY module that answers `features_module` must answer this too:
4427        // `bin_zmodload_features` (c:3163-3167) bails out of the listing when
4428        // `enables_module` fails, and treats a left-alone `enables` as an
4429        // all-zero bitmap. These six fell to `_ => 0` below, so a LOADED
4430        // zsh/zle / zsh/computil listed every builtin as `-` where zsh lists
4431        // `+`.
4432        "zsh/zle" => crate::ported::zle::zle_main::enables_(std::ptr::null(), enables),
4433        "zsh/sched" => crate::ported::builtins::sched::enables_(std::ptr::null(), enables),
4434        "zsh/rlimits" => crate::ported::builtins::rlimits::enables_(std::ptr::null(), enables),
4435        // These three keep their feature list inline in `features_module`
4436        // above rather than in a per-module `features_()`, so the enables
4437        // half reads it back from there and hands it to the name-keyed
4438        // `handlefeatures` (c:3392) that holds the per-feature ADDED bit.
4439        "zsh/computil" | "zsh/zleparameter" | "zsh/compctl" => {
4440            let mut feats: Vec<String> = Vec::new();
4441            if features_module(table, name, &mut feats) != 0 {
4442                return 1; // c:3164
4443            }
4444            handlefeatures(name, &feats, enables) // c:3396
4445        }
4446        _ => 0,
4447    }
4448}
4449
4450/// Port of `boot_module()` from `Src/module.c:1910`. — C decl `boot_module(Module m)`.
4451///
4452/// C body:
4453/// ```c
4454/// boot_module(Module m) {
4455///     return ((m->node.flags & MOD_LINKED) ?
4456///             (m->u.linked->boot)(m) : dyn_boot_module(m));
4457/// }
4458/// ```
4459///
4460/// Static-link path: every modulestab entry is MOD_LINKED, so the
4461/// branch collapses to `(m->u.linked->boot)(m)`. The Rust analog
4462/// of C's `m->u.linked->boot` function-pointer is the per-module
4463/// `boot_(m)` defined in `src/ported/modules/<name>.rs`. Dispatch
4464/// via a static name → fn-pointer table.
4465///
4466/// Modules not in the dispatch table (because their per-module
4467/// boot_ isn't ported yet) fall through to 0 — same observable
4468/// outcome as a no-op boot, matching pre-port behaviour.
4469/// WARNING: param names don't match C — Rust=(_table, name) vs C=(m)
4470pub fn boot_module(_table: &mut modulestab, name: &str) -> i32 {
4471    // c:1910
4472    // c:1912 — `(m->u.linked->boot)(m)` for MOD_LINKED.
4473    // The per-module boot_ entry points carry the partab/bintab
4474    // dispatch that addparamdef/addbuiltins would otherwise run
4475    // for a dlopen'd module (e.g. watch.rs::boot_ seeds
4476    // WATCH/watch in paramtab + installs the checksched preprompt
4477    // hook). NULL module pointer is fine — every per-module boot_
4478    // either ignores the arg or null-checks before deref.
4479    match name {
4480        "zsh/attr" => crate::ported::modules::attr::boot_(std::ptr::null()),
4481        "zsh/cap" => crate::ported::modules::cap::boot_(std::ptr::null()),
4482        "zsh/clone" => crate::ported::modules::clone::boot_(std::ptr::null()),
4483        // c:Src/Zle/complist.c:3566 boot_ — installs the `menuselect` and
4484        // `listscroll` keymaps (via menuselect_bindings). Without this,
4485        // `zmodload zsh/complist` was a no-op and every `bindkey -M
4486        // menuselect …` (zpwr's completion-menu keybindings) errored
4487        // "no such keymap `menuselect'".
4488        // c:Src/Zle/complete.c:1758 boot_ — registers the six completion
4489        // hookfns and (via the cotab half of `handlefeatures`,
4490        // c:3364-3369) installs the four `-prefix`/`-suffix`/`-after`/
4491        // `-between` conddefs that replace the `c:` autoload stubs.
4492        // Missing from this table, `zmodload zsh/complete` left all four
4493        // stubs in condtab where `zsh -f` clears them.
4494        "zsh/complete" => crate::ported::zle::complete::boot_(std::ptr::null()),
4495        "zsh/complist" => crate::ported::zle::complist::boot_(),
4496        "zsh/curses" => crate::ported::modules::curses::boot_(std::ptr::null()),
4497        "zsh/datetime" => crate::ported::modules::datetime::boot_(std::ptr::null()),
4498        "zsh/db/gdbm" => crate::ported::modules::db_gdbm::boot_(std::ptr::null()),
4499        "zsh/example" => crate::ported::modules::example::boot_(std::ptr::null()),
4500        "zsh/files" => crate::ported::modules::files::boot_(std::ptr::null()),
4501        "zsh/hlgroup" => crate::ported::modules::hlgroup::boot_(std::ptr::null()),
4502        "zsh/ksh93" => crate::ported::modules::ksh93::boot_(std::ptr::null()),
4503        "zsh/langinfo" => crate::ported::modules::langinfo::boot_(std::ptr::null()),
4504        "zsh/mapfile" => crate::ported::modules::mapfile::boot_(std::ptr::null()),
4505        "zsh/mathfunc" => crate::ported::modules::mathfunc::boot_(std::ptr::null()),
4506        "zsh/nearcolor" => crate::ported::modules::nearcolor::boot_(std::ptr::null()),
4507        "zsh/newuser" => crate::ported::modules::newuser::boot_(std::ptr::null()),
4508        "zsh/deltochar" => crate::ported::zle::deltochar::boot_(), // c:Zle/deltochar.c:112
4509        "zsh/parameter" => crate::ported::modules::parameter::boot_(std::ptr::null()),
4510        "zsh/param/private" => crate::ported::modules::param_private::boot_(std::ptr::null()),
4511        "zsh/pcre" => crate::ported::modules::pcre::boot_(std::ptr::null()),
4512        "zsh/random" => crate::ported::modules::random::boot_(std::ptr::null()),
4513        "zsh/regex" => crate::ported::modules::regex::boot_(std::ptr::null()),
4514        // c:Src/Builtins/sched.c:421 boot_ — `addprepromptfn(&checksched)`.
4515        // That single line is the ONLY thing that arms the schedule at
4516        // prompt time; `sched.mdd` is `load=yes` with `autofeatures="b:sched
4517        // p:zsh_scheduled_events"`, so C boots the module the first time
4518        // `sched` is run (`$modules[zsh/sched]` goes `autoloaded` ->
4519        // `loaded`). With no arm here `sched::boot_` had zero callers, so a
4520        // due entry was never checked between commands — only the
4521        // `addtimedfn` deadline inside ZLE's read loop ever reached
4522        // `checksched`, and a shell that was NOT idle in the editor (ZLE
4523        // off, or busy running a command when the deadline passed) never
4524        // ran the command at all.
4525        "zsh/sched" => crate::ported::builtins::sched::boot_(std::ptr::null()),
4526        "zsh/net/socket" => crate::ported::modules::socket::boot_(std::ptr::null()),
4527        "zsh/stat" => crate::ported::modules::stat::boot_(std::ptr::null()),
4528        "zsh/system" => crate::ported::modules::system::boot_(std::ptr::null()),
4529        "zsh/net/tcp" => crate::ported::modules::tcp::boot_(std::ptr::null()),
4530        "zsh/termcap" => crate::ported::modules::termcap::boot_(std::ptr::null()),
4531        "zsh/terminfo" => crate::ported::modules::terminfo::boot_(std::ptr::null()),
4532        // c:1912 — C passes the real Module m to boot. watch::boot_
4533        // reads m->node.flags (MOD_SETUP = mid-load_module) to gate
4534        // its WATCHFMT/LOGCHECK seeding under --zsh; passing the
4535        // table entry avoids re-locking MODULESTAB (the caller chain
4536        // load_module -> do_boot_module already holds it).
4537        "zsh/watch" => {
4538            let mptr = _table
4539                .modules
4540                .get(name)
4541                .map(|m| m as *const crate::ported::zsh_h::module)
4542                .unwrap_or(std::ptr::null());
4543            crate::ported::modules::watch::boot_(mptr)
4544        }
4545        "zsh/zftp" => crate::ported::modules::zftp::boot_(std::ptr::null()),
4546        "zsh/zprof" => crate::ported::modules::zprof::boot_(std::ptr::null()),
4547        "zsh/zpty" => crate::ported::modules::zpty::boot_(std::ptr::null()),
4548        "zsh/zselect" => crate::ported::modules::zselect::boot_(std::ptr::null()),
4549        "zsh/zutil" => crate::ported::modules::zutil::boot_(std::ptr::null()),
4550        // Modules without a ported per-module boot_ (e.g. zsh/main —
4551        // purely-static modules with no setup hook):
4552        // 0 == success no-op, matching the pre-port behaviour.
4553        _ => 0,
4554    }
4555}
4556
4557/// Port of `cleanup_module()` from `Src/module.c:1918`. — C decl `cleanup_module(Module m)`.
4558///
4559/// C body:
4560/// ```c
4561/// cleanup_module(Module m) {
4562///     return ((m->node.flags & MOD_LINKED) ?
4563///             (m->u.linked->cleanup)(m) : dyn_cleanup_module(m));
4564/// }
4565/// ```
4566///
4567/// Static-link path: dispatch via name → fn-pointer table to the
4568/// per-module `cleanup_(m)` defined in `src/ported/modules/<name>.rs`.
4569/// Mirrors the symmetric `boot_module` dispatcher (`Src/module.c:1910`).
4570/// Per-module cleanup_ typically calls `delprepromptfn` /
4571/// `setfeatureenables(NULL)` to roll back what boot_ installed.
4572/// WARNING: param names don't match C — Rust=(_table, name) vs C=(m)
4573pub fn cleanup_module(_table: &mut modulestab, name: &str) -> i32 {
4574    // c:1918
4575    match name {
4576        "zsh/attr" => crate::ported::modules::attr::cleanup_(std::ptr::null()),
4577        "zsh/cap" => crate::ported::modules::cap::cleanup_(std::ptr::null()),
4578        "zsh/clone" => crate::ported::modules::clone::cleanup_(std::ptr::null()),
4579        "zsh/curses" => crate::ported::modules::curses::cleanup_(std::ptr::null()),
4580        "zsh/datetime" => crate::ported::modules::datetime::cleanup_(std::ptr::null()),
4581        "zsh/db/gdbm" => crate::ported::modules::db_gdbm::cleanup_(std::ptr::null()),
4582        "zsh/example" => crate::ported::modules::example::cleanup_(std::ptr::null()),
4583        "zsh/files" => crate::ported::modules::files::cleanup_(std::ptr::null()),
4584        "zsh/hlgroup" => crate::ported::modules::hlgroup::cleanup_(std::ptr::null()),
4585        "zsh/ksh93" => crate::ported::modules::ksh93::cleanup_(std::ptr::null()),
4586        "zsh/langinfo" => crate::ported::modules::langinfo::cleanup_(std::ptr::null()),
4587        "zsh/mapfile" => crate::ported::modules::mapfile::cleanup_(std::ptr::null()),
4588        "zsh/mathfunc" => crate::ported::modules::mathfunc::cleanup_(std::ptr::null()),
4589        "zsh/nearcolor" => crate::ported::modules::nearcolor::cleanup_(std::ptr::null()),
4590        "zsh/newuser" => crate::ported::modules::newuser::cleanup_(std::ptr::null()),
4591        "zsh/deltochar" => crate::ported::zle::deltochar::cleanup_(), // c:Zle/deltochar.c:129
4592        "zsh/parameter" => crate::ported::modules::parameter::cleanup_(std::ptr::null()),
4593        "zsh/param/private" => crate::ported::modules::param_private::cleanup_(std::ptr::null()),
4594        "zsh/pcre" => crate::ported::modules::pcre::cleanup_(std::ptr::null()),
4595        "zsh/random" => crate::ported::modules::random::cleanup_(std::ptr::null()),
4596        "zsh/regex" => crate::ported::modules::regex::cleanup_(std::ptr::null()),
4597        // c:Src/Builtins/sched.c:438 cleanup_ — `delprepromptfn(&checksched)`
4598        // plus the free of every pending entry. Symmetric with the boot_ arm
4599        // above: without it a `zmodload -u zsh/sched` would leave the
4600        // preprompt hook installed and re-loading would push a second copy.
4601        "zsh/sched" => crate::ported::builtins::sched::cleanup_(std::ptr::null()),
4602        "zsh/net/socket" => crate::ported::modules::socket::cleanup_(std::ptr::null()),
4603        "zsh/stat" => crate::ported::modules::stat::cleanup_(std::ptr::null()),
4604        "zsh/system" => crate::ported::modules::system::cleanup_(std::ptr::null()),
4605        "zsh/net/tcp" => crate::ported::modules::tcp::cleanup_(std::ptr::null()),
4606        "zsh/termcap" => crate::ported::modules::termcap::cleanup_(std::ptr::null()),
4607        "zsh/terminfo" => crate::ported::modules::terminfo::cleanup_(std::ptr::null()),
4608        "zsh/watch" => crate::ported::modules::watch::cleanup_(std::ptr::null()),
4609        "zsh/zftp" => crate::ported::modules::zftp::cleanup_(std::ptr::null()),
4610        "zsh/zprof" => crate::ported::modules::zprof::cleanup_(std::ptr::null()),
4611        "zsh/zpty" => crate::ported::modules::zpty::cleanup_(std::ptr::null()),
4612        "zsh/zselect" => crate::ported::modules::zselect::cleanup_(std::ptr::null()),
4613        "zsh/zutil" => crate::ported::modules::zutil::cleanup_(std::ptr::null()),
4614        _ => 0,
4615    }
4616}
4617
4618/// Port of `finish_module()` from `Src/module.c:1926`. — C decl `finish_module(Module m)`.
4619///
4620/// C body:
4621/// ```c
4622/// finish_module(Module m) {
4623///     return ((m->node.flags & MOD_LINKED) ?
4624///             (m->u.linked->finish)(m) : dyn_finish_module(m));
4625/// }
4626/// ```
4627///
4628/// Static-link path: dispatch to the per-module `finish_(m)`.
4629/// finish_ is the second teardown step (after cleanup_) — it frees
4630/// process-lifetime state that survives a single `zmodload -u`.
4631/// Most modules have an empty body returning 0; a few (zftp, zpty,
4632/// watch with utmpx descriptors) keep state explicitly.
4633/// WARNING: param names don't match C — Rust=(_table, name) vs C=(m)
4634pub fn finish_module(_table: &mut modulestab, name: &str) -> i32 {
4635    // c:1926
4636    match name {
4637        "zsh/attr" => crate::ported::modules::attr::finish_(std::ptr::null()),
4638        "zsh/cap" => crate::ported::modules::cap::finish_(std::ptr::null()),
4639        "zsh/clone" => crate::ported::modules::clone::finish_(std::ptr::null()),
4640        "zsh/curses" => crate::ported::modules::curses::finish_(std::ptr::null()),
4641        "zsh/datetime" => crate::ported::modules::datetime::finish_(std::ptr::null()),
4642        "zsh/db/gdbm" => crate::ported::modules::db_gdbm::finish_(std::ptr::null()),
4643        "zsh/example" => crate::ported::modules::example::finish_(std::ptr::null()),
4644        "zsh/files" => crate::ported::modules::files::finish_(std::ptr::null()),
4645        "zsh/hlgroup" => crate::ported::modules::hlgroup::finish_(std::ptr::null()),
4646        "zsh/ksh93" => crate::ported::modules::ksh93::finish_(std::ptr::null()),
4647        "zsh/langinfo" => crate::ported::modules::langinfo::finish_(std::ptr::null()),
4648        "zsh/mapfile" => crate::ported::modules::mapfile::finish_(std::ptr::null()),
4649        "zsh/mathfunc" => crate::ported::modules::mathfunc::finish_(std::ptr::null()),
4650        "zsh/nearcolor" => crate::ported::modules::nearcolor::finish_(std::ptr::null()),
4651        "zsh/newuser" => crate::ported::modules::newuser::finish_(std::ptr::null()),
4652        "zsh/deltochar" => crate::ported::zle::deltochar::finish_(), // c:Zle/deltochar.c:138
4653        "zsh/parameter" => crate::ported::modules::parameter::finish_(std::ptr::null()),
4654        "zsh/param/private" => crate::ported::modules::param_private::finish_(std::ptr::null()),
4655        "zsh/pcre" => crate::ported::modules::pcre::finish_(std::ptr::null()),
4656        "zsh/random" => crate::ported::modules::random::finish_(std::ptr::null()),
4657        "zsh/regex" => crate::ported::modules::regex::finish_(std::ptr::null()),
4658        "zsh/net/socket" => crate::ported::modules::socket::finish_(std::ptr::null()),
4659        "zsh/stat" => crate::ported::modules::stat::finish_(std::ptr::null()),
4660        "zsh/system" => crate::ported::modules::system::finish_(std::ptr::null()),
4661        "zsh/net/tcp" => crate::ported::modules::tcp::finish_(std::ptr::null()),
4662        "zsh/termcap" => crate::ported::modules::termcap::finish_(std::ptr::null()),
4663        "zsh/terminfo" => crate::ported::modules::terminfo::finish_(std::ptr::null()),
4664        "zsh/watch" => crate::ported::modules::watch::finish_(std::ptr::null()),
4665        "zsh/zftp" => crate::ported::modules::zftp::finish_(std::ptr::null()),
4666        "zsh/zprof" => crate::ported::modules::zprof::finish_(std::ptr::null()),
4667        "zsh/zpty" => crate::ported::modules::zpty::finish_(std::ptr::null()),
4668        "zsh/zselect" => crate::ported::modules::zselect::finish_(std::ptr::null()),
4669        "zsh/zutil" => crate::ported::modules::zutil::finish_(std::ptr::null()),
4670        _ => 0,
4671    }
4672}
4673
4674/// Port of `do_module_features()` from `Src/module.c:1998`. — C decl `do_module_features(Module m, Feature_enables enablesarr, int flags)`.
4675///
4676/// C body c:1998-2125 (128 lines):
4677/// ```c
4678/// if (features_module(m, &features) == 0) {
4679///     int *enables = NULL;
4680///     if (enables_module(m, &enables)) {
4681///         if (!(flags & FEAT_IGNORE)) zwarn(...);
4682///         return 1;
4683///     }
4684///     if ((flags & FEAT_CHECKAUTO) && m->autoloads) {
4685///         /* validate autoloads against features list */
4686///         /* on mismatch: zwarn + autofeatures(REMOVE|IGNORE) +
4687///            expunge from enablesarr */
4688///     }
4689///     if (enablesarr) {
4690///         /* walk enablesarr, flip enables bits per +/- prefix */
4691///     } else {
4692///         /* enable all features */
4693///     }
4694///     if (enables_module(m, &enables)) return 2;
4695/// } else if (enablesarr) {
4696///     if (!(flags & FEAT_IGNORE)) zwarn("module does not support features");
4697///     return 1;
4698/// }
4699/// return ret;
4700/// ```
4701///
4702/// Prior Rust port confused module-name and enables-string into a
4703/// single `enablesarr: &str` param — the module-lookup and zwarn
4704/// diagnostic both used the feature-list string instead of the
4705/// module's name. Signature now separates them per C semantics:
4706/// modname identifies the module, features is the list of features
4707/// to enable (None = "enable all").
4708/// WARNING: param names don't match C — Rust=(table, modname, features, flags) vs C=(m, enablesarr, flags)
4709pub fn do_module_features(
4710    table: &mut modulestab,
4711    modname: &str,
4712    features: Option<&[String]>,
4713    flags: i32,
4714) -> i32 {
4715    // c:1998
4716    let mut module_features: Vec<String> = Vec::new(); // c:2000
4717    let mut ret: i32 = 0; // c:2001
4718    // C edits the caller's `enablesarr` in place (c:2059-2066) and no
4719    // caller reads it afterwards, so a local copy carries the same effect.
4720    let mut features: Option<Vec<String>> = features.map(|a| a.to_vec());
4721
4722    // c:2003 — `if (features_module(m, &features) == 0)`.
4723    if features_module(table, modname, &mut module_features) == 0 {
4724        // c:2011-2018 — fetch enables. Features supported → enables
4725        // should be too; error here is reported unless FEAT_IGNORE.
4726        let mut enables: Option<Vec<i32>> = None;
4727        if enables_module(table, modname, &mut enables) != 0 {
4728            // c:2012
4729            if (flags & FEAT_IGNORE) == 0 {
4730                // c:2014
4731                zwarn(&format!(
4732                    "error getting enabled features for module `{}'", // c:2015
4733                    modname
4734                ));
4735            }
4736            return 1; // c:2017
4737        }
4738
4739        // c:2020 — `if ((flags & FEAT_CHECKAUTO) && m->autoloads)`
4740        if (flags & FEAT_CHECKAUTO) != 0 {
4741            let autoloads: Vec<String> = table
4742                .modules
4743                .get(modname)
4744                .and_then(|m| m.autoloads.as_ref())
4745                .map(|al| al.iter().cloned().collect())
4746                .unwrap_or_default();
4747            // c:2027-2074 — walk autoloads, cancel mismatches.
4748            for al in &autoloads {
4749                // c:2028
4750                // c:2032-2034 — match `al` against the features array.
4751                let found = module_features.iter().any(|f| f == al);
4752                if !found {
4753                    // c:2035
4754                    if (flags & FEAT_IGNORE) == 0 {
4755                        // c:2037
4756                        zwarn(&format!(
4757                            "module `{}' has no such feature: `{}': autoload cancelled", // c:2038-2040
4758                            modname, al
4759                        ));
4760                    }
4761                    // c:2045-2047 — autofeatures(NULL, m->node.nam, arg, 0, FEAT_IGNORE|FEAT_REMOVE)
4762                    let arg = vec![al.clone()];
4763                    autofeatures(table, "", Some(modname), &arg, 0, FEAT_IGNORE | FEAT_REMOVE);
4764                    /*
4765                     * don't want to try to enable *that*...
4766                     * expunge it from the enable string.
4767                     */
4768                    // c:2053-2072 — without this the enables walk below
4769                    // warned a second time ("has no such feature: `b:fail'")
4770                    // for the feature the autoload just cancelled.
4771                    if let Some(arr) = features.as_mut() {
4772                        // c:2055-2071
4773                        let pos = arr.iter().position(|fep| {
4774                            let s = fep
4775                                .strip_prefix('+')
4776                                .or_else(|| fep.strip_prefix('-'))
4777                                .unwrap_or(fep); // c:2057-2058
4778                            if (flags & FEAT_PATTERN_ARGS) != 0 {
4779                                let mut pat_src = crate::ported::string::dupstring(s);
4780                                crate::ported::glob::tokenize(&mut pat_src);
4781                                crate::ported::pattern::patcompile(
4782                                    &pat_src,
4783                                    crate::ported::zsh_h::PAT_STATIC,
4784                                    None,
4785                                )
4786                                .is_some_and(|p| crate::ported::pattern::pattry(&p, al))
4787                            } else {
4788                                al == s // c:2059-2060
4789                            }
4790                        });
4791                        if let Some(i) = pos {
4792                            /* can't enable it after all, so return 1 */
4793                            ret = 1; // c:2062
4794                            // c:2063-2067 — shift the rest down; the loop
4795                            // leaves `fep` on the terminator, so c:2068
4796                            // `if (!fep->pat) break;` always stops here.
4797                            arr.remove(i);
4798                        }
4799                    }
4800                }
4801            }
4802        }
4803
4804        // c:2077-2113 — apply enablesarr (or enable all).
4805        match &features {
4806            Some(arr) => {
4807                // c:2079-2103 — walk enablesarr.
4808                let enables_vec = enables.get_or_insert_with(Vec::new);
4809                if enables_vec.len() < module_features.len() {
4810                    enables_vec.resize(module_features.len(), 0);
4811                }
4812                for fep_str in arr {
4813                    // c:2079 for (fep = enablesarr; fep->str; fep++)
4814                    let (on, esp) = if let Some(rest) = fep_str.strip_prefix('+') {
4815                        // c:2082-2083
4816                        (1i32, rest)
4817                    } else if let Some(rest) = fep_str.strip_prefix('-') {
4818                        // c:2084-2086
4819                        (0i32, rest)
4820                    } else {
4821                        (1i32, fep_str.as_str())
4822                    };
4823                    // c:2088-2094 —
4824                    //   for (fp = features; *fp; fp++)
4825                    //       if (fep->pat ? pattry(fep->pat, *fp)
4826                    //                    : !strcmp(*fp, esp)) {
4827                    //           enables[fp - features] = on;
4828                    //           found++;
4829                    //           if (!fep->pat) break;
4830                    //       }
4831                    // `fep->pat` is non-NULL exactly when `zmodload -m`
4832                    // was given (c:3258); zshrs carries that as
4833                    // FEAT_PATTERN_ARGS (see its doc comment for why).
4834                    // A pattern keeps scanning — it may enable several
4835                    // features — while an exact name stops at the first
4836                    // hit.
4837                    let pat = if (flags & FEAT_PATTERN_ARGS) != 0 {
4838                        let mut pat_src = crate::ported::string::dupstring(esp);
4839                        crate::ported::glob::tokenize(&mut pat_src);
4840                        crate::ported::pattern::patcompile(
4841                            &pat_src,
4842                            crate::ported::zsh_h::PAT_STATIC,
4843                            None,
4844                        )
4845                    } else {
4846                        None
4847                    };
4848                    let mut found = false;
4849                    for (i, f) in module_features.iter().enumerate() {
4850                        let hit = match pat.as_ref() {
4851                            Some(p) => crate::ported::pattern::pattry(p, f), // c:2093
4852                            None => f == esp,                                // c:2093
4853                        };
4854                        if hit {
4855                            enables_vec[i] = on; // c:2090
4856                            found = true;
4857                            if pat.is_none() {
4858                                break; // c:2096-2097 `if (!fep->pat) break;`
4859                            }
4860                        }
4861                    }
4862                    if !found {
4863                        // c:2099-2106 — the diagnostic differs for the
4864                        // pattern form.
4865                        if (flags & FEAT_IGNORE) == 0 {
4866                            zwarn(&format!(
4867                                "module `{}' has no {}: `{}'",
4868                                modname,
4869                                if pat.is_some() {
4870                                    "feature matching" // c:2102
4871                                } else {
4872                                    "such feature" // c:2103
4873                                },
4874                                esp
4875                            ));
4876                        }
4877                        return 1; // c:2105
4878                    }
4879                }
4880            }
4881            None => {
4882                // c:2105-2112 — enable all features.
4883                let enables_vec = enables.get_or_insert_with(Vec::new);
4884                enables_vec.clear();
4885                enables_vec.resize(module_features.len(), 1);
4886                // c:2115-2116 — `for (ep = enables; n_features--; ep++) *ep = 1;`
4887                // with the comment "Enable all features.  This is used when
4888                // loading without using zmodload -F."  The commit below
4889                // (`enables_module`, c:2119) runs the module's `enables_` →
4890                // `handlefeatures` (c:3392) → `setfeatureenables` (c:3354) →
4891                // `setparamdefs` (c:1169-1181), which calls `addparamdef`
4892                // (c:1060) for EVERY `p:` feature whose enable bit is set.
4893                // `addparamdef` installs the real special Param over the
4894                // PM_AUTOLOAD stub `autofeatures` planted, so after a plain
4895                // `zmodload zsh/parameter` NONE of that module's parameters
4896                // is an autoload stub any more — only the single-feature
4897                // form (`ensurefeature(mn, "p:", nam)` from `loadparamnode`,
4898                // c:Src/params.c:568, which lands in the `Some(arr)` arm
4899                // above) leaves the siblings as stubs.
4900                //
4901                // zshrs seeds every magic parameter eagerly
4902                // (`vm_helper::init_partab_params`) and models PM_AUTOLOAD as
4903                // the `MATERIALIZED_MODULE_PARAMS` side set instead of a node
4904                // flag, so the stub-clearing half of `setparamdefs` has to be
4905                // replayed here. Without it a plain `zmodload zsh/parameter`
4906                // (which plugin managers such as zinit run at startup) left
4907                // `commands` reading as a
4908                // stub, so `local -A +h commands` in `_command_names` built a
4909                // PLAIN empty local instead of a special one (c:Src/builtin.c
4910                // :2083-2085 needs PM_SPECIAL on the node to set
4911                // `newspecial`), and `_path_commands`' `compadd -k commands`
4912                // added nothing until the user touched `$commands` by hand.
4913                for f in &module_features {
4914                    if let Some(pname) = f.strip_prefix("p:") {
4915                        crate::vm_helper::mark_module_param_used(pname);
4916                    }
4917                }
4918            }
4919        }
4920
4921        // c:2115 — final `enables_module(m, &enables)` commits the bits.
4922        if enables_module(table, modname, &mut enables) != 0 {
4923            return 2; // c:2116
4924        }
4925    } else if features.is_some() {
4926        // c:2117-2121 — features_module failed AND enablesarr non-NULL:
4927        // module doesn't support features. zwarn unless FEAT_IGNORE.
4928        if (flags & FEAT_IGNORE) == 0 {
4929            zwarn(&format!(
4930                "module `{}' does not support features", // c:2119
4931                modname
4932            ));
4933        }
4934        return 1; // c:2120
4935    }
4936    // c:2122 — `Else it doesn't support features but we don't care.`
4937    let _ = &mut ret;
4938    ret // c:2124
4939}
4940
4941/// Port of `do_boot_module()` from `Src/module.c:2139`. — C decl `do_boot_module(Module m, Feature_enables enablesarr, int silent)`.
4942///
4943/// C body:
4944/// ```c
4945/// do_boot_module(Module m, Feature_enables enablesarr, int silent)
4946/// {
4947///     int ret = do_module_features(m, enablesarr,
4948///                                  silent ? FEAT_IGNORE|FEAT_CHECKAUTO :
4949///                                  FEAT_CHECKAUTO);
4950///     if (ret == 1) return 1;
4951///     if (boot_module(m)) return 1;
4952///     return ret;
4953/// }
4954/// ```
4955pub fn do_boot_module(
4956    table: &mut modulestab,
4957    modname: &str,
4958    features: Option<&[String]>,
4959    silent: i32,
4960    // !!! RUST-ONLY PARAM !!! — carries C's per-entry
4961    // `Feature_enables.pat` (see FEAT_PATTERN_ARGS).
4962    feat_pat: bool,
4963) -> i32 {
4964    // c:2139
4965    let mut flags = if silent != 0 {
4966        // c:2142 — silent → IGNORE | CHECKAUTO
4967        FEAT_IGNORE | FEAT_CHECKAUTO
4968    } else {
4969        FEAT_CHECKAUTO // c:2143
4970    };
4971    if feat_pat {
4972        flags |= FEAT_PATTERN_ARGS;
4973    }
4974    let ret = do_module_features(table, modname, features, flags); // c:2141
4975    if ret == 1 {
4976        // c:2145
4977        return 1; // c:2146
4978    }
4979    if boot_module(table, modname) != 0 {
4980        // c:2148
4981        return 1; // c:2149
4982    }
4983    ret // c:2150
4984}
4985
4986/// Port of `do_cleanup_module()` from `Src/module.c:2159`. — C decl `do_cleanup_module(Module m)`.
4987///
4988/// C body:
4989/// ```c
4990/// do_cleanup_module(Module m) {
4991///     return (m->node.flags & MOD_LINKED) ?
4992///         (m->u.linked && m->u.linked->cleanup(m)) :
4993///         (m->u.handle && cleanup_module(m));
4994/// }
4995/// ```
4996/// WARNING: param names don't match C — Rust=(table, name) vs C=(m)
4997pub fn do_cleanup_module(table: &mut modulestab, name: &str) -> i32 {
4998    // c:2159
4999    // Check the module is registered, then dispatch to cleanup_module.
5000    if table.modules.contains_key(name) {
5001        // c:2162 m->u.linked
5002        cleanup_module(table, name) // c:2163 cleanup_module(m)
5003    } else {
5004        0
5005    }
5006}
5007
5008/// Port of `modname_ok()` from `Src/module.c:2173`. — C decl `modname_ok(char const *p)`.
5009///
5010/// Returns 1 iff `p` is a valid module name: one or more
5011/// `/`-separated identifier segments.
5012///
5013/// C body:
5014/// ```c
5015/// modname_ok(char const *p)
5016/// {
5017///     do {
5018///         p = itype_end(p, IIDENT, 0);
5019///         if (!*p)
5020///             return 1;
5021///     } while(*p++ == '/');
5022///     return 0;
5023/// }
5024/// ```
5025pub fn modname_ok(p: &str) -> i32 {
5026    // c:2173
5027    let bytes = p.as_bytes();
5028    let mut i: usize = 0;
5029    loop {
5030        // c:2176 — `p = itype_end(p, IIDENT, 0);`
5031        // IIDENT = identifier-byte (alpha/digit/underscore + extended).
5032        while i < bytes.len() {
5033            let b = bytes[i];
5034            // Inline IIDENT check — alphanumeric or underscore. Mirrors
5035            // utils.c:itype_end stepping for the IIDENT bit.
5036            if b.is_ascii_alphanumeric() || b == b'_' {
5037                i += 1;
5038            } else {
5039                break;
5040            }
5041        }
5042        if i >= bytes.len() {
5043            // c:2177 if (!*p)
5044            return 1; // c:2178
5045        }
5046        if bytes[i] != b'/' {
5047            break;
5048        } // c:2179 while(*p++ == '/')
5049        i += 1;
5050    }
5051    0 // c:2180
5052}
5053
5054/// Port of `require_module()` from `Src/module.c:2344`. — C decl `require_module(const char *module, Feature_enables features, int silent)`.
5055///
5056/// C body c:2342-2360:
5057/// ```c
5058/// mod_export int
5059/// require_module(const char *module, Feature_enables features, int silent)
5060/// {
5061///     Module m = NULL;
5062///     int ret = 0;
5063///     queue_signals();
5064///     m = find_module(module, FINDMOD_ALIASP, &module);
5065///     if (!m || !m->u.handle ||
5066///         (m->node.flags & MOD_UNLOAD))
5067///         ret = load_module(module, features, silent);
5068///     else
5069///         ret = do_module_features(m, features, 0);
5070///     unqueue_signals();
5071///     return ret;
5072/// }
5073/// ```
5074///
5075/// Two branches: when the module isn't loaded yet (or is mid-unload),
5076/// route through `load_module` which runs the full setup → features →
5077/// boot lifecycle. When it's already loaded, skip to
5078/// `do_module_features` to just enable the requested per-feature
5079/// surface — much cheaper.
5080///
5081/// Static-link analog of `m->u.handle` is `MOD_INIT_B` (boot ran):
5082/// once boot_module has fired, the module is "loaded" in zshrs's
5083/// non-dlopen world.
5084/// WARNING: param names don't match C — Rust=(table, modname, features, silent) vs C=(module, features, silent)
5085pub fn require_module(
5086    table: &mut modulestab,
5087    modname: &str,
5088    features: Option<&[String]>,
5089    silent: i32,
5090    // !!! RUST-ONLY PARAM !!! — carries C's per-entry
5091    // `Feature_enables.pat` (see FEAT_PATTERN_ARGS).
5092    feat_pat: bool,
5093) -> i32 {
5094    // c:2344
5095    // c:2350 — queue_signals(): signal-deferral wrapper.
5096    crate::ported::signals::queue_signals();
5097
5098    // c:2351 — `m = find_module(module, FINDMOD_ALIASP, &module);`
5099    // Resolves alias chain; canonical name lives in `mname`.
5100    let mname_opt = find_module(table, modname, FINDMOD_ALIASP);
5101
5102    // c:2352-2353 — `if (!m || !m->u.handle || MOD_UNLOAD)`.
5103    // Static-link analog of `m->u.handle`: MOD_INIT_B (boot ran).
5104    let needs_load = match &mname_opt {
5105        None => true, // c:2352 !m
5106        Some(mname) => match table.modules.get(mname) {
5107            None => true,
5108            Some(m) => {
5109                (m.node.flags & MOD_INIT_B) == 0 // c:2352 !u.handle analog
5110                    || (m.node.flags & MOD_UNLOAD) != 0 // c:2353
5111            }
5112        },
5113    };
5114
5115    let mname = mname_opt.unwrap_or_else(|| modname.to_string());
5116
5117    let ret = if needs_load {
5118        // c:2354 — `ret = load_module(module, features, silent);`
5119        // c:2208-2211 — load_module's first step: `if (!modname_ok(name))
5120        //   { if (!silent) zerr("invalid module name `%s'", name); return 1; }`.
5121        //   It runs before any lookup, so `zmodload 'zsh/da*'` is a fatal
5122        //   `invalid module name`, not a failed load.
5123        if modname_ok(&mname) == 0 {
5124            if silent == 0 {
5125                crate::ported::utils::zerr(&format!("invalid module name `{}'", mname)); // c:2210
5126            }
5127            crate::ported::signals::unqueue_signals();
5128            return 1; // c:2211
5129        }
5130        // try_load_module gates the static-link path. On miss, emit
5131        // the canonical zwarn (gated by silent).
5132        if try_load_module(table, &mname) == 0 {
5133            if silent == 0 {
5134                // c:1622 — same message and same reasoning as do_load_module
5135                // (module.rs, `failed to load module `%s'` with the dlerror tail
5136                // omitted). docs/BUGS.md #376 records the backquoted prefix as
5137                // the FINAL intended state and the parity test pins it, so this
5138                // site must not diverge from the other one.
5139                crate::ported::utils::zwarn(&format!("failed to load module `{}'", mname));
5140            }
5141            crate::ported::signals::unqueue_signals();
5142            return 1;
5143        }
5144        // c:2354 — `ret = load_module(module, features, silent);` — the full
5145        // 0/1/2 status, NOT a success flag: 2 means the module loaded but some
5146        // feature could not be set, and `zmodload` reports it.
5147        table.load_module(&mname, features, feat_pat)
5148    } else {
5149        // c:2356 — `ret = do_module_features(m, features, 0);`
5150        // Module already loaded; just enable the requested features.
5151        // features=NULL in C means "enable all features"; the Rust
5152        // do_module_features takes a single enablesstr arg, so flatten
5153        // C: features=NULL means "enable all"; pass through.
5154        do_module_features(
5155            table,
5156            &mname,
5157            features,
5158            if feat_pat { FEAT_PATTERN_ARGS } else { 0 },
5159        )
5160    };
5161
5162    // c:2357 — unqueue_signals();
5163    crate::ported::signals::unqueue_signals();
5164
5165    ret // c:2359
5166}
5167
5168/// Port of `add_dep()` from `Src/module.c:2369`. — C decl `add_dep(const char *name, char *from)`.
5169///
5170/// C body:
5171/// ```c
5172/// add_dep(const char *name, char *from)
5173/// {
5174///     LinkNode node;
5175///     Module m;
5176///     m = find_module(name, FINDMOD_ALIASP|FINDMOD_CREATE, &name);
5177///     if (!m->deps)
5178///         m->deps = znewlinklist();
5179///     for (node = firstnode(m->deps);
5180///          node && strcmp((char *) getdata(node), from);
5181///          incnode(node));
5182///     if (!node)
5183///         zaddlinknode(m->deps, ztrdup(from));
5184/// }
5185/// ```
5186///
5187/// Records that module `name` depends on module `from`. Resolves
5188/// aliases so dependency graphs always point at canonical names.
5189/// WARNING: param names don't match C — Rust=(table, name, from) vs C=(name, from)
5190pub fn add_dep(table: &mut modulestab, name: &str, from: &str) -> i32 {
5191    // c:2369
5192    // c:2369 — m = find_module(name, FINDMOD_ALIASP|FINDMOD_CREATE, &name)
5193    let canon = match find_module(table, name, FINDMOD_ALIASP | FINDMOD_CREATE) {
5194        Some(n) => n,
5195        None => return 0,
5196    };
5197    if let Some(m) = table.modules.get_mut(&canon) {
5198        // c:2389-2391 — walk deps, skip if `from` already present.
5199        let deps = m
5200            .deps
5201            .get_or_insert_with(crate::ported::linklist::LinkList::new);
5202        if !deps.iter().any(|d| d == from) {
5203            // c:2392 if (!node)
5204            deps.push_back(from.to_string()); // c:2393 zaddlinknode
5205        }
5206    }
5207    0
5208}
5209
5210/// Port of `autoloadscan()` from `Src/module.c:2403`. — C decl `autoloadscan(HashNode hn, int printflags)`.
5211///
5212/// C body:
5213/// ```c
5214/// autoloadscan(HashNode hn, int printflags)
5215/// {
5216///     Builtin bn = (Builtin) hn;
5217///     if(bn->node.flags & BINF_ADDED)
5218///         return;
5219///     if(printflags & PRINT_LIST) {
5220///         fputs("zmodload -ab ", stdout);
5221///         if(bn->optstr[0] == '-') fputs("-- ", stdout);
5222///         quotedzputs(bn->optstr, stdout);
5223///         if(strcmp(bn->node.nam, bn->optstr)) {
5224///             putchar(' ');
5225///             quotedzputs(bn->node.nam, stdout);
5226///         }
5227///     } else {
5228///         nicezputs(bn->node.nam, stdout);
5229///         if(strcmp(bn->node.nam, bn->optstr)) {
5230///             fputs(" (", stdout);
5231///             nicezputs(bn->optstr, stdout);
5232///             putchar(')');
5233///         }
5234///     }
5235///     putchar('\n');
5236/// }
5237/// ```
5238///
5239/// Hash-table scan callback for autoloadable-builtin listing.
5240/// `printflags & PRINT_LIST` selects long form (`zmodload -ab MOD NAME`)
5241/// vs short form (`NAME (MOD)`). Skips already-registered builtins
5242/// (BINF_ADDED set).
5243/// WARNING: param names don't match C — Rust=(name, optstr, flags, printflags) vs C=(hn, printflags)
5244pub fn autoloadscan(name: &str, optstr: &str, flags: u32, printflags: i32) {
5245    // c:2403
5246    use crate::ported::utils::{nicezputs, quotedzputs};
5247    let mut stdout = std::io::stdout();
5248    if (flags & BINF_ADDED) != 0 {
5249        // c:2407
5250        return; // c:2408
5251    }
5252    if (printflags & PRINT_LIST) != 0 {
5253        // c:2409
5254        // c:2410-2417 — long form `zmodload -ab MOD NAME`
5255        print!("zmodload -ab ");
5256        if optstr.starts_with('-') {
5257            // c:2411
5258            print!("-- "); // c:2412
5259        }
5260        // c:2413 — `quotedzputs(bn->optstr, stdout);`
5261        print!("{}", quotedzputs(optstr));
5262        if name != optstr {
5263            // c:2414 — `if(strcmp(bn->node.nam, bn->optstr))`
5264            print!(" "); // c:2415 putchar(' ')
5265                         // c:2416 — `quotedzputs(bn->node.nam, stdout);`
5266            print!("{}", quotedzputs(name));
5267        }
5268    } else {
5269        // c:2418-2424 — short form `NAME (MOD)`
5270        let _ = nicezputs(name, &mut stdout); // c:2419
5271        if name != optstr {
5272            // c:2420 — `if(strcmp(bn->node.nam, bn->optstr))`
5273            print!(" ("); // c:2421
5274            let _ = nicezputs(optstr, &mut stdout); // c:2422
5275            print!(")"); // c:2423
5276        }
5277    }
5278    println!(); // c:2426 putchar('\n')
5279}
5280
5281/// Port of `bin_zmodload()` from `Src/module.c:2440`. — C decl `bin_zmodload(char *nam, char **args, Options ops, UNUSED(int func))`.
5282/// Top-level dispatcher for the `zmodload` builtin. Validates flag
5283/// combinations then routes to one of the per-mode helpers:
5284///   -F        → bin_zmodload_features (c:3003)
5285///   -e        → bin_zmodload_exist    (c:2623)
5286///   -d        → bin_zmodload_dep      (c:2649)
5287///   -a/-b/-c/-p/-f → bin_zmodload_auto (c:2726)
5288///   default   → bin_zmodload_load     (c:2971)
5289///   -A/-R     → bin_zmodload_alias    (c:2515)
5290/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
5291pub fn bin_zmodload(
5292    nam: &str,
5293    args: &[String], // c:2440
5294    ops: &options,
5295    _func: i32,
5296) -> i32 {
5297    let mut table = MODULESTAB.lock().unwrap();
5298    let table = &mut *table;
5299
5300    let ops_bcpf = OPT_ISSET(ops, b'b') || OPT_ISSET(ops, b'c')              // c:2443
5301                || OPT_ISSET(ops, b'p') || OPT_ISSET(ops, b'f');
5302    let ops_au = OPT_ISSET(ops, b'a') || OPT_ISSET(ops, b'u'); // c:2445
5303    let mut ret: i32; // c:2446
5304
5305    if ops_bcpf && !ops_au {
5306        // c:2451
5307        zwarnnam(nam, "-b, -c, -f, and -p must be combined with -a or -u"); // c:2452
5308        return 1; // c:2453
5309    }
5310    if OPT_ISSET(ops, b'F') && (ops_bcpf || OPT_ISSET(ops, b'u')) {
5311        // c:2455
5312        zwarnnam(nam, "-b, -c, -f, -p and -u cannot be combined with -F"); // c:2456
5313        return 1; // c:2457
5314    }
5315    // !!! WARNING: RUST-ONLY EXTENSION — NO C COUNTERPART !!!
5316    // zshrs repurposes `-R` WITHOUT `-A` to load a native (Rust)
5317    // plugin cdylib through the stable C ABI (src/extensions/
5318    // plugin_host.rs): the first JIT-compiled Unix shell hosting
5319    // compiled-language plugins loaded at runtime. In C zsh `-R` means
5320    // "remove module alias" and is only ever meaningful alongside `-A`
5321    // (Src/module.c:2459); that behaviour is preserved for `-A -R name`
5322    // (both flags set falls through to bin_zmodload_alias below), so no
5323    // parity is lost for the near-unused alias-removal case.
5324    //   zmodload -R  <path>...    load each plugin cdylib
5325    //   zmodload -R               list loaded plugins
5326    //   zmodload -uR <name>...    unload each plugin by name
5327    // …but only when no operand names a REAL zsh module. `zmodload -R
5328    // zsh/complete` is C's alias-removal form applied to a known module
5329    // and must keep C's diagnostic (`module is not an alias: zsh/complete`,
5330    // c:2561), while a native plugin is a cdylib path or an arbitrary
5331    // plugin name that is never in `linkedmodules`. Discriminating on the
5332    // operand lets BOTH behaviours coexist instead of the extension
5333    // shadowing the ported one: previously every `-R` operand went to the
5334    // plugin host, so a zsh module name came back as a dlopen error.
5335    // No operands (`zmodload -R` = list loaded plugins) still routes here.
5336    // A name that already has a `modulestab` node is C's territory too —
5337    // most importantly a MOD_ALIAS created by `zmodload -A NAME=REAL`,
5338    // whose removal is the whole point of C's `-R` (c:2552-2568). Those
5339    // never appear in `linkedmodules`, so the `module_linked` test alone
5340    // sent `zmodload -R example` to the plugin host and it came back as a
5341    // dlopen error where zsh silently deletes the alias
5342    // (V01zmodload.ztst "Delete the module alias again").
5343    if OPT_ISSET(ops, b'R')
5344        && !OPT_ISSET(ops, b'A')
5345        && !args
5346            .iter()
5347            .any(|a| table.module_linked(a) || table.modules.contains_key(a))
5348    {
5349        // Placed before the c:2490 queue_signals, so nothing to unqueue.
5350        // Handler lives in the extensions tree (not a port) —
5351        // src/extensions/plugin_host.rs.
5352        return crate::plugin_host::zmodload_rust_cmd(nam, args, ops);
5353    }
5354    if OPT_ISSET(ops, b'A') || OPT_ISSET(ops, b'R') {
5355        // c:2459
5356        if ops_bcpf || ops_au || OPT_ISSET(ops, b'd')                        // c:2460
5357           || (OPT_ISSET(ops, b'R') && OPT_ISSET(ops, b'e'))
5358        {
5359            zwarnnam(nam, "invalid flags combined with -A or -R"); // c:2462
5360            return 1; // c:2463
5361        }
5362        if !OPT_ISSET(ops, b'e') {
5363            // c:2465
5364            return bin_zmodload_alias(table, nam, args, ops); // c:2466
5365        }
5366    }
5367    if OPT_ISSET(ops, b'd') && OPT_ISSET(ops, b'a') {
5368        // c:2468
5369        zwarnnam(nam, "-d cannot be combined with -a"); // c:2469
5370        return 1; // c:2470
5371    }
5372    if OPT_ISSET(ops, b'u') && args.is_empty() {
5373        // c:2472
5374        zwarnnam(nam, "what do you want to unload?"); // c:2473
5375        return 1; // c:2474
5376    }
5377    if OPT_ISSET(ops, b'e')
5378        && (OPT_ISSET(ops, b'I') || OPT_ISSET(ops, b'L') // c:2476
5379        || (OPT_ISSET(ops, b'a') && !OPT_ISSET(ops, b'F'))
5380        || OPT_ISSET(ops, b'd') || OPT_ISSET(ops, b'i')
5381        || OPT_ISSET(ops, b'u'))
5382    {
5383        zwarnnam(nam, "-e cannot be combined with other options"); // c:2480
5384        return 1; // c:2482
5385    }
5386    // c:2484 — `for (fp = fonly; *fp; fp++)` — `l` and `P` only with `-F`.
5387    for fp in [b'l', b'P'] {
5388        // c:2484
5389        if OPT_ISSET(ops, fp) && !OPT_ISSET(ops, b'F') {
5390            // c:2485
5391            zwarnnam(nam, &format!("-{} is only allowed with -F", fp as char)); // c:2486
5392            return 1; // c:2487
5393        }
5394    }
5395    crate::ported::mem::queue_signals(); // c:2490
5396    if OPT_ISSET(ops, b'F') {
5397        // c:2491
5398        ret = bin_zmodload_features(table, nam, args, ops); // c:2492
5399    } else if OPT_ISSET(ops, b'e') {
5400        // c:2493
5401        ret = bin_zmodload_exist(table, nam, args, ops); // c:2494
5402    } else if OPT_ISSET(ops, b'd') {
5403        // c:2495
5404        ret = bin_zmodload_dep(table, nam, args, ops); // c:2496
5405    } else {
5406        let autoopts = (OPT_ISSET(ops, b'b') as i32)                         // c:2497
5407                     + (OPT_ISSET(ops, b'c') as i32)
5408                     + (OPT_ISSET(ops, b'p') as i32)
5409                     + (OPT_ISSET(ops, b'f') as i32);
5410        if autoopts != 0 || OPT_ISSET(ops, b'a') {
5411            // c:2497-2499
5412            if autoopts > 1 {
5413                // c:2502
5414                zwarnnam(nam, "use only one of -b, -c, or -p"); // c:2503
5415                ret = 1; // c:2504
5416            } else {
5417                ret = bin_zmodload_auto(table, nam, args, ops); // c:2506
5418            }
5419        } else {
5420            ret = bin_zmodload_load(table, nam, args, ops); // c:2508
5421        }
5422    }
5423    unqueue_signals(); // c:2515
5424    ret // c:2515
5425}
5426
5427/// Port of `bin_zmodload_alias()` from `Src/module.c:2515`. — C decl `bin_zmodload_alias(char *nam, char **args, Options ops)`.
5428///
5429/// `zmodload -A [-L|-R] [name=alias ...]`. Three modes:
5430/// - no args: list all module aliases (`-L` = long form).
5431/// - `-R name`: remove alias `name` (must already be MOD_ALIAS).
5432/// - `name=target`: install/replace alias `name` pointing at `target`.
5433///   Detects self-cycles before committing.
5434/// WARNING: param names don't match C — Rust=(table, nam, args, ops) vs C=(nam, args, ops)
5435pub fn bin_zmodload_alias(
5436    table: &mut modulestab,
5437    nam: &str,
5438    args: &[String],
5439    ops: &options,
5440) -> i32 {
5441    // c:2515
5442    /*
5443     * TODO: while it would be too nasty to have aliases, as opposed
5444     * to real loadable modules, with dependencies --- just what would
5445     * we need to load when, exactly? --- there is in principle no objection
5446     * to making it possible to force an alias onto an existing unloaded
5447     * module which has dependencies.  This would simply transfer
5448     * the dependencies down the line to the aliased-to module name.
5449     * This is actually useful, since then you can alias zsh/zle=mytestzle
5450     * to load another version of zle.  But then what happens when the
5451     * alias is removed?  Do you transfer the dependencies back? And
5452     * suppose other names are aliased to the same file?  It might be
5453     * kettle of fish best left unwormed.
5454     */                                                                  // c:2517-2529
5455
5456    // c:2532-2541 — no args: list aliases.
5457    // C body:
5458    //   if (!*args) {
5459    //       if (OPT_ISSET(ops,'R')) {
5460    //           zwarnnam(nam, "no module alias to remove");
5461    //           return 1;
5462    //       }
5463    //       scanhashtable(modulestab, 1, MOD_ALIAS, 0,
5464    //                     modulestab->printnode,
5465    //                     OPT_ISSET(ops,'L') ? PRINTMOD_LIST : 0);
5466    //       return 0;
5467    //   }
5468    //
5469    // scanhashtable args: sorted=1, INCLUDE=MOD_ALIAS, EXCLUDE=0.
5470    // Only MOD_ALIAS entries pass, walked in name order. Each entry
5471    // dispatched through printmodulenode (the alias-emit arm).
5472    if args.is_empty() {
5473        if OPT_ISSET(ops, b'R') {
5474            // c:2533
5475            zwarnnam(nam, "no module alias to remove"); // c:2534
5476            return 1; // c:2535
5477        }
5478        let listflags = if OPT_ISSET(ops, b'L') {
5479            PRINTMOD_LIST
5480        } else {
5481            0
5482        };
5483        let mut names: Vec<&String> = table
5484            .modules
5485            .iter()
5486            .filter(|(_, m)| (m.node.flags & MOD_ALIAS) != 0) // c:2537 INCLUDE
5487            .map(|(n, _)| n)
5488            .collect();
5489        names.sort(); // c:2537 sorted=1
5490        for name in names {
5491            let m = &table.modules[name];
5492            let line = printmodulenode(name, m, listflags);
5493            if !line.is_empty() {
5494                println!("{}", line);
5495            }
5496        }
5497        return 0; // c:2540
5498    }
5499
5500    // c:2543 — for each arg, parse name=alias and dispatch.
5501    for arg in args {
5502        // c:2544-2547 — split at '='
5503        let (lhs, aliasname): (&str, Option<&str>) = match arg.find('=') {
5504            Some(eq) => (&arg[..eq], Some(&arg[eq + 1..])),
5505            None => (arg.as_str(), None),
5506        };
5507        // c:2548 — modname_ok check on the LHS
5508        if modname_ok(lhs) == 0 {
5509            // c:2548
5510            zwarnnam(nam, &format!("invalid module name `{}'", lhs)); // c:2549
5511            return 1; // c:2550
5512        }
5513        if OPT_ISSET(ops, b'R') {
5514            // c:2552
5515            // -R: remove alias path.
5516            if aliasname.is_some() {
5517                // c:2553
5518                zwarnnam(
5519                    nam,
5520                    &format!("bad syntax for removing module alias: {}", lhs),
5521                ); // c:2554
5522                return 1; // c:2556
5523            }
5524            // c:2558 — find_module(lhs, 0, NULL)
5525            match table.modules.get(lhs) {
5526                Some(m) => {
5527                    if (m.node.flags & MOD_ALIAS) == 0 {
5528                        // c:2560
5529                        zwarnnam(nam, &format!("module is not an alias: {}", lhs)); // c:2561
5530                        return 1; // c:2562
5531                    }
5532                    table.modules.remove(lhs); // c:2564 delete_module
5533                }
5534                None => {
5535                    zwarnnam(nam, &format!("no such module alias: {}", lhs)); // c:2566
5536                    return 1; // c:2567
5537                }
5538            }
5539        } else {
5540            // No -R: install/replace alias OR list one.
5541            if let Some(target) = aliasname {
5542                // c:2570
5543                if modname_ok(target) == 0 {
5544                    // c:2572
5545                    zwarnnam(nam, &format!("invalid module name `{}'", target)); // c:2573
5546                    return 1; // c:2574
5547                }
5548                // c:2576-2584 — cycle detection: walk alias chain
5549                let mut mname = target;
5550                let mut depth = 0;
5551                loop {
5552                    if depth > 256 {
5553                        break;
5554                    }
5555                    depth += 1;
5556                    if mname == lhs {
5557                        // c:2577
5558                        zwarnnam(nam, &format!("module alias would refer to itself: {}", lhs)); // c:2578
5559                        return 1; // c:2580
5560                    }
5561                    match table.modules.get(mname) {
5562                        Some(m) if (m.node.flags & MOD_ALIAS) != 0 => {
5563                            mname = m.alias.as_deref().unwrap_or("");
5564                        }
5565                        _ => break,
5566                    }
5567                }
5568                // c:2585-2596 — install or replace
5569                if let Some(m) = table.modules.get_mut(lhs) {
5570                    if (m.node.flags & MOD_ALIAS) == 0 {
5571                        // c:2587
5572                        zwarnnam(nam, &format!("module is not an alias: {}", lhs)); // c:2588
5573                        return 1; // c:2589
5574                    }
5575                    m.alias = Some(target.to_string()); // c:2591/2597
5576                } else {
5577                    let mut m = module::new(lhs); // c:2593 zshcalloc
5578                    m.node.flags = MOD_ALIAS; // c:2594
5579                    m.alias = Some(target.to_string()); // c:2597
5580                    table.modules.insert(lhs.to_string(), m); // c:2595
5581                }
5582            } else {
5583                // c:2599-2611 — list one alias.
5584                // C body:
5585                //   if ((m = find_module(*args, 0, NULL))) {
5586                //       if (m->node.flags & MOD_ALIAS)
5587                //           modulestab->printnode(&m->node,
5588                //                                 OPT_ISSET(ops,'L')
5589                //                                     ? PRINTMOD_LIST : 0);
5590                //       else { zwarnnam(...); return 1; }
5591                //   } else { zwarnnam(...); return 1; }
5592                match table.modules.get(lhs) {
5593                    Some(m) if (m.node.flags & MOD_ALIAS) != 0 => {
5594                        // c:2601-2603 — printnode dispatch
5595                        let listflags = if OPT_ISSET(ops, b'L') {
5596                            PRINTMOD_LIST
5597                        } else {
5598                            0
5599                        };
5600                        let line = printmodulenode(lhs, m, listflags);
5601                        if !line.is_empty() {
5602                            println!("{}", line);
5603                        }
5604                    }
5605                    Some(_) => {
5606                        zwarnnam(nam, &format!("module is not an alias: {}", lhs)); // c:2605
5607                        return 1; // c:2606
5608                    }
5609                    None => {
5610                        zwarnnam(nam, &format!("no such module alias: {}", lhs)); // c:2609
5611                        return 1; // c:2610
5612                    }
5613                }
5614            }
5615        }
5616    }
5617    0 // c:2616
5618}
5619
5620/// Port of `bin_zmodload_exist()` from `Src/module.c:2623`. — C decl `bin_zmodload_exist(UNUSED(char *nam), char **args, Options ops)`.
5621///
5622/// C body:
5623/// ```c
5624/// bin_zmodload_exist(UNUSED(char *nam), char **args, Options ops)
5625/// {
5626///     Module m;
5627///     if (!*args) {
5628///         scanhashtable(modulestab, 1, 0, 0, modulestab->printnode,
5629///                       OPT_ISSET(ops,'A') ? PRINTMOD_EXIST|PRINTMOD_ALIAS :
5630///                       PRINTMOD_EXIST);
5631///         return 0;
5632///     } else {
5633///         int ret = 0;
5634///         for (; !ret && *args; args++) {
5635///             if (!(m = find_module(*args, FINDMOD_ALIASP, NULL))
5636///                 || !m->u.handle
5637///                 || (m->node.flags & MOD_UNLOAD))
5638///                 ret = 1;
5639///         }
5640///         return ret;
5641///     }
5642/// }
5643/// ```
5644///
5645/// `zmodload [-A]` lists or tests module presence. Returns 0 if all
5646/// named modules exist (or if no args, after listing); 1 if any
5647/// named module is missing/unloading.
5648/// WARNING: param names don't match C — Rust=(table, _nam, args, _ops) vs C=(nam, args, ops)
5649pub fn bin_zmodload_exist(
5650    table: &mut modulestab,
5651    _nam: &str,
5652    args: &[String],
5653    ops: &options,
5654) -> i32 {
5655    // c:2623
5656    if args.is_empty() {
5657        // c:2627
5658        // c:2628-2630 — scanhashtable(modulestab, 1, 0, 0,
5659        //                              modulestab->printnode,
5660        //                              OPT_ISSET(ops,'A')
5661        //                                  ? PRINTMOD_EXIST|PRINTMOD_ALIAS
5662        //                                  : PRINTMOD_EXIST);
5663        // Sorted walk (the `1` 2nd arg) over the full table (no
5664        // INCLUDE/EXCLUDE filters), dispatching every entry through
5665        // printmodulenode. -A toggles PRINTMOD_ALIAS so alias entries
5666        // emit their alias target alongside the existence line.
5667        let printflags = if OPT_ISSET(ops, b'A') {
5668            PRINTMOD_EXIST | PRINTMOD_ALIAS
5669        } else {
5670            PRINTMOD_EXIST
5671        };
5672        let mut names: Vec<&String> = table.modules.keys().collect();
5673        names.sort(); // c:2628 sorted=1
5674        for name in names {
5675            let m = &table.modules[name];
5676            let line = printmodulenode(name, m, printflags);
5677            if !line.is_empty() {
5678                println!("{}", line);
5679            }
5680        }
5681        return 0; // c:2631
5682    }
5683    // c:2633-2640 — for each arg, test existence.
5684    // C:
5685    //   for (; !ret && *args; args++) {
5686    //       if (!(m = find_module(*args, FINDMOD_ALIASP, NULL))
5687    //           || !m->u.handle
5688    //           || (m->node.flags & MOD_UNLOAD))
5689    //           ret = 1;
5690    //   }
5691    // The `!m->u.handle` clause is union-typed in C: for static-link
5692    // modules it reads `m->u.linked` (non-NULL when linked, NULL when
5693    // pre-registered but not yet bound); for dynamic modules it's the
5694    // dlopen handle. Translate to: handle.is_none() && linked.is_none()
5695    // — both representations of "no live binding."
5696    let mut ret: i32 = 0;
5697    for arg in args {
5698        // c:2635
5699        if ret != 0 {
5700            break;
5701        }
5702        let canon = match find_module(table, arg, FINDMOD_ALIASP) {
5703            // c:2636
5704            Some(n) => n,
5705            None => {
5706                ret = 1; // c:2639
5707                continue;
5708            }
5709        };
5710        let live = match table.modules.get(&canon) {
5711            Some(m) => {
5712                // c:2637 — `!m->u.handle` (union-semantics).
5713                let bound = m.handle.is_some() || m.linked.is_some();
5714                // c:2638 — `(m->node.flags & MOD_UNLOAD)`.
5715                let unloading = (m.node.flags & MOD_UNLOAD) != 0;
5716                bound && !unloading
5717            }
5718            None => false,
5719        };
5720        if !live {
5721            ret = 1; // c:2639
5722        }
5723    }
5724    ret // c:2641
5725}
5726
5727/// Port of `bin_zmodload_dep()` from `Src/module.c:2649`. — C decl `bin_zmodload_dep(UNUSED(char *nam), char **args, Options ops)`.
5728///
5729/// `zmodload -d [-u] [target [dep ...]]`. Three modes:
5730/// - `-u target` removes all deps from target; `-u target d1 d2` removes
5731///   only those.
5732/// - no args lists all dependencies.
5733/// - `target dep1 ...` adds each dep to target's dependency list.
5734/// WARNING: param names don't match C — Rust=(table, _nam, args, ops) vs C=(nam, args, ops)
5735pub fn bin_zmodload_dep(table: &mut modulestab, _nam: &str, args: &[String], ops: &options) -> i32 {
5736    // c:2649
5737    if OPT_ISSET(ops, b'u') {
5738        // c:2652
5739        // c:2654 — `const char *tnam = *args++;`
5740        if args.is_empty() {
5741            return 0;
5742        }
5743        let tnam = &args[0];
5744        let rest = &args[1..];
5745        // c:2655 — `find_module(tnam, FINDMOD_ALIASP, &tnam)`
5746        let canon = match find_module(table, tnam, FINDMOD_ALIASP) {
5747            Some(n) => n,
5748            None => return 0, // c:2656-2657
5749        };
5750        if let Some(m) = table.modules.get_mut(&canon) {
5751            if !rest.is_empty() && m.deps.is_some() {
5752                // c:2658-2671 — remove specific deps.
5753                // C body c:2659-2667 walks args, finds each in deps,
5754                // removes the matched node; the inner break ends the
5755                // current arg's search but the outer while continues
5756                // to the next arg. Mirrors with a for-loop.
5757                let deps = m.deps.as_mut().unwrap();
5758                for to_remove in rest {
5759                    // c:2659 do { ... } while(*++args)
5760                    if let Some(pos) = deps.iter().position(|d| d == to_remove) {
5761                        deps.delete_node(pos); // c:2664 remnode
5762                    }
5763                }
5764                // c:2668-2671 — if deps now empty, free the list.
5765                if deps.is_empty() {
5766                    m.deps = None;
5767                }
5768            } else if m.deps.is_some() {
5769                // c:2672-2676 — no specific deps given: clear all.
5770                m.deps = None;
5771            }
5772            // c:2678-2679 — `if (!m->deps && !m->u.handle) delete_module(m);`
5773            // Static-link analog of `!u.handle` is `!MOD_INIT_B` (boot
5774            // hasn't run = no loaded handle). Without the MOD_INIT_B
5775            // gate, the prior port deleted any module whose deps got
5776            // cleared, dropping the modulestab entries for already-
5777            // loaded modules (zsh/main, zsh/watch, etc.) the moment
5778            // their dep list went empty.
5779            let no_deps = m.deps.is_none();
5780            let no_handle = (m.node.flags & MOD_INIT_B) == 0;
5781            if no_deps && no_handle {
5782                table.modules.remove(&canon); // c:2679 delete_module
5783            }
5784        }
5785        return 0; // c:2680
5786    }
5787    // c:2681 — `else if (!args[0] || !args[1])`: list-mode (one or all).
5788    if args.is_empty() || args.len() == 1 {
5789        // c:2682-2691 — `int depflags = OPT_ISSET(ops,'L')
5790        //                  ? PRINTMOD_DEPS|PRINTMOD_LIST : PRINTMOD_DEPS;`
5791        let depflags = if OPT_ISSET(ops, b'L') {
5792            PRINTMOD_DEPS | PRINTMOD_LIST
5793        } else {
5794            PRINTMOD_DEPS
5795        };
5796        if !args.is_empty() {
5797            // c:2685-2687 — single-name list:
5798            //   if ((m = modulestab->getnode2(modulestab, args[0])))
5799            //       modulestab->printnode(&m->node, depflags);
5800            if let Some(m) = table.modules.get(&args[0]) {
5801                let line = printmodulenode(&args[0], m, depflags);
5802                if !line.is_empty() {
5803                    println!("{}", line);
5804                }
5805            }
5806        } else {
5807            // c:2688-2691 — full sorted scan.
5808            //   scanhashtable(modulestab, 1, 0, 0, printnode, depflags);
5809            let mut names: Vec<&String> = table.modules.keys().collect();
5810            names.sort(); // c:2689 sorted=1
5811            for name in names {
5812                let m = &table.modules[name];
5813                let line = printmodulenode(name, m, depflags);
5814                if !line.is_empty() {
5815                    println!("{}", line);
5816                }
5817            }
5818        }
5819        return 0; // c:2692
5820    }
5821    // c:2693-2701 — add deps: args[0] target, args[1..] deps to add.
5822    let target = &args[0];
5823    for dep in &args[1..] {
5824        add_dep(table, target, dep); // c:2699
5825    }
5826    0 // c:2700 (ret stays 0)
5827}
5828
5829/// Port of `printautoparams()` from `Src/module.c:2710`. — C decl `printautoparams(HashNode hn, int lon)`.
5830///
5831/// C body:
5832/// ```c
5833/// printautoparams(HashNode hn, int lon)
5834/// {
5835///     Param pm = (Param) hn;
5836///     if (pm->node.flags & PM_AUTOLOAD) {
5837///         if (lon)
5838///             printf("zmodload -ap %s %s\n", pm->u.str, pm->node.nam);
5839///         else
5840///             printf("%s (%s)\n", pm->node.nam, pm->u.str);
5841///     }
5842/// }
5843/// ```
5844///
5845/// Hash-table scan callback for `zmodload -ap` listing. Rust port
5846/// takes a `(name, module, flags)` triple instead of a HashNode ptr
5847/// since zshrs's autoload-params live in `ModuleTable.autoload_params`.
5848/// WARNING: param names don't match C — Rust=(name, module, flags, lon) vs C=(hn, lon)
5849pub fn printautoparams(name: &str, module: &str, flags: u32, lon: i32) {
5850    // c:2710
5851    if (flags & PM_AUTOLOAD) != 0 {
5852        // c:2710
5853        if lon != 0 {
5854            // c:2715
5855            // c:2716 — printf("zmodload -ap %s %s\n", pm->u.str, pm->node.nam);
5856            println!("zmodload -ap {} {}", module, name);
5857        } else {
5858            // c:2718 — printf("%s (%s)\n", pm->node.nam, pm->u.str);
5859            println!("{} ({})", name, module);
5860        }
5861    }
5862}
5863
5864/// Port of `bin_zmodload_auto()` from `Src/module.c:2726`. — C decl `bin_zmodload_auto(char *nam, char **args, Options ops)`.
5865///
5866/// `zmodload [-c] [-p] [-f] [-a] module name [name ...]` —
5867/// register-autoload of builtins/conditions/params/mathfns. C body
5868/// (80 lines) walks the appropriate dispatch table per opt flag.
5869///
5870/// `-c` lists/registers conditions, `-p` parameters, `-f` math ported,
5871/// default is builtins. `-L` toggles long-form listing.
5872///
5873/// Static-link path: registers via `add_autoaliasbuiltin` /
5874/// `add_autoparam` / `add_automathfunc` already ported. Without a
5875/// module name (just `-a`), runs the listing scan via `autoloadscan`
5876/// or its conddef/param/mathfn equivalents.
5877/// WARNING: param names don't match C — Rust=(table, _nam, args, ops) vs C=(nam, args, ops)
5878pub fn bin_zmodload_auto(
5879    table: &mut modulestab,
5880    _nam: &str,
5881    args: &[String],
5882    ops: &options,
5883) -> i32 {
5884    // c:2726
5885    let fchar: char; // c:2726
5886    let _flags: i32 = if OPT_ISSET(ops, b'i') { FEAT_IGNORE } else { 0 }; // c:2728
5887
5888    // c:2731-2753 — conditions branch (-c).
5889    // C body:
5890    //   if (!*args) {
5891    //       Conddef p;
5892    //       for (p = condtab; p; p = p->next) {
5893    //           if (p->module) {
5894    //               if (OPT_ISSET(ops,'L')) {
5895    //                   fputs("zmodload -ac", stdout);
5896    //                   if (p->flags & CONDF_INFIX) putchar('I');
5897    //                   printf(" %s %s\n", p->module, p->name);
5898    //               } else {
5899    //                   if (p->flags & CONDF_INFIX) fputs("infix ", stdout);
5900    //                   else fputs("post ", stdout);
5901    //                   printf("%s (%s)\n", p->name, p->module);
5902    //               }
5903    //           }
5904    //       }
5905    //       return 0;
5906    //   }
5907    // C walks condtab in registration order (linked-list head→tail).
5908    // The Rust port walks CONDTAB (same shape, Vec instead of list).
5909    if OPT_ISSET(ops, b'c') {
5910        fchar = if OPT_ISSET(ops, b'I') { 'C' } else { 'c' };
5911        let _ = fchar;
5912        if args.is_empty() {
5913            let snap: Vec<(String, i32, String)> = {
5914                let tab = CONDTAB.lock().unwrap();
5915                tab.iter()
5916                    // c:2737 — `if (p->module)` skips entries without
5917                    // an owning module (built-in cond defs use NULL).
5918                    .filter_map(|p| {
5919                        p.module
5920                            .as_ref()
5921                            .map(|m| (p.name.clone(), p.flags, m.clone()))
5922                    })
5923                    .collect()
5924            };
5925            let l_flag = OPT_ISSET(ops, b'L');
5926            for (name, flags, module) in snap {
5927                if l_flag {
5928                    // c:2738-2742 — `zmodload -ac[I] MODULE NAME`.
5929                    if (flags & CONDF_INFIX) != 0 {
5930                        // c:2740
5931                        println!("zmodload -acI {} {}", module, name);
5932                    } else {
5933                        println!("zmodload -ac {} {}", module, name);
5934                    }
5935                } else {
5936                    // c:2743-2748 — `infix NAME (MODULE)` /
5937                    //                `post NAME (MODULE)`.
5938                    let kind = if (flags & CONDF_INFIX) != 0 {
5939                        "infix"
5940                    } else {
5941                        "post"
5942                    };
5943                    println!("{} {} ({})", kind, name, module);
5944                }
5945            }
5946            return 0;
5947        }
5948    } else if OPT_ISSET(ops, b'p') {
5949        // c:2755-2761 — params branch.
5950        // C body:
5951        //   if (!*args) {
5952        //       /* list autoloaded parameters */
5953        //       scanhashtable(paramtab, 1, 0, 0, printautoparams,
5954        //                     OPT_ISSET(ops,'L'));
5955        //       return 0;
5956        //   }
5957        // The `sorted=1` (2nd arg) walks the table in name order.
5958        // `printautoparams` (c:2710) checks `pm->flags & PM_AUTOLOAD`
5959        // and emits either `zmodload -ap MODULE NAME` (under `-L`)
5960        // or `NAME (MODULE)` form. Module name is read from
5961        // `pm->u.str`, which `add_autoparam` (c:1218) sets to the
5962        // module that registered the autoload stub.
5963        if args.is_empty() {
5964            let lon = if OPT_ISSET(ops, b'L') { 1 } else { 0 };
5965            let entries: Vec<(String, u32, String)> = {
5966                let tab = crate::ported::params::paramtab()
5967                    .read()
5968                    .expect("paramtab poisoned");
5969                let mut v: Vec<(String, u32, String)> = tab
5970                    .iter()
5971                    .filter(|(name, p)| {
5972                        (p.node.flags as u32 & crate::ported::zsh_h::PM_AUTOLOAD) != 0
5973                            // !!! RUST-ONLY CONDITION !!!
5974                            // C has ONE storage for a module parameter: the
5975                            // PM_AUTOLOAD stub `add_autoparam` (c:1218) plants
5976                            // is the paramtab node, and `addparamdef`
5977                            // (c:1065) only replaces it with the real special
5978                            // when the module actually loads. zshrs seeds every
5979                            // `partab[]` special EAGERLY at startup
5980                            // (`vm_helper::init_partab_params`), which wipes
5981                            // the stub's PM_AUTOLOAD bit, so it models
5982                            // "still an unresolved stub" as a side-set instead
5983                            // (`vm_helper::MATERIALIZED_MODULE_PARAMS`, whose
5984                            // doc comment carries the same warning). That set
5985                            // is per-NAME, exactly like C's flag: after
5986                            // `: ${keymaps}`, zsh drops `keymaps` from
5987                            // `zmodload -ap` but keeps its sibling `widgets`,
5988                            // which a per-MODULE "is it loaded" test cannot
5989                            // express. Read it here so `-ap`/`-apL` see the
5990                            // same stubs C's PM_AUTOLOAD scan does.
5991                            || crate::vm_helper::module_param_is_autoload_stub(name)
5992                    })
5993                    .map(|(name, p)| {
5994                        (
5995                            name.clone(),
5996                            p.node.flags as u32 | crate::ported::zsh_h::PM_AUTOLOAD,
5997                            // c:2716/2718 read the module off `pm->u.str`.
5998                            // An eagerly-seeded special has no `u_str`; its
5999                            // owning module is the one the boot
6000                            // `autofeatures` replay recorded.
6001                            p.u_str.clone().unwrap_or_else(|| {
6002                                table.autoload_params.get(name).cloned().unwrap_or_default()
6003                            }),
6004                        )
6005                    })
6006                    .collect();
6007                v.sort_by(|a, b| a.0.cmp(&b.0)); // c:2758 sorted=1
6008                v
6009            };
6010            for (name, flags, module) in entries {
6011                printautoparams(&name, &module, flags, lon); // c:2758
6012            }
6013            return 0;
6014        }
6015    } else if OPT_ISSET(ops, b'f') {
6016        // c:2763-2778 — math-function branch (-f).
6017        // C body:
6018        //   if (!*args) {
6019        //       MathFunc p;
6020        //       for (p = mathfuncs; p; p = p->next) {
6021        //           if (!(p->flags & MFF_USERFUNC) && p->module) {
6022        //               if (OPT_ISSET(ops,'L')) {
6023        //                   fputs("zmodload -af", stdout);
6024        //                   printf(" %s %s\n", p->module, p->name);
6025        //               } else
6026        //                   printf("%s (%s)\n", p->name, p->module);
6027        //           }
6028        //       }
6029        //       return 0;
6030        //   }
6031        // C walks `mathfuncs` (file-static linked list). Rust port
6032        // walks MATHFUNCS (same shape, Vec). The MFF_USERFUNC filter
6033        // excludes `functions -M` user math from the autoload list.
6034        if args.is_empty() {
6035            let snap: Vec<(String, String)> = {
6036                let tab = MATHFUNCS.lock().unwrap();
6037                tab.iter()
6038                    // c:2769 — `!(p->flags & MFF_USERFUNC) && p->module`.
6039                    .filter_map(|p| {
6040                        if (p.flags & MFF_USERFUNC) != 0 {
6041                            return None;
6042                        }
6043                        p.module.as_ref().map(|m| (p.name.clone(), m.clone()))
6044                    })
6045                    .collect()
6046            };
6047            let l_flag = OPT_ISSET(ops, b'L');
6048            for (name, module) in snap {
6049                if l_flag {
6050                    // c:2770-2772 — `zmodload -af MODULE NAME`.
6051                    println!("zmodload -af {} {}", module, name);
6052                } else {
6053                    // c:2774 — `NAME (MODULE)`.
6054                    println!("{} ({})", name, module);
6055                }
6056            }
6057            return 0;
6058        }
6059    } else {
6060        // Default: builtins branch
6061        if args.is_empty() {
6062            // c:Src/module.c:2756 — `scanhashtable(autoloadtab, 1, 0,
6063            //   0, ...)`. The sorted=1 arg sorts entries by name before
6064            // dispatch. zshrs's HashMap iteration is unordered, so the
6065            // 27 auto-loaded entries came out in random order. Bug
6066            // #222 in docs/BUGS.md. Sort by name to match zsh.
6067            let mut entries: Vec<(&String, &String)> = table.autoload_builtins.iter().collect();
6068            entries.sort_by(|a, b| a.0.cmp(b.0));
6069            for (name, module) in entries {
6070                autoloadscan(
6071                    name,
6072                    module,
6073                    0,
6074                    if OPT_ISSET(ops, b'L') { PRINT_LIST } else { 0 },
6075                );
6076            }
6077            return 0;
6078        }
6079    }
6080
6081    // c:2791-2805 — register/unregister via autofeatures with
6082    // FEAT_AUTOALL. (Earlier zshrs revision inserted into the
6083    // autoload_* maps directly, bypassing autofeatures — so
6084    // `m->autoloads` bookkeeping and the add_autobin/add_autoparam/…
6085    // canonical-table dispatch never ran.)
6086    let fchar: u8 = if OPT_ISSET(ops, b'c') {
6087        if OPT_ISSET(ops, b'I') {
6088            b'C' // c:2754 fchar = OPT_ISSET(ops,'I') ? 'C' : 'c'
6089        } else {
6090            b'c'
6091        }
6092    } else if OPT_ISSET(ops, b'p') {
6093        b'p' // c:2762
6094    } else if OPT_ISSET(ops, b'f') {
6095        b'f' // c:2779
6096    } else {
6097        b'b' // c:2789
6098    };
6099    let mut flags = FEAT_AUTOALL; // c:2791
6100    if OPT_ISSET(ops, b'i') {
6101        flags |= FEAT_IGNORE; // c:2792-2793
6102    }
6103    if OPT_ISSET(ops, b'u') {
6104        /* remove autoload */
6105        // c:2795
6106        flags |= FEAT_REMOVE; // c:2796
6107                              // c:2797 — `modnam = NULL;` — every arg is a feature name.
6108        return autofeatures(table, _nam, None, args, fchar, flags); // c:2805
6109    }
6110    /* add autoload */
6111    // c:2799
6112    let modnam = &args[0]; // c:2800
6113                           // c:2802-2803 — `if (args[1]) args++;` — with a single arg the
6114                           // module name doubles as the feature arg (C quirk; autofeatures
6115                           // then rejects the `/` in it).
6116    let feat_args: &[String] = if args.len() > 1 { &args[1..] } else { args };
6117    autofeatures(table, _nam, Some(modnam), feat_args, fchar, flags) // c:2805
6118}
6119
6120/// Port of `unload_named_module()` from `Src/module.c:2924`. — C decl `unload_named_module(char *modname, char *nam, int silent)`.
6121///
6122/// C body:
6123/// ```c
6124/// int
6125/// unload_named_module(char *modname, char *nam, int silent)
6126/// {
6127///     const char *mname;
6128///     Module m;
6129///     int ret = 0;
6130///     m = find_module(modname, FINDMOD_ALIASP, &mname);
6131///     if (m) {
6132///         int i, del = 0;
6133///         Module dm;
6134///         for (i = 0; i < modulestab->hsize; i++) {
6135///             for (dm = (Module)modulestab->nodes[i]; dm;
6136///                  dm = (Module)dm->node.next) {
6137///                 LinkNode dn;
6138///                 if (!dm->deps || !dm->u.handle)
6139///                     continue;
6140///                 for (dn = firstnode(dm->deps); dn; incnode(dn)) {
6141///                     if (!strcmp((char *) getdata(dn), mname)) {
6142///                         if (dm->node.flags & MOD_UNLOAD)
6143///                             del = 1;
6144///                         else {
6145///                             zwarnnam(nam, "module %s is in use ...");
6146///                             return 1;
6147///                         }
6148///                     }
6149///                 }
6150///             }
6151///         }
6152///         if (del) m->wrapper++;
6153///         if (unload_module(m)) ret = 1;
6154///         if (del) m->wrapper--;
6155///     } else if (!silent) {
6156///         zwarnnam(nam, "no such module %s", modname);
6157///         ret = 1;
6158///     }
6159///     return ret;
6160/// }
6161/// ```
6162///
6163/// Now that unload_module is faithfully ported (2b986c9e8b), this
6164/// wraps it with the C dep-walk that gates the unload on whether
6165/// any other loaded module depends on the target. If a dependent
6166/// is already tagged MOD_UNLOAD, set del=1 to bracket the unload
6167/// with `m->wrapper++` / `m->wrapper--` so unload_module's wrapper
6168/// branch (c:2839-2842) defers correctly.
6169/// WARNING: param names don't match C — Rust=(table, name, nam, silent) vs C=(modname, nam, silent)
6170pub fn unload_named_module(table: &mut modulestab, name: &str, nam: &str, silent: i32) -> i32 {
6171    // c:2924
6172    // c:2930 — `m = find_module(modname, FINDMOD_ALIASP, &mname);`
6173    // Returns the canonical (alias-resolved) name; we drive
6174    // unload_module against that, matching C's `m` pointer.
6175    let mname = match find_module(table, name, FINDMOD_ALIASP) {
6176        Some(n) => n,
6177        None => {
6178            // c:2959-2961 — !m branch: silent gate.
6179            if silent == 0 {
6180                crate::ported::utils::zwarnnam(nam, &format!("no such module {}", name));
6181                return 1;
6182            }
6183            return 0;
6184        }
6185    };
6186
6187    // c:2932 — `int del = 0;`
6188    let mut del = 0;
6189
6190    // c:2935-2952 — scan every module's deps for `mname`.
6191    // Snapshot first so we don't double-borrow the modules HashMap
6192    // when we mutate via unload_module below.
6193    let candidates: Vec<(String, i32, bool, Vec<String>)> = table
6194        .modules
6195        .iter()
6196        .filter_map(|(other_name, other)| {
6197            // c:2939 — `if (!dm->deps || !dm->u.handle) continue;`
6198            // Static-link analog of `u.handle` is MOD_INIT_B ("boot_
6199            // ran"), the same analog `require_module` (c:2352) and
6200            // `getpmmodule` (c:1069) use. It is NOT MOD_LINKED, which
6201            // only means "this name has compiled-in code" and is set on
6202            // every boot node: with that gate, the never-loaded
6203            // `zsh/compctl` / `zsh/complete` / `zsh/computil` /
6204            // `zsh/zleparameter` boot nodes counted as live dependents,
6205            // so a bare `zmodload -u zsh/zle` (nothing loaded) failed
6206            // with "in use by another module" where zsh silently
6207            // returns 0.
6208            // The MOD_UNLOAD check stays INSIDE the inner loop: a
6209            // dependent already flagged for deferred unload must still
6210            // be seen there so it can set `del = 1`.
6211            let deps = other.deps.as_ref()?;
6212            if (other.node.flags & MOD_INIT_B) == 0 {
6213                return None;
6214            }
6215            // Note: C checks `u.handle` (loaded handle) not
6216            // `!MOD_UNLOAD` here — MOD_UNLOAD-flagged modules still
6217            // get scanned because the inner branch needs to see them
6218            // to set `del = 1`.
6219            Some((
6220                other_name.clone(),
6221                other.node.flags,
6222                (other.node.flags & MOD_UNLOAD) != 0,
6223                deps.iter().cloned().collect(),
6224            ))
6225        })
6226        .collect();
6227
6228    for (_other_name, _other_flags, other_unloading, other_deps) in candidates {
6229        for dep in &other_deps {
6230            if dep != &mname {
6231                continue; // c:2942
6232            }
6233            if other_unloading {
6234                // c:2943-2944 — dependent already marked MOD_UNLOAD →
6235                // we'll be cascade-unloading it after the target.
6236                del = 1;
6237            } else {
6238                // c:2945-2948 — live dependent: refuse the unload.
6239                crate::ported::utils::zwarnnam(
6240                    nam,
6241                    &format!(
6242                        "module {} is in use by another module and cannot be unloaded",
6243                        mname
6244                    ),
6245                );
6246                return 1;
6247            }
6248        }
6249    }
6250
6251    // c:2953-2954 — `if (del) m->wrapper++;`. The wrapper++/--
6252    // bracket gates unload_module's wrapper branch (c:2839-2842):
6253    // with wrapper > 0, unload_module sets MOD_UNLOAD and returns
6254    // rather than running finish. The actual cascade fires via the
6255    // recursive deferred-dep walk inside unload_module.
6256    if del != 0 {
6257        if let Some(m) = table.modules.get_mut(&mname) {
6258            m.wrapper += 1;
6259        }
6260    }
6261
6262    // c:2955-2956 — `if (unload_module(m)) ret = 1;`. Rust's
6263    // unload_module returns bool (true=success). Map to C ret:
6264    // false → 1, true → 0.
6265    let mut ret = if !table.unload_module(&mname) { 1 } else { 0 };
6266
6267    // c:2957-2958 — `if (del) m->wrapper--;`
6268    if del != 0 {
6269        if let Some(m) = table.modules.get_mut(&mname) {
6270            m.wrapper -= 1;
6271        }
6272    }
6273    let _ = silent;
6274    let _ = &mut ret;
6275    ret // c:2964
6276}
6277
6278/// Port of `bin_zmodload_load()` from `Src/module.c:2971`. — C decl `bin_zmodload_load(char *nam, char **args, Options ops)`.
6279///
6280/// C body:
6281/// ```c
6282/// bin_zmodload_load(char *nam, char **args, Options ops)
6283/// {
6284///     int ret = 0;
6285///     if(OPT_ISSET(ops,'u')) {
6286///         for(; *args; args++) {
6287///             if (unload_named_module(*args, nam, OPT_ISSET(ops,'i')))
6288///                 ret = 1;
6289///         }
6290///         return ret;
6291///     } else if(!*args) {
6292///         scanhashtable(modulestab, ..., PRINTMOD_LIST);
6293///         return 0;
6294///     } else {
6295///         for (; *args; args++) {
6296///             int tmpret = require_module(*args, NULL, OPT_ISSET(ops,'s'));
6297///             if (tmpret && ret != 1) ret = tmpret;
6298///         }
6299///         return ret;
6300///     }
6301/// }
6302/// ```
6303///
6304/// `zmodload [-u] [args]`: load, unload, or list modules.
6305/// WARNING: param names don't match C — Rust=(table, nam, args, ops) vs C=(nam, args, ops)
6306pub fn bin_zmodload_load(table: &mut modulestab, nam: &str, args: &[String], ops: &options) -> i32 {
6307    // c:2971
6308    let mut ret: i32 = 0;
6309    if OPT_ISSET(ops, b'u') {
6310        // c:2974
6311        // c:2976-2979 — unload loop
6312        for arg in args {
6313            if unload_named_module(table, arg, nam, OPT_ISSET(ops, b'i') as i32) != 0 {
6314                ret = 1;
6315            }
6316        }
6317        return ret; // c:2980
6318    } else if args.is_empty() {
6319        // c:2981
6320        // c:2983-2985 — list modules:
6321        //   `scanhashtable(modulestab, 1, 0, MOD_UNLOAD|MOD_ALIAS,
6322        //                  modulestab->printnode,
6323        //                  OPT_ISSET(ops,'L') ? PRINTMOD_LIST : 0);`
6324        // The 4th arg to scanhashtable is the EXCLUDE mask — entries
6325        // with `MOD_UNLOAD` or `MOD_ALIAS` set are skipped. The
6326        // surviving names are routed through `printmodulenode`,
6327        // which itself gates the visible-line emission on
6328        // `m->u.handle` (=> `MOD_INIT_B` in zshrs) so registered-
6329        // but-unloaded modules drop out. Plain `zmodload` (no `-L`)
6330        // passes `flags=0`; `zmodload -L` passes `PRINTMOD_LIST`.
6331        // Previous Rust impl printed every key in `table.modules`
6332        // unconditionally, which leaked the 32 statically-registered
6333        // builtin entries (#76 in docs/BUGS.md).
6334        let listflags = if OPT_ISSET(ops, b'L') {
6335            PRINTMOD_LIST
6336        } else {
6337            0
6338        };
6339        let mut names: Vec<&String> = table.modules.keys().collect();
6340        names.sort(); // c:scanhashtable sorted=1 arg
6341        for name in names {
6342            let m = &table.modules[name]; // c:154 printnode call
6343            if (m.node.flags & (MOD_UNLOAD | MOD_ALIAS)) != 0 {
6344                continue; // c:2983 EXCLUDE mask
6345            }
6346            let line = printmodulenode(name, m, listflags);
6347            if !line.is_empty() {
6348                println!("{}", line);
6349            }
6350        }
6351        return 0; // c:2986
6352    } else {
6353        // c:2989-2992 — load loop
6354        for arg in args {
6355            let tmpret = require_module(table, arg, None, OPT_ISSET(ops, b's') as i32, false); // c:2990
6356            if tmpret != 0 && ret != 1 {
6357                // c:2991
6358                ret = tmpret;
6359            }
6360            // PFA-SMR: one event per `zmodload MODULE` load form
6361            // (only the bare-load path; listing/-u/-d/-A handled
6362            // upstream and never reaches here). Pass empty flags
6363            // since the per-feature -F path lives in
6364            // bin_zmodload_features. Record even on failure so
6365            // replay sees the user's intent.
6366            #[cfg(feature = "recorder")]
6367            if crate::recorder::is_enabled() {
6368                let ctx = crate::recorder::recorder_ctx_global();
6369                crate::recorder::emit_zmodload(arg, "", ctx);
6370            }
6371        }
6372        ret
6373    }
6374}
6375
6376/// Port of `bin_zmodload_features()` from `Src/module.c:3003`. — C decl `bin_zmodload_features(const char *nam, char **args, Options ops)`.
6377///
6378/// `zmodload -F [-L|-l|-P|-a|-m|-i] module [+/-feature ...]` —
6379/// per-feature enable/disable for an already-loaded module.
6380///
6381/// C body (~135 lines) handles:
6382/// - no module: list all modules with their features (`-L` long form,
6383///   `-l` show all enables, `-a` show autoloads).
6384/// - `-P` requires a module name; lists patterns.
6385/// - `-m` interprets each feature as a glob pattern.
6386/// - default: `+feature` enables, `-feature` disables, then calls
6387///   `do_module_features` to apply.
6388/// WARNING: param names don't match C — Rust=(table, nam, args, ops) vs C=(nam, args, ops)
6389pub fn bin_zmodload_features(
6390    table: &mut modulestab,
6391    nam: &str,
6392    args: &[String],
6393    ops: &options,
6394) -> i32 {
6395    // c:3003
6396    let modname = args.first(); // c:3003
6397    let rest_args = if args.is_empty() {
6398        &args[..]
6399    } else {
6400        &args[1..]
6401    };
6402
6403    // c:3010-3030 — no-module-name listing branch.
6404    // C body:
6405    //   if (modname)
6406    //       args++;
6407    //   else if (OPT_ISSET(ops,'L')) {
6408    //       int printflags = PRINTMOD_LIST|PRINTMOD_FEATURES;
6409    //       if (OPT_ISSET(ops,'P')) {
6410    //           zwarnnam(nam, "-P is only allowed with a module name");
6411    //           return 1;
6412    //       }
6413    //       if (OPT_ISSET(ops,'l')) printflags |= PRINTMOD_LISTALL;
6414    //       if (OPT_ISSET(ops,'a')) printflags |= PRINTMOD_AUTO;
6415    //       scanhashtable(modulestab, 1, 0, MOD_ALIAS,
6416    //                     modulestab->printnode, printflags);
6417    //       return 0;
6418    //   }
6419    //   if (!modname) {
6420    //       zwarnnam(nam, "-F requires a module name");
6421    //       return 1;
6422    //   }
6423    if modname.is_none() {
6424        if OPT_ISSET(ops, b'L') {
6425            // c:3012
6426            // c:3014-3016 — `-P` check must fire BEFORE the listing
6427            // dispatch. Without modname, -P is illegal.
6428            if OPT_ISSET(ops, b'P') {
6429                zwarnnam(nam, "-P is only allowed with a module name"); // c:3015
6430                return 1; // c:3016
6431            }
6432            // c:3013 / c:3018-3021 — assemble PRINTMOD_LIST|PRINTMOD_FEATURES
6433            //                       [|PRINTMOD_LISTALL][|PRINTMOD_AUTO]
6434            let mut printflags = PRINTMOD_LIST | PRINTMOD_FEATURES;
6435            if OPT_ISSET(ops, b'l') {
6436                printflags |= PRINTMOD_LISTALL; // c:3019
6437            }
6438            if OPT_ISSET(ops, b'a') {
6439                printflags |= PRINTMOD_AUTO; // c:3021
6440            }
6441            // c:3022-3023 — `scanhashtable(modulestab, 1, 0, MOD_ALIAS,
6442            //                              printnode, printflags);`
6443            // sorted=1, INCLUDE=0 (all), EXCLUDE=MOD_ALIAS (skip aliases).
6444            let mut names: Vec<String> = table
6445                .modules
6446                .iter()
6447                .filter(|(_, m)| (m.node.flags & MOD_ALIAS) == 0) // c:3022 EXCLUDE
6448                .map(|(n, _)| n.clone())
6449                .collect();
6450            names.sort(); // c:3022 sorted=1
6451            for name in &names {
6452                if printflags & PRINTMOD_AUTO != 0 {
6453                    // c:229-231 / c:238-251 — autoload form; printmodulenode
6454                    // covers this branch fully.
6455                    let m = &table.modules[name];
6456                    let line = printmodulenode(name, m, printflags);
6457                    if !line.is_empty() {
6458                        println!("{}", line);
6459                    }
6460                    continue;
6461                }
6462                // c:218 / c:232-235 — loaded-module + features gate:
6463                // `if (features_module(m, &features) ||
6464                //      enables_module(m, &enables) || !*features) return;`
6465                // printmodulenode has no &table handle, so the FEATURES
6466                // dispatch happens here (see its c:252-263 comment).
6467                let loaded = {
6468                    let m = &table.modules[name];
6469                    (m.node.flags & MOD_INIT_B) != 0 && (m.node.flags & MOD_UNLOAD) == 0
6470                };
6471                if !loaded {
6472                    continue;
6473                }
6474                let mut features: Vec<String> = Vec::new();
6475                if features_module(table, name, &mut features) != 0 || features.is_empty() {
6476                    continue; // c:233-235
6477                }
6478                let mut enables_opt: Option<Vec<i32>> = None;
6479                if enables_module(table, name, &mut enables_opt) != 0 {
6480                    continue; // c:233-235
6481                }
6482                let enables = enables_opt.unwrap_or_else(|| vec![0; features.len()]);
6483                // c:237-245 — `printf("zmodload "); fputs("-F ", stdout);`
6484                let mut line = String::from("zmodload -F ");
6485                if name.starts_with('-') {
6486                    line.push_str("-- "); // c:244-245
6487                }
6488                line.push_str(&crate::ported::utils::quotedzputs(name)); // c:246
6489                                                                         // c:252-262 — per-feature tail: LISTALL emits ` +f`/` -f`,
6490                                                                         // plain -L skips disabled and emits ` f`.
6491                for (f, on) in features.iter().zip(enables.iter()) {
6492                    if printflags & PRINTMOD_LISTALL != 0 {
6493                        line.push_str(if *on != 0 { " +" } else { " -" }); // c:256
6494                    } else if *on == 0 {
6495                        continue; // c:258
6496                    } else {
6497                        line.push(' '); // c:260
6498                    }
6499                    line.push_str(&crate::ported::utils::quotedzputs(f)); // c:261
6500                }
6501                println!("{}", line);
6502            }
6503            return 0; // c:3024
6504        }
6505        zwarnnam(nam, "-F requires a module name"); // c:3028
6506        return 1; // c:3029
6507    }
6508
6509    let modname = modname.unwrap();
6510
6511    // c:3032-3047 — `-m` glob-pattern branch.
6512    // C body:
6513    //   if (OPT_ISSET(ops,'m')) {
6514    //       patprogs = zhalloc(arrlen(args)*sizeof(Patprog));
6515    //       for (argp = args; *argp; argp++, patprogp++) {
6516    //           if (*arg == '+' || *arg == '-') arg++;
6517    //           tokenize(arg);
6518    //           *patprogp = patcompile(arg, 0, 0);
6519    //       }
6520    //   } else patprogs = NULL;
6521    let patprogs = if OPT_ISSET(ops, b'm') {
6522        Some(
6523            rest_args
6524                .iter()
6525                .map(|raw| {
6526                    let arg = raw
6527                        .strip_prefix('+')
6528                        .or_else(|| raw.strip_prefix('-'))
6529                        .unwrap_or(raw); // c:3044-3045
6530                    let mut pat_src = arg.to_string();
6531                    crate::ported::glob::tokenize(&mut pat_src); // c:3046
6532                    crate::ported::pattern::patcompile(&pat_src, 0, None) // c:3047
6533                })
6534                .collect::<Vec<_>>(),
6535        )
6536    } else {
6537        None
6538    };
6539    // `-m` also reaches autofeatures / do_module_features through
6540    // FEAT_PATTERN_ARGS (see its doc comment).
6541
6542    // c:3049-3226 — `-l/-L/-e` arm: list features one per line with
6543    // +/- (-l), as a `zmodload -F` statement (-L), or test existence
6544    // (-e).
6545    if OPT_ISSET(ops, b'l') || OPT_ISSET(ops, b'L') || OPT_ISSET(ops, b'e') {
6546        let param: Option<String> = OPT_ARG_SAFE(ops, b'P').map(|s| s.to_string()); // c:3060
6547                                                                                    // c:3062 — `m = find_module(modname, FINDMOD_ALIASP, NULL);`
6548        let resolved = find_module(table, modname, FINDMOD_ALIASP);
6549
6550        // c:3063-3107 — `-a` sub-arm: autoload listing/testing.
6551        if OPT_ISSET(ops, b'a') {
6552            // c:3067-3068 — `if (!m || !m->autoloads) return 1;`
6553            let autoloads: Vec<String> = match resolved
6554                .as_ref()
6555                .and_then(|r| table.modules.get(r))
6556                .and_then(|m| m.autoloads.as_ref())
6557            {
6558                Some(al) => al.iter().cloned().collect(),
6559                None => return 1,
6560            };
6561            if OPT_ISSET(ops, b'e') {
6562                // c:3070-3085 — each arg must (mis)match per its +/- sense.
6563                for fstr in rest_args {
6564                    let (sense, name) = match fstr.strip_prefix('+') {
6565                        Some(rest) => (true, rest), // c:3074
6566                        None => match fstr.strip_prefix('-') {
6567                            Some(rest) => (false, rest), // c:3076-3078
6568                            None => (true, fstr.as_str()),
6569                        },
6570                    };
6571                    // c:3080-3082 — `(linknodebystring(...) != NULL) != sense`
6572                    if autoloads.iter().any(|a| a == name) != sense {
6573                        return 1; // c:3082
6574                    }
6575                }
6576                return 0; // c:3084
6577            }
6578            if let Some(p) = param {
6579                // c:3086-3088 / c:3098-3100 — collect into the array,
6580                // then `setaparam(param, arrset)`.
6581                // c:3103-3106
6582                if crate::ported::params::setaparam(&p, autoloads).is_none() {
6583                    return 1; // c:3105
6584                }
6585                return 0; // c:3106
6586            }
6587            if OPT_ISSET(ops, b'L') {
6588                // c:3089-3091 — `printf("zmodload -aF %s%c", ...)`
6589                let rname = resolved.as_deref().unwrap_or(modname);
6590                print!(
6591                    "zmodload -aF {}{}",
6592                    crate::ported::utils::quotedzputs(rname),
6593                    if autoloads.is_empty() { '\n' } else { ' ' }
6594                );
6595                // c:3092-3098 — space-separated, final '\n'.
6596                for (i, al) in autoloads.iter().enumerate() {
6597                    print!("{}{}", al, if i + 1 < autoloads.len() { ' ' } else { '\n' });
6598                }
6599            } else {
6600                // c:3093-3097 — one per line.
6601                for al in &autoloads {
6602                    println!("{}", al);
6603                }
6604            }
6605            return 0; // c:3107
6606        }
6607
6608        // c:3108-3112 — `if (!m || !m->u.handle || (m->node.flags &
6609        // MOD_UNLOAD))`. zshrs maps "u.handle installed" to MOD_INIT_B
6610        // (see the printmodulenode comment at c:218-241 above).
6611        let loaded = resolved
6612            .as_ref()
6613            .and_then(|r| table.modules.get(r))
6614            .map(|m| (m.node.flags & MOD_INIT_B) != 0 && (m.node.flags & MOD_UNLOAD) == 0)
6615            .unwrap_or(false);
6616        if !loaded {
6617            if !OPT_ISSET(ops, b'e') {
6618                zwarnnam(nam, &format!("module `{}' is not yet loaded", modname));
6619                // c:3110
6620            }
6621            return 1; // c:3111
6622        }
6623        let rname = resolved.unwrap();
6624
6625        // c:3113-3118 — `features_module(m, &features)`.
6626        let mut features: Vec<String> = Vec::new();
6627        if features_module(table, &rname, &mut features) != 0 {
6628            if !OPT_ISSET(ops, b'e') {
6629                zwarnnam(
6630                    nam,
6631                    &format!("module `{}' does not support features", rname), // c:3115
6632                );
6633            }
6634            return 1; // c:3117
6635        }
6636        // c:3119-3124 — `enables_module(m, &enables)`.
6637        let mut enables_opt: Option<Vec<i32>> = None;
6638        if enables_module(table, &rname, &mut enables_opt) != 0 {
6639            /* this shouldn't ever happen, so don't silence this error */
6640            // c:3120
6641            zwarnnam(
6642                nam,
6643                &format!("error getting enabled features for module `{}'", rname), // c:3121
6644            );
6645            return 1; // c:3123
6646        }
6647        let enables: Vec<i32> = enables_opt.unwrap_or_else(|| vec![0; features.len()]);
6648
6649        // c:3125-3155 — validate every feature argument.
6650        for (iarg, raw) in rest_args.iter().enumerate() {
6651            // c:3127-3135 — strip +/- into `on`.
6652            let (on, arg): (i32, &str) = match raw.strip_prefix('-') {
6653                Some(rest) => (0, rest),
6654                None => match raw.strip_prefix('+') {
6655                    Some(rest) => (1, rest),
6656                    None => (-1, raw.as_str()),
6657                },
6658            };
6659            let mut found = 0;
6660            for (fp, ep) in features.iter().zip(enables.iter()) {
6661                // c:3142-3143 — `patprogs ? pattry(patprogs[iarg], *fp) : !strcmp(arg, *fp)`
6662                let hit = match patprogs.as_ref() {
6663                    Some(pp) => pp[iarg]
6664                        .as_ref()
6665                        .is_some_and(|p| crate::ported::pattern::pattry(p, fp)),
6666                    None => arg == fp,
6667                };
6668                if hit {
6669                    // c:3140-3142 — for -e, check given state, if any.
6670                    if OPT_ISSET(ops, b'e') && on != -1 && on != (ep & 1) {
6671                        return 1; // c:3142
6672                    }
6673                    found += 1;
6674                    if patprogs.is_none() {
6675                        break; // c:3149-3150
6676                    }
6677                }
6678            }
6679            if found == 0 {
6680                // c:3148-3154
6681                if !OPT_ISSET(ops, b'e') {
6682                    zwarnnam(
6683                        nam,
6684                        &if patprogs.is_some() {
6685                            format!("module `{}' has no feature matching: `{}'", modname, raw)
6686                        } else {
6687                            format!("module `{}' has no such feature: `{}'", modname, raw)
6688                        },
6689                    );
6690                }
6691                return 1; // c:3153
6692            }
6693        }
6694        if OPT_ISSET(ops, b'e') {
6695            /* yep, everything we want exists */
6696            // c:3156
6697            return 0; // c:3157
6698        }
6699
6700        let opt_big_l = OPT_ISSET(ops, b'L');
6701        let opt_small_l = OPT_ISSET(ops, b'l');
6702        // c:3186-3194 / c:3164-3172 — arg filter helpers. The C print
6703        // loop compares the UNSTRIPPED arg (`!strcmp(*fp, *argp)`,
6704        // c:3193) while the param-count loop compares stripped
6705        // (`!strcmp(*fp, arg)`, c:3170). Ported as written.
6706        let matches_stripped = |f: &str| -> bool {
6707            rest_args.is_empty()
6708                || rest_args.iter().enumerate().any(|(iarg, raw)| {
6709                    let arg = raw
6710                        .strip_prefix('+')
6711                        .or_else(|| raw.strip_prefix('-'))
6712                        .unwrap_or(raw);
6713                    match patprogs.as_ref() {
6714                        Some(pp) => pp[iarg]
6715                            .as_ref()
6716                            .is_some_and(|p| crate::ported::pattern::pattry(p, f)),
6717                        None => f == arg,
6718                    }
6719                })
6720        };
6721        let matches_unstripped = |f: &str| -> bool {
6722            rest_args.is_empty()
6723                || rest_args.iter().enumerate().any(|(iarg, raw)| match patprogs.as_ref() {
6724                    Some(pp) => pp[iarg]
6725                        .as_ref()
6726                        .is_some_and(|p| crate::ported::pattern::pattry(p, f)),
6727                    None => f == raw,
6728                })
6729        };
6730
6731        let mut arrset: Option<Vec<String>> = None;
6732        if param.is_some() {
6733            // c:3158-3183 — size pass folded away (Vec grows); keep the
6734            // same membership filter.
6735            arrset = Some(Vec::new());
6736        } else if opt_big_l {
6737            // c:3184-3185 — `printf("zmodload -F %s ", m->node.nam);`
6738            print!("zmodload -F {} ", crate::ported::utils::quotedzputs(&rname));
6739        }
6740        // c:3186-3219 — main feature emit loop.
6741        for (i, (f, ep)) in features.iter().zip(enables.iter()).enumerate() {
6742            if param.is_some() {
6743                if !matches_stripped(f) {
6744                    continue; // c:3170-3173 stripped compare
6745                }
6746            } else if !matches_unstripped(f) {
6747                continue; // c:3193-3196 unstripped compare
6748            }
6749            let onoff: &str = if opt_big_l && !opt_small_l {
6750                // c:3198-3200
6751                if *ep == 0 {
6752                    continue; // c:3199
6753                }
6754                ""
6755            } else if *ep != 0 {
6756                "+" // c:3203
6757            } else {
6758                "-" // c:3205
6759            };
6760            if let Some(ref mut arr) = arrset {
6761                arr.push(format!("{}{}", onoff, f)); // c:3208 bicat
6762            } else {
6763                // c:3210-3216 — term ' ' while a next feature EXISTS in
6764                // the full array (fp[1]), even if it won't be printed.
6765                let term = if opt_big_l && i + 1 < features.len() {
6766                    ' '
6767                } else {
6768                    '\n'
6769                };
6770                print!("{}{}{}", onoff, crate::ported::utils::quotedzputs(f), term);
6771            }
6772        }
6773        if let (Some(p), Some(arr)) = (param, arrset) {
6774            // c:3220-3224 — `setaparam(param, arrset)`.
6775            if crate::ported::params::setaparam(&p, arr).is_none() {
6776                return 1; // c:3223
6777            }
6778        }
6779        return 0; // c:3225
6780    }
6781
6782    // c:3227-3229 — `-P` is illegal without -l/-L/-e.
6783    if OPT_ISSET(ops, b'P')
6784        && !(OPT_ISSET(ops, b'l') || OPT_ISSET(ops, b'L') || OPT_ISSET(ops, b'e'))
6785    {
6786        zwarnnam(nam, "-P can only be used with -l or -L"); // c:3228
6787        return 1; // c:3229
6788    }
6789
6790    // c:3230-3247 — `-a` arm: route through autofeatures with
6791    // FEAT_IGNORE (the autoload-feature registration path).
6792    if OPT_ISSET(ops, b'a') {
6793        // c:3231-3234 — `-m` incompatible with `-a`.
6794        if OPT_ISSET(ops, b'm') {
6795            zwarnnam(nam, "-m cannot be used with -a"); // c:3232
6796            return 1; // c:3233
6797        }
6798        // c:3246 — `return autofeatures(nam, modname, args, 0, FEAT_IGNORE);`
6799        // FEAT_IGNORE is hard-coded here because marking-for-autoload
6800        // is separate from enable/disable (per the c:3236-3244 comment).
6801        return autofeatures(table, nam, Some(modname), rest_args, 0, FEAT_IGNORE);
6802    }
6803
6804    // c:3249-3260 — default arm: build Feature_enables array from
6805    // `+name`/`-name` args, then `require_module(modname, features,
6806    // OPT_ISSET(ops,'s'))`.
6807    //
6808    // C builds a fep[] array with str + (optional) patprog pairs.
6809    // The Rust port flattens to a `Vec<String>`; the `-m` decision rides
6810    // along as the `pat` argument of require_module (FEAT_PATTERN_ARGS).
6811    let feats: Vec<String> = rest_args.to_vec();
6812    // c:3252-3260 —
6813    //   fep = features = (Feature_enables)zhalloc((arrlen(args)+1)*sizeof(*fep));
6814    //   while (*args) { fep->str = *args++; fep->pat = …; fep++; }
6815    //   fep->str = NULL; fep->pat = NULL;
6816    //   return require_module(modname, features, OPT_ISSET(ops,'s'));
6817    // The array is ALWAYS allocated in this arm, so `enablesarr` reaching
6818    // do_module_features (c:2077) is never NULL even when `zmodload -F
6819    // MODULE` is given no feature words — and NULL is what selects C's
6820    // "enable all features" arm (c:2105-2112). An empty non-NULL array
6821    // therefore means "enable NOTHING", which is exactly what
6822    // `zmodload -F zsh/datetime; zmodload -lF zsh/datetime` must report
6823    // (`-b:strftime -p:EPOCHSECONDS …`, V04features.ztst:1). Mapping the
6824    // empty case to `None` collapsed the two and enabled everything.
6825    let features_arg = Some(feats.as_slice());
6826    require_module(
6827        table,
6828        modname,
6829        features_arg,
6830        OPT_ISSET(ops, b's') as i32,
6831        OPT_ISSET(ops, b'm'), // c:3258 `fep->pat = patprogs ? *patprogs++ : NULL`
6832    ) // c:3260
6833}
6834
6835/// Port of `ensurefeature()` from `Src/module.c:3415`. — C decl `ensurefeature(const char *modname, const char *prefix, const char *feature)`.
6836///
6837/// C body:
6838/// ```c
6839/// ensurefeature(const char *modname, const char *prefix, const char *feature)
6840/// {
6841///     char *f;
6842///     struct feature_enables features[2];
6843///     if (!feature)
6844///         return require_module(modname, NULL, 0);
6845///     f = dyncat(prefix, feature);
6846///     features[0].str = f;
6847///     features[0].pat = NULL;
6848///     features[1].str = NULL;
6849///     features[1].pat = NULL;
6850///     return require_module(modname, features, 0);
6851/// }
6852/// ```
6853/// WARNING: param names don't match C — Rust=(table, modname, prefix, feature) vs C=(modname, prefix, feature)
6854pub fn ensurefeature(
6855    table: &mut modulestab,
6856    modname: &str,
6857    prefix: &str,
6858    feature: Option<&str>,
6859) -> i32 {
6860    // c:3415
6861    match feature {
6862        // c:3420-3421 — `if (!feature) return require_module(modname, NULL, 0);`
6863        None => require_module(table, modname, None, 0, false),
6864        Some(f) => {
6865            // c:3422-3428 — build single-element features[2] array.
6866            let combined = crate::ported::string::dyncat(prefix, f); // c:3422
6867            let arr = vec![combined];
6868            require_module(table, modname, Some(&arr), 0, false) // c:3428
6869        }
6870    }
6871}
6872
6873/// Port of `autofeatures()` from `Src/module.c:3437`. — C decl `autofeatures(const char *cmdnam, const char *module, char **features, int prefchar, int defflags)`.
6874///
6875/// C body is ~140 lines. Top-level structure:
6876/// ```c
6877/// autofeatures(const char *cmdnam, const char *module, char **features,
6878///              int prefchar, int defflags)
6879/// {
6880///     // Resolve module, fetch its features+enables tables.
6881///     // For each feature in `features`:
6882///     //   parse `+`/`-` prefix → add/remove
6883///     //   parse type prefix (b/c/C/p/f) → fchar
6884///     //   dispatch to add_aliasbuiltin / add_autocondition /
6885///     //     add_autoparam / add_automathfunc / del_* matching
6886/// }
6887/// ```
6888///
6889/// Static-link path: registers each `module:feature` pair into the
6890/// matching `table.autoload_*` map. Honors `+`/`-` prefix for
6891/// add/remove, and the type prefix or `prefchar` arg for routing.
6892/// WARNING: param names don't match C — Rust=(table, _cmdnam, module, features, prefchar, defflags) vs C=(cmdnam, module, features, prefchar, defflags)
6893pub fn autofeatures(
6894    table: &mut modulestab,
6895    cmdnam: &str,
6896    module: Option<&str>,
6897    features: &[String],
6898    prefchar: u8,
6899    defflags: i32,
6900) -> i32 {
6901    // c:3437
6902    let mut ret: i32 = 0;
6903
6904    // c:3445-3453 — resolve `defm` up front (FINDMOD_ALIASP|
6905    // FINDMOD_CREATE); if its union slot is populated (loaded —
6906    // MOD_INIT_B in zshrs, see the printmodulenode c:218-241 note)
6907    // fetch the feature + enable tables for the c:3558-3577 checks.
6908    let mut modfeatures: Option<Vec<String>> = None;
6909    let mut modenables: Vec<i32> = Vec::new();
6910    let defm_name: Option<String> = match module {
6911        Some(modn) => {
6912            let resolved = find_module(table, modn, FINDMOD_ALIASP | FINDMOD_CREATE);
6913            if let Some(ref r) = resolved {
6914                let booted = table
6915                    .modules
6916                    .get(r)
6917                    .map(|m| (m.node.flags & MOD_INIT_B) != 0)
6918                    .unwrap_or(false);
6919                if booted {
6920                    // c:3449-3451
6921                    let mut f: Vec<String> = Vec::new();
6922                    if features_module(table, r, &mut f) == 0 {
6923                        let mut e: Option<Vec<i32>> = None;
6924                        let _ = enables_module(table, r, &mut e);
6925                        modenables = e.unwrap_or_else(|| vec![0; f.len()]);
6926                        modfeatures = Some(f);
6927                    }
6928                }
6929            }
6930            resolved
6931        }
6932        None => None, // c:3454-3455 `defm = NULL`
6933    };
6934
6935    for feature in features {
6936        let s = feature.as_str();
6937        let mut add: bool = true; // c:3466 / c:3477 default `add = 1`
6938        let mut flags = defflags; // c:3458 `flags = defflags`
6939
6940        // `feature_full` is the string C keeps in `m->autoloads`
6941        // (c:3584/3597 `ztrdup(feature)`): the arg after the +/-
6942        // strip, type prefix included — prefchar mode synthesizes it
6943        // via `sprintf(feature, "%c:%s", fchar, fnam)` (c:3468-3469).
6944        let prefixed: String;
6945        let (fchar, fnam, feature_full): (u8, &str, &str) = if prefchar != 0 {
6946            // c:3461-3470 — `prefchar` mode: feature is bare name with
6947            // no `+`/`-` / `b:` prefix; fchar comes from the arg.
6948            prefixed = format!("{}:{}", prefchar as char, s); // c:3468-3469
6949            (prefchar, s, prefixed.as_str()) // c:3467-3468
6950        } else {
6951            // c:3471-3490 — parse `+`/`-` then the `b:`/`c:`/`C:`/`p:`/`f:`
6952            // type prefix.
6953            let mut t = s;
6954            if let Some(rest) = t.strip_prefix('-') {
6955                // c:3473
6956                add = false;
6957                t = rest;
6958            } else if let Some(rest) = t.strip_prefix('+') {
6959                // c:3478
6960                t = rest;
6961            }
6962            // c:3482-3487 — bad format check: `!*feature || feature[1] != ':'`
6963            let bytes = t.as_bytes();
6964            if bytes.is_empty() || bytes.len() < 2 || bytes[1] != b':' {
6965                // c:3483-3486 — zwarnnam + ret=1 + continue.
6966                crate::ported::utils::zwarnnam(
6967                    cmdnam,
6968                    &format!("bad format for autoloadable feature: `{}'", t),
6969                );
6970                ret = 1; // c:3485
6971                continue; // c:3486
6972            }
6973            // c:3488-3489 — `fnam = feature + 2; fchar = feature[0];`
6974            (bytes[0], &t[2..], t)
6975        };
6976
6977        // c:3491-3492 — `if (flags & FEAT_REMOVE) add = 0;`
6978        if (flags & FEAT_REMOVE) != 0 {
6979            add = false;
6980        }
6981
6982        let typnam: &str; // c:3457
6983        let _ = typnam; // referenced below for fnam validation
6984        let typnam = match fchar {
6985            // c:3494-3522 — switch (fchar): map each type-letter to
6986            // the typnam used in the diagnostic + add/del fn dispatch.
6987            b'b' => "builtin", // c:3495-3498
6988            b'c' | b'C' => {
6989                if fchar == b'C' {
6990                    flags |= FEAT_INFIX; // c:3501
6991                }
6992                "condition" // c:3505
6993            }
6994            b'f' => "math function", // c:3508-3511
6995            b'p' => "parameter",     // c:3513-3516
6996            _ => {
6997                // c:3518-3522 — `zwarnnam(cmdnam, "bad autoloadable
6998                // feature type: `%c'", fchar); ret = 1; continue;`
6999                crate::ported::utils::zwarnnam(
7000                    cmdnam,
7001                    &format!("bad autoloadable feature type: `{}'", fchar as char),
7002                );
7003                ret = 1; // c:3521
7004                continue; // c:3522
7005            }
7006        };
7007
7008        // c:3525-3529 — reject `/` in the feature name.
7009        if fnam.contains('/') {
7010            crate::ported::utils::zwarnnam(
7011                cmdnam,
7012                &format!("{}: `/' is invalid in a {}", fnam, typnam),
7013            );
7014            ret = 1;
7015            continue;
7016        }
7017
7018        // c:3531-3553 — resolve module: if `module` arg is None,
7019        // walk every module's `m->autoloads` list looking for the
7020        // feature; if found, that's the owning module. C's
7021        // `m->autoloads` per-module list isn't modelled in the Rust
7022        // port — the autoload_* HashMaps store `feature → module`
7023        // directly, so we can derive the owning module from those.
7024        // When `module` arg IS set, use it (c:3553 `m = defm;`).
7025        let modname_owned: String = match module {
7026            Some(m) => m.to_string(), // c:3553
7027            None => {
7028                // c:3537-3551 — search for the owning module across all
7029                // autoload maps; fall back to error if not found.
7030                let map = match fchar {
7031                    b'b' => &table.autoload_builtins,
7032                    b'c' | b'C' => &table.autoload_conditions,
7033                    b'p' => &table.autoload_params,
7034                    b'f' => &table.autoload_mathfuncs,
7035                    _ => unreachable!(),
7036                };
7037                match map.get(fnam).cloned() {
7038                    Some(m) => m,
7039                    None => {
7040                        if (flags & FEAT_IGNORE) == 0 {
7041                            // c:3546-3549
7042                            ret = 1;
7043                            crate::ported::utils::zwarnnam(
7044                                cmdnam,
7045                                &format!("{}: no such {}", fnam, typnam),
7046                            );
7047                        }
7048                        continue; // c:3550
7049                    }
7050                }
7051            }
7052        };
7053        let modname = modname_owned.as_str();
7054
7055        // c:3554 `subret = 0;` — the m->autoloads maintenance below can
7056        // set it to ±2 on the remove-missing path (c:3614).
7057        let mut autoload_subret: i32 = 0;
7058        // Owning module node: the alias-resolved defm when a module arg
7059        // was given (C `m = defm`, c:3553), else the searched-up name.
7060        let owner: &str = match (module.is_some(), defm_name.as_deref()) {
7061            (true, Some(r)) => r,
7062            _ => modname,
7063        };
7064        if add {
7065            // c:3558-3577 — if the module is already loaded, the feature
7066            // must exist in its table; if it's already enabled there is
7067            // nothing to mark.
7068            if module.is_some() {
7069                if let Some(ref mf) = modfeatures {
7070                    match mf.iter().position(|f| f == feature_full) {
7071                        None => {
7072                            // c:3566-3570
7073                            crate::ported::utils::zwarnnam(
7074                                cmdnam,
7075                                &format!(
7076                                    "module `{}' has no such feature: `{}'",
7077                                    owner, feature_full
7078                                ),
7079                            );
7080                            ret = 1;
7081                            continue;
7082                        }
7083                        Some(idx) => {
7084                            if modenables.get(idx).copied().unwrap_or(0) != 0 {
7085                                continue; // c:3572-3577 already provided
7086                            }
7087                        }
7088                    }
7089                }
7090            }
7091            // c:3583-3603 — insert into m->autoloads in lexical order
7092            // (dup is "never an error", c:3590-3593).
7093            if let Some(m) = table.modules.get_mut(owner) {
7094                let list = m
7095                    .autoloads
7096                    .get_or_insert_with(crate::ported::linklist::znewlinklist);
7097                let mut insert_at: Option<usize> = Some(list.len()); // c:3602 append default
7098                for (i, existing) in list.iter().enumerate() {
7099                    match feature_full.cmp(existing.as_str()) {
7100                        std::cmp::Ordering::Equal => {
7101                            insert_at = None; // c:3591-3593 already there
7102                            break;
7103                        }
7104                        std::cmp::Ordering::Less => {
7105                            insert_at = Some(i); // c:3595-3598
7106                            break;
7107                        }
7108                        std::cmp::Ordering::Greater => {}
7109                    }
7110                }
7111                if let Some(i) = insert_at {
7112                    list.insert_at(i, feature_full.to_string());
7113                }
7114            }
7115        } else {
7116            // c:3605-3615 — `else if (m->autoloads) { remnode or
7117            // subret = FEAT_IGNORE ? -2 : 2; }`
7118            let removed = table
7119                .modules
7120                .get_mut(owner)
7121                .and_then(|m| m.autoloads.as_mut())
7122                .map(|list| {
7123                    match list
7124                        .iter()
7125                        .position(|existing| existing.as_str() == feature_full)
7126                    {
7127                        Some(i) => {
7128                            list.delete_node(i);
7129                            true
7130                        }
7131                        None => false,
7132                    }
7133                })
7134                .unwrap_or(false);
7135            if !removed {
7136                autoload_subret = if (flags & FEAT_IGNORE) != 0 { -2 } else { 2 };
7137                // c:3614
7138            }
7139        }
7140
7141        // c:3618-3619 — `if (subret == 0) subret = fn(module, fnam, flags);`
7142        // The fn does NOT run when the m->autoloads remove already
7143        // produced ±2. Dispatch through the per-type add/del fn so the
7144        // canonical tables (paramtab for `p:`, condtab for `c:`, etc.)
7145        // carry the PM_AUTOLOAD / CONDF flag bits expected by
7146        // downstream code (e.g. paramtypestr's `undefined NAME`).
7147        let subret = if autoload_subret != 0 {
7148            autoload_subret // c:3618 subret already set
7149        } else if add {
7150            match fchar {
7151                b'p' => add_autoparam(modname, fnam, flags),
7152                b'f' => add_automathfunc(table, modname, fnam, flags),
7153                b'b' => table.add_autobin(fnam, modname, flags),
7154                b'c' | b'C' => table.add_autocond(fnam, modname, flags),
7155                _ => unreachable!(),
7156            }
7157        } else {
7158            match fchar {
7159                b'p' => del_autoparam(modname, fnam, flags),
7160                b'f' => del_automathfunc(table, modname, fnam, flags),
7161                b'b' => table.del_autobin(fnam, flags),
7162                b'c' | b'C' => table.del_autocond(fnam, flags),
7163                _ => unreachable!(),
7164            }
7165        };
7166
7167        // !!! RUST-ONLY BOOKKEEPING — NO DIRECT C COUNTERPART !!!
7168        // C keeps ONE store per feature kind: the autoload stub IS the
7169        // `builtintab`/`condtab`/`paramtab`/`mathfuncs` entry, so
7170        // `add_autobin`/`del_autobin` (c:426/464) et al. are the only
7171        // writers. zshrs splits that into the canonical table PLUS the
7172        // `autoload_*` feature→module reverse maps the static-link
7173        // dispatch needs (`resolve_autoload_builtin`, printautoparams'
7174        // module fallback, autofeatures' own c:3535-3555 owner search).
7175        //
7176        // These MUST run AFTER the c:3618-3619 fn dispatch, never
7177        // before: `del_autobin` probes `autoload_builtins` as its
7178        // stand-in for C's `builtintab->getnode2(builtintab, bnam)`
7179        // (c:466), so removing the entry first made every
7180        // `zmodload -ub NAME` report `NAME: no such builtin` (subret 2)
7181        // for a builtin it had just autoloaded — V01zmodload.ztst
7182        // "Add/remove autoloaded builtin".
7183        //
7184        // Mirror the canonical table's outcome: insert only on a clean
7185        // add (subret 0), drop only when the delete actually happened
7186        // (subret 0, or -2 = "not there, FEAT_IGNORE" per c:3614).
7187        if add {
7188            if subret == 0 {
7189                match fchar {
7190                    b'b' => {
7191                        table
7192                            .autoload_builtins
7193                            .insert(fnam.to_string(), modname.to_string());
7194                    }
7195                    b'c' | b'C' => {
7196                        table
7197                            .autoload_conditions
7198                            .insert(fnam.to_string(), modname.to_string());
7199                    }
7200                    b'p' => {
7201                        table
7202                            .autoload_params
7203                            .insert(fnam.to_string(), modname.to_string());
7204                    }
7205                    b'f' => {
7206                        table
7207                            .autoload_mathfuncs
7208                            .insert(fnam.to_string(), modname.to_string());
7209                    }
7210                    _ => unreachable!(),
7211                }
7212            }
7213        } else if subret == 0 || subret == -2 {
7214            match fchar {
7215                b'b' => {
7216                    table.autoload_builtins.remove(fnam);
7217                }
7218                b'c' | b'C' => {
7219                    table.autoload_conditions.remove(fnam);
7220                }
7221                b'p' => {
7222                    table.autoload_params.remove(fnam);
7223                }
7224                b'f' => {
7225                    table.autoload_mathfuncs.remove(fnam);
7226                }
7227                _ => unreachable!(),
7228            }
7229        }
7230
7231        // c:3621-3642 — per-error-code diagnostic.
7232        if subret != 0 && subret != -2 {
7233            ret = 1; // c:3624
7234            match subret {
7235                1 => {
7236                    // c:3627
7237                    crate::ported::utils::zwarnnam(
7238                        cmdnam,
7239                        &format!("failed to add {} `{}'", typnam, fnam),
7240                    );
7241                }
7242                2 => {
7243                    // c:3631
7244                    crate::ported::utils::zwarnnam(
7245                        cmdnam,
7246                        &format!("{}: no such {}", fnam, typnam),
7247                    );
7248                }
7249                3 => {
7250                    // c:3635
7251                    crate::ported::utils::zwarnnam(
7252                        cmdnam,
7253                        &format!("{}: {} is already defined", fnam, typnam),
7254                    );
7255                }
7256                _ => { /* c:3638 no further message */ }
7257            }
7258        }
7259    }
7260    ret
7261}
7262
7263/// Port of `MathFunc mathfuncs;` from `Src/module.c:1258` — the
7264/// global head of the linked list of math functions. Both
7265/// autoloadable math ported (added by modules) and user math ported
7266/// (added by `functions -M`) live here.
7267///
7268/// C is a singly linked list with `mathfunc.next` chaining. The
7269/// Rust port stores entries in a `Vec` — the call sites only ever
7270/// walk linearly and erase by name, so the linked-list shape buys
7271/// nothing in safe Rust.
7272pub static MATHFUNCS: Lazy<Mutex<Vec<mathfunc>>> = // c:1258
7273    Lazy::new(|| Mutex::new(Vec::new()));
7274
7275/// Port of `setconddefs()` from `Src/module.c:754`. — C decl `setconddefs(char const *nam, Conddef c, int size, int *e)`.
7276/// Bulk add/delete of condition definitions:
7277/// the parallel `e[]` array selects per-entry add (`e[i] != 0`) vs delete
7278/// (`e[i] == 0`). Returns 1 if any individual op clashed, 0 if all clean.
7279pub fn setconddefs(
7280    nam: &str, // c:754
7281    c: &mut [conddef],
7282    e: Option<&[i32]>,
7283) -> i32 {
7284    let mut ret = 0; // c:758
7285    for (i, entry) in c.iter_mut().enumerate() {
7286        // c:760 while (size--)
7287        let want_add = e.map(|es| es[i] != 0).unwrap_or(false); // c:761 if (e && *e++) — NULL e deletes
7288        if want_add {
7289            if (entry.flags & CONDF_ADDED) != 0 {
7290                continue;
7291            } // c:763 already added
7292            let dup = conddef {
7293                next: None,
7294                name: entry.name.clone(),
7295                flags: entry.flags,
7296                handler: entry.handler,
7297                min: entry.min,
7298                max: entry.max,
7299                condid: entry.condid,
7300                module: entry.module.clone(),
7301            };
7302            if addconddef(dup) != 0 {
7303                // c:768 addconddef
7304                zwarnnam(
7305                    nam, // c:769 zwarnnam
7306                    &format!("name clash when adding condition `{}'", entry.name),
7307                );
7308                ret = 1;
7309            } else {
7310                entry.flags |= CONDF_ADDED; // c:773
7311            }
7312        } else {
7313            if (entry.flags & CONDF_ADDED) == 0 {
7314                continue;
7315            } // c:776
7316            if deleteconddef(entry) != 0 {
7317                // c:780 deleteconddef
7318                zwarnnam(
7319                    nam, // c:781
7320                    &format!("condition `{}' already deleted", entry.name),
7321                );
7322                ret = 1;
7323            } else {
7324                entry.flags &= !CONDF_ADDED; // c:785
7325            }
7326        }
7327    }
7328    ret // c:790
7329}
7330
7331/// Port of `setmathfuncs()` from `Src/module.c:1374`. — C decl `setmathfuncs(char const *nam, MathFunc f, int size, int *e)`.
7332/// Bulk add/delete of math-function definitions
7333/// via the parallel `e[]` selector array (same shape as setconddefs).
7334pub fn setmathfuncs(
7335    nam: &str, // c:1374
7336    f: &mut [mathfunc],
7337    e: Option<&[i32]>,
7338) -> i32 {
7339    let mut ret = 0; // c:1378
7340    for (i, entry) in f.iter_mut().enumerate() {
7341        // c:1380 while (size--)
7342        // c:1381 — `if (e && *e++)`: e == NULL means REMOVE all
7343        // (same contract as setbuiltins, c:497-503 doc: "e is either
7344        // NULL, in which case all builtins in the table are
7345        // removed"). The previous `.unwrap_or(true)` inverted the
7346        // None case into add-all.
7347        let want_add = e.map(|es| es[i] != 0).unwrap_or(false); // c:1381
7348        if want_add {
7349            if (entry.flags & MFF_ADDED) != 0 {
7350                continue;
7351            } // c:1383
7352            let dup = mathfunc {
7353                next: None,
7354                name: entry.name.clone(),
7355                flags: entry.flags,
7356                nfunc: entry.nfunc,
7357                sfunc: entry.sfunc,
7358                module: entry.module.clone(),
7359                minargs: entry.minargs,
7360                maxargs: entry.maxargs,
7361                funcid: entry.funcid,
7362            };
7363            if addmathfunc(dup) != 0 {
7364                // c:1388 addmathfunc
7365                zwarnnam(
7366                    nam, // c:1389
7367                    &format!("name clash when adding math function `{}'", entry.name),
7368                );
7369                ret = 1;
7370            } else {
7371                entry.flags |= MFF_ADDED; // c:1393
7372                                          // c:1388+1393 — C links `f` ITSELF into mathfuncs and
7373                                          // the flag-set above mutates that same (aliased) node.
7374                                          // The Rust insert is a by-value dup, so mirror the flag
7375                                          // onto the global-list entry; otherwise
7376                                          // del_automathfunc / getfeatureenables reading
7377                                          // MATHFUNCS never observe MFF_ADDED.
7378                if let Ok(mut gtab) = MATHFUNCS.lock() {
7379                    if let Some(p) = gtab.iter_mut().find(|p| p.name == entry.name) {
7380                        p.flags |= MFF_ADDED;
7381                    }
7382                }
7383            }
7384        } else {
7385            if (entry.flags & MFF_ADDED) == 0 {
7386                continue;
7387            } // c:1396
7388            if deletemathfunc(entry) != 0 {
7389                // c:1400 deletemathfunc
7390                zwarnnam(
7391                    nam, // c:1401
7392                    &format!("math function `{}' already deleted", entry.name),
7393                );
7394                ret = 1;
7395            } else {
7396                // c:1356-1357 — C's deletemathfunc clears MFF_ADDED on
7397                // the (aliased) static mftab struct; Rust's global-list
7398                // entry is a dup, so clear it here on the caller's
7399                // record. Without this, a disable→re-enable cycle hits
7400                // the c:1383 `continue` and never re-registers.
7401                entry.flags &= !MFF_ADDED;
7402            }
7403        }
7404    }
7405    ret // c:1407
7406}
7407
7408/// Port of file-static `Conddef condtab;` from `Src/cond.c:21` — the
7409/// global condition-definition linked-list head consulted by `[[ ... ]]`
7410/// dispatch. Modules register custom conditions via `addconddef`; the
7411/// runtime walks `condtab` looking for the matching name+infix flag at
7412/// each `[[` evaluation. Rust port stores entries in a `Vec` (linear
7413/// add/remove + walk; same observable behaviour as C linked list).
7414pub static CONDTAB: Lazy<Mutex<Vec<conddef>>> = // c:cond.c:21
7415    Lazy::new(|| Mutex::new(Vec::new()));
7416
7417/// Port of `getconddef()` from `Src/module.c:648`. — C decl `getconddef(int inf, const char *name, int autol)`.
7418///
7419/// C body c:648-689:
7420/// ```c
7421/// Conddef
7422/// getconddef(int inf, const char *name, int autol)
7423/// {
7424///     Conddef p;
7425///     int f = 1;
7426///     char *lookup, *s;
7427///     lookup = dupstring(name);
7428///     if (!lookup) return NULL;
7429///     for (s = lookup; *s != '\0'; s++) {
7430///         if (*s == Dash) *s = '-';
7431///     }
7432///     do {
7433///         for (p = condtab; p; p = p->next) {
7434///             if ((!!inf == !!(p->flags & CONDF_INFIX)) &&
7435///                 !strcmp(lookup, p->name))
7436///                 break;
7437///         }
7438///         if (autol && p && p->module) {
7439///             if (f) {
7440///                 (void)ensurefeature(p->module,
7441///                                     (p->flags & CONDF_INFIX) ? "C:" : "c:",
7442///                                     (p->flags & CONDF_AUTOALL) ? NULL : lookup);
7443///                 f = 0;
7444///                 p = NULL;
7445///             } else {
7446///                 deleteconddef(p);
7447///                 return NULL;
7448///             }
7449///         } else
7450///             break;
7451///     } while (!p);
7452///     return p;
7453/// }
7454/// ```
7455///
7456/// Returns a clone of the matched `conddef` (or `None` if absent).
7457/// `inf` selects between infix-style (`[[ A op B ]]`) and prefix-style
7458/// (`[[ -X arg ]]`) conditions — `CONDF_INFIX` on the entry must match.
7459/// `autol` triggers `ensurefeature` on autoload-stubs; the autoload
7460/// loop runs at most once (gated by `f`) — if the second iteration
7461/// still finds the entry, the stub is treated as a failed load and
7462/// removed via `deleteconddef`.
7463pub fn getconddef(inf: i32, name: &str, autol: i32, table: &mut modulestab) -> Option<conddef> {
7464    // c:648
7465    // c:655 — `lookup = dupstring(name)` then Dash → '-' substitution.
7466    let lookup: String = crate::ported::string::dupstring(name)
7467        .chars()
7468        .map(|c| {
7469            if c == crate::ported::zsh_h::Dash {
7470                '-'
7471            } else {
7472                c
7473            }
7474        })
7475        .collect();
7476    let mut f = 1; // c:651 `int f = 1;`
7477    loop {
7478        // c:663 do { ... } while (!p);
7479        // c:664-668 — walk condtab matching (!!inf == !!CONDF_INFIX) && name.
7480        let want_infix = inf != 0;
7481        let hit: Option<conddef> = {
7482            let tab = CONDTAB.lock().unwrap();
7483            tab.iter().find_map(|p| {
7484                let p_infix = (p.flags & CONDF_INFIX) != 0;
7485                if p_infix == want_infix && p.name == lookup {
7486                    // Manual field-by-field clone: conddef doesn't
7487                    // derive Clone (function-pointer + Option<Conddef>
7488                    // mix with no PartialEq).
7489                    Some(conddef {
7490                        next: None,
7491                        name: p.name.clone(),
7492                        flags: p.flags,
7493                        handler: p.handler,
7494                        min: p.min,
7495                        max: p.max,
7496                        condid: p.condid,
7497                        module: p.module.clone(),
7498                    })
7499                } else {
7500                    None
7501                }
7502            })
7503        };
7504        // c:669-685 — autoload trigger + failure-retry loop.
7505        let has_autoload_module = hit.as_ref().map(|p| p.module.is_some()).unwrap_or(false);
7506        if autol != 0 && hit.is_some() && has_autoload_module {
7507            let p = hit.as_ref().unwrap();
7508            if f != 0 {
7509                // c:674-678 — first miss: load the module + retry.
7510                let module = p.module.as_ref().unwrap().clone();
7511                let prefix = if (p.flags & CONDF_INFIX) != 0 {
7512                    "C:"
7513                } else {
7514                    "c:"
7515                };
7516                let feature_arg = if (p.flags & CONDF_AUTOALL) != 0 {
7517                    // c:677 — NULL → autoload-all branch.
7518                    None
7519                } else {
7520                    Some(lookup.as_str())
7521                };
7522                let _ = ensurefeature(table, &module, prefix, feature_arg);
7523                f = 0;
7524                continue; // c:680 `p = NULL;` + outer do-while re-tries.
7525            } else {
7526                // c:681-683 — second pass still hit autoload entry →
7527                // load failed; remove the stub and return None.
7528                let _ = deleteconddef(p);
7529                return None;
7530            }
7531        }
7532        return hit; // c:684-685 `else break;` then return p.
7533    }
7534}
7535
7536/// Port of `deleteconddef()` from `Src/module.c:724`. — C decl `deleteconddef(Conddef c)`.
7537/// Removes condition definition `c` from `condtab`. Returns 0 on
7538/// success, -1 on miss. C also frees the autoloaded entry's name +
7539/// module; Rust drop subsumes that.
7540pub fn deleteconddef(c: &conddef) -> i32 {
7541    // c:724
7542    let mut tab = CONDTAB.lock().unwrap();
7543    // c:728 — `for (p = condtab, q = NULL; p && p != c; ...)`. C uses
7544    // pointer identity; the Rust analog is name+infix-flag equality
7545    // (the natural key — `[[ -z STR ]]` and `STR == VAL` share neither).
7546    let infix = c.flags & CONDF_INFIX;
7547    match tab
7548        .iter()
7549        .position(|p| p.name == c.name && (p.flags & CONDF_INFIX) == infix)
7550    {
7551        Some(i) => {
7552            tab.remove(i);
7553            0
7554        } // c:733-738 unlink + free
7555        None => -1, // c:743 not found
7556    }
7557}
7558
7559/// Port of `addconddef()` from `Src/module.c:703`. — C decl `addconddef(Conddef c)`.
7560/// Walks
7561/// CONDTAB for a clash on (name, infix-flag); replaces autoloadable
7562/// entries via deleteconddef; otherwise prepends. Returns 0 on add,
7563/// 1 on clash (existing entry already added).
7564pub fn addconddef(c: conddef) -> i32 {
7565    // c:703
7566    let infix = c.flags & CONDF_INFIX;
7567    let clash_idx = {
7568        let tab = CONDTAB.lock().unwrap();
7569        tab.iter()
7570            .position(|p| p.name == c.name && (p.flags & CONDF_INFIX) == infix) // c:705 getconddef
7571    };
7572    if let Some(i) = clash_idx {
7573        let (autoload, added) = {
7574            let tab = CONDTAB.lock().unwrap();
7575            (tab[i].module.is_some(), (tab[i].flags & CONDF_ADDED) != 0)
7576        };
7577        if !autoload || added {
7578            return 1;
7579        } // c:708 already added
7580        CONDTAB.lock().unwrap().remove(i); // c:711 deleteconddef
7581    }
7582    CONDTAB.lock().unwrap().insert(0, c); // c:713-714 c->next = condtab; condtab = c
7583    0
7584}
7585
7586/// Port of file-static `FuncWrap wrappers;` from `Src/module.c:567`
7587/// — the global wrapper-function linked-list head. Modules register
7588/// wrapper callbacks via `addwrapper(FuncWrap)` and the runtime fires
7589/// them around `runshfunc()`. The Rust port stores entries in a `Vec`
7590/// (linear add/remove + iterate; same observable behaviour).
7591pub static WRAPPERS: Lazy<Mutex<Vec<funcwrap>>> = // c:567
7592    Lazy::new(|| Mutex::new(Vec::new()));
7593
7594// !!! WARNING: RUST-ONLY HELPER !!!
7595//
7596// No C counterpart. C's `runshfunc` (`Src/exec.c:6177`) walks the
7597// `wrappers` linked list with plain pointer chasing — no lock, because
7598// the C shell is single-threaded. zshrs keeps the same list behind
7599// [`WRAPPERS`]'s `Mutex`, and `runshfunc` sits on the shell-function
7600// call path, the hottest path in the shell: taking that mutex on every
7601// call would cost far more than the whole wrapper mechanism.
7602//
7603// So `addwrapper` / `deletewrapper` mirror list MEMBERSHIP into this
7604// relaxed atomic bitmask, one bit per module that can install a
7605// wrapper. `runshfunc` tests a chain node with a single relaxed load
7606// and, when no module has registered, does not touch the mutex at all.
7607// The mask is derived state — [`WRAPPERS`] stays the source of truth.
7608/// Relaxed-atomic membership mirror of [`WRAPPERS`], one bit per
7609/// wrapper-installing module (see [`WRAPPER_BIT_ZPROF`]). Written by
7610/// `addwrapper` / `deletewrapper`, read by `exec::runshfunc`. See the
7611/// WARNING block above.
7612pub static WRAPPERS_ADDED: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
7613
7614/// [`WRAPPERS_ADDED`] bit for `zsh/zprof`'s
7615/// `WRAPDEF(zprof_wrapper)` (`Src/Modules/zprof.c:318-320`).
7616pub const WRAPPER_BIT_ZPROF: u32 = 1 << 0;
7617
7618/// [`WRAPPERS_ADDED`] bit for `zsh/ksh93`'s
7619/// `WRAPDEF(ksh93_wrapper)` (`Src/Modules/ksh93.c:230-232`).
7620pub const WRAPPER_BIT_KSH93: u32 = 1 << 1;
7621
7622/// Port of `addwrapper()` from `Src/module.c:577`. — C decl `addwrapper(Module m, FuncWrap w)`.
7623/// Tail-appends a module's function wrapper onto the
7624/// global [`WRAPPERS`] list. Returns 1 on error, 0 on success.
7625///
7626/// C body (c:578-600):
7627/// ```c
7628/// FuncWrap p, q;
7629/// if (m->node.flags & MOD_ALIAS)
7630///     return 1;
7631/// if (w->flags & WRAPF_ADDED)
7632///     return 1;
7633/// for (p = wrappers, q = NULL; p; q = p, p = p->next);
7634/// if (q)
7635///     q->next = w;
7636/// else
7637///     wrappers = w;
7638/// w->next = NULL;
7639/// w->flags |= WRAPF_ADDED;
7640/// w->module = m;
7641/// return 0;
7642/// ```
7643///
7644/// WARNING: param types don't match C — Rust=(m: &str, w) vs
7645/// C=(Module m, FuncWrap w). Every zshrs module entry point is reached
7646/// by NAME through the `module.rs` dispatcher (`setup_module` /
7647/// `boot_module` / `cleanup_module` all take `name: &str` and hand the
7648/// per-module `*_` fn a null `*const module`), so a `Module` pointer is
7649/// not available at the call site. The name is looked up in
7650/// [`MODULESTAB`] for the c:586 `MOD_ALIAS` test and stored back into
7651/// `w.module` — C keeps the pointer, this port keeps a name-carrying
7652/// [`module`] node, which is what `deletewrapper` matches on.
7653pub fn addwrapper(m: &str, mut w: funcwrap) -> i32 {
7654    // c:576
7655    // c:586-587 — `if (m->node.flags & MOD_ALIAS) return 1;`
7656    // We can't add a wrapper to an alias, since it's supposed to behave
7657    // identically to the resolved module.  This shouldn't happen since
7658    // we usually add wrappers when a real module is loaded.  (c:580-585)
7659    //
7660    // `try_lock`, not `lock`: every caller of this function is a
7661    // module `boot_`, which `bin_zmodload` reaches with the
7662    // [`MODULESTAB`] mutex already held (module.rs:4877 →
7663    // `load_module` → `do_boot_module`). `std::sync::Mutex` is not
7664    // reentrant, so a blocking `lock()` here self-deadlocks. A failed
7665    // `try_lock` means WE are the holder, i.e. the module is mid-load
7666    // and therefore not an alias — exactly the c:580-585 "shouldn't
7667    // happen" case — so the test is skipped.
7668    if let Ok(tab) = MODULESTAB.try_lock() {
7669        if let Some(md) = tab.modules.get(m) {
7670            if (md.node.flags & MOD_ALIAS) != 0 {
7671                return 1; // c:587
7672            }
7673        }
7674    }
7675
7676    // c:589-590 — `if (w->flags & WRAPF_ADDED) return 1;`
7677    if (w.flags & crate::ported::zsh_h::WRAPF_ADDED) != 0 {
7678        return 1; // c:590
7679    }
7680
7681    let mut wrappers = WRAPPERS.lock().unwrap_or_else(|e| e.into_inner());
7682    // c:591-595 — walk to the tail and link on (`q->next = w`), or
7683    // become the head (`wrappers = w`). A `Vec` push IS that walk.
7684    // c:596 — `w->next = NULL;`
7685    w.next = None;
7686    // c:597 — `w->flags |= WRAPF_ADDED;`
7687    w.flags |= crate::ported::zsh_h::WRAPF_ADDED;
7688    // c:598 — `w->module = m;`
7689    w.module = Some(Box::new(module::new(m)));
7690    wrappers.push(w); // c:593/595
7691
7692    // Rust-only — see [`WRAPPERS_ADDED`]. Modules with no bit (the
7693    // statically-linked wrappers that never reach `addwrapper`) map to
7694    // 0, which leaves the mask untouched.
7695    let bit = match m {
7696        "zsh/zprof" => WRAPPER_BIT_ZPROF,
7697        "zsh/ksh93" => WRAPPER_BIT_KSH93,
7698        _ => 0,
7699    };
7700    WRAPPERS_ADDED.fetch_or(bit, std::sync::atomic::Ordering::Relaxed);
7701
7702    0 // c:600
7703}
7704
7705/// Port of `deletewrapper()` from `Src/module.c:609`. — C decl `deletewrapper(Module m, FuncWrap w)`.
7706/// Unlinks a module's wrapper from [`WRAPPERS`].
7707/// Returns 0 when the node was found and removed, 1 otherwise.
7708///
7709/// C body (c:610-628):
7710/// ```c
7711/// FuncWrap p, q;
7712/// if (m->node.flags & MOD_ALIAS)
7713///     return 1;
7714/// if (w->flags & WRAPF_ADDED) {
7715///     for (p = wrappers, q = NULL; p && p != w; q = p, p = p->next);
7716///     if (p) {
7717///         if (q) q->next = p->next; else wrappers = p->next;
7718///         p->flags &= ~WRAPF_ADDED;
7719///         return 0;
7720///     }
7721/// }
7722/// return 1;
7723/// ```
7724///
7725/// WARNING: param types don't match C — Rust=(m: &str) vs C=(Module m,
7726/// FuncWrap w), and the node is identified by owning-module name rather
7727/// than by the `p != w` pointer compare at c:616. C's `w` is always the
7728/// module's own file-static `wrapper[]` array (`Src/Modules/zprof.c:318`,
7729/// `Src/Zle/complete.c:1694`, `Src/Modules/param_private.c:541`), so
7730/// "the node whose `module` is `m`" selects exactly the same entry;
7731/// zshrs has no stable address to compare against because the port
7732/// stores the node by value in a `Vec`.
7733pub fn deletewrapper(m: &str) -> i32 {
7734    // c:608
7735    // c:612-613 — `if (m->node.flags & MOD_ALIAS) return 1;`
7736    // `try_lock` for the same reentrancy reason as `addwrapper`: the
7737    // caller is a module `cleanup_`, reached with [`MODULESTAB`] held.
7738    if let Ok(tab) = MODULESTAB.try_lock() {
7739        if let Some(md) = tab.modules.get(m) {
7740            if (md.node.flags & MOD_ALIAS) != 0 {
7741                return 1; // c:613
7742            }
7743        }
7744    }
7745
7746    let mut wrappers = WRAPPERS.lock().unwrap_or_else(|e| e.into_inner());
7747    // c:615-616 — `if (w->flags & WRAPF_ADDED)` then walk for the node.
7748    let found = wrappers.iter().position(|p| {
7749        (p.flags & crate::ported::zsh_h::WRAPF_ADDED) != 0
7750            && p.module.as_ref().map(|md| md.node.nam.as_str()) == Some(m)
7751    });
7752    if let Some(i) = found {
7753        // c:618-623 — unlink and clear WRAPF_ADDED. Removing from the
7754        // `Vec` both unlinks (c:619-622) and drops the flag with the
7755        // node (c:623).
7756        wrappers.remove(i);
7757        // Rust-only — see [`WRAPPERS_ADDED`].
7758        let bit = match m {
7759            "zsh/zprof" => WRAPPER_BIT_ZPROF,
7760            "zsh/ksh93" => WRAPPER_BIT_KSH93,
7761            _ => 0,
7762        };
7763        WRAPPERS_ADDED.fetch_and(!bit, std::sync::atomic::Ordering::Relaxed);
7764        return 0; // c:625
7765    }
7766    1 // c:628
7767}
7768
7769/// Port of `addmathfunc()` from `Src/module.c:1313`. — C decl `addmathfunc(MathFunc f)`.
7770/// Returns 0 on add, 1 on clash (existing entry not autoloadable).
7771/// Replaces autoloadable entries via `removemathfunc`.
7772pub fn addmathfunc(f: mathfunc) -> i32 {
7773    // c:1313
7774    if (f.flags & MFF_ADDED) != 0 {
7775        return 1;
7776    } // c:1318
7777    let mut tab = MATHFUNCS.lock().unwrap();
7778    let mut found_idx: Option<usize> = None;
7779    for (i, p) in tab.iter().enumerate() {
7780        // c:1321
7781        if p.name == f.name {
7782            // c:1322
7783            if p.module.is_some() && (p.flags & MFF_USERFUNC) == 0 {
7784                // c:1323
7785                found_idx = Some(i); // c:1327 removemathfunc + replace
7786                break;
7787            }
7788            return 1; // c:1330
7789        }
7790    }
7791    if let Some(i) = found_idx {
7792        tab.remove(i);
7793    } // c:1327
7794    tab.insert(0, f); // c:1334-1335 f->next = mathfuncs; mathfuncs = f
7795    0
7796}
7797
7798/// Port of `removemathfunc()` from `Src/module.c:1267`. — C decl `removemathfunc(MathFunc previous, MathFunc current)`.
7799/// Removes the named entry from MATHFUNCS and
7800/// drops it (Rust drop subsumes C's zsfree/zfree ladder).
7801/// WARNING: param names don't match C — Rust=(name) vs C=(previous, current)
7802pub fn removemathfunc(name: &str) {
7803    // c:1267
7804    let mut tab = MATHFUNCS.lock().unwrap();
7805    if let Some(i) = tab.iter().position(|m| m.name == name) {
7806        // c:1270 walk
7807        tab.remove(i); // c:1273-1274 unlink + zfree
7808    }
7809}
7810
7811/// Port of `deletemathfunc()` from `Src/module.c:1342`. — C decl `deletemathfunc(MathFunc f)`.
7812/// Removes f from MATHFUNCS; for unloaded/user-defined entries clears
7813/// the MFF_ADDED flag instead of dropping the node (C: `f->flags &=
7814/// ~MFF_ADDED` when f->module is null).
7815pub fn deletemathfunc(f: &mathfunc) -> i32 {
7816    // c:1342
7817    // c:1348-1352 — the node is unlinked from the global list in BOTH
7818    // arms (`q->next = f->next` / `mathfuncs = f->next`); f->module
7819    // only decides free-vs-keep of the struct itself. The struct for
7820    // module=NULL entries lives in the module's static mftab (the C
7821    // list links the static structs by pointer), so "keep" means the
7822    // mftab record survives with MFF_ADDED cleared — in Rust the
7823    // global-list entry is a by-value dup, so the unlink is a plain
7824    // remove either way and the MFF_ADDED clear on the static record
7825    // happens at the setmathfuncs caller (which holds `&mut` to the
7826    // mftab entry). The previous port left module-less entries IN the
7827    // global list, so a feature-disable never actually deregistered.
7828    let mut tab = MATHFUNCS.lock().unwrap();
7829    match tab.iter().position(|m| m.name == f.name) {
7830        // c:1346
7831        Some(i) => {
7832            tab.remove(i); // c:1349-1352 unlink (+ Rust Drop ≙ c:1355-1357 free)
7833            0 // c:1361
7834        }
7835        None => -1, // c:1363
7836    }
7837}
7838
7839/// Port of `featuresarray()` from `Src/module.c:3279`. — C decl `featuresarray(UNUSED(Module m), Features f)`.
7840/// Construct the feature-name array for a
7841/// module: builtins get `b:NAME`, conditions `c:NAME` or `C:NAME` if
7842/// `CONDF_INFIX`, math funcs `f:NAME`, params `p:NAME`. Trailing
7843/// abstract slots (`n_abstract`) are pre-allocated but left empty so
7844/// the module's own setup can fill them in. C uses zhalloc heap
7845/// allocation — Box goes out of scope here as Rust's `Vec<String>`
7846/// owns the entries (Drop happens automatically). Per-module Rust
7847/// files in `src/ported/modules/*.rs` and `src/ported/builtins/*.rs`
7848/// each carry a `featuresarray` shim that delegates to this
7849/// canonical free fn once the modules table is wired through.
7850/// WARNING: param names don't match C — Rust=(_m, bn, cd, mf, pd, n_abstract) vs C=(m, f)
7851pub fn featuresarray(
7852    // c:3284
7853    _m: *const module,
7854    bn: &[builtin],  // c:3289 f->bn_list
7855    cd: &[conddef],  // c:3290 f->cd_list
7856    mf: &[mathfunc], // c:3291 f->mf_list
7857    pd: &[paramdef], // c:3292 f->pd_list
7858    n_abstract: i32, // c:3288 f->n_abstract
7859) -> Vec<String> {
7860    let features_size = bn.len() + cd.len() + mf.len() + pd.len()            // c:3288
7861        + n_abstract.max(0) as usize;
7862    let mut features: Vec<String> = Vec::with_capacity(features_size + 1); // c:3293
7863    for b in bn {
7864        // c:3296
7865        features.push(format!("b:{}", b.node.nam)); // c:3297
7866    }
7867    for c in cd {
7868        // c:3298
7869        let prefix = if (c.flags & CONDF_INFIX) != 0 {
7870            "C:"
7871        } else {
7872            "c:"
7873        }; // c:3299
7874        features.push(format!("{}{}", prefix, c.name)); // c:3299-3300
7875    }
7876    for m in mf {
7877        // c:3303
7878        features.push(format!("f:{}", m.name)); // c:3304
7879    }
7880    for p in pd {
7881        // c:3305
7882        features.push(format!("p:{}", p.name)); // c:3306
7883    }
7884    // c:3308 — features[features_size] = NULL; Rust analog: trailing
7885    // abstract slots remain unset (Vec is one-shot allocated).
7886    features
7887}
7888
7889/// Port of `getfeatureenables()` from `Src/module.c:3314`. — C decl `getfeatureenables(UNUSED(Module m), Features f)`.
7890/// Returns the per-feature
7891/// enable bitmap for a module: builtins use `BINF_ADDED`, conditions
7892/// `CONDF_ADDED`, math funcs `MFF_ADDED`, params the `pm` non-null
7893/// check. Trailing abstract slots are left at 0 (filled by the
7894/// module's own enables_). C uses zhalloc heap allocation; Rust's
7895/// `Vec<i32>` owns the entries (Drop happens automatically). Per-
7896/// module shims in `src/ported/modules/*.rs` delegate to this
7897/// canonical free fn once the modules table is wired through.
7898/// WARNING: param names don't match C — Rust=(_m, bn, cd, mf, pd, n_abstract) vs C=(m, f)
7899pub fn getfeatureenables(
7900    // c:3319
7901    _m: *const module,
7902    bn: &[builtin],  // c:3324
7903    cd: &[conddef],  // c:3325
7904    mf: &[mathfunc], // c:3326
7905    pd: &[paramdef], // c:3327
7906    n_abstract: i32, // c:3323
7907) -> Vec<i32> {
7908    let features_size = bn.len() + cd.len() + mf.len() + pd.len()            // c:3323
7909        + n_abstract.max(0) as usize;
7910    let mut enables: Vec<i32> = Vec::with_capacity(features_size); // c:3328
7911    for b in bn {
7912        // c:3331
7913        enables.push(if (b.node.flags & BINF_ADDED as i32) != 0 {
7914            1
7915        } else {
7916            0
7917        });
7918    }
7919    for c in cd {
7920        // c:3333
7921        enables.push(if (c.flags & CONDF_ADDED) != 0 { 1 } else { 0 });
7922    }
7923    for m in mf {
7924        // c:3335
7925        enables.push(if (m.flags & MFF_ADDED) != 0 { 1 } else { 0 });
7926    }
7927    for p in pd {
7928        // c:3337
7929        enables.push(if p.pm.is_some() { 1 } else { 0 });
7930    }
7931    for _ in 0..n_abstract.max(0) {
7932        // c:3323 n_abstract slots
7933        enables.push(0);
7934    }
7935    enables // c:3340
7936}
7937
7938/// !!! RUST-ONLY REGISTRY — NO C COUNTERPART !!!
7939///
7940/// The per-feature "is this feature currently ADDED" bit, keyed by module
7941/// name and by the feature string `featuresarray` emits (`b:zstyle`,
7942/// `p:functions`, `c:prefix`, `f:sin`, …).
7943///
7944/// C keeps that bit ON THE DESCRIPTOR the module ships. `getfeatureenables`
7945/// (c:3330-3337) reads `b->node.flags & BINF_ADDED`, `cd->flags &
7946/// CONDF_ADDED`, `mf->flags & MFF_ADDED` and `pd->pm != NULL` straight out of
7947/// the module's `static struct features module_features` tables, and
7948/// `setfeatureenables` (c:3358-3381) writes them back through `setbuiltins` /
7949/// `setconddefs` / `setmathfuncs` / `setparamdefs`. The bit is therefore
7950/// per-module storage that outlives a single `zmodload` call.
7951///
7952/// Most zshrs module ports carry no descriptor table — their `features_()`
7953/// returns a hardcoded name list, so there is no `Builtin`/`Paramdef` struct
7954/// whose flags could hold the bit. This map is that storage. Modules that DO
7955/// ship real `Vec<builtin>` / `Vec<paramdef>` statics (`zsh/datetime`) keep
7956/// using the descriptor path and never appear here.
7957static MODULE_FEATURE_ENABLES: Lazy<Mutex<HashMap<String, std::collections::HashSet<String>>>> =
7958    Lazy::new(|| Mutex::new(HashMap::new()));
7959
7960/// Port of `setfeatureenables()` from `Src/module.c:3354`. — C decl
7961/// `setfeatureenables(Module m, Features f, int *e)`.
7962///
7963/// C body:
7964/// ```c
7965/// if (f->bn_size) { if (setbuiltins(m->node.nam, f->bn_list, f->bn_size, e)) ret = 1;
7966///                   if (e) e += f->bn_size; }
7967/// if (f->cd_size) { if (setconddefs(m->node.nam, f->cd_list, f->cd_size, e)) ret = 1;
7968///                   if (e) e += f->cd_size; }
7969/// if (f->mf_size) { if (setmathfuncs(m->node.nam, f->mf_list, f->mf_size, e)) ret = 1;
7970///                   if (e) e += f->mf_size; }
7971/// if (f->pd_size) { if (setparamdefs(m->node.nam, f->pd_list, f->pd_size, e)) ret = 1; }
7972/// return ret;
7973/// ```
7974///
7975/// WARNING: param names don't match C — Rust=(modname, features, e) vs
7976/// C=(m, f, e). The four per-kind blocks C walks in the fixed order
7977/// `bn`/`cd`/`mf`/`pd` are already flattened into that same order by
7978/// `featuresarray` (c:3288-3305), so this variant walks the ONE name list
7979/// and keys the enable bit off the `b:`/`c:`/`f:`/`p:` prefix instead of the
7980/// block it fell in. It is the entry point for the module ports that have no
7981/// `Features` descriptor tables — see `MODULE_FEATURE_ENABLES`.
7982///
7983/// C's contract "If e is NULL, disable everything" (c:3345) is preserved:
7984/// `None` clears every bit.
7985pub fn setfeatureenables(modname: &str, features: &[String], e: Option<&[i32]>) -> i32 {
7986    // c:3354
7987    let mut ret = 0; // c:3356
7988                 // Collect the p:-feature transitions while the ledger lock is held, and
7989                 // apply them after it is dropped: `mark_module_param_used` re-enters
7990                 // this fn through `ensurefeature` -> `require_module` ->
7991                 // `do_module_features` -> `enables_module`, and `Mutex` is not
7992                 // reentrant.
7993    let mut param_on: Vec<String> = Vec::new();
7994    let mut param_off: Vec<String> = Vec::new();
7995    {
7996        let mut tab = MODULE_FEATURE_ENABLES.lock().unwrap();
7997        let set = tab.entry(modname.to_string()).or_default();
7998        for (n, f) in features.iter().enumerate() {
7999            // c:3345 — `If e is NULL, disable everything.`
8000            let on = e
8001                .map(|a| a.get(n).copied().unwrap_or(0) != 0)
8002                .unwrap_or(false);
8003            if on {
8004                set.insert(f.clone()); // c:515 `b->node.flags |= BINF_ADDED`
8005            } else {
8006                set.remove(f); // c:524 `b->node.flags &= ~BINF_ADDED`
8007            }
8008            // c:3377 `setparamdefs` -> c:1060 `addparamdef` / c:1128
8009            // `deleteparamdef`: the `p:` block installs the real special Param
8010            // over the PM_AUTOLOAD stub, or removes it again. zshrs seeds every
8011            // magic parameter eagerly and models PM_AUTOLOAD as vm_helper's
8012            // MATERIALIZED_MODULE_PARAMS side set, so `pd->pm` is that set's
8013            // membership and this is where it has to move.
8014            if let Some(pname) = f.strip_prefix("p:") {
8015                if on {
8016                    param_on.push(pname.to_string());
8017                } else {
8018                    param_off.push(pname.to_string());
8019                }
8020            }
8021        }
8022    }
8023    // c:3377 setparamdefs (c:1169-1196) for the zmodload-gated specials
8024    // (`errnos`/`sysparams`, `mapfile`, `langinfo`): unlike the
8025    // `zsh/parameter` rows, which zshrs seeds eagerly and toggles through
8026    // the side set above, these enter paramtab only when their module turns
8027    // them on, so `d->pm` is "the live node is the module's special".
8028    let gated = crate::vm_helper::module_gated_params_for(modname);
8029    let live_special = |name: &str| -> Option<Option<Param>> {
8030        let tab = paramtab().read().ok()?;
8031        Some(tab.get(name).cloned())
8032    };
8033    for p in &param_on {
8034        crate::vm_helper::mark_module_param_used(p); // c:1069-1073 createspecialhash/createparam
8035        if gated.contains(&p.as_str()) {
8036            match live_special(p) {
8037                // c:1176-1179 — `if (d->pm) { d++; continue; }`
8038                Some(Some(pm)) if (pm.node.flags as u32 & PM_SPECIAL) != 0 => {}
8039                // c:1180-1183 — addparamdef fails when a non-special of the
8040                // same name is in the way (checkaddparam, c:1062).
8041                Some(Some(_)) => {
8042                    zwarnnam(modname, &format!("error when adding parameter `{}'", p));
8043                    ret = 1;
8044                }
8045                // c:1180 — `addparamdef(d)`.
8046                _ => crate::vm_helper::seed_partab_param(p),
8047            }
8048        }
8049    }
8050    for p in &param_off {
8051        crate::vm_helper::unmark_module_param_used(p); // c:1177 unsetparam_pm
8052        if gated.contains(&p.as_str()) {
8053            // c:1186-1189 — `if (!d->pm) { d++; continue; }`: only the
8054            // module's own special node is deleted.
8055            if let Some(Some(pm)) = live_special(p) {
8056                if (pm.node.flags as u32 & PM_SPECIAL) != 0 {
8057                    let mut d = paramdef {
8058                        name: p.clone(),
8059                        flags: pm.node.flags,
8060                        var: 0,
8061                        gsu: 0,
8062                        getnfn: None,
8063                        scantfn: None,
8064                        pm: Some(pm),
8065                    };
8066                    // c:1190 — `if (deleteparamdef(d))`
8067                    if deleteparamdef(&mut d) != 0 {
8068                        zwarnnam(modname, &format!("parameter `{}' already deleted", p)); // c:1191
8069                        ret = 1; // c:1192
8070                    }
8071                }
8072            }
8073        }
8074    }
8075    ret // c:3382
8076}
8077
8078/// Port of `handlefeatures()` from `Src/module.c:3392`. — C decl
8079/// `handlefeatures(Module m, Features f, int **enables)`.
8080///
8081/// C body:
8082/// ```c
8083/// if (!enables || *enables)
8084///     return setfeatureenables(m, f, enables ? *enables : NULL);
8085/// *enables = getfeatureenables(m, f);
8086/// return 0;
8087/// ```
8088///
8089/// WARNING: param names don't match C — Rust=(modname, features, enables) vs
8090/// C=(m, f, enables). Rust's `&mut Option<Vec<i32>>` is never NULL, so C's
8091/// `*enables` non-NULL is `Some` (SET the given bitmap) and NULL is `None`
8092/// (GET the live one). The name-keyed variant for module ports with no
8093/// `Features` descriptor tables — see `MODULE_FEATURE_ENABLES`.
8094pub fn handlefeatures(
8095    modname: &str,
8096    features: &[String],
8097    enables: &mut Option<Vec<i32>>,
8098) -> i32 {
8099    // c:3392
8100    if let Some(e) = enables.as_ref() {
8101        // c:3394-3395
8102        let e = e.clone();
8103        return setfeatureenables(modname, features, Some(&e));
8104    }
8105    // c:3396 — `*enables = getfeatureenables(m, f);`
8106    let tab = MODULE_FEATURE_ENABLES.lock().unwrap();
8107    let set = tab.get(modname);
8108    *enables = Some(
8109        features
8110            .iter()
8111            .map(|f| match f.strip_prefix("p:") {
8112                // c:3337 — `*enablep++ = (pdp++)->pm ? 1 : 0;`. For a module
8113                // whose `.mdd` declares the parameter autoloadable, `pd->pm`
8114                // is set by `addparamdef` from EITHER the module load or
8115                // `loadparamnode`'s single-feature `ensurefeature` (c:Src/
8116                // params.c:568) — the second is what leaves a plain
8117                // `compinit` with `+p:commands` and every sibling `-`. zshrs
8118                // routes both through `mark_module_param_used`, so the
8119                // materialized set is the authority here, not the ledger.
8120                Some(pname)
8121                    if crate::vm_helper::AUTOLOAD_PARAMS
8122                        .iter()
8123                        .any(|(n, owner)| *n == pname && *owner == modname) =>
8124                {
8125                    i32::from(!crate::vm_helper::module_param_is_autoload_stub(pname))
8126                }
8127                // c:3331-3335 — the BINF_ADDED / CONDF_ADDED / MFF_ADDED
8128                // reads, plus any `p:` row whose module declares no autoload.
8129                _ => i32::from(set.is_some_and(|s| s.contains(f))),
8130            })
8131            .collect(),
8132    );
8133    0 // c:3397
8134}
8135
8136/// Port of `Hookdef hooktab;` from `Src/module.c:843` — the file-static
8137/// linked-list head pointer to the chain of registered `hookdef`
8138/// nodes. Walked by `gethookdef`; mutated by `addhookdef` /
8139/// `deletehookdef`. Each node is a `Box::leak`'d hookdef (so the raw
8140/// pointer has program-lifetime, matching C's static-storage
8141/// `zshhooks[]` and module-side hookdef arrays).
8142pub static hooktab: std::sync::atomic::AtomicPtr<hookdef> = // c:843
8143    std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());
8144
8145/// Port of `mod_export ModuleTable modulestab` from
8146/// `Src/Modules/zmodload.c:32`. The C source keeps the module
8147/// hashtable as a process-global accessed by every module-mgmt
8148/// path (zmodload, addbuiltin, deletebuiltin, etc.). This Rust
8149/// global mirrors that — bin_zmodload_handler reaches for it so
8150/// the canonical `bin_zmodload` can be wired into BUILTINS via
8151/// HandlerFunc without an extra table-arg.
8152pub static MODULESTAB: Lazy<Mutex<modulestab>> = // c:zmodload.c:32
8153    Lazy::new(|| Mutex::new(modulestab::new()));
8154
8155/// !!! RUST-ONLY REGISTRY — NO C COUNTERPART !!!
8156///
8157/// Names a module's `setbuiltins` (`Src/module.c:501`) has turned OFF
8158/// through the feature interface (`zmodload -F MODULE -b:NAME`).
8159///
8160/// C has no such table: `setbuiltins` calls `deletebuiltin` (c:521), which
8161/// physically removes the node from the live `builtintab`, so "is this
8162/// builtin currently visible" is answered by the table itself. zshrs's
8163/// `builtintab` (`createbuiltintable()`) is an immutable, statically-linked
8164/// map that already carries every module builtin, and per-MODULE visibility
8165/// is decided by `module_builtin_available`
8166/// (`src/extensions/ext_builtins.rs`) from the modulestab load flags. That
8167/// is one granularity too coarse for features: `zmodload -F zsh/datetime
8168/// -b:strftime` leaves the module LOADED while removing just `strftime`
8169/// (V04features.ztst "Enabling and disabling of builtins as features"), so
8170/// the per-NAME half needs its own store.
8171///
8172/// Per-NAME, exactly like C's node removal — a module can disable one of
8173/// its builtins and keep the rest.
8174pub static DISABLED_MODULE_BUILTINS: Lazy<Mutex<std::collections::HashSet<String>>> =
8175    Lazy::new(|| Mutex::new(std::collections::HashSet::new()));
8176
8177// C zsh classifies module exports by bare integer `type` index 0..4
8178// (no named constants — just position-in-table) in `features_()`
8179// (`Src/module.c:313+`). Module load state is the `MOD_*` bitmask in
8180// `module.node.flags` (`Src/zsh.h:1516-1532`, mirrored at
8181// `zsh_h.rs:2249-2255`). C does not record which features a module
8182// added on the module struct — feature registration flows into the
8183// canonical per-feature-kind tables (`builtintab`, `condtab`,
8184// `paramtab`, `mathfuncs`, `hooktab`).
8185
8186/// Feature-type index passed to `features_()` (`Src/module.c:313+`).
8187/// C ships bare ints; Rust adds names for readability.
8188pub const FEATURE_TYPE_BUILTIN: i32 = 0;
8189/// `FEATURE_TYPE_CONDITION` constant.
8190pub const FEATURE_TYPE_CONDITION: i32 = 1;
8191/// `FEATURE_TYPE_PARAMETER` constant.
8192pub const FEATURE_TYPE_PARAMETER: i32 = 2;
8193/// `FEATURE_TYPE_MATHFUNC` constant.
8194pub const FEATURE_TYPE_MATHFUNC: i32 = 3;
8195/// `FEATURE_TYPE_HOOK` constant.
8196pub const FEATURE_TYPE_HOOK: i32 = 4;
8197/// Module table (from module.c module hash table)
8198#[derive(Debug, Default)]
8199/// Table of registered modules.
8200/// Port of the `modulestab` HashTable Src/module.c keeps —
8201/// `newmoduletable()` (line 274) creates it, `register_module()`
8202/// (line 359) inserts entries, `printmodulenode()` (line 154)
8203/// renders for `zmodload`.
8204pub struct modulestab {
8205    /// C's `HashTable modulestab` node storage (`Src/module.c:274`
8206    /// `newmoduletable`, created as `newmoduletable(17, "modules")` at
8207    /// `Src/init.c:1193`). The bucket walk is user-visible: the
8208    /// `modules` special parameter (`Src/Modules/parameter.c`) emits
8209    /// `${(k)modules}` straight out of `scanhashtable(modulestab, …)`,
8210    /// so a Rust `HashMap` here produced an order that matched zsh on
8211    /// no line at all.
8212    pub modules: crate::ported::hashtable::hashtable_nodes<module>,
8213    /// C's `LinkList linkedmodules` (`Src/module.c:39`, created at
8214    /// `Src/init.c:1194` `linkedmodules = znewlinklist()`), the list
8215    /// `register_module` appends to (`c:378 zaddlinknode(linkedmodules,
8216    /// m)`) and `module_linked` (`c:385`) searches. It is a SEPARATE
8217    /// store from `modulestab`: a statically-linked module is on this
8218    /// list from boot but gets a `modulestab` node only when something
8219    /// creates one — `autofeatures`/`add_dep` at boot (`c:3449`,
8220    /// `c:2390`) or `load_module`'s allocate-on-miss branch
8221    /// (`c:2229-2237`).
8222    ///
8223    /// zshrs compiles every module in, so this list holds every name
8224    /// `zmodload` can resolve; C only reaches it for `link=static`
8225    /// modules and dlopens the rest. Keeping it separate is what makes
8226    /// `modulestab->hsize` stay at C's boot value of 17 — inserting all
8227    /// ~40 known modules as nodes tripped `ct >= hsize * 2`
8228    /// (`Src/hashtable.c` addhashnode) and quadrupled the table to 68
8229    /// buckets, so every `${(k)modules}` bucket index differed from C.
8230    ///
8231    /// C stores `Linkedmod` records (name + six entry points); zshrs
8232    /// dispatches the entry points by name (`setup_module` etc.), so
8233    /// only the name is kept.
8234    pub linkedmodules: Vec<String>,
8235    /// Builtin name → module name mapping for autoload
8236    pub autoload_builtins: HashMap<String, String>,
8237    /// Condition name → module name mapping for autoload
8238    pub autoload_conditions: HashMap<String, String>,
8239    /// Parameter name → module name mapping for autoload
8240    pub autoload_params: HashMap<String, String>,
8241    /// Math function name → module name mapping for autoload
8242    pub autoload_mathfuncs: HashMap<String, String>,
8243    /// BINF_ADDED ledger — tracks which builtins have been added via
8244    /// `setbuiltins` (C: `b->node.flags & BINF_ADDED`, c:508).
8245    pub added_builtins: HashMap<String, u32>,
8246}
8247
8248// =====================================================================
8249// Builtin / Conddef / MathFunc / Paramdef descriptors and the
8250// `struct features` aggregator from `Src/zsh.h:1440-1571` and
8251// `Src/module.c:3279+`.
8252//
8253// In zsh C these are linked into modules via `dlsym()`; in zshrs
8254// modules are compiled in (no dlopen), so each module ships a
8255// `static` `Features` describing its `bintab[]` / etc. that the
8256// `features_` / `enables_` / `cleanup_` entry points hand to the
8257// helpers below.
8258// =====================================================================
8259
8260// `BINF_ADDED` / `CONDF_INFIX` / `CONDF_ADDED` / `MFF_ADDED` are
8261// re-exported from zsh_h.rs (single source of truth, i32 matching
8262// C `int`).
8263
8264// ===========================================================
8265// Methods moved verbatim from src/ported/vm_helper because their
8266// C counterpart's source file maps 1:1 to this Rust module.
8267// Rust permits multiple inherent impl blocks for the same
8268// type within a crate, so call sites in vm_helper are unchanged.
8269// ===========================================================
8270
8271// BEGIN moved-from-exec-rs
8272// (impl ShellExecutor block moved to src/exec_shims.rs — see file marker)
8273
8274// END moved-from-exec-rs
8275
8276// ===========================================================
8277// Direct ports of module-loader / dlsym / feature-array /
8278// math-func registration entries from Src/module.c. The Rust
8279// rewrite uses statically-linked module impls (each module
8280// compiled into the binary, registered through a static
8281// dispatch table — see `crate::ported::modules::mod`), so the
8282// dynamic-loader plumbing collapses to no-ops. These free-fn
8283// entries satisfy ABI/name parity for the drift gate.
8284// ===========================================================
8285
8286/// `FEAT_IGNORE` — bit in the `flags` arg to add_/del_-automathfunc
8287/// and friends. Port of `enum { FEAT_IGNORE = 0x0001 }` from
8288/// `Src/module.c:62`. /* `-i` option: ignore redefinition errors. */
8289pub const FEAT_IGNORE: i32 = 0x0001; // c:62
8290
8291/// `FEAT_INFIX` — bit indicating a condition is infix-style. Port of
8292/// `enum { FEAT_INFIX = 0x0002 }` from `Src/module.c:64`.
8293pub const FEAT_INFIX: i32 = 0x0002; // c:64
8294
8295/// `FEAT_AUTOALL` — `zmodload -a` enable-all-features. Port of
8296/// `enum { FEAT_AUTOALL = 0x0004 }` from `Src/module.c:69`.
8297pub const FEAT_AUTOALL: i32 = 0x0004; // c:69
8298
8299/// `FEAT_REMOVE` — bit indicating feature removal pass. Port of
8300/// `enum { FEAT_REMOVE = 0x0008 }` from `Src/module.c:76`.
8301pub const FEAT_REMOVE: i32 = 0x0008; // c:76
8302
8303/// `FEAT_CHECKAUTO` — verify autoloads are actually provided. Port of
8304/// `enum { FEAT_CHECKAUTO = 0x0010 }` from `Src/module.c:81`.
8305pub const FEAT_CHECKAUTO: i32 = 0x0010; // c:81
8306
8307/// !!! WARNING: RUST-ONLY FLAG BIT !!!
8308/// C has no such bit. It carries the `zmodload -m` decision on the
8309/// `Patprog pat` field of each `struct feature_enables` entry
8310/// (`Src/module.c:3253-3262` fills `fep->pat = patprogs ? *patprogs++ :
8311/// NULL`, and `do_module_features` at c:2093 branches
8312/// `fep->pat ? pattry(fep->pat, *fp) : !strcmp(*fp, esp)`). zshrs's
8313/// feature list is a plain `&[String]` with no per-entry payload, so
8314/// there is nowhere to hang the compiled pattern. `-m` sets the pat for
8315/// EVERY entry of one invocation uniformly, so the decision is carried
8316/// as this one flag bit alongside `FEAT_IGNORE`/`FEAT_CHECKAUTO` and the
8317/// pattern is compiled at the match site. Deliberately outside C's
8318/// 0x0001-0x0010 range so it cannot collide with a future upstream bit.
8319pub const FEAT_PATTERN_ARGS: i32 = 0x1000;
8320
8321/// `FINDMOD_ALIASP` — bit in `find_module()`'s `flags` arg.
8322/// Port of `enum { FINDMOD_ALIASP = 0x0001 }` from `Src/module.c:110`.
8323/// /* Resolve any aliases to the underlying module. */
8324pub const FINDMOD_ALIASP: i32 = 0x0001; // c:110
8325
8326/// `FINDMOD_CREATE` — bit in `find_module()`'s `flags` arg.
8327/// Port of `enum { FINDMOD_CREATE = 0x0002 }` from `Src/module.c:115`.
8328/// /* Create an element for the module in the list if not found. */
8329pub const FINDMOD_CREATE: i32 = 0x0002; // c:115
8330
8331#[cfg(test)]
8332mod tests {
8333    use super::*;
8334    use crate::ported::zsh_h::hashnode;
8335
8336    #[test]
8337    fn test_module_table_new() {
8338        let _g = crate::test_util::global_state_lock();
8339        let table = modulestab::new();
8340        // zsh/complete is in the default-loaded set (module.rs:1128).
8341        // zsh/datetime and zsh/system are REGISTERED but carry
8342        // MOD_UNLOAD at init — they require explicit `zmodload NAME`
8343        // to become is_loaded(). Bugs #530/#532/#535.
8344        assert!(table.is_loaded("zsh/complete"));
8345        assert!(!table.is_loaded("zsh/datetime"));
8346        assert!(!table.is_loaded("zsh/system"));
8347        assert!(!table.is_loaded("nonexistent"));
8348    }
8349
8350    /// c:2812-2913 — `unload_module` clears the bound state (finish hook
8351    /// runs, `u.linked` goes NULL, MOD_INIT_B/MOD_INIT_S drop) but only
8352    /// *deletes* the record when the module has no deps (c:2910
8353    /// `else if (!m->deps) delete_module(m)`). `zsh/complete` declares
8354    /// `zsh/zle` as a dep (Src/Zle/complete.mdd, module.rs:1513), so its
8355    /// record survives the unload with MOD_LINKED still set — which is
8356    /// what `is_loaded()` reports. `is_bound()` is the predicate that
8357    /// tracks C's "actually loaded" test (`m->u.handle && !MOD_UNLOAD`,
8358    /// c:2637), so that is what flips here.
8359    #[test]
8360    fn test_load_unload() {
8361        let _g = crate::test_util::global_state_lock();
8362        let mut table = modulestab::new();
8363        assert!(table.is_loaded("zsh/complete"));
8364        table.load_module("zsh/complete", None, false);
8365        assert!(table.is_bound("zsh/complete"));
8366
8367        table.unload_module("zsh/complete");
8368        assert!(!table.is_bound("zsh/complete"));
8369        assert!(
8370            table.is_loaded("zsh/complete"),
8371            "record must survive: zsh/complete has deps (c:2910)"
8372        );
8373
8374        table.load_module("zsh/complete", None, false);
8375        assert!(table.is_loaded("zsh/complete"));
8376        assert!(table.is_bound("zsh/complete"));
8377    }
8378
8379    #[test]
8380    fn test_list_loaded() {
8381        let _g = crate::test_util::global_state_lock();
8382        let table = modulestab::new();
8383        let loaded = table.list_loaded();
8384        // C zsh ships exactly 14 default-loaded modules (verified
8385        // against Homebrew zsh 5.9.1; full list at module.rs:1126).
8386        // The other ~17 registered modules carry MOD_UNLOAD until
8387        // `zmodload NAME` clears it (see module.rs:1148-1152).
8388        // Default-loaded count intersects (a) the registered set in
8389        // `builtin_modules` (module.rs:1034-1105, ~30 entries) with
8390        // (b) the `zsh_default_loaded` whitelist (module.rs:1126,
8391        // 14 entries). Items in (b) but NOT (a) — `zsh/compctl`,
8392        // `zsh/main`, `zsh/rlimits`, `zsh/zle` — currently land in the
8393        // autoload registry only, so the actual count is 14 - 4 = 10
8394        // (a couple of overlaps land it at ~11 in practice).
8395        assert!(
8396            loaded.len() >= 10,
8397            "expected >= 10 default-loaded modules, got {}",
8398            loaded.len()
8399        );
8400        assert!(loaded.contains(&"zsh/complete"));
8401    }
8402
8403    #[test]
8404    fn test_autoload() {
8405        let _g = crate::test_util::global_state_lock();
8406        let mut table = modulestab::new();
8407        table.add_autobin("my_cmd", "zsh/mymodule", 0);
8408        assert_eq!(
8409            table.resolve_autoload_builtin("my_cmd"),
8410            Some("zsh/mymodule")
8411        );
8412        assert_eq!(table.resolve_autoload_builtin("nonexistent"), None);
8413    }
8414
8415    #[test]
8416    fn test_features() {
8417        let _g = crate::test_util::global_state_lock();
8418        // The per-module feature ledger has been deleted (canonical
8419        // C-tables track features in `BUILTINTAB`/`CONDTAB`/`PARAMTAB`).
8420        // `list_features` now returns an empty vec — `module_linked`
8421        // is the right test for "is this module registered".
8422        let table = modulestab::new();
8423        let features = table.list_features("zsh/complete");
8424        assert!(features.is_empty());
8425        assert!(table.module_linked("zsh/complete"));
8426    }
8427
8428    #[test]
8429    fn test_module_linked() {
8430        let _g = crate::test_util::global_state_lock();
8431        let table = modulestab::new();
8432        assert!(table.module_linked("zsh/complete"));
8433        assert!(table.module_linked("zsh/stat"));
8434        assert!(!table.module_linked("zsh/nonexistent"));
8435    }
8436
8437    #[test]
8438    fn test_printmodulenode() {
8439        let _g = crate::test_util::global_state_lock();
8440        // C-source-true print gate: `m->u.handle || (flags &
8441        // PRINTMOD_AUTO)` at Src/module.c:218 — only fires once
8442        // `load_module` has wired up the union slot AND set
8443        // MOD_INIT_B (c:2244). Fresh `module::new` returns a
8444        // registered-but-not-loaded entry, so the gate misses.
8445        // Mark MOD_INIT_B to simulate the post-boot state.
8446        let mut module = module::new("zsh/test");
8447        module.node.flags |= MOD_INIT_B;
8448        // Loaded module, no flags → emit just the module name
8449        // (c:240 nicezputs(modname)).
8450        let output = printmodulenode("zsh/test", &module, 0);
8451        assert_eq!(output, "zsh/test");
8452        // Under PRINTMOD_LIST the loaded branch emits `zmodload MOD`.
8453        let listed = printmodulenode("zsh/test", &module, PRINTMOD_LIST);
8454        assert_eq!(listed, "zmodload zsh/test");
8455        // Registered-but-not-loaded: no MOD_INIT_B → empty output
8456        // (matches C's `m->u.handle` being NULL).
8457        let unloaded = module::new("zsh/unloaded");
8458        let nope = printmodulenode("zsh/unloaded", &unloaded, 0);
8459        assert_eq!(nope, "");
8460    }
8461
8462    // ===== Tests for the `addmathfunc` / `removemathfunc` /
8463    // `deletemathfunc` family ported in this session against the
8464    // MATHFUNCS Lazy<Mutex<Vec<mathfunc>>> global. Each test isolates
8465    // its names with a unique prefix so they don't collide if the
8466    // suite runs in parallel.
8467
8468    fn mk_mf(name: &str, autoload: bool) -> mathfunc {
8469        mathfunc {
8470            next: None,
8471            name: name.to_string(),
8472            flags: 0,
8473            nfunc: None,
8474            sfunc: None,
8475            module: if autoload {
8476                Some("zsh/test".to_string())
8477            } else {
8478                None
8479            },
8480            minargs: 0,
8481            maxargs: 0,
8482            funcid: 0,
8483        }
8484    }
8485
8486    #[test]
8487    fn addmathfunc_clash_returns_one_when_already_added() {
8488        let _g = crate::test_util::global_state_lock();
8489        // C addmathfunc returns 1 when an existing entry has the same
8490        // name AND is non-autoloadable (no module set). Verifies the
8491        // "already in table" branch at module.c:1322-1330.
8492        let f1 = mk_mf("zshrs_test_clash_a", false);
8493        let f2 = mk_mf("zshrs_test_clash_a", false);
8494        assert_eq!(addmathfunc(f1), 0);
8495        assert_eq!(addmathfunc(f2), 1, "second add should clash");
8496        removemathfunc("zshrs_test_clash_a");
8497    }
8498
8499    #[test]
8500    fn addmathfunc_autoload_replace_succeeds() {
8501        let _g = crate::test_util::global_state_lock();
8502        // When existing entry IS autoloadable (module.is_some, no
8503        // MFF_USERFUNC), C removes-then-replaces. The new entry should
8504        // land at index 0 (prepend) per c:1334-1335.
8505        let auto = mk_mf("zshrs_test_replace", true);
8506        let real = mk_mf("zshrs_test_replace", false);
8507        assert_eq!(addmathfunc(auto), 0);
8508        assert_eq!(
8509            addmathfunc(real),
8510            0,
8511            "autoloadable entry must be replaceable"
8512        );
8513        let tab = MATHFUNCS.lock().unwrap();
8514        let entry = tab.iter().find(|m| m.name == "zshrs_test_replace").unwrap();
8515        assert!(
8516            entry.module.is_none(),
8517            "after replace, module should be None"
8518        );
8519        drop(tab);
8520        removemathfunc("zshrs_test_replace");
8521    }
8522
8523    #[test]
8524    fn removemathfunc_returns_unit_and_drops() {
8525        let _g = crate::test_util::global_state_lock();
8526        let f = mk_mf("zshrs_test_remove", false);
8527        assert_eq!(addmathfunc(f), 0);
8528        assert!(MATHFUNCS
8529            .lock()
8530            .unwrap()
8531            .iter()
8532            .any(|m| m.name == "zshrs_test_remove"));
8533        removemathfunc("zshrs_test_remove");
8534        assert!(!MATHFUNCS
8535            .lock()
8536            .unwrap()
8537            .iter()
8538            .any(|m| m.name == "zshrs_test_remove"));
8539    }
8540
8541    #[test]
8542    fn deletemathfunc_returns_minus_one_on_miss() {
8543        let _g = crate::test_util::global_state_lock();
8544        // C: returns -1 when no matching entry; verifies the c:1361 branch.
8545        let probe = mk_mf("zshrs_test_never_added_xyz", false);
8546        assert_eq!(deletemathfunc(&probe), -1);
8547    }
8548
8549    #[test]
8550    fn deletemathfunc_clears_added_flag_for_userfunc() {
8551        let _g = crate::test_util::global_state_lock();
8552        // For non-module entries (`!f->module`), C clears the MFF_ADDED
8553        // flag instead of dropping the node (c:1357). Tests by adding
8554        // a user-defined mathfunc, flipping MFF_ADDED on, then deleting.
8555        let mut f = mk_mf("zshrs_test_clear_flag", false);
8556        f.flags = MFF_ADDED;
8557        assert_eq!(
8558            addmathfunc(f),
8559            1,
8560            "MFF_ADDED set → addmathfunc clashes at c:1318"
8561        );
8562        // Now seed it manually with module=None and MFF_ADDED so deletemathfunc
8563        // exercises the clear-flag branch.
8564        MATHFUNCS
8565            .lock()
8566            .unwrap()
8567            .insert(0, mk_mf("zshrs_test_clear_flag2", false));
8568        let mut f2 = mk_mf("zshrs_test_clear_flag2", false);
8569        f2.flags = MFF_ADDED;
8570        // f2 is the lookup probe; by name it matches the seeded entry.
8571        assert_eq!(deletemathfunc(&f2), 0);
8572        let tab = MATHFUNCS.lock().unwrap();
8573        let entry = tab.iter().find(|m| m.name == "zshrs_test_clear_flag2");
8574        // Entry stays in the table (module was None) but MFF_ADDED cleared.
8575        if let Some(e) = entry {
8576            assert_eq!(e.flags & MFF_ADDED, 0);
8577        }
8578        drop(tab);
8579        removemathfunc("zshrs_test_clear_flag2");
8580    }
8581
8582    // ===== Tests for `addconddef` / `deleteconddef` against CONDTAB.
8583
8584    fn mk_cd(name: &str, infix: bool, autoload: bool) -> conddef {
8585        conddef {
8586            next: None,
8587            name: name.to_string(),
8588            flags: if infix { CONDF_INFIX } else { 0 },
8589            handler: None,
8590            min: 0,
8591            max: 0,
8592            condid: 0,
8593            module: if autoload {
8594                Some("zsh/cond".to_string())
8595            } else {
8596                None
8597            },
8598        }
8599    }
8600
8601    #[test]
8602    fn addconddef_clash_returns_one() {
8603        let _g = crate::test_util::global_state_lock();
8604        // C addconddef: clash when existing has same name+infix AND is
8605        // not autoloadable, OR is already added (CONDF_ADDED flag).
8606        let c1 = mk_cd("zshrs_test_cond_clash", false, false);
8607        let c2 = mk_cd("zshrs_test_cond_clash", false, false);
8608        assert_eq!(addconddef(c1), 0);
8609        assert_eq!(addconddef(c2), 1);
8610        let probe = mk_cd("zshrs_test_cond_clash", false, false);
8611        assert_eq!(deleteconddef(&probe), 0);
8612    }
8613
8614    #[test]
8615    fn deleteconddef_returns_minus_one_on_miss() {
8616        let _g = crate::test_util::global_state_lock();
8617        let probe = mk_cd("zshrs_test_cond_never_added", false, false);
8618        assert_eq!(deleteconddef(&probe), -1);
8619    }
8620
8621    #[test]
8622    fn addconddef_distinguishes_infix_from_prefix() {
8623        let _g = crate::test_util::global_state_lock();
8624        // CONDF_INFIX is part of the clash key — a prefix-form `-z` and
8625        // an infix-form `==` share neither name nor flag, so adding both
8626        // names with different infix bits should both succeed.
8627        let prefix = mk_cd("zshrs_test_cond_dual", false, false);
8628        let infix = mk_cd("zshrs_test_cond_dual", true, false);
8629        assert_eq!(addconddef(prefix), 0);
8630        assert_eq!(
8631            addconddef(infix),
8632            0,
8633            "infix variant must not clash with prefix variant"
8634        );
8635        // Cleanup both
8636        let _ = deleteconddef(&mk_cd("zshrs_test_cond_dual", false, false));
8637        let _ = deleteconddef(&mk_cd("zshrs_test_cond_dual", true, false));
8638    }
8639
8640    // ===== Tests for `setconddefs` / `setmathfuncs` bulk dispatch.
8641
8642    #[test]
8643    fn setconddefs_bulk_add_then_bulk_delete_via_e_array() {
8644        let _g = crate::test_util::global_state_lock();
8645        // C setconddefs: walks (c, e) pairs; e[i]!=0 → addconddef path,
8646        // e[i]==0 → deleteconddef path. Tests the round trip.
8647        let mut entries = vec![
8648            mk_cd("zshrs_test_bulk_a", false, false),
8649            mk_cd("zshrs_test_bulk_b", false, false),
8650        ];
8651        let add_selectors = [1, 1];
8652        assert_eq!(setconddefs("test", &mut entries, Some(&add_selectors)), 0);
8653        // Both should now have CONDF_ADDED set per c:773.
8654        assert_ne!(entries[0].flags & CONDF_ADDED, 0);
8655        assert_ne!(entries[1].flags & CONDF_ADDED, 0);
8656        // Now delete both via e=[0,0].
8657        let del_selectors = [0, 0];
8658        assert_eq!(setconddefs("test", &mut entries, Some(&del_selectors)), 0);
8659        assert_eq!(entries[0].flags & CONDF_ADDED, 0);
8660        assert_eq!(entries[1].flags & CONDF_ADDED, 0);
8661    }
8662
8663    // ===== Tests for `addbuiltin` / `addbuiltins` against canonical builtintab.
8664
8665    fn mk_b(nam: &str) -> builtin {
8666        builtin {
8667            node: hashnode {
8668                next: None,
8669                nam: nam.to_string(),
8670                flags: 0,
8671            },
8672            handlerfunc: None,
8673            minargs: 0,
8674            maxargs: 0,
8675            funcid: 0,
8676            optstr: None,
8677            defopts: None,
8678        }
8679    }
8680
8681    #[test]
8682    fn addbuiltin_clash_against_existing_builtintab_entry() {
8683        let _g = crate::test_util::global_state_lock();
8684        // C addbuiltin: returns 1 when builtintab already has an entry
8685        // for the same name with BINF_ADDED set. The canonical Rust
8686        // builtintab is populated at startup via createbuiltintable;
8687        // probing a real builtin like "echo" should clash if BINF_ADDED.
8688        let _ = createbuiltintable();
8689        let mut b = mk_b("echo");
8690        let r = addbuiltin(&mut b);
8691        // BINF_ADDED gets set on b when no clash. If echo was BINF_ADDED in
8692        // the static table, r==1; otherwise r==0 and b.flags now has BINF_ADDED.
8693        assert!(r == 0 || r == 1);
8694        if r == 0 {
8695            assert_ne!(b.node.flags & BINF_ADDED as i32, 0);
8696        }
8697    }
8698
8699    #[test]
8700    fn addbuiltins_skips_already_added_entries() {
8701        let _g = crate::test_util::global_state_lock();
8702        // C addbuiltins (c:553): `if (b->node.flags & BINF_ADDED) continue`.
8703        // Pre-marking BINF_ADDED should skip both entries; ret stays 0.
8704        let mut b1 = mk_b("zshrs_test_already_added_1");
8705        b1.node.flags = BINF_ADDED as i32;
8706        let mut b2 = mk_b("zshrs_test_already_added_2");
8707        b2.node.flags = BINF_ADDED as i32;
8708        let mut binl = vec![b1, b2];
8709        assert_eq!(addbuiltins("test", &mut binl), 0);
8710    }
8711
8712    // ===== Tests for `addwrapper` / `deletewrapper` against WRAPPERS.
8713
8714    fn mk_w() -> funcwrap {
8715        funcwrap {
8716            next: None,
8717            flags: 0,
8718            handler: Some(|_prog, _w, _name| 0),
8719            module: None,
8720        }
8721    }
8722
8723    #[test]
8724    fn addwrapper_then_deletewrapper_round_trip() {
8725        let _g = crate::test_util::global_state_lock();
8726        // Register a non-alias module in the live table so the c:586
8727        // MOD_ALIAS gate is exercised and clears.
8728        MODULESTAB
8729            .lock()
8730            .unwrap()
8731            .modules
8732            .insert("zsh/test".to_string(), module::new("zsh/test"));
8733        let w = mk_w();
8734        assert_eq!(addwrapper("zsh/test", w), 0);
8735        // c:597-598 — the node lands in `wrappers` carrying WRAPF_ADDED
8736        // and a back-link to its module, which is what `deletewrapper`
8737        // matches on.
8738        assert!(WRAPPERS.lock().unwrap().iter().any(|p| {
8739            p.flags & crate::ported::zsh_h::WRAPF_ADDED != 0
8740                && p.module.as_ref().map(|m| m.node.nam.as_str()) == Some("zsh/test")
8741        }));
8742        // c:618-625 — found: unlink and return 0.
8743        assert_eq!(deletewrapper("zsh/test"), 0);
8744        // c:628 — second removal misses.
8745        assert_eq!(deletewrapper("zsh/test"), 1);
8746        MODULESTAB.lock().unwrap().modules.remove("zsh/test");
8747    }
8748
8749    /// c:586-587 — `addwrapper` refuses an alias module, so nothing
8750    /// lands in `wrappers` and `deletewrapper` reports the same refusal.
8751    #[test]
8752    fn addwrapper_refuses_alias_module() {
8753        let _g = crate::test_util::global_state_lock();
8754        {
8755            let mut tab = MODULESTAB.lock().unwrap();
8756            let mut m = module::new("zsh/testalias");
8757            m.node.flags |= MOD_ALIAS;
8758            tab.modules.insert("zsh/testalias".to_string(), m);
8759        }
8760        assert_eq!(addwrapper("zsh/testalias", mk_w()), 1); // c:587
8761        assert_eq!(deletewrapper("zsh/testalias"), 1); // c:613
8762        MODULESTAB.lock().unwrap().modules.remove("zsh/testalias");
8763    }
8764
8765    /// c:589-590 — a node that already carries `WRAPF_ADDED` is
8766    /// refused rather than double-linked.
8767    #[test]
8768    fn addwrapper_refuses_already_added_node() {
8769        let _g = crate::test_util::global_state_lock();
8770        let mut w = mk_w();
8771        w.flags |= crate::ported::zsh_h::WRAPF_ADDED;
8772        assert_eq!(addwrapper("zsh/test", w), 1); // c:590
8773    }
8774
8775    #[test]
8776    fn deletewrapper_returns_one_when_not_found() {
8777        let _g = crate::test_util::global_state_lock();
8778        // Empty WRAPPERS means any probe misses. Take a snapshot of the
8779        // current state, drain WRAPPERS, run the test, restore.
8780        let snapshot: Vec<_> = WRAPPERS.lock().unwrap().drain(..).collect();
8781        assert_eq!(deletewrapper("zsh/test"), 1);
8782        WRAPPERS.lock().unwrap().extend(snapshot);
8783    }
8784
8785    // C-faithful hookdef tests. The system under test is:
8786    //   - `hooktab` (file-static linked-list head, port of c:843)
8787    //   - `gethookdef` / `addhookdef` / `addhookdefs` / `deletehookdef` /
8788    //     `deletehookdefs` / `addhookdeffunc` / `addhookfunc` /
8789    //     `deletehookdeffunc` / `deletehookfunc` / `runhookdef` —
8790    //     C-identical signatures over real `Hookfn` fn pointers.
8791    //
8792    // Each test holds `global_state_lock` and snapshots the chain at
8793    // start so any incidental registrations from other state leak don't
8794    // affect outcomes.
8795
8796    // Real Rust ported matching the `Hookfn` shape. Used as test handlers
8797    // that bump a per-test atomic counter when invoked, proving
8798    // `runhookdef` actually dispatches the registered fn pointers.
8799    static H1_CALLS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
8800    static H2_CALLS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
8801    static H1_RETVAL: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
8802
8803    fn h1(_h: *mut hookdef, _d: *mut std::ffi::c_void) -> i32 {
8804        H1_CALLS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
8805        H1_RETVAL.load(std::sync::atomic::Ordering::SeqCst)
8806    }
8807    fn h2(_h: *mut hookdef, _d: *mut std::ffi::c_void) -> i32 {
8808        H2_CALLS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
8809        0
8810    }
8811
8812    // Allocate a fresh hookdef on the heap and leak its Box so that the
8813    // returned `*mut hookdef` has program-lifetime — matching C's
8814    // static-storage hookdef nodes (zshhooks[] etc.). The test then
8815    // takes responsibility for splicing it out via deletehookdef.
8816    fn leak_hookdef(name: &str, flags: i32) -> *mut hookdef {
8817        Box::into_raw(Box::new(hookdef {
8818            next: std::ptr::null_mut(),
8819            name: name.to_string(),
8820            def: None,
8821            flags,
8822            funcs: std::ptr::null_mut(),
8823        }))
8824    }
8825
8826    /// `gethookdef` returns null when no hookdef with that name is in
8827    /// `hooktab`, and the registered pointer once `addhookdef` runs.
8828    #[test]
8829    fn gethookdef_returns_null_then_registered_ptr() {
8830        let _g = crate::test_util::global_state_lock();
8831        // Unique name to avoid colliding with other tests' registrations.
8832        let h = leak_hookdef("zshrs_test_get_hook", HOOKF_ALL);
8833        unsafe {
8834            assert!(gethookdef(&(*h).name).is_null());
8835        }
8836        assert_eq!(addhookdef(h), 0);
8837        unsafe {
8838            assert_eq!(gethookdef(&(*h).name), h);
8839        }
8840        // Cleanup so the chain doesn't leak into other tests.
8841        assert_eq!(deletehookdef(h), 0);
8842        unsafe {
8843            drop(Box::from_raw(h));
8844        }
8845    }
8846
8847    /// `addhookdef` rejects a name already present in `hooktab`
8848    /// (port of c:866-867: `if (gethookdef(h->name)) return 1`).
8849    #[test]
8850    fn addhookdef_rejects_duplicate_name() {
8851        let _g = crate::test_util::global_state_lock();
8852        let h1 = leak_hookdef("zshrs_test_dup_hook", HOOKF_ALL);
8853        let h2 = leak_hookdef("zshrs_test_dup_hook", HOOKF_ALL);
8854        assert_eq!(addhookdef(h1), 0);
8855        assert_eq!(addhookdef(h2), 1, "duplicate name must return 1");
8856        // Cleanup.
8857        assert_eq!(deletehookdef(h1), 0);
8858        unsafe {
8859            drop(Box::from_raw(h1));
8860            drop(Box::from_raw(h2));
8861        }
8862    }
8863
8864    /// `deletehookdef` returns 1 when the hookdef isn't in the chain
8865    /// (port of c:909-910: `if (!p) return 1`) and 0 after add.
8866    #[test]
8867    fn deletehookdef_returns_one_on_miss_zero_on_hit() {
8868        let _g = crate::test_util::global_state_lock();
8869        let h = leak_hookdef("zshrs_test_del_hook", HOOKF_ALL);
8870        assert_eq!(deletehookdef(h), 1, "not in chain → 1");
8871        assert_eq!(addhookdef(h), 0);
8872        assert_eq!(deletehookdef(h), 0, "spliced out → 0");
8873        assert_eq!(deletehookdef(h), 1, "second delete misses → 1");
8874        unsafe {
8875            drop(Box::from_raw(h));
8876        }
8877    }
8878
8879    /// `runhookdef` dispatches every registered Hookfn under `HOOKF_ALL`
8880    /// (port of c:996-1004) — the test handlers' counters bump on every
8881    /// fire, and the first non-zero return short-circuits.
8882    #[test]
8883    fn runhookdef_dispatches_all_under_hookf_all_short_circuits_on_nonzero() {
8884        let _g = crate::test_util::global_state_lock();
8885        H1_CALLS.store(0, std::sync::atomic::Ordering::SeqCst);
8886        H2_CALLS.store(0, std::sync::atomic::Ordering::SeqCst);
8887        H1_RETVAL.store(0, std::sync::atomic::Ordering::SeqCst);
8888
8889        let h = leak_hookdef("zshrs_test_runall", HOOKF_ALL);
8890        assert_eq!(addhookdef(h), 0);
8891        assert_eq!(addhookdeffunc(h, h1), 0);
8892        assert_eq!(addhookdeffunc(h, h2), 0);
8893
8894        // Both fire → r1=0, r2=0 → final return 0.
8895        assert_eq!(runhookdef(h, std::ptr::null_mut()), 0);
8896        assert_eq!(H1_CALLS.load(std::sync::atomic::Ordering::SeqCst), 1);
8897        assert_eq!(H2_CALLS.load(std::sync::atomic::Ordering::SeqCst), 1);
8898
8899        // Make h1 return 7 → runhookdef should return 7 without calling h2.
8900        H1_CALLS.store(0, std::sync::atomic::Ordering::SeqCst);
8901        H2_CALLS.store(0, std::sync::atomic::Ordering::SeqCst);
8902        H1_RETVAL.store(7, std::sync::atomic::Ordering::SeqCst);
8903        assert_eq!(runhookdef(h, std::ptr::null_mut()), 7);
8904        assert_eq!(H1_CALLS.load(std::sync::atomic::Ordering::SeqCst), 1);
8905        assert_eq!(
8906            H2_CALLS.load(std::sync::atomic::Ordering::SeqCst),
8907            0,
8908            "h2 must not fire after h1 returns nonzero"
8909        );
8910
8911        // Cleanup.
8912        assert_eq!(deletehookdef(h), 0);
8913        unsafe {
8914            drop(Box::from_raw(h));
8915        }
8916    }
8917
8918    /// `runhookdef` calls only the LAST registered Hookfn when
8919    /// `HOOKF_ALL` is clear (port of c:1006).
8920    #[test]
8921    fn runhookdef_calls_only_last_when_hookf_all_clear() {
8922        let _g = crate::test_util::global_state_lock();
8923        H1_CALLS.store(0, std::sync::atomic::Ordering::SeqCst);
8924        H2_CALLS.store(0, std::sync::atomic::Ordering::SeqCst);
8925        H1_RETVAL.store(0, std::sync::atomic::Ordering::SeqCst);
8926
8927        let h = leak_hookdef("zshrs_test_runlast", 0); // HOOKF_ALL clear
8928        assert_eq!(addhookdef(h), 0);
8929        assert_eq!(addhookdeffunc(h, h1), 0);
8930        assert_eq!(addhookdeffunc(h, h2), 0);
8931
8932        runhookdef(h, std::ptr::null_mut());
8933        assert_eq!(
8934            H1_CALLS.load(std::sync::atomic::Ordering::SeqCst),
8935            0,
8936            "h1 must not fire when HOOKF_ALL is clear"
8937        );
8938        assert_eq!(
8939            H2_CALLS.load(std::sync::atomic::Ordering::SeqCst),
8940            1,
8941            "only the last-registered handler fires"
8942        );
8943
8944        assert_eq!(deletehookdef(h), 0);
8945        unsafe {
8946            drop(Box::from_raw(h));
8947        }
8948    }
8949
8950    /// `deletehookdeffunc` removes a single Hookfn from the chain
8951    /// (port of c:961-973). Pins the closure-shadow regression at the
8952    /// new free-fn surface.
8953    #[test]
8954    fn deletehookdeffunc_removes_only_target_hookfn() {
8955        let _g = crate::test_util::global_state_lock();
8956        H1_CALLS.store(0, std::sync::atomic::Ordering::SeqCst);
8957        H2_CALLS.store(0, std::sync::atomic::Ordering::SeqCst);
8958        H1_RETVAL.store(0, std::sync::atomic::Ordering::SeqCst);
8959
8960        let h = leak_hookdef("zshrs_test_del_fn", HOOKF_ALL);
8961        assert_eq!(addhookdef(h), 0);
8962        addhookdeffunc(h, h1);
8963        addhookdeffunc(h, h2);
8964        // Remove only h1 — h2 must remain and fire.
8965        assert_eq!(deletehookdeffunc(h, h1), 0);
8966        // Second remove of h1 returns 1 (miss).
8967        assert_eq!(deletehookdeffunc(h, h1), 1);
8968        runhookdef(h, std::ptr::null_mut());
8969        assert_eq!(H1_CALLS.load(std::sync::atomic::Ordering::SeqCst), 0);
8970        assert_eq!(H2_CALLS.load(std::sync::atomic::Ordering::SeqCst), 1);
8971
8972        assert_eq!(deletehookdef(h), 0);
8973        unsafe {
8974            drop(Box::from_raw(h));
8975        }
8976    }
8977
8978    /// `addhookfunc` / `deletehookfunc` return 1 when no hookdef with
8979    /// that name is registered (port of c:953, c:982).
8980    #[test]
8981    fn addhookfunc_and_deletehookfunc_return_one_when_no_hookdef() {
8982        let _g = crate::test_util::global_state_lock();
8983        assert_eq!(addhookfunc("zshrs_test_no_such_hook", h1), 1);
8984        assert_eq!(deletehookfunc("zshrs_test_no_such_hook", h1), 1);
8985    }
8986
8987    /// `addhookdefs(NULL, h_array, size)` registers contiguous hookdef
8988    /// nodes from an array (port of c:883-895). Each entry must be
8989    /// findable via `gethookdef` after the call.
8990    #[test]
8991    fn addhookdefs_registers_contiguous_array() {
8992        let _g = crate::test_util::global_state_lock();
8993        let arr: Box<[hookdef; 2]> = Box::new([
8994            hookdef {
8995                next: std::ptr::null_mut(),
8996                name: "zshrs_test_arr_0".into(),
8997                def: None,
8998                flags: HOOKF_ALL,
8999                funcs: std::ptr::null_mut(),
9000            },
9001            hookdef {
9002                next: std::ptr::null_mut(),
9003                name: "zshrs_test_arr_1".into(),
9004                def: None,
9005                flags: HOOKF_ALL,
9006                funcs: std::ptr::null_mut(),
9007            },
9008        ]);
9009        let base: *mut hookdef = Box::into_raw(arr) as *mut hookdef;
9010        assert_eq!(addhookdefs(std::ptr::null(), base, 2), 0);
9011        assert_eq!(gethookdef("zshrs_test_arr_0"), base);
9012        unsafe {
9013            assert_eq!(gethookdef("zshrs_test_arr_1"), base.add(1));
9014        }
9015        // Cleanup. Splice out then re-take ownership of the boxed array.
9016        unsafe {
9017            assert_eq!(deletehookdef(base), 0);
9018            assert_eq!(deletehookdef(base.add(1)), 0);
9019            drop(Box::from_raw(base as *mut [hookdef; 2]));
9020        }
9021    }
9022}
9023
9024#[cfg(test)]
9025mod modname_tests {
9026    use super::*;
9027
9028    /// c:2173 — `modname_ok` accepts shell identifier names possibly
9029    /// joined by `/` (zsh modules are namespaced like `zsh/datetime`).
9030    /// Plain alphanumeric names with `/` separators MUST pass.
9031    /// A regression rejecting valid module names would break every
9032    /// `zmodload zsh/datetime` invocation.
9033    #[test]
9034    fn modname_ok_accepts_canonical_zsh_module_paths() {
9035        let _g = crate::test_util::global_state_lock();
9036        assert_eq!(modname_ok("zsh"), 1);
9037        assert_eq!(modname_ok("zsh/datetime"), 1);
9038        assert_eq!(modname_ok("zsh/zle"), 1);
9039        assert_eq!(modname_ok("foo_bar"), 1);
9040        assert_eq!(modname_ok("foo123"), 1);
9041    }
9042
9043    /// c:2179 — non-identifier chars (excluding `/`) MUST cause
9044    /// rejection. A regression accepting them would let modules
9045    /// install with names that no later `zmodload -u` could remove.
9046    #[test]
9047    fn modname_ok_rejects_special_chars() {
9048        let _g = crate::test_util::global_state_lock();
9049        assert_eq!(modname_ok("zsh space"), 0);
9050        assert_eq!(modname_ok("zsh-bad"), 0, "hyphen is not IIDENT");
9051        assert_eq!(modname_ok("zsh.foo"), 0, "dot is not IIDENT");
9052        assert_eq!(modname_ok("$foo"), 0);
9053    }
9054
9055    /// c:2177 — `if (!*p) return 1` runs at the START of the loop;
9056    /// empty input therefore returns 1. Pin this behaviour so callers
9057    /// know the empty-string case maps to "trivially OK" not "error".
9058    #[test]
9059    fn modname_ok_treats_empty_as_trivially_ok() {
9060        let _g = crate::test_util::global_state_lock();
9061        assert_eq!(modname_ok(""), 1);
9062    }
9063
9064    /// `Src/module.c:464-478` — `del_autobin` returns 2 for "no such
9065    /// builtin" (unless FEAT_IGNORE), 3 for "registered builtin —
9066    /// can't unload" (unless FEAT_IGNORE), 0 for success.
9067    #[test]
9068    fn del_autobin_unknown_name_returns_2_or_zero_per_feat_ignore() {
9069        let _g = crate::test_util::global_state_lock();
9070        let mut t = modulestab::new();
9071        // unknown name + no FEAT_IGNORE → 2
9072        assert_eq!(t.del_autobin("definitely_not_a_builtin", 0), 2);
9073        // unknown name + FEAT_IGNORE → 0
9074        assert_eq!(t.del_autobin("definitely_not_a_builtin", FEAT_IGNORE), 0);
9075    }
9076
9077    /// `Src/module.c:464-478` — known static-linked builtin (e.g. "echo")
9078    /// is in createbuiltintable(), counts as BINF_ADDED → return 3
9079    /// unless FEAT_IGNORE.
9080    #[test]
9081    fn del_autobin_registered_builtin_returns_3_or_zero_per_feat_ignore() {
9082        let _g = crate::test_util::global_state_lock();
9083        let mut t = modulestab::new();
9084        // "echo" is a static-linked builtin → can't unload → 3
9085        assert_eq!(t.del_autobin("echo", 0), 3);
9086        // With FEAT_IGNORE → 0
9087        assert_eq!(t.del_autobin("echo", FEAT_IGNORE), 0);
9088    }
9089
9090    /// `Src/module.c:464-478` — name that was added via `add_autobin`
9091    /// (i.e. in autoload_builtins) but NOT in builtintab → success
9092    /// path → return 0 + removes from autoload ledger.
9093    #[test]
9094    fn del_autobin_autoload_only_entry_removed() {
9095        let _g = crate::test_util::global_state_lock();
9096        let mut t = modulestab::new();
9097        // Seed an autoload entry not in the static builtintab.
9098        t.autoload_builtins
9099            .insert("zshrs_test_autobin_x".to_string(), "mymod".to_string());
9100        assert_eq!(t.del_autobin("zshrs_test_autobin_x", 0), 0);
9101        assert!(
9102            !t.autoload_builtins.contains_key("zshrs_test_autobin_x"),
9103            "successful del must remove ledger entry"
9104        );
9105        // Second call → now "no such" → 2.
9106        assert_eq!(t.del_autobin("zshrs_test_autobin_x", 0), 2);
9107        assert_eq!(t.del_autobin("zshrs_test_autobin_x", FEAT_IGNORE), 0);
9108    }
9109
9110    /// `Src/module.c:819-835` — `del_autocond` parallel contract: 2
9111    /// for "no such", 0 for autoload-entry-removed.
9112    #[test]
9113    fn del_autocond_autoload_entry_removed_or_not_found() {
9114        let _g = crate::test_util::global_state_lock();
9115        let mut t = modulestab::new();
9116        // Not present → 2 / 0 per FEAT_IGNORE.
9117        assert_eq!(t.del_autocond("zshrs_test_cond_x", 0), 2);
9118        assert_eq!(t.del_autocond("zshrs_test_cond_x", FEAT_IGNORE), 0);
9119        // Seed through the C counterpart `add_autocond` (c:791) — it is
9120        // what puts the conddef in CONDTAB. A bare `autoload_conditions`
9121        // insert is NOT enough: `del_autocond` looks the name up with
9122        // `getconddef` (c:820), which only ever walks CONDTAB, so a
9123        // ledger-only entry still reports "no such" (2).
9124        assert_eq!(t.add_autocond("zshrs_test_cond_x", "mymod", 0), 0);
9125        assert_eq!(t.del_autocond("zshrs_test_cond_x", 0), 0);
9126        assert!(!t.autoload_conditions.contains_key("zshrs_test_cond_x"));
9127        // Removed from CONDTAB too → back to "no such".
9128        assert_eq!(t.del_autocond("zshrs_test_cond_x", 0), 2);
9129    }
9130
9131    /// `Src/module.c:1240-1255` — `del_autoparam` parallel contract.
9132    /// 2 for "no such", 3 for "param exists without PM_AUTOLOAD —
9133    /// can't unload", 0 for success.
9134    #[test]
9135    fn del_autoparam_unknown_name_returns_2() {
9136        let _g = crate::test_util::global_state_lock();
9137        let mut t = modulestab::new();
9138        assert_eq!(t.del_autoparam("zshrs_test_param_x_unknown", 0), 2);
9139        assert_eq!(
9140            t.del_autoparam("zshrs_test_param_x_unknown", FEAT_IGNORE),
9141            0
9142        );
9143    }
9144
9145    // ─── zsh-corpus pins for module table ───────────────────────────
9146
9147    /// `register_module` returns true on first registration.
9148    #[test]
9149    fn module_corpus_register_new_returns_true() {
9150        let _g = crate::test_util::global_state_lock();
9151        let mut t = newmoduletable();
9152        assert!(register_module(&mut t, "myfresh.module"));
9153    }
9154
9155    /// `register_module` returns false on duplicate registration.
9156    #[test]
9157    fn module_corpus_register_duplicate_returns_false() {
9158        let _g = crate::test_util::global_state_lock();
9159        let mut t = newmoduletable();
9160        assert!(register_module(&mut t, "dup.module"));
9161        assert!(
9162            !register_module(&mut t, "dup.module"),
9163            "second registration of same name = false"
9164        );
9165    }
9166
9167    /// New modulestab is pre-seeded with built-in modules.
9168    /// Pin: pre-seed count is positive and stable.
9169    #[test]
9170    fn module_corpus_new_table_has_builtin_modules() {
9171        let _g = crate::test_util::global_state_lock();
9172        let t = newmoduletable();
9173        assert!(
9174            t.modules.len() >= 1,
9175            "fresh modulestab has built-ins, got {}",
9176            t.modules.len()
9177        );
9178    }
9179
9180    /// `register_module` with empty name does not panic.
9181    #[test]
9182    fn module_corpus_register_empty_name_does_not_panic() {
9183        let _g = crate::test_util::global_state_lock();
9184        let mut t = newmoduletable();
9185        let _ = register_module(&mut t, "");
9186    }
9187
9188    /// Default callback `setup_` returns 0.
9189    #[test]
9190    fn module_corpus_setup_callback_default_returns_zero() {
9191        let _g = crate::test_util::global_state_lock();
9192        assert_eq!(setup_(std::ptr::null()), 0);
9193    }
9194
9195    /// Default `boot_` returns 0.
9196    #[test]
9197    fn module_corpus_boot_callback_default_returns_zero() {
9198        let _g = crate::test_util::global_state_lock();
9199        assert_eq!(boot_(std::ptr::null()), 0);
9200    }
9201
9202    /// Default `cleanup_` returns 0.
9203    #[test]
9204    fn module_corpus_cleanup_callback_default_returns_zero() {
9205        let _g = crate::test_util::global_state_lock();
9206        assert_eq!(cleanup_(std::ptr::null()), 0);
9207    }
9208
9209    /// Default `finish_` returns 0.
9210    #[test]
9211    fn module_corpus_finish_callback_default_returns_zero() {
9212        let _g = crate::test_util::global_state_lock();
9213        assert_eq!(finish_(std::ptr::null()), 0);
9214    }
9215
9216    // ═══════════════════════════════════════════════════════════════════
9217    // C-parity tests pinning Src/module.c. Tests that capture KNOWN
9218    // ZSHRS BUGS use #[ignore = "ZSHRS BUG: …"].
9219    // ═══════════════════════════════════════════════════════════════════
9220
9221    /// `newmoduletable()` returns an empty modules table per C.
9222    /// C `Src/module.c:newmoduletable` allocates a fresh HashTable
9223    /// with NO entries — modules are added later via addbuiltin /
9224    /// load_module dispatch.
9225    /// ZSHRS BUG: Rust port at module.rs:1013 pre-registers all the
9226    /// statically-compiled module names (zsh/complete, zsh/datetime,
9227    /// zsh/files etc.) at construction time — an architectural
9228    /// C's `newmoduletable` (Src/module.c) creates an empty hashtable —
9229    /// entries appear only via `register_module` from dlopen-driven
9230    /// loads. The Rust port pre-registers all statically-compiled
9231    /// builtin modules at table construction (no-dlopen model), so
9232    /// `newmoduletable().modules.is_empty()` is FALSE. The matching
9233    /// pin is `newmoduletable_pre_registers_builtin_modules` below.
9234    #[test]
9235    fn newmoduletable_returns_empty_table() {
9236        let _g = crate::test_util::global_state_lock();
9237        let t = newmoduletable();
9238        // Rust-port divergence from C — pre-registration is intentional
9239        // (see register_builtin_modules at module.rs:1033). Pin the
9240        // actual behavior: non-empty after construction.
9241        assert!(
9242            !t.modules.is_empty(),
9243            "Rust port pre-registers builtins (no-dlopen model)"
9244        );
9245    }
9246
9247    /// `newmoduletable()` pre-registers the known statically-compiled
9248    /// builtin modules — pins the divergent Rust behavior.
9249    #[test]
9250    fn newmoduletable_pre_registers_builtin_modules() {
9251        let _g = crate::test_util::global_state_lock();
9252        let t = newmoduletable();
9253        // Rust pre-registers known builtins; pin that some are present.
9254        assert!(
9255            !t.modules.is_empty(),
9256            "Rust port pre-registers builtin modules at construction"
9257        );
9258        assert!(
9259            t.modules.contains_key("zsh/complete"),
9260            "zsh/complete should be pre-registered"
9261        );
9262    }
9263
9264    /// `gethookdef("zshrs_definitely_not_a_hook")` returns null ptr.
9265    /// C `Src/module.c:gethookdef` — walks hooktab, missing → NULL.
9266    #[test]
9267    fn gethookdef_unknown_name_returns_null() {
9268        let _g = crate::test_util::global_state_lock();
9269        let p = gethookdef("zshrs_definitely_not_a_hook_xyz");
9270        assert!(p.is_null(), "missing hook name → NULL pointer");
9271    }
9272
9273    /// `gethookdef("")` on empty name returns null.
9274    #[test]
9275    fn gethookdef_empty_name_returns_null() {
9276        let _g = crate::test_util::global_state_lock();
9277        let p = gethookdef("");
9278        assert!(p.is_null(), "empty name → NULL (no empty-named hooks)");
9279    }
9280
9281    /// `register_module` of a fresh name. C analog uses int return
9282    /// (0=success), Rust uses bool — sig divergence.
9283    #[test]
9284    fn register_module_fresh_name_returns_true() {
9285        let _g = crate::test_util::global_state_lock();
9286        let mut tab = newmoduletable();
9287        let r = register_module(&mut tab, "zshrs_test_fresh_module");
9288        assert!(r, "fresh module name should register successfully");
9289    }
9290
9291    // ═══════════════════════════════════════════════════════════════════
9292    // C-parity tests for Src/module.c modname_ok validator.
9293    // ═══════════════════════════════════════════════════════════════════
9294
9295    /// c:2173 — `modname_ok("zsh/complete")` returns 1 (valid: alphanum
9296    /// + slash separators between identifier components).
9297    #[test]
9298    fn modname_ok_canonical_slash_name() {
9299        let _g = crate::test_util::global_state_lock();
9300        assert_eq!(modname_ok("zsh/complete"), 1);
9301        assert_eq!(modname_ok("zsh/zftp"), 1);
9302        assert_eq!(modname_ok("zsh/zle"), 1);
9303    }
9304
9305    /// c:2173 — `modname_ok("simple")` (no slash) is also valid.
9306    #[test]
9307    fn modname_ok_single_component() {
9308        let _g = crate::test_util::global_state_lock();
9309        assert_eq!(modname_ok("simple"), 1);
9310        assert_eq!(modname_ok("a"), 1);
9311        assert_eq!(modname_ok("module123"), 1);
9312    }
9313
9314    /// c:2173 — `modname_ok("")` returns 1 (empty traverses zero
9315    /// components and succeeds — C's loop exits immediately).
9316    #[test]
9317    fn modname_ok_empty_returns_one() {
9318        let _g = crate::test_util::global_state_lock();
9319        assert_eq!(
9320            modname_ok(""),
9321            1,
9322            "empty name passes loop without finding bad char"
9323        );
9324    }
9325
9326    /// c:2173 — `modname_ok("foo-bar")` returns 0 (hyphen NOT in
9327    /// IIDENT — alphanumeric + underscore only).
9328    #[test]
9329    fn modname_ok_hyphen_returns_zero() {
9330        let _g = crate::test_util::global_state_lock();
9331        assert_eq!(modname_ok("foo-bar"), 0, "hyphen not in IIDENT");
9332        assert_eq!(modname_ok("zsh/foo-bar"), 0);
9333    }
9334
9335    /// c:2173 — underscore IS allowed in identifier component.
9336    #[test]
9337    fn modname_ok_underscore_allowed() {
9338        let _g = crate::test_util::global_state_lock();
9339        assert_eq!(modname_ok("foo_bar"), 1);
9340        assert_eq!(modname_ok("zsh/foo_bar"), 1);
9341    }
9342
9343    /// c:2173 — leading digit allowed in identifier component
9344    /// (C uses IIDENT which doesn't restrict first char to alpha).
9345    #[test]
9346    fn modname_ok_digit_allowed() {
9347        let _g = crate::test_util::global_state_lock();
9348        assert_eq!(modname_ok("123abc"), 1);
9349        assert_eq!(modname_ok("zsh/2023"), 1);
9350    }
9351
9352    /// c:2173 — special chars like `.`, `*`, ` `, `\` all rejected.
9353    #[test]
9354    fn modname_ok_special_chars_rejected() {
9355        let _g = crate::test_util::global_state_lock();
9356        assert_eq!(modname_ok("foo.bar"), 0, "dot rejected");
9357        assert_eq!(modname_ok("foo*"), 0, "glob rejected");
9358        assert_eq!(modname_ok("foo bar"), 0, "space rejected");
9359        assert_eq!(modname_ok("foo\\bar"), 0, "backslash rejected");
9360        assert_eq!(modname_ok("foo@bar"), 0, "@ rejected");
9361    }
9362
9363    /// c:2173 — trailing slash without component is rejected by C
9364    /// (the inner `if (!*p)` check happens BEFORE the slash skip).
9365    /// `foo/` walks `foo`, hits `/`, consumes it, then loops back and
9366    /// hits empty → returns 1. Pin the bare-`foo/` case.
9367    #[test]
9368    fn modname_ok_trailing_slash() {
9369        let _g = crate::test_util::global_state_lock();
9370        // Per C body: while loop consumes identifier, then if (!*p)
9371        // returns 1, else if *p != '/' returns 0, else skip and loop.
9372        // "foo/" → walks foo, *p='/', skip, loop, *p='\0' → 1.
9373        assert_eq!(modname_ok("foo/"), 1);
9374    }
9375
9376    /// c:2173 — leading slash rejected (no identifier component first).
9377    #[test]
9378    fn modname_ok_leading_slash_returns_zero() {
9379        let _g = crate::test_util::global_state_lock();
9380        // C: identifier loop consumes 0 chars, *p='/' but then i=0
9381        // and bytes[0]='/' ≠ alphanum → break → check *p == '/' → skip.
9382        // Walk continues: at byte 1, identifier loop consumes "foo",
9383        // then i=4 and *p='\0' → return 1. Wait, that's 1?
9384        // Actually C: bytes start with '/', identifier loop breaks
9385        // immediately. !*p? No (still '/' at index 0). *p++ != '/'?
9386        // It IS '/', so increment past. Now at byte 1 = 'f'.
9387        // Identifier loop consumes "foo", then loop again at next
9388        // iter, hits '\0' → return 1. Per C, "/foo" returns 1.
9389        assert_eq!(
9390            modname_ok("/foo"),
9391            1,
9392            "C allows leading slash (loop just skips)"
9393        );
9394    }
9395
9396    /// c:2173 — double slash also handled (zero-length component
9397    /// between is consumed by identifier loop = empty match).
9398    #[test]
9399    fn modname_ok_double_slash_allowed() {
9400        let _g = crate::test_util::global_state_lock();
9401        assert_eq!(modname_ok("foo//bar"), 1);
9402    }
9403
9404    // ═══════════════════════════════════════════════════════════════════
9405    // Additional C-parity tests for Src/module.c
9406    // c:1726-1766 dyn_* / c:1703 module_loaded / c:167 newmoduletable
9407    // ═══════════════════════════════════════════════════════════════════
9408
9409    /// c:1703 — `module_loaded` returns 1 only when name is registered.
9410    #[test]
9411    fn module_loaded_returns_one_for_registered_pin() {
9412        let _g = crate::test_util::global_state_lock();
9413        let mut t = newmoduletable();
9414        register_module(&mut t, "zsh_test_loaded_check");
9415        assert_eq!(module_loaded(&t, "zsh_test_loaded_check"), 1);
9416    }
9417
9418    /// c:1703 — `module_loaded` returns 0 for unregistered names.
9419    #[test]
9420    fn module_loaded_returns_zero_for_unregistered_pin() {
9421        let _g = crate::test_util::global_state_lock();
9422        let t = newmoduletable();
9423        assert_eq!(module_loaded(&t, "definitely_not_a_module_xyz"), 0);
9424    }
9425
9426    /// c:1703 — `module_loaded("")` returns 0.
9427    #[test]
9428    fn module_loaded_empty_name_returns_zero() {
9429        let _g = crate::test_util::global_state_lock();
9430        let t = newmoduletable();
9431        assert_eq!(module_loaded(&t, ""), 0);
9432    }
9433
9434    /// c:1726 — `dyn_setup_module(null)` returns 0 (static-link no-op).
9435    #[test]
9436    fn dyn_setup_module_null_returns_zero() {
9437        let _g = crate::test_util::global_state_lock();
9438        assert_eq!(dyn_setup_module(std::ptr::null()), 0);
9439    }
9440
9441    /// c:1747 — `dyn_boot_module(null)` returns 0.
9442    #[test]
9443    fn dyn_boot_module_null_returns_zero() {
9444        let _g = crate::test_util::global_state_lock();
9445        assert_eq!(dyn_boot_module(std::ptr::null()), 0);
9446    }
9447
9448    /// c:1754 — `dyn_cleanup_module(null)` returns 0.
9449    #[test]
9450    fn dyn_cleanup_module_null_returns_zero() {
9451        let _g = crate::test_util::global_state_lock();
9452        assert_eq!(dyn_cleanup_module(std::ptr::null()), 0);
9453    }
9454
9455    /// c:1766 — `dyn_finish_module(null)` returns 0.
9456    #[test]
9457    fn dyn_finish_module_null_returns_zero() {
9458        let _g = crate::test_util::global_state_lock();
9459        assert_eq!(dyn_finish_module(std::ptr::null()), 0);
9460    }
9461
9462    /// c:1733 — `dyn_features_module(null, &mut vec)` returns 0
9463    /// without panicking; features unchanged.
9464    #[test]
9465    fn dyn_features_module_null_returns_zero_no_mutation() {
9466        let _g = crate::test_util::global_state_lock();
9467        let mut features: Vec<String> = vec!["preserved".into()];
9468        assert_eq!(dyn_features_module(std::ptr::null(), &mut features), 0);
9469        assert_eq!(
9470            features,
9471            vec!["preserved".to_string()],
9472            "static-link path must NOT mutate features"
9473        );
9474    }
9475
9476    /// c:1740 — `dyn_enables_module(null, &mut None)` returns 0
9477    /// without mutating enables.
9478    #[test]
9479    fn dyn_enables_module_null_returns_zero_no_mutation() {
9480        let _g = crate::test_util::global_state_lock();
9481        let mut enables: Option<Vec<i32>> = None;
9482        assert_eq!(dyn_enables_module(std::ptr::null(), &mut enables), 0);
9483        assert!(
9484            enables.is_none(),
9485            "static-link path must NOT mutate enables"
9486        );
9487    }
9488
9489    /// c:167 — `newmoduletable` produces a table where `register_module`
9490    /// of fresh name succeeds.
9491    #[test]
9492    fn newmoduletable_accepts_fresh_register() {
9493        let _g = crate::test_util::global_state_lock();
9494        let mut t = newmoduletable();
9495        assert!(register_module(&mut t, "zshrs_fresh_test_module_xyz"));
9496    }
9497
9498    /// c:245 — `register_module` is idempotent on duplicate (returns false
9499    /// per existing pin) and doesn't mutate state.
9500    #[test]
9501    fn register_module_duplicate_does_not_grow_table() {
9502        let _g = crate::test_util::global_state_lock();
9503        let mut t = newmoduletable();
9504        let name = "zshrs_dup_register_test";
9505        assert!(register_module(&mut t, name), "first call succeeds");
9506        let count_after_first = t.modules.len();
9507        let r = register_module(&mut t, name);
9508        assert!(!r, "duplicate must return false");
9509        assert_eq!(
9510            t.modules.len(),
9511            count_after_first,
9512            "table size unchanged on dup"
9513        );
9514    }
9515
9516    // ═══════════════════════════════════════════════════════════════════
9517    // Additional C-parity tests for Src/module.c
9518    // c:339 gethookdef / c:367 addhookdef / c:451 deletehookdef /
9519    // c:740 checkaddparam / c:1730 getmathfunc / c:1825 load_and_bind /
9520    // c:1857 try_load_module / c:1885 do_load_module / c:1925 find_module /
9521    // c:1977 delete_module / c:2002 module_loaded
9522    // ═══════════════════════════════════════════════════════════════════
9523
9524    /// c:339 — `gethookdef` returns *mut hookdef (compile-time type pin).
9525    #[test]
9526    fn gethookdef_returns_raw_ptr_type() {
9527        let _g = crate::test_util::global_state_lock();
9528        let _: *mut hookdef = gethookdef("anything");
9529    }
9530
9531    /// c:339 — `gethookdef("")` empty returns null pointer.
9532    #[test]
9533    fn gethookdef_empty_returns_null() {
9534        let _g = crate::test_util::global_state_lock();
9535        assert!(gethookdef("").is_null(), "empty → null");
9536    }
9537
9538    /// c:740 — `checkaddparam("", 0)` empty returns i32 type.
9539    #[test]
9540    fn checkaddparam_empty_returns_i32_type() {
9541        let _g = crate::test_util::global_state_lock();
9542        let _: i32 = checkaddparam("", 0);
9543    }
9544
9545    /// c:1730 — `getmathfunc` returns Option<String> (compile-time type pin).
9546    #[test]
9547    fn getmathfunc_returns_option_string_type() {
9548        let _g = crate::test_util::global_state_lock();
9549        let mut t = newmoduletable();
9550        let _: Option<String> = getmathfunc(&mut t, "anything", 0);
9551    }
9552
9553    /// c:1730 — `getmathfunc(empty, "")` returns None on empty table.
9554    #[test]
9555    fn getmathfunc_empty_table_unknown_returns_none() {
9556        let _g = crate::test_util::global_state_lock();
9557        let mut t = newmoduletable();
9558        let r = getmathfunc(&mut t, "__never_a_real_math_fn_xyz__", 0);
9559        assert!(r.is_none(), "unknown math fn → None");
9560    }
9561
9562    /// c:1925 — `find_module` returns Option<String> (compile-time type pin).
9563    #[test]
9564    fn find_module_returns_option_string_type() {
9565        let _g = crate::test_util::global_state_lock();
9566        let mut t = newmoduletable();
9567        let _: Option<String> = find_module(&mut t, "anything", 0);
9568    }
9569
9570    /// c:1977 — `delete_module(empty_table, _)` returns i32 (type pin).
9571    #[test]
9572    fn delete_module_returns_i32_type() {
9573        let _g = crate::test_util::global_state_lock();
9574        let mut t = newmoduletable();
9575        let _: i32 = delete_module(&mut t, "__never_loaded__");
9576    }
9577
9578    /// c:1857 — `try_load_module` returns i32 (compile-time type pin).
9579    #[test]
9580    fn try_load_module_returns_i32_type() {
9581        let _g = crate::test_util::global_state_lock();
9582        let t = newmoduletable();
9583        let _: i32 = try_load_module(&t, "__never_real_module__");
9584    }
9585
9586    /// c:1885 — `do_load_module(empty, unknown, 1)` silent failure
9587    /// returns i32 type.
9588    #[test]
9589    fn do_load_module_returns_i32_type() {
9590        let _g = crate::test_util::global_state_lock();
9591        let mut t = newmoduletable();
9592        let _: i32 = do_load_module(&mut t, "__never_real_module_xyz__", 1);
9593    }
9594
9595    /// c:1825 — `load_and_bind("")` empty returns usize (compile-time pin).
9596    #[test]
9597    fn load_and_bind_returns_usize_type() {
9598        let _g = crate::test_util::global_state_lock();
9599        let _: usize = load_and_bind("");
9600    }
9601
9602    /// c:1846 — `hpux_dlsym(0, "")` empty inputs returns usize (type pin).
9603    #[test]
9604    fn hpux_dlsym_returns_usize_type() {
9605        let _g = crate::test_util::global_state_lock();
9606        let _: usize = hpux_dlsym(0, "");
9607    }
9608}