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("edzputs(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("edzputs(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("edzputs(modname)); // c:210
213 out.push('='); // c:211
214 out.push_str("edzputs(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("edzputs(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("edzputs(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}