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_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(HashNode hn)` from Src/module.c:119 — Rust's
39/// `Drop` handles the per-field free; this exists for API
40/// parity with C callers.
41pub fn freemodulenode(hn: module) {
42    // Rust Drop handles this
43}
44
45/// `PRINTMOD_LIST` from `Src/module.c:136`. Long-form (`zmodload -L`)
46/// output.
47pub const PRINTMOD_LIST: i32 = 0x0001; // c:136
48/// `PRINTMOD_EXIST` from `Src/module.c:138`. Print only when the
49/// module exists.
50pub const PRINTMOD_EXIST: i32 = 0x0002; // c:138
51/// `PRINTMOD_ALIAS` from `Src/module.c:140`. Resolve aliases when
52/// emitting under `PRINTMOD_EXIST`.
53pub const PRINTMOD_ALIAS: i32 = 0x0004; // c:140
54/// `PRINTMOD_DEPS` from `Src/module.c:142`. Emit the dependency list
55/// (`zmodload -d`).
56pub const PRINTMOD_DEPS: i32 = 0x0008; // c:142
57/// `PRINTMOD_FEATURES` from `Src/module.c:144`. Emit feature flags
58/// (`zmodload -F`).
59pub const PRINTMOD_FEATURES: i32 = 0x0010; // c:144
60/// `PRINTMOD_LISTALL` from `Src/module.c:146`. Include disabled
61/// features (`zmodload -lL`).
62pub const PRINTMOD_LISTALL: i32 = 0x0020; // c:146
63/// `PRINTMOD_AUTO` from `Src/module.c:148`. Emit autoloads
64/// (`zmodload -a`).
65pub const PRINTMOD_AUTO: i32 = 0x0040; // c:148
66
67/// Direct port of `void printmodulenode(HashNode hn, int flags)` from
68/// `Src/module.c:154`.
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(int size, char const *name)` from Src/module.c:274 — the C
303/// source allocates the `modulestab` hash with `createhashtable`.
304/// WARNING: param names don't match C — Rust=() vs C=(size, name)
305pub fn newmoduletable() -> modulestab {
306    modulestab::new()
307}
308
309/// Port of `setup_(UNUSED(Module m))` from `Src/module.c:306`.
310///
311/// C body: `setup_(UNUSED(Module m)) { return 0; }` — the no-op
312/// setup hook of the module subsystem itself.
313#[allow(unused_variables)]
314pub fn setup_(m: *const module) -> i32 {
315    // c:306
316    0 // c:306
317}
318
319/// Port of `features_(UNUSED(Module m), UNUSED(char ***features))` from `Src/module.c:313`.
320///
321/// C body:
322/// ```c
323/// features_(UNUSED(Module m), UNUSED(char ***features))
324/// {
325///     /* There are lots and lots of features, but they're not handled here. */
326///     return 1;
327/// }
328/// ```
329#[allow(unused_variables)]
330pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
331    // c:313
332    /* There are lots and lots of features, but they're not handled here. */ // c:313-318
333    1 // c:319
334}
335
336/// Port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from `Src/module.c:324`.
337///
338/// C body: `enables_(UNUSED(Module m), UNUSED(int **enables)) { return 1; }`
339/// — the module subsystem itself doesn't manage feature enables.
340#[allow(unused_variables)]
341pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
342    // c:324
343    1 // c:324
344}
345
346/// Port of `boot_(UNUSED(Module m))` from `Src/module.c:331`.
347///
348/// C body: `boot_(UNUSED(Module m)) { return 0; }` — the no-op
349/// boot hook of the module subsystem itself.
350#[allow(unused_variables)]
351pub fn boot_(m: *const module) -> i32 {
352    // c:331
353    0 // c:331
354}
355
356/// Port of `cleanup_(UNUSED(Module m))` from `Src/module.c:338`.
357///
358/// C body: `cleanup_(UNUSED(Module m)) { return 0; }` — the no-op
359/// cleanup hook of the module subsystem itself.
360#[allow(unused_variables)]
361pub fn cleanup_(m: *const module) -> i32 {
362    // c:338
363    0 // c:338
364}
365
366/// Port of `finish_(UNUSED(Module m))` from `Src/module.c:345`.
367///
368/// C body: `finish_(UNUSED(Module m)) { return 0; }` —
369/// the no-op finish hook for the module subsystem itself.
370#[allow(unused_variables)]
371pub fn finish_(m: *const module) -> i32 {
372    // c:345
373    0 // c:345
374}
375
376// This registers a builtin module.                                        // c:359
377/// Register module (from module.c register_module)
378/// Register a module by name.
379/// Port of `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)` from Src/module.c:359 — wraps
380/// a slot in the global `modulestab` and seeds its lifecycle
381/// callbacks.
382/// WARNING: param names don't match C — Rust=(table, name) vs C=(n, setup, features, enables, boot, cleanup, finish)
383pub fn register_module(table: &mut modulestab, name: &str) -> bool {
384    // c:359
385    if table.modules.contains_key(name) {
386        return false;
387    }
388    table.modules.insert(name.to_string(), module::new(name));
389    true
390}
391
392/// Port of `addbuiltins(char const *nam, Builtin binl, int size)` from `Src/module.c:544`.
393///
394/// C body:
395/// ```c
396/// addbuiltins(char const *nam, Builtin binl, int size)
397/// {
398///     int ret = 0, n;
399///     for(n = 0; n < size; n++) {
400///         Builtin b = &binl[n];
401///         if(b->node.flags & BINF_ADDED)
402///             continue;
403///         if(addbuiltin(b)) {
404///             zwarnnam(nam, "name clash when adding builtin `%s'", b->node.nam);
405///             ret = 1;
406///         } else {
407///             b->node.flags |= BINF_ADDED;
408///         }
409///     }
410///     return ret;
411/// }
412/// ```
413///
414/// Rust port: walks the slice, checks BINF_ADDED, registers via the
415/// module-table addbuiltin if not already registered. `binl` is taken
416/// by `&mut [Builtin]` so the BINF_ADDED flag-set after success
417/// matches C's in-place mutation.
418/// Port of `addbuiltin(Builtin b)` from `Src/module.c:524`. C body:
419/// look up `b->node.nam` in builtintab; if BINF_ADDED clash → return 1;
420/// otherwise replace any pre-existing entry and add `b`. Returns 0 on
421/// add, 1 on clash.
422///
423/// The Rust canonical builtintab is `OnceLock<HashMap<String,
424/// &'static builtin>>` — immutable after first access. Runtime `addbuiltin`
425/// calls check the immutable table for the clash gate; the BINF_ADDED
426/// flag-set on the input record is what callers observe (matching the
427/// C in-place mutation that `addbuiltins` then propagates).
428pub fn addbuiltin(b: &mut builtin) -> i32 {
429    // c:524
430    let tab = createbuiltintable();
431    if let Some(existing) = tab.get(&b.node.nam) {
432        // c:526 getnode2
433        if (existing.node.flags & BINF_ADDED as i32) != 0 {
434            return 1;
435        } // c:527 clash
436    }
437    b.node.flags |= BINF_ADDED as i32; // c:531 b->node.flags |= BINF_ADDED
438    0
439}
440
441/// Port of `int deletebuiltin(const char *nam)` from `Src/module.c:449`.
442///
443/// C body c:449-458:
444/// ```c
445/// int
446/// deletebuiltin(const char *nam)
447/// {
448///     Builtin bn;
449///     bn = (Builtin) builtintab->removenode(builtintab, nam);
450///     if (!bn)
451///         return -1;
452///     builtintab->freenode(&bn->node);
453///     return 0;
454/// }
455/// ```
456///
457/// Returns 0 on success (entry was found + removed), -1 on miss.
458///
459/// zshrs's `createbuiltintable()` returns an immutable HashMap —
460/// the canonical table is static-linked, so this fn can't actually
461/// `removenode`. The faithful structural equivalent: probe the
462/// canonical table to honour the present/absent contract that
463/// callers (setbuiltins, del_autobin) rely on for their
464/// `already-deleted` / `no such builtin` diagnostics. Actual
465/// runtime `enabled` state lives on the modulestab's
466/// `added_builtins` ledger; that's where the caller flips the
467/// observable BINF_ADDED bit.
468pub fn deletebuiltin(nam: &str) -> i32 {
469    // c:449
470    // c:453 — `bn = builtintab->removenode(builtintab, nam);`
471    let tab = createbuiltintable();
472    match tab.get(nam) {
473        None => -1,   // c:454-455 — `if (!bn) return -1;`
474        Some(_) => 0, // c:457 — freenode is owned by createbuiltintable, no-op.
475    }
476}
477
478/// Port of `addbuiltins(char const *nam, Builtin binl, int size)` from
479/// `Src/module.c:544`. Walks the slice; for each entry not already
480/// flagged BINF_ADDED, calls `addbuiltin`. Returns 0 if all succeeded,
481/// 1 if any clashed. zwarnnam emitted on each clash matches C.
482pub fn addbuiltins(nam: &str, binl: &mut [builtin]) -> i32 {
483    // c:544
484    let mut ret = 0; // c:548
485    for b in binl.iter_mut() {
486        // c:550 for(n = 0; n < size; n++)
487        if (b.node.flags & BINF_ADDED as i32) != 0 {
488            continue;
489        } // c:553
490        if addbuiltin(b) != 0 {
491            // c:555
492            zwarnnam(
493                nam, // c:556 zwarnnam(nam, "name clash...")
494                &format!("name clash when adding builtin `{}'", b.node.nam),
495            );
496            ret = 1;
497        }
498    }
499    ret // c:563
500}
501
502/// Port of `Hookdef gethookdef(const char *n)` from `Src/module.c:849`.
503///
504/// C body (c:849-861):
505/// ```c
506/// Hookdef gethookdef(const char *n) {
507///     Hookdef p;
508///     for (p = hooktab; p; p = p->next)
509///         if (!strcmp(n, p->name))
510///             return p;
511///     return NULL;
512/// }
513/// ```
514pub fn gethookdef(n: &str) -> *mut hookdef {
515    // c:849
516    let mut p = hooktab.load(std::sync::atomic::Ordering::SeqCst); // c:852 p = hooktab
517    while !p.is_null() {
518        // c:853
519        unsafe {
520            if (*p).name == n {
521                // c:854 !strcmp
522                return p; // c:855
523            }
524            p = (*p).next; // c:853 p = p->next
525        }
526    }
527    std::ptr::null_mut() // c:856
528}
529
530/// Port of `int addhookdef(Hookdef h)` from `Src/module.c:864`.
531///
532/// C body (c:864-874):
533/// ```c
534/// int addhookdef(Hookdef h) {
535///     if (gethookdef(h->name)) return 1;
536///     h->next = hooktab;
537///     hooktab = h;
538///     h->funcs = znewlinklist();
539///     return 0;
540/// }
541/// ```
542pub fn addhookdef(h: *mut hookdef) -> i32 {
543    // c:864
544    unsafe {
545        if !gethookdef(&(*h).name).is_null() {
546            // c:866
547            return 1; // c:867
548        }
549        (*h).next = hooktab.load(std::sync::atomic::Ordering::SeqCst); // c:869 h->next = hooktab
550        hooktab.store(h, std::sync::atomic::Ordering::SeqCst); // c:870 hooktab = h
551        if (*h).funcs.is_null() {
552            // c:871 h->funcs = znewlinklist()
553            (*h).funcs = Box::into_raw(Box::new(linklist {
554                first: None,
555                last: None,
556                flags: 0,
557            }));
558        }
559    }
560    0 // c:873
561}
562
563/// Port of `int addhookdefs(Module m, Hookdef h, int size)` from
564/// `Src/module.c:883`.
565///
566/// C body (c:883-895):
567/// ```c
568/// int addhookdefs(Module m, Hookdef h, int size) {
569///     int ret = 0;
570///     while (size--) {
571///         if (addhookdef(h)) {
572///             zwarnnam(m ? m->node.nam : NULL, "name clash when adding hook `%s'", h->name);
573///             ret = 1;
574///         }
575///         h++;
576///     }
577///     return ret;
578/// }
579/// ```
580pub fn addhookdefs(
581    // c:883
582    m: *const module,
583    mut h: *mut hookdef,
584    mut size: i32,
585) -> i32 {
586    let mut ret: i32 = 0; // c:885
587    while size > 0 {
588        // c:887 size--
589        if addhookdef(h) != 0 {
590            // c:888
591            let nam: String = if m.is_null() {
592                // c:889 m ? m->node.nam : NULL
593                String::new()
594            } else {
595                unsafe { (*m).node.nam.clone() }
596            };
597            let hook_name = unsafe { (*h).name.clone() };
598            zwarnnam(
599                // c:889 zwarnnam
600                &nam,
601                &format!("name clash when adding hook `{}'", hook_name),
602            );
603            ret = 1; // c:891
604        }
605        unsafe {
606            h = h.add(1);
607        } // c:893 h++
608        size -= 1; // c:887
609    }
610    ret // c:894
611}
612
613/// Port of `int deletehookdef(Hookdef h)` from `Src/module.c:902`.
614///
615/// C body (c:902-919):
616/// ```c
617/// int deletehookdef(Hookdef h) {
618///     Hookdef p, q;
619///     for (p = hooktab, q = NULL; p && p != h; q = p, p = p->next);
620///     if (!p) return 1;
621///     if (q) q->next = p->next; else hooktab = p->next;
622///     freelinklist(p->funcs, NULL);
623///     return 0;
624/// }
625/// ```
626pub fn deletehookdef(h: *mut hookdef) -> i32 {
627    // c:902
628    let mut p = hooktab.load(std::sync::atomic::Ordering::SeqCst); // c:906 p = hooktab
629    let mut q: *mut hookdef = std::ptr::null_mut(); // c:906 q = NULL
630    while !p.is_null() && p != h {
631        // c:907
632        q = p; // c:907 q = p
633        unsafe {
634            p = (*p).next;
635        } // c:907 p = p->next
636    }
637    if p.is_null() {
638        // c:909
639        return 1; // c:910
640    }
641    unsafe {
642        if !q.is_null() {
643            // c:912
644            (*q).next = (*p).next; // c:913 q->next = p->next
645        } else {
646            hooktab.store((*p).next, std::sync::atomic::Ordering::SeqCst); // c:915 hooktab = p->next
647        }
648        if !(*p).funcs.is_null() {
649            // c:916 freelinklist(p->funcs, NULL)
650            drop(Box::from_raw((*p).funcs));
651            (*p).funcs = std::ptr::null_mut();
652        }
653    }
654    0 // c:917
655}
656
657/// Port of `int deletehookdefs(Module m, Hookdef h, int size)` from
658/// `Src/module.c:923`. `m` is unused per `UNUSED(Module m)` in C.
659pub fn deletehookdefs(
660    // c:923
661    _m: *const module,
662    mut h: *mut hookdef,
663    mut size: i32,
664) -> i32 {
665    let mut ret: i32 = 0; // c:925
666    while size > 0 {
667        // c:927 size--
668        if deletehookdef(h) != 0 {
669            // c:928
670            ret = 1; // c:929
671        }
672        unsafe {
673            h = h.add(1);
674        } // c:930 h++
675        size -= 1; // c:927
676    }
677    ret // c:931
678}
679
680/// Port of `int addhookdeffunc(Hookdef h, Hookfn f)` from `Src/module.c:939`.
681///
682/// C body (c:939-944):
683/// ```c
684/// int addhookdeffunc(Hookdef h, Hookfn f) {
685///     zaddlinknode(h->funcs, (void *) f);
686///     return 0;
687/// }
688/// ```
689pub fn addhookdeffunc(
690    // c:939
691    h: *mut hookdef,
692    f: Hookfn,
693) -> i32 {
694    unsafe {
695        if (*h).funcs.is_null() {
696            (*h).funcs = Box::into_raw(Box::new(linklist {
697                first: None,
698                last: None,
699                flags: 0,
700            }));
701        }
702        let funcs = &mut *(*h).funcs;
703        // c:942 — zaddlinknode(h->funcs, f) appends to end of LinkList.
704        // Walk to tail of owned chain (linklist.last cannot be a Box
705        // pointer because that would duplicate ownership of the tail
706        // node — the C `last` field is a raw pointer; the Rust
707        // representation keeps it as None and resolves the tail by
708        // walking forward from .first).
709        let new_node = Box::new(linknode {
710            next: None,
711            prev: None,
712            dat: f as usize,
713        });
714        if funcs.first.is_none() {
715            funcs.first = Some(new_node);
716        } else {
717            let mut tail = funcs.first.as_mut().unwrap();
718            while tail.next.is_some() {
719                tail = tail.next.as_mut().unwrap();
720            }
721            tail.next = Some(new_node);
722        }
723    }
724    0 // c:943
725}
726
727/// Port of `int addhookfunc(char *n, Hookfn f)` from `Src/module.c:948`.
728///
729/// C body (c:948-955):
730/// ```c
731/// int addhookfunc(char *n, Hookfn f) {
732///     Hookdef h = gethookdef(n);
733///     if (h) return addhookdeffunc(h, f);
734///     return 1;
735/// }
736/// ```
737pub fn addhookfunc(
738    // c:948
739    n: &str,
740    f: Hookfn,
741) -> i32 {
742    let h = gethookdef(n); // c:950 h = gethookdef(n)
743    if !h.is_null() {
744        // c:951
745        return addhookdeffunc(h, f); // c:952
746    }
747    1 // c:953
748}
749
750/// Port of `int deletehookdeffunc(Hookdef h, Hookfn f)` from
751/// `Src/module.c:961`.
752///
753/// C body (c:961-973):
754/// ```c
755/// int deletehookdeffunc(Hookdef h, Hookfn f) {
756///     LinkNode p;
757///     for (p = firstnode(h->funcs); p; incnode(p))
758///         if (f == (Hookfn) getdata(p)) {
759///             remnode(h->funcs, p);
760///             return 0;
761///         }
762///     return 1;
763/// }
764/// ```
765pub fn deletehookdeffunc(
766    // c:961
767    h: *mut hookdef,
768    f: Hookfn,
769) -> i32 {
770    unsafe {
771        if (*h).funcs.is_null() {
772            return 1;
773        }
774        let funcs = &mut *(*h).funcs;
775        let f_val = f as usize;
776        // Walk owning chain looking for the matching dat. Splice on hit.
777        let mut prev: &mut Option<Box<linknode>> = &mut funcs.first;
778        loop {
779            match prev {
780                None => return 1, // c:971
781                Some(node) if node.dat == f_val => {
782                    // c:966 f == getdata(p)
783                    let next = node.next.take(); // c:967 remnode
784                    *prev = next;
785                    return 0; // c:968
786                }
787                Some(_) => {
788                    // c:965 incnode(p) — advance.
789                    prev = &mut prev.as_mut().unwrap().next;
790                }
791            }
792        }
793    }
794}
795
796/// Port of `int deletehookfunc(const char *n, Hookfn f)` from
797/// `Src/module.c:977`.
798///
799/// C body (c:977-984):
800/// ```c
801/// int deletehookfunc(const char *n, Hookfn f) {
802///     Hookdef h = gethookdef(n);
803///     if (h) return deletehookdeffunc(h, f);
804///     return 1;
805/// }
806/// ```
807pub fn deletehookfunc(
808    // c:977
809    n: &str,
810    f: Hookfn,
811) -> i32 {
812    let h = gethookdef(n); // c:979 h = gethookdef(n)
813    if !h.is_null() {
814        // c:980
815        return deletehookdeffunc(h, f); // c:981
816    }
817    1 // c:982
818}
819
820/// Port of `int runhookdef(Hookdef h, void *d)` from `Src/module.c:990`.
821///
822/// C body (c:990-1010):
823/// ```c
824/// int runhookdef(Hookdef h, void *d) {
825///     if (empty(h->funcs)) {
826///         if (h->def) return h->def(h, d);
827///         return 0;
828///     } else if (h->flags & HOOKF_ALL) {
829///         LinkNode p; int r;
830///         for (p = firstnode(h->funcs); p; incnode(p))
831///             if ((r = ((Hookfn) getdata(p))(h, d))) return r;
832///         if (h->def) return h->def(h, d);
833///         return 0;
834///     } else
835///         return ((Hookfn) getdata(lastnode(h->funcs)))(h, d);
836/// }
837/// ```
838pub fn runhookdef(
839    // c:990
840    h: *mut hookdef,
841    d: *mut std::ffi::c_void,
842) -> i32 {
843    unsafe {
844        let funcs_ptr = (*h).funcs;
845        let funcs_empty = funcs_ptr.is_null() || (*funcs_ptr).first.is_none(); // c:992 empty()
846        if funcs_empty {
847            // c:992
848            if let Some(def) = (*h).def {
849                // c:993 if (h->def)
850                return def(h, d); // c:994 return h->def(h,d)
851            }
852            return 0; // c:995
853        }
854        if (*h).flags & HOOKF_ALL != 0 {
855            // c:996 h->flags & HOOKF_ALL
856            let mut node = (*funcs_ptr).first.as_ref(); // c:999 firstnode
857            while let Some(n) = node {
858                // c:999 ; p ;
859                let fn_ptr: Hookfn = std::mem::transmute(n.dat); // c:1000 (Hookfn) getdata(p)
860                let r = fn_ptr(h, d); // c:1000 (...)(h, d)
861                if r != 0 {
862                    // c:1000 if ((r = ...))
863                    return r; // c:1001
864                }
865                node = n.next.as_ref(); // c:999 incnode
866            }
867            if let Some(def) = (*h).def {
868                // c:1002 if (h->def)
869                return def(h, d); // c:1003 return h->def(h, d)
870            }
871            return 0; // c:1004
872        }
873        // c:1006 — last fn only.
874        let mut tail = (*funcs_ptr).first.as_ref().expect("non-empty");
875        while let Some(next) = tail.next.as_ref() {
876            tail = next;
877        }
878        let fn_ptr: Hookfn = std::mem::transmute(tail.dat);
879        fn_ptr(h, d) // c:1006
880    }
881}
882
883/// Port of `checkaddparam(const char *nam, int opt_i)` from `Src/module.c:1026`.
884///
885/// C body:
886/// ```c
887/// checkaddparam(const char *nam, int opt_i)
888/// {
889///     Param pm;
890///     if (!(pm = (Param) gethashnode2(paramtab, nam)))
891///         return 0;
892///     if (pm->level || !(pm->node.flags & PM_AUTOLOAD)) {
893///         if (!opt_i || pm->level) {
894///             zwarn("Can't add module parameter `%s': %s",
895///                   nam, pm->level ? "local parameter exists" :
896///                                    "parameter already exists");
897///             return 1;
898///         }
899///         return 2;
900///     }
901///     unsetparam_pm(pm, 0, 1);
902///     return 0;
903/// }
904/// ```
905///
906/// Returns: 0 = OK to add, 1 = error printed, 2 = blocked but `-i`
907/// suppressed warning. `pm->level != 0` means a local param shadows
908/// the name (always errors). `PM_AUTOLOAD` set means the existing
909/// param is an autoload stub the C source unsets to make room.
910///
911/// Checks for an existing param of the same name; if absent → 0
912/// (free to add). If present and not autoloadable → 1 (warn) or
913/// 2 (skip warning under `-i`). If present + autoload-marked →
914/// unset the existing entry and return 0.
915pub fn checkaddparam(nam: &str, opt_i: i32) -> i32 {
916    // c:1026
917    // c:1030 — if (!(pm = gethashnode2(paramtab, nam))) return 0;
918    let pm_clone = {
919        let tab = paramtab().read().expect("paramtab poisoned");
920        tab.get(nam).cloned()
921    };
922    let mut pm = match pm_clone {
923        Some(p) => p,
924        None => return 0,
925    };
926    // c:1033 — if (pm->level || !(pm->node.flags & PM_AUTOLOAD)) {
927    if pm.level != 0 || (pm.node.flags as u32 & PM_AUTOLOAD) == 0 {
928        // c:1042-1048 — if (!opt_i || pm->level) zwarn(...); return 1;
929        if opt_i == 0 || pm.level != 0 {
930            zwarn(&format!(
931                "Can't add module parameter `{}': {}",
932                nam,
933                if pm.level != 0 {
934                    "local parameter exists"
935                } else {
936                    "parameter already exists"
937                }
938            ));
939            return 1;
940        }
941        // c:1049 — return 2;
942        return 2;
943    }
944    // c:1052 — unsetparam_pm(pm, 0, 1);
945    unsetparam_pm(&mut pm, 0, 1);
946    // c:1053 — return 0;
947    0
948}
949
950/// Port of `int addparamdef(Paramdef d)` from `Src/module.c:1061`.
951/// Registers a module-supplied parameter definition into the canonical
952/// `paramtab`, wiring the GSU vtable per `PM_TYPE`. Returns 0 on
953/// success, 1 on error.
954///
955/// ```c
956/// int
957/// addparamdef(Paramdef d)
958/// {
959///     Param pm;
960///     if (checkaddparam(d->name, 0)) return 1;
961///     if (d->getnfn) {
962///         if (!(pm = createspecialhash(d->name, d->getnfn,
963///                                      d->scantfn, d->flags)))
964///             return 1;
965///     }
966///     else if (!(pm = createparam(d->name, d->flags)) &&
967///         !(pm = (Param) paramtab->getnode(paramtab, d->name)))
968///         return 1;
969///     d->pm = pm;
970///     pm->level = 0;
971///     if (d->var) pm->u.data = d->var;
972///     if (d->var || d->gsu) {
973///         switch (PM_TYPE(pm->node.flags)) {
974///         case PM_SCALAR:
975///             if (pm->node.flags & PM_TIED)
976///                 pm->ename = ztrdup(casemodify(pm->node.nam, CASMOD_LOWER));
977///             /* fall-through */
978///         case PM_NAMEREF:
979///             pm->gsu.s = d->gsu ? (GsuScalar)d->gsu : &varscalar_gsu;
980///             break;
981///         case PM_INTEGER:
982///             pm->gsu.i = d->gsu ? (GsuInteger)d->gsu : &varinteger_gsu;
983///             break;
984///         case PM_FFLOAT: case PM_EFLOAT:
985///             pm->gsu.f = d->gsu;
986///             break;
987///         case PM_ARRAY:
988///             if (pm->node.flags & PM_TIED)
989///                 pm->ename = ztrdup(casemodify(pm->node.nam, CASMOD_UPPER));
990///             pm->gsu.a = d->gsu ? (GsuArray)d->gsu : &vararray_gsu;
991///             break;
992///         case PM_HASHED:
993///             if (d->gsu) pm->gsu.h = (GsuHash)d->gsu;
994///             break;
995///         default:
996///             unsetparam_pm(pm, 0, 1);
997///             return 1;
998///         }
999///     }
1000///     return 0;
1001/// }
1002/// ```
1003pub fn addparamdef(d: &mut paramdef) -> i32 {
1004    // c:1061
1005
1006    // c:1065 — `if (checkaddparam(d->name, 0)) return 1;`
1007    if checkaddparam(&d.name, 0) != 0 {
1008        // c:1065
1009        return 1; // c:1066
1010    }
1011
1012    // c:1068-1075 — either createspecialhash (hash params with getnfn)
1013    // or createparam, falling back to gethashnode on collision.
1014    let pm_opt: Option<Param> = if d.getnfn.is_some() {
1015        // c:1068
1016        // c:1069-1071 — createspecialhash(d->name, d->getnfn, d->scantfn, d->flags)
1017        // The Rust createspecialhash takes (name, flags) only; the
1018        // getnfn/scantfn fields aren't yet wired through the typed
1019        // Rust API. Pass flags and let the param be created.
1020        createspecialhash(&d.name, d.flags) // c:1069
1021    } else {
1022        // c:1072
1023        match createparam(&d.name, d.flags) {
1024            // c:1073
1025            Some(p) => Some(p),
1026            None => {
1027                // c:1074 — fall back to paramtab->getnode(paramtab, d->name)
1028                let tab = paramtab().read().ok();
1029                tab.and_then(|t| {
1030                    t.get(&d.name).map(|p| {
1031                        // Clone the existing param so we can mutate the
1032                        // returned handle without holding the read lock.
1033                        let mut clone = p.clone();
1034                        clone.level = 0;
1035                        Box::new(*clone)
1036                    })
1037                })
1038            }
1039        }
1040    };
1041    let mut pm = match pm_opt {
1042        // c:1074-1075
1043        Some(p) => p,
1044        None => return 1,
1045    };
1046
1047    // c:1077-1078 — `d->pm = pm; pm->level = 0;`
1048    pm.level = 0; // c:1078
1049
1050    // c:1079-1080 — `if (d->var) pm->u.data = d->var;`
1051    if d.var != 0 { // c:1079
1052         // pm.u.data is a raw `void *` slot — not yet exposed on the
1053         // Rust param mirror. Carry the assignment as a comment.
1054         // pm.u.data = d->var as *mut _;                                     // c:1080
1055    }
1056
1057    if d.var != 0 || d.gsu != 0 {
1058        // c:1081
1059        let t = PM_TYPE(pm.node.flags as u32); // c:1086
1060        let pmflags = pm.node.flags as u32;
1061        if t == PM_SCALAR || t == PM_NAMEREF {
1062            // c:1087/1091
1063            if t == PM_SCALAR && (pmflags & PM_TIED) != 0 {
1064                // c:1088
1065                let lower = casemodify(&pm.node.nam, CASMOD_LOWER);
1066                pm.ename = Some(ztrdup(&lower)); // c:1089
1067            }
1068            // c:1092 pm->gsu.s = d->gsu ? d->gsu : &varscalar_gsu;
1069            // gsu vtable wireup is opaque (function pointers via usize);
1070            // the Rust param dispatch reads directly from typed accessors.
1071            let _ = d.gsu; // c:1092
1072        } else if t == PM_INTEGER {
1073            // c:1095
1074            let _ = d.gsu; // c:1096
1075        } else if t == PM_FFLOAT || t == PM_EFLOAT {
1076            // c:1099-1100
1077            let _ = d.gsu; // c:1101
1078        } else if t == PM_ARRAY {
1079            // c:1104
1080            if (pmflags & PM_TIED) != 0 {
1081                // c:1105
1082                let upper = casemodify(&pm.node.nam, CASMOD_UPPER);
1083                pm.ename = Some(ztrdup(&upper)); // c:1106
1084            }
1085            let _ = d.gsu; // c:1107
1086        } else if t == PM_HASHED {
1087            // c:1110
1088            let _ = d.gsu; // c:1112-1113
1089        } else {
1090            // c:1116
1091            unsetparam_pm(&mut pm, 0, 1); // c:1117
1092            return 1; // c:1118
1093        }
1094    }
1095
1096    d.pm = Some(pm); // c:1077 d->pm = pm
1097    0 // c:1122
1098}
1099
1100/// Port of `int deleteparamdef(Paramdef d)` from `Src/module.c:1128`.
1101/// Removes a previously-registered module parameter, unwinding any
1102/// hidden-param shadow chain so the matching `d->pm` instance is the
1103/// one actually unset.
1104///
1105/// ```c
1106/// int
1107/// deleteparamdef(Paramdef d)
1108/// {
1109///     Param pm = (Param) paramtab->getnode(paramtab, d->name);
1110///     if (!pm) return 1;
1111///     if (pm != d->pm) {
1112///         Param prevpm, searchpm;
1113///         for (prevpm = pm, searchpm = pm->old;
1114///              searchpm;
1115///              prevpm = searchpm, searchpm = searchpm->old)
1116///             if (searchpm == d->pm) break;
1117///         if (!searchpm) return 1;
1118///         paramtab->removenode(paramtab, pm->node.nam);
1119///         prevpm->old = searchpm->old;
1120///         searchpm->old = pm;
1121///         paramtab->addnode(paramtab, searchpm->node.nam, searchpm);
1122///         pm = searchpm;
1123///     }
1124///     pm->node.flags = (pm->node.flags & ~PM_READONLY) | PM_REMOVABLE;
1125///     unsetparam_pm(pm, 0, 1);
1126///     d->pm = NULL;
1127///     return 0;
1128/// }
1129/// ```
1130pub fn deleteparamdef(d: &mut paramdef) -> i32 {
1131    // c:1128
1132
1133    // c:1131 — `Param pm = (Param) paramtab->getnode(paramtab, d->name);`
1134    let mut pm: Param = {
1135        let tab = paramtab().read();
1136        match tab {
1137            Ok(t) => match t.get(&d.name) {
1138                Some(p) => p.clone(),
1139                None => return 1, // c:1133-1134
1140            },
1141            Err(_) => return 1,
1142        }
1143    };
1144
1145    // c:1135-1156 — shadow-chain unwind: if the live pm isn't d->pm,
1146    // walk pm->old searching for d->pm; if found, splice it out,
1147    // re-add the matching node under its name, and operate on it.
1148    // The Rust param mirror's `old` chain isn't yet wired through
1149    // paramtab; the typed paramtab dispatches the latest binding
1150    // directly. Mirror the C structure so callers see the same
1151    // semantics when the shadow chain lands.
1152    if let Some(expected) = d.pm.as_ref() {
1153        // c:1135
1154        if !std::ptr::eq(pm.as_ref(), expected.as_ref()) {
1155            // c:1135 pm != d->pm
1156            // c:1141-1145 — walk pm->old looking for d->pm.
1157            let mut searchpm = pm.old.clone(); // c:1142
1158            let mut found = false;
1159            while let Some(s) = searchpm {
1160                // c:1142
1161                if std::ptr::eq(s.as_ref(), expected.as_ref()) {
1162                    // c:1144
1163                    found = true; // c:1145
1164                    break;
1165                }
1166                searchpm = s.old.clone(); // c:1143
1167            }
1168            if !found {
1169                // c:1147
1170                return 1; // c:1148
1171            }
1172            // c:1150-1153 — splice searchpm out of the chain and
1173            // re-add it under its node.nam. Without the shadow chain
1174            // wired through paramtab, this is a no-op; the unset
1175            // proceeds against the live pm.
1176        }
1177    }
1178
1179    // c:1157 — `pm->node.flags = (pm->node.flags & ~PM_READONLY) | PM_REMOVABLE;`
1180    pm.node.flags = (pm.node.flags & !(PM_READONLY as i32)) | (PM_REMOVABLE as i32); // c:1157
1181    unsetparam_pm(&mut pm, 0, 1); // c:1158
1182    d.pm = None; // c:1159 d->pm = NULL
1183    0 // c:1160
1184}
1185
1186/// Port of `static int add_autoparam(const char *module, const char *pnam, int flags)`
1187/// from `Src/module.c:1197`.
1188///
1189/// C body c:1197-1228:
1190/// ```c
1191/// static int
1192/// add_autoparam(const char *module, const char *pnam, int flags)
1193/// {
1194///     Param pm;
1195///     int ret;
1196///     int ne = noerrs;
1197///
1198///     queue_signals();
1199///     if ((ret = checkaddparam(pnam, (flags & FEAT_IGNORE)))) {
1200///         unqueue_signals();
1201///         return ret == 2 ? 0 : -1;
1202///     }
1203///     noerrs = 2;
1204///     if ((pm = setsparam(dupstring(pnam), ztrdup(module)))) {
1205///         pm->node.flags |= PM_AUTOLOAD;
1206///         if (flags & FEAT_AUTOALL)
1207///             pm->node.flags |= PM_AUTOALL;
1208///         ret = 0;
1209///     } else
1210///         ret = -1;
1211///     noerrs = ne;
1212///     unqueue_signals();
1213///
1214///     return ret;
1215/// }
1216/// ```
1217///
1218/// Adds an autoload stub for a module parameter: `setsparam` creates
1219/// the param as a scalar with `value=module name`, then OR-in
1220/// `PM_AUTOLOAD` so first access fires `loadparamnode` (c:546) which
1221/// calls `ensurefeature(module, "p:", nam)` to actually load the
1222/// module. `typeset +` listing checks `PM_AUTOLOAD` and emits
1223/// "undefined NAME" instead of the usual type prefix.
1224pub fn add_autoparam(module: &str, pnam: &str, flags: i32) -> i32 {
1225    // c:1197
1226    use crate::ported::exec::noerrs;
1227    use crate::ported::signals::queue_signals;
1228    use std::sync::atomic::Ordering;
1229
1230    // c:1202 — int ne = noerrs;
1231    let ne = noerrs.load(Ordering::Relaxed);
1232
1233    // c:1204 — queue_signals();
1234    queue_signals();
1235
1236    // c:1205 — if ((ret = checkaddparam(pnam, (flags & FEAT_IGNORE)))) {
1237    let ret_check = checkaddparam(pnam, flags & FEAT_IGNORE as i32);
1238    if ret_check != 0 {
1239        // c:1206 — unqueue_signals();
1240        unqueue_signals();
1241        // c:1214 — return ret == 2 ? 0 : -1;
1242        return if ret_check == 2 { 0 } else { -1 };
1243    }
1244
1245    // c:1217 — noerrs = 2;
1246    noerrs.store(2, Ordering::Relaxed);
1247
1248    // c:1218 — if ((pm = setsparam(dupstring(pnam), ztrdup(module))))
1249    let pm_opt = crate::ported::params::setsparam(pnam, module);
1250    let ret = if let Some(mut pm) = pm_opt {
1251        // c:1219 — pm->node.flags |= PM_AUTOLOAD;
1252        pm.node.flags |= PM_AUTOLOAD as i32;
1253        // c:1220-1221 — if (flags & FEAT_AUTOALL) pm->node.flags |= PM_AUTOALL;
1254        if (flags & FEAT_AUTOALL as i32) != 0 {
1255            pm.node.flags |= PM_AUTOALL as i32;
1256        }
1257        // Re-insert the modified clone back into paramtab so the flag
1258        // bits stick (paramtab stores `Param` by value, not by pointer).
1259        {
1260            let mut tab = paramtab().write().expect("paramtab poisoned");
1261            tab.insert(pnam.to_string(), pm);
1262        }
1263        // c:1222 — ret = 0;
1264        0
1265    } else {
1266        // c:1224 — ret = -1;
1267        -1
1268    };
1269
1270    // c:1225 — noerrs = ne;
1271    noerrs.store(ne, Ordering::Relaxed);
1272    // c:1226 — unqueue_signals();
1273    unqueue_signals();
1274    // c:1228 — return ret;
1275    ret
1276}
1277
1278/// Port of `static int del_autoparam(const char *modnam, const char *pnam, int flags)`
1279/// from `Src/module.c:1234`.
1280///
1281/// C body c:1234-1248:
1282/// ```c
1283/// static int
1284/// del_autoparam(UNUSED(const char *modnam), const char *pnam, int flags)
1285/// {
1286///     Param pm = (Param) gethashnode2(paramtab, pnam);
1287///     if (!pm) {
1288///         if (!(flags & FEAT_IGNORE)) return 2;
1289///     } else if (!(pm->node.flags & PM_AUTOLOAD)) {
1290///         if (!(flags & FEAT_IGNORE)) return 3;
1291///     } else
1292///         unsetparam_pm(pm, 0, 1);
1293///     return 0;
1294/// }
1295/// ```
1296///
1297/// Removes a param previously registered by `add_autoparam`. Returns
1298/// 2 when the param doesn't exist, 3 when it exists but isn't an
1299/// autoload stub, 0 on success — `FEAT_IGNORE` masks both error
1300/// returns to 0.
1301pub fn del_autoparam(_modnam: &str, pnam: &str, flags: i32) -> i32 {
1302    // c:1234
1303    // c:1237 — Param pm = (Param) gethashnode2(paramtab, pnam);
1304    let pm_opt = {
1305        let tab = paramtab().read().expect("paramtab poisoned");
1306        tab.get(pnam).cloned()
1307    };
1308    match pm_opt {
1309        // c:1239 — if (!pm) { if (!(flags & FEAT_IGNORE)) return 2; }
1310        None => {
1311            if (flags & FEAT_IGNORE as i32) == 0 {
1312                return 2; // c:1241
1313            }
1314        }
1315        Some(mut pm) => {
1316            if (pm.node.flags as u32 & PM_AUTOLOAD) == 0 {
1317                // c:1242 — else if (!(pm->node.flags & PM_AUTOLOAD))
1318                if (flags & FEAT_IGNORE as i32) == 0 {
1319                    return 3; // c:1244
1320                }
1321            } else {
1322                // c:1246 — else unsetparam_pm(pm, 0, 1);
1323                unsetparam_pm(&mut pm, 0, 1);
1324            }
1325        }
1326    }
1327    0 // c:1248
1328}
1329
1330impl modulestab {
1331    /// `new` — see implementation.
1332    pub fn new() -> Self {
1333        let mut table = Self::default();
1334        table.register_builtin_modules();
1335        table
1336    }
1337
1338    /// Register all statically-compiled modules (replaces dlopen)
1339    fn register_builtin_modules(&mut self) {
1340        let builtin_modules = [
1341            // Module→features MUST match the C BUILTIN() homes or a
1342            // builtin's feature lookup (`b:<name>` against its home module)
1343            // fails with "module `X' has no such feature". These were
1344            // SCRAMBLED: compadd/compset (complete.c:1693-1694) were under
1345            // zsh/computil, while the computil builtins (computil.c:5131-
1346            // 5138) were under zsh/complete, so calling `compset` from a
1347            // shell fn errored `module 'zsh/complete' has no such feature`.
1348            ("zsh/complete", &["compadd", "compset"][..]), // complete.c:1693-1694
1349            ("zsh/complist", &["complist"][..]),
1350            (
1351                "zsh/computil",
1352                &[
1353                    "comparguments", // computil.c:5131
1354                    "compdescribe",  // c:5132
1355                    "compfiles",     // c:5133
1356                    "compgroups",    // c:5134
1357                    "compquote",     // c:5135
1358                    "comptags",      // c:5136
1359                    "comptry",       // c:5137
1360                    "compvalues",    // c:5138
1361                ][..],
1362            ),
1363            ("zsh/datetime", &["output_strftime"][..]),
1364            (
1365                "zsh/files",
1366                &[
1367                    "mkdir", "rmdir", "ln", "mv", "cp", "rm", "chmod", "chown", "sync",
1368                ][..],
1369            ),
1370            ("zsh/langinfo", &[][..]),
1371            ("zsh/mapfile", &[][..]),
1372            ("zsh/mathfunc", &[][..]),
1373            ("zsh/nearcolor", &[][..]),
1374            ("zsh/net/socket", &["zsocket"][..]),
1375            ("zsh/net/tcp", &["ztcp"][..]),
1376            ("zsh/parameter", &[][..]),
1377            (
1378                "zsh/pcre",
1379                &["pcre_compile", "pcre_match", "pcre_study"][..],
1380            ),
1381            ("zsh/regex", &[][..]),
1382            ("zsh/sched", &["sched"][..]),
1383            ("zsh/stat", &["zstat"][..]),
1384            (
1385                "zsh/system",
1386                &[
1387                    "bin_sysread",
1388                    "bin_syswrite",
1389                    "bin_sysopen",
1390                    "bin_sysseek",
1391                    "bin_syserror",
1392                    "zsystem",
1393                ][..],
1394            ),
1395            ("zsh/termcap", &["echotc"][..]),
1396            ("zsh/terminfo", &["echoti"][..]),
1397            ("zsh/watch", &["log"][..]),
1398            ("zsh/zftp", &["zftp"][..]),
1399            ("zsh/zleparameter", &[][..]),
1400            ("zsh/zprof", &["zprof"][..]),
1401            ("zsh/zpty", &["zpty"][..]),
1402            ("zsh/zselect", &["zselect"][..]),
1403            (
1404                "zsh/zutil",
1405                &["zstyle", "zformat", "zparseopts", "zregexparse"][..],
1406            ),
1407            (
1408                "zsh/attr",
1409                &["zgetattr", "zsetattr", "zdelattr", "zlistattr"][..],
1410            ),
1411            ("zsh/cap", &["cap", "getcap", "setcap"][..]),
1412            ("zsh/clone", &["clone"][..]),
1413            ("zsh/curses", &["zcurses"][..]),
1414            ("zsh/db/gdbm", &["ztie", "zuntie", "zgdbmpath"][..]),
1415            ("zsh/param/private", &["private"][..]),
1416            // c:Src/Modules/compctl.c — statically-linked completion
1417            // module providing the compctl/compcall builtins. zsh
1418            // exposes it as autoloadable; `zmodload zsh/compctl`
1419            // succeeds on the running zsh because the symbol is
1420            // baked in. The zshrs auto-load registry (zsh_default_
1421            // loaded at line 1127) references it but the entry was
1422            // missing from this builtin_modules table, so
1423            // try_load_module returned 0 and zmodload failed with
1424            // "failed to load module `zsh/compctl'".
1425            ("zsh/compctl", &["compctl", "compcall"][..]),
1426            // c:Src/Builtins/rlimits.c — limit/ulimit/unlimit are
1427            // baked in. Same gap as zsh/compctl above.
1428            ("zsh/rlimits", &["limit", "ulimit", "unlimit"][..]),
1429            // c:Src/Zle/zle_main.c — zle/vared/bindkey baked in.
1430            ("zsh/zle", &["zle", "vared", "bindkey"][..]),
1431            // c:Src/Modules/example.c — example module that prints
1432            // "The example module has now been set up." on boot;
1433            // statically linked so `zmodload zsh/example` succeeds.
1434            ("zsh/example", &["example"][..]),
1435            // NOT registered (zsh -fc parity probes confirm these
1436            // FAIL to load on this system because the dynamic
1437            // .bundle file doesn't exist):
1438            //   zsh/calendar      — pure dynamic module, no static
1439            //                       linkage in upstream zsh build.
1440            //   zsh/db_gdbm       — underscore alias for zsh/db/gdbm;
1441            //                       on the system zsh tested, neither
1442            //                       form loads (dlopen "no such file").
1443            //   zsh/deltochar     — Zle widget addon; system zsh's
1444            //                       bundle missing the _bindk symbol.
1445            //   zsh/compwid       — compwid bundle missing.
1446            // Letting zshrs zmodload succeed on these names would
1447            // diverge from `zsh -fc` which reports the dlopen error.
1448            // The names appear above only via try_load_module's
1449            // negative path (returns 0 → zmodload prints "failed to
1450            // load module").
1451        ];
1452
1453        // c:Src/init.c::init_bltinmods — C zsh's bltinmods.list is
1454        // generated at build time from Config/installmodules + the
1455        // active Modules/*.mdd files. Homebrew's stock zsh 5.9.1
1456        // binary ships these 14 modules in modulestab from the
1457        // start (verified via `${(@k)modules}` under `zsh -fc`):
1458        //   zsh/compctl zsh/complete zsh/computil zsh/main
1459        //   zsh/param/private zsh/parameter zsh/rlimits zsh/sched
1460        //   zsh/termcap zsh/terminfo zsh/watch zsh/zle
1461        //   zsh/zleparameter zsh/zutil
1462        // The rest (zsh/system, zsh/stat, zsh/zftp, zsh/zpty,
1463        // zsh/zselect, zsh/files, zsh/mapfile, etc.) require
1464        // explicit `zmodload NAME` before `${modules[NAME]}`
1465        // returns "loaded". All entries are registered into
1466        // modulestab so `zmodload NAME` can resolve them, but
1467        // entries OUTSIDE the default-loaded set carry `MOD_UNLOAD`
1468        // at init so `getpmmodule`'s is_loaded() check
1469        // (MOD_LINKED && !MOD_UNLOAD per zsh_h::is_loaded c:962)
1470        // returns false until `zmodload` clears the MOD_UNLOAD bit.
1471        // Bugs #530/#532/#535 in docs/BUGS.md.
1472        let zsh_default_loaded: &[&str] = &[
1473            "zsh/compctl",
1474            "zsh/complete",
1475            "zsh/computil",
1476            "zsh/main",
1477            "zsh/param/private",
1478            "zsh/parameter",
1479            "zsh/rlimits",
1480            "zsh/sched",
1481            "zsh/termcap",
1482            "zsh/terminfo",
1483            "zsh/watch",
1484            "zsh/zle",
1485            "zsh/zleparameter",
1486            "zsh/zutil",
1487        ];
1488        for (name, _builtins) in &builtin_modules {
1489            // C zsh tracks builtin→module mapping in `builtintab` (the
1490            // canonical hashtable), not on a per-module ledger. We
1491            // just register the module here; the builtins themselves
1492            // come in via the canonical table in `cmd.rs`.
1493            let mut module = module::new(name);
1494            if !zsh_default_loaded.contains(name) {
1495                // Mark as registered-but-not-loaded so
1496                // ${modules[NAME]} reads as unset until zmodload.
1497                module.node.flags |= crate::ported::zsh_h::MOD_UNLOAD;
1498            }
1499            self.modules.insert(name.to_string(), module);
1500        }
1501        // c:Src/init.c:1708 init_bltinmods — run per-module `boot_`
1502        // for each statically-linked default-loaded module so paramtab
1503        // entries (e.g. `watch`/`WATCH` from zsh/watch, c:734) get
1504        // installed without an explicit zmodload. Without this,
1505        // `${(t)watch}` returns empty instead of `array-special`
1506        // even though zsh treats zsh/watch as effectively part of
1507        // the shell. Bug #270 in docs/BUGS.md. Keep the modules at
1508        // their MOD_LINKED-without-MOD_INIT_B initial state so the
1509        // `zmodload` (no-args) listing — gated on MOD_INIT_B per
1510        // bug #76 — still shows only `zsh/main`.
1511        //
1512        // boot_ honours --zsh parity mode internally — the
1513        // partab registration always runs (so `$+WATCH`/`$+watch`/
1514        // `${(t)WATCHFMT}` report the same shape as zsh -fc) but
1515        // the WATCHFMT/LOGCHECK default-value seeding is skipped
1516        // when IS_ZSH_MODE is set, matching zsh -fc where the
1517        // names are declared but empty until `zmodload zsh/watch`.
1518        for name in zsh_default_loaded {
1519            #[allow(clippy::single_match)]
1520            match *name {
1521                "zsh/watch" => {
1522                    crate::ported::modules::watch::boot_(std::ptr::null());
1523                }
1524                // c:Src/Zle/compctl.c:4016 setup_ — seed the hardwired
1525                // default compctls (cc_compos/cc_default/cc_first) the
1526                // bare shell uses for command/file completion. Runs here
1527                // at module registration (before any rc file), so it
1528                // never clobbers a user's later `compctl` definitions.
1529                // Without it CC_COMPOS is unset and `l<Tab>` in `zsh -f`
1530                // produced no matches.
1531                "zsh/compctl" => {
1532                    crate::ported::zle::compctl::setup_();
1533                }
1534                _ => {}
1535            }
1536        }
1537
1538        // c:Src/init.c:1708 init_bltinmods + Config/installmodules —
1539        // canonical auto-load builtin→module bindings reported by
1540        // `zmodload -a` (no args). The 27 entries match
1541        // `/opt/homebrew/bin/zsh -fc 'zmodload -a'` exactly. NOT the
1542        // same as the full module→builtin index above: `zsh/files`
1543        // builtins (mkdir, rm, etc.) are statically linked but NOT
1544        // in the auto-load registry (zsh requires explicit
1545        // `zmodload zsh/files` to get them). Bug #222.
1546        let autoload_pairs: &[(&str, &str)] = &[
1547            ("bindkey", "zsh/zle"),
1548            ("compadd", "zsh/complete"),
1549            ("comparguments", "zsh/computil"),
1550            ("compcall", "zsh/compctl"),
1551            ("compctl", "zsh/compctl"),
1552            ("compdescribe", "zsh/computil"),
1553            ("compfiles", "zsh/computil"),
1554            ("compgroups", "zsh/computil"),
1555            ("compquote", "zsh/computil"),
1556            ("compset", "zsh/complete"),
1557            ("comptags", "zsh/computil"),
1558            ("comptry", "zsh/computil"),
1559            ("compvalues", "zsh/computil"),
1560            ("echotc", "zsh/termcap"),
1561            ("echoti", "zsh/terminfo"),
1562            ("limit", "zsh/rlimits"),
1563            ("log", "zsh/watch"),
1564            ("private", "zsh/param/private"),
1565            ("sched", "zsh/sched"),
1566            ("ulimit", "zsh/rlimits"),
1567            ("unlimit", "zsh/rlimits"),
1568            ("vared", "zsh/zle"),
1569            ("zformat", "zsh/zutil"),
1570            ("zle", "zsh/zle"),
1571            ("zparseopts", "zsh/zutil"),
1572            ("zregexparse", "zsh/zutil"),
1573            ("zstyle", "zsh/zutil"),
1574        ];
1575        for (b, m) in autoload_pairs {
1576            self.autoload_builtins
1577                .insert((*b).to_string(), (*m).to_string());
1578        }
1579
1580        // c:Src/init.c:1708 — `init_bltinmods` ends with
1581        // `load_module("zsh/main", NULL, 0)`. `zsh/main` is the
1582        // always-loaded master module: every zsh process has it in
1583        // `modulestab` from boot, with `m->u.handle` (or `u.linked`)
1584        // non-NULL so `printmodulenode`'s "loaded" gate (c:218
1585        // `m->u.handle`) fires. Register here with `MOD_INIT_B` set
1586        // so `zmodload` (no args) lists `zsh/main` and ONLY `zsh/main`
1587        // — matching `/opt/homebrew/bin/zsh -fc 'zmodload'` output
1588        // exactly. Bug #76 in docs/BUGS.md.
1589        let mut main = module::new("zsh/main");
1590        main.node.flags |= crate::ported::zsh_h::MOD_INIT_B | MOD_INIT_S; // c:2244
1591                                                                          // c:2289 — load_module("zsh/main") assigns `m->u.linked`; the
1592                                                                          // union must be non-NULL so `zmodload -e zsh/main` (c:2637
1593                                                                          // `!m->u.handle`) reports the master module as loaded.
1594        main.linked = Some(Box::new(linkedmod {
1595            name: "zsh/main".to_string(),
1596            setup: None,
1597            features: None,
1598            enables: None,
1599            boot: None,
1600            cleanup: None,
1601            finish: None,
1602        }));
1603        self.modules.insert("zsh/main".to_string(), main);
1604    }
1605
1606    // Returns 0 success, 1 complete failure, 2 partial-features-fail.     // c:2200-2201
1607    /// Port of `int load_module(char const *name, Feature_enables
1608    /// enablesarr, int silent)` from `Src/module.c:2206`. C body:
1609    /// validate name with `modname_ok`, queue_signals(), resolve alias
1610    /// chain via `find_module(FINDMOD_ALIASP)`. If not found, attempt
1611    /// `module_linked` then `do_load_module` (dlopen). Allocate a new
1612    /// `Module`, set `MOD_SETUP` (+ `MOD_LINKED` if statically linked),
1613    /// add to `modulestab`, run `setup_module`/`do_boot_module`. If
1614    /// either fails, cleanup+finish+delete and return 1. Else clear
1615    /// `MOD_SETUP`, set `MOD_INIT_S | MOD_INIT_B`, return `bootret`.
1616    /// If found and already `MOD_SETUP`, return 0. Detect circular
1617    /// deps via `MOD_BUSY`, load dependency list recursively. zshrs:
1618    /// all modules are statically linked, so dlopen path is skipped
1619    /// and we operate on the static registry.
1620    /// WARNING: param names don't match C — Rust=(name) vs C=(name, enablesarr, silent)
1621    pub fn load_module(&mut self, name: &str) -> bool {
1622        // c:2200
1623        // Faithful port of the find_module-found branch (c:2249-2320).
1624        // The !find_module branch (c:2219-2247) requires DSO loading and
1625        // never fires in zshrs's static-link path: every linked module
1626        // is pre-registered by register_builtin_modules.
1627        //
1628        // C body c:2249-2320:
1629        //   if (m->flags & MOD_SETUP) return 0;
1630        //   if (m->flags & MOD_UNLOAD) m->flags &= ~MOD_UNLOAD;
1631        //   else if (m->u.linked / m->u.handle) return 0;
1632        //   if (m->flags & MOD_BUSY) {
1633        //       zerr("circular dependencies for module ;%s", name);
1634        //       return 1;
1635        //   }
1636        //   m->flags |= MOD_BUSY;
1637        //   if (m->deps) for each dep: load_module(dep, NULL, silent);
1638        //   m->flags &= ~MOD_BUSY;
1639        //   if (!m->u.handle) {
1640        //       module_linked / do_load_module
1641        //       if (handle) m->u.handle = handle, m->flags |= MOD_SETUP
1642        //       else m->u.linked = linked, m->flags |= MOD_SETUP|MOD_LINKED
1643        //       if (setup_module(m)) { finish, NULL handles, ~SETUP, return 1; }
1644        //       m->flags |= MOD_INIT_S;
1645        //   }
1646        //   m->flags |= MOD_SETUP;
1647        //   if ((bootret = do_boot_module(m, enablesarr, silent)) == 1) {
1648        //       do_cleanup_module(m); finish_module(m);
1649        //       m->u.linked/handle = NULL;
1650        //       m->flags &= ~MOD_SETUP;
1651        //       return 1;
1652        //   }
1653        //   m->flags |= MOD_INIT_B;
1654        //   m->flags &= ~MOD_SETUP;
1655        //   return bootret;
1656
1657        // c:2208 — modname_ok(name)
1658        if modname_ok(name) == 0 {
1659            // c:2210 — zerr if !silent (silent flag not threaded yet)
1660            return false;
1661        }
1662        crate::ported::signals::queue_signals(); // c:2218
1663                                                 // c:2219 — find_module(name, FINDMOD_ALIASP)
1664        if !self.modules.contains_key(name) {
1665            // c:2219 — !m branch: static-link path can't dlopen.
1666            unqueue_signals(); // c:2222
1667            return false; // c:2223 return 1
1668        }
1669
1670        // c:2249 — if (MOD_SETUP) return 0;
1671        let flags = self.modules.get(name).unwrap().node.flags;
1672        if (flags & MOD_SETUP) != 0 {
1673            unqueue_signals(); // c:2250
1674            return true; // c:2251 return 0
1675        }
1676        // c:2253-2257 —
1677        //   if (m->node.flags & MOD_UNLOAD)
1678        //       m->node.flags &= ~MOD_UNLOAD;
1679        //   else if ((m->node.flags & MOD_LINKED) ? m->u.linked
1680        //                                         : m->u.handle) {
1681        //       unqueue_signals();
1682        //       return 0;
1683        //   }
1684        // The union read is real now: load assigns m.linked at c:2289,
1685        // so already-loaded modules early-return here while
1686        // pre-registered-but-unloaded ones (linked: None) fall through.
1687        if (flags & MOD_UNLOAD) != 0 {
1688            self.modules.get_mut(name).unwrap().node.flags &= !MOD_UNLOAD;
1689        } else if {
1690            let m = self.modules.get(name).unwrap();
1691            if (flags & MOD_LINKED) != 0 {
1692                m.linked.is_some() // c:2255 m->u.linked
1693            } else {
1694                m.handle.is_some() // c:2255 m->u.handle
1695            }
1696        } {
1697            unqueue_signals(); // c:2256
1698            return true; // c:2257 return 0
1699        }
1700        // c:2259-2262 — circular-dependency detection.
1701        if (flags & MOD_BUSY) != 0 {
1702            unqueue_signals(); // c:2260
1703            crate::ported::utils::zerr(&format!("circular dependencies for module ;{}", name));
1704            return false; // c:2262 return 1
1705        }
1706        self.modules.get_mut(name).unwrap().node.flags |= MOD_BUSY; // c:2264
1707
1708        // c:2269-2277 — recurse into m->deps.
1709        let deps_snapshot: Vec<String> = self
1710            .modules
1711            .get(name)
1712            .and_then(|m| m.deps.as_ref())
1713            .map(|d| d.iter().cloned().collect())
1714            .unwrap_or_default();
1715        for dep in &deps_snapshot {
1716            if !self.load_module(dep) {
1717                // c:2272 — return 1 on dep failure
1718                self.modules.get_mut(name).unwrap().node.flags &= !MOD_BUSY; // c:2273
1719                unqueue_signals(); // c:2274
1720                return false; // c:2275 return 1
1721            }
1722        }
1723        self.modules.get_mut(name).unwrap().node.flags &= !MOD_BUSY; // c:2278
1724
1725        // c:2279-2304 — `if (!m->u.handle)` setup branch. The union
1726        // read means "no live binding yet" — neither handle (DSO) nor
1727        // linked (static) assigned. A deferred-unload reload re-enters
1728        // with m.linked still set and skips straight to boot, like C.
1729        let needs_setup = {
1730            let m = self.modules.get(name).unwrap();
1731            m.handle.is_none() && m.linked.is_none() // c:2279 !m->u.handle
1732        };
1733        if needs_setup {
1734            // c:2281 — `linked = module_linked(name)`: every zshrs
1735            // module is statically linked, so the lookup always hits;
1736            // callbacks dispatch by name (839f32249b), the record
1737            // carries the name like C's linkedmod.
1738            // c:2289-2291 — `m->u.linked = linked;
1739            //                m->node.flags |= MOD_SETUP | MOD_LINKED;`
1740            if let Some(m) = self.modules.get_mut(name) {
1741                m.linked = Some(Box::new(linkedmod {
1742                    name: name.to_string(),
1743                    setup: None,
1744                    features: None,
1745                    enables: None,
1746                    boot: None,
1747                    cleanup: None,
1748                    finish: None,
1749                }));
1750                m.node.flags |= MOD_SETUP | MOD_LINKED;
1751            }
1752            // c:2293 — setup_module(m). Routes through the dispatcher
1753            // (839f32249b) to per-module setup_(m). Most modules return
1754            // 0; some initialise module-private state.
1755            if setup_module(self, name) != 0 {
1756                // c:2294-2301 — failure: finish, clear handles, return 1.
1757                //   else m->u.linked = NULL;  (c:2298)
1758                let _ = finish_module(self, name);
1759                if let Some(m) = self.modules.get_mut(name) {
1760                    m.linked = None; // c:2298
1761                    m.node.flags &= !MOD_SETUP;
1762                }
1763                unqueue_signals(); // c:2300
1764                return false; // c:2301 return 1
1765            }
1766            // c:2303 — `m->flags |= MOD_INIT_S;`
1767            self.modules.get_mut(name).unwrap().node.flags |= MOD_INIT_S;
1768        }
1769        // c:2305 — `m->flags |= MOD_SETUP;`
1770        self.modules.get_mut(name).unwrap().node.flags |= MOD_SETUP;
1771
1772        // c:Src/Modules/system.c:902,904 + zsh/mapfile — SPECIALPMDEF
1773        // entries get added to paramtab via the module's feature
1774        // dispatch (enables_ → handlefeatures → addparam). zshrs's
1775        // vm_helper::init_partab_params skips zmodload-gated names to
1776        // avoid them appearing before explicit load (bug #69). Re-seed
1777        // here once boot completes. This is a Rust-only bridge — no
1778        // direct C counterpart since C's handlefeatures runs implicitly
1779        // inside do_boot_module → enables_module.
1780        for nm in crate::vm_helper::module_gated_params_for(name) {
1781            crate::vm_helper::seed_partab_param(nm);
1782        }
1783
1784        // c:2306 — `bootret = do_boot_module(m, enablesarr, silent);`
1785        // The Rust do_boot_module routes through boot_module dispatcher
1786        // (b474b62898) to the per-module boot_(m) — the real partab
1787        // and bintab installations land here.
1788        let bootret = do_boot_module(self, name, None, 0);
1789        if bootret == 1 {
1790            // c:2306-2315 — boot failure: cleanup + finish + clear, return 1.
1791            //   else m->u.linked = NULL;  (c:2312)
1792            let _ = cleanup_module(self, name);
1793            let _ = finish_module(self, name);
1794            if let Some(m) = self.modules.get_mut(name) {
1795                m.linked = None; // c:2312
1796                m.node.flags &= !MOD_SETUP;
1797            }
1798            unqueue_signals(); // c:2314
1799            return false; // c:2315 return 1
1800        }
1801        // c:2317-2318 — `m->flags |= MOD_INIT_B; m->flags &= ~MOD_SETUP;`
1802        if let Some(m) = self.modules.get_mut(name) {
1803            m.node.flags |= MOD_INIT_B;
1804            m.node.flags &= !MOD_SETUP;
1805        }
1806        unqueue_signals(); // c:2319
1807        true // c:2320 return bootret (0)
1808    }
1809
1810    // Backend handler for zmodload -u                                       // c:2813
1811    /// Port of `int unload_module(Module m)` from `Src/module.c:2817`.
1812    /// C body: resolve `MOD_ALIAS` via `find_module(FINDMOD_ALIASP)`;
1813    /// if `MOD_INIT_S` set and !`MOD_UNLOAD`, call `do_cleanup_module`.
1814    /// Clear `MOD_INIT_B|MOD_INIT_S`. If a `wrapper` is present, set
1815    /// `MOD_UNLOAD` and bail (deferred). Else clear `MOD_UNLOAD` and
1816    /// call `m->u.linked->finish(m)` or `finish_module(m)` depending
1817    /// on `MOD_LINKED`. Finally walk `m->deps` and unload modules
1818    /// that were tagged `MOD_UNLOAD` when the last dependent dies.
1819    /// WARNING: param names don't match C — Rust=(name) vs C=(m)
1820    pub fn unload_module(&mut self, name: &str) -> bool {
1821        // c:2812 — faithful port of the static-link control flow.
1822        // C body c:2818-2913:
1823        //   if (m->flags & MOD_ALIAS) { resolve via find_module }
1824        //   if (MOD_INIT_S && !MOD_UNLOAD && do_cleanup_module(m))
1825        //       return 1;
1826        //   m->flags &= ~(MOD_INIT_B | MOD_INIT_S);
1827        //   del = m->flags & MOD_UNLOAD;
1828        //   if (m->wrapper) { m->flags |= MOD_UNLOAD; return 0; }
1829        //   m->flags &= ~MOD_UNLOAD;
1830        //   if (MOD_LINKED) { if (u.linked) { u.linked->finish(m);
1831        //                                     u.linked = NULL; } }
1832        //   else { if (u.handle) { finish_module(m);
1833        //                          u.handle = NULL; } }
1834        //   if (del && m->deps) { /* deferred dep walk */ }
1835        //   if (m->autoloads && firstnode(m->autoloads))
1836        //       autofeatures("zsh", name, hlinklist2array(autoloads),
1837        //                    0, FEAT_IGNORE);
1838        //   else if (!m->deps) delete_module(m);
1839        //   return 0;
1840
1841        // c:2819-2823 — alias resolve. Mutate the lookup name, not
1842        // the live record (alias chases the target).
1843        let mut target_name = name.to_string();
1844        let needs_alias_chase = self
1845            .modules
1846            .get(&target_name)
1847            .map(|m| (m.node.flags & MOD_ALIAS) != 0)
1848            .unwrap_or(false);
1849        if needs_alias_chase {
1850            target_name = match self.modules.get(name).and_then(|m| m.alias.clone()) {
1851                Some(a) => a,
1852                None => return false, // c:2821-2822 — alias target gone.
1853            };
1854        }
1855
1856        // c:2831-2834 — cleanup if booted.
1857        let (init_s, unload_flag) = match self.modules.get(&target_name) {
1858            Some(m) => (
1859                (m.node.flags & MOD_INIT_S) != 0,
1860                (m.node.flags & MOD_UNLOAD) != 0,
1861            ),
1862            None => return false, // c:2820-2822 fall-through to !m
1863        };
1864        if init_s && !unload_flag {
1865            // c:2833 — `do_cleanup_module` is `cleanup_module` for the
1866            // MOD_LINKED branch (static-link path always).
1867            if cleanup_module(self, &target_name) != 0 {
1868                return false; // c:2834 cleanup error
1869            }
1870        }
1871
1872        // c:2835 — `m->flags &= ~(MOD_INIT_B | MOD_INIT_S);`
1873        if let Some(m) = self.modules.get_mut(&target_name) {
1874            m.node.flags &= !(MOD_INIT_B | MOD_INIT_S);
1875        }
1876
1877        // c:2837 — `del = m->flags & MOD_UNLOAD;`
1878        let del = unload_flag;
1879
1880        // c:2839-2842 — wrapper deferred-unload path.
1881        let has_wrapper = self
1882            .modules
1883            .get(&target_name)
1884            .map(|m| m.wrapper != 0)
1885            .unwrap_or(false);
1886        if has_wrapper {
1887            if let Some(m) = self.modules.get_mut(&target_name) {
1888                m.node.flags |= MOD_UNLOAD; // c:2840
1889            }
1890            return true; // c:2841
1891        }
1892
1893        // c:2843 — `m->flags &= ~MOD_UNLOAD;`
1894        if let Some(m) = self.modules.get_mut(&target_name) {
1895            m.node.flags &= !MOD_UNLOAD;
1896        }
1897
1898        // c:2849-2859 — finish hook:
1899        //   if (m->node.flags & MOD_LINKED) {
1900        //       if (m->u.linked) {
1901        //           m->u.linked->finish(m);
1902        //           m->u.linked = NULL;
1903        //       }
1904        //   }
1905        // Static-link branch routes through finish_module which
1906        // dispatches to the per-module finish_(m) (839f32249b), gated
1907        // and cleared exactly like C's u.linked.
1908        let was_linked = self
1909            .modules
1910            .get(&target_name)
1911            .map(|m| m.linked.is_some())
1912            .unwrap_or(false);
1913        if was_linked {
1914            let _ = finish_module(self, &target_name); // c:2851
1915            if let Some(m) = self.modules.get_mut(&target_name) {
1916                m.linked = None; // c:2852
1917            }
1918        }
1919
1920        // c:2861-2902 — deferred dep walk: when del was set, find every
1921        // dep that has MOD_UNLOAD and check no other live module
1922        // depends on it, then recursively unload.
1923        if del {
1924            let deps_snapshot: Vec<String> = self
1925                .modules
1926                .get(&target_name)
1927                .and_then(|m| m.deps.as_ref())
1928                .map(|d| d.iter().cloned().collect())
1929                .unwrap_or_default();
1930            for dep_name in deps_snapshot {
1931                let dm_target = match find_module(self, &dep_name, FINDMOD_ALIASP) {
1932                    Some(n) => n,
1933                    None => continue, // c:2867 dm == NULL
1934                };
1935                let dm_unloading = self
1936                    .modules
1937                    .get(&dm_target)
1938                    .map(|m| (m.node.flags & MOD_UNLOAD) != 0)
1939                    .unwrap_or(false);
1940                if !dm_unloading {
1941                    continue; // c:2870-2871
1942                }
1943                // c:2872-2897 — scan every other module's deps for
1944                // dm_target.nam; bail if any live module still
1945                // depends on it.
1946                let still_depended = self.modules.iter().any(|(other_name, other)| {
1947                    if other_name == &target_name {
1948                        return false; // c:2884 don't scan ourselves
1949                    }
1950                    let other_deps = match other.deps.as_ref() {
1951                        Some(d) => d,
1952                        None => return false, // c:2884 no deps to scan
1953                    };
1954                    // c:2887-2889 — only scan live modules (MOD_LINKED
1955                    // set + !MOD_UNLOAD in zshrs's static-link path).
1956                    if (other.node.flags & MOD_LINKED) == 0 || (other.node.flags & MOD_UNLOAD) != 0
1957                    {
1958                        return false;
1959                    }
1960                    other_deps.iter().any(|d| d == &dm_target)
1961                });
1962                if !still_depended {
1963                    // c:2898-2899 — `unload_module(dm)` recursive.
1964                    self.unload_module(&dm_target);
1965                }
1966            }
1967        }
1968
1969        // c:2903-2912 — autoload restore OR delete_module.
1970        let (has_autoloads, has_deps, autoloads) = match self.modules.get(&target_name) {
1971            Some(m) => (
1972                m.autoloads.as_ref().map(|a| !a.is_empty()).unwrap_or(false),
1973                m.deps.is_some(),
1974                m.autoloads
1975                    .as_ref()
1976                    .map(|a| a.iter().cloned().collect::<Vec<_>>())
1977                    .unwrap_or_default(),
1978            ),
1979            None => (false, false, Vec::new()),
1980        };
1981        if has_autoloads {
1982            // c:2908-2909 — autofeatures("zsh", m->nam, ..., 0, FEAT_IGNORE)
1983            autofeatures(self, "zsh", Some(&target_name), &autoloads, 0, FEAT_IGNORE);
1984        } else if !has_deps {
1985            // c:2910-2911 — delete_module(m). Inline since the free fn
1986            // also calls remove + drop.
1987            self.modules.remove(&target_name);
1988        }
1989        true // c:2913 return 0
1990    }
1991
1992    /// Check if module is loaded
1993    pub fn is_loaded(&self, name: &str) -> bool {
1994        self.modules
1995            .get(name)
1996            .map(|m| m.is_loaded())
1997            .unwrap_or(false)
1998    }
1999
2000    /// Whether a module is BOUND — its setup_/boot_ has actually run.
2001    /// Mirrors the `zmodload -e` existence test (bin_zmodload_exist,
2002    /// c:Src/module.c:2637): the union slot `m->u.handle` is non-NULL
2003    /// (handle for dlopen, linked for a static module) and the module is
2004    /// not mid-unload. Distinct from `is_loaded()` / `module_loaded()`,
2005    /// which key off `MOD_LINKED` — a flag `register_builtin_modules`
2006    /// pre-seeds for every statically-compiled module at startup, so it
2007    /// is true before `zmodload` ever runs. Use this for "was the module
2008    /// actually loaded by the user" gates.
2009    pub fn is_bound(&self, name: &str) -> bool {
2010        self.modules
2011            .get(name)
2012            .map(|m| (m.handle.is_some() || m.linked.is_some()) && (m.node.flags & MOD_UNLOAD) == 0)
2013            .unwrap_or(false)
2014    }
2015
2016    /// List all loaded modules
2017    pub fn list_loaded(&self) -> Vec<&str> {
2018        self.modules
2019            .iter()
2020            .filter(|(_, m)| m.is_loaded())
2021            .map(|(name, _)| name.as_str())
2022            .collect()
2023    }
2024
2025    /// List all modules (including unloaded). Returns name + raw
2026    /// `MOD_*` flag bits — caller can inspect `MOD_UNLOAD` / `MOD_LINKED`
2027    /// directly (matches C, which exposes `m->node.flags`).
2028    pub fn list_all(&self) -> Vec<(&str, i32)> {
2029        self.modules
2030            .iter()
2031            .map(|(name, m)| (name.as_str(), m.node.flags))
2032            .collect()
2033    }
2034
2035    // ------- Builtin management (from module.c addbuiltin/deletebuiltin) -------
2036
2037    /// Register a builtin (from module.c addbuiltin)
2038    /// Port of `addbuiltin(Builtin b)` from `Src/module.c:409`.
2039    /// WARNING: param names don't match C — Rust=(name, module) vs C=(b)
2040    ///
2041    /// In C, this inserts the builtin into the canonical `builtintab`
2042    /// hashtable (Src/builtin.c). The real builtin registration lives
2043    /// in `cmd.rs::BUILTINTAB` and the routing fn here delegates to
2044    /// the free `addbuiltin` (c:303 port) so the canonical BINF_ADDED
2045    /// clash gate fires.
2046    ///
2047    /// Returns 0 on success, 1 on clash (per C's signature).
2048    pub fn addbuiltin(&mut self, name: &str, module: &str) -> i32 {
2049        // c:409
2050        // Construct a probe builtin matching what add_autobin /
2051        // setbuiltins build; route through the free addbuiltin which
2052        // probes createbuiltintable() for BINF_ADDED.
2053        let mut bn = builtin {
2054            node: hashnode {
2055                next: None,
2056                nam: name.to_string(),
2057                flags: 0,
2058            },
2059            handlerfunc: None,
2060            minargs: 0,
2061            maxargs: 0,
2062            funcid: 0,
2063            optstr: Some(module.to_string()),
2064            defopts: None,
2065        };
2066        if addbuiltin(&mut bn) != 0 {
2067            return 1; // c:417 clash
2068        }
2069        // On success: record in added_builtins ledger so the
2070        // module's setbuiltins delete path can find this entry.
2071        self.added_builtins.insert(name.to_string(), BINF_ADDED);
2072        0 // c:417 OK
2073    }
2074
2075    /// Unregister a builtin (from module.c deletebuiltin)
2076    /// Port of `deletebuiltin(const char *nam)` from `Src/module.c:449`.
2077    /// Returns 0 on success (entry found + removed from ledger), -1
2078    /// on miss — matching the canonical free fn (00e6a9ce7e) and
2079    /// C's deletebuiltin return contract.
2080    /// WARNING: param names don't match C — Rust=(name, module) vs C=(nam)
2081    pub fn deletebuiltin(&mut self, name: &str, _module: &str) -> i32 {
2082        // c:449
2083        // Route through the free deletebuiltin for the canonical
2084        // present/absent probe.
2085        let r = deletebuiltin(name);
2086        if r == 0 {
2087            // Drop the ledger entry so setbuiltins's `already_added`
2088            // probe sees this name as removed.
2089            self.added_builtins.remove(name);
2090        }
2091        r
2092    }
2093
2094    /// Register autoloading builtin.
2095    /// Port of `static int add_autobin(const char *module, const char *bnam,
2096    /// int flags)` from `Src/module.c:426`. C allocates a `Builtin` node
2097    /// with `optstr=module`, sets `BINF_AUTOALL` if `FEAT_AUTOALL` is in
2098    /// `flags`, then calls `addbuiltin(bn)`. On failure, the freshly
2099    /// allocated node is freed; success returns 0, conflict returns 1
2100    /// unless `FEAT_IGNORE` masks it.
2101    /// WARNING: param names don't match C — Rust=(name, module, flags) vs C=(module, bnam, flags)
2102    pub fn add_autobin(&mut self, name: &str, module: &str, flags: i32) -> i32 {
2103        // c:426
2104        // Faithful port of c:426-441:
2105        //   Builtin bn = zshcalloc(sizeof(*bn));
2106        //   bn->node.nam = ztrdup(bnam);
2107        //   bn->optstr = ztrdup(module);
2108        //   if (flags & FEAT_AUTOALL)
2109        //       bn->node.flags |= BINF_AUTOALL;
2110        //   if ((ret = addbuiltin(bn))) {
2111        //       builtintab->freenode(&bn->node);
2112        //       if (!(flags & FEAT_IGNORE))
2113        //           return 1;
2114        //   }
2115        //   return 0;
2116        //
2117        // Prior port did a ledger-only insert into autoload_builtins
2118        // without ever calling the canonical addbuiltin. Now constructs
2119        // the builtin struct and routes through the free addbuiltin
2120        // (c:303 port) which probes createbuiltintable() for the
2121        // BINF_ADDED clash gate. Ledger entry preserved on success so
2122        // resolve_autoload_builtin() / del_autobin's fast path can
2123        // find it without re-scanning.
2124        let node_flags: i32 = if (flags & FEAT_AUTOALL as i32) != 0 {
2125            BINF_AUTOALL as i32 // c:435
2126        } else {
2127            0
2128        };
2129        let mut bn = builtin {
2130            // c:431-433 — zshcalloc + populate.
2131            node: hashnode {
2132                next: None,
2133                nam: name.to_string(),
2134                flags: node_flags,
2135            },
2136            handlerfunc: None,
2137            minargs: 0,
2138            maxargs: 0,
2139            funcid: 0,
2140            optstr: Some(module.to_string()),
2141            defopts: None,
2142        };
2143        // c:436 — `if ((ret = addbuiltin(bn)))`. Clash gate via the
2144        // canonical builtintab.
2145        if addbuiltin(&mut bn) != 0 {
2146            // c:437 — freenode drops the input via Rust's Drop.
2147            if (flags & FEAT_IGNORE as i32) == 0 {
2148                return 1; // c:439
2149            }
2150            // c:440 — FEAT_IGNORE masks → fall through to ret 0 but
2151            // don't insert into the ledger (the canonical table has
2152            // this name already).
2153            return 0;
2154        }
2155        // c:441 success path: register in the autoload ledger.
2156        self.autoload_builtins
2157            .insert(name.to_string(), module.to_string());
2158        0
2159    }
2160
2161    // Remove an autoloaded added by add_autobin                             // c:464
2162    /// Port of `static int del_autobin(const char *module, const char *bnam,
2163    /// int flags)` from `Src/module.c:464`.
2164    ///
2165    /// C body (c:464-478):
2166    /// ```c
2167    /// Builtin bn = (Builtin) builtintab->getnode2(builtintab, bnam);
2168    /// if (!bn) { if(!(flags & FEAT_IGNORE)) return 2; }
2169    /// else if (bn->node.flags & BINF_ADDED) {
2170    ///     if (!(flags & FEAT_IGNORE)) return 3;
2171    /// } else deletebuiltin(bnam);
2172    /// return 0;
2173    /// ```
2174    ///
2175    /// 2 = "no such builtin", 3 = "real registered builtin (BINF_ADDED) —
2176    /// can't unload", 0 = success (removed autoload entry).
2177    /// FEAT_IGNORE masks both error returns.
2178    ///
2179    /// zshrs architecture: `builtintab` is static-linked at startup
2180    /// (`createbuiltintable()` builds an immutable HashMap), so every
2181    /// entry there is effectively BINF_ADDED. The autoload-only entries
2182    /// live in `self.autoload_builtins`. The faithful mapping:
2183    ///   * if name is in `builtintab` → BINF_ADDED → return 3 (or 0 with
2184    ///     FEAT_IGNORE).
2185    ///   * else if name is in `autoload_builtins` → remove it, return 0.
2186    ///   * else → not present → return 2 (or 0 with FEAT_IGNORE).
2187    /// WARNING: param names don't match C — Rust=(name, flags) vs C=(module, bnam, flags)
2188    pub fn del_autobin(&mut self, name: &str, flags: i32) -> i32 {
2189        // c:464
2190        // Faithful port of c:464-478:
2191        //   Builtin bn = (Builtin) builtintab->getnode2(builtintab, bnam);
2192        //   if (!bn) {
2193        //       if(!(flags & FEAT_IGNORE)) return 2;
2194        //   } else if (bn->node.flags & BINF_ADDED) {
2195        //       if (!(flags & FEAT_IGNORE)) return 3;
2196        //   } else
2197        //       deletebuiltin(bnam);
2198        //   return 0;
2199        //
2200        // Three distinct return codes: 2 = no such builtin (autoload
2201        // ledger absent), 3 = real registered (BINF_ADDED set — can't
2202        // unload via del_auto*), 0 = success.
2203        //
2204        // zshrs's createbuiltintable() entries are all BINF_ADDED by
2205        // construction (the static-linked canonical table), so a hit
2206        // there always means "real registered" → return 3. The
2207        // autoload stubs live in self.autoload_builtins; a hit there
2208        // is the c:475 deletebuiltin path.
2209        //
2210        // Now routes through the free deletebuiltin (00e6a9ce7e) for
2211        // the canonical-table probe so the present/absent contract
2212        // matches C exactly. Prior port did the probe inline.
2213
2214        // c:466 — `builtintab->getnode2(builtintab, bnam)`. zshrs's
2215        // builtintab is split: the immutable createbuiltintable()
2216        // carries the static flags, the autoload_builtins ledger holds
2217        // runtime stubs from add_autobin, and added_builtins is the
2218        // runtime BINF_ADDED bit a module load flips (setbuiltins
2219        // c:508 probes the same ledger).
2220        let static_flags: Option<i32> = createbuiltintable().get(name).map(|b| b.node.flags);
2221        let in_ledger = self.autoload_builtins.contains_key(name);
2222        // BINF_ADDED equivalent: C sets the bit at addbuiltins time —
2223        // startup for core builtins, the module's boot_ for module
2224        // builtins. zshrs's static table folds BOTH in unflagged, so a
2225        // static hit counts as ADDED only when the name is core (no
2226        // owning module advertises `b:NAME`) or its owning module is
2227        // loaded. Runtime loads also land in the added_builtins ledger
2228        // (setbuiltins c:508 probes the same).
2229        let added = self.added_builtins.contains_key(name)
2230            || static_flags
2231                .map(|f| (f & BINF_ADDED as i32) != 0)
2232                .unwrap_or(false)
2233            || (static_flags.is_some() && {
2234                let mod_names: Vec<String> = self.modules.keys().cloned().collect();
2235                let mut owner_loaded: Option<bool> = None; // None = core builtin
2236                'outer: for mn in &mod_names {
2237                    let mut feats: Vec<String> = Vec::new();
2238                    if features_module(self, mn, &mut feats) != 0 {
2239                        continue;
2240                    }
2241                    for f in &feats {
2242                        if f.strip_prefix("b:") == Some(name) {
2243                            owner_loaded = Some(self.is_loaded(mn));
2244                            break 'outer;
2245                        }
2246                    }
2247                }
2248                owner_loaded != Some(false)
2249            });
2250        if static_flags.is_none() && !in_ledger {
2251            // c:467-469 — `if (!bn) { if(!(flags & FEAT_IGNORE)) return 2; }`
2252            if (flags & FEAT_IGNORE as i32) == 0 {
2253                return 2; // c:469
2254            }
2255        } else if added {
2256            // c:470-473 — `else if (bn->node.flags & BINF_ADDED)` —
2257            // a real, live builtin can't be un-autoloaded.
2258            if (flags & FEAT_IGNORE as i32) == 0 {
2259                return 3; // c:472
2260            }
2261        } else {
2262            // c:474-475 — `else deletebuiltin(bnam);` — drop the
2263            // autoload stub from the ledger.
2264            self.autoload_builtins.remove(name);
2265        }
2266        0 // c:477
2267    }
2268
2269    /// Set/clear a slice of builtins per `e[]` mask.
2270    /// Port of `static int setbuiltins(char const *nam, Builtin binl,
2271    /// int size, int *e)` from `Src/module.c:501`. For each Builtin in
2272    /// `binl[0..size]`: if `e[n]` is set, add the builtin (skip if
2273    /// already `BINF_ADDED`); else delete the builtin (skip if not
2274    /// `BINF_ADDED`). Warnings on clash/already-deleted; returns 1 if
2275    /// any op failed.
2276    /// WARNING: param names don't match C — Rust=(module, names, e) vs C=(nam, binl, size, e)
2277    pub fn setbuiltins(&mut self, module: &str, names: &[&str], e: Option<&[i32]>) -> i32 {
2278        // c:501
2279        let mut ret: i32 = 0; // c:503
2280        for (n, name) in names.iter().enumerate() {
2281            // c:505 — `for (n = 0; n < size; n++) { Builtin b = &binl[n]; ... }`
2282            let enable = e
2283                .map(|arr| arr.get(n).copied().unwrap_or(0)) // c:507 *e++
2284                .unwrap_or(1);
2285            let already_added = self.added_builtins.contains_key(*name); // c:508 b->flags & BINF_ADDED
2286            if enable != 0 {
2287                // c:507 — `if (e && *e++)` add branch
2288                if already_added {
2289                    // c:508-509 — skip already-added.
2290                    continue;
2291                }
2292                // c:510 — `if (addbuiltin(b))` — probe the canonical
2293                // table for the clash gate. The free fn returns 1
2294                // when an existing entry already has BINF_ADDED set.
2295                let mut probe = builtin {
2296                    node: hashnode {
2297                        next: None,
2298                        nam: name.to_string(),
2299                        flags: 0,
2300                    },
2301                    handlerfunc: None,
2302                    minargs: 0,
2303                    maxargs: 0,
2304                    funcid: 0,
2305                    optstr: Some(module.to_string()),
2306                    defopts: None,
2307                };
2308                if addbuiltin(&mut probe) != 0 {
2309                    // c:511-513 — `zwarnnam(nam, "name clash...")`
2310                    zwarnnam(
2311                        module,
2312                        &format!("name clash when adding builtin `{}'", name),
2313                    );
2314                    ret = 1; // c:513
2315                } else {
2316                    // c:515 — `b->node.flags |= BINF_ADDED;`. Mirror
2317                    // the in-place bit-set with the per-module
2318                    // ledger flip.
2319                    self.added_builtins.insert(name.to_string(), BINF_ADDED);
2320                }
2321            } else {
2322                // c:517-525 — del branch.
2323                if !already_added {
2324                    // c:518-519 — skip already-not-added.
2325                    continue;
2326                }
2327                // c:520 — `if (deletebuiltin(b->node.nam))`. Free fn
2328                // returns -1 on miss; treat any non-zero as the
2329                // "already deleted" condition.
2330                if deletebuiltin(name) != 0 {
2331                    // c:521-523 — `zwarnnam(nam, "builtin `%s' already
2332                    // deleted")`.
2333                    zwarnnam(module, &format!("builtin `{}' already deleted", name));
2334                    ret = 1; // c:523
2335                } else {
2336                    // c:524 — `b->node.flags &= ~BINF_ADDED;`
2337                    self.added_builtins.remove(*name);
2338                }
2339            }
2340        }
2341        ret // c:528
2342    }
2343
2344    // ------- Condition management (from module.c addconddef/deleteconddef) -------
2345
2346    /// Register a condition (from module.c addconddef)
2347    /// Port of `addconddef(Conddef c)` from `Src/module.c:703`.
2348    /// WARNING: param names don't match C — Rust=(name, module) vs C=(c)
2349    ///
2350    /// Like `addbuiltin`, the real registration lives in
2351    /// `cond.rs::CONDTAB`; the routing fn here delegates to the free
2352    /// `addconddef` (4304-port) so the canonical name+infix-flag
2353    /// clash gate fires.
2354    ///
2355    /// Returns 0 on success, 1 on clash (matches C's signature).
2356    pub fn addconddef(&mut self, name: &str, module: &str) -> i32 {
2357        // c:703
2358        // Construct a probe conddef matching what add_autocond /
2359        // setconddefs build. handler/min/max/condid stay default
2360        // (filled in by the actual module on load).
2361        let cd = conddef {
2362            next: None,
2363            name: name.to_string(),
2364            flags: 0, // c:703 entries here aren't infix by default
2365            handler: None,
2366            min: 0,
2367            max: 0,
2368            condid: 0,
2369            module: Some(module.to_string()),
2370        };
2371        // c:705-715 — addconddef walks CONDTAB for clash, replaces
2372        // autoload entries, prepends on success. Returns 0/1.
2373        addconddef(cd)
2374    }
2375
2376    /// Unregister a condition (from module.c deleteconddef)
2377    /// Port of `deleteconddef(Conddef c)` from `Src/module.c:724`.
2378    /// Returns 0 on success (entry was found + removed), -1 on miss.
2379    /// Mirrors C's deleteconddef return contract.
2380    /// WARNING: param names don't match C — Rust=(name, module) vs C=(c)
2381    pub fn deleteconddef(&mut self, name: &str, _module: &str) -> i32 {
2382        // c:724
2383        // Build a probe conddef and call the free deleteconddef
2384        // (4304-port) which walks CONDTAB by (name, infix-flag) for
2385        // identity. infix=0 here matches the prefix-style default
2386        // used by the auto* registrations.
2387        let probe = conddef {
2388            next: None,
2389            name: name.to_string(),
2390            flags: 0,
2391            handler: None,
2392            min: 0,
2393            max: 0,
2394            condid: 0,
2395            module: None,
2396        };
2397        deleteconddef(&probe)
2398    }
2399
2400    /// Get condition definition (from module.c getconddef)
2401    /// Port of `getconddef(int inf, const char *name, int autol)` from `Src/module.c:648`.
2402    /// WARNING: param names don't match C — Rust=(name) vs C=(inf, name, autol)
2403    ///
2404    /// Returns the autoload mapping if any. C consults the canonical
2405    /// `condtab` first; the autoload table is the fallback.
2406    pub fn getconddef(&self, name: &str) -> Option<&str> {
2407        self.autoload_conditions.get(name).map(|s| s.as_str())
2408    }
2409
2410    /// Register autoloading condition.
2411    /// Port of `static int add_autocond(const char *module, const char
2412    /// *cnam, int flags)` from `Src/module.c:792`. C body allocates a
2413    /// `Conddef`, copies name/module, sets `CONDF_INFIX` if
2414    /// `FEAT_INFIX` and `CONDF_AUTOALL` if `FEAT_AUTOALL`, then calls
2415    /// `addconddef(c)`. On addconddef failure (already exists) the
2416    /// node is freed; returns 1 unless `FEAT_IGNORE`.
2417    /// WARNING: param names don't match C — Rust=(name, module, flags) vs C=(module, cnam, flags)
2418    pub fn add_autocond(&mut self, name: &str, module: &str, flags: i32) -> i32 {
2419        // c:792
2420        // Faithful port of c:792-810:
2421        //   Conddef c = zshcalloc(sizeof(*c));
2422        //   c->name = ztrdup(cnam);
2423        //   c->module = ztrdup(module);
2424        //   c->flags = (flags & FEAT_INFIX) ? CONDF_INFIX : 0;
2425        //   if (flags & FEAT_AUTOALL) c->flags |= CONDF_AUTOALL;
2426        //   if (addconddef(c)) {
2427        //       zsfree(c->name); zsfree(c->module); zfree(c, sizeof(*c));
2428        //       if (!(flags & FEAT_IGNORE)) return 1;
2429        //   }
2430        //   return 0;
2431        //
2432        // Prior port did a ledger-only insert into autoload_conditions
2433        // without ever touching the canonical CONDTAB. Now constructs
2434        // the conddef struct and routes through the free addconddef
2435        // (1395acf3a7 dependency) which walks CONDTAB for clashes,
2436        // replaces autoload entries, and prepends on success.
2437        let mut cflags: i32 = if (flags & FEAT_INFIX) != 0 {
2438            CONDF_INFIX // c:799
2439        } else {
2440            0
2441        };
2442        if (flags & FEAT_AUTOALL) != 0 {
2443            cflags |= CONDF_AUTOALL; // c:801
2444        }
2445        // c:796-803 — populate the conddef record. `handler` is None
2446        // because autoload stubs don't carry the dispatch fn until
2447        // the module loads and addconddef replaces the entry.
2448        let cd = conddef {
2449            next: None,
2450            name: name.to_string(),
2451            flags: cflags,
2452            handler: None,
2453            min: 0,
2454            max: 0,
2455            condid: 0,
2456            module: Some(module.to_string()),
2457        };
2458        // c:804 — addconddef(c).
2459        if addconddef(cd) != 0 {
2460            // c:805-807 — zsfree/zfree happen via Rust drop.
2461            if (flags & FEAT_IGNORE) == 0 {
2462                return 1; // c:810
2463            }
2464        }
2465        // Keep the ledger entry too so resolve_autoload_condition()
2466        // and del_autocond's fast path can find it without re-scanning
2467        // CONDTAB.
2468        self.autoload_conditions
2469            .insert(name.to_string(), module.to_string());
2470        0 // c:812
2471    }
2472
2473    /// Port of `static int del_autocond(const char *modnam, const char *cnam,
2474    /// int flags)` from `Src/module.c:819`.
2475    ///
2476    /// C body (c:819-835):
2477    /// ```c
2478    /// Conddef cd = getconddef((flags & FEAT_INFIX) ? 1 : 0, cnam, 0);
2479    /// if (!cd) { if (!(flags & FEAT_IGNORE)) return 2; }
2480    /// else if (cd->flags & CONDF_ADDED) {
2481    ///     if (!(flags & FEAT_IGNORE)) return 3;
2482    /// } else deleteconddef(cd);
2483    /// return 0;
2484    /// ```
2485    ///
2486    /// 2 = "no such condition", 3 = "registered condition (CONDF_ADDED) —
2487    /// can't unload", 0 = success. FEAT_IGNORE masks both error returns.
2488    /// WARNING: param names don't match C — Rust=(name, flags) vs C=(modnam, cnam, flags)
2489    pub fn del_autocond(&mut self, name: &str, flags: i32) -> i32 {
2490        // c:819
2491        // Faithful port of c:819-835:
2492        //   Conddef cd = getconddef((flags & FEAT_INFIX) ? 1 : 0,
2493        //                           cnam, 0);
2494        //   if (!cd) {
2495        //       if (!(flags & FEAT_IGNORE)) return 2;
2496        //   } else if (cd->flags & CONDF_ADDED) {
2497        //       if (!(flags & FEAT_IGNORE)) return 3;
2498        //   } else
2499        //       deleteconddef(cd);
2500        //   return 0;
2501        //
2502        // Prior port skipped CONDTAB entirely; checked the ledger
2503        // only. Now routes through getconddef (1395acf3a7) which
2504        // walks CONDTAB filtered by infix-flag, then deleteconddef
2505        // (free fn) which removes the matched entry.
2506        let inf = if (flags & FEAT_INFIX) != 0 { 1 } else { 0 };
2507        // c:821 — `getconddef(inf, cnam, 0)`.
2508        let cd = getconddef(inf, name, 0, self);
2509        match cd {
2510            None => {
2511                // c:823-825 — !cd: return 2 unless FEAT_IGNORE.
2512                if (flags & FEAT_IGNORE) == 0 {
2513                    return 2; // c:825
2514                }
2515                0 // c:834
2516            }
2517            Some(ref entry) if (entry.flags & CONDF_ADDED) != 0 => {
2518                // c:826-828 — CONDF_ADDED set: real registered
2519                // condition, can't unload via del_auto*. Return 3
2520                // unless FEAT_IGNORE.
2521                if (flags & FEAT_IGNORE) == 0 {
2522                    return 3; // c:828
2523                }
2524                0
2525            }
2526            Some(ref entry) => {
2527                // c:831-832 — deleteconddef(cd); drop ledger too.
2528                let _ = deleteconddef(entry);
2529                self.autoload_conditions.remove(name);
2530                0 // c:834
2531            }
2532        }
2533    }
2534
2535    // ------- Hook management lives in the file-static free ported above ------
2536    //
2537    // C `hooktab` (Src/module.c:843) is a file-static `Hookdef` linked
2538    // list, NOT a member of `ModuleTable` (which is the `modulestab`
2539    // HashTable of Module nodes at c:Modules/zmodload.c:32). Hook ops
2540    // are free ported operating on the file-static `hooktab` (above):
2541    // `gethookdef`, `addhookdef`, `addhookdefs`, `deletehookdef`,
2542    // `deletehookdefs`, `addhookdeffunc`, `addhookfunc`,
2543    // `deletehookdeffunc`, `deletehookfunc`, `runhookdef`.
2544
2545    // ------- Parameter management (from module.c addparamdef/deleteparamdef) -------
2546
2547    /// Add or remove sets of parameters; same shape as `setbuiltins`.
2548    /// Port of `static int setparamdefs(char const *nam, Paramdef d,
2549    /// int size, int *e)` from `Src/module.c:1170`. For each Paramdef
2550    /// in `d[0..size]`: if `e[n]` is set and `d->pm` is null, add the
2551    /// param via `addparamdef(d)`; if `e[n]` is clear and `d->pm` is
2552    /// non-null, remove via `deleteparamdef(d)`. Warnings on
2553    /// error/already-deleted; returns 1 if any op failed.
2554    /// WARNING: param names don't match C — Rust=(module, names, e) vs C=(nam, d, size, e)
2555    pub fn setparamdefs(&mut self, module: &str, names: &[&str], e: Option<&[i32]>) -> i32 {
2556        // c:1165
2557        // Faithful port of c:1165-1192. Prior Rust port skipped the
2558        // diagnostics + ret-tracking C does on failure:
2559        //
2560        //   if (e && *e++) {           // add branch
2561        //       if (d->pm) continue;   // already registered
2562        //       if (addparamdef(d)) {
2563        //           zwarnnam(nam,
2564        //               "error when adding parameter `%s'", d->name);
2565        //           ret = 1;
2566        //       }
2567        //   } else {                   // del branch
2568        //       if (!d->pm) continue;  // not registered
2569        //       if (deleteparamdef(d)) {
2570        //           zwarnnam(nam,
2571        //               "parameter `%s' already deleted", d->name);
2572        //           ret = 1;
2573        //       }
2574        //   }
2575        //
2576        // Maps to the autoload_params ledger (zshrs-only structural
2577        // equivalent of d->pm presence). Diagnostics now fire when
2578        // the ledger insert/remove conflicts with the C-side
2579        // contract (probe + retry on the canonical paramtab to
2580        // observe the same clash behaviour addparamdef would).
2581        use crate::ported::params::paramtab;
2582        let mut ret: i32 = 0; // c:1167
2583        for (n, name) in names.iter().enumerate() {
2584            // c:1169 — `while (size--)`
2585            let enable = e
2586                .map(|arr| arr.get(n).copied().unwrap_or(0)) // c:1170 *e++
2587                .unwrap_or(1);
2588            // c:1171 / c:1180 — `if (d->pm)` / `if (!d->pm)`.
2589            // Static-link analog: autoload_params contains the name
2590            // when the module has registered it as a paramdef.
2591            let already = self.autoload_params.contains_key(*name);
2592            if enable != 0 {
2593                // c:1170 add branch
2594                if already {
2595                    continue; // c:1172
2596                }
2597                // c:1175 — `if (addparamdef(d))`. Faithful path:
2598                // probe the canonical paramtab for a clash; if the
2599                // name already exists with a non-MOD-derived param,
2600                // mirror addparamdef's failure.
2601                let canonical_clash = paramtab()
2602                    .read()
2603                    .ok()
2604                    .map(|t| t.contains_key(*name))
2605                    .unwrap_or(false);
2606                if canonical_clash {
2607                    // c:1176-1178 — `zwarnnam('error when adding ...')`.
2608                    zwarnnam(module, &format!("error when adding parameter `{}'", name));
2609                    ret = 1; // c:1177
2610                    continue;
2611                }
2612                // c:1181 — register the ledger entry on success.
2613                self.autoload_params
2614                    .insert(name.to_string(), module.to_string());
2615            } else {
2616                // c:1179 del branch
2617                if !already {
2618                    continue; // c:1181
2619                }
2620                // c:1184 — `if (deleteparamdef(d))`. With the ledger
2621                // hit, the canonical removal succeeds; emit the
2622                // C-equivalent diagnostic only if the paramtab probe
2623                // says the entry got tampered with externally
2624                // (already-deleted from paramtab while still on the
2625                // ledger).
2626                let canonical_present = paramtab()
2627                    .read()
2628                    .ok()
2629                    .map(|t| t.contains_key(*name))
2630                    .unwrap_or(false);
2631                self.autoload_params.remove(*name);
2632                if !canonical_present {
2633                    // c:1185-1187 — `parameter `%s' already deleted`.
2634                    zwarnnam(module, &format!("parameter `{}' already deleted", name));
2635                    ret = 1; // c:1186
2636                }
2637            }
2638        }
2639        ret // c:1191
2640    }
2641
2642    /// Register autoloading parameter.
2643    /// Port of `static int add_autoparam(const char *module, const char
2644    /// *pnam, int flags)` from `Src/module.c:1198`. C body:
2645    /// `checkaddparam()` clash check (returns 2 if `-i`'d), then
2646    /// `setsparam(pnam, module)` creating the param with `PM_AUTOLOAD`
2647    /// (+ `PM_AUTOALL` if `FEAT_AUTOALL`). `queue_signals`/`noerrs=2`
2648    /// bracket so the setsparam doesn't echo errors out.
2649    /// WARNING: param names don't match C — Rust=(name, module, flags) vs C=(module, pnam, flags)
2650    pub fn add_autoparam(&mut self, name: &str, module: &str, flags: i32) -> i32 {
2651        // c:1198
2652        // Faithful port of c:1198-1228 routes through the free
2653        // add_autoparam fn (293c041e2f) which already does the full
2654        // checkaddparam + setsparam + PM_AUTOLOAD (+PM_AUTOALL) +
2655        // noerrs/queue_signals bracket.
2656        //
2657        // Prior method was a ledger-only HashMap insert that did the
2658        // queue_signals dance without ever calling setsparam — so
2659        // PM_AUTOLOAD never landed on the canonical paramtab entry,
2660        // and `typeset +` wouldn't see the 'undefined NAME' shape.
2661        //
2662        // The free fn returns 0 / -1 (vs C's 0 / 1 / -1) — preserve
2663        // the existing method-level contract by mapping -1 → 1 below.
2664        let r = add_autoparam(module, name, flags); // c:1198
2665        if r != 0 {
2666            // c:1213-1219 — error: 2 (FEAT_IGNORE soft-fail) maps to
2667            // 0 here per the prior method contract.
2668            if (flags & FEAT_IGNORE) != 0 {
2669                return 0;
2670            }
2671            return r; // -1 → -1
2672        }
2673        // c:1218-1221 success: also keep the ledger entry up so the
2674        // resolve_autoload_param fast path stays consistent.
2675        self.autoload_params
2676            .insert(name.to_string(), module.to_string());
2677        0
2678    }
2679
2680    /// Port of `static int del_autoparam(const char *modnam, const char *pnam,
2681    /// int flags)` from `Src/module.c:1240`.
2682    ///
2683    /// C body (c:1240-1255):
2684    /// ```c
2685    /// Param pm = (Param) gethashnode2(paramtab, pnam);
2686    /// if (!pm) { if (!(flags & FEAT_IGNORE)) return 2; }
2687    /// else if (!(pm->node.flags & PM_AUTOLOAD)) {
2688    ///     if (!(flags & FEAT_IGNORE)) return 3;
2689    /// } else unsetparam_pm(pm, 0, 1);
2690    /// return 0;
2691    /// ```
2692    ///
2693    /// 2 = "no such param", 3 = "real param (not autoload) — can't
2694    /// unload", 0 = success. FEAT_IGNORE masks both error returns.
2695    /// WARNING: param names don't match C — Rust=(name, flags) vs C=(modnam, pnam, flags)
2696    pub fn del_autoparam(&mut self, name: &str, flags: i32) -> i32 {
2697        // c:1234
2698        // Faithful port of c:1234-1248 routes through the free
2699        // del_autoparam fn (293c041e2f) which does the canonical
2700        // paramtab probe + PM_AUTOLOAD gate + unsetparam_pm.
2701        //
2702        // Prior method walked paramtab directly and emulated the
2703        // PM_AUTOLOAD gate inline; routing through the free fn keeps
2704        // the logic single-sourced so PM_AUTOLOAD semantics evolve
2705        // in one place. Ledger cleanup preserved on the success path.
2706        let r = del_autoparam("", name, flags); // c:1234 (modnam unused per UNUSED())
2707        if r == 0 {
2708            // c:1246 — `unsetparam_pm` already ran via the free fn;
2709            // also drop the ledger entry so resolve_autoload_param
2710            // stops returning the stale module name.
2711            self.autoload_params.remove(name);
2712        }
2713        r
2714    }
2715
2716    // ------- Feature enable/disable (from module.c features_/enables_) -------
2717
2718    /// Enable a feature (from module.c enables_)
2719    ///
2720    /// Without a per-module feature ledger, enable/disable maps onto
2721    /// the canonical builtin/conddef/paramdef tables. Returns true if
2722    /// the module itself is registered. The actual per-feature
2723    /// enabled-bit lives on the canonical record (e.g. `Builtin.flags`
2724    /// `BINF_DISABLED`).
2725    pub fn enable_feature(&mut self, module: &str, _name: &str) -> bool {
2726        self.modules.contains_key(module)
2727    }
2728
2729    /// Disable a feature
2730    pub fn disable_feature(&mut self, module: &str, _name: &str) -> bool {
2731        self.modules.contains_key(module)
2732    }
2733
2734    /// List feature *names* of a module (from module.c features_).
2735    /// Without a per-module ledger, this returns an empty list — C
2736    /// computes feature names by walking the canonical tables for
2737    /// entries that name the given module. Callers that care use
2738    /// `features_module`/`features_` directly.
2739    pub fn list_features(&self, _module: &str) -> Vec<String> {
2740        Vec::new()
2741    }
2742
2743    /// Check if a module is linked (statically compiled) (from module.c module_linked)
2744    /// Port of `module_linked(char const *name)` from `Src/module.c:385`.
2745    pub fn module_linked(&self, name: &str) -> bool {
2746        self.modules.contains_key(name)
2747    }
2748
2749    /// Resolve autoload — find which module provides a builtin
2750    pub fn resolve_autoload_builtin(&self, name: &str) -> Option<&str> {
2751        self.autoload_builtins.get(name).map(|s| s.as_str())
2752    }
2753
2754    /// Resolve autoload — find which module provides a parameter
2755    pub fn resolve_autoload_param(&self, name: &str) -> Option<&str> {
2756        self.autoload_params.get(name).map(|s| s.as_str())
2757    }
2758
2759    /// Ensure a module's feature is available
2760    /// Port of `ensurefeature(const char *modname, const char *prefix, const char *feature)` from `Src/module.c:3415`.
2761    /// WARNING: param names don't match C — Rust=(module, feature) vs C=(modname, prefix, feature)
2762    pub fn ensurefeature(&mut self, module: &str, feature: &str) -> bool {
2763        if !self.is_loaded(module) {
2764            self.load_module(module);
2765        }
2766        self.is_loaded(module)
2767    }
2768}
2769
2770/// Module lifecycle callbacks (from module.c setup_/getrandom_buffer/cleanup_/finish_)
2771/// Lifecycle hooks every module must implement.
2772/// Port of the `setup_`/`features_`/`enables_`/`getrandom_buffer`/`cleanup_`
2773/// /`finish_` entry points every C module exposes (Src/module.c
2774/// lines 306-345 illustrate the canonical no-op set). Rust
2775/// modules implement this trait directly.
2776pub trait ModuleLifecycle {
2777    fn setup(&mut self) -> i32 {
2778        0
2779    }
2780    fn boot(&mut self) -> i32 {
2781        0
2782    }
2783    fn cleanup(&mut self) -> i32 {
2784        0
2785    }
2786    fn finish(&mut self) -> i32 {
2787        0
2788    }
2789}
2790
2791/// Port of `getmathfunc(const char *name, int autol)` from `Src/module.c:1283`.
2792///
2793/// C body: linear-search `mathfuncs` for `name`; if found and `autol`
2794/// is true and the entry is autoloadable, demand-load via
2795/// `ensurefeature("f:", name)`. Returns the resolved entry or NULL.
2796///
2797/// Rust port returns `Some(module_name)` on hit, `None` on miss.
2798///
2799/// Faithful port of c:1283-1306:
2800/// ```c
2801/// MathFunc
2802/// getmathfunc(const char *name, int autol)
2803/// {
2804///     MathFunc p, q = NULL;
2805///     for (p = mathfuncs; p; q = p, p = p->next)
2806///         if (!strcmp(name, p->name)) {
2807///             if (autol && p->module && !(p->flags & MFF_USERFUNC)) {
2808///                 char *n = dupstring(p->module);
2809///                 int flags = p->flags;
2810///                 removemathfunc(q, p);
2811///                 (void)ensurefeature(n, "f:",
2812///                                     (flags & MFF_AUTOALL) ? NULL : name);
2813///                 p = getmathfunc(name, 0);
2814///                 if (!p) {
2815///                     zerr("autoloading module %s failed to define math function: %s", n, name);
2816///                 }
2817///             }
2818///             return p;
2819///         }
2820///     return NULL;
2821/// }
2822/// ```
2823///
2824/// C walks `mathfuncs` (file-static linked list) — Rust walks
2825/// MATHFUNCS (same shape, Vec). The MFF_USERFUNC filter
2826/// (`functions -M` user-defined math) keeps user fns from being
2827/// autoloaded out from under the caller. The autoload arm:
2828///   1. snapshot module name + flags
2829///   2. removemathfunc(name) — drop the autoload stub
2830///   3. ensurefeature(module, "f:", AUTOALL?NULL:name) — load real
2831///   4. re-query mathfuncs (autol=0) — return the loaded entry
2832///   5. zerr if still missing (load failed to populate the table)
2833/// WARNING: param names don't match C — Rust=(table, name, autol) vs C=(name, autol)
2834pub fn getmathfunc(table: &mut modulestab, name: &str, autol: i32) -> Option<String> {
2835    // c:1283
2836    // c:1287-1288 — `for (p = mathfuncs; p; q = p, p = p->next)`:
2837    // walk MATHFUNCS for the first name match. Snapshot under the
2838    // lock so we can release before mutating (autoload arm calls
2839    // ensurefeature which can re-enter MATHFUNCS via addmathfunc).
2840    let hit: Option<(String, i32, bool)> = {
2841        let tab = MATHFUNCS.lock().unwrap();
2842        tab.iter().find_map(|p| {
2843            if p.name == name {
2844                Some((
2845                    p.module.clone().unwrap_or_default(),
2846                    p.flags,
2847                    p.module.is_some(),
2848                ))
2849            } else {
2850                None
2851            }
2852        })
2853    };
2854    let (module, flags, has_module) = match hit {
2855        Some(t) => t,
2856        None => return None, // c:1306 `return NULL;`
2857    };
2858    // c:1289 — `if (autol && p->module && !(p->flags & MFF_USERFUNC))`.
2859    if autol != 0 && has_module && (flags & MFF_USERFUNC) == 0 {
2860        // c:1290-1291 — snapshot already done above.
2861        // c:1293 — `removemathfunc(q, p)`: drop the stub before load.
2862        removemathfunc(name);
2863        // c:1295-1296 — `ensurefeature(n, "f:", AUTOALL?NULL:name)`.
2864        let feature_arg = if (flags & crate::ported::zsh_h::MFF_AUTOALL) != 0 {
2865            None
2866        } else {
2867            Some(name)
2868        };
2869        let _ = ensurefeature(table, &module, "f:", feature_arg);
2870        // c:1298 — `p = getmathfunc(name, 0);` recurse w/o autol.
2871        // EXISTENCE check: C zerrs only when the re-lookup returns
2872        // NULL. A freshly registered real entry has module == NULL
2873        // (mftab entries, zsh.h:133 NUMMATHFUNC), so mapping the hit
2874        // through `p.module.clone()` (None for real entries) wrongly
2875        // reported a successful load as "failed to define". Mirror the
2876        // non-autoload tail below: empty string marks a module-less
2877        // hit.
2878        let after = {
2879            let tab = MATHFUNCS.lock().unwrap();
2880            tab.iter().find_map(|p| {
2881                if p.name == name {
2882                    Some(p.module.clone().unwrap_or_default())
2883                } else {
2884                    None
2885                }
2886            })
2887        };
2888        // c:1299-1301 — `if (!p) zerr(...)`.
2889        if after.is_none() {
2890            crate::ported::utils::zerr(&format!(
2891                "autoloading module {} failed to define math function: {}",
2892                module, name
2893            ));
2894        }
2895        return after; // c:1303 `return p;`
2896    }
2897    // c:1303 — non-autoload (or user-fn) hit: return the entry.
2898    if has_module {
2899        Some(module)
2900    } else {
2901        // User-fn entries (`functions -M`) have no module — return
2902        // an empty string so the caller still observes the hit.
2903        Some(String::new())
2904    }
2905}
2906
2907/// Port of `add_automathfunc(const char *module, const char *fnam, int flags)` from `Src/module.c:1410`.
2908///
2909/// C body:
2910/// ```c
2911/// add_automathfunc(const char *module, const char *fnam, int flags) {
2912///     MathFunc f = zalloc(sizeof(*f));
2913///     f->name = ztrdup(fnam);
2914///     f->module = ztrdup(module);
2915///     f->flags = 0;
2916///     if (addmathfunc(f)) {
2917///         zsfree(f->name); zsfree(f->module); zfree(f, sizeof(*f));
2918///         if (!(flags & FEAT_IGNORE))
2919///             return 1;
2920///     }
2921///     return 0;
2922/// }
2923/// ```
2924///
2925/// Registers `fnam` as an autoloadable math function provided by `module`.
2926/// WARNING: param names don't match C — Rust=(table, module, fnam, flags) vs C=(module, fnam, flags)
2927pub fn add_automathfunc(table: &mut modulestab, module: &str, fnam: &str, flags: i32) -> i32 {
2928    // c:1410
2929    // Faithful port of c:1410-1429:
2930    //   MathFunc f = zalloc(sizeof(*f));
2931    //   f->name = ztrdup(fnam);
2932    //   f->module = ztrdup(module);
2933    //   f->flags = 0;
2934    //   if (addmathfunc(f)) {
2935    //       zsfree(f->name); zsfree(f->module); zfree(f, sizeof(*f));
2936    //       if (!(flags & FEAT_IGNORE)) return 1;
2937    //   }
2938    //   return 0;
2939    //
2940    // Prior port did ledger-only autoload_mathfuncs.insert without
2941    // ever touching the canonical MATHFUNCS table. Now constructs
2942    // the mathfunc struct and routes through the free addmathfunc
2943    // (c:1313) which walks MATHFUNCS for clashes and replaces
2944    // autoloadable entries.
2945    let f = mathfunc {
2946        next: None,
2947        name: fnam.to_string(),
2948        flags: 0, // c:1417 — autoload entries don't carry MFF_ADDED
2949        nfunc: None,
2950        sfunc: None,
2951        module: Some(module.to_string()),
2952        minargs: 0,
2953        maxargs: 0,
2954        funcid: 0,
2955    };
2956    // c:1420 — `if (addmathfunc(f))` clash gate.
2957    if addmathfunc(f) != 0 {
2958        // c:1421-1424 — free happens via Rust drop on the returned-
2959        // by-value f going out of scope.
2960        if (flags & FEAT_IGNORE) == 0 {
2961            return 1; // c:1426
2962        }
2963        // c:1427 — FEAT_IGNORE: fall through to success but skip
2964        // the ledger insert (the canonical table already has this).
2965        return 0;
2966    }
2967    // c:1429 success path: register in the autoload ledger.
2968    table
2969        .autoload_mathfuncs
2970        .insert(fnam.to_string(), module.to_string());
2971    0
2972}
2973
2974/// Port of `del_automathfunc(UNUSED(const char *modnam), const char *fnam, int flags)` from `Src/module.c:1436`.
2975///
2976/// C body:
2977/// ```c
2978/// del_automathfunc(UNUSED(const char *modnam), const char *fnam, int flags) {
2979///     MathFunc f = getmathfunc(fnam, 0);
2980///     if (!f) {
2981///         if (!(flags & FEAT_IGNORE)) return 2;
2982///     } else if (f->flags & MFF_ADDED) {
2983///         if (!(flags & FEAT_IGNORE)) return 3;
2984///     } else
2985///         deletemathfunc(f);
2986///     return 0;
2987/// }
2988/// ```
2989///
2990/// Removes `fnam` from the autoloadable math-function registry.
2991/// WARNING: param names don't match C — Rust=(table, _modnam, fnam, flags) vs C=(modnam, fnam, flags)
2992pub fn del_automathfunc(table: &mut modulestab, _modnam: &str, fnam: &str, flags: i32) -> i32 {
2993    // c:1436
2994    // Faithful port of c:1436-1449:
2995    //   MathFunc f = getmathfunc(fnam, 0);
2996    //   if (!f) { if (!(flags & FEAT_IGNORE)) return 2; }
2997    //   else if (f->flags & MFF_ADDED) {
2998    //       if (!(flags & FEAT_IGNORE)) return 3;
2999    //   } else deletemathfunc(f);
3000    //   return 0;
3001    //
3002    // Prior port skipped the MFF_ADDED gate at c:1444 entirely.
3003    // That meant `zmodload -ufd` on a real (module-registered)
3004    // math function silently succeeded instead of returning 3 like
3005    // C does, dropping the user's actual function out from under
3006    // them. Now uses getmathfunc (fd1ec84bab) to find the entry
3007    // and checks MFF_ADDED before removal.
3008
3009    // c:1440 — `getmathfunc(fnam, 0)`. autol=0 since we don't want
3010    // the autoload trigger to fire during a deletion query.
3011    let entry = getmathfunc(table, fnam, 0);
3012    match entry {
3013        None => {
3014            // c:1441-1442 — `if (!f) { if (!FEAT_IGNORE) return 2; }`
3015            if (flags & FEAT_IGNORE) == 0 {
3016                return 2;
3017            }
3018            0
3019        }
3020        Some(_) => {
3021            // c:1443 — `else if (f->flags & MFF_ADDED)`.
3022            // Look up the entry in MATHFUNCS to read its flags
3023            // (getmathfunc returns the module string, not the
3024            // mathfunc struct).
3025            let added = {
3026                let tab = MATHFUNCS.lock().unwrap();
3027                tab.iter()
3028                    .find(|m| m.name == fnam)
3029                    .map(|m| (m.flags & MFF_ADDED) != 0)
3030                    .unwrap_or(false)
3031            };
3032            if added {
3033                // c:1444-1445 — real registered, can't unload via
3034                // del_auto*. Return 3 unless FEAT_IGNORE.
3035                if (flags & FEAT_IGNORE) == 0 {
3036                    return 3;
3037                }
3038                return 0;
3039            }
3040            // c:1447 — deletemathfunc(f). Use removemathfunc
3041            // (which deletemathfunc delegates to in the autoload
3042            // path) + drop the ledger entry.
3043            removemathfunc(fnam);
3044            table.autoload_mathfuncs.remove(fnam);
3045            0
3046        }
3047    }
3048}
3049
3050/// Port of `load_and_bind(const char *fn)` from `Src/module.c:1468`.
3051///
3052/// C body: AIX-only `load() + loadbind()` wrapper. Iterates the
3053/// `modulestab` hash table, binding each loaded module's handle to
3054/// the new module's symbols. On loadbind failure, calls `unload()`
3055/// and stores the error in `dlerrstr`.
3056///
3057/// Static-link path: dlopen/dlsym aren't used since modules are
3058/// linked at compile time. Returns 0 (NULL handle).
3059/// WARNING: param names don't match C — Rust=(_fn_path) vs C=(fn)
3060pub fn load_and_bind(_fn_path: &str) -> usize {
3061    // c:1468
3062    0 // c:1492 NULL
3063}
3064
3065/// Port of `hpux_dlsym(void *handle, char *name)` from `Src/module.c:1530`.
3066///
3067/// C body:
3068/// ```c
3069/// hpux_dlsym(void *handle, char *name)
3070/// {
3071///     void *sym_addr;
3072///     if (!shl_findsym((shl_t *)&handle, name, TYPE_UNDEFINED, &sym_addr))
3073///         return sym_addr;
3074///     return NULL;
3075/// }
3076/// ```
3077///
3078/// HP-UX-specific dlsym wrapper around `shl_findsym(3)`. Static-link
3079/// path: never invoked since zshrs doesn't dlopen modules.
3080#[allow(unused_variables)]
3081pub fn hpux_dlsym(handle: usize, name: &str) -> usize {
3082    // c:1530
3083    0 // c:1530 NULL
3084}
3085
3086/// Port of `try_load_module(char const *name)` from `Src/module.c:1583`.
3087///
3088/// C body iterates `module_path` looking for a loadable file via
3089/// `dlopen`. Static-link path: a module is "loadable" iff it's in
3090/// our static `ModuleTable.modules` map.
3091/// WARNING: param names don't match C — Rust=(table, name) vs C=(name)
3092pub fn try_load_module(table: &modulestab, name: &str) -> i32 {
3093    // c:1583
3094    // C dlopens the module path (or falls back to module_linked for
3095    // compiled-in modules). Static-link analog: the node must carry
3096    // MOD_LINKED (seeded by register_module / register_builtin_modules
3097    // for every compiled-in module). A bare FINDMOD_CREATE bookkeeping
3098    // node (flags=0 per C's zshcalloc at c:1676) has no backing code —
3099    // dlopen would fail, so the loadable probe must too. Without the
3100    // flag gate, `zmodload -ab zsh/bogus x; x` "booted" the phantom
3101    // module instead of emitting `failed to load module`.
3102    match table.modules.get(name) {
3103        Some(m) if (m.node.flags & MOD_LINKED) != 0 => 1,
3104        _ => 0,
3105    }
3106}
3107
3108/// Port of `do_load_module(char const *name, int silent)` from `Src/module.c:1610`.
3109///
3110/// C body:
3111/// ```c
3112/// do_load_module(char const *name, int silent)
3113/// {
3114///     void *ret;
3115///     ret = try_load_module(name);
3116///     if (!ret && !silent) {
3117///         zwarn("failed to load module `%s': %s", name, ...);
3118///     }
3119///     return ret;
3120/// }
3121/// ```
3122///
3123/// C returns `void *` (the dlopen handle); Rust port returns 0 on
3124/// success / 1 on failure. zshrs's static-link path: `try_load_module`
3125/// always succeeds for built-in modules. Returns 1 + zwarn on miss.
3126/// WARNING: param names don't match C — Rust=(table, name, silent) vs C=(name, silent)
3127pub fn do_load_module(table: &mut modulestab, name: &str, silent: i32) -> i32 {
3128    // c:1610
3129    // c:1610 — ret = try_load_module(name);
3130    let ret = try_load_module(table, name);
3131    if ret == 0 && silent == 0 {
3132        // c:1615
3133        // c:1618-1621 — zwarn("failed to load module ...")
3134        zwarn(&format!("failed to load module: {}", name));
3135    }
3136    ret // c:1624
3137}
3138
3139/// Port of `find_module(const char *name, int flags, const char **namep)` from `Src/module.c:1659`.
3140///
3141/// C body:
3142/// ```c
3143/// find_module(const char *name, int flags, const char **namep)
3144/// {
3145///     Module m;
3146///     m = (Module)modulestab->getnode2(modulestab, name);
3147///     if (m) {
3148///         if ((flags & FINDMOD_ALIASP) && (m->node.flags & MOD_ALIAS)) {
3149///             if (namep) *namep = m->u.alias;
3150///             return find_module(m->u.alias, flags, namep);
3151///         }
3152///         if (namep) *namep = m->node.nam;
3153///         return m;
3154///     }
3155///     if (!(flags & FINDMOD_CREATE))
3156///         return NULL;
3157///     m = zshcalloc(sizeof(*m));
3158///     modulestab->addnode(modulestab, ztrdup(name), m);
3159///     return m;
3160/// }
3161/// ```
3162///
3163/// Returns the resolved module name (after alias chasing) and
3164/// whether an entry was created. C's `Module` return becomes
3165/// `Option<String>` of the canonical name.
3166/// WARNING: param names don't match C — Rust=(table, name, flags) vs C=(name, flags, namep)
3167pub fn find_module(table: &mut modulestab, name: &str, flags: i32) -> Option<String> {
3168    // c:1659
3169    // c:1659 — m = modulestab->getnode2(modulestab, name);
3170    let mut cur_name = name.to_string();
3171    let mut depth = 0;
3172    loop {
3173        if depth > 64 {
3174            return None;
3175        } // alias-cycle guard
3176        depth += 1;
3177        match table.modules.get(&cur_name) {
3178            Some(m) => {
3179                // c:1665 — if ((flags & FINDMOD_ALIASP) && (m->node.flags & MOD_ALIAS))
3180                if (flags & FINDMOD_ALIASP) != 0 && (m.node.flags & MOD_ALIAS) != 0 {
3181                    // c:1668 — return find_module(m->u.alias, flags, namep);
3182                    if let Some(target) = m.alias.clone() {
3183                        cur_name = target;
3184                        continue;
3185                    }
3186                    return None;
3187                }
3188                // c:1671 — *namep = m->node.nam; return m;
3189                return Some(cur_name);
3190            }
3191            None => {
3192                // c:1674 — if (!(flags & FINDMOD_CREATE)) return NULL;
3193                if (flags & FINDMOD_CREATE) == 0 {
3194                    return None;
3195                }
3196                // c:1676-1677 — m = zshcalloc(...); addnode(name, m);
3197                // zshcalloc zero-fills: the created node has flags=0
3198                // and NULL handle/linked — a bookkeeping entry (alias
3199                // targets, autoload owners), NOT a loadable module.
3200                // module::new() seeds MOD_LINKED (the register_module
3201                // shape, c:359); that bit wrongly made phantom nodes
3202                // (`zmodload -ab zsh/bogus x`) pass try_load_module's
3203                // loadable gate and "boot" successfully.
3204                let mut m = module::new(&cur_name);
3205                m.node.flags = 0; // c:1676 zshcalloc — all-zero node
3206                table.modules.insert(cur_name.clone(), m);
3207                return Some(cur_name);
3208            }
3209        }
3210    }
3211}
3212
3213/// Port of `delete_module(Module m)` from `Src/module.c:1687`.
3214///
3215/// C body:
3216/// ```c
3217/// delete_module(Module m) {
3218///     modulestab->removenode(modulestab, m->node.nam);
3219///     modulestab->freenode(&m->node);
3220/// }
3221/// ```
3222///
3223/// Removes a module from the live `modulestab` and frees its node.
3224/// Rust port operates on the `ModuleTable` `modules` HashMap.
3225/// WARNING: param names don't match C — Rust=(table, name) vs C=(m)
3226pub fn delete_module(table: &mut modulestab, name: &str) -> i32 {
3227    // c:1687
3228    table.modules.remove(name); // c:1687 removenode
3229                                // c:1691 — freenode(&m->node) — Rust drops on `remove` return.
3230    0
3231}
3232
3233/// Port of `module_loaded(const char *name)` from `Src/module.c:1703`.
3234///
3235/// C body:
3236/// ```c
3237/// module_loaded(const char *name)
3238/// {
3239///     Module m;
3240///     return ((m = find_module(name, FINDMOD_ALIASP, NULL)) &&
3241///             m->u.handle &&
3242///             !(m->node.flags & MOD_UNLOAD));
3243/// }
3244/// ```
3245///
3246/// Returns true (non-zero) if the named module is currently loaded.
3247///
3248/// Faithful port of c:1702-1710:
3249/// ```c
3250/// mod_export int
3251/// module_loaded(const char *name)
3252/// {
3253///     Module m;
3254///     return ((m = find_module(name, FINDMOD_ALIASP, NULL)) &&
3255///             m->u.handle &&
3256///             !(m->node.flags & MOD_UNLOAD));
3257/// }
3258/// ```
3259///
3260/// All three gates matter in zshrs's static-link path:
3261///   1. `find_module(FINDMOD_ALIASP)` — resolves aliases, returns the
3262///      target's modulestab entry (or None on miss).
3263///   2. `m->u.handle` — non-null when the module's setup has run; in
3264///      the Rust mirror, MOD_INIT_S being set is the structural
3265///      equivalent (no dlopen handle to test).
3266///   3. `!(m->node.flags & MOD_UNLOAD)` — MOD_UNLOAD is the
3267///      "registered + autoload-only" sentinel set by
3268///      register_builtin_modules for entries OUTSIDE
3269///      zsh_default_loaded (e.g. zsh/files, zsh/system, zsh/zftp).
3270///      Without this gate, `${modules[zsh/files]}` reads as "loaded"
3271///      from initial register even though no `zmodload zsh/files` has
3272///      run — diverging from `zsh -fc` which reports the names as
3273///      autoload-pending.
3274/// WARNING: param names don't match C — Rust=(table, name) vs C=(name)
3275pub fn module_loaded(table: &modulestab, name: &str) -> i32 {
3276    // c:1703
3277    // c:1707 — `find_module(name, FINDMOD_ALIASP, NULL)`: resolve
3278    // alias chains via the existing free-fn port (c:1659).
3279    // The Rust port returns Option<String> — Some(target) on hit.
3280    // Inline the resolution here so we can read the target's flags
3281    // without re-locking.
3282    let target = match table.modules.get(name) {
3283        Some(m) if (m.node.flags & MOD_ALIAS) != 0 => {
3284            // c:FINDMOD_ALIASP — chase alias.
3285            // m->u.alias is the alias target; the Rust mirror stores
3286            // it on `module::aliased` (set by zmodload -A). Probe.
3287            match m.alias.as_ref().and_then(|a| table.modules.get(a)) {
3288                Some(t) => t,
3289                None => return 0, // alias points nowhere → not loaded
3290            }
3291        }
3292        Some(m) => m,
3293        None => return 0, // c:1707-1709 — find_module miss.
3294    };
3295    // c:1708 — `m->u.handle` — non-null on a fully-loaded module.
3296    // Static-link analog: MOD_LINKED set + module entry exists in
3297    // modulestab means register_module fired (= setup ran). The
3298    // dlopen `u.handle` check translates to MOD_LINKED here because
3299    // every modulestab entry is a statically-linked module in
3300    // zshrs's compile-time-only loader.
3301    if (target.node.flags & MOD_LINKED) == 0 {
3302        return 0;
3303    }
3304    // c:1709 — `!(m->node.flags & MOD_UNLOAD)`. MOD_UNLOAD is set by
3305    // register_builtin_modules on the autoload-only subset
3306    // (zsh/files, zsh/system, zsh/zftp, …); cleared by an explicit
3307    // `zmodload NAME` once the user wants the module live.
3308    if (target.node.flags & MOD_UNLOAD) != 0 {
3309        return 0;
3310    }
3311    1
3312}
3313
3314/// Port of `dyn_setup_module(Module m)` from `Src/module.c:1726`.
3315///
3316/// C body: `return ((int (*)(int,Module,void*)) m->u.handle)(0, m, NULL);`
3317/// Op-code 0 = setup. AIX-only path that multiplexes all six module
3318/// hooks through one symbol; static-link path skips it entirely.
3319#[allow(unused_variables)]
3320pub fn dyn_setup_module(m: *const module) -> i32 {
3321    // c:1726
3322    0 // c:1726
3323}
3324
3325/// Port of `dyn_features_module(Module m, char ***features)` from `Src/module.c:1733`.
3326///
3327/// C body: `return ((int (*)(int,Module,void*)) m->u.handle)(4, m, features);`
3328/// Op-code 4 = features.
3329#[allow(unused_variables)]
3330pub fn dyn_features_module(m: *const module, features: &mut Vec<String>) -> i32 {
3331    // c:1733
3332    0 // c:1733
3333}
3334
3335/// Port of `dyn_enables_module(Module m, int **enables)` from `Src/module.c:1740`.
3336///
3337/// C body: `return ((int (*)(int,Module,void*)) m->u.handle)(5, m, enables);`
3338/// Op-code 5 = enables.
3339#[allow(unused_variables)]
3340pub fn dyn_enables_module(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
3341    // c:1740
3342    0 // c:1733
3343}
3344
3345/// Port of `dyn_boot_module(Module m)` from `Src/module.c:1747`.
3346///
3347/// C body: `return ((int (*)(int,Module,void*)) m->u.handle)(1, m, NULL);`
3348/// Calls the dynamic module's exported entry-point with op-code 1
3349/// (boot). Static-link path: opcode dispatch unused, returns 0.
3350#[allow(unused_variables)]
3351pub fn dyn_boot_module(m: *const module) -> i32 {
3352    // c:1747
3353    0 // c:1754
3354}
3355
3356/// Port of `dyn_cleanup_module(Module m)` from `Src/module.c:1754`.
3357///
3358/// C body: `return ((int (*)(int,Module,void*)) m->u.handle)(2, m, NULL);`
3359/// Op-code 2 = cleanup.
3360#[allow(unused_variables)]
3361pub fn dyn_cleanup_module(m: *const module) -> i32 {
3362    // c:1754
3363    0 // c:1740
3364}
3365
3366/// Port of `static int dyn_finish_module(Module m)` from
3367/// `Src/module.c:1766`. C body: `return ((int (*)(int,Module,void*))
3368/// m->u.handle)(3, m, NULL);` — invokes the DSO entry point with
3369/// opcode 3 (finish). no-op in Rust: zshrs has no dlopen path, all
3370/// modules are statically linked, `m->u.handle` is always null, and
3371/// finish-time resource release happens at process exit. Static-link
3372/// path defers all DSO entry calls; the no-op return preserves
3373/// caller semantics (0 = success).
3374#[allow(unused_variables)]
3375pub fn dyn_finish_module(m: *const module) -> i32 {
3376    // c:1766
3377    // c:1768 — ((int (*)(int,Module,void*)) m->u.handle)(3, m, NULL).
3378    // Static modules: no handle, opcode 3 (finish) is a no-op.
3379    0 // c:1768 success
3380}
3381
3382/// Port of `module_func(Module m, const char *name)` from `Src/module.c:1770`.
3383///
3384/// C body (DYNAMIC_NAME_CLASH_OK off — the typical case):
3385/// ```c
3386/// module_func(Module m, const char *name)
3387/// {
3388///     VARARR(char, buf, strlen(name) + strlen(m->node.nam)*2 + 1);
3389///     char const *p; char *q;
3390///     strcpy(buf, name);
3391///     q = strchr(buf, 0);
3392///     for(p = m->node.nam; *p; p++) {
3393///         if(*p == '/')      { *q++ = 'Q'; *q++ = 's'; }
3394///         else if(*p == '_') { *q++ = 'Q'; *q++ = 'u'; }
3395///         else if(*p == 'Q') { *q++ = 'Q'; *q++ = 'q'; }
3396///         else                 *q++ = *p;
3397///     }
3398///     *q = 0;
3399///     return (Module_generic_func) dlsym(m->u.handle, buf);
3400/// }
3401/// ```
3402///
3403/// Builds a mangled symbol name (`<name><module-name-mangled>`) and
3404/// dlsym's it. The mangling encodes `/` as `Qs`, `_` as `Qu`, `Q` as
3405/// `Qq` so e.g. `setup_zsh_random` becomes `setup_zshQurandom`.
3406///
3407/// Static-link path: dlsym not used; returns 0 (NULL handle).
3408#[allow(unused_variables)]
3409pub fn module_func(m: &module, name: &str) -> usize {
3410    // c:1770
3411    0 // c:1794 NULL
3412}
3413
3414/// Port of `setup_module(Module m)` from `Src/module.c:1884`.
3415///
3416/// C body:
3417/// ```c
3418/// setup_module(Module m) {
3419///     return ((m->node.flags & MOD_LINKED) ?
3420///             (m->u.linked->setup)(m) : dyn_setup_module(m));
3421/// }
3422/// ```
3423/// Symmetric with boot_module/cleanup_module/finish_module: routes
3424/// to the per-module `setup_(m)`. C `setup_` is the first lifecycle
3425/// hook called when load_module loads the module (before
3426/// do_module_features registers features and before boot_ runs).
3427/// Most per-module setup_ bodies are `return 0;` — exceptions
3428/// initialise module-private state (curses screen handle, regex
3429/// compile cache, etc.).
3430/// WARNING: param names don't match C — Rust=(_table, name) vs C=(m)
3431pub fn setup_module(_table: &mut modulestab, name: &str) -> i32 {
3432    // c:1884
3433    match name {
3434        "zsh/attr" => crate::ported::modules::attr::setup_(std::ptr::null()),
3435        "zsh/cap" => crate::ported::modules::cap::setup_(std::ptr::null()),
3436        "zsh/clone" => crate::ported::modules::clone::setup_(std::ptr::null()),
3437        "zsh/curses" => crate::ported::modules::curses::setup_(std::ptr::null()),
3438        "zsh/datetime" => crate::ported::modules::datetime::setup_(std::ptr::null()),
3439        "zsh/db/gdbm" => crate::ported::modules::db_gdbm::setup_(std::ptr::null()),
3440        "zsh/example" => crate::ported::modules::example::setup_(std::ptr::null()),
3441        "zsh/files" => crate::ported::modules::files::setup_(std::ptr::null()),
3442        "zsh/hlgroup" => crate::ported::modules::hlgroup::setup_(std::ptr::null()),
3443        "zsh/ksh93" => crate::ported::modules::ksh93::setup_(std::ptr::null()),
3444        "zsh/langinfo" => crate::ported::modules::langinfo::setup_(std::ptr::null()),
3445        "zsh/mapfile" => crate::ported::modules::mapfile::setup_(std::ptr::null()),
3446        "zsh/mathfunc" => crate::ported::modules::mathfunc::setup_(std::ptr::null()),
3447        "zsh/nearcolor" => crate::ported::modules::nearcolor::setup_(std::ptr::null()),
3448        "zsh/newuser" => crate::ported::modules::newuser::setup_(std::ptr::null()),
3449        "zsh/parameter" => crate::ported::modules::parameter::setup_(std::ptr::null()),
3450        "zsh/param/private" => crate::ported::modules::param_private::setup_(std::ptr::null()),
3451        "zsh/pcre" => crate::ported::modules::pcre::setup_(std::ptr::null()),
3452        "zsh/random" => crate::ported::modules::random::setup_(std::ptr::null()),
3453        "zsh/regex" => crate::ported::modules::regex::setup_(std::ptr::null()),
3454        "zsh/net/socket" => crate::ported::modules::socket::setup_(std::ptr::null()),
3455        "zsh/stat" => crate::ported::modules::stat::setup_(std::ptr::null()),
3456        "zsh/system" => crate::ported::modules::system::setup_(std::ptr::null()),
3457        "zsh/net/tcp" => crate::ported::modules::tcp::setup_(std::ptr::null()),
3458        "zsh/termcap" => crate::ported::modules::termcap::setup_(std::ptr::null()),
3459        "zsh/terminfo" => crate::ported::modules::terminfo::setup_(std::ptr::null()),
3460        "zsh/watch" => crate::ported::modules::watch::setup_(std::ptr::null()),
3461        "zsh/zftp" => crate::ported::modules::zftp::setup_(std::ptr::null()),
3462        "zsh/zprof" => crate::ported::modules::zprof::setup_(std::ptr::null()),
3463        "zsh/zpty" => crate::ported::modules::zpty::setup_(std::ptr::null()),
3464        "zsh/zselect" => crate::ported::modules::zselect::setup_(std::ptr::null()),
3465        "zsh/zutil" => crate::ported::modules::zutil::setup_(std::ptr::null()),
3466        "zsh/compctl" => crate::ported::zle::compctl::setup_(),
3467        _ => 0,
3468    }
3469}
3470
3471/// Port of `features_module(Module m, char ***features)` from `Src/module.c:1892`.
3472///
3473/// C body:
3474/// ```c
3475/// features_module(Module m, char ***features) {
3476///     return ((m->node.flags & MOD_LINKED) ?
3477///             (m->u.linked->features)(m, features) :
3478///             dyn_features_module(m, features));
3479/// }
3480/// ```
3481///
3482/// Per-module features_ populates the out-array with each feature
3483/// the module provides — `b:bn`, `c:cn`, `C:Cn`, `f:fn`, `p:pn`
3484/// per featuresarray (Src/module.c:3279). Used by zmodload -L to
3485/// list feature surfaces.
3486/// WARNING: param names don't match C — Rust=(_table, name, features) vs C=(m, features)
3487pub fn features_module(_table: &mut modulestab, name: &str, features: &mut Vec<String>) -> i32 {
3488    // c:1892
3489    match name {
3490        "zsh/attr" => crate::ported::modules::attr::features_(std::ptr::null(), features),
3491        "zsh/cap" => crate::ported::modules::cap::features_(std::ptr::null(), features),
3492        "zsh/clone" => crate::ported::modules::clone::features_(std::ptr::null(), features),
3493        "zsh/curses" => crate::ported::modules::curses::features_(std::ptr::null(), features),
3494        "zsh/datetime" => crate::ported::modules::datetime::features_(std::ptr::null(), features),
3495        "zsh/db/gdbm" => crate::ported::modules::db_gdbm::features_(std::ptr::null(), features),
3496        "zsh/example" => crate::ported::modules::example::features_(std::ptr::null(), features),
3497        "zsh/files" => crate::ported::modules::files::features_(std::ptr::null(), features),
3498        "zsh/hlgroup" => crate::ported::modules::hlgroup::features_(std::ptr::null(), features),
3499        "zsh/ksh93" => crate::ported::modules::ksh93::features_(std::ptr::null(), features),
3500        "zsh/langinfo" => crate::ported::modules::langinfo::features_(std::ptr::null(), features),
3501        "zsh/mapfile" => crate::ported::modules::mapfile::features_(std::ptr::null(), features),
3502        "zsh/mathfunc" => crate::ported::modules::mathfunc::features_(std::ptr::null(), features),
3503        "zsh/nearcolor" => crate::ported::modules::nearcolor::features_(std::ptr::null(), features),
3504        "zsh/newuser" => crate::ported::modules::newuser::features_(std::ptr::null(), features),
3505        "zsh/parameter" => crate::ported::modules::parameter::features_(std::ptr::null(), features),
3506        "zsh/param/private" => {
3507            crate::ported::modules::param_private::features_(std::ptr::null(), features)
3508        }
3509        "zsh/pcre" => crate::ported::modules::pcre::features_(std::ptr::null(), features),
3510        "zsh/random" => crate::ported::modules::random::features_(std::ptr::null(), features),
3511        "zsh/regex" => crate::ported::modules::regex::features_(std::ptr::null(), features),
3512        "zsh/net/socket" => crate::ported::modules::socket::features_(std::ptr::null(), features),
3513        "zsh/stat" => crate::ported::modules::stat::features_(std::ptr::null(), features),
3514        "zsh/system" => crate::ported::modules::system::features_(std::ptr::null(), features),
3515        "zsh/net/tcp" => crate::ported::modules::tcp::features_(std::ptr::null(), features),
3516        "zsh/termcap" => crate::ported::modules::termcap::features_(std::ptr::null(), features),
3517        "zsh/terminfo" => crate::ported::modules::terminfo::features_(std::ptr::null(), features),
3518        "zsh/watch" => crate::ported::modules::watch::features_(std::ptr::null(), features),
3519        // c:Src/Zle/zle_main.c:2286 — zsh/zle's features_() returns
3520        // b:zle/b:bindkey/b:vared + the p:BUFFER-family params via
3521        // featuresarray. Without this arm the dispatch fell to the
3522        // `_ => 0` default with an EMPTY features vec, so once any
3523        // zle/bindkey use marked the module loaded, the NEXT
3524        // autoloaded-builtin dispatch (ensurefeature → autofeatures
3525        // c:3558-3577 loaded-module check) found no `b:bindkey` in
3526        // the table and errored `module has no such feature` —
3527        // breaking every zinit plugin that calls `zle -N` before
3528        // `bindkey` (zsh-autopair, zsh-hist, zconvey, zui, …).
3529        "zsh/zle" => crate::ported::zle::zle_main::features_(std::ptr::null(), features),
3530        "zsh/zftp" => crate::ported::modules::zftp::features_(std::ptr::null(), features),
3531        "zsh/zprof" => crate::ported::modules::zprof::features_(std::ptr::null(), features),
3532        "zsh/zpty" => crate::ported::modules::zpty::features_(std::ptr::null(), features),
3533        "zsh/zselect" => crate::ported::modules::zselect::features_(std::ptr::null(), features),
3534        "zsh/zutil" => crate::ported::modules::zutil::features_(std::ptr::null(), features),
3535        // The three statically-linked completion modules have no per-module
3536        // features_() fn in the port, so they fell to `_ => 0` (EMPTY
3537        // features). That broke `ensurefeature`: calling an autoloadable
3538        // completion builtin (`compset`/`compadd` from a shell completer)
3539        // looked up `b:<name>` against its home module and errored "module
3540        // `zsh/X' has no such feature: `b:<name>'" (same class as the
3541        // zsh/sched `b:sched` error). The feature surface is `b:<builtin>`
3542        // per the C BUILTIN() homes (complete.c:1693-1694 / computil.c:5131-
3543        // 5138 / compctl.c:4006-4007).
3544        "zsh/complete" => {
3545            for f in ["b:compadd", "b:compset"] {
3546                features.push(f.to_string());
3547            }
3548            0
3549        }
3550        "zsh/computil" => {
3551            for f in [
3552                "b:comparguments",
3553                "b:compdescribe",
3554                "b:compfiles",
3555                "b:compgroups",
3556                "b:compquote",
3557                "b:comptags",
3558                "b:comptry",
3559                "b:compvalues",
3560            ] {
3561                features.push(f.to_string());
3562            }
3563            0
3564        }
3565        "zsh/compctl" => {
3566            for f in ["b:compctl", "b:compcall"] {
3567                features.push(f.to_string());
3568            }
3569            0
3570        }
3571        // The two statically-linked Builtins/ modules with builtins also
3572        // had no features_() fn → `_ => 0`. `zsh/sched` is the one the user
3573        // hit at session start: `zmodload zsh/sched; sched …` errored
3574        // "module `zsh/sched' has no such feature: `b:sched'". Feature
3575        // surface = `b:<builtin>` (sched.c:376 / rlimits.c limit/ulimit/
3576        // unlimit).
3577        "zsh/sched" => {
3578            features.push("b:sched".to_string());
3579            0
3580        }
3581        "zsh/rlimits" => {
3582            for f in ["b:limit", "b:ulimit", "b:unlimit"] {
3583                features.push(f.to_string());
3584            }
3585            0
3586        }
3587        _ => 0,
3588    }
3589}
3590
3591/// Port of `enables_module(Module m, int **enables)` from `Src/module.c:1901`.
3592///
3593/// C body:
3594/// ```c
3595/// enables_module(Module m, int **enables) {
3596///     return ((m->node.flags & MOD_LINKED) ?
3597///             (m->u.linked->enables)(m, enables) :
3598///             dyn_enables_module(m, enables));
3599/// }
3600/// ```
3601///
3602/// Per-module enables_ populates the enable-bit array parallel to
3603/// the features_ surface — getfeatureenables (c:3314) emits 1 for
3604/// each currently-active feature (BINF_ADDED / CONDF_ADDED /
3605/// MFF_ADDED / pd->pm non-null), 0 otherwise. Used by zmodload -L
3606/// to report enabled-vs-disabled features per module.
3607/// WARNING: param names don't match C — Rust=(_table, name, enables) vs C=(m, enables)
3608pub fn enables_module(_table: &mut modulestab, name: &str, enables: &mut Option<Vec<i32>>) -> i32 {
3609    // c:1901
3610    match name {
3611        "zsh/attr" => crate::ported::modules::attr::enables_(std::ptr::null(), enables),
3612        "zsh/cap" => crate::ported::modules::cap::enables_(std::ptr::null(), enables),
3613        "zsh/clone" => crate::ported::modules::clone::enables_(std::ptr::null(), enables),
3614        "zsh/curses" => crate::ported::modules::curses::enables_(std::ptr::null(), enables),
3615        "zsh/datetime" => crate::ported::modules::datetime::enables_(std::ptr::null(), enables),
3616        "zsh/db/gdbm" => crate::ported::modules::db_gdbm::enables_(std::ptr::null(), enables),
3617        "zsh/example" => crate::ported::modules::example::enables_(std::ptr::null(), enables),
3618        "zsh/files" => crate::ported::modules::files::enables_(std::ptr::null(), enables),
3619        "zsh/hlgroup" => crate::ported::modules::hlgroup::enables_(std::ptr::null(), enables),
3620        "zsh/ksh93" => crate::ported::modules::ksh93::enables_(std::ptr::null(), enables),
3621        "zsh/langinfo" => crate::ported::modules::langinfo::enables_(std::ptr::null(), enables),
3622        "zsh/mapfile" => crate::ported::modules::mapfile::enables_(std::ptr::null(), enables),
3623        "zsh/mathfunc" => crate::ported::modules::mathfunc::enables_(std::ptr::null(), enables),
3624        "zsh/nearcolor" => crate::ported::modules::nearcolor::enables_(std::ptr::null(), enables),
3625        "zsh/newuser" => crate::ported::modules::newuser::enables_(std::ptr::null(), enables),
3626        "zsh/parameter" => crate::ported::modules::parameter::enables_(std::ptr::null(), enables),
3627        "zsh/param/private" => {
3628            crate::ported::modules::param_private::enables_(std::ptr::null(), enables)
3629        }
3630        "zsh/pcre" => crate::ported::modules::pcre::enables_(std::ptr::null(), enables),
3631        "zsh/random" => crate::ported::modules::random::enables_(std::ptr::null(), enables),
3632        "zsh/regex" => crate::ported::modules::regex::enables_(std::ptr::null(), enables),
3633        "zsh/net/socket" => crate::ported::modules::socket::enables_(std::ptr::null(), enables),
3634        "zsh/stat" => crate::ported::modules::stat::enables_(std::ptr::null(), enables),
3635        "zsh/system" => crate::ported::modules::system::enables_(std::ptr::null(), enables),
3636        "zsh/net/tcp" => crate::ported::modules::tcp::enables_(std::ptr::null(), enables),
3637        "zsh/termcap" => crate::ported::modules::termcap::enables_(std::ptr::null(), enables),
3638        "zsh/terminfo" => crate::ported::modules::terminfo::enables_(std::ptr::null(), enables),
3639        "zsh/watch" => crate::ported::modules::watch::enables_(std::ptr::null(), enables),
3640        "zsh/zftp" => crate::ported::modules::zftp::enables_(std::ptr::null(), enables),
3641        "zsh/zprof" => crate::ported::modules::zprof::enables_(std::ptr::null(), enables),
3642        "zsh/zpty" => crate::ported::modules::zpty::enables_(std::ptr::null(), enables),
3643        "zsh/zselect" => crate::ported::modules::zselect::enables_(std::ptr::null(), enables),
3644        "zsh/zutil" => crate::ported::modules::zutil::enables_(std::ptr::null(), enables),
3645        _ => 0,
3646    }
3647}
3648
3649/// Port of `boot_module(Module m)` from `Src/module.c:1910`.
3650///
3651/// C body:
3652/// ```c
3653/// boot_module(Module m) {
3654///     return ((m->node.flags & MOD_LINKED) ?
3655///             (m->u.linked->boot)(m) : dyn_boot_module(m));
3656/// }
3657/// ```
3658///
3659/// Static-link path: every modulestab entry is MOD_LINKED, so the
3660/// branch collapses to `(m->u.linked->boot)(m)`. The Rust analog
3661/// of C's `m->u.linked->boot` function-pointer is the per-module
3662/// `boot_(m)` defined in `src/ported/modules/<name>.rs`. Dispatch
3663/// via a static name → fn-pointer table.
3664///
3665/// Modules not in the dispatch table (because their per-module
3666/// boot_ isn't ported yet) fall through to 0 — same observable
3667/// outcome as a no-op boot, matching pre-port behaviour.
3668/// WARNING: param names don't match C — Rust=(_table, name) vs C=(m)
3669pub fn boot_module(_table: &mut modulestab, name: &str) -> i32 {
3670    // c:1910
3671    // c:1912 — `(m->u.linked->boot)(m)` for MOD_LINKED.
3672    // The per-module boot_ entry points carry the partab/bintab
3673    // dispatch that addparamdef/addbuiltins would otherwise run
3674    // for a dlopen'd module (e.g. watch.rs::boot_ seeds
3675    // WATCH/watch in paramtab + installs the checksched preprompt
3676    // hook). NULL module pointer is fine — every per-module boot_
3677    // either ignores the arg or null-checks before deref.
3678    match name {
3679        "zsh/attr" => crate::ported::modules::attr::boot_(std::ptr::null()),
3680        "zsh/cap" => crate::ported::modules::cap::boot_(std::ptr::null()),
3681        "zsh/clone" => crate::ported::modules::clone::boot_(std::ptr::null()),
3682        // c:Src/Zle/complist.c:3566 boot_ — installs the `menuselect` and
3683        // `listscroll` keymaps (via menuselect_bindings). Without this,
3684        // `zmodload zsh/complist` was a no-op and every `bindkey -M
3685        // menuselect …` (zpwr's completion-menu keybindings) errored
3686        // "no such keymap `menuselect'".
3687        "zsh/complist" => crate::ported::zle::complist::boot_(),
3688        "zsh/curses" => crate::ported::modules::curses::boot_(std::ptr::null()),
3689        "zsh/datetime" => crate::ported::modules::datetime::boot_(std::ptr::null()),
3690        "zsh/db/gdbm" => crate::ported::modules::db_gdbm::boot_(std::ptr::null()),
3691        "zsh/example" => crate::ported::modules::example::boot_(std::ptr::null()),
3692        "zsh/files" => crate::ported::modules::files::boot_(std::ptr::null()),
3693        "zsh/hlgroup" => crate::ported::modules::hlgroup::boot_(std::ptr::null()),
3694        "zsh/ksh93" => crate::ported::modules::ksh93::boot_(std::ptr::null()),
3695        "zsh/langinfo" => crate::ported::modules::langinfo::boot_(std::ptr::null()),
3696        "zsh/mapfile" => crate::ported::modules::mapfile::boot_(std::ptr::null()),
3697        "zsh/mathfunc" => crate::ported::modules::mathfunc::boot_(std::ptr::null()),
3698        "zsh/nearcolor" => crate::ported::modules::nearcolor::boot_(std::ptr::null()),
3699        "zsh/newuser" => crate::ported::modules::newuser::boot_(std::ptr::null()),
3700        "zsh/parameter" => crate::ported::modules::parameter::boot_(std::ptr::null()),
3701        "zsh/param/private" => crate::ported::modules::param_private::boot_(std::ptr::null()),
3702        "zsh/pcre" => crate::ported::modules::pcre::boot_(std::ptr::null()),
3703        "zsh/random" => crate::ported::modules::random::boot_(std::ptr::null()),
3704        "zsh/regex" => crate::ported::modules::regex::boot_(std::ptr::null()),
3705        "zsh/net/socket" => crate::ported::modules::socket::boot_(std::ptr::null()),
3706        "zsh/stat" => crate::ported::modules::stat::boot_(std::ptr::null()),
3707        "zsh/system" => crate::ported::modules::system::boot_(std::ptr::null()),
3708        "zsh/net/tcp" => crate::ported::modules::tcp::boot_(std::ptr::null()),
3709        "zsh/termcap" => crate::ported::modules::termcap::boot_(std::ptr::null()),
3710        "zsh/terminfo" => crate::ported::modules::terminfo::boot_(std::ptr::null()),
3711        // c:1912 — C passes the real Module m to boot. watch::boot_
3712        // reads m->node.flags (MOD_SETUP = mid-load_module) to gate
3713        // its WATCHFMT/LOGCHECK seeding under --zsh; passing the
3714        // table entry avoids re-locking MODULESTAB (the caller chain
3715        // load_module -> do_boot_module already holds it).
3716        "zsh/watch" => {
3717            let mptr = _table
3718                .modules
3719                .get(name)
3720                .map(|m| m as *const crate::ported::zsh_h::module)
3721                .unwrap_or(std::ptr::null());
3722            crate::ported::modules::watch::boot_(mptr)
3723        }
3724        "zsh/zftp" => crate::ported::modules::zftp::boot_(std::ptr::null()),
3725        "zsh/zprof" => crate::ported::modules::zprof::boot_(std::ptr::null()),
3726        "zsh/zpty" => crate::ported::modules::zpty::boot_(std::ptr::null()),
3727        "zsh/zselect" => crate::ported::modules::zselect::boot_(std::ptr::null()),
3728        "zsh/zutil" => crate::ported::modules::zutil::boot_(std::ptr::null()),
3729        // Modules without a ported per-module boot_ (e.g. zsh/main,
3730        // zsh/complete — purely-static modules with no setup hook):
3731        // 0 == success no-op, matching the pre-port behaviour.
3732        _ => 0,
3733    }
3734}
3735
3736/// Port of `cleanup_module(Module m)` from `Src/module.c:1918`.
3737///
3738/// C body:
3739/// ```c
3740/// cleanup_module(Module m) {
3741///     return ((m->node.flags & MOD_LINKED) ?
3742///             (m->u.linked->cleanup)(m) : dyn_cleanup_module(m));
3743/// }
3744/// ```
3745///
3746/// Static-link path: dispatch via name → fn-pointer table to the
3747/// per-module `cleanup_(m)` defined in `src/ported/modules/<name>.rs`.
3748/// Mirrors the symmetric `boot_module` dispatcher (`Src/module.c:1910`).
3749/// Per-module cleanup_ typically calls `delprepromptfn` /
3750/// `setfeatureenables(NULL)` to roll back what boot_ installed.
3751/// WARNING: param names don't match C — Rust=(_table, name) vs C=(m)
3752pub fn cleanup_module(_table: &mut modulestab, name: &str) -> i32 {
3753    // c:1918
3754    match name {
3755        "zsh/attr" => crate::ported::modules::attr::cleanup_(std::ptr::null()),
3756        "zsh/cap" => crate::ported::modules::cap::cleanup_(std::ptr::null()),
3757        "zsh/clone" => crate::ported::modules::clone::cleanup_(std::ptr::null()),
3758        "zsh/curses" => crate::ported::modules::curses::cleanup_(std::ptr::null()),
3759        "zsh/datetime" => crate::ported::modules::datetime::cleanup_(std::ptr::null()),
3760        "zsh/db/gdbm" => crate::ported::modules::db_gdbm::cleanup_(std::ptr::null()),
3761        "zsh/example" => crate::ported::modules::example::cleanup_(std::ptr::null()),
3762        "zsh/files" => crate::ported::modules::files::cleanup_(std::ptr::null()),
3763        "zsh/hlgroup" => crate::ported::modules::hlgroup::cleanup_(std::ptr::null()),
3764        "zsh/ksh93" => crate::ported::modules::ksh93::cleanup_(std::ptr::null()),
3765        "zsh/langinfo" => crate::ported::modules::langinfo::cleanup_(std::ptr::null()),
3766        "zsh/mapfile" => crate::ported::modules::mapfile::cleanup_(std::ptr::null()),
3767        "zsh/mathfunc" => crate::ported::modules::mathfunc::cleanup_(std::ptr::null()),
3768        "zsh/nearcolor" => crate::ported::modules::nearcolor::cleanup_(std::ptr::null()),
3769        "zsh/newuser" => crate::ported::modules::newuser::cleanup_(std::ptr::null()),
3770        "zsh/parameter" => crate::ported::modules::parameter::cleanup_(std::ptr::null()),
3771        "zsh/param/private" => crate::ported::modules::param_private::cleanup_(std::ptr::null()),
3772        "zsh/pcre" => crate::ported::modules::pcre::cleanup_(std::ptr::null()),
3773        "zsh/random" => crate::ported::modules::random::cleanup_(std::ptr::null()),
3774        "zsh/regex" => crate::ported::modules::regex::cleanup_(std::ptr::null()),
3775        "zsh/net/socket" => crate::ported::modules::socket::cleanup_(std::ptr::null()),
3776        "zsh/stat" => crate::ported::modules::stat::cleanup_(std::ptr::null()),
3777        "zsh/system" => crate::ported::modules::system::cleanup_(std::ptr::null()),
3778        "zsh/net/tcp" => crate::ported::modules::tcp::cleanup_(std::ptr::null()),
3779        "zsh/termcap" => crate::ported::modules::termcap::cleanup_(std::ptr::null()),
3780        "zsh/terminfo" => crate::ported::modules::terminfo::cleanup_(std::ptr::null()),
3781        "zsh/watch" => crate::ported::modules::watch::cleanup_(std::ptr::null()),
3782        "zsh/zftp" => crate::ported::modules::zftp::cleanup_(std::ptr::null()),
3783        "zsh/zprof" => crate::ported::modules::zprof::cleanup_(std::ptr::null()),
3784        "zsh/zpty" => crate::ported::modules::zpty::cleanup_(std::ptr::null()),
3785        "zsh/zselect" => crate::ported::modules::zselect::cleanup_(std::ptr::null()),
3786        "zsh/zutil" => crate::ported::modules::zutil::cleanup_(std::ptr::null()),
3787        _ => 0,
3788    }
3789}
3790
3791/// Port of `finish_module(Module m)` from `Src/module.c:1926`.
3792///
3793/// C body:
3794/// ```c
3795/// finish_module(Module m) {
3796///     return ((m->node.flags & MOD_LINKED) ?
3797///             (m->u.linked->finish)(m) : dyn_finish_module(m));
3798/// }
3799/// ```
3800///
3801/// Static-link path: dispatch to the per-module `finish_(m)`.
3802/// finish_ is the second teardown step (after cleanup_) — it frees
3803/// process-lifetime state that survives a single `zmodload -u`.
3804/// Most modules have an empty body returning 0; a few (zftp, zpty,
3805/// watch with utmpx descriptors) keep state explicitly.
3806/// WARNING: param names don't match C — Rust=(_table, name) vs C=(m)
3807pub fn finish_module(_table: &mut modulestab, name: &str) -> i32 {
3808    // c:1926
3809    match name {
3810        "zsh/attr" => crate::ported::modules::attr::finish_(std::ptr::null()),
3811        "zsh/cap" => crate::ported::modules::cap::finish_(std::ptr::null()),
3812        "zsh/clone" => crate::ported::modules::clone::finish_(std::ptr::null()),
3813        "zsh/curses" => crate::ported::modules::curses::finish_(std::ptr::null()),
3814        "zsh/datetime" => crate::ported::modules::datetime::finish_(std::ptr::null()),
3815        "zsh/db/gdbm" => crate::ported::modules::db_gdbm::finish_(std::ptr::null()),
3816        "zsh/example" => crate::ported::modules::example::finish_(std::ptr::null()),
3817        "zsh/files" => crate::ported::modules::files::finish_(std::ptr::null()),
3818        "zsh/hlgroup" => crate::ported::modules::hlgroup::finish_(std::ptr::null()),
3819        "zsh/ksh93" => crate::ported::modules::ksh93::finish_(std::ptr::null()),
3820        "zsh/langinfo" => crate::ported::modules::langinfo::finish_(std::ptr::null()),
3821        "zsh/mapfile" => crate::ported::modules::mapfile::finish_(std::ptr::null()),
3822        "zsh/mathfunc" => crate::ported::modules::mathfunc::finish_(std::ptr::null()),
3823        "zsh/nearcolor" => crate::ported::modules::nearcolor::finish_(std::ptr::null()),
3824        "zsh/newuser" => crate::ported::modules::newuser::finish_(std::ptr::null()),
3825        "zsh/parameter" => crate::ported::modules::parameter::finish_(std::ptr::null()),
3826        "zsh/param/private" => crate::ported::modules::param_private::finish_(std::ptr::null()),
3827        "zsh/pcre" => crate::ported::modules::pcre::finish_(std::ptr::null()),
3828        "zsh/random" => crate::ported::modules::random::finish_(std::ptr::null()),
3829        "zsh/regex" => crate::ported::modules::regex::finish_(std::ptr::null()),
3830        "zsh/net/socket" => crate::ported::modules::socket::finish_(std::ptr::null()),
3831        "zsh/stat" => crate::ported::modules::stat::finish_(std::ptr::null()),
3832        "zsh/system" => crate::ported::modules::system::finish_(std::ptr::null()),
3833        "zsh/net/tcp" => crate::ported::modules::tcp::finish_(std::ptr::null()),
3834        "zsh/termcap" => crate::ported::modules::termcap::finish_(std::ptr::null()),
3835        "zsh/terminfo" => crate::ported::modules::terminfo::finish_(std::ptr::null()),
3836        "zsh/watch" => crate::ported::modules::watch::finish_(std::ptr::null()),
3837        "zsh/zftp" => crate::ported::modules::zftp::finish_(std::ptr::null()),
3838        "zsh/zprof" => crate::ported::modules::zprof::finish_(std::ptr::null()),
3839        "zsh/zpty" => crate::ported::modules::zpty::finish_(std::ptr::null()),
3840        "zsh/zselect" => crate::ported::modules::zselect::finish_(std::ptr::null()),
3841        "zsh/zutil" => crate::ported::modules::zutil::finish_(std::ptr::null()),
3842        _ => 0,
3843    }
3844}
3845
3846/// Port of `do_module_features(Module m, Feature_enables enablesarr, int flags)` from `Src/module.c:1998`.
3847///
3848/// C body c:1998-2125 (128 lines):
3849/// ```c
3850/// if (features_module(m, &features) == 0) {
3851///     int *enables = NULL;
3852///     if (enables_module(m, &enables)) {
3853///         if (!(flags & FEAT_IGNORE)) zwarn(...);
3854///         return 1;
3855///     }
3856///     if ((flags & FEAT_CHECKAUTO) && m->autoloads) {
3857///         /* validate autoloads against features list */
3858///         /* on mismatch: zwarn + autofeatures(REMOVE|IGNORE) +
3859///            expunge from enablesarr */
3860///     }
3861///     if (enablesarr) {
3862///         /* walk enablesarr, flip enables bits per +/- prefix */
3863///     } else {
3864///         /* enable all features */
3865///     }
3866///     if (enables_module(m, &enables)) return 2;
3867/// } else if (enablesarr) {
3868///     if (!(flags & FEAT_IGNORE)) zwarn("module does not support features");
3869///     return 1;
3870/// }
3871/// return ret;
3872/// ```
3873///
3874/// Prior Rust port confused module-name and enables-string into a
3875/// single `enablesarr: &str` param — the module-lookup and zwarn
3876/// diagnostic both used the feature-list string instead of the
3877/// module's name. Signature now separates them per C semantics:
3878/// modname identifies the module, features is the list of features
3879/// to enable (None = "enable all").
3880/// WARNING: param names don't match C — Rust=(table, modname, features, flags) vs C=(m, enablesarr, flags)
3881pub fn do_module_features(
3882    table: &mut modulestab,
3883    modname: &str,
3884    features: Option<&[String]>,
3885    flags: i32,
3886) -> i32 {
3887    // c:1998
3888    let mut module_features: Vec<String> = Vec::new(); // c:2000
3889    let mut ret: i32 = 0; // c:2001
3890
3891    // c:2003 — `if (features_module(m, &features) == 0)`.
3892    if features_module(table, modname, &mut module_features) == 0 {
3893        // c:2011-2018 — fetch enables. Features supported → enables
3894        // should be too; error here is reported unless FEAT_IGNORE.
3895        let mut enables: Option<Vec<i32>> = None;
3896        if enables_module(table, modname, &mut enables) != 0 {
3897            // c:2012
3898            if (flags & FEAT_IGNORE) == 0 {
3899                // c:2014
3900                zwarn(&format!(
3901                    "error getting enabled features for module `{}'", // c:2015
3902                    modname
3903                ));
3904            }
3905            return 1; // c:2017
3906        }
3907
3908        // c:2020 — `if ((flags & FEAT_CHECKAUTO) && m->autoloads)`
3909        if (flags & FEAT_CHECKAUTO) != 0 {
3910            let autoloads: Vec<String> = table
3911                .modules
3912                .get(modname)
3913                .and_then(|m| m.autoloads.as_ref())
3914                .map(|al| al.iter().cloned().collect())
3915                .unwrap_or_default();
3916            // c:2027-2074 — walk autoloads, cancel mismatches.
3917            for al in &autoloads {
3918                // c:2028
3919                // c:2032-2034 — match `al` against the features array.
3920                let found = module_features.iter().any(|f| f == al);
3921                if !found {
3922                    // c:2035
3923                    if (flags & FEAT_IGNORE) == 0 {
3924                        // c:2037
3925                        zwarn(&format!(
3926                            "module `{}' has no such feature: `{}': autoload cancelled", // c:2038-2040
3927                            modname, al
3928                        ));
3929                    }
3930                    // c:2045-2047 — autofeatures(NULL, m->node.nam, arg, 0, FEAT_IGNORE|FEAT_REMOVE)
3931                    let arg = vec![al.clone()];
3932                    autofeatures(table, "", Some(modname), &arg, 0, FEAT_IGNORE | FEAT_REMOVE);
3933                    // c:2053-2072 — expunge from enablesarr.
3934                    // features arg is &[String] — Rust slice can't be
3935                    // mutated through &. The C path mutates the passed
3936                    // Feature_enables array; the Rust callers that need
3937                    // expunge build a fresh list. Skipped here.
3938                }
3939            }
3940        }
3941
3942        // c:2077-2113 — apply enablesarr (or enable all).
3943        match features {
3944            Some(arr) => {
3945                // c:2079-2103 — walk enablesarr.
3946                let enables_vec = enables.get_or_insert_with(Vec::new);
3947                if enables_vec.len() < module_features.len() {
3948                    enables_vec.resize(module_features.len(), 0);
3949                }
3950                for fep_str in arr {
3951                    // c:2079 for (fep = enablesarr; fep->str; fep++)
3952                    let (on, esp) = if let Some(rest) = fep_str.strip_prefix('+') {
3953                        // c:2082-2083
3954                        (1i32, rest)
3955                    } else if let Some(rest) = fep_str.strip_prefix('-') {
3956                        // c:2084-2086
3957                        (0i32, rest)
3958                    } else {
3959                        (1i32, fep_str.as_str())
3960                    };
3961                    // c:2088-2094 — exact name match (pattern path
3962                    // skipped: zshrs doesn't carry the patprog from
3963                    // Feature_enables).
3964                    let mut found = false;
3965                    for (i, f) in module_features.iter().enumerate() {
3966                        if f == esp {
3967                            enables_vec[i] = on; // c:2090
3968                            found = true;
3969                            break; // c:2093 break-on-non-pat
3970                        }
3971                    }
3972                    if !found {
3973                        // c:2095-2102 — `module has no such feature`.
3974                        if (flags & FEAT_IGNORE) == 0 {
3975                            zwarn(&format!(
3976                                "module `{}' has no such feature: `{}'",
3977                                modname, esp
3978                            ));
3979                        }
3980                        return 1; // c:2101
3981                    }
3982                }
3983            }
3984            None => {
3985                // c:2105-2112 — enable all features.
3986                let enables_vec = enables.get_or_insert_with(Vec::new);
3987                enables_vec.clear();
3988                enables_vec.resize(module_features.len(), 1);
3989            }
3990        }
3991
3992        // c:2115 — final `enables_module(m, &enables)` commits the bits.
3993        if enables_module(table, modname, &mut enables) != 0 {
3994            return 2; // c:2116
3995        }
3996    } else if features.is_some() {
3997        // c:2117-2121 — features_module failed AND enablesarr non-NULL:
3998        // module doesn't support features. zwarn unless FEAT_IGNORE.
3999        if (flags & FEAT_IGNORE) == 0 {
4000            zwarn(&format!(
4001                "module `{}' does not support features", // c:2119
4002                modname
4003            ));
4004        }
4005        return 1; // c:2120
4006    }
4007    // c:2122 — `Else it doesn't support features but we don't care.`
4008    let _ = &mut ret;
4009    ret // c:2124
4010}
4011
4012/// Port of `deletemathfunc(MathFunc f)` from `Src/module.c:1342`.
4013///
4014/// C body:
4015/// ```c
4016/// deletemathfunc(MathFunc f) {
4017///     MathFunc p, q;
4018///     for (p = mathfuncs, q = NULL; p && p != f; q = p, p = p->next);
4019///     if (p) {
4020///         if (q) q->next = f->next; else mathfuncs = f->next;
4021///         if (f->module) {
4022///             zsfree(f->name); zsfree(f->module); zfree(f, sizeof(*f));
4023///         } else
4024///             f->flags &= ~MFF_ADDED;
4025///         return 0;
4026///     }
4027///     return -1;
4028/// }
4029/// ```
4030///
4031/// Removes math function `f` from the global registry. Returns 0
4032/// on hit, -1 on miss.
4033
4034/// Port of `do_boot_module(Module m, Feature_enables enablesarr, int silent)` from `Src/module.c:2139`.
4035///
4036/// C body:
4037/// ```c
4038/// do_boot_module(Module m, Feature_enables enablesarr, int silent)
4039/// {
4040///     int ret = do_module_features(m, enablesarr,
4041///                                  silent ? FEAT_IGNORE|FEAT_CHECKAUTO :
4042///                                  FEAT_CHECKAUTO);
4043///     if (ret == 1) return 1;
4044///     if (boot_module(m)) return 1;
4045///     return ret;
4046/// }
4047/// ```
4048pub fn do_boot_module(
4049    table: &mut modulestab,
4050    modname: &str,
4051    features: Option<&[String]>,
4052    silent: i32,
4053) -> i32 {
4054    // c:2139
4055    let flags = if silent != 0 {
4056        // c:2142 — silent → IGNORE | CHECKAUTO
4057        FEAT_IGNORE | FEAT_CHECKAUTO
4058    } else {
4059        FEAT_CHECKAUTO // c:2143
4060    };
4061    let ret = do_module_features(table, modname, features, flags); // c:2141
4062    if ret == 1 {
4063        // c:2145
4064        return 1; // c:2146
4065    }
4066    if boot_module(table, modname) != 0 {
4067        // c:2148
4068        return 1; // c:2149
4069    }
4070    ret // c:2150
4071}
4072
4073/// Port of `do_cleanup_module(Module m)` from `Src/module.c:2159`.
4074///
4075/// C body:
4076/// ```c
4077/// do_cleanup_module(Module m) {
4078///     return (m->node.flags & MOD_LINKED) ?
4079///         (m->u.linked && m->u.linked->cleanup(m)) :
4080///         (m->u.handle && cleanup_module(m));
4081/// }
4082/// ```
4083/// WARNING: param names don't match C — Rust=(table, name) vs C=(m)
4084pub fn do_cleanup_module(table: &mut modulestab, name: &str) -> i32 {
4085    // c:2159
4086    // Check the module is registered, then dispatch to cleanup_module.
4087    if table.modules.contains_key(name) {
4088        // c:2162 m->u.linked
4089        cleanup_module(table, name) // c:2163 cleanup_module(m)
4090    } else {
4091        0
4092    }
4093}
4094
4095/// Port of `modname_ok(char const *p)` from `Src/module.c:2173`.
4096///
4097/// Returns 1 iff `p` is a valid module name: one or more
4098/// `/`-separated identifier segments.
4099///
4100/// C body:
4101/// ```c
4102/// modname_ok(char const *p)
4103/// {
4104///     do {
4105///         p = itype_end(p, IIDENT, 0);
4106///         if (!*p)
4107///             return 1;
4108///     } while(*p++ == '/');
4109///     return 0;
4110/// }
4111/// ```
4112pub fn modname_ok(p: &str) -> i32 {
4113    // c:2173
4114    let bytes = p.as_bytes();
4115    let mut i: usize = 0;
4116    loop {
4117        // c:2176 — `p = itype_end(p, IIDENT, 0);`
4118        // IIDENT = identifier-byte (alpha/digit/underscore + extended).
4119        while i < bytes.len() {
4120            let b = bytes[i];
4121            // Inline IIDENT check — alphanumeric or underscore. Mirrors
4122            // utils.c:itype_end stepping for the IIDENT bit.
4123            if b.is_ascii_alphanumeric() || b == b'_' {
4124                i += 1;
4125            } else {
4126                break;
4127            }
4128        }
4129        if i >= bytes.len() {
4130            // c:2177 if (!*p)
4131            return 1; // c:2178
4132        }
4133        if bytes[i] != b'/' {
4134            break;
4135        } // c:2179 while(*p++ == '/')
4136        i += 1;
4137    }
4138    0 // c:2180
4139}
4140
4141/// Port of `removemathfunc(MathFunc previous, MathFunc current)` from `Src/module.c:1267`.
4142///
4143/// C body:
4144/// ```c
4145/// removemathfunc(MathFunc previous, MathFunc current)
4146/// {
4147///     if (previous)
4148///         previous->next = current->next;
4149///     else
4150///         mathfuncs = current->next;
4151///     zsfree(current->name);
4152///     zsfree(current->module);
4153///     zfree(current, sizeof(*current));
4154/// }
4155/// ```
4156///
4157/// Unlinks `current` from the global `mathfuncs` list and frees it.
4158/// Rust port: `previous` is unused since the underlying HashMap
4159/// removal doesn't need predecessor tracking.
4160
4161/// Port of `require_module(const char *module, Feature_enables features, int silent)` from `Src/module.c:2344`.
4162///
4163/// C body c:2342-2360:
4164/// ```c
4165/// mod_export int
4166/// require_module(const char *module, Feature_enables features, int silent)
4167/// {
4168///     Module m = NULL;
4169///     int ret = 0;
4170///     queue_signals();
4171///     m = find_module(module, FINDMOD_ALIASP, &module);
4172///     if (!m || !m->u.handle ||
4173///         (m->node.flags & MOD_UNLOAD))
4174///         ret = load_module(module, features, silent);
4175///     else
4176///         ret = do_module_features(m, features, 0);
4177///     unqueue_signals();
4178///     return ret;
4179/// }
4180/// ```
4181///
4182/// Two branches: when the module isn't loaded yet (or is mid-unload),
4183/// route through `load_module` which runs the full setup → features →
4184/// boot lifecycle. When it's already loaded, skip to
4185/// `do_module_features` to just enable the requested per-feature
4186/// surface — much cheaper.
4187///
4188/// Static-link analog of `m->u.handle` is `MOD_INIT_B` (boot ran):
4189/// once boot_module has fired, the module is "loaded" in zshrs's
4190/// non-dlopen world.
4191/// WARNING: param names don't match C — Rust=(table, modname, features, silent) vs C=(module, features, silent)
4192pub fn require_module(
4193    table: &mut modulestab,
4194    modname: &str,
4195    features: Option<&[String]>,
4196    silent: i32,
4197) -> i32 {
4198    // c:2344
4199    // c:2350 — queue_signals(): signal-deferral wrapper.
4200    crate::ported::signals::queue_signals();
4201
4202    // c:2351 — `m = find_module(module, FINDMOD_ALIASP, &module);`
4203    // Resolves alias chain; canonical name lives in `mname`.
4204    let mname_opt = find_module(table, modname, FINDMOD_ALIASP);
4205
4206    // c:2352-2353 — `if (!m || !m->u.handle || MOD_UNLOAD)`.
4207    // Static-link analog of `m->u.handle`: MOD_INIT_B (boot ran).
4208    let needs_load = match &mname_opt {
4209        None => true, // c:2352 !m
4210        Some(mname) => match table.modules.get(mname) {
4211            None => true,
4212            Some(m) => {
4213                (m.node.flags & MOD_INIT_B) == 0 // c:2352 !u.handle analog
4214                    || (m.node.flags & MOD_UNLOAD) != 0 // c:2353
4215            }
4216        },
4217    };
4218
4219    let mname = mname_opt.unwrap_or_else(|| modname.to_string());
4220
4221    let ret = if needs_load {
4222        // c:2354 — `ret = load_module(module, features, silent);`
4223        // try_load_module gates the static-link path. On miss, emit
4224        // the canonical zwarn (gated by silent).
4225        if try_load_module(table, &mname) == 0 {
4226            if silent == 0 {
4227                crate::ported::utils::zwarn(&format!("failed to load module `{}'", mname));
4228            }
4229            crate::ported::signals::unqueue_signals();
4230            return 1;
4231        }
4232        if !table.load_module(&mname) {
4233            crate::ported::signals::unqueue_signals();
4234            return 1;
4235        }
4236        0
4237    } else {
4238        // c:2356 — `ret = do_module_features(m, features, 0);`
4239        // Module already loaded; just enable the requested features.
4240        // features=NULL in C means "enable all features"; the Rust
4241        // do_module_features takes a single enablesstr arg, so flatten
4242        // C: features=NULL means "enable all"; pass through.
4243        do_module_features(table, &mname, features, 0)
4244    };
4245
4246    // c:2357 — unqueue_signals();
4247    crate::ported::signals::unqueue_signals();
4248
4249    ret // c:2359
4250}
4251
4252/// Port of `add_dep(const char *name, char *from)` from `Src/module.c:2369`.
4253///
4254/// C body:
4255/// ```c
4256/// add_dep(const char *name, char *from)
4257/// {
4258///     LinkNode node;
4259///     Module m;
4260///     m = find_module(name, FINDMOD_ALIASP|FINDMOD_CREATE, &name);
4261///     if (!m->deps)
4262///         m->deps = znewlinklist();
4263///     for (node = firstnode(m->deps);
4264///          node && strcmp((char *) getdata(node), from);
4265///          incnode(node));
4266///     if (!node)
4267///         zaddlinknode(m->deps, ztrdup(from));
4268/// }
4269/// ```
4270///
4271/// Records that module `name` depends on module `from`. Resolves
4272/// aliases so dependency graphs always point at canonical names.
4273/// WARNING: param names don't match C — Rust=(table, name, from) vs C=(name, from)
4274pub fn add_dep(table: &mut modulestab, name: &str, from: &str) -> i32 {
4275    // c:2369
4276    // c:2369 — m = find_module(name, FINDMOD_ALIASP|FINDMOD_CREATE, &name)
4277    let canon = match find_module(table, name, FINDMOD_ALIASP | FINDMOD_CREATE) {
4278        Some(n) => n,
4279        None => return 0,
4280    };
4281    if let Some(m) = table.modules.get_mut(&canon) {
4282        // c:2389-2391 — walk deps, skip if `from` already present.
4283        let deps = m
4284            .deps
4285            .get_or_insert_with(crate::ported::linklist::LinkList::new);
4286        if !deps.iter().any(|d| d == from) {
4287            // c:2392 if (!node)
4288            deps.push_back(from.to_string()); // c:2393 zaddlinknode
4289        }
4290    }
4291    0
4292}
4293
4294/// Port of `autoloadscan(HashNode hn, int printflags)` from `Src/module.c:2403`.
4295///
4296/// C body:
4297/// ```c
4298/// autoloadscan(HashNode hn, int printflags)
4299/// {
4300///     Builtin bn = (Builtin) hn;
4301///     if(bn->node.flags & BINF_ADDED)
4302///         return;
4303///     if(printflags & PRINT_LIST) {
4304///         fputs("zmodload -ab ", stdout);
4305///         if(bn->optstr[0] == '-') fputs("-- ", stdout);
4306///         quotedzputs(bn->optstr, stdout);
4307///         if(strcmp(bn->node.nam, bn->optstr)) {
4308///             putchar(' ');
4309///             quotedzputs(bn->node.nam, stdout);
4310///         }
4311///     } else {
4312///         nicezputs(bn->node.nam, stdout);
4313///         if(strcmp(bn->node.nam, bn->optstr)) {
4314///             fputs(" (", stdout);
4315///             nicezputs(bn->optstr, stdout);
4316///             putchar(')');
4317///         }
4318///     }
4319///     putchar('\n');
4320/// }
4321/// ```
4322///
4323/// Hash-table scan callback for autoloadable-builtin listing.
4324/// `printflags & PRINT_LIST` selects long form (`zmodload -ab MOD NAME`)
4325/// vs short form (`NAME (MOD)`). Skips already-registered builtins
4326/// (BINF_ADDED set).
4327/// WARNING: param names don't match C — Rust=(name, optstr, flags, printflags) vs C=(hn, printflags)
4328pub fn autoloadscan(name: &str, optstr: &str, flags: u32, printflags: i32) {
4329    // c:2403
4330    use crate::ported::utils::{nicezputs, quotedzputs};
4331    let mut stdout = std::io::stdout();
4332    if (flags & BINF_ADDED) != 0 {
4333        // c:2407
4334        return; // c:2408
4335    }
4336    if (printflags & PRINT_LIST) != 0 {
4337        // c:2409
4338        // c:2410-2417 — long form `zmodload -ab MOD NAME`
4339        print!("zmodload -ab ");
4340        if optstr.starts_with('-') {
4341            // c:2411
4342            print!("-- "); // c:2412
4343        }
4344        // c:2413 — `quotedzputs(bn->optstr, stdout);`
4345        print!("{}", quotedzputs(optstr));
4346        if name != optstr {
4347            // c:2414 — `if(strcmp(bn->node.nam, bn->optstr))`
4348            print!(" "); // c:2415 putchar(' ')
4349                         // c:2416 — `quotedzputs(bn->node.nam, stdout);`
4350            print!("{}", quotedzputs(name));
4351        }
4352    } else {
4353        // c:2418-2424 — short form `NAME (MOD)`
4354        let _ = nicezputs(name, &mut stdout); // c:2419
4355        if name != optstr {
4356            // c:2420 — `if(strcmp(bn->node.nam, bn->optstr))`
4357            print!(" ("); // c:2421
4358            let _ = nicezputs(optstr, &mut stdout); // c:2422
4359            print!(")"); // c:2423
4360        }
4361    }
4362    println!(); // c:2426 putchar('\n')
4363}
4364
4365/// Direct port of `bin_zmodload(char *nam, char **args, Options ops, UNUSED(int func))` from `Src/module.c:2440`.
4366/// Top-level dispatcher for the `zmodload` builtin. Validates flag
4367/// combinations then routes to one of the per-mode helpers:
4368///   -F        → bin_zmodload_features (c:3003)
4369///   -e        → bin_zmodload_exist    (c:2623)
4370///   -d        → bin_zmodload_dep      (c:2649)
4371///   -a/-b/-c/-p/-f → bin_zmodload_auto (c:2726)
4372///   default   → bin_zmodload_load     (c:2971)
4373///   -A/-R     → bin_zmodload_alias    (c:2515)
4374/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
4375pub fn bin_zmodload(
4376    nam: &str,
4377    args: &[String], // c:2440
4378    ops: &options,
4379    _func: i32,
4380) -> i32 {
4381    let mut table = MODULESTAB.lock().unwrap();
4382    let table = &mut *table;
4383
4384    let ops_bcpf = OPT_ISSET(ops, b'b') || OPT_ISSET(ops, b'c')              // c:2443
4385                || OPT_ISSET(ops, b'p') || OPT_ISSET(ops, b'f');
4386    let ops_au = OPT_ISSET(ops, b'a') || OPT_ISSET(ops, b'u'); // c:2445
4387    let mut ret: i32; // c:2446
4388
4389    if ops_bcpf && !ops_au {
4390        // c:2451
4391        zwarnnam(nam, "-b, -c, -f, and -p must be combined with -a or -u"); // c:2452
4392        return 1; // c:2453
4393    }
4394    if OPT_ISSET(ops, b'F') && (ops_bcpf || OPT_ISSET(ops, b'u')) {
4395        // c:2455
4396        zwarnnam(nam, "-b, -c, -f, -p and -u cannot be combined with -F"); // c:2456
4397        return 1; // c:2457
4398    }
4399    // !!! WARNING: RUST-ONLY EXTENSION — NO C COUNTERPART !!!
4400    // zshrs repurposes `-R` WITHOUT `-A` to load a native (Rust)
4401    // plugin cdylib through the stable C ABI (src/extensions/
4402    // plugin_host.rs): the first compiled Unix shell hosting
4403    // compiled-language plugins loaded at runtime. In C zsh `-R` means
4404    // "remove module alias" and is only ever meaningful alongside `-A`
4405    // (Src/module.c:2459); that behaviour is preserved for `-A -R name`
4406    // (both flags set falls through to bin_zmodload_alias below), so no
4407    // parity is lost for the near-unused alias-removal case.
4408    //   zmodload -R  <path>...    load each plugin cdylib
4409    //   zmodload -R               list loaded plugins
4410    //   zmodload -uR <name>...    unload each plugin by name
4411    if OPT_ISSET(ops, b'R') && !OPT_ISSET(ops, b'A') {
4412        // Placed before the c:2490 queue_signals, so nothing to unqueue.
4413        // Handler lives in the extensions tree (not a port) —
4414        // src/extensions/plugin_host.rs.
4415        return crate::plugin_host::zmodload_rust_cmd(nam, args, ops);
4416    }
4417    if OPT_ISSET(ops, b'A') || OPT_ISSET(ops, b'R') {
4418        // c:2459
4419        if ops_bcpf || ops_au || OPT_ISSET(ops, b'd')                        // c:2460
4420           || (OPT_ISSET(ops, b'R') && OPT_ISSET(ops, b'e'))
4421        {
4422            zwarnnam(nam, "illegal flags combined with -A or -R"); // c:2462
4423            return 1; // c:2463
4424        }
4425        if !OPT_ISSET(ops, b'e') {
4426            // c:2465
4427            return bin_zmodload_alias(table, nam, args, ops); // c:2466
4428        }
4429    }
4430    if OPT_ISSET(ops, b'd') && OPT_ISSET(ops, b'a') {
4431        // c:2468
4432        zwarnnam(nam, "-d cannot be combined with -a"); // c:2469
4433        return 1; // c:2470
4434    }
4435    if OPT_ISSET(ops, b'u') && args.is_empty() {
4436        // c:2472
4437        zwarnnam(nam, "what do you want to unload?"); // c:2473
4438        return 1; // c:2474
4439    }
4440    if OPT_ISSET(ops, b'e')
4441        && (OPT_ISSET(ops, b'I') || OPT_ISSET(ops, b'L') // c:2476
4442        || (OPT_ISSET(ops, b'a') && !OPT_ISSET(ops, b'F'))
4443        || OPT_ISSET(ops, b'd') || OPT_ISSET(ops, b'i')
4444        || OPT_ISSET(ops, b'u'))
4445    {
4446        zwarnnam(nam, "-e cannot be combined with other options"); // c:2480
4447        return 1; // c:2482
4448    }
4449    // c:2484 — `for (fp = fonly; *fp; fp++)` — `l` and `P` only with `-F`.
4450    for fp in [b'l', b'P'] {
4451        // c:2484
4452        if OPT_ISSET(ops, fp) && !OPT_ISSET(ops, b'F') {
4453            // c:2485
4454            zwarnnam(nam, &format!("-{} is only allowed with -F", fp as char)); // c:2486
4455            return 1; // c:2487
4456        }
4457    }
4458    crate::ported::mem::queue_signals(); // c:2490
4459    if OPT_ISSET(ops, b'F') {
4460        // c:2491
4461        ret = bin_zmodload_features(table, nam, args, ops); // c:2492
4462    } else if OPT_ISSET(ops, b'e') {
4463        // c:2493
4464        ret = bin_zmodload_exist(table, nam, args, ops); // c:2494
4465    } else if OPT_ISSET(ops, b'd') {
4466        // c:2495
4467        ret = bin_zmodload_dep(table, nam, args, ops); // c:2496
4468    } else {
4469        let autoopts = (OPT_ISSET(ops, b'b') as i32)                         // c:2497
4470                     + (OPT_ISSET(ops, b'c') as i32)
4471                     + (OPT_ISSET(ops, b'p') as i32)
4472                     + (OPT_ISSET(ops, b'f') as i32);
4473        if autoopts != 0 || OPT_ISSET(ops, b'a') {
4474            // c:2497-2499
4475            if autoopts > 1 {
4476                // c:2502
4477                zwarnnam(nam, "use only one of -b, -c, or -p"); // c:2503
4478                ret = 1; // c:2504
4479            } else {
4480                ret = bin_zmodload_auto(table, nam, args, ops); // c:2506
4481            }
4482        } else {
4483            ret = bin_zmodload_load(table, nam, args, ops); // c:2508
4484        }
4485    }
4486    unqueue_signals(); // c:2515
4487    ret // c:2515
4488}
4489
4490/// Port of `bin_zmodload_alias(char *nam, char **args, Options ops)` from `Src/module.c:2515`.
4491///
4492/// `zmodload -A [-L|-R] [name=alias ...]`. Three modes:
4493/// - no args: list all module aliases (`-L` = long form).
4494/// - `-R name`: remove alias `name` (must already be MOD_ALIAS).
4495/// - `name=target`: install/replace alias `name` pointing at `target`.
4496///   Detects self-cycles before committing.
4497/// WARNING: param names don't match C — Rust=(table, nam, args, ops) vs C=(nam, args, ops)
4498pub fn bin_zmodload_alias(
4499    table: &mut modulestab,
4500    nam: &str,
4501    args: &[String],
4502    ops: &options,
4503) -> i32 {
4504    // c:2515
4505    /*
4506     * TODO: while it would be too nasty to have aliases, as opposed
4507     * to real loadable modules, with dependencies --- just what would
4508     * we need to load when, exactly? --- there is in principle no objection
4509     * to making it possible to force an alias onto an existing unloaded
4510     * module which has dependencies.  This would simply transfer
4511     * the dependencies down the line to the aliased-to module name.
4512     * This is actually useful, since then you can alias zsh/zle=mytestzle
4513     * to load another version of zle.  But then what happens when the
4514     * alias is removed?  Do you transfer the dependencies back? And
4515     * suppose other names are aliased to the same file?  It might be
4516     * kettle of fish best left unwormed.
4517     */                                                                  // c:2517-2529
4518
4519    // c:2532-2541 — no args: list aliases.
4520    // C body:
4521    //   if (!*args) {
4522    //       if (OPT_ISSET(ops,'R')) {
4523    //           zwarnnam(nam, "no module alias to remove");
4524    //           return 1;
4525    //       }
4526    //       scanhashtable(modulestab, 1, MOD_ALIAS, 0,
4527    //                     modulestab->printnode,
4528    //                     OPT_ISSET(ops,'L') ? PRINTMOD_LIST : 0);
4529    //       return 0;
4530    //   }
4531    //
4532    // scanhashtable args: sorted=1, INCLUDE=MOD_ALIAS, EXCLUDE=0.
4533    // Only MOD_ALIAS entries pass, walked in name order. Each entry
4534    // dispatched through printmodulenode (the alias-emit arm).
4535    if args.is_empty() {
4536        if OPT_ISSET(ops, b'R') {
4537            // c:2533
4538            zwarnnam(nam, "no module alias to remove"); // c:2534
4539            return 1; // c:2535
4540        }
4541        let listflags = if OPT_ISSET(ops, b'L') {
4542            PRINTMOD_LIST
4543        } else {
4544            0
4545        };
4546        let mut names: Vec<&String> = table
4547            .modules
4548            .iter()
4549            .filter(|(_, m)| (m.node.flags & MOD_ALIAS) != 0) // c:2537 INCLUDE
4550            .map(|(n, _)| n)
4551            .collect();
4552        names.sort(); // c:2537 sorted=1
4553        for name in names {
4554            let m = &table.modules[name];
4555            let line = printmodulenode(name, m, listflags);
4556            if !line.is_empty() {
4557                println!("{}", line);
4558            }
4559        }
4560        return 0; // c:2540
4561    }
4562
4563    // c:2543 — for each arg, parse name=alias and dispatch.
4564    for arg in args {
4565        // c:2544-2547 — split at '='
4566        let (lhs, aliasname): (&str, Option<&str>) = match arg.find('=') {
4567            Some(eq) => (&arg[..eq], Some(&arg[eq + 1..])),
4568            None => (arg.as_str(), None),
4569        };
4570        // c:2548 — modname_ok check on the LHS
4571        if modname_ok(lhs) == 0 {
4572            // c:2548
4573            zwarnnam(nam, &format!("invalid module name `{}'", lhs)); // c:2549
4574            return 1; // c:2550
4575        }
4576        if OPT_ISSET(ops, b'R') {
4577            // c:2552
4578            // -R: remove alias path.
4579            if aliasname.is_some() {
4580                // c:2553
4581                zwarnnam(
4582                    nam,
4583                    &format!("bad syntax for removing module alias: {}", lhs),
4584                ); // c:2554
4585                return 1; // c:2556
4586            }
4587            // c:2558 — find_module(lhs, 0, NULL)
4588            match table.modules.get(lhs) {
4589                Some(m) => {
4590                    if (m.node.flags & MOD_ALIAS) == 0 {
4591                        // c:2560
4592                        zwarnnam(nam, &format!("module is not an alias: {}", lhs)); // c:2561
4593                        return 1; // c:2562
4594                    }
4595                    table.modules.remove(lhs); // c:2564 delete_module
4596                }
4597                None => {
4598                    zwarnnam(nam, &format!("no such module alias: {}", lhs)); // c:2566
4599                    return 1; // c:2567
4600                }
4601            }
4602        } else {
4603            // No -R: install/replace alias OR list one.
4604            if let Some(target) = aliasname {
4605                // c:2570
4606                if modname_ok(target) == 0 {
4607                    // c:2572
4608                    zwarnnam(nam, &format!("invalid module name `{}'", target)); // c:2573
4609                    return 1; // c:2574
4610                }
4611                // c:2576-2584 — cycle detection: walk alias chain
4612                let mut mname = target;
4613                let mut depth = 0;
4614                loop {
4615                    if depth > 256 {
4616                        break;
4617                    }
4618                    depth += 1;
4619                    if mname == lhs {
4620                        // c:2577
4621                        zwarnnam(nam, &format!("module alias would refer to itself: {}", lhs)); // c:2578
4622                        return 1; // c:2580
4623                    }
4624                    match table.modules.get(mname) {
4625                        Some(m) if (m.node.flags & MOD_ALIAS) != 0 => {
4626                            mname = m.alias.as_deref().unwrap_or("");
4627                        }
4628                        _ => break,
4629                    }
4630                }
4631                // c:2585-2596 — install or replace
4632                if let Some(m) = table.modules.get_mut(lhs) {
4633                    if (m.node.flags & MOD_ALIAS) == 0 {
4634                        // c:2587
4635                        zwarnnam(nam, &format!("module is not an alias: {}", lhs)); // c:2588
4636                        return 1; // c:2589
4637                    }
4638                    m.alias = Some(target.to_string()); // c:2591/2597
4639                } else {
4640                    let mut m = module::new(lhs); // c:2593 zshcalloc
4641                    m.node.flags = MOD_ALIAS; // c:2594
4642                    m.alias = Some(target.to_string()); // c:2597
4643                    table.modules.insert(lhs.to_string(), m); // c:2595
4644                }
4645            } else {
4646                // c:2599-2611 — list one alias.
4647                // C body:
4648                //   if ((m = find_module(*args, 0, NULL))) {
4649                //       if (m->node.flags & MOD_ALIAS)
4650                //           modulestab->printnode(&m->node,
4651                //                                 OPT_ISSET(ops,'L')
4652                //                                     ? PRINTMOD_LIST : 0);
4653                //       else { zwarnnam(...); return 1; }
4654                //   } else { zwarnnam(...); return 1; }
4655                match table.modules.get(lhs) {
4656                    Some(m) if (m.node.flags & MOD_ALIAS) != 0 => {
4657                        // c:2601-2603 — printnode dispatch
4658                        let listflags = if OPT_ISSET(ops, b'L') {
4659                            PRINTMOD_LIST
4660                        } else {
4661                            0
4662                        };
4663                        let line = printmodulenode(lhs, m, listflags);
4664                        if !line.is_empty() {
4665                            println!("{}", line);
4666                        }
4667                    }
4668                    Some(_) => {
4669                        zwarnnam(nam, &format!("module is not an alias: {}", lhs)); // c:2605
4670                        return 1; // c:2606
4671                    }
4672                    None => {
4673                        zwarnnam(nam, &format!("no such module alias: {}", lhs)); // c:2609
4674                        return 1; // c:2610
4675                    }
4676                }
4677            }
4678        }
4679    }
4680    0 // c:2616
4681}
4682
4683/// Port of `bin_zmodload_exist(UNUSED(char *nam), char **args, Options ops)` from `Src/module.c:2623`.
4684///
4685/// C body:
4686/// ```c
4687/// bin_zmodload_exist(UNUSED(char *nam), char **args, Options ops)
4688/// {
4689///     Module m;
4690///     if (!*args) {
4691///         scanhashtable(modulestab, 1, 0, 0, modulestab->printnode,
4692///                       OPT_ISSET(ops,'A') ? PRINTMOD_EXIST|PRINTMOD_ALIAS :
4693///                       PRINTMOD_EXIST);
4694///         return 0;
4695///     } else {
4696///         int ret = 0;
4697///         for (; !ret && *args; args++) {
4698///             if (!(m = find_module(*args, FINDMOD_ALIASP, NULL))
4699///                 || !m->u.handle
4700///                 || (m->node.flags & MOD_UNLOAD))
4701///                 ret = 1;
4702///         }
4703///         return ret;
4704///     }
4705/// }
4706/// ```
4707///
4708/// `zmodload [-A]` lists or tests module presence. Returns 0 if all
4709/// named modules exist (or if no args, after listing); 1 if any
4710/// named module is missing/unloading.
4711/// WARNING: param names don't match C — Rust=(table, _nam, args, _ops) vs C=(nam, args, ops)
4712pub fn bin_zmodload_exist(
4713    table: &mut modulestab,
4714    _nam: &str,
4715    args: &[String],
4716    ops: &options,
4717) -> i32 {
4718    // c:2623
4719    if args.is_empty() {
4720        // c:2627
4721        // c:2628-2630 — scanhashtable(modulestab, 1, 0, 0,
4722        //                              modulestab->printnode,
4723        //                              OPT_ISSET(ops,'A')
4724        //                                  ? PRINTMOD_EXIST|PRINTMOD_ALIAS
4725        //                                  : PRINTMOD_EXIST);
4726        // Sorted walk (the `1` 2nd arg) over the full table (no
4727        // INCLUDE/EXCLUDE filters), dispatching every entry through
4728        // printmodulenode. -A toggles PRINTMOD_ALIAS so alias entries
4729        // emit their alias target alongside the existence line.
4730        let printflags = if OPT_ISSET(ops, b'A') {
4731            PRINTMOD_EXIST | PRINTMOD_ALIAS
4732        } else {
4733            PRINTMOD_EXIST
4734        };
4735        let mut names: Vec<&String> = table.modules.keys().collect();
4736        names.sort(); // c:2628 sorted=1
4737        for name in names {
4738            let m = &table.modules[name];
4739            let line = printmodulenode(name, m, printflags);
4740            if !line.is_empty() {
4741                println!("{}", line);
4742            }
4743        }
4744        return 0; // c:2631
4745    }
4746    // c:2633-2640 — for each arg, test existence.
4747    // C:
4748    //   for (; !ret && *args; args++) {
4749    //       if (!(m = find_module(*args, FINDMOD_ALIASP, NULL))
4750    //           || !m->u.handle
4751    //           || (m->node.flags & MOD_UNLOAD))
4752    //           ret = 1;
4753    //   }
4754    // The `!m->u.handle` clause is union-typed in C: for static-link
4755    // modules it reads `m->u.linked` (non-NULL when linked, NULL when
4756    // pre-registered but not yet bound); for dynamic modules it's the
4757    // dlopen handle. Translate to: handle.is_none() && linked.is_none()
4758    // — both representations of "no live binding."
4759    let mut ret: i32 = 0;
4760    for arg in args {
4761        // c:2635
4762        if ret != 0 {
4763            break;
4764        }
4765        let canon = match find_module(table, arg, FINDMOD_ALIASP) {
4766            // c:2636
4767            Some(n) => n,
4768            None => {
4769                ret = 1; // c:2639
4770                continue;
4771            }
4772        };
4773        let live = match table.modules.get(&canon) {
4774            Some(m) => {
4775                // c:2637 — `!m->u.handle` (union-semantics).
4776                let bound = m.handle.is_some() || m.linked.is_some();
4777                // c:2638 — `(m->node.flags & MOD_UNLOAD)`.
4778                let unloading = (m.node.flags & MOD_UNLOAD) != 0;
4779                bound && !unloading
4780            }
4781            None => false,
4782        };
4783        if !live {
4784            ret = 1; // c:2639
4785        }
4786    }
4787    ret // c:2641
4788}
4789
4790/// Port of `bin_zmodload_dep(UNUSED(char *nam), char **args, Options ops)` from `Src/module.c:2649`.
4791///
4792/// `zmodload -d [-u] [target [dep ...]]`. Three modes:
4793/// - `-u target` removes all deps from target; `-u target d1 d2` removes
4794///   only those.
4795/// - no args lists all dependencies.
4796/// - `target dep1 ...` adds each dep to target's dependency list.
4797/// WARNING: param names don't match C — Rust=(table, _nam, args, ops) vs C=(nam, args, ops)
4798pub fn bin_zmodload_dep(table: &mut modulestab, _nam: &str, args: &[String], ops: &options) -> i32 {
4799    // c:2649
4800    if OPT_ISSET(ops, b'u') {
4801        // c:2652
4802        // c:2654 — `const char *tnam = *args++;`
4803        if args.is_empty() {
4804            return 0;
4805        }
4806        let tnam = &args[0];
4807        let rest = &args[1..];
4808        // c:2655 — `find_module(tnam, FINDMOD_ALIASP, &tnam)`
4809        let canon = match find_module(table, tnam, FINDMOD_ALIASP) {
4810            Some(n) => n,
4811            None => return 0, // c:2656-2657
4812        };
4813        if let Some(m) = table.modules.get_mut(&canon) {
4814            if !rest.is_empty() && m.deps.is_some() {
4815                // c:2658-2671 — remove specific deps.
4816                // C body c:2659-2667 walks args, finds each in deps,
4817                // removes the matched node; the inner break ends the
4818                // current arg's search but the outer while continues
4819                // to the next arg. Mirrors with a for-loop.
4820                let deps = m.deps.as_mut().unwrap();
4821                for to_remove in rest {
4822                    // c:2659 do { ... } while(*++args)
4823                    if let Some(pos) = deps.iter().position(|d| d == to_remove) {
4824                        deps.delete_node(pos); // c:2664 remnode
4825                    }
4826                }
4827                // c:2668-2671 — if deps now empty, free the list.
4828                if deps.is_empty() {
4829                    m.deps = None;
4830                }
4831            } else if m.deps.is_some() {
4832                // c:2672-2676 — no specific deps given: clear all.
4833                m.deps = None;
4834            }
4835            // c:2678-2679 — `if (!m->deps && !m->u.handle) delete_module(m);`
4836            // Static-link analog of `!u.handle` is `!MOD_INIT_B` (boot
4837            // hasn't run = no loaded handle). Without the MOD_INIT_B
4838            // gate, the prior port deleted any module whose deps got
4839            // cleared, dropping the modulestab entries for already-
4840            // loaded modules (zsh/main, zsh/watch, etc.) the moment
4841            // their dep list went empty.
4842            let no_deps = m.deps.is_none();
4843            let no_handle = (m.node.flags & MOD_INIT_B) == 0;
4844            if no_deps && no_handle {
4845                table.modules.remove(&canon); // c:2679 delete_module
4846            }
4847        }
4848        return 0; // c:2680
4849    }
4850    // c:2681 — `else if (!args[0] || !args[1])`: list-mode (one or all).
4851    if args.is_empty() || args.len() == 1 {
4852        // c:2682-2691 — `int depflags = OPT_ISSET(ops,'L')
4853        //                  ? PRINTMOD_DEPS|PRINTMOD_LIST : PRINTMOD_DEPS;`
4854        let depflags = if OPT_ISSET(ops, b'L') {
4855            PRINTMOD_DEPS | PRINTMOD_LIST
4856        } else {
4857            PRINTMOD_DEPS
4858        };
4859        if !args.is_empty() {
4860            // c:2685-2687 — single-name list:
4861            //   if ((m = modulestab->getnode2(modulestab, args[0])))
4862            //       modulestab->printnode(&m->node, depflags);
4863            if let Some(m) = table.modules.get(&args[0]) {
4864                let line = printmodulenode(&args[0], m, depflags);
4865                if !line.is_empty() {
4866                    println!("{}", line);
4867                }
4868            }
4869        } else {
4870            // c:2688-2691 — full sorted scan.
4871            //   scanhashtable(modulestab, 1, 0, 0, printnode, depflags);
4872            let mut names: Vec<&String> = table.modules.keys().collect();
4873            names.sort(); // c:2689 sorted=1
4874            for name in names {
4875                let m = &table.modules[name];
4876                let line = printmodulenode(name, m, depflags);
4877                if !line.is_empty() {
4878                    println!("{}", line);
4879                }
4880            }
4881        }
4882        return 0; // c:2692
4883    }
4884    // c:2693-2701 — add deps: args[0] target, args[1..] deps to add.
4885    let target = &args[0];
4886    for dep in &args[1..] {
4887        add_dep(table, target, dep); // c:2699
4888    }
4889    0 // c:2700 (ret stays 0)
4890}
4891
4892/// Port of `printautoparams(HashNode hn, int lon)` from `Src/module.c:2710`.
4893///
4894/// C body:
4895/// ```c
4896/// printautoparams(HashNode hn, int lon)
4897/// {
4898///     Param pm = (Param) hn;
4899///     if (pm->node.flags & PM_AUTOLOAD) {
4900///         if (lon)
4901///             printf("zmodload -ap %s %s\n", pm->u.str, pm->node.nam);
4902///         else
4903///             printf("%s (%s)\n", pm->node.nam, pm->u.str);
4904///     }
4905/// }
4906/// ```
4907///
4908/// Hash-table scan callback for `zmodload -ap` listing. Rust port
4909/// takes a `(name, module, flags)` triple instead of a HashNode ptr
4910/// since zshrs's autoload-params live in `ModuleTable.autoload_params`.
4911/// WARNING: param names don't match C — Rust=(name, module, flags, lon) vs C=(hn, lon)
4912pub fn printautoparams(name: &str, module: &str, flags: u32, lon: i32) {
4913    // c:2710
4914    if (flags & PM_AUTOLOAD) != 0 {
4915        // c:2710
4916        if lon != 0 {
4917            // c:2715
4918            // c:2716 — printf("zmodload -ap %s %s\n", pm->u.str, pm->node.nam);
4919            println!("zmodload -ap {} {}", module, name);
4920        } else {
4921            // c:2718 — printf("%s (%s)\n", pm->node.nam, pm->u.str);
4922            println!("{} ({})", name, module);
4923        }
4924    }
4925}
4926
4927/// Port of `bin_zmodload_auto(char *nam, char **args, Options ops)` from `Src/module.c:2726`.
4928///
4929/// `zmodload [-c] [-p] [-f] [-a] module name [name ...]` —
4930/// register-autoload of builtins/conditions/params/mathfns. C body
4931/// (80 lines) walks the appropriate dispatch table per opt flag.
4932///
4933/// `-c` lists/registers conditions, `-p` parameters, `-f` math ported,
4934/// default is builtins. `-L` toggles long-form listing.
4935///
4936/// Static-link path: registers via `add_autoaliasbuiltin` /
4937/// `add_autoparam` / `add_automathfunc` already ported. Without a
4938/// module name (just `-a`), runs the listing scan via `autoloadscan`
4939/// or its conddef/param/mathfn equivalents.
4940/// WARNING: param names don't match C — Rust=(table, _nam, args, ops) vs C=(nam, args, ops)
4941pub fn bin_zmodload_auto(
4942    table: &mut modulestab,
4943    _nam: &str,
4944    args: &[String],
4945    ops: &options,
4946) -> i32 {
4947    // c:2726
4948    let fchar: char; // c:2726
4949    let _flags: i32 = if OPT_ISSET(ops, b'i') { FEAT_IGNORE } else { 0 }; // c:2728
4950
4951    // c:2731-2753 — conditions branch (-c).
4952    // C body:
4953    //   if (!*args) {
4954    //       Conddef p;
4955    //       for (p = condtab; p; p = p->next) {
4956    //           if (p->module) {
4957    //               if (OPT_ISSET(ops,'L')) {
4958    //                   fputs("zmodload -ac", stdout);
4959    //                   if (p->flags & CONDF_INFIX) putchar('I');
4960    //                   printf(" %s %s\n", p->module, p->name);
4961    //               } else {
4962    //                   if (p->flags & CONDF_INFIX) fputs("infix ", stdout);
4963    //                   else fputs("post ", stdout);
4964    //                   printf("%s (%s)\n", p->name, p->module);
4965    //               }
4966    //           }
4967    //       }
4968    //       return 0;
4969    //   }
4970    // C walks condtab in registration order (linked-list head→tail).
4971    // The Rust port walks CONDTAB (same shape, Vec instead of list).
4972    if OPT_ISSET(ops, b'c') {
4973        fchar = if OPT_ISSET(ops, b'I') { 'C' } else { 'c' };
4974        let _ = fchar;
4975        if args.is_empty() {
4976            let snap: Vec<(String, i32, String)> = {
4977                let tab = CONDTAB.lock().unwrap();
4978                tab.iter()
4979                    // c:2737 — `if (p->module)` skips entries without
4980                    // an owning module (built-in cond defs use NULL).
4981                    .filter_map(|p| {
4982                        p.module
4983                            .as_ref()
4984                            .map(|m| (p.name.clone(), p.flags, m.clone()))
4985                    })
4986                    .collect()
4987            };
4988            let l_flag = OPT_ISSET(ops, b'L');
4989            for (name, flags, module) in snap {
4990                if l_flag {
4991                    // c:2738-2742 — `zmodload -ac[I] MODULE NAME`.
4992                    if (flags & CONDF_INFIX) != 0 {
4993                        // c:2740
4994                        println!("zmodload -acI {} {}", module, name);
4995                    } else {
4996                        println!("zmodload -ac {} {}", module, name);
4997                    }
4998                } else {
4999                    // c:2743-2748 — `infix NAME (MODULE)` /
5000                    //                `post NAME (MODULE)`.
5001                    let kind = if (flags & CONDF_INFIX) != 0 {
5002                        "infix"
5003                    } else {
5004                        "post"
5005                    };
5006                    println!("{} {} ({})", kind, name, module);
5007                }
5008            }
5009            return 0;
5010        }
5011    } else if OPT_ISSET(ops, b'p') {
5012        // c:2755-2761 — params branch.
5013        // C body:
5014        //   if (!*args) {
5015        //       /* list autoloaded parameters */
5016        //       scanhashtable(paramtab, 1, 0, 0, printautoparams,
5017        //                     OPT_ISSET(ops,'L'));
5018        //       return 0;
5019        //   }
5020        // The `sorted=1` (2nd arg) walks the table in name order.
5021        // `printautoparams` (c:2710) checks `pm->flags & PM_AUTOLOAD`
5022        // and emits either `zmodload -ap MODULE NAME` (under `-L`)
5023        // or `NAME (MODULE)` form. Module name is read from
5024        // `pm->u.str`, which `add_autoparam` (c:1218) sets to the
5025        // module that registered the autoload stub.
5026        if args.is_empty() {
5027            let lon = if OPT_ISSET(ops, b'L') { 1 } else { 0 };
5028            let entries: Vec<(String, u32, String)> = {
5029                let tab = crate::ported::params::paramtab()
5030                    .read()
5031                    .expect("paramtab poisoned");
5032                let mut v: Vec<(String, u32, String)> = tab
5033                    .iter()
5034                    .filter(|(_, p)| (p.node.flags as u32 & crate::ported::zsh_h::PM_AUTOLOAD) != 0)
5035                    .map(|(name, p)| {
5036                        (
5037                            name.clone(),
5038                            p.node.flags as u32,
5039                            p.u_str.clone().unwrap_or_default(),
5040                        )
5041                    })
5042                    .collect();
5043                v.sort_by(|a, b| a.0.cmp(&b.0)); // c:2758 sorted=1
5044                v
5045            };
5046            for (name, flags, module) in entries {
5047                printautoparams(&name, &module, flags, lon); // c:2758
5048            }
5049            return 0;
5050        }
5051    } else if OPT_ISSET(ops, b'f') {
5052        // c:2763-2778 — math-function branch (-f).
5053        // C body:
5054        //   if (!*args) {
5055        //       MathFunc p;
5056        //       for (p = mathfuncs; p; p = p->next) {
5057        //           if (!(p->flags & MFF_USERFUNC) && p->module) {
5058        //               if (OPT_ISSET(ops,'L')) {
5059        //                   fputs("zmodload -af", stdout);
5060        //                   printf(" %s %s\n", p->module, p->name);
5061        //               } else
5062        //                   printf("%s (%s)\n", p->name, p->module);
5063        //           }
5064        //       }
5065        //       return 0;
5066        //   }
5067        // C walks `mathfuncs` (file-static linked list). Rust port
5068        // walks MATHFUNCS (same shape, Vec). The MFF_USERFUNC filter
5069        // excludes `functions -M` user math from the autoload list.
5070        if args.is_empty() {
5071            let snap: Vec<(String, String)> = {
5072                let tab = MATHFUNCS.lock().unwrap();
5073                tab.iter()
5074                    // c:2769 — `!(p->flags & MFF_USERFUNC) && p->module`.
5075                    .filter_map(|p| {
5076                        if (p.flags & MFF_USERFUNC) != 0 {
5077                            return None;
5078                        }
5079                        p.module.as_ref().map(|m| (p.name.clone(), m.clone()))
5080                    })
5081                    .collect()
5082            };
5083            let l_flag = OPT_ISSET(ops, b'L');
5084            for (name, module) in snap {
5085                if l_flag {
5086                    // c:2770-2772 — `zmodload -af MODULE NAME`.
5087                    println!("zmodload -af {} {}", module, name);
5088                } else {
5089                    // c:2774 — `NAME (MODULE)`.
5090                    println!("{} ({})", name, module);
5091                }
5092            }
5093            return 0;
5094        }
5095    } else {
5096        // Default: builtins branch
5097        if args.is_empty() {
5098            // c:Src/module.c:2756 — `scanhashtable(autoloadtab, 1, 0,
5099            //   0, ...)`. The sorted=1 arg sorts entries by name before
5100            // dispatch. zshrs's HashMap iteration is unordered, so the
5101            // 27 auto-loaded entries came out in random order. Bug
5102            // #222 in docs/BUGS.md. Sort by name to match zsh.
5103            let mut entries: Vec<(&String, &String)> = table.autoload_builtins.iter().collect();
5104            entries.sort_by(|a, b| a.0.cmp(b.0));
5105            for (name, module) in entries {
5106                autoloadscan(
5107                    name,
5108                    module,
5109                    0,
5110                    if OPT_ISSET(ops, b'L') { PRINT_LIST } else { 0 },
5111                );
5112            }
5113            return 0;
5114        }
5115    }
5116
5117    // c:2791-2805 — register/unregister via autofeatures with
5118    // FEAT_AUTOALL. (Earlier zshrs revision inserted into the
5119    // autoload_* maps directly, bypassing autofeatures — so
5120    // `m->autoloads` bookkeeping and the add_autobin/add_autoparam/…
5121    // canonical-table dispatch never ran.)
5122    let fchar: u8 = if OPT_ISSET(ops, b'c') {
5123        if OPT_ISSET(ops, b'I') {
5124            b'C' // c:2754 fchar = OPT_ISSET(ops,'I') ? 'C' : 'c'
5125        } else {
5126            b'c'
5127        }
5128    } else if OPT_ISSET(ops, b'p') {
5129        b'p' // c:2762
5130    } else if OPT_ISSET(ops, b'f') {
5131        b'f' // c:2779
5132    } else {
5133        b'b' // c:2789
5134    };
5135    let mut flags = FEAT_AUTOALL; // c:2791
5136    if OPT_ISSET(ops, b'i') {
5137        flags |= FEAT_IGNORE; // c:2792-2793
5138    }
5139    if OPT_ISSET(ops, b'u') {
5140        /* remove autoload */
5141        // c:2795
5142        flags |= FEAT_REMOVE; // c:2796
5143                              // c:2797 — `modnam = NULL;` — every arg is a feature name.
5144        return autofeatures(table, _nam, None, args, fchar, flags); // c:2805
5145    }
5146    /* add autoload */
5147    // c:2799
5148    let modnam = &args[0]; // c:2800
5149                           // c:2802-2803 — `if (args[1]) args++;` — with a single arg the
5150                           // module name doubles as the feature arg (C quirk; autofeatures
5151                           // then rejects the `/` in it).
5152    let feat_args: &[String] = if args.len() > 1 { &args[1..] } else { args };
5153    autofeatures(table, _nam, Some(modnam), feat_args, fchar, flags) // c:2805
5154}
5155
5156/// Port of `unload_named_module(char *modname, char *nam, int silent)`
5157/// from `Src/module.c:2923-2965`.
5158///
5159/// C body:
5160/// ```c
5161/// int
5162/// unload_named_module(char *modname, char *nam, int silent)
5163/// {
5164///     const char *mname;
5165///     Module m;
5166///     int ret = 0;
5167///     m = find_module(modname, FINDMOD_ALIASP, &mname);
5168///     if (m) {
5169///         int i, del = 0;
5170///         Module dm;
5171///         for (i = 0; i < modulestab->hsize; i++) {
5172///             for (dm = (Module)modulestab->nodes[i]; dm;
5173///                  dm = (Module)dm->node.next) {
5174///                 LinkNode dn;
5175///                 if (!dm->deps || !dm->u.handle)
5176///                     continue;
5177///                 for (dn = firstnode(dm->deps); dn; incnode(dn)) {
5178///                     if (!strcmp((char *) getdata(dn), mname)) {
5179///                         if (dm->node.flags & MOD_UNLOAD)
5180///                             del = 1;
5181///                         else {
5182///                             zwarnnam(nam, "module %s is in use ...");
5183///                             return 1;
5184///                         }
5185///                     }
5186///                 }
5187///             }
5188///         }
5189///         if (del) m->wrapper++;
5190///         if (unload_module(m)) ret = 1;
5191///         if (del) m->wrapper--;
5192///     } else if (!silent) {
5193///         zwarnnam(nam, "no such module %s", modname);
5194///         ret = 1;
5195///     }
5196///     return ret;
5197/// }
5198/// ```
5199///
5200/// Now that unload_module is faithfully ported (2b986c9e8b), this
5201/// wraps it with the C dep-walk that gates the unload on whether
5202/// any other loaded module depends on the target. If a dependent
5203/// is already tagged MOD_UNLOAD, set del=1 to bracket the unload
5204/// with `m->wrapper++` / `m->wrapper--` so unload_module's wrapper
5205/// branch (c:2839-2842) defers correctly.
5206/// WARNING: param names don't match C — Rust=(table, name, nam, silent) vs C=(modname, nam, silent)
5207pub fn unload_named_module(table: &mut modulestab, name: &str, nam: &str, silent: i32) -> i32 {
5208    // c:2924
5209    // c:2930 — `m = find_module(modname, FINDMOD_ALIASP, &mname);`
5210    // Returns the canonical (alias-resolved) name; we drive
5211    // unload_module against that, matching C's `m` pointer.
5212    let mname = match find_module(table, name, FINDMOD_ALIASP) {
5213        Some(n) => n,
5214        None => {
5215            // c:2959-2961 — !m branch: silent gate.
5216            if silent == 0 {
5217                crate::ported::utils::zwarnnam(nam, &format!("no such module {}", name));
5218                return 1;
5219            }
5220            return 0;
5221        }
5222    };
5223
5224    // c:2932 — `int del = 0;`
5225    let mut del = 0;
5226
5227    // c:2935-2952 — scan every module's deps for `mname`.
5228    // Snapshot first so we don't double-borrow the modules HashMap
5229    // when we mutate via unload_module below.
5230    let candidates: Vec<(String, i32, bool, Vec<String>)> = table
5231        .modules
5232        .iter()
5233        .filter_map(|(other_name, other)| {
5234            // c:2939 — `if (!dm->deps || !dm->u.handle) continue;`
5235            // Static-link analog of `u.handle`: MOD_LINKED && !MOD_UNLOAD.
5236            // Actually C's gate is `u.handle` (loaded) — the MOD_UNLOAD
5237            // check happens INSIDE the inner loop. So here we only skip
5238            // modules with no deps or no handle (= not loaded).
5239            let deps = other.deps.as_ref()?;
5240            if (other.node.flags & MOD_LINKED) == 0 {
5241                return None;
5242            }
5243            // Note: C checks `u.handle` (loaded handle) not
5244            // `!MOD_UNLOAD` here — MOD_UNLOAD-flagged modules still
5245            // get scanned because the inner branch needs to see them
5246            // to set `del = 1`.
5247            Some((
5248                other_name.clone(),
5249                other.node.flags,
5250                (other.node.flags & MOD_UNLOAD) != 0,
5251                deps.iter().cloned().collect(),
5252            ))
5253        })
5254        .collect();
5255
5256    for (_other_name, _other_flags, other_unloading, other_deps) in candidates {
5257        for dep in &other_deps {
5258            if dep != &mname {
5259                continue; // c:2942
5260            }
5261            if other_unloading {
5262                // c:2943-2944 — dependent already marked MOD_UNLOAD →
5263                // we'll be cascade-unloading it after the target.
5264                del = 1;
5265            } else {
5266                // c:2945-2948 — live dependent: refuse the unload.
5267                crate::ported::utils::zwarnnam(
5268                    nam,
5269                    &format!(
5270                        "module {} is in use by another module and cannot be unloaded",
5271                        mname
5272                    ),
5273                );
5274                return 1;
5275            }
5276        }
5277    }
5278
5279    // c:2953-2954 — `if (del) m->wrapper++;`. The wrapper++/--
5280    // bracket gates unload_module's wrapper branch (c:2839-2842):
5281    // with wrapper > 0, unload_module sets MOD_UNLOAD and returns
5282    // rather than running finish. The actual cascade fires via the
5283    // recursive deferred-dep walk inside unload_module.
5284    if del != 0 {
5285        if let Some(m) = table.modules.get_mut(&mname) {
5286            m.wrapper += 1;
5287        }
5288    }
5289
5290    // c:2955-2956 — `if (unload_module(m)) ret = 1;`. Rust's
5291    // unload_module returns bool (true=success). Map to C ret:
5292    // false → 1, true → 0.
5293    let mut ret = if !table.unload_module(&mname) { 1 } else { 0 };
5294
5295    // c:2957-2958 — `if (del) m->wrapper--;`
5296    if del != 0 {
5297        if let Some(m) = table.modules.get_mut(&mname) {
5298            m.wrapper -= 1;
5299        }
5300    }
5301    let _ = silent;
5302    let _ = &mut ret;
5303    ret // c:2964
5304}
5305
5306/// Port of `bin_zmodload_load(char *nam, char **args, Options ops)` from `Src/module.c:2971`.
5307///
5308/// C body:
5309/// ```c
5310/// bin_zmodload_load(char *nam, char **args, Options ops)
5311/// {
5312///     int ret = 0;
5313///     if(OPT_ISSET(ops,'u')) {
5314///         for(; *args; args++) {
5315///             if (unload_named_module(*args, nam, OPT_ISSET(ops,'i')))
5316///                 ret = 1;
5317///         }
5318///         return ret;
5319///     } else if(!*args) {
5320///         scanhashtable(modulestab, ..., PRINTMOD_LIST);
5321///         return 0;
5322///     } else {
5323///         for (; *args; args++) {
5324///             int tmpret = require_module(*args, NULL, OPT_ISSET(ops,'s'));
5325///             if (tmpret && ret != 1) ret = tmpret;
5326///         }
5327///         return ret;
5328///     }
5329/// }
5330/// ```
5331///
5332/// `zmodload [-u] [args]`: load, unload, or list modules.
5333/// WARNING: param names don't match C — Rust=(table, nam, args, ops) vs C=(nam, args, ops)
5334pub fn bin_zmodload_load(table: &mut modulestab, nam: &str, args: &[String], ops: &options) -> i32 {
5335    // c:2971
5336    let mut ret: i32 = 0;
5337    if OPT_ISSET(ops, b'u') {
5338        // c:2974
5339        // c:2976-2979 — unload loop
5340        for arg in args {
5341            if unload_named_module(table, arg, nam, OPT_ISSET(ops, b'i') as i32) != 0 {
5342                ret = 1;
5343            }
5344        }
5345        return ret; // c:2980
5346    } else if args.is_empty() {
5347        // c:2981
5348        // c:2983-2985 — list modules:
5349        //   `scanhashtable(modulestab, 1, 0, MOD_UNLOAD|MOD_ALIAS,
5350        //                  modulestab->printnode,
5351        //                  OPT_ISSET(ops,'L') ? PRINTMOD_LIST : 0);`
5352        // The 4th arg to scanhashtable is the EXCLUDE mask — entries
5353        // with `MOD_UNLOAD` or `MOD_ALIAS` set are skipped. The
5354        // surviving names are routed through `printmodulenode`,
5355        // which itself gates the visible-line emission on
5356        // `m->u.handle` (=> `MOD_INIT_B` in zshrs) so registered-
5357        // but-unloaded modules drop out. Plain `zmodload` (no `-L`)
5358        // passes `flags=0`; `zmodload -L` passes `PRINTMOD_LIST`.
5359        // Previous Rust impl printed every key in `table.modules`
5360        // unconditionally, which leaked the 32 statically-registered
5361        // builtin entries (#76 in docs/BUGS.md).
5362        let listflags = if OPT_ISSET(ops, b'L') {
5363            PRINTMOD_LIST
5364        } else {
5365            0
5366        };
5367        let mut names: Vec<&String> = table.modules.keys().collect();
5368        names.sort(); // c:scanhashtable sorted=1 arg
5369        for name in names {
5370            let m = &table.modules[name]; // c:154 printnode call
5371            if (m.node.flags & (MOD_UNLOAD | MOD_ALIAS)) != 0 {
5372                continue; // c:2983 EXCLUDE mask
5373            }
5374            let line = printmodulenode(name, m, listflags);
5375            if !line.is_empty() {
5376                println!("{}", line);
5377            }
5378        }
5379        return 0; // c:2986
5380    } else {
5381        // c:2989-2992 — load loop
5382        for arg in args {
5383            let tmpret = require_module(table, arg, None, OPT_ISSET(ops, b's') as i32); // c:2990
5384            if tmpret != 0 && ret != 1 {
5385                // c:2991
5386                ret = tmpret;
5387            }
5388            // PFA-SMR: one event per `zmodload MODULE` load form
5389            // (only the bare-load path; listing/-u/-d/-A handled
5390            // upstream and never reaches here). Pass empty flags
5391            // since the per-feature -F path lives in
5392            // bin_zmodload_features. Record even on failure so
5393            // replay sees the user's intent.
5394            #[cfg(feature = "recorder")]
5395            if crate::recorder::is_enabled() {
5396                let ctx = crate::recorder::recorder_ctx_global();
5397                crate::recorder::emit_zmodload(arg, "", ctx);
5398            }
5399        }
5400        ret
5401    }
5402}
5403
5404/// Port of `bin_zmodload_features(const char *nam, char **args, Options ops)` from `Src/module.c:3003`.
5405///
5406/// `zmodload -F [-L|-l|-P|-a|-m|-i] module [+/-feature ...]` —
5407/// per-feature enable/disable for an already-loaded module.
5408///
5409/// C body (~135 lines) handles:
5410/// - no module: list all modules with their features (`-L` long form,
5411///   `-l` show all enables, `-a` show autoloads).
5412/// - `-P` requires a module name; lists patterns.
5413/// - `-m` interprets each feature as a glob pattern.
5414/// - default: `+feature` enables, `-feature` disables, then calls
5415///   `do_module_features` to apply.
5416/// WARNING: param names don't match C — Rust=(table, nam, args, ops) vs C=(nam, args, ops)
5417pub fn bin_zmodload_features(
5418    table: &mut modulestab,
5419    nam: &str,
5420    args: &[String],
5421    ops: &options,
5422) -> i32 {
5423    // c:3003
5424    let modname = args.first(); // c:3003
5425    let rest_args = if args.is_empty() {
5426        &args[..]
5427    } else {
5428        &args[1..]
5429    };
5430
5431    // c:3010-3030 — no-module-name listing branch.
5432    // C body:
5433    //   if (modname)
5434    //       args++;
5435    //   else if (OPT_ISSET(ops,'L')) {
5436    //       int printflags = PRINTMOD_LIST|PRINTMOD_FEATURES;
5437    //       if (OPT_ISSET(ops,'P')) {
5438    //           zwarnnam(nam, "-P is only allowed with a module name");
5439    //           return 1;
5440    //       }
5441    //       if (OPT_ISSET(ops,'l')) printflags |= PRINTMOD_LISTALL;
5442    //       if (OPT_ISSET(ops,'a')) printflags |= PRINTMOD_AUTO;
5443    //       scanhashtable(modulestab, 1, 0, MOD_ALIAS,
5444    //                     modulestab->printnode, printflags);
5445    //       return 0;
5446    //   }
5447    //   if (!modname) {
5448    //       zwarnnam(nam, "-F requires a module name");
5449    //       return 1;
5450    //   }
5451    if modname.is_none() {
5452        if OPT_ISSET(ops, b'L') {
5453            // c:3012
5454            // c:3014-3016 — `-P` check must fire BEFORE the listing
5455            // dispatch. Without modname, -P is illegal.
5456            if OPT_ISSET(ops, b'P') {
5457                zwarnnam(nam, "-P is only allowed with a module name"); // c:3015
5458                return 1; // c:3016
5459            }
5460            // c:3013 / c:3018-3021 — assemble PRINTMOD_LIST|PRINTMOD_FEATURES
5461            //                       [|PRINTMOD_LISTALL][|PRINTMOD_AUTO]
5462            let mut printflags = PRINTMOD_LIST | PRINTMOD_FEATURES;
5463            if OPT_ISSET(ops, b'l') {
5464                printflags |= PRINTMOD_LISTALL; // c:3019
5465            }
5466            if OPT_ISSET(ops, b'a') {
5467                printflags |= PRINTMOD_AUTO; // c:3021
5468            }
5469            // c:3022-3023 — `scanhashtable(modulestab, 1, 0, MOD_ALIAS,
5470            //                              printnode, printflags);`
5471            // sorted=1, INCLUDE=0 (all), EXCLUDE=MOD_ALIAS (skip aliases).
5472            let mut names: Vec<String> = table
5473                .modules
5474                .iter()
5475                .filter(|(_, m)| (m.node.flags & MOD_ALIAS) == 0) // c:3022 EXCLUDE
5476                .map(|(n, _)| n.clone())
5477                .collect();
5478            names.sort(); // c:3022 sorted=1
5479            for name in &names {
5480                if printflags & PRINTMOD_AUTO != 0 {
5481                    // c:229-231 / c:238-251 — autoload form; printmodulenode
5482                    // covers this branch fully.
5483                    let m = &table.modules[name];
5484                    let line = printmodulenode(name, m, printflags);
5485                    if !line.is_empty() {
5486                        println!("{}", line);
5487                    }
5488                    continue;
5489                }
5490                // c:218 / c:232-235 — loaded-module + features gate:
5491                // `if (features_module(m, &features) ||
5492                //      enables_module(m, &enables) || !*features) return;`
5493                // printmodulenode has no &table handle, so the FEATURES
5494                // dispatch happens here (see its c:252-263 comment).
5495                let loaded = {
5496                    let m = &table.modules[name];
5497                    (m.node.flags & MOD_INIT_B) != 0 && (m.node.flags & MOD_UNLOAD) == 0
5498                };
5499                if !loaded {
5500                    continue;
5501                }
5502                let mut features: Vec<String> = Vec::new();
5503                if features_module(table, name, &mut features) != 0 || features.is_empty() {
5504                    continue; // c:233-235
5505                }
5506                let mut enables_opt: Option<Vec<i32>> = None;
5507                if enables_module(table, name, &mut enables_opt) != 0 {
5508                    continue; // c:233-235
5509                }
5510                let enables = enables_opt.unwrap_or_else(|| vec![0; features.len()]);
5511                // c:237-245 — `printf("zmodload "); fputs("-F ", stdout);`
5512                let mut line = String::from("zmodload -F ");
5513                if name.starts_with('-') {
5514                    line.push_str("-- "); // c:244-245
5515                }
5516                line.push_str(&crate::ported::utils::quotedzputs(name)); // c:246
5517                                                                         // c:252-262 — per-feature tail: LISTALL emits ` +f`/` -f`,
5518                                                                         // plain -L skips disabled and emits ` f`.
5519                for (f, on) in features.iter().zip(enables.iter()) {
5520                    if printflags & PRINTMOD_LISTALL != 0 {
5521                        line.push_str(if *on != 0 { " +" } else { " -" }); // c:256
5522                    } else if *on == 0 {
5523                        continue; // c:258
5524                    } else {
5525                        line.push(' '); // c:260
5526                    }
5527                    line.push_str(&crate::ported::utils::quotedzputs(f)); // c:261
5528                }
5529                println!("{}", line);
5530            }
5531            return 0; // c:3024
5532        }
5533        zwarnnam(nam, "-F requires a module name"); // c:3028
5534        return 1; // c:3029
5535    }
5536
5537    let modname = modname.unwrap();
5538
5539    // c:3032-3047 — `-m` glob-pattern branch.
5540    // C body:
5541    //   if (OPT_ISSET(ops,'m')) {
5542    //       patprogs = zhalloc(arrlen(args)*sizeof(Patprog));
5543    //       for (argp = args; *argp; argp++, patprogp++) {
5544    //           if (*arg == '+' || *arg == '-') arg++;
5545    //           tokenize(arg);
5546    //           *patprogp = patcompile(arg, 0, 0);
5547    //       }
5548    //   } else patprogs = NULL;
5549    // Static-link path: pattern compilation deferred. The -m flag is
5550    // observed at the -a / require_module dispatch below, but the
5551    // patprogs array stays NULL — patcompile callers (autofeatures,
5552    // do_module_features) fall back to exact-name matching.
5553
5554    // c:3049-3226 — `-l/-L/-e` arm: list features one per line with
5555    // +/- (-l), as a `zmodload -F` statement (-L), or test existence
5556    // (-e). `-m` patprogs stay deferred (exact-name matching), same
5557    // as the c:3032-3047 note above.
5558    if OPT_ISSET(ops, b'l') || OPT_ISSET(ops, b'L') || OPT_ISSET(ops, b'e') {
5559        let param: Option<String> = OPT_ARG_SAFE(ops, b'P').map(|s| s.to_string()); // c:3060
5560                                                                                    // c:3062 — `m = find_module(modname, FINDMOD_ALIASP, NULL);`
5561        let resolved = find_module(table, modname, FINDMOD_ALIASP);
5562
5563        // c:3063-3107 — `-a` sub-arm: autoload listing/testing.
5564        if OPT_ISSET(ops, b'a') {
5565            // c:3067-3068 — `if (!m || !m->autoloads) return 1;`
5566            let autoloads: Vec<String> = match resolved
5567                .as_ref()
5568                .and_then(|r| table.modules.get(r))
5569                .and_then(|m| m.autoloads.as_ref())
5570            {
5571                Some(al) => al.iter().cloned().collect(),
5572                None => return 1,
5573            };
5574            if OPT_ISSET(ops, b'e') {
5575                // c:3070-3085 — each arg must (mis)match per its +/- sense.
5576                for fstr in rest_args {
5577                    let (sense, name) = match fstr.strip_prefix('+') {
5578                        Some(rest) => (true, rest), // c:3074
5579                        None => match fstr.strip_prefix('-') {
5580                            Some(rest) => (false, rest), // c:3076-3078
5581                            None => (true, fstr.as_str()),
5582                        },
5583                    };
5584                    // c:3080-3082 — `(linknodebystring(...) != NULL) != sense`
5585                    if autoloads.iter().any(|a| a == name) != sense {
5586                        return 1; // c:3082
5587                    }
5588                }
5589                return 0; // c:3084
5590            }
5591            if let Some(p) = param {
5592                // c:3086-3088 / c:3098-3100 — collect into the array,
5593                // then `setaparam(param, arrset)`.
5594                // c:3103-3106
5595                if crate::ported::params::setaparam(&p, autoloads).is_none() {
5596                    return 1; // c:3105
5597                }
5598                return 0; // c:3106
5599            }
5600            if OPT_ISSET(ops, b'L') {
5601                // c:3089-3091 — `printf("zmodload -aF %s%c", ...)`
5602                let rname = resolved.as_deref().unwrap_or(modname);
5603                print!(
5604                    "zmodload -aF {}{}",
5605                    crate::ported::utils::quotedzputs(rname),
5606                    if autoloads.is_empty() { '\n' } else { ' ' }
5607                );
5608                // c:3092-3098 — space-separated, final '\n'.
5609                for (i, al) in autoloads.iter().enumerate() {
5610                    print!("{}{}", al, if i + 1 < autoloads.len() { ' ' } else { '\n' });
5611                }
5612            } else {
5613                // c:3093-3097 — one per line.
5614                for al in &autoloads {
5615                    println!("{}", al);
5616                }
5617            }
5618            return 0; // c:3107
5619        }
5620
5621        // c:3108-3112 — `if (!m || !m->u.handle || (m->node.flags &
5622        // MOD_UNLOAD))`. zshrs maps "u.handle installed" to MOD_INIT_B
5623        // (see the printmodulenode comment at c:218-241 above).
5624        let loaded = resolved
5625            .as_ref()
5626            .and_then(|r| table.modules.get(r))
5627            .map(|m| (m.node.flags & MOD_INIT_B) != 0 && (m.node.flags & MOD_UNLOAD) == 0)
5628            .unwrap_or(false);
5629        if !loaded {
5630            if !OPT_ISSET(ops, b'e') {
5631                zwarnnam(nam, &format!("module `{}' is not yet loaded", modname));
5632                // c:3110
5633            }
5634            return 1; // c:3111
5635        }
5636        let rname = resolved.unwrap();
5637
5638        // c:3113-3118 — `features_module(m, &features)`.
5639        let mut features: Vec<String> = Vec::new();
5640        if features_module(table, &rname, &mut features) != 0 {
5641            if !OPT_ISSET(ops, b'e') {
5642                zwarnnam(
5643                    nam,
5644                    &format!("module `{}' does not support features", rname), // c:3115
5645                );
5646            }
5647            return 1; // c:3117
5648        }
5649        // c:3119-3124 — `enables_module(m, &enables)`.
5650        let mut enables_opt: Option<Vec<i32>> = None;
5651        if enables_module(table, &rname, &mut enables_opt) != 0 {
5652            /* this shouldn't ever happen, so don't silence this error */
5653            // c:3120
5654            zwarnnam(
5655                nam,
5656                &format!("error getting enabled features for module `{}'", rname), // c:3121
5657            );
5658            return 1; // c:3123
5659        }
5660        let enables: Vec<i32> = enables_opt.unwrap_or_else(|| vec![0; features.len()]);
5661
5662        // c:3125-3155 — validate every feature argument.
5663        for raw in rest_args {
5664            // c:3127-3135 — strip +/- into `on`.
5665            let (on, arg): (i32, &str) = match raw.strip_prefix('-') {
5666                Some(rest) => (0, rest),
5667                None => match raw.strip_prefix('+') {
5668                    Some(rest) => (1, rest),
5669                    None => (-1, raw.as_str()),
5670                },
5671            };
5672            let mut found = 0;
5673            for (fp, ep) in features.iter().zip(enables.iter()) {
5674                // c:3137-3138 — patprogs deferred: exact `strcmp`.
5675                if arg == fp {
5676                    // c:3140-3142 — for -e, check given state, if any.
5677                    if OPT_ISSET(ops, b'e') && on != -1 && on != (ep & 1) {
5678                        return 1; // c:3142
5679                    }
5680                    found += 1;
5681                    break; // c:3144-3145
5682                }
5683            }
5684            if found == 0 {
5685                // c:3148-3154
5686                if !OPT_ISSET(ops, b'e') {
5687                    zwarnnam(
5688                        nam,
5689                        &format!("module `{}' has no such feature: `{}'", modname, raw),
5690                    );
5691                }
5692                return 1; // c:3153
5693            }
5694        }
5695        if OPT_ISSET(ops, b'e') {
5696            /* yep, everything we want exists */
5697            // c:3156
5698            return 0; // c:3157
5699        }
5700
5701        let opt_big_l = OPT_ISSET(ops, b'L');
5702        let opt_small_l = OPT_ISSET(ops, b'l');
5703        // c:3186-3194 / c:3164-3172 — arg filter helpers. The C print
5704        // loop compares the UNSTRIPPED arg (`!strcmp(*fp, *argp)`,
5705        // c:3193) while the param-count loop compares stripped
5706        // (`!strcmp(*fp, arg)`, c:3170). Ported as written.
5707        let matches_stripped = |f: &str| -> bool {
5708            rest_args.is_empty()
5709                || rest_args.iter().any(|raw| {
5710                    let arg = raw
5711                        .strip_prefix('+')
5712                        .or_else(|| raw.strip_prefix('-'))
5713                        .unwrap_or(raw);
5714                    f == arg
5715                })
5716        };
5717        let matches_unstripped =
5718            |f: &str| -> bool { rest_args.is_empty() || rest_args.iter().any(|raw| f == raw) };
5719
5720        let mut arrset: Option<Vec<String>> = None;
5721        if param.is_some() {
5722            // c:3158-3183 — size pass folded away (Vec grows); keep the
5723            // same membership filter.
5724            arrset = Some(Vec::new());
5725        } else if opt_big_l {
5726            // c:3184-3185 — `printf("zmodload -F %s ", m->node.nam);`
5727            print!("zmodload -F {} ", crate::ported::utils::quotedzputs(&rname));
5728        }
5729        // c:3186-3219 — main feature emit loop.
5730        for (i, (f, ep)) in features.iter().zip(enables.iter()).enumerate() {
5731            if param.is_some() {
5732                if !matches_stripped(f) {
5733                    continue; // c:3170-3173 stripped compare
5734                }
5735            } else if !matches_unstripped(f) {
5736                continue; // c:3193-3196 unstripped compare
5737            }
5738            let onoff: &str = if opt_big_l && !opt_small_l {
5739                // c:3198-3200
5740                if *ep == 0 {
5741                    continue; // c:3199
5742                }
5743                ""
5744            } else if *ep != 0 {
5745                "+" // c:3203
5746            } else {
5747                "-" // c:3205
5748            };
5749            if let Some(ref mut arr) = arrset {
5750                arr.push(format!("{}{}", onoff, f)); // c:3208 bicat
5751            } else {
5752                // c:3210-3216 — term ' ' while a next feature EXISTS in
5753                // the full array (fp[1]), even if it won't be printed.
5754                let term = if opt_big_l && i + 1 < features.len() {
5755                    ' '
5756                } else {
5757                    '\n'
5758                };
5759                print!("{}{}{}", onoff, crate::ported::utils::quotedzputs(f), term);
5760            }
5761        }
5762        if let (Some(p), Some(arr)) = (param, arrset) {
5763            // c:3220-3224 — `setaparam(param, arrset)`.
5764            if crate::ported::params::setaparam(&p, arr).is_none() {
5765                return 1; // c:3223
5766            }
5767        }
5768        return 0; // c:3225
5769    }
5770
5771    // c:3227-3229 — `-P` is illegal without -l/-L/-e.
5772    if OPT_ISSET(ops, b'P')
5773        && !(OPT_ISSET(ops, b'l') || OPT_ISSET(ops, b'L') || OPT_ISSET(ops, b'e'))
5774    {
5775        zwarnnam(nam, "-P can only be used with -l or -L"); // c:3228
5776        return 1; // c:3229
5777    }
5778
5779    // c:3230-3247 — `-a` arm: route through autofeatures with
5780    // FEAT_IGNORE (the autoload-feature registration path).
5781    if OPT_ISSET(ops, b'a') {
5782        // c:3231-3234 — `-m` incompatible with `-a`.
5783        if OPT_ISSET(ops, b'm') {
5784            zwarnnam(nam, "-m cannot be used with -a"); // c:3232
5785            return 1; // c:3233
5786        }
5787        // c:3246 — `return autofeatures(nam, modname, args, 0, FEAT_IGNORE);`
5788        // FEAT_IGNORE is hard-coded here because marking-for-autoload
5789        // is separate from enable/disable (per the c:3236-3244 comment).
5790        return autofeatures(table, nam, Some(modname), rest_args, 0, FEAT_IGNORE);
5791    }
5792
5793    // c:3249-3260 — default arm: build Feature_enables array from
5794    // `+name`/`-name` args, then `require_module(modname, features,
5795    // OPT_ISSET(ops,'s'))`.
5796    //
5797    // C builds a fep[] array with str + (optional) patprog pairs.
5798    // The Rust port flattens to a `Vec<String>` since patprogs are
5799    // deferred; require_module accepts Option<&[String]>.
5800    let feats: Vec<String> = rest_args.to_vec();
5801    let features_arg = if feats.is_empty() {
5802        None
5803    } else {
5804        Some(feats.as_slice())
5805    };
5806    require_module(table, modname, features_arg, OPT_ISSET(ops, b's') as i32) // c:3260
5807}
5808
5809/// Port of `ensurefeature(const char *modname, const char *prefix, const char *feature)` from `Src/module.c:3415`.
5810///
5811/// C body:
5812/// ```c
5813/// ensurefeature(const char *modname, const char *prefix, const char *feature)
5814/// {
5815///     char *f;
5816///     struct feature_enables features[2];
5817///     if (!feature)
5818///         return require_module(modname, NULL, 0);
5819///     f = dyncat(prefix, feature);
5820///     features[0].str = f;
5821///     features[0].pat = NULL;
5822///     features[1].str = NULL;
5823///     features[1].pat = NULL;
5824///     return require_module(modname, features, 0);
5825/// }
5826/// ```
5827/// WARNING: param names don't match C — Rust=(table, modname, prefix, feature) vs C=(modname, prefix, feature)
5828pub fn ensurefeature(
5829    table: &mut modulestab,
5830    modname: &str,
5831    prefix: &str,
5832    feature: Option<&str>,
5833) -> i32 {
5834    // c:3415
5835    match feature {
5836        // c:3420-3421 — `if (!feature) return require_module(modname, NULL, 0);`
5837        None => require_module(table, modname, None, 0),
5838        Some(f) => {
5839            // c:3422-3428 — build single-element features[2] array.
5840            let combined = crate::ported::string::dyncat(prefix, f); // c:3422
5841            let arr = vec![combined];
5842            require_module(table, modname, Some(&arr), 0) // c:3428
5843        }
5844    }
5845}
5846
5847/// Port of `static HashNode resolvebuiltin(const char *cmdarg, HashNode hn)`
5848/// from `Src/exec.c:2700-2724` — the autoloaded-builtin stub firing.
5849///
5850/// C body:
5851/// ```c
5852/// if (!((Builtin) hn)->handlerfunc) {
5853///     char *modname = dupstring(((Builtin) hn)->optstr);
5854///     (void)ensurefeature(modname, "b:",
5855///                         (hn->flags & BINF_AUTOALL) ? NULL : hn->nam);
5856///     hn = builtintab->getnode(builtintab, cmdarg);
5857///     if (!hn) {
5858///         lastval = 1;
5859///         zerr("autoloading module %s failed to define builtin: %s",
5860///              modname, cmdarg);
5861///         return NULL;
5862///     }
5863/// }
5864/// return hn;
5865/// ```
5866///
5867/// zshrs split: the C autoload stub (builtintab node with NULL
5868/// handlerfunc, installed by `add_autobin` c:426) lives in the
5869/// `autoload_builtins` ledger (name → module). This fn is the
5870/// dispatch-time consult:
5871///   - `None` — name has no autoload stub; caller continues its
5872///     normal lookup chain.
5873///   - `Some(0)` — module loaded; caller re-dispatches the (now
5874///     registered) builtin.
5875///   - `Some(1)` — load failed or the loaded module didn't define
5876///     the feature; diagnostics already printed (load_module's
5877///     `failed to load module` zwarn, or the c:2718 zerr here).
5878///     Caller returns status 1.
5879///
5880/// Ledger upkeep: the entry is removed in every fired path —
5881///   - success: C's addbuiltin (module.c:411-415) replaces the stub
5882///     with the real node, so the stub is gone;
5883///   - load failure: C's execbuiltin head (Src/builtin.c:264-267)
5884///     hits the still-NULL handlerfunc and `deletebuiltin`s the
5885///     stub — a second call reports `command not found` / 127
5886///     (probed: zsh 5.9 `zmodload -ab zsh/bogus mybltn; mybltn;
5887///     mybltn` → rc1=1, rc2=127).
5888///
5889/// AUTOALL note: the ledger doesn't carry BINF_AUTOALL, so the
5890/// ensurefeature arg is always `Some(name)` (the non-AUTOALL form).
5891/// The AUTOALL path is unreachable for builtins via `zmodload -a MOD`
5892/// (both shells error "`/' is illegal in a builtin"); revisit if the
5893/// ledger grows flags.
5894pub fn resolvebuiltin(name: &str) -> Option<i32> {
5895    // c:2700
5896    let mut tab = MODULESTAB.lock().ok()?;
5897    // c:2705 — `if (!((Builtin) hn)->handlerfunc)`: ledger hit IS the
5898    // "no handlerfunc" stub in zshrs.
5899    let module = tab.autoload_builtins.get(name)?.clone();
5900    // c:2706 — `modname = dupstring(hn->optstr)` (done: `module`).
5901    // Stub fires exactly once per registration (see ledger upkeep
5902    // in the doc above).
5903    tab.autoload_builtins.remove(name);
5904    // c:2711-2713 — ensurefeature(modname, "b:", hn->nam).
5905    let _ = ensurefeature(&mut tab, &module, "b:", Some(name));
5906    // c:2714 — `hn = builtintab->getnode(builtintab, cmdarg);`
5907    // zshrs analog: the module booted (is_loaded) AND the name is in
5908    // the static builtintab (createbuiltintable pre-registers every
5909    // module bintab entry).
5910    let defined =
5911        tab.is_loaded(&module) && crate::ported::builtin::createbuiltintable().contains_key(name);
5912    if defined {
5913        return Some(0); // c:2723 `return hn;`
5914    }
5915    if tab.is_loaded(&module) {
5916        // c:2716-2720 — module loaded but feature missing.
5917        crate::ported::builtin::LASTVAL.store(1, std::sync::atomic::Ordering::Relaxed); // c:2717 lastval = 1
5918        crate::ported::utils::zerr(&format!(
5919            "autoloading module {} failed to define builtin: {}",
5920            module, name
5921        )); // c:2718
5922    }
5923    // Load failure: load_module already printed `failed to load
5924    // module \`...'`; C's execbuiltin head returns 1 silently
5925    // (Src/builtin.c:264-267).
5926    Some(1)
5927}
5928
5929/// Port of `addmathfunc(MathFunc f)` from `Src/module.c:1313`.
5930///
5931/// C body: walks the global `mathfuncs` linked list, refuses to
5932/// re-register MFF_ADDED entries, replaces autoloadable shims, then
5933/// links into head. Rust port operates on `autoload_mathfuncs` map
5934/// since zshrs's static-link path doesn't have per-entry MFF flags.
5935
5936/// Port of `autofeatures(const char *cmdnam, const char *module, char **features, int prefchar, int defflags)` from `Src/module.c:3437`.
5937///
5938/// C body is ~140 lines. Top-level structure:
5939/// ```c
5940/// autofeatures(const char *cmdnam, const char *module, char **features,
5941///              int prefchar, int defflags)
5942/// {
5943///     // Resolve module, fetch its features+enables tables.
5944///     // For each feature in `features`:
5945///     //   parse `+`/`-` prefix → add/remove
5946///     //   parse type prefix (b/c/C/p/f) → fchar
5947///     //   dispatch to add_aliasbuiltin / add_autocondition /
5948///     //     add_autoparam / add_automathfunc / del_* matching
5949/// }
5950/// ```
5951///
5952/// Static-link path: registers each `module:feature` pair into the
5953/// matching `table.autoload_*` map. Honors `+`/`-` prefix for
5954/// add/remove, and the type prefix or `prefchar` arg for routing.
5955/// WARNING: param names don't match C — Rust=(table, _cmdnam, module, features, prefchar, defflags) vs C=(cmdnam, module, features, prefchar, defflags)
5956pub fn autofeatures(
5957    table: &mut modulestab,
5958    cmdnam: &str,
5959    module: Option<&str>,
5960    features: &[String],
5961    prefchar: u8,
5962    defflags: i32,
5963) -> i32 {
5964    // c:3437
5965    let mut ret: i32 = 0;
5966
5967    // c:3445-3453 — resolve `defm` up front (FINDMOD_ALIASP|
5968    // FINDMOD_CREATE); if its union slot is populated (loaded —
5969    // MOD_INIT_B in zshrs, see the printmodulenode c:218-241 note)
5970    // fetch the feature + enable tables for the c:3558-3577 checks.
5971    let mut modfeatures: Option<Vec<String>> = None;
5972    let mut modenables: Vec<i32> = Vec::new();
5973    let defm_name: Option<String> = match module {
5974        Some(modn) => {
5975            let resolved = find_module(table, modn, FINDMOD_ALIASP | FINDMOD_CREATE);
5976            if let Some(ref r) = resolved {
5977                let booted = table
5978                    .modules
5979                    .get(r)
5980                    .map(|m| (m.node.flags & MOD_INIT_B) != 0)
5981                    .unwrap_or(false);
5982                if booted {
5983                    // c:3449-3451
5984                    let mut f: Vec<String> = Vec::new();
5985                    if features_module(table, r, &mut f) == 0 {
5986                        let mut e: Option<Vec<i32>> = None;
5987                        let _ = enables_module(table, r, &mut e);
5988                        modenables = e.unwrap_or_else(|| vec![0; f.len()]);
5989                        modfeatures = Some(f);
5990                    }
5991                }
5992            }
5993            resolved
5994        }
5995        None => None, // c:3454-3455 `defm = NULL`
5996    };
5997
5998    for feature in features {
5999        let s = feature.as_str();
6000        let mut add: bool = true; // c:3466 / c:3477 default `add = 1`
6001        let mut flags = defflags; // c:3458 `flags = defflags`
6002
6003        // `feature_full` is the string C keeps in `m->autoloads`
6004        // (c:3584/3597 `ztrdup(feature)`): the arg after the +/-
6005        // strip, type prefix included — prefchar mode synthesizes it
6006        // via `sprintf(feature, "%c:%s", fchar, fnam)` (c:3468-3469).
6007        let prefixed: String;
6008        let (fchar, fnam, feature_full): (u8, &str, &str) = if prefchar != 0 {
6009            // c:3461-3470 — `prefchar` mode: feature is bare name with
6010            // no `+`/`-` / `b:` prefix; fchar comes from the arg.
6011            prefixed = format!("{}:{}", prefchar as char, s); // c:3468-3469
6012            (prefchar, s, prefixed.as_str()) // c:3467-3468
6013        } else {
6014            // c:3471-3490 — parse `+`/`-` then the `b:`/`c:`/`C:`/`p:`/`f:`
6015            // type prefix.
6016            let mut t = s;
6017            if let Some(rest) = t.strip_prefix('-') {
6018                // c:3473
6019                add = false;
6020                t = rest;
6021            } else if let Some(rest) = t.strip_prefix('+') {
6022                // c:3478
6023                t = rest;
6024            }
6025            // c:3482-3487 — bad format check: `!*feature || feature[1] != ':'`
6026            let bytes = t.as_bytes();
6027            if bytes.is_empty() || bytes.len() < 2 || bytes[1] != b':' {
6028                // c:3483-3486 — zwarnnam + ret=1 + continue.
6029                crate::ported::utils::zwarnnam(
6030                    cmdnam,
6031                    &format!("bad format for autoloadable feature: `{}'", t),
6032                );
6033                ret = 1; // c:3485
6034                continue; // c:3486
6035            }
6036            // c:3488-3489 — `fnam = feature + 2; fchar = feature[0];`
6037            (bytes[0], &t[2..], t)
6038        };
6039
6040        // c:3491-3492 — `if (flags & FEAT_REMOVE) add = 0;`
6041        if (flags & FEAT_REMOVE) != 0 {
6042            add = false;
6043        }
6044
6045        let typnam: &str; // c:3457
6046        let _ = typnam; // referenced below for fnam validation
6047        let typnam = match fchar {
6048            // c:3494-3522 — switch (fchar): map each type-letter to
6049            // the typnam used in the diagnostic + add/del fn dispatch.
6050            b'b' => "builtin", // c:3495-3498
6051            b'c' | b'C' => {
6052                if fchar == b'C' {
6053                    flags |= FEAT_INFIX; // c:3501
6054                }
6055                "condition" // c:3505
6056            }
6057            b'f' => "math function", // c:3508-3511
6058            b'p' => "parameter",     // c:3513-3516
6059            _ => {
6060                // c:3518-3522 — `zwarnnam(cmdnam, "bad autoloadable
6061                // feature type: `%c'", fchar); ret = 1; continue;`
6062                crate::ported::utils::zwarnnam(
6063                    cmdnam,
6064                    &format!("bad autoloadable feature type: `{}'", fchar as char),
6065                );
6066                ret = 1; // c:3521
6067                continue; // c:3522
6068            }
6069        };
6070
6071        // c:3525-3529 — reject `/` in the feature name.
6072        if fnam.contains('/') {
6073            crate::ported::utils::zwarnnam(
6074                cmdnam,
6075                &format!("{}: `/' is illegal in a {}", fnam, typnam),
6076            );
6077            ret = 1;
6078            continue;
6079        }
6080
6081        // c:3531-3553 — resolve module: if `module` arg is None,
6082        // walk every module's `m->autoloads` list looking for the
6083        // feature; if found, that's the owning module. C's
6084        // `m->autoloads` per-module list isn't modelled in the Rust
6085        // port — the autoload_* HashMaps store `feature → module`
6086        // directly, so we can derive the owning module from those.
6087        // When `module` arg IS set, use it (c:3553 `m = defm;`).
6088        let modname_owned: String = match module {
6089            Some(m) => m.to_string(), // c:3553
6090            None => {
6091                // c:3537-3551 — search for the owning module across all
6092                // autoload maps; fall back to error if not found.
6093                let map = match fchar {
6094                    b'b' => &table.autoload_builtins,
6095                    b'c' | b'C' => &table.autoload_conditions,
6096                    b'p' => &table.autoload_params,
6097                    b'f' => &table.autoload_mathfuncs,
6098                    _ => unreachable!(),
6099                };
6100                match map.get(fnam).cloned() {
6101                    Some(m) => m,
6102                    None => {
6103                        if (flags & FEAT_IGNORE) == 0 {
6104                            // c:3546-3549
6105                            ret = 1;
6106                            crate::ported::utils::zwarnnam(
6107                                cmdnam,
6108                                &format!("{}: no such {}", fnam, typnam),
6109                            );
6110                        }
6111                        continue; // c:3550
6112                    }
6113                }
6114            }
6115        };
6116        let modname = modname_owned.as_str();
6117
6118        // c:3554 `subret = 0;` — the m->autoloads maintenance below can
6119        // set it to ±2 on the remove-missing path (c:3614).
6120        let mut autoload_subret: i32 = 0;
6121        // Owning module node: the alias-resolved defm when a module arg
6122        // was given (C `m = defm`, c:3553), else the searched-up name.
6123        let owner: &str = match (module.is_some(), defm_name.as_deref()) {
6124            (true, Some(r)) => r,
6125            _ => modname,
6126        };
6127        if add {
6128            // c:3558-3577 — if the module is already loaded, the feature
6129            // must exist in its table; if it's already enabled there is
6130            // nothing to mark.
6131            if module.is_some() {
6132                if let Some(ref mf) = modfeatures {
6133                    match mf.iter().position(|f| f == feature_full) {
6134                        None => {
6135                            // c:3566-3570
6136                            crate::ported::utils::zwarnnam(
6137                                cmdnam,
6138                                &format!(
6139                                    "module `{}' has no such feature: `{}'",
6140                                    owner, feature_full
6141                                ),
6142                            );
6143                            ret = 1;
6144                            continue;
6145                        }
6146                        Some(idx) => {
6147                            if modenables.get(idx).copied().unwrap_or(0) != 0 {
6148                                continue; // c:3572-3577 already provided
6149                            }
6150                        }
6151                    }
6152                }
6153            }
6154            // c:3583-3603 — insert into m->autoloads in lexical order
6155            // (dup is "never an error", c:3590-3593).
6156            if let Some(m) = table.modules.get_mut(owner) {
6157                let list = m
6158                    .autoloads
6159                    .get_or_insert_with(crate::ported::linklist::znewlinklist);
6160                let mut insert_at: Option<usize> = Some(list.len()); // c:3602 append default
6161                for (i, existing) in list.iter().enumerate() {
6162                    match feature_full.cmp(existing.as_str()) {
6163                        std::cmp::Ordering::Equal => {
6164                            insert_at = None; // c:3591-3593 already there
6165                            break;
6166                        }
6167                        std::cmp::Ordering::Less => {
6168                            insert_at = Some(i); // c:3595-3598
6169                            break;
6170                        }
6171                        std::cmp::Ordering::Greater => {}
6172                    }
6173                }
6174                if let Some(i) = insert_at {
6175                    list.insert_at(i, feature_full.to_string());
6176                }
6177            }
6178        } else {
6179            // c:3605-3615 — `else if (m->autoloads) { remnode or
6180            // subret = FEAT_IGNORE ? -2 : 2; }`
6181            let removed = table
6182                .modules
6183                .get_mut(owner)
6184                .and_then(|m| m.autoloads.as_mut())
6185                .map(|list| {
6186                    match list
6187                        .iter()
6188                        .position(|existing| existing.as_str() == feature_full)
6189                    {
6190                        Some(i) => {
6191                            list.delete_node(i);
6192                            true
6193                        }
6194                        None => false,
6195                    }
6196                })
6197                .unwrap_or(false);
6198            if !removed {
6199                autoload_subret = if (flags & FEAT_IGNORE) != 0 { -2 } else { 2 };
6200                // c:3614
6201            }
6202        }
6203
6204        // c:3556-3616 — m->autoloads insert/remove in lexical order;
6205        // the autoload_* maps mirror the list for the feature→module
6206        // reverse lookups the static-link dispatch needs.
6207        if add {
6208            match fchar {
6209                b'b' => {
6210                    table
6211                        .autoload_builtins
6212                        .insert(fnam.to_string(), modname.to_string());
6213                }
6214                b'c' | b'C' => {
6215                    table
6216                        .autoload_conditions
6217                        .insert(fnam.to_string(), modname.to_string());
6218                }
6219                b'p' => {
6220                    table
6221                        .autoload_params
6222                        .insert(fnam.to_string(), modname.to_string());
6223                }
6224                b'f' => {
6225                    table
6226                        .autoload_mathfuncs
6227                        .insert(fnam.to_string(), modname.to_string());
6228                }
6229                _ => unreachable!(),
6230            }
6231        } else {
6232            // c:3605-3615 — handled above by the m->autoloads remove
6233            // (autoload_subret carries the c:3614 ±2). Keep the
6234            // feature→module reverse maps in sync silently — the
6235            // not-present diagnostic flows through subret below,
6236            // exactly like C's c:3631 arm.
6237            match fchar {
6238                b'b' => {
6239                    table.autoload_builtins.remove(fnam);
6240                }
6241                b'c' | b'C' => {
6242                    table.autoload_conditions.remove(fnam);
6243                }
6244                b'p' => {
6245                    table.autoload_params.remove(fnam);
6246                }
6247                b'f' => {
6248                    table.autoload_mathfuncs.remove(fnam);
6249                }
6250                _ => unreachable!(),
6251            }
6252        }
6253
6254        // c:3618-3619 — `if (subret == 0) subret = fn(module, fnam, flags);`
6255        // The fn does NOT run when the m->autoloads remove already
6256        // produced ±2. Dispatch through the per-type add/del fn so the
6257        // canonical tables (paramtab for `p:`, condtab for `c:`, etc.)
6258        // carry the PM_AUTOLOAD / CONDF flag bits expected by
6259        // downstream code (e.g. paramtypestr's `undefined NAME`).
6260        let subret = if autoload_subret != 0 {
6261            autoload_subret // c:3618 subret already set
6262        } else if add {
6263            match fchar {
6264                b'p' => add_autoparam(modname, fnam, flags),
6265                b'f' => add_automathfunc(table, modname, fnam, flags),
6266                b'b' => table.add_autobin(fnam, modname, flags),
6267                b'c' | b'C' => table.add_autocond(fnam, modname, flags),
6268                _ => unreachable!(),
6269            }
6270        } else {
6271            match fchar {
6272                b'p' => del_autoparam(modname, fnam, flags),
6273                b'f' => del_automathfunc(table, modname, fnam, flags),
6274                b'b' => table.del_autobin(fnam, flags),
6275                b'c' | b'C' => table.del_autocond(fnam, flags),
6276                _ => unreachable!(),
6277            }
6278        };
6279
6280        // c:3621-3642 — per-error-code diagnostic.
6281        if subret != 0 && subret != -2 {
6282            ret = 1; // c:3624
6283            match subret {
6284                1 => {
6285                    // c:3627
6286                    crate::ported::utils::zwarnnam(
6287                        cmdnam,
6288                        &format!("failed to add {} `{}'", typnam, fnam),
6289                    );
6290                }
6291                2 => {
6292                    // c:3631
6293                    crate::ported::utils::zwarnnam(
6294                        cmdnam,
6295                        &format!("{}: no such {}", fnam, typnam),
6296                    );
6297                }
6298                3 => {
6299                    // c:3635
6300                    crate::ported::utils::zwarnnam(
6301                        cmdnam,
6302                        &format!("{}: {} is already defined", fnam, typnam),
6303                    );
6304                }
6305                _ => { /* c:3638 no further message */ }
6306            }
6307        }
6308    }
6309    ret
6310}
6311
6312/// Port of `MathFunc mathfuncs;` from `Src/module.c:1258` — the
6313/// global head of the linked list of math functions. Both
6314/// autoloadable math ported (added by modules) and user math ported
6315/// (added by `functions -M`) live here.
6316///
6317/// C is a singly linked list with `mathfunc.next` chaining. The
6318/// Rust port stores entries in a `Vec` — the call sites only ever
6319/// walk linearly and erase by name, so the linked-list shape buys
6320/// nothing in safe Rust.
6321pub static MATHFUNCS: Lazy<Mutex<Vec<mathfunc>>> = // c:1258
6322    Lazy::new(|| Mutex::new(Vec::new()));
6323
6324/// Port of `int setconddefs(char const *nam, Conddef c, int size, int *e)`
6325/// from `Src/module.c:754`. Bulk add/delete of condition definitions:
6326/// the parallel `e[]` array selects per-entry add (`e[i] != 0`) vs delete
6327/// (`e[i] == 0`). Returns 1 if any individual op clashed, 0 if all clean.
6328pub fn setconddefs(
6329    nam: &str, // c:754
6330    c: &mut [conddef],
6331    e: Option<&[i32]>,
6332) -> i32 {
6333    let mut ret = 0; // c:758
6334    for (i, entry) in c.iter_mut().enumerate() {
6335        // c:760 while (size--)
6336        let want_add = e.map(|es| es[i] != 0).unwrap_or(true); // c:761 if (e && *e++)
6337        if want_add {
6338            if (entry.flags & CONDF_ADDED) != 0 {
6339                continue;
6340            } // c:763 already added
6341            let dup = conddef {
6342                next: None,
6343                name: entry.name.clone(),
6344                flags: entry.flags,
6345                handler: entry.handler,
6346                min: entry.min,
6347                max: entry.max,
6348                condid: entry.condid,
6349                module: entry.module.clone(),
6350            };
6351            if addconddef(dup) != 0 {
6352                // c:768 addconddef
6353                zwarnnam(
6354                    nam, // c:769 zwarnnam
6355                    &format!("name clash when adding condition `{}'", entry.name),
6356                );
6357                ret = 1;
6358            } else {
6359                entry.flags |= CONDF_ADDED; // c:773
6360            }
6361        } else {
6362            if (entry.flags & CONDF_ADDED) == 0 {
6363                continue;
6364            } // c:776
6365            if deleteconddef(entry) != 0 {
6366                // c:780 deleteconddef
6367                zwarnnam(
6368                    nam, // c:781
6369                    &format!("condition `{}' already deleted", entry.name),
6370                );
6371                ret = 1;
6372            } else {
6373                entry.flags &= !CONDF_ADDED; // c:785
6374            }
6375        }
6376    }
6377    ret // c:790
6378}
6379
6380/// Port of `int setmathfuncs(char const *nam, MathFunc f, int size, int *e)`
6381/// from `Src/module.c:1374`. Bulk add/delete of math-function definitions
6382/// via the parallel `e[]` selector array (same shape as setconddefs).
6383pub fn setmathfuncs(
6384    nam: &str, // c:1374
6385    f: &mut [mathfunc],
6386    e: Option<&[i32]>,
6387) -> i32 {
6388    let mut ret = 0; // c:1378
6389    for (i, entry) in f.iter_mut().enumerate() {
6390        // c:1380 while (size--)
6391        // c:1381 — `if (e && *e++)`: e == NULL means REMOVE all
6392        // (same contract as setbuiltins, c:497-503 doc: "e is either
6393        // NULL, in which case all builtins in the table are
6394        // removed"). The previous `.unwrap_or(true)` inverted the
6395        // None case into add-all.
6396        let want_add = e.map(|es| es[i] != 0).unwrap_or(false); // c:1381
6397        if want_add {
6398            if (entry.flags & MFF_ADDED) != 0 {
6399                continue;
6400            } // c:1383
6401            let dup = mathfunc {
6402                next: None,
6403                name: entry.name.clone(),
6404                flags: entry.flags,
6405                nfunc: entry.nfunc,
6406                sfunc: entry.sfunc,
6407                module: entry.module.clone(),
6408                minargs: entry.minargs,
6409                maxargs: entry.maxargs,
6410                funcid: entry.funcid,
6411            };
6412            if addmathfunc(dup) != 0 {
6413                // c:1388 addmathfunc
6414                zwarnnam(
6415                    nam, // c:1389
6416                    &format!("name clash when adding math function `{}'", entry.name),
6417                );
6418                ret = 1;
6419            } else {
6420                entry.flags |= MFF_ADDED; // c:1393
6421                                          // c:1388+1393 — C links `f` ITSELF into mathfuncs and
6422                                          // the flag-set above mutates that same (aliased) node.
6423                                          // The Rust insert is a by-value dup, so mirror the flag
6424                                          // onto the global-list entry; otherwise
6425                                          // del_automathfunc / getfeatureenables reading
6426                                          // MATHFUNCS never observe MFF_ADDED.
6427                if let Ok(mut gtab) = MATHFUNCS.lock() {
6428                    if let Some(p) = gtab.iter_mut().find(|p| p.name == entry.name) {
6429                        p.flags |= MFF_ADDED;
6430                    }
6431                }
6432            }
6433        } else {
6434            if (entry.flags & MFF_ADDED) == 0 {
6435                continue;
6436            } // c:1396
6437            if deletemathfunc(entry) != 0 {
6438                // c:1400 deletemathfunc
6439                zwarnnam(
6440                    nam, // c:1401
6441                    &format!("math function `{}' already deleted", entry.name),
6442                );
6443                ret = 1;
6444            } else {
6445                // c:1356-1357 — C's deletemathfunc clears MFF_ADDED on
6446                // the (aliased) static mftab struct; Rust's global-list
6447                // entry is a dup, so clear it here on the caller's
6448                // record. Without this, a disable→re-enable cycle hits
6449                // the c:1383 `continue` and never re-registers.
6450                entry.flags &= !MFF_ADDED;
6451            }
6452        }
6453    }
6454    ret // c:1407
6455}
6456
6457/// Port of file-static `Conddef condtab;` from `Src/cond.c:21` — the
6458/// global condition-definition linked-list head consulted by `[[ ... ]]`
6459/// dispatch. Modules register custom conditions via `addconddef`; the
6460/// runtime walks `condtab` looking for the matching name+infix flag at
6461/// each `[[` evaluation. Rust port stores entries in a `Vec` (linear
6462/// add/remove + walk; same observable behaviour as C linked list).
6463pub static CONDTAB: Lazy<Mutex<Vec<conddef>>> = // c:cond.c:21
6464    Lazy::new(|| Mutex::new(Vec::new()));
6465
6466/// Port of `Conddef getconddef(int inf, const char *name, int autol)`
6467/// from `Src/module.c:648`.
6468///
6469/// C body c:648-689:
6470/// ```c
6471/// Conddef
6472/// getconddef(int inf, const char *name, int autol)
6473/// {
6474///     Conddef p;
6475///     int f = 1;
6476///     char *lookup, *s;
6477///     lookup = dupstring(name);
6478///     if (!lookup) return NULL;
6479///     for (s = lookup; *s != '\0'; s++) {
6480///         if (*s == Dash) *s = '-';
6481///     }
6482///     do {
6483///         for (p = condtab; p; p = p->next) {
6484///             if ((!!inf == !!(p->flags & CONDF_INFIX)) &&
6485///                 !strcmp(lookup, p->name))
6486///                 break;
6487///         }
6488///         if (autol && p && p->module) {
6489///             if (f) {
6490///                 (void)ensurefeature(p->module,
6491///                                     (p->flags & CONDF_INFIX) ? "C:" : "c:",
6492///                                     (p->flags & CONDF_AUTOALL) ? NULL : lookup);
6493///                 f = 0;
6494///                 p = NULL;
6495///             } else {
6496///                 deleteconddef(p);
6497///                 return NULL;
6498///             }
6499///         } else
6500///             break;
6501///     } while (!p);
6502///     return p;
6503/// }
6504/// ```
6505///
6506/// Returns a clone of the matched `conddef` (or `None` if absent).
6507/// `inf` selects between infix-style (`[[ A op B ]]`) and prefix-style
6508/// (`[[ -X arg ]]`) conditions — `CONDF_INFIX` on the entry must match.
6509/// `autol` triggers `ensurefeature` on autoload-stubs; the autoload
6510/// loop runs at most once (gated by `f`) — if the second iteration
6511/// still finds the entry, the stub is treated as a failed load and
6512/// removed via `deleteconddef`.
6513pub fn getconddef(inf: i32, name: &str, autol: i32, table: &mut modulestab) -> Option<conddef> {
6514    // c:648
6515    // c:655 — `lookup = dupstring(name)` then Dash → '-' substitution.
6516    let lookup: String = crate::ported::string::dupstring(name)
6517        .chars()
6518        .map(|c| {
6519            if c == crate::ported::zsh_h::Dash {
6520                '-'
6521            } else {
6522                c
6523            }
6524        })
6525        .collect();
6526    let mut f = 1; // c:651 `int f = 1;`
6527    loop {
6528        // c:663 do { ... } while (!p);
6529        // c:664-668 — walk condtab matching (!!inf == !!CONDF_INFIX) && name.
6530        let want_infix = inf != 0;
6531        let hit: Option<conddef> = {
6532            let tab = CONDTAB.lock().unwrap();
6533            tab.iter().find_map(|p| {
6534                let p_infix = (p.flags & CONDF_INFIX) != 0;
6535                if p_infix == want_infix && p.name == lookup {
6536                    // Manual field-by-field clone: conddef doesn't
6537                    // derive Clone (function-pointer + Option<Conddef>
6538                    // mix with no PartialEq).
6539                    Some(conddef {
6540                        next: None,
6541                        name: p.name.clone(),
6542                        flags: p.flags,
6543                        handler: p.handler,
6544                        min: p.min,
6545                        max: p.max,
6546                        condid: p.condid,
6547                        module: p.module.clone(),
6548                    })
6549                } else {
6550                    None
6551                }
6552            })
6553        };
6554        // c:669-685 — autoload trigger + failure-retry loop.
6555        let has_autoload_module = hit.as_ref().map(|p| p.module.is_some()).unwrap_or(false);
6556        if autol != 0 && hit.is_some() && has_autoload_module {
6557            let p = hit.as_ref().unwrap();
6558            if f != 0 {
6559                // c:674-678 — first miss: load the module + retry.
6560                let module = p.module.as_ref().unwrap().clone();
6561                let prefix = if (p.flags & CONDF_INFIX) != 0 {
6562                    "C:"
6563                } else {
6564                    "c:"
6565                };
6566                let feature_arg = if (p.flags & CONDF_AUTOALL) != 0 {
6567                    // c:677 — NULL → autoload-all branch.
6568                    None
6569                } else {
6570                    Some(lookup.as_str())
6571                };
6572                let _ = ensurefeature(table, &module, prefix, feature_arg);
6573                f = 0;
6574                continue; // c:680 `p = NULL;` + outer do-while re-tries.
6575            } else {
6576                // c:681-683 — second pass still hit autoload entry →
6577                // load failed; remove the stub and return None.
6578                let _ = deleteconddef(p);
6579                return None;
6580            }
6581        }
6582        return hit; // c:684-685 `else break;` then return p.
6583    }
6584}
6585
6586/// Port of `int deleteconddef(Conddef c)` from `Src/module.c:724`.
6587/// Removes condition definition `c` from `condtab`. Returns 0 on
6588/// success, -1 on miss. C also frees the autoloaded entry's name +
6589/// module; Rust drop subsumes that.
6590pub fn deleteconddef(c: &conddef) -> i32 {
6591    // c:724
6592    let mut tab = CONDTAB.lock().unwrap();
6593    // c:728 — `for (p = condtab, q = NULL; p && p != c; ...)`. C uses
6594    // pointer identity; the Rust analog is name+infix-flag equality
6595    // (the natural key — `[[ -z STR ]]` and `STR == VAL` share neither).
6596    let infix = c.flags & CONDF_INFIX;
6597    match tab
6598        .iter()
6599        .position(|p| p.name == c.name && (p.flags & CONDF_INFIX) == infix)
6600    {
6601        Some(i) => {
6602            tab.remove(i);
6603            0
6604        } // c:733-738 unlink + free
6605        None => -1, // c:743 not found
6606    }
6607}
6608
6609/// Port of `int addconddef(Conddef c)` from `Src/module.c:703`. Walks
6610/// CONDTAB for a clash on (name, infix-flag); replaces autoloadable
6611/// entries via deleteconddef; otherwise prepends. Returns 0 on add,
6612/// 1 on clash (existing entry already added).
6613pub fn addconddef(c: conddef) -> i32 {
6614    // c:703
6615    let infix = c.flags & CONDF_INFIX;
6616    let clash_idx = {
6617        let tab = CONDTAB.lock().unwrap();
6618        tab.iter()
6619            .position(|p| p.name == c.name && (p.flags & CONDF_INFIX) == infix) // c:705 getconddef
6620    };
6621    if let Some(i) = clash_idx {
6622        let (autoload, added) = {
6623            let tab = CONDTAB.lock().unwrap();
6624            (tab[i].module.is_some(), (tab[i].flags & CONDF_ADDED) != 0)
6625        };
6626        if !autoload || added {
6627            return 1;
6628        } // c:708 already added
6629        CONDTAB.lock().unwrap().remove(i); // c:711 deleteconddef
6630    }
6631    CONDTAB.lock().unwrap().insert(0, c); // c:713-714 c->next = condtab; condtab = c
6632    0
6633}
6634
6635/// Port of file-static `FuncWrap wrappers;` from `Src/module.c:567`
6636/// — the global wrapper-function linked-list head. Modules register
6637/// wrapper callbacks via `addwrapper(FuncWrap)` and the runtime fires
6638/// them around `runshfunc()`. The Rust port stores entries in a `Vec`
6639/// (linear add/remove + iterate; same observable behaviour).
6640pub static WRAPPERS: Lazy<Mutex<Vec<funcwrap>>> = // c:567
6641    Lazy::new(|| Mutex::new(Vec::new()));
6642
6643/// Port of `addmathfunc(MathFunc f)` from `Src/module.c:1313`.
6644/// Returns 0 on add, 1 on clash (existing entry not autoloadable).
6645/// Replaces autoloadable entries via `removemathfunc`.
6646pub fn addmathfunc(f: mathfunc) -> i32 {
6647    // c:1313
6648    if (f.flags & MFF_ADDED) != 0 {
6649        return 1;
6650    } // c:1318
6651    let mut tab = MATHFUNCS.lock().unwrap();
6652    let mut found_idx: Option<usize> = None;
6653    for (i, p) in tab.iter().enumerate() {
6654        // c:1321
6655        if p.name == f.name {
6656            // c:1322
6657            if p.module.is_some() && (p.flags & MFF_USERFUNC) == 0 {
6658                // c:1323
6659                found_idx = Some(i); // c:1327 removemathfunc + replace
6660                break;
6661            }
6662            return 1; // c:1330
6663        }
6664    }
6665    if let Some(i) = found_idx {
6666        tab.remove(i);
6667    } // c:1327
6668    tab.insert(0, f); // c:1334-1335 f->next = mathfuncs; mathfuncs = f
6669    0
6670}
6671
6672/// Port of `removemathfunc(MathFunc previous, MathFunc current)` from
6673/// `Src/module.c:1267`. Removes the named entry from MATHFUNCS and
6674/// drops it (Rust drop subsumes C's zsfree/zfree ladder).
6675/// WARNING: param names don't match C — Rust=(name) vs C=(previous, current)
6676pub fn removemathfunc(name: &str) {
6677    // c:1267
6678    let mut tab = MATHFUNCS.lock().unwrap();
6679    if let Some(i) = tab.iter().position(|m| m.name == name) {
6680        // c:1270 walk
6681        tab.remove(i); // c:1273-1274 unlink + zfree
6682    }
6683}
6684
6685/// Port of `deletemathfunc(MathFunc f)` from `Src/module.c:1342`.
6686/// Removes f from MATHFUNCS; for unloaded/user-defined entries clears
6687/// the MFF_ADDED flag instead of dropping the node (C: `f->flags &=
6688/// ~MFF_ADDED` when f->module is null).
6689pub fn deletemathfunc(f: &mathfunc) -> i32 {
6690    // c:1342
6691    // c:1348-1352 — the node is unlinked from the global list in BOTH
6692    // arms (`q->next = f->next` / `mathfuncs = f->next`); f->module
6693    // only decides free-vs-keep of the struct itself. The struct for
6694    // module=NULL entries lives in the module's static mftab (the C
6695    // list links the static structs by pointer), so "keep" means the
6696    // mftab record survives with MFF_ADDED cleared — in Rust the
6697    // global-list entry is a by-value dup, so the unlink is a plain
6698    // remove either way and the MFF_ADDED clear on the static record
6699    // happens at the setmathfuncs caller (which holds `&mut` to the
6700    // mftab entry). The previous port left module-less entries IN the
6701    // global list, so a feature-disable never actually deregistered.
6702    let mut tab = MATHFUNCS.lock().unwrap();
6703    match tab.iter().position(|m| m.name == f.name) {
6704        // c:1346
6705        Some(i) => {
6706            tab.remove(i); // c:1349-1352 unlink (+ Rust Drop ≙ c:1355-1357 free)
6707            0 // c:1361
6708        }
6709        None => -1, // c:1363
6710    }
6711}
6712
6713/// Port of `addwrapper(Module m, FuncWrap w)` from `Src/module.c:577`.
6714/// Returns 0 on add, 1 on clash. Walks WRAPPERS for an existing entry
6715/// with the same handler; appends if absent and sets WRAPF_ADDED on
6716/// the input record.
6717pub fn addwrapper(table: &modulestab, modname: &str, w: funcwrap) -> i32 {
6718    // c:577
6719    // c:587-588 — `if (m->node.flags & MOD_ALIAS) return 1;`
6720    // Wrappers can't bind to an alias entry because they're supposed
6721    // to behave identically to the resolved module; the alias would
6722    // double-dispatch.
6723    if let Some(m) = table.modules.get(modname) {
6724        if (m.node.flags & MOD_ALIAS) != 0 {
6725            return 1;
6726        }
6727    } else {
6728        // C asserts a real module here; absent in modulestab → fail.
6729        return 1;
6730    }
6731    // c:590-591 — `if (w->flags & WRAPF_ADDED) return 1;` — refuse to
6732    // double-add the same wrapper record.
6733    if (w.flags & crate::ported::zsh_h::WRAPF_ADDED) != 0 {
6734        return 1;
6735    }
6736    // c:592 — `for (p = wrappers, q = NULL; p; q = p, p = p->next);`
6737    // Walks to the tail just to append. The Rust port keeps the
6738    // additional "no-duplicate handler" gate the prior commit added
6739    // — C doesn't have that gate (it appends unconditionally once
6740    // WRAPF_ADDED is clear), but reaching the tail-walk with the
6741    // same handler twice would indicate caller misuse, so the gate
6742    // is defensive without changing observable behaviour for valid
6743    // callers.
6744    let mut tab = WRAPPERS.lock().unwrap();
6745    if tab.iter().any(|x| match (x.handler, w.handler) {
6746        (Some(a), Some(b)) => std::ptr::fn_addr_eq(a, b),
6747        (None, None) => true,
6748        _ => false,
6749    }) {
6750        return 1;
6751    }
6752    let mut entry = w;
6753    entry.flags |= crate::ported::zsh_h::WRAPF_ADDED; // c:598 w->flags |= WRAPF_ADDED
6754                                                      // c:599 — `w->module = m;`. Module pointer not modelled in
6755                                                      // the Rust mirror; the name lookup on the next deletewrapper
6756                                                      // call uses the parameter, so we don't need to back-link.
6757    tab.push(entry); // c:593-597 append at tail
6758    0 // c:601
6759}
6760
6761/// Port of `deletewrapper(Module m, FuncWrap w)` from `Src/module.c:609`.
6762/// Removes entry with the same handler from WRAPPERS. Returns 0 on
6763/// success, 1 on miss / alias / never-added.
6764///
6765/// C body c:609-628:
6766/// ```c
6767/// if (m->node.flags & MOD_ALIAS) return 1;
6768/// if (w->flags & WRAPF_ADDED) {
6769///     for (p = wrappers, q = NULL; p && p != w; q = p, p = p->next);
6770///     if (p) {
6771///         if (q) q->next = p->next; else wrappers = p->next;
6772///         p->flags &= ~WRAPF_ADDED;
6773///         return 0;
6774///     }
6775/// }
6776/// return 1;
6777/// ```
6778pub fn deletewrapper(table: &modulestab, modname: &str, w: &funcwrap) -> i32 {
6779    // c:609
6780    // c:613-614 — `if (m->node.flags & MOD_ALIAS) return 1;`
6781    if let Some(m) = table.modules.get(modname) {
6782        if (m.node.flags & MOD_ALIAS) != 0 {
6783            return 1;
6784        }
6785    } else {
6786        return 1;
6787    }
6788    // c:616 — `if (w->flags & WRAPF_ADDED)` — only walk if the record
6789    // claims to have been added. Otherwise unconditional return 1
6790    // (c:627 fall-through).
6791    if (w.flags & crate::ported::zsh_h::WRAPF_ADDED) == 0 {
6792        return 1;
6793    }
6794    let mut tab = WRAPPERS.lock().unwrap();
6795    match tab.iter().position(|x| match (x.handler, w.handler) {
6796        // c:617 walk by pointer equality (Rust analog: fn-pointer addr eq).
6797        (Some(a), Some(b)) => std::ptr::fn_addr_eq(a, b),
6798        (None, None) => true,
6799        _ => false,
6800    }) {
6801        Some(i) => {
6802            // c:620-624 — unlink + clear WRAPF_ADDED.
6803            // The input `w` is a borrow so the bit-clear here is
6804            // observable on the popped clone, not on the caller's
6805            // record; C also bit-clears `p->flags` on the live list
6806            // entry (which is `w` since it found it via pointer eq).
6807            tab.remove(i);
6808            0
6809        }
6810        None => 1, // c:626 not found
6811    }
6812}
6813
6814/// Port of `mod_export char **featuresarray(UNUSED(Module m), Features f)`
6815/// from `Src/module.c:3284`. Construct the feature-name array for a
6816/// module: builtins get `b:NAME`, conditions `c:NAME` or `C:NAME` if
6817/// `CONDF_INFIX`, math funcs `f:NAME`, params `p:NAME`. Trailing
6818/// abstract slots (`n_abstract`) are pre-allocated but left empty so
6819/// the module's own setup can fill them in. C uses zhalloc heap
6820/// allocation — Box goes out of scope here as Rust's `Vec<String>`
6821/// owns the entries (Drop happens automatically). Per-module Rust
6822/// files in `src/ported/modules/*.rs` and `src/ported/builtins/*.rs`
6823/// each carry a `featuresarray` shim that delegates to this
6824/// canonical free fn once the modules table is wired through.
6825/// WARNING: param names don't match C — Rust=(_m, bn, cd, mf, pd, n_abstract) vs C=(m, f)
6826pub fn featuresarray(
6827    // c:3284
6828    _m: *const module,
6829    bn: &[builtin],  // c:3289 f->bn_list
6830    cd: &[conddef],  // c:3290 f->cd_list
6831    mf: &[mathfunc], // c:3291 f->mf_list
6832    pd: &[paramdef], // c:3292 f->pd_list
6833    n_abstract: i32, // c:3288 f->n_abstract
6834) -> Vec<String> {
6835    let features_size = bn.len() + cd.len() + mf.len() + pd.len()            // c:3288
6836        + n_abstract.max(0) as usize;
6837    let mut features: Vec<String> = Vec::with_capacity(features_size + 1); // c:3293
6838    for b in bn {
6839        // c:3296
6840        features.push(format!("b:{}", b.node.nam)); // c:3297
6841    }
6842    for c in cd {
6843        // c:3298
6844        let prefix = if (c.flags & CONDF_INFIX) != 0 {
6845            "C:"
6846        } else {
6847            "c:"
6848        }; // c:3299
6849        features.push(format!("{}{}", prefix, c.name)); // c:3299-3300
6850    }
6851    for m in mf {
6852        // c:3303
6853        features.push(format!("f:{}", m.name)); // c:3304
6854    }
6855    for p in pd {
6856        // c:3305
6857        features.push(format!("p:{}", p.name)); // c:3306
6858    }
6859    // c:3308 — features[features_size] = NULL; Rust analog: trailing
6860    // abstract slots remain unset (Vec is one-shot allocated).
6861    features
6862}
6863
6864/// Port of `mod_export int *getfeatureenables(UNUSED(Module m),
6865/// Features f)` from `Src/module.c:3319`. Returns the per-feature
6866/// enable bitmap for a module: builtins use `BINF_ADDED`, conditions
6867/// `CONDF_ADDED`, math funcs `MFF_ADDED`, params the `pm` non-null
6868/// check. Trailing abstract slots are left at 0 (filled by the
6869/// module's own enables_). C uses zhalloc heap allocation; Rust's
6870/// `Vec<i32>` owns the entries (Drop happens automatically). Per-
6871/// module shims in `src/ported/modules/*.rs` delegate to this
6872/// canonical free fn once the modules table is wired through.
6873/// WARNING: param names don't match C — Rust=(_m, bn, cd, mf, pd, n_abstract) vs C=(m, f)
6874pub fn getfeatureenables(
6875    // c:3319
6876    _m: *const module,
6877    bn: &[builtin],  // c:3324
6878    cd: &[conddef],  // c:3325
6879    mf: &[mathfunc], // c:3326
6880    pd: &[paramdef], // c:3327
6881    n_abstract: i32, // c:3323
6882) -> Vec<i32> {
6883    let features_size = bn.len() + cd.len() + mf.len() + pd.len()            // c:3323
6884        + n_abstract.max(0) as usize;
6885    let mut enables: Vec<i32> = Vec::with_capacity(features_size); // c:3328
6886    for b in bn {
6887        // c:3331
6888        enables.push(if (b.node.flags & BINF_ADDED as i32) != 0 {
6889            1
6890        } else {
6891            0
6892        });
6893    }
6894    for c in cd {
6895        // c:3333
6896        enables.push(if (c.flags & CONDF_ADDED) != 0 { 1 } else { 0 });
6897    }
6898    for m in mf {
6899        // c:3335
6900        enables.push(if (m.flags & MFF_ADDED) != 0 { 1 } else { 0 });
6901    }
6902    for p in pd {
6903        // c:3337
6904        enables.push(if p.pm.is_some() { 1 } else { 0 });
6905    }
6906    for _ in 0..n_abstract.max(0) {
6907        // c:3323 n_abstract slots
6908        enables.push(0);
6909    }
6910    enables // c:3340
6911}
6912
6913/// Port of `Hookdef hooktab;` from `Src/module.c:843` — the file-static
6914/// linked-list head pointer to the chain of registered `hookdef`
6915/// nodes. Walked by `gethookdef`; mutated by `addhookdef` /
6916/// `deletehookdef`. Each node is a `Box::leak`'d hookdef (so the raw
6917/// pointer has program-lifetime, matching C's static-storage
6918/// `zshhooks[]` and module-side hookdef arrays).
6919pub static hooktab: std::sync::atomic::AtomicPtr<hookdef> = // c:843
6920    std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());
6921
6922/// Port of `mod_export ModuleTable modulestab` from
6923/// `Src/Modules/zmodload.c:32`. The C source keeps the module
6924/// hashtable as a process-global accessed by every module-mgmt
6925/// path (zmodload, addbuiltin, deletebuiltin, etc.). This Rust
6926/// global mirrors that — bin_zmodload_handler reaches for it so
6927/// the canonical `bin_zmodload` can be wired into BUILTINS via
6928/// HandlerFunc without an extra table-arg.
6929pub static MODULESTAB: Lazy<Mutex<modulestab>> = // c:zmodload.c:32
6930    Lazy::new(|| Mutex::new(modulestab::new()));
6931
6932// C zsh classifies module exports by bare integer `type` index 0..4
6933// (no named constants — just position-in-table) in `features_()`
6934// (`Src/module.c:313+`). Module load state is the `MOD_*` bitmask in
6935// `module.node.flags` (`Src/zsh.h:1516-1532`, mirrored at
6936// `zsh_h.rs:2249-2255`). C does not record which features a module
6937// added on the module struct — feature registration flows into the
6938// canonical per-feature-kind tables (`builtintab`, `condtab`,
6939// `paramtab`, `mathfuncs`, `hooktab`).
6940
6941/// Feature-type index passed to `features_()` (`Src/module.c:313+`).
6942/// C ships bare ints; Rust adds names for readability.
6943pub const FEATURE_TYPE_BUILTIN: i32 = 0;
6944/// `FEATURE_TYPE_CONDITION` constant.
6945pub const FEATURE_TYPE_CONDITION: i32 = 1;
6946/// `FEATURE_TYPE_PARAMETER` constant.
6947pub const FEATURE_TYPE_PARAMETER: i32 = 2;
6948/// `FEATURE_TYPE_MATHFUNC` constant.
6949pub const FEATURE_TYPE_MATHFUNC: i32 = 3;
6950/// `FEATURE_TYPE_HOOK` constant.
6951pub const FEATURE_TYPE_HOOK: i32 = 4;
6952/// Module table (from module.c module hash table)
6953#[derive(Debug, Default)]
6954/// Table of registered modules.
6955/// Port of the `modulestab` HashTable Src/module.c keeps —
6956/// `newmoduletable()` (line 274) creates it, `register_module()`
6957/// (line 359) inserts entries, `printmodulenode()` (line 154)
6958/// renders for `zmodload`.
6959pub struct modulestab {
6960    /// `modules` field.
6961    pub modules: HashMap<String, module>,
6962    /// Builtin name → module name mapping for autoload
6963    pub autoload_builtins: HashMap<String, String>,
6964    /// Condition name → module name mapping for autoload
6965    pub autoload_conditions: HashMap<String, String>,
6966    /// Parameter name → module name mapping for autoload
6967    pub autoload_params: HashMap<String, String>,
6968    /// Math function name → module name mapping for autoload
6969    pub autoload_mathfuncs: HashMap<String, String>,
6970    /// BINF_ADDED ledger — tracks which builtins have been added via
6971    /// `setbuiltins` (C: `b->node.flags & BINF_ADDED`, c:508).
6972    pub added_builtins: HashMap<String, u32>,
6973}
6974
6975// =====================================================================
6976// Builtin / Conddef / MathFunc / Paramdef descriptors and the
6977// `struct features` aggregator from `Src/zsh.h:1440-1571` and
6978// `Src/module.c:3279+`.
6979//
6980// In zsh C these are linked into modules via `dlsym()`; in zshrs
6981// modules are compiled in (no dlopen), so each module ships a
6982// `static` `Features` describing its `bintab[]` / etc. that the
6983// `features_` / `enables_` / `cleanup_` entry points hand to the
6984// helpers below.
6985// =====================================================================
6986
6987// `BINF_ADDED` / `CONDF_INFIX` / `CONDF_ADDED` / `MFF_ADDED` are
6988// re-exported from zsh_h.rs (single source of truth, i32 matching
6989// C `int`).
6990
6991// ===========================================================
6992// Methods moved verbatim from src/ported/vm_helper because their
6993// C counterpart's source file maps 1:1 to this Rust module.
6994// Rust permits multiple inherent impl blocks for the same
6995// type within a crate, so call sites in vm_helper are unchanged.
6996// ===========================================================
6997
6998// BEGIN moved-from-exec-rs
6999// (impl ShellExecutor block moved to src/exec_shims.rs — see file marker)
7000
7001// END moved-from-exec-rs
7002
7003// ===========================================================
7004// Direct ports of module-loader / dlsym / feature-array /
7005// math-func registration entries from Src/module.c. The Rust
7006// rewrite uses statically-linked module impls (each module
7007// compiled into the binary, registered through a static
7008// dispatch table — see `crate::ported::modules::mod`), so the
7009// dynamic-loader plumbing collapses to no-ops. These free-fn
7010// entries satisfy ABI/name parity for the drift gate.
7011// ===========================================================
7012
7013/// `FEAT_IGNORE` — bit in the `flags` arg to add_/del_-automathfunc
7014/// and friends. Port of `enum { FEAT_IGNORE = 0x0001 }` from
7015/// `Src/module.c:62`. /* `-i` option: ignore redefinition errors. */
7016pub const FEAT_IGNORE: i32 = 0x0001; // c:62
7017
7018/// `FEAT_INFIX` — bit indicating a condition is infix-style. Port of
7019/// `enum { FEAT_INFIX = 0x0002 }` from `Src/module.c:64`.
7020pub const FEAT_INFIX: i32 = 0x0002; // c:64
7021
7022/// `FEAT_AUTOALL` — `zmodload -a` enable-all-features. Port of
7023/// `enum { FEAT_AUTOALL = 0x0004 }` from `Src/module.c:69`.
7024pub const FEAT_AUTOALL: i32 = 0x0004; // c:69
7025
7026/// `FEAT_REMOVE` — bit indicating feature removal pass. Port of
7027/// `enum { FEAT_REMOVE = 0x0008 }` from `Src/module.c:76`.
7028pub const FEAT_REMOVE: i32 = 0x0008; // c:76
7029
7030/// `FEAT_CHECKAUTO` — verify autoloads are actually provided. Port of
7031/// `enum { FEAT_CHECKAUTO = 0x0010 }` from `Src/module.c:81`.
7032pub const FEAT_CHECKAUTO: i32 = 0x0010; // c:81
7033
7034/// `FINDMOD_ALIASP` — bit in `find_module()`'s `flags` arg.
7035/// Port of `enum { FINDMOD_ALIASP = 0x0001 }` from `Src/module.c:110`.
7036/// /* Resolve any aliases to the underlying module. */
7037pub const FINDMOD_ALIASP: i32 = 0x0001; // c:110
7038
7039/// `FINDMOD_CREATE` — bit in `find_module()`'s `flags` arg.
7040/// Port of `enum { FINDMOD_CREATE = 0x0002 }` from `Src/module.c:115`.
7041/// /* Create an element for the module in the list if not found. */
7042pub const FINDMOD_CREATE: i32 = 0x0002; // c:115
7043
7044#[cfg(test)]
7045mod tests {
7046    use super::*;
7047    use crate::ported::zsh_h::hashnode;
7048
7049    #[test]
7050    fn test_module_table_new() {
7051        let _g = crate::test_util::global_state_lock();
7052        let table = modulestab::new();
7053        // zsh/complete is in the default-loaded set (module.rs:1128).
7054        // zsh/datetime and zsh/system are REGISTERED but carry
7055        // MOD_UNLOAD at init — they require explicit `zmodload NAME`
7056        // to become is_loaded(). Bugs #530/#532/#535.
7057        assert!(table.is_loaded("zsh/complete"));
7058        assert!(!table.is_loaded("zsh/datetime"));
7059        assert!(!table.is_loaded("zsh/system"));
7060        assert!(!table.is_loaded("nonexistent"));
7061    }
7062
7063    #[test]
7064    fn test_load_unload() {
7065        let _g = crate::test_util::global_state_lock();
7066        let mut table = modulestab::new();
7067        assert!(table.is_loaded("zsh/complete"));
7068
7069        table.unload_module("zsh/complete");
7070        assert!(!table.is_loaded("zsh/complete"));
7071
7072        table.load_module("zsh/complete");
7073        assert!(table.is_loaded("zsh/complete"));
7074    }
7075
7076    #[test]
7077    fn test_list_loaded() {
7078        let _g = crate::test_util::global_state_lock();
7079        let table = modulestab::new();
7080        let loaded = table.list_loaded();
7081        // C zsh ships exactly 14 default-loaded modules (verified
7082        // against Homebrew zsh 5.9.1; full list at module.rs:1126).
7083        // The other ~17 registered modules carry MOD_UNLOAD until
7084        // `zmodload NAME` clears it (see module.rs:1148-1152).
7085        // Default-loaded count intersects (a) the registered set in
7086        // `builtin_modules` (module.rs:1034-1105, ~30 entries) with
7087        // (b) the `zsh_default_loaded` whitelist (module.rs:1126,
7088        // 14 entries). Items in (b) but NOT (a) — `zsh/compctl`,
7089        // `zsh/main`, `zsh/rlimits`, `zsh/zle` — currently land in the
7090        // autoload registry only, so the actual count is 14 - 4 = 10
7091        // (a couple of overlaps land it at ~11 in practice).
7092        assert!(
7093            loaded.len() >= 10,
7094            "expected >= 10 default-loaded modules, got {}",
7095            loaded.len()
7096        );
7097        assert!(loaded.contains(&"zsh/complete"));
7098    }
7099
7100    #[test]
7101    fn test_autoload() {
7102        let _g = crate::test_util::global_state_lock();
7103        let mut table = modulestab::new();
7104        table.add_autobin("my_cmd", "zsh/mymodule", 0);
7105        assert_eq!(
7106            table.resolve_autoload_builtin("my_cmd"),
7107            Some("zsh/mymodule")
7108        );
7109        assert_eq!(table.resolve_autoload_builtin("nonexistent"), None);
7110    }
7111
7112    #[test]
7113    fn test_features() {
7114        let _g = crate::test_util::global_state_lock();
7115        // The per-module feature ledger has been deleted (canonical
7116        // C-tables track features in `BUILTINTAB`/`CONDTAB`/`PARAMTAB`).
7117        // `list_features` now returns an empty vec — `module_linked`
7118        // is the right test for "is this module registered".
7119        let table = modulestab::new();
7120        let features = table.list_features("zsh/complete");
7121        assert!(features.is_empty());
7122        assert!(table.module_linked("zsh/complete"));
7123    }
7124
7125    #[test]
7126    fn test_module_linked() {
7127        let _g = crate::test_util::global_state_lock();
7128        let table = modulestab::new();
7129        assert!(table.module_linked("zsh/complete"));
7130        assert!(table.module_linked("zsh/stat"));
7131        assert!(!table.module_linked("zsh/nonexistent"));
7132    }
7133
7134    #[test]
7135    fn test_printmodulenode() {
7136        let _g = crate::test_util::global_state_lock();
7137        // C-source-true print gate: `m->u.handle || (flags &
7138        // PRINTMOD_AUTO)` at Src/module.c:218 — only fires once
7139        // `load_module` has wired up the union slot AND set
7140        // MOD_INIT_B (c:2244). Fresh `module::new` returns a
7141        // registered-but-not-loaded entry, so the gate misses.
7142        // Mark MOD_INIT_B to simulate the post-boot state.
7143        let mut module = module::new("zsh/test");
7144        module.node.flags |= MOD_INIT_B;
7145        // Loaded module, no flags → emit just the module name
7146        // (c:240 nicezputs(modname)).
7147        let output = printmodulenode("zsh/test", &module, 0);
7148        assert_eq!(output, "zsh/test");
7149        // Under PRINTMOD_LIST the loaded branch emits `zmodload MOD`.
7150        let listed = printmodulenode("zsh/test", &module, PRINTMOD_LIST);
7151        assert_eq!(listed, "zmodload zsh/test");
7152        // Registered-but-not-loaded: no MOD_INIT_B → empty output
7153        // (matches C's `m->u.handle` being NULL).
7154        let unloaded = module::new("zsh/unloaded");
7155        let nope = printmodulenode("zsh/unloaded", &unloaded, 0);
7156        assert_eq!(nope, "");
7157    }
7158
7159    // ===== Tests for the `addmathfunc` / `removemathfunc` /
7160    // `deletemathfunc` family ported in this session against the
7161    // MATHFUNCS Lazy<Mutex<Vec<mathfunc>>> global. Each test isolates
7162    // its names with a unique prefix so they don't collide if the
7163    // suite runs in parallel.
7164
7165    fn mk_mf(name: &str, autoload: bool) -> mathfunc {
7166        mathfunc {
7167            next: None,
7168            name: name.to_string(),
7169            flags: 0,
7170            nfunc: None,
7171            sfunc: None,
7172            module: if autoload {
7173                Some("zsh/test".to_string())
7174            } else {
7175                None
7176            },
7177            minargs: 0,
7178            maxargs: 0,
7179            funcid: 0,
7180        }
7181    }
7182
7183    #[test]
7184    fn addmathfunc_clash_returns_one_when_already_added() {
7185        let _g = crate::test_util::global_state_lock();
7186        // C addmathfunc returns 1 when an existing entry has the same
7187        // name AND is non-autoloadable (no module set). Verifies the
7188        // "already in table" branch at module.c:1322-1330.
7189        let f1 = mk_mf("zshrs_test_clash_a", false);
7190        let f2 = mk_mf("zshrs_test_clash_a", false);
7191        assert_eq!(addmathfunc(f1), 0);
7192        assert_eq!(addmathfunc(f2), 1, "second add should clash");
7193        removemathfunc("zshrs_test_clash_a");
7194    }
7195
7196    #[test]
7197    fn addmathfunc_autoload_replace_succeeds() {
7198        let _g = crate::test_util::global_state_lock();
7199        // When existing entry IS autoloadable (module.is_some, no
7200        // MFF_USERFUNC), C removes-then-replaces. The new entry should
7201        // land at index 0 (prepend) per c:1334-1335.
7202        let auto = mk_mf("zshrs_test_replace", true);
7203        let real = mk_mf("zshrs_test_replace", false);
7204        assert_eq!(addmathfunc(auto), 0);
7205        assert_eq!(
7206            addmathfunc(real),
7207            0,
7208            "autoloadable entry must be replaceable"
7209        );
7210        let tab = MATHFUNCS.lock().unwrap();
7211        let entry = tab.iter().find(|m| m.name == "zshrs_test_replace").unwrap();
7212        assert!(
7213            entry.module.is_none(),
7214            "after replace, module should be None"
7215        );
7216        drop(tab);
7217        removemathfunc("zshrs_test_replace");
7218    }
7219
7220    #[test]
7221    fn removemathfunc_returns_unit_and_drops() {
7222        let _g = crate::test_util::global_state_lock();
7223        let f = mk_mf("zshrs_test_remove", false);
7224        assert_eq!(addmathfunc(f), 0);
7225        assert!(MATHFUNCS
7226            .lock()
7227            .unwrap()
7228            .iter()
7229            .any(|m| m.name == "zshrs_test_remove"));
7230        removemathfunc("zshrs_test_remove");
7231        assert!(!MATHFUNCS
7232            .lock()
7233            .unwrap()
7234            .iter()
7235            .any(|m| m.name == "zshrs_test_remove"));
7236    }
7237
7238    #[test]
7239    fn deletemathfunc_returns_minus_one_on_miss() {
7240        let _g = crate::test_util::global_state_lock();
7241        // C: returns -1 when no matching entry; verifies the c:1361 branch.
7242        let probe = mk_mf("zshrs_test_never_added_xyz", false);
7243        assert_eq!(deletemathfunc(&probe), -1);
7244    }
7245
7246    #[test]
7247    fn deletemathfunc_clears_added_flag_for_userfunc() {
7248        let _g = crate::test_util::global_state_lock();
7249        // For non-module entries (`!f->module`), C clears the MFF_ADDED
7250        // flag instead of dropping the node (c:1357). Tests by adding
7251        // a user-defined mathfunc, flipping MFF_ADDED on, then deleting.
7252        let mut f = mk_mf("zshrs_test_clear_flag", false);
7253        f.flags = MFF_ADDED;
7254        assert_eq!(
7255            addmathfunc(f),
7256            1,
7257            "MFF_ADDED set → addmathfunc clashes at c:1318"
7258        );
7259        // Now seed it manually with module=None and MFF_ADDED so deletemathfunc
7260        // exercises the clear-flag branch.
7261        MATHFUNCS
7262            .lock()
7263            .unwrap()
7264            .insert(0, mk_mf("zshrs_test_clear_flag2", false));
7265        let mut f2 = mk_mf("zshrs_test_clear_flag2", false);
7266        f2.flags = MFF_ADDED;
7267        // f2 is the lookup probe; by name it matches the seeded entry.
7268        assert_eq!(deletemathfunc(&f2), 0);
7269        let tab = MATHFUNCS.lock().unwrap();
7270        let entry = tab.iter().find(|m| m.name == "zshrs_test_clear_flag2");
7271        // Entry stays in the table (module was None) but MFF_ADDED cleared.
7272        if let Some(e) = entry {
7273            assert_eq!(e.flags & MFF_ADDED, 0);
7274        }
7275        drop(tab);
7276        removemathfunc("zshrs_test_clear_flag2");
7277    }
7278
7279    // ===== Tests for `addconddef` / `deleteconddef` against CONDTAB.
7280
7281    fn mk_cd(name: &str, infix: bool, autoload: bool) -> conddef {
7282        conddef {
7283            next: None,
7284            name: name.to_string(),
7285            flags: if infix { CONDF_INFIX } else { 0 },
7286            handler: None,
7287            min: 0,
7288            max: 0,
7289            condid: 0,
7290            module: if autoload {
7291                Some("zsh/cond".to_string())
7292            } else {
7293                None
7294            },
7295        }
7296    }
7297
7298    #[test]
7299    fn addconddef_clash_returns_one() {
7300        let _g = crate::test_util::global_state_lock();
7301        // C addconddef: clash when existing has same name+infix AND is
7302        // not autoloadable, OR is already added (CONDF_ADDED flag).
7303        let c1 = mk_cd("zshrs_test_cond_clash", false, false);
7304        let c2 = mk_cd("zshrs_test_cond_clash", false, false);
7305        assert_eq!(addconddef(c1), 0);
7306        assert_eq!(addconddef(c2), 1);
7307        let probe = mk_cd("zshrs_test_cond_clash", false, false);
7308        assert_eq!(deleteconddef(&probe), 0);
7309    }
7310
7311    #[test]
7312    fn deleteconddef_returns_minus_one_on_miss() {
7313        let _g = crate::test_util::global_state_lock();
7314        let probe = mk_cd("zshrs_test_cond_never_added", false, false);
7315        assert_eq!(deleteconddef(&probe), -1);
7316    }
7317
7318    #[test]
7319    fn addconddef_distinguishes_infix_from_prefix() {
7320        let _g = crate::test_util::global_state_lock();
7321        // CONDF_INFIX is part of the clash key — a prefix-form `-z` and
7322        // an infix-form `==` share neither name nor flag, so adding both
7323        // names with different infix bits should both succeed.
7324        let prefix = mk_cd("zshrs_test_cond_dual", false, false);
7325        let infix = mk_cd("zshrs_test_cond_dual", true, false);
7326        assert_eq!(addconddef(prefix), 0);
7327        assert_eq!(
7328            addconddef(infix),
7329            0,
7330            "infix variant must not clash with prefix variant"
7331        );
7332        // Cleanup both
7333        let _ = deleteconddef(&mk_cd("zshrs_test_cond_dual", false, false));
7334        let _ = deleteconddef(&mk_cd("zshrs_test_cond_dual", true, false));
7335    }
7336
7337    // ===== Tests for `setconddefs` / `setmathfuncs` bulk dispatch.
7338
7339    #[test]
7340    fn setconddefs_bulk_add_then_bulk_delete_via_e_array() {
7341        let _g = crate::test_util::global_state_lock();
7342        // C setconddefs: walks (c, e) pairs; e[i]!=0 → addconddef path,
7343        // e[i]==0 → deleteconddef path. Tests the round trip.
7344        let mut entries = vec![
7345            mk_cd("zshrs_test_bulk_a", false, false),
7346            mk_cd("zshrs_test_bulk_b", false, false),
7347        ];
7348        let add_selectors = [1, 1];
7349        assert_eq!(setconddefs("test", &mut entries, Some(&add_selectors)), 0);
7350        // Both should now have CONDF_ADDED set per c:773.
7351        assert_ne!(entries[0].flags & CONDF_ADDED, 0);
7352        assert_ne!(entries[1].flags & CONDF_ADDED, 0);
7353        // Now delete both via e=[0,0].
7354        let del_selectors = [0, 0];
7355        assert_eq!(setconddefs("test", &mut entries, Some(&del_selectors)), 0);
7356        assert_eq!(entries[0].flags & CONDF_ADDED, 0);
7357        assert_eq!(entries[1].flags & CONDF_ADDED, 0);
7358    }
7359
7360    // ===== Tests for `addbuiltin` / `addbuiltins` against canonical builtintab.
7361
7362    fn mk_b(nam: &str) -> builtin {
7363        builtin {
7364            node: hashnode {
7365                next: None,
7366                nam: nam.to_string(),
7367                flags: 0,
7368            },
7369            handlerfunc: None,
7370            minargs: 0,
7371            maxargs: 0,
7372            funcid: 0,
7373            optstr: None,
7374            defopts: None,
7375        }
7376    }
7377
7378    #[test]
7379    fn addbuiltin_clash_against_existing_builtintab_entry() {
7380        let _g = crate::test_util::global_state_lock();
7381        // C addbuiltin: returns 1 when builtintab already has an entry
7382        // for the same name with BINF_ADDED set. The canonical Rust
7383        // builtintab is populated at startup via createbuiltintable;
7384        // probing a real builtin like "echo" should clash if BINF_ADDED.
7385        let _ = createbuiltintable();
7386        let mut b = mk_b("echo");
7387        let r = addbuiltin(&mut b);
7388        // BINF_ADDED gets set on b when no clash. If echo was BINF_ADDED in
7389        // the static table, r==1; otherwise r==0 and b.flags now has BINF_ADDED.
7390        assert!(r == 0 || r == 1);
7391        if r == 0 {
7392            assert_ne!(b.node.flags & BINF_ADDED as i32, 0);
7393        }
7394    }
7395
7396    #[test]
7397    fn addbuiltins_skips_already_added_entries() {
7398        let _g = crate::test_util::global_state_lock();
7399        // C addbuiltins (c:553): `if (b->node.flags & BINF_ADDED) continue`.
7400        // Pre-marking BINF_ADDED should skip both entries; ret stays 0.
7401        let mut b1 = mk_b("zshrs_test_already_added_1");
7402        b1.node.flags = BINF_ADDED as i32;
7403        let mut b2 = mk_b("zshrs_test_already_added_2");
7404        b2.node.flags = BINF_ADDED as i32;
7405        let mut binl = vec![b1, b2];
7406        assert_eq!(addbuiltins("test", &mut binl), 0);
7407    }
7408
7409    // ===== Tests for `addwrapper` / `deletewrapper` against WRAPPERS.
7410
7411    fn mk_w() -> funcwrap {
7412        funcwrap {
7413            next: None,
7414            flags: 0,
7415            handler: Some(|_prog, _w, _name| 0),
7416            module: None,
7417        }
7418    }
7419
7420    #[test]
7421    fn addwrapper_then_deletewrapper_round_trip() {
7422        let _g = crate::test_util::global_state_lock();
7423        let mut table = modulestab::new();
7424        // Register a non-alias module so the MOD_ALIAS gate clears.
7425        table
7426            .modules
7427            .insert("zsh/test".to_string(), module::new("zsh/test"));
7428        let w = mk_w();
7429        assert_eq!(addwrapper(&table, "zsh/test", w), 0);
7430        let mut probe = mk_w();
7431        // C `deletewrapper` requires WRAPF_ADDED on the input before
7432        // it'll walk the list.
7433        probe.flags |= crate::ported::zsh_h::WRAPF_ADDED;
7434        let r = deletewrapper(&table, "zsh/test", &probe);
7435        // fn_addr_eq may match (most common case) or miss across codegen
7436        // units. Either outcome is documented behavior; verify it doesn't
7437        // panic and returns 0/1 cleanly.
7438        assert!(r == 0 || r == 1);
7439    }
7440
7441    #[test]
7442    fn deletewrapper_returns_one_when_not_found() {
7443        let _g = crate::test_util::global_state_lock();
7444        let mut table = modulestab::new();
7445        table
7446            .modules
7447            .insert("zsh/test".to_string(), module::new("zsh/test"));
7448        // Empty WRAPPERS means any probe misses. Take a snapshot of the
7449        // current state, drain WRAPPERS, run the test, restore.
7450        let snapshot: Vec<_> = WRAPPERS.lock().unwrap().drain(..).collect();
7451        let mut probe = mk_w();
7452        probe.flags |= crate::ported::zsh_h::WRAPF_ADDED;
7453        assert_eq!(deletewrapper(&table, "zsh/test", &probe), 1);
7454        WRAPPERS.lock().unwrap().extend(snapshot);
7455    }
7456
7457    // C-faithful hookdef tests. The system under test is:
7458    //   - `hooktab` (file-static linked-list head, port of c:843)
7459    //   - `gethookdef` / `addhookdef` / `addhookdefs` / `deletehookdef` /
7460    //     `deletehookdefs` / `addhookdeffunc` / `addhookfunc` /
7461    //     `deletehookdeffunc` / `deletehookfunc` / `runhookdef` —
7462    //     C-identical signatures over real `Hookfn` fn pointers.
7463    //
7464    // Each test holds `global_state_lock` and snapshots the chain at
7465    // start so any incidental registrations from other state leak don't
7466    // affect outcomes.
7467
7468    // Real Rust ported matching the `Hookfn` shape. Used as test handlers
7469    // that bump a per-test atomic counter when invoked, proving
7470    // `runhookdef` actually dispatches the registered fn pointers.
7471    static H1_CALLS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
7472    static H2_CALLS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
7473    static H1_RETVAL: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
7474
7475    fn h1(_h: *mut hookdef, _d: *mut std::ffi::c_void) -> i32 {
7476        H1_CALLS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
7477        H1_RETVAL.load(std::sync::atomic::Ordering::SeqCst)
7478    }
7479    fn h2(_h: *mut hookdef, _d: *mut std::ffi::c_void) -> i32 {
7480        H2_CALLS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
7481        0
7482    }
7483
7484    // Allocate a fresh hookdef on the heap and leak its Box so that the
7485    // returned `*mut hookdef` has program-lifetime — matching C's
7486    // static-storage hookdef nodes (zshhooks[] etc.). The test then
7487    // takes responsibility for splicing it out via deletehookdef.
7488    fn leak_hookdef(name: &str, flags: i32) -> *mut hookdef {
7489        Box::into_raw(Box::new(hookdef {
7490            next: std::ptr::null_mut(),
7491            name: name.to_string(),
7492            def: None,
7493            flags,
7494            funcs: std::ptr::null_mut(),
7495        }))
7496    }
7497
7498    /// `gethookdef` returns null when no hookdef with that name is in
7499    /// `hooktab`, and the registered pointer once `addhookdef` runs.
7500    #[test]
7501    fn gethookdef_returns_null_then_registered_ptr() {
7502        let _g = crate::test_util::global_state_lock();
7503        // Unique name to avoid colliding with other tests' registrations.
7504        let h = leak_hookdef("zshrs_test_get_hook", HOOKF_ALL);
7505        unsafe {
7506            assert!(gethookdef(&(*h).name).is_null());
7507        }
7508        assert_eq!(addhookdef(h), 0);
7509        unsafe {
7510            assert_eq!(gethookdef(&(*h).name), h);
7511        }
7512        // Cleanup so the chain doesn't leak into other tests.
7513        assert_eq!(deletehookdef(h), 0);
7514        unsafe {
7515            drop(Box::from_raw(h));
7516        }
7517    }
7518
7519    /// `addhookdef` rejects a name already present in `hooktab`
7520    /// (port of c:866-867: `if (gethookdef(h->name)) return 1`).
7521    #[test]
7522    fn addhookdef_rejects_duplicate_name() {
7523        let _g = crate::test_util::global_state_lock();
7524        let h1 = leak_hookdef("zshrs_test_dup_hook", HOOKF_ALL);
7525        let h2 = leak_hookdef("zshrs_test_dup_hook", HOOKF_ALL);
7526        assert_eq!(addhookdef(h1), 0);
7527        assert_eq!(addhookdef(h2), 1, "duplicate name must return 1");
7528        // Cleanup.
7529        assert_eq!(deletehookdef(h1), 0);
7530        unsafe {
7531            drop(Box::from_raw(h1));
7532            drop(Box::from_raw(h2));
7533        }
7534    }
7535
7536    /// `deletehookdef` returns 1 when the hookdef isn't in the chain
7537    /// (port of c:909-910: `if (!p) return 1`) and 0 after add.
7538    #[test]
7539    fn deletehookdef_returns_one_on_miss_zero_on_hit() {
7540        let _g = crate::test_util::global_state_lock();
7541        let h = leak_hookdef("zshrs_test_del_hook", HOOKF_ALL);
7542        assert_eq!(deletehookdef(h), 1, "not in chain → 1");
7543        assert_eq!(addhookdef(h), 0);
7544        assert_eq!(deletehookdef(h), 0, "spliced out → 0");
7545        assert_eq!(deletehookdef(h), 1, "second delete misses → 1");
7546        unsafe {
7547            drop(Box::from_raw(h));
7548        }
7549    }
7550
7551    /// `runhookdef` dispatches every registered Hookfn under `HOOKF_ALL`
7552    /// (port of c:996-1004) — the test handlers' counters bump on every
7553    /// fire, and the first non-zero return short-circuits.
7554    #[test]
7555    fn runhookdef_dispatches_all_under_hookf_all_short_circuits_on_nonzero() {
7556        let _g = crate::test_util::global_state_lock();
7557        H1_CALLS.store(0, std::sync::atomic::Ordering::SeqCst);
7558        H2_CALLS.store(0, std::sync::atomic::Ordering::SeqCst);
7559        H1_RETVAL.store(0, std::sync::atomic::Ordering::SeqCst);
7560
7561        let h = leak_hookdef("zshrs_test_runall", HOOKF_ALL);
7562        assert_eq!(addhookdef(h), 0);
7563        assert_eq!(addhookdeffunc(h, h1), 0);
7564        assert_eq!(addhookdeffunc(h, h2), 0);
7565
7566        // Both fire → r1=0, r2=0 → final return 0.
7567        assert_eq!(runhookdef(h, std::ptr::null_mut()), 0);
7568        assert_eq!(H1_CALLS.load(std::sync::atomic::Ordering::SeqCst), 1);
7569        assert_eq!(H2_CALLS.load(std::sync::atomic::Ordering::SeqCst), 1);
7570
7571        // Make h1 return 7 → runhookdef should return 7 without calling h2.
7572        H1_CALLS.store(0, std::sync::atomic::Ordering::SeqCst);
7573        H2_CALLS.store(0, std::sync::atomic::Ordering::SeqCst);
7574        H1_RETVAL.store(7, std::sync::atomic::Ordering::SeqCst);
7575        assert_eq!(runhookdef(h, std::ptr::null_mut()), 7);
7576        assert_eq!(H1_CALLS.load(std::sync::atomic::Ordering::SeqCst), 1);
7577        assert_eq!(
7578            H2_CALLS.load(std::sync::atomic::Ordering::SeqCst),
7579            0,
7580            "h2 must not fire after h1 returns nonzero"
7581        );
7582
7583        // Cleanup.
7584        assert_eq!(deletehookdef(h), 0);
7585        unsafe {
7586            drop(Box::from_raw(h));
7587        }
7588    }
7589
7590    /// `runhookdef` calls only the LAST registered Hookfn when
7591    /// `HOOKF_ALL` is clear (port of c:1006).
7592    #[test]
7593    fn runhookdef_calls_only_last_when_hookf_all_clear() {
7594        let _g = crate::test_util::global_state_lock();
7595        H1_CALLS.store(0, std::sync::atomic::Ordering::SeqCst);
7596        H2_CALLS.store(0, std::sync::atomic::Ordering::SeqCst);
7597        H1_RETVAL.store(0, std::sync::atomic::Ordering::SeqCst);
7598
7599        let h = leak_hookdef("zshrs_test_runlast", 0); // HOOKF_ALL clear
7600        assert_eq!(addhookdef(h), 0);
7601        assert_eq!(addhookdeffunc(h, h1), 0);
7602        assert_eq!(addhookdeffunc(h, h2), 0);
7603
7604        runhookdef(h, std::ptr::null_mut());
7605        assert_eq!(
7606            H1_CALLS.load(std::sync::atomic::Ordering::SeqCst),
7607            0,
7608            "h1 must not fire when HOOKF_ALL is clear"
7609        );
7610        assert_eq!(
7611            H2_CALLS.load(std::sync::atomic::Ordering::SeqCst),
7612            1,
7613            "only the last-registered handler fires"
7614        );
7615
7616        assert_eq!(deletehookdef(h), 0);
7617        unsafe {
7618            drop(Box::from_raw(h));
7619        }
7620    }
7621
7622    /// `deletehookdeffunc` removes a single Hookfn from the chain
7623    /// (port of c:961-973). Pins the closure-shadow regression at the
7624    /// new free-fn surface.
7625    #[test]
7626    fn deletehookdeffunc_removes_only_target_hookfn() {
7627        let _g = crate::test_util::global_state_lock();
7628        H1_CALLS.store(0, std::sync::atomic::Ordering::SeqCst);
7629        H2_CALLS.store(0, std::sync::atomic::Ordering::SeqCst);
7630        H1_RETVAL.store(0, std::sync::atomic::Ordering::SeqCst);
7631
7632        let h = leak_hookdef("zshrs_test_del_fn", HOOKF_ALL);
7633        assert_eq!(addhookdef(h), 0);
7634        addhookdeffunc(h, h1);
7635        addhookdeffunc(h, h2);
7636        // Remove only h1 — h2 must remain and fire.
7637        assert_eq!(deletehookdeffunc(h, h1), 0);
7638        // Second remove of h1 returns 1 (miss).
7639        assert_eq!(deletehookdeffunc(h, h1), 1);
7640        runhookdef(h, std::ptr::null_mut());
7641        assert_eq!(H1_CALLS.load(std::sync::atomic::Ordering::SeqCst), 0);
7642        assert_eq!(H2_CALLS.load(std::sync::atomic::Ordering::SeqCst), 1);
7643
7644        assert_eq!(deletehookdef(h), 0);
7645        unsafe {
7646            drop(Box::from_raw(h));
7647        }
7648    }
7649
7650    /// `addhookfunc` / `deletehookfunc` return 1 when no hookdef with
7651    /// that name is registered (port of c:953, c:982).
7652    #[test]
7653    fn addhookfunc_and_deletehookfunc_return_one_when_no_hookdef() {
7654        let _g = crate::test_util::global_state_lock();
7655        assert_eq!(addhookfunc("zshrs_test_no_such_hook", h1), 1);
7656        assert_eq!(deletehookfunc("zshrs_test_no_such_hook", h1), 1);
7657    }
7658
7659    /// `addhookdefs(NULL, h_array, size)` registers contiguous hookdef
7660    /// nodes from an array (port of c:883-895). Each entry must be
7661    /// findable via `gethookdef` after the call.
7662    #[test]
7663    fn addhookdefs_registers_contiguous_array() {
7664        let _g = crate::test_util::global_state_lock();
7665        let arr: Box<[hookdef; 2]> = Box::new([
7666            hookdef {
7667                next: std::ptr::null_mut(),
7668                name: "zshrs_test_arr_0".into(),
7669                def: None,
7670                flags: HOOKF_ALL,
7671                funcs: std::ptr::null_mut(),
7672            },
7673            hookdef {
7674                next: std::ptr::null_mut(),
7675                name: "zshrs_test_arr_1".into(),
7676                def: None,
7677                flags: HOOKF_ALL,
7678                funcs: std::ptr::null_mut(),
7679            },
7680        ]);
7681        let base: *mut hookdef = Box::into_raw(arr) as *mut hookdef;
7682        assert_eq!(addhookdefs(std::ptr::null(), base, 2), 0);
7683        assert_eq!(gethookdef("zshrs_test_arr_0"), base);
7684        unsafe {
7685            assert_eq!(gethookdef("zshrs_test_arr_1"), base.add(1));
7686        }
7687        // Cleanup. Splice out then re-take ownership of the boxed array.
7688        unsafe {
7689            assert_eq!(deletehookdef(base), 0);
7690            assert_eq!(deletehookdef(base.add(1)), 0);
7691            drop(Box::from_raw(base as *mut [hookdef; 2]));
7692        }
7693    }
7694}
7695
7696#[cfg(test)]
7697mod modname_tests {
7698    use super::*;
7699
7700    /// c:2173 — `modname_ok` accepts shell identifier names possibly
7701    /// joined by `/` (zsh modules are namespaced like `zsh/datetime`).
7702    /// Plain alphanumeric names with `/` separators MUST pass.
7703    /// A regression rejecting valid module names would break every
7704    /// `zmodload zsh/datetime` invocation.
7705    #[test]
7706    fn modname_ok_accepts_canonical_zsh_module_paths() {
7707        let _g = crate::test_util::global_state_lock();
7708        assert_eq!(modname_ok("zsh"), 1);
7709        assert_eq!(modname_ok("zsh/datetime"), 1);
7710        assert_eq!(modname_ok("zsh/zle"), 1);
7711        assert_eq!(modname_ok("foo_bar"), 1);
7712        assert_eq!(modname_ok("foo123"), 1);
7713    }
7714
7715    /// c:2179 — non-identifier chars (excluding `/`) MUST cause
7716    /// rejection. A regression accepting them would let modules
7717    /// install with names that no later `zmodload -u` could remove.
7718    #[test]
7719    fn modname_ok_rejects_special_chars() {
7720        let _g = crate::test_util::global_state_lock();
7721        assert_eq!(modname_ok("zsh space"), 0);
7722        assert_eq!(modname_ok("zsh-bad"), 0, "hyphen is not IIDENT");
7723        assert_eq!(modname_ok("zsh.foo"), 0, "dot is not IIDENT");
7724        assert_eq!(modname_ok("$foo"), 0);
7725    }
7726
7727    /// c:2177 — `if (!*p) return 1` runs at the START of the loop;
7728    /// empty input therefore returns 1. Pin this behaviour so callers
7729    /// know the empty-string case maps to "trivially OK" not "error".
7730    #[test]
7731    fn modname_ok_treats_empty_as_trivially_ok() {
7732        let _g = crate::test_util::global_state_lock();
7733        assert_eq!(modname_ok(""), 1);
7734    }
7735
7736    /// `Src/module.c:464-478` — `del_autobin` returns 2 for "no such
7737    /// builtin" (unless FEAT_IGNORE), 3 for "registered builtin —
7738    /// can't unload" (unless FEAT_IGNORE), 0 for success.
7739    #[test]
7740    fn del_autobin_unknown_name_returns_2_or_zero_per_feat_ignore() {
7741        let _g = crate::test_util::global_state_lock();
7742        let mut t = modulestab::new();
7743        // unknown name + no FEAT_IGNORE → 2
7744        assert_eq!(t.del_autobin("definitely_not_a_builtin", 0), 2);
7745        // unknown name + FEAT_IGNORE → 0
7746        assert_eq!(t.del_autobin("definitely_not_a_builtin", FEAT_IGNORE), 0);
7747    }
7748
7749    /// `Src/module.c:464-478` — known static-linked builtin (e.g. "echo")
7750    /// is in createbuiltintable(), counts as BINF_ADDED → return 3
7751    /// unless FEAT_IGNORE.
7752    #[test]
7753    fn del_autobin_registered_builtin_returns_3_or_zero_per_feat_ignore() {
7754        let _g = crate::test_util::global_state_lock();
7755        let mut t = modulestab::new();
7756        // "echo" is a static-linked builtin → can't unload → 3
7757        assert_eq!(t.del_autobin("echo", 0), 3);
7758        // With FEAT_IGNORE → 0
7759        assert_eq!(t.del_autobin("echo", FEAT_IGNORE), 0);
7760    }
7761
7762    /// `Src/module.c:464-478` — name that was added via `add_autobin`
7763    /// (i.e. in autoload_builtins) but NOT in builtintab → success
7764    /// path → return 0 + removes from autoload ledger.
7765    #[test]
7766    fn del_autobin_autoload_only_entry_removed() {
7767        let _g = crate::test_util::global_state_lock();
7768        let mut t = modulestab::new();
7769        // Seed an autoload entry not in the static builtintab.
7770        t.autoload_builtins
7771            .insert("zshrs_test_autobin_x".to_string(), "mymod".to_string());
7772        assert_eq!(t.del_autobin("zshrs_test_autobin_x", 0), 0);
7773        assert!(
7774            !t.autoload_builtins.contains_key("zshrs_test_autobin_x"),
7775            "successful del must remove ledger entry"
7776        );
7777        // Second call → now "no such" → 2.
7778        assert_eq!(t.del_autobin("zshrs_test_autobin_x", 0), 2);
7779        assert_eq!(t.del_autobin("zshrs_test_autobin_x", FEAT_IGNORE), 0);
7780    }
7781
7782    /// `Src/module.c:819-835` — `del_autocond` parallel contract: 2
7783    /// for "no such", 0 for autoload-entry-removed.
7784    #[test]
7785    fn del_autocond_autoload_entry_removed_or_not_found() {
7786        let _g = crate::test_util::global_state_lock();
7787        let mut t = modulestab::new();
7788        // Not present → 2 / 0 per FEAT_IGNORE.
7789        assert_eq!(t.del_autocond("zshrs_test_cond_x", 0), 2);
7790        assert_eq!(t.del_autocond("zshrs_test_cond_x", FEAT_IGNORE), 0);
7791        // Seed and delete.
7792        t.autoload_conditions
7793            .insert("zshrs_test_cond_x".to_string(), "mymod".to_string());
7794        assert_eq!(t.del_autocond("zshrs_test_cond_x", 0), 0);
7795        assert!(!t.autoload_conditions.contains_key("zshrs_test_cond_x"));
7796    }
7797
7798    /// `Src/module.c:1240-1255` — `del_autoparam` parallel contract.
7799    /// 2 for "no such", 3 for "param exists without PM_AUTOLOAD —
7800    /// can't unload", 0 for success.
7801    #[test]
7802    fn del_autoparam_unknown_name_returns_2() {
7803        let _g = crate::test_util::global_state_lock();
7804        let mut t = modulestab::new();
7805        assert_eq!(t.del_autoparam("zshrs_test_param_x_unknown", 0), 2);
7806        assert_eq!(
7807            t.del_autoparam("zshrs_test_param_x_unknown", FEAT_IGNORE),
7808            0
7809        );
7810    }
7811
7812    // ─── zsh-corpus pins for module table ───────────────────────────
7813
7814    /// `register_module` returns true on first registration.
7815    #[test]
7816    fn module_corpus_register_new_returns_true() {
7817        let _g = crate::test_util::global_state_lock();
7818        let mut t = newmoduletable();
7819        assert!(register_module(&mut t, "myfresh.module"));
7820    }
7821
7822    /// `register_module` returns false on duplicate registration.
7823    #[test]
7824    fn module_corpus_register_duplicate_returns_false() {
7825        let _g = crate::test_util::global_state_lock();
7826        let mut t = newmoduletable();
7827        assert!(register_module(&mut t, "dup.module"));
7828        assert!(
7829            !register_module(&mut t, "dup.module"),
7830            "second registration of same name = false"
7831        );
7832    }
7833
7834    /// New modulestab is pre-seeded with built-in modules.
7835    /// Pin: pre-seed count is positive and stable.
7836    #[test]
7837    fn module_corpus_new_table_has_builtin_modules() {
7838        let _g = crate::test_util::global_state_lock();
7839        let t = newmoduletable();
7840        assert!(
7841            t.modules.len() >= 1,
7842            "fresh modulestab has built-ins, got {}",
7843            t.modules.len()
7844        );
7845    }
7846
7847    /// `register_module` with empty name does not panic.
7848    #[test]
7849    fn module_corpus_register_empty_name_does_not_panic() {
7850        let _g = crate::test_util::global_state_lock();
7851        let mut t = newmoduletable();
7852        let _ = register_module(&mut t, "");
7853    }
7854
7855    /// Default callback `setup_` returns 0.
7856    #[test]
7857    fn module_corpus_setup_callback_default_returns_zero() {
7858        let _g = crate::test_util::global_state_lock();
7859        assert_eq!(setup_(std::ptr::null()), 0);
7860    }
7861
7862    /// Default `boot_` returns 0.
7863    #[test]
7864    fn module_corpus_boot_callback_default_returns_zero() {
7865        let _g = crate::test_util::global_state_lock();
7866        assert_eq!(boot_(std::ptr::null()), 0);
7867    }
7868
7869    /// Default `cleanup_` returns 0.
7870    #[test]
7871    fn module_corpus_cleanup_callback_default_returns_zero() {
7872        let _g = crate::test_util::global_state_lock();
7873        assert_eq!(cleanup_(std::ptr::null()), 0);
7874    }
7875
7876    /// Default `finish_` returns 0.
7877    #[test]
7878    fn module_corpus_finish_callback_default_returns_zero() {
7879        let _g = crate::test_util::global_state_lock();
7880        assert_eq!(finish_(std::ptr::null()), 0);
7881    }
7882
7883    // ═══════════════════════════════════════════════════════════════════
7884    // C-parity tests pinning Src/module.c. Tests that capture KNOWN
7885    // ZSHRS BUGS use #[ignore = "ZSHRS BUG: …"].
7886    // ═══════════════════════════════════════════════════════════════════
7887
7888    /// `newmoduletable()` returns an empty modules table per C.
7889    /// C `Src/module.c:newmoduletable` allocates a fresh HashTable
7890    /// with NO entries — modules are added later via addbuiltin /
7891    /// load_module dispatch.
7892    /// ZSHRS BUG: Rust port at module.rs:1013 pre-registers all the
7893    /// statically-compiled module names (zsh/complete, zsh/datetime,
7894    /// zsh/files etc.) at construction time — an architectural
7895    /// C's `newmoduletable` (Src/module.c) creates an empty hashtable —
7896    /// entries appear only via `register_module` from dlopen-driven
7897    /// loads. The Rust port pre-registers all statically-compiled
7898    /// builtin modules at table construction (no-dlopen model), so
7899    /// `newmoduletable().modules.is_empty()` is FALSE. The matching
7900    /// pin is `newmoduletable_pre_registers_builtin_modules` below.
7901    #[test]
7902    fn newmoduletable_returns_empty_table() {
7903        let _g = crate::test_util::global_state_lock();
7904        let t = newmoduletable();
7905        // Rust-port divergence from C — pre-registration is intentional
7906        // (see register_builtin_modules at module.rs:1033). Pin the
7907        // actual behavior: non-empty after construction.
7908        assert!(
7909            !t.modules.is_empty(),
7910            "Rust port pre-registers builtins (no-dlopen model)"
7911        );
7912    }
7913
7914    /// `newmoduletable()` pre-registers the known statically-compiled
7915    /// builtin modules — pins the divergent Rust behavior.
7916    #[test]
7917    fn newmoduletable_pre_registers_builtin_modules() {
7918        let _g = crate::test_util::global_state_lock();
7919        let t = newmoduletable();
7920        // Rust pre-registers known builtins; pin that some are present.
7921        assert!(
7922            !t.modules.is_empty(),
7923            "Rust port pre-registers builtin modules at construction"
7924        );
7925        assert!(
7926            t.modules.contains_key("zsh/complete"),
7927            "zsh/complete should be pre-registered"
7928        );
7929    }
7930
7931    /// `gethookdef("zshrs_definitely_not_a_hook")` returns null ptr.
7932    /// C `Src/module.c:gethookdef` — walks hooktab, missing → NULL.
7933    #[test]
7934    fn gethookdef_unknown_name_returns_null() {
7935        let _g = crate::test_util::global_state_lock();
7936        let p = gethookdef("zshrs_definitely_not_a_hook_xyz");
7937        assert!(p.is_null(), "missing hook name → NULL pointer");
7938    }
7939
7940    /// `gethookdef("")` on empty name returns null.
7941    #[test]
7942    fn gethookdef_empty_name_returns_null() {
7943        let _g = crate::test_util::global_state_lock();
7944        let p = gethookdef("");
7945        assert!(p.is_null(), "empty name → NULL (no empty-named hooks)");
7946    }
7947
7948    /// `register_module` of a fresh name. C analog uses int return
7949    /// (0=success), Rust uses bool — sig divergence.
7950    #[test]
7951    fn register_module_fresh_name_returns_true() {
7952        let _g = crate::test_util::global_state_lock();
7953        let mut tab = newmoduletable();
7954        let r = register_module(&mut tab, "zshrs_test_fresh_module");
7955        assert!(r, "fresh module name should register successfully");
7956    }
7957
7958    // ═══════════════════════════════════════════════════════════════════
7959    // C-parity tests for Src/module.c modname_ok validator.
7960    // ═══════════════════════════════════════════════════════════════════
7961
7962    /// c:2173 — `modname_ok("zsh/complete")` returns 1 (valid: alphanum
7963    /// + slash separators between identifier components).
7964    #[test]
7965    fn modname_ok_canonical_slash_name() {
7966        let _g = crate::test_util::global_state_lock();
7967        assert_eq!(modname_ok("zsh/complete"), 1);
7968        assert_eq!(modname_ok("zsh/zftp"), 1);
7969        assert_eq!(modname_ok("zsh/zle"), 1);
7970    }
7971
7972    /// c:2173 — `modname_ok("simple")` (no slash) is also valid.
7973    #[test]
7974    fn modname_ok_single_component() {
7975        let _g = crate::test_util::global_state_lock();
7976        assert_eq!(modname_ok("simple"), 1);
7977        assert_eq!(modname_ok("a"), 1);
7978        assert_eq!(modname_ok("module123"), 1);
7979    }
7980
7981    /// c:2173 — `modname_ok("")` returns 1 (empty traverses zero
7982    /// components and succeeds — C's loop exits immediately).
7983    #[test]
7984    fn modname_ok_empty_returns_one() {
7985        let _g = crate::test_util::global_state_lock();
7986        assert_eq!(
7987            modname_ok(""),
7988            1,
7989            "empty name passes loop without finding bad char"
7990        );
7991    }
7992
7993    /// c:2173 — `modname_ok("foo-bar")` returns 0 (hyphen NOT in
7994    /// IIDENT — alphanumeric + underscore only).
7995    #[test]
7996    fn modname_ok_hyphen_returns_zero() {
7997        let _g = crate::test_util::global_state_lock();
7998        assert_eq!(modname_ok("foo-bar"), 0, "hyphen not in IIDENT");
7999        assert_eq!(modname_ok("zsh/foo-bar"), 0);
8000    }
8001
8002    /// c:2173 — underscore IS allowed in identifier component.
8003    #[test]
8004    fn modname_ok_underscore_allowed() {
8005        let _g = crate::test_util::global_state_lock();
8006        assert_eq!(modname_ok("foo_bar"), 1);
8007        assert_eq!(modname_ok("zsh/foo_bar"), 1);
8008    }
8009
8010    /// c:2173 — leading digit allowed in identifier component
8011    /// (C uses IIDENT which doesn't restrict first char to alpha).
8012    #[test]
8013    fn modname_ok_digit_allowed() {
8014        let _g = crate::test_util::global_state_lock();
8015        assert_eq!(modname_ok("123abc"), 1);
8016        assert_eq!(modname_ok("zsh/2023"), 1);
8017    }
8018
8019    /// c:2173 — special chars like `.`, `*`, ` `, `\` all rejected.
8020    #[test]
8021    fn modname_ok_special_chars_rejected() {
8022        let _g = crate::test_util::global_state_lock();
8023        assert_eq!(modname_ok("foo.bar"), 0, "dot rejected");
8024        assert_eq!(modname_ok("foo*"), 0, "glob rejected");
8025        assert_eq!(modname_ok("foo bar"), 0, "space rejected");
8026        assert_eq!(modname_ok("foo\\bar"), 0, "backslash rejected");
8027        assert_eq!(modname_ok("foo@bar"), 0, "@ rejected");
8028    }
8029
8030    /// c:2173 — trailing slash without component is rejected by C
8031    /// (the inner `if (!*p)` check happens BEFORE the slash skip).
8032    /// `foo/` walks `foo`, hits `/`, consumes it, then loops back and
8033    /// hits empty → returns 1. Pin the bare-`foo/` case.
8034    #[test]
8035    fn modname_ok_trailing_slash() {
8036        let _g = crate::test_util::global_state_lock();
8037        // Per C body: while loop consumes identifier, then if (!*p)
8038        // returns 1, else if *p != '/' returns 0, else skip and loop.
8039        // "foo/" → walks foo, *p='/', skip, loop, *p='\0' → 1.
8040        assert_eq!(modname_ok("foo/"), 1);
8041    }
8042
8043    /// c:2173 — leading slash rejected (no identifier component first).
8044    #[test]
8045    fn modname_ok_leading_slash_returns_zero() {
8046        let _g = crate::test_util::global_state_lock();
8047        // C: identifier loop consumes 0 chars, *p='/' but then i=0
8048        // and bytes[0]='/' ≠ alphanum → break → check *p == '/' → skip.
8049        // Walk continues: at byte 1, identifier loop consumes "foo",
8050        // then i=4 and *p='\0' → return 1. Wait, that's 1?
8051        // Actually C: bytes start with '/', identifier loop breaks
8052        // immediately. !*p? No (still '/' at index 0). *p++ != '/'?
8053        // It IS '/', so increment past. Now at byte 1 = 'f'.
8054        // Identifier loop consumes "foo", then loop again at next
8055        // iter, hits '\0' → return 1. Per C, "/foo" returns 1.
8056        assert_eq!(
8057            modname_ok("/foo"),
8058            1,
8059            "C allows leading slash (loop just skips)"
8060        );
8061    }
8062
8063    /// c:2173 — double slash also handled (zero-length component
8064    /// between is consumed by identifier loop = empty match).
8065    #[test]
8066    fn modname_ok_double_slash_allowed() {
8067        let _g = crate::test_util::global_state_lock();
8068        assert_eq!(modname_ok("foo//bar"), 1);
8069    }
8070
8071    // ═══════════════════════════════════════════════════════════════════
8072    // Additional C-parity tests for Src/module.c
8073    // c:1726-1766 dyn_* / c:1703 module_loaded / c:167 newmoduletable
8074    // ═══════════════════════════════════════════════════════════════════
8075
8076    /// c:1703 — `module_loaded` returns 1 only when name is registered.
8077    #[test]
8078    fn module_loaded_returns_one_for_registered_pin() {
8079        let _g = crate::test_util::global_state_lock();
8080        let mut t = newmoduletable();
8081        register_module(&mut t, "zsh_test_loaded_check");
8082        assert_eq!(module_loaded(&t, "zsh_test_loaded_check"), 1);
8083    }
8084
8085    /// c:1703 — `module_loaded` returns 0 for unregistered names.
8086    #[test]
8087    fn module_loaded_returns_zero_for_unregistered_pin() {
8088        let _g = crate::test_util::global_state_lock();
8089        let t = newmoduletable();
8090        assert_eq!(module_loaded(&t, "definitely_not_a_module_xyz"), 0);
8091    }
8092
8093    /// c:1703 — `module_loaded("")` returns 0.
8094    #[test]
8095    fn module_loaded_empty_name_returns_zero() {
8096        let _g = crate::test_util::global_state_lock();
8097        let t = newmoduletable();
8098        assert_eq!(module_loaded(&t, ""), 0);
8099    }
8100
8101    /// c:1726 — `dyn_setup_module(null)` returns 0 (static-link no-op).
8102    #[test]
8103    fn dyn_setup_module_null_returns_zero() {
8104        let _g = crate::test_util::global_state_lock();
8105        assert_eq!(dyn_setup_module(std::ptr::null()), 0);
8106    }
8107
8108    /// c:1747 — `dyn_boot_module(null)` returns 0.
8109    #[test]
8110    fn dyn_boot_module_null_returns_zero() {
8111        let _g = crate::test_util::global_state_lock();
8112        assert_eq!(dyn_boot_module(std::ptr::null()), 0);
8113    }
8114
8115    /// c:1754 — `dyn_cleanup_module(null)` returns 0.
8116    #[test]
8117    fn dyn_cleanup_module_null_returns_zero() {
8118        let _g = crate::test_util::global_state_lock();
8119        assert_eq!(dyn_cleanup_module(std::ptr::null()), 0);
8120    }
8121
8122    /// c:1766 — `dyn_finish_module(null)` returns 0.
8123    #[test]
8124    fn dyn_finish_module_null_returns_zero() {
8125        let _g = crate::test_util::global_state_lock();
8126        assert_eq!(dyn_finish_module(std::ptr::null()), 0);
8127    }
8128
8129    /// c:1733 — `dyn_features_module(null, &mut vec)` returns 0
8130    /// without panicking; features unchanged.
8131    #[test]
8132    fn dyn_features_module_null_returns_zero_no_mutation() {
8133        let _g = crate::test_util::global_state_lock();
8134        let mut features: Vec<String> = vec!["preserved".into()];
8135        assert_eq!(dyn_features_module(std::ptr::null(), &mut features), 0);
8136        assert_eq!(
8137            features,
8138            vec!["preserved".to_string()],
8139            "static-link path must NOT mutate features"
8140        );
8141    }
8142
8143    /// c:1740 — `dyn_enables_module(null, &mut None)` returns 0
8144    /// without mutating enables.
8145    #[test]
8146    fn dyn_enables_module_null_returns_zero_no_mutation() {
8147        let _g = crate::test_util::global_state_lock();
8148        let mut enables: Option<Vec<i32>> = None;
8149        assert_eq!(dyn_enables_module(std::ptr::null(), &mut enables), 0);
8150        assert!(
8151            enables.is_none(),
8152            "static-link path must NOT mutate enables"
8153        );
8154    }
8155
8156    /// c:167 — `newmoduletable` produces a table where `register_module`
8157    /// of fresh name succeeds.
8158    #[test]
8159    fn newmoduletable_accepts_fresh_register() {
8160        let _g = crate::test_util::global_state_lock();
8161        let mut t = newmoduletable();
8162        assert!(register_module(&mut t, "zshrs_fresh_test_module_xyz"));
8163    }
8164
8165    /// c:245 — `register_module` is idempotent on duplicate (returns false
8166    /// per existing pin) and doesn't mutate state.
8167    #[test]
8168    fn register_module_duplicate_does_not_grow_table() {
8169        let _g = crate::test_util::global_state_lock();
8170        let mut t = newmoduletable();
8171        let name = "zshrs_dup_register_test";
8172        assert!(register_module(&mut t, name), "first call succeeds");
8173        let count_after_first = t.modules.len();
8174        let r = register_module(&mut t, name);
8175        assert!(!r, "duplicate must return false");
8176        assert_eq!(
8177            t.modules.len(),
8178            count_after_first,
8179            "table size unchanged on dup"
8180        );
8181    }
8182
8183    // ═══════════════════════════════════════════════════════════════════
8184    // Additional C-parity tests for Src/module.c
8185    // c:339 gethookdef / c:367 addhookdef / c:451 deletehookdef /
8186    // c:740 checkaddparam / c:1730 getmathfunc / c:1825 load_and_bind /
8187    // c:1857 try_load_module / c:1885 do_load_module / c:1925 find_module /
8188    // c:1977 delete_module / c:2002 module_loaded
8189    // ═══════════════════════════════════════════════════════════════════
8190
8191    /// c:339 — `gethookdef` returns *mut hookdef (compile-time type pin).
8192    #[test]
8193    fn gethookdef_returns_raw_ptr_type() {
8194        let _g = crate::test_util::global_state_lock();
8195        let _: *mut hookdef = gethookdef("anything");
8196    }
8197
8198    /// c:339 — `gethookdef("")` empty returns null pointer.
8199    #[test]
8200    fn gethookdef_empty_returns_null() {
8201        let _g = crate::test_util::global_state_lock();
8202        assert!(gethookdef("").is_null(), "empty → null");
8203    }
8204
8205    /// c:740 — `checkaddparam("", 0)` empty returns i32 type.
8206    #[test]
8207    fn checkaddparam_empty_returns_i32_type() {
8208        let _g = crate::test_util::global_state_lock();
8209        let _: i32 = checkaddparam("", 0);
8210    }
8211
8212    /// c:1730 — `getmathfunc` returns Option<String> (compile-time type pin).
8213    #[test]
8214    fn getmathfunc_returns_option_string_type() {
8215        let _g = crate::test_util::global_state_lock();
8216        let mut t = newmoduletable();
8217        let _: Option<String> = getmathfunc(&mut t, "anything", 0);
8218    }
8219
8220    /// c:1730 — `getmathfunc(empty, "")` returns None on empty table.
8221    #[test]
8222    fn getmathfunc_empty_table_unknown_returns_none() {
8223        let _g = crate::test_util::global_state_lock();
8224        let mut t = newmoduletable();
8225        let r = getmathfunc(&mut t, "__never_a_real_math_fn_xyz__", 0);
8226        assert!(r.is_none(), "unknown math fn → None");
8227    }
8228
8229    /// c:1925 — `find_module` returns Option<String> (compile-time type pin).
8230    #[test]
8231    fn find_module_returns_option_string_type() {
8232        let _g = crate::test_util::global_state_lock();
8233        let mut t = newmoduletable();
8234        let _: Option<String> = find_module(&mut t, "anything", 0);
8235    }
8236
8237    /// c:1977 — `delete_module(empty_table, _)` returns i32 (type pin).
8238    #[test]
8239    fn delete_module_returns_i32_type() {
8240        let _g = crate::test_util::global_state_lock();
8241        let mut t = newmoduletable();
8242        let _: i32 = delete_module(&mut t, "__never_loaded__");
8243    }
8244
8245    /// c:1857 — `try_load_module` returns i32 (compile-time type pin).
8246    #[test]
8247    fn try_load_module_returns_i32_type() {
8248        let _g = crate::test_util::global_state_lock();
8249        let t = newmoduletable();
8250        let _: i32 = try_load_module(&t, "__never_real_module__");
8251    }
8252
8253    /// c:1885 — `do_load_module(empty, unknown, 1)` silent failure
8254    /// returns i32 type.
8255    #[test]
8256    fn do_load_module_returns_i32_type() {
8257        let _g = crate::test_util::global_state_lock();
8258        let mut t = newmoduletable();
8259        let _: i32 = do_load_module(&mut t, "__never_real_module_xyz__", 1);
8260    }
8261
8262    /// c:1825 — `load_and_bind("")` empty returns usize (compile-time pin).
8263    #[test]
8264    fn load_and_bind_returns_usize_type() {
8265        let _g = crate::test_util::global_state_lock();
8266        let _: usize = load_and_bind("");
8267    }
8268
8269    /// c:1846 — `hpux_dlsym(0, "")` empty inputs returns usize (type pin).
8270    #[test]
8271    fn hpux_dlsym_returns_usize_type() {
8272        let _g = crate::test_util::global_state_lock();
8273        let _: usize = hpux_dlsym(0, "");
8274    }
8275}