zsh/ported/module.rs
1//! Module system for zshrs
2//!
3//! Port from zsh/Src/module.c (3,646 lines)
4//!
5//! Hash of modules // c:46
6//! The list of hook functions defined. // c:840
7//! List of math functions. // c:1255
8//!
9//! In C, module.c provides dynamic loading of .so modules at runtime
10//! via dlopen/dlsym. In Rust, all modules are statically compiled into
11//! the binary — there's no dynamic loading. This module provides the
12//! registration, lookup, and management API that the rest of the shell
13//! uses to interact with module features (builtins, conditions, parameters,
14//! hooks, and math functions).
15
16use crate::ported::builtin::createbuiltintable;
17use crate::ported::hist::casemodify;
18use crate::ported::mem::ztrdup;
19use crate::ported::params::{createparam, createspecialhash, paramtab, unsetparam_pm};
20use crate::ported::signals::unqueue_signals;
21use crate::ported::utils::{zwarn, zwarnnam};
22use crate::ported::zsh_h::hashnode;
23use crate::ported::zsh_h::{
24 builtin, conddef, funcwrap, hookdef, linkedmod, linklist, linknode, mathfunc, options,
25 paramdef, Hookfn, Param, BINF_AUTOALL, CASMOD_LOWER, CASMOD_UPPER, CONDF_AUTOALL, HOOKF_ALL,
26 MFF_USERFUNC, MOD_ALIAS, MOD_BUSY, MOD_INIT_B, MOD_INIT_S, MOD_LINKED, MOD_SETUP, MOD_UNLOAD,
27 OPT_ARG_SAFE, OPT_ISSET, PM_ARRAY, PM_AUTOALL, PM_AUTOLOAD, PM_EFLOAT, PM_FFLOAT, PM_HASHED,
28 PM_INTEGER, PM_NAMEREF, PM_READONLY, PM_REMOVABLE, PM_SCALAR, PM_SPECIAL, PM_TIED, PM_TYPE, PRINT_LIST,
29};
30pub use crate::ported::zsh_h::{BINF_ADDED, CONDF_ADDED, CONDF_INFIX, MFF_ADDED};
31use crate::zsh_h::module;
32use once_cell::sync::Lazy;
33use std::collections::HashMap;
34use std::sync::Mutex;
35
36/// Free module node (from module.c freemodulenode)
37/// Free a module table entry.
38/// Port of `freemodulenode()` from `Src/module.c:119`. — C decl `freemodulenode(HashNode hn)`.
39/// Rust's
40/// `Drop` handles the per-field free; this exists for API
41/// parity with C callers.
42pub fn freemodulenode(hn: module) {
43 // Rust Drop handles this
44}
45
46/// `PRINTMOD_LIST` from `Src/module.c:136`. Long-form (`zmodload -L`)
47/// output.
48pub const PRINTMOD_LIST: i32 = 0x0001; // c:136
49/// `PRINTMOD_EXIST` from `Src/module.c:138`. Print only when the
50/// module exists.
51pub const PRINTMOD_EXIST: i32 = 0x0002; // c:138
52/// `PRINTMOD_ALIAS` from `Src/module.c:140`. Resolve aliases when
53/// emitting under `PRINTMOD_EXIST`.
54pub const PRINTMOD_ALIAS: i32 = 0x0004; // c:140
55/// `PRINTMOD_DEPS` from `Src/module.c:142`. Emit the dependency list
56/// (`zmodload -d`).
57pub const PRINTMOD_DEPS: i32 = 0x0008; // c:142
58/// `PRINTMOD_FEATURES` from `Src/module.c:144`. Emit feature flags
59/// (`zmodload -F`).
60pub const PRINTMOD_FEATURES: i32 = 0x0010; // c:144
61/// `PRINTMOD_LISTALL` from `Src/module.c:146`. Include disabled
62/// features (`zmodload -lL`).
63pub const PRINTMOD_LISTALL: i32 = 0x0020; // c:146
64/// `PRINTMOD_AUTO` from `Src/module.c:148`. Emit autoloads
65/// (`zmodload -a`).
66pub const PRINTMOD_AUTO: i32 = 0x0040; // c:148
67
68/// Port of `printmodulenode()` from `Src/module.c:154`. — C decl `printmodulenode(HashNode hn, int flags)`.
69///
70/// Formats one module entry for the various `zmodload -L`/`-a`/
71/// `-d`/`-F` listings. C writes to stdout; Rust returns the
72/// formatted string so call sites can route to the right output fd
73/// without depending on stdio. Dispatches on `flags`:
74///
75/// - `PRINTMOD_DEPS`: emit dep list (`zmodload -d MOD: dep1 dep2`
76/// under PRINTMOD_LIST, else `MOD: dep1 dep2`) — c:163-194
77/// - `PRINTMOD_EXIST`: emit just the module name when loaded
78/// (resolving alias when PRINTMOD_ALIAS set) — c:195-201
79/// - alias module: under PRINTMOD_LIST emit
80/// `zmodload -A MOD=ALIAS`, else `MOD -> ALIAS` — c:202-217
81/// - loaded module: under PRINTMOD_LIST emit `zmodload [-Fa] MOD`,
82/// else just the name — c:218-241
83pub fn printmodulenode(hn: &str, m: &module, flags: i32) -> String {
84 let modname = hn;
85 let mut out = String::new();
86
87 // c:163-194 — PRINTMOD_DEPS branch.
88 // C body:
89 // if (!m->deps) return;
90 // if (flags & PRINTMOD_LIST) {
91 // printf("zmodload -d ");
92 // if (modname[0] == '-') fputs("-- ", stdout);
93 // quotedzputs(modname, stdout);
94 // } else {
95 // nicezputs(modname, stdout);
96 // putchar(':');
97 // }
98 // for (n = firstnode(m->deps); n; incnode(n)) {
99 // putchar(' ');
100 // if (flags & PRINTMOD_LIST)
101 // quotedzputs((char *) getdata(n), stdout);
102 // else
103 // nicezputs((char *) getdata(n), stdout);
104 // }
105 //
106 // C uses quotedzputs (round-trippable shell input) under
107 // PRINTMOD_LIST so the emitted `zmodload -d MOD DEP1 DEP2` is
108 // re-parseable. The default path uses nicezputs (printable form
109 // with \M-/^ escapes for control bytes) which is listing-friendly.
110 // The two helpers are both already ported in src/ported/utils.rs.
111 if flags & PRINTMOD_DEPS != 0 {
112 use crate::ported::utils::{nicezputs, quotedzputs};
113 let deps = match m.deps.as_ref() {
114 Some(d) if !d.is_empty() => d,
115 _ => return out, // c:170-171
116 };
117 if flags & PRINTMOD_LIST != 0 {
118 out.push_str("zmodload -d "); // c:174
119 if modname.starts_with('-') {
120 out.push_str("-- "); // c:176
121 }
122 out.push_str("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()` from `Src/module.c:274`. — C decl `newmoduletable(int size, char const *name)`.
303/// the C
304/// source allocates the `modulestab` hash with `createhashtable`.
305/// WARNING: param names don't match C — Rust=() vs C=(size, name)
306pub fn newmoduletable() -> modulestab {
307 modulestab::new()
308}
309
310/// Port of `setup_()` from `Src/module.c:306`. — C decl `setup_(UNUSED(Module m))`.
311///
312/// C body: `setup_(UNUSED(Module m)) { return 0; }` — the no-op
313/// setup hook of the module subsystem itself.
314#[allow(unused_variables)]
315pub fn setup_(m: *const module) -> i32 {
316 // c:306
317 0 // c:306
318}
319
320/// Port of `features_()` from `Src/module.c:313`. — C decl `features_(UNUSED(Module m), UNUSED(char ***features))`.
321///
322/// C body:
323/// ```c
324/// features_(UNUSED(Module m), UNUSED(char ***features))
325/// {
326/// /* There are lots and lots of features, but they're not handled here. */
327/// return 1;
328/// }
329/// ```
330#[allow(unused_variables)]
331pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
332 // c:313
333 /* There are lots and lots of features, but they're not handled here. */ // c:313-318
334 1 // c:319
335}
336
337/// Port of `enables_()` from `Src/module.c:324`. — C decl `enables_(UNUSED(Module m), UNUSED(int **enables))`.
338///
339/// C body: `enables_(UNUSED(Module m), UNUSED(int **enables)) { return 1; }`
340/// — the module subsystem itself doesn't manage feature enables.
341#[allow(unused_variables)]
342pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
343 // c:324
344 1 // c:324
345}
346
347/// Port of `boot_()` from `Src/module.c:331`. — C decl `boot_(UNUSED(Module m))`.
348///
349/// C body: `boot_(UNUSED(Module m)) { return 0; }` — the no-op
350/// boot hook of the module subsystem itself.
351#[allow(unused_variables)]
352pub fn boot_(m: *const module) -> i32 {
353 // c:331
354 0 // c:331
355}
356
357/// Port of `cleanup_()` from `Src/module.c:338`. — C decl `cleanup_(UNUSED(Module m))`.
358///
359/// C body: `cleanup_(UNUSED(Module m)) { return 0; }` — the no-op
360/// cleanup hook of the module subsystem itself.
361#[allow(unused_variables)]
362pub fn cleanup_(m: *const module) -> i32 {
363 // c:338
364 0 // c:338
365}
366
367/// Port of `finish_()` from `Src/module.c:345`. — C decl `finish_(UNUSED(Module m))`.
368///
369/// C body: `finish_(UNUSED(Module m)) { return 0; }` —
370/// the no-op finish hook for the module subsystem itself.
371#[allow(unused_variables)]
372pub fn finish_(m: *const module) -> i32 {
373 // c:345
374 0 // c:345
375}
376
377// This registers a builtin module. // c:359
378/// Register module (from module.c register_module)
379/// Register a module by name.
380/// Port of `register_module()` from `Src/module.c:359`. — C decl `register_module(const char *n, Module_void_func setup, Module_features_func features, Module_enables_func enables, Module_void_func boot, Module_void_func cleanup, Module_void_func finish)`.
381/// wraps
382/// a slot in the global `modulestab` and seeds its lifecycle
383/// callbacks.
384/// WARNING: param names don't match C — Rust=(table, name) vs C=(n, setup, features, enables, boot, cleanup, finish)
385pub fn register_module(table: &mut modulestab, name: &str) -> bool {
386 // c:359
387 if table.modules.contains_key(name) {
388 return false;
389 }
390 // c:378 — `zaddlinknode(linkedmodules, m)`: the static entry-point
391 // record goes on `linkedmodules`, which is what `module_linked`
392 // (c:385) searches from `load_module` (c:2224, c:2285).
393 if !table.linkedmodules.iter().any(|n| n == name) {
394 table.linkedmodules.push(name.to_string());
395 }
396 // WARNING: RUST-ONLY — C's `register_module` does NOT add a
397 // `modulestab` node (only `load_module`/`autofeatures`/`add_dep`
398 // do). The node insert is kept here because zshrs callers treat
399 // this free fn as "make the module exist and be loadable"; the
400 // boot path (`register_builtin_modules`) deliberately does NOT use
401 // it, and populates `linkedmodules` directly so that the
402 // `modulestab` node set matches C's boot set exactly.
403 table.modules.insert(name.to_string(), module::new(name));
404 true
405}
406
407/// Port of `addbuiltin()` from `Src/module.c:409`. — C decl `addbuiltin(Builtin b)`.
408/// C body:
409/// look up `b->node.nam` in builtintab; if BINF_ADDED clash → return 1;
410/// otherwise replace any pre-existing entry and add `b`. Returns 0 on
411/// add, 1 on clash.
412///
413/// The Rust canonical builtintab is `OnceLock<HashMap<String,
414/// &'static builtin>>` — immutable after first access. Runtime `addbuiltin`
415/// calls check the immutable table for the clash gate; the BINF_ADDED
416/// flag-set on the input record is what callers observe (matching the
417/// C in-place mutation that `addbuiltins` then propagates).
418pub fn addbuiltin(b: &mut builtin) -> i32 {
419 // c:524
420 let tab = createbuiltintable();
421 if let Some(existing) = tab.get(&b.node.nam) {
422 // c:526 getnode2
423 if (existing.node.flags & BINF_ADDED as i32) != 0 {
424 return 1;
425 } // c:527 clash
426 }
427 b.node.flags |= BINF_ADDED as i32; // c:531 b->node.flags |= BINF_ADDED
428 0
429}
430
431/// Port of `deletebuiltin()` from `Src/module.c:449`. — C decl `deletebuiltin(const char *nam)`.
432///
433/// C body c:449-458:
434/// ```c
435/// int
436/// deletebuiltin(const char *nam)
437/// {
438/// Builtin bn;
439/// bn = (Builtin) builtintab->removenode(builtintab, nam);
440/// if (!bn)
441/// return -1;
442/// builtintab->freenode(&bn->node);
443/// return 0;
444/// }
445/// ```
446///
447/// Returns 0 on success (entry was found + removed), -1 on miss.
448///
449/// zshrs's `createbuiltintable()` returns an immutable HashMap —
450/// the canonical table is static-linked, so this fn can't actually
451/// `removenode`. The faithful structural equivalent: probe the
452/// canonical table to honour the present/absent contract that
453/// callers (setbuiltins, del_autobin) rely on for their
454/// `already-deleted` / `no such builtin` diagnostics. Actual
455/// runtime `enabled` state lives on the modulestab's
456/// `added_builtins` ledger; that's where the caller flips the
457/// observable BINF_ADDED bit.
458pub fn deletebuiltin(nam: &str) -> i32 {
459 // c:449
460 // c:453 — `bn = builtintab->removenode(builtintab, nam);`
461 let tab = createbuiltintable();
462 match tab.get(nam) {
463 None => -1, // c:454-455 — `if (!bn) return -1;`
464 Some(_) => 0, // c:457 — freenode is owned by createbuiltintable, no-op.
465 }
466}
467
468/// Port of `addbuiltins()` from `Src/module.c:544`. — C decl `addbuiltins(char const *nam, Builtin binl, int size)`.
469/// Walks the slice; for each entry not already
470/// flagged BINF_ADDED, calls `addbuiltin`. Returns 0 if all succeeded,
471/// 1 if any clashed. zwarnnam emitted on each clash matches C.
472pub fn addbuiltins(nam: &str, binl: &mut [builtin]) -> i32 {
473 // c:544
474 let mut ret = 0; // c:548
475 for b in binl.iter_mut() {
476 // c:550 for(n = 0; n < size; n++)
477 if (b.node.flags & BINF_ADDED as i32) != 0 {
478 continue;
479 } // c:553
480 if addbuiltin(b) != 0 {
481 // c:555
482 zwarnnam(
483 nam, // c:556 zwarnnam(nam, "name clash...")
484 &format!("name clash when adding builtin `{}'", b.node.nam),
485 );
486 ret = 1;
487 }
488 }
489 ret // c:563
490}
491
492// Moved here from `src/ported/modules/datetime.rs`: this is a
493// `Src/module.c` function, and every module's `setfeatureenables`
494// (c:3450-3470) calls it, not just zsh/datetime's.
495/// Port of `setbuiltins()` from `Src/module.c:501`. — C decl `setbuiltins(char const *nam, Builtin binl, int size, int *e)`.
496///
497/// C body:
498/// ```c
499/// for(n = 0; n < size; n++) {
500/// Builtin b = &binl[n];
501/// if (e && *e++) {
502/// if (b->node.flags & BINF_ADDED) continue;
503/// if (addbuiltin(b)) { zwarnnam(nam, "name clash when adding builtin `%s'", …); ret = 1; }
504/// else b->node.flags |= BINF_ADDED;
505/// } else {
506/// if (!(b->node.flags & BINF_ADDED)) continue;
507/// if (deletebuiltin(b->node.nam)) { zwarnnam(nam, "builtin `%s' already deleted", …); ret = 1; }
508/// else b->node.flags &= ~BINF_ADDED;
509/// }
510/// }
511/// ```
512///
513/// !!! RUST-ONLY DEVIATION !!! C's `addbuiltin`/`deletebuiltin` insert into
514/// and remove from the live `builtintab`. zshrs's `builtintab`
515/// (`createbuiltintable()`) is an immutable statically-linked table that
516/// already holds every module builtin, so the only thing this can flip is
517/// the "is it visible right now" bit that `module_builtin_available`
518/// (`src/extensions/ext_builtins.rs`) reads — hence the
519/// `DISABLED_MODULE_BUILTINS` registry instead of a table mutation. The
520/// BINF_ADDED bookkeeping on the entry itself is unchanged from C.
521/// WARNING: param names don't match C — Rust=(nam, binl, e) vs C=(nam, binl, size, e)
522pub(crate) fn setbuiltins(nam: &str, binl: &mut [crate::ported::zsh_h::builtin], e: Option<&[i32]>) -> i32 {
523 // c:501
524 use crate::ported::zsh_h::BINF_ADDED;
525 // c:503 — neither branch below can fail (see the deviation note above),
526 // so C's `ret` stays 0 here.
527 let ret = 0;
528 for (n, b) in binl.iter_mut().enumerate() {
529 // c:505
530 let on = e
531 .map(|a| a.get(n).copied().unwrap_or(0) != 0)
532 .unwrap_or(false); // c:507
533 if on {
534 if (b.node.flags & BINF_ADDED as i32) != 0 {
535 continue; // c:508-509
536 }
537 DISABLED_MODULE_BUILTINS
538 .lock()
539 .unwrap()
540 .remove(&b.node.nam); // c:511 addbuiltin(b)
541 b.node.flags |= BINF_ADDED as i32; // c:516
542 } else {
543 if (b.node.flags & BINF_ADDED as i32) == 0 {
544 continue; // c:518-519
545 }
546 DISABLED_MODULE_BUILTINS
547 .lock()
548 .unwrap()
549 .insert(b.node.nam.clone()); // c:521 deletebuiltin(b->node.nam)
550 b.node.flags &= !(BINF_ADDED as i32); // c:526
551 }
552 }
553 let _ = nam;
554 ret // c:530
555}
556
557/// Port of `gethookdef()` from `Src/module.c:849`. — C decl `gethookdef(const char *n)`.
558///
559/// C body (c:849-861):
560/// ```c
561/// Hookdef gethookdef(const char *n) {
562/// Hookdef p;
563/// for (p = hooktab; p; p = p->next)
564/// if (!strcmp(n, p->name))
565/// return p;
566/// return NULL;
567/// }
568/// ```
569pub fn gethookdef(n: &str) -> *mut hookdef {
570 // c:849
571 let mut p = hooktab.load(std::sync::atomic::Ordering::SeqCst); // c:852 p = hooktab
572 while !p.is_null() {
573 // c:853
574 unsafe {
575 if (*p).name == n {
576 // c:854 !strcmp
577 return p; // c:855
578 }
579 p = (*p).next; // c:853 p = p->next
580 }
581 }
582 std::ptr::null_mut() // c:856
583}
584
585/// Port of `addhookdef()` from `Src/module.c:864`. — C decl `addhookdef(Hookdef h)`.
586///
587/// C body (c:864-874):
588/// ```c
589/// int addhookdef(Hookdef h) {
590/// if (gethookdef(h->name)) return 1;
591/// h->next = hooktab;
592/// hooktab = h;
593/// h->funcs = znewlinklist();
594/// return 0;
595/// }
596/// ```
597pub fn addhookdef(h: *mut hookdef) -> i32 {
598 // c:864
599 unsafe {
600 if !gethookdef(&(*h).name).is_null() {
601 // c:866
602 return 1; // c:867
603 }
604 (*h).next = hooktab.load(std::sync::atomic::Ordering::SeqCst); // c:869 h->next = hooktab
605 hooktab.store(h, std::sync::atomic::Ordering::SeqCst); // c:870 hooktab = h
606 if (*h).funcs.is_null() {
607 // c:871 h->funcs = znewlinklist()
608 (*h).funcs = Box::into_raw(Box::new(linklist {
609 first: None,
610 last: None,
611 flags: 0,
612 }));
613 }
614 }
615 0 // c:873
616}
617
618/// Port of `addhookdefs()` from `Src/module.c:883`. — C decl `addhookdefs(Module m, Hookdef h, int size)`.
619///
620/// C body (c:883-895):
621/// ```c
622/// int addhookdefs(Module m, Hookdef h, int size) {
623/// int ret = 0;
624/// while (size--) {
625/// if (addhookdef(h)) {
626/// zwarnnam(m ? m->node.nam : NULL, "name clash when adding hook `%s'", h->name);
627/// ret = 1;
628/// }
629/// h++;
630/// }
631/// return ret;
632/// }
633/// ```
634pub fn addhookdefs(
635 // c:883
636 m: *const module,
637 mut h: *mut hookdef,
638 mut size: i32,
639) -> i32 {
640 let mut ret: i32 = 0; // c:885
641 while size > 0 {
642 // c:887 size--
643 if addhookdef(h) != 0 {
644 // c:888
645 let nam: String = if m.is_null() {
646 // c:889 m ? m->node.nam : NULL
647 String::new()
648 } else {
649 unsafe { (*m).node.nam.clone() }
650 };
651 let hook_name = unsafe { (*h).name.clone() };
652 zwarnnam(
653 // c:889 zwarnnam
654 &nam,
655 &format!("name clash when adding hook `{}'", hook_name),
656 );
657 ret = 1; // c:891
658 }
659 unsafe {
660 h = h.add(1);
661 } // c:893 h++
662 size -= 1; // c:887
663 }
664 ret // c:894
665}
666
667/// Port of `deletehookdef()` from `Src/module.c:902`. — C decl `deletehookdef(Hookdef h)`.
668///
669/// C body (c:902-919):
670/// ```c
671/// int deletehookdef(Hookdef h) {
672/// Hookdef p, q;
673/// for (p = hooktab, q = NULL; p && p != h; q = p, p = p->next);
674/// if (!p) return 1;
675/// if (q) q->next = p->next; else hooktab = p->next;
676/// freelinklist(p->funcs, NULL);
677/// return 0;
678/// }
679/// ```
680pub fn deletehookdef(h: *mut hookdef) -> i32 {
681 // c:902
682 let mut p = hooktab.load(std::sync::atomic::Ordering::SeqCst); // c:906 p = hooktab
683 let mut q: *mut hookdef = std::ptr::null_mut(); // c:906 q = NULL
684 while !p.is_null() && p != h {
685 // c:907
686 q = p; // c:907 q = p
687 unsafe {
688 p = (*p).next;
689 } // c:907 p = p->next
690 }
691 if p.is_null() {
692 // c:909
693 return 1; // c:910
694 }
695 unsafe {
696 if !q.is_null() {
697 // c:912
698 (*q).next = (*p).next; // c:913 q->next = p->next
699 } else {
700 hooktab.store((*p).next, std::sync::atomic::Ordering::SeqCst); // c:915 hooktab = p->next
701 }
702 if !(*p).funcs.is_null() {
703 // c:916 freelinklist(p->funcs, NULL)
704 drop(Box::from_raw((*p).funcs));
705 (*p).funcs = std::ptr::null_mut();
706 }
707 }
708 0 // c:917
709}
710
711/// Port of `deletehookdefs()` from `Src/module.c:923`. — C decl `deletehookdefs(UNUSED(Module m), Hookdef h, int size)`.
712/// `m` is unused per `UNUSED(Module m)` in C.
713pub fn deletehookdefs(
714 // c:923
715 _m: *const module,
716 mut h: *mut hookdef,
717 mut size: i32,
718) -> i32 {
719 let mut ret: i32 = 0; // c:925
720 while size > 0 {
721 // c:927 size--
722 if deletehookdef(h) != 0 {
723 // c:928
724 ret = 1; // c:929
725 }
726 unsafe {
727 h = h.add(1);
728 } // c:930 h++
729 size -= 1; // c:927
730 }
731 ret // c:931
732}
733
734/// Port of `addhookdeffunc()` from `Src/module.c:939`. — C decl `addhookdeffunc(Hookdef h, Hookfn f)`.
735///
736/// C body (c:939-944):
737/// ```c
738/// int addhookdeffunc(Hookdef h, Hookfn f) {
739/// zaddlinknode(h->funcs, (void *) f);
740/// return 0;
741/// }
742/// ```
743pub fn addhookdeffunc(
744 // c:939
745 h: *mut hookdef,
746 f: Hookfn,
747) -> i32 {
748 unsafe {
749 if (*h).funcs.is_null() {
750 (*h).funcs = Box::into_raw(Box::new(linklist {
751 first: None,
752 last: None,
753 flags: 0,
754 }));
755 }
756 let funcs = &mut *(*h).funcs;
757 // c:942 — zaddlinknode(h->funcs, f) appends to end of LinkList.
758 // Walk to tail of owned chain (linklist.last cannot be a Box
759 // pointer because that would duplicate ownership of the tail
760 // node — the C `last` field is a raw pointer; the Rust
761 // representation keeps it as None and resolves the tail by
762 // walking forward from .first).
763 let new_node = Box::new(linknode {
764 next: None,
765 prev: None,
766 dat: f as usize,
767 });
768 if funcs.first.is_none() {
769 funcs.first = Some(new_node);
770 } else {
771 let mut tail = funcs.first.as_mut().unwrap();
772 while tail.next.is_some() {
773 tail = tail.next.as_mut().unwrap();
774 }
775 tail.next = Some(new_node);
776 }
777 }
778 0 // c:943
779}
780
781/// Port of `addhookfunc()` from `Src/module.c:948`. — C decl `addhookfunc(char *n, Hookfn f)`.
782///
783/// C body (c:948-955):
784/// ```c
785/// int addhookfunc(char *n, Hookfn f) {
786/// Hookdef h = gethookdef(n);
787/// if (h) return addhookdeffunc(h, f);
788/// return 1;
789/// }
790/// ```
791pub fn addhookfunc(
792 // c:948
793 n: &str,
794 f: Hookfn,
795) -> i32 {
796 let h = gethookdef(n); // c:950 h = gethookdef(n)
797 if !h.is_null() {
798 // c:951
799 return addhookdeffunc(h, f); // c:952
800 }
801 1 // c:953
802}
803
804/// Port of `deletehookdeffunc()` from `Src/module.c:961`. — C decl `deletehookdeffunc(Hookdef h, Hookfn f)`.
805///
806/// C body (c:961-973):
807/// ```c
808/// int deletehookdeffunc(Hookdef h, Hookfn f) {
809/// LinkNode p;
810/// for (p = firstnode(h->funcs); p; incnode(p))
811/// if (f == (Hookfn) getdata(p)) {
812/// remnode(h->funcs, p);
813/// return 0;
814/// }
815/// return 1;
816/// }
817/// ```
818pub fn deletehookdeffunc(
819 // c:961
820 h: *mut hookdef,
821 f: Hookfn,
822) -> i32 {
823 unsafe {
824 if (*h).funcs.is_null() {
825 return 1;
826 }
827 let funcs = &mut *(*h).funcs;
828 let f_val = f as usize;
829 // Walk owning chain looking for the matching dat. Splice on hit.
830 let mut prev: &mut Option<Box<linknode>> = &mut funcs.first;
831 loop {
832 match prev {
833 None => return 1, // c:971
834 Some(node) if node.dat == f_val => {
835 // c:966 f == getdata(p)
836 let next = node.next.take(); // c:967 remnode
837 *prev = next;
838 return 0; // c:968
839 }
840 Some(_) => {
841 // c:965 incnode(p) — advance.
842 prev = &mut prev.as_mut().unwrap().next;
843 }
844 }
845 }
846 }
847}
848
849/// Port of `deletehookfunc()` from `Src/module.c:977`. — C decl `deletehookfunc(const char *n, Hookfn f)`.
850///
851/// C body (c:977-984):
852/// ```c
853/// int deletehookfunc(const char *n, Hookfn f) {
854/// Hookdef h = gethookdef(n);
855/// if (h) return deletehookdeffunc(h, f);
856/// return 1;
857/// }
858/// ```
859pub fn deletehookfunc(
860 // c:977
861 n: &str,
862 f: Hookfn,
863) -> i32 {
864 let h = gethookdef(n); // c:979 h = gethookdef(n)
865 if !h.is_null() {
866 // c:980
867 return deletehookdeffunc(h, f); // c:981
868 }
869 1 // c:982
870}
871
872/// Port of `runhookdef()` from `Src/module.c:990`. — C decl `runhookdef(Hookdef h, void *d)`.
873///
874/// C body (c:990-1010):
875/// ```c
876/// int runhookdef(Hookdef h, void *d) {
877/// if (empty(h->funcs)) {
878/// if (h->def) return h->def(h, d);
879/// return 0;
880/// } else if (h->flags & HOOKF_ALL) {
881/// LinkNode p; int r;
882/// for (p = firstnode(h->funcs); p; incnode(p))
883/// if ((r = ((Hookfn) getdata(p))(h, d))) return r;
884/// if (h->def) return h->def(h, d);
885/// return 0;
886/// } else
887/// return ((Hookfn) getdata(lastnode(h->funcs)))(h, d);
888/// }
889/// ```
890pub fn runhookdef(
891 // c:990
892 h: *mut hookdef,
893 d: *mut std::ffi::c_void,
894) -> i32 {
895 unsafe {
896 let funcs_ptr = (*h).funcs;
897 let funcs_empty = funcs_ptr.is_null() || (*funcs_ptr).first.is_none(); // c:992 empty()
898 if funcs_empty {
899 // c:992
900 if let Some(def) = (*h).def {
901 // c:993 if (h->def)
902 return def(h, d); // c:994 return h->def(h,d)
903 }
904 return 0; // c:995
905 }
906 if (*h).flags & HOOKF_ALL != 0 {
907 // c:996 h->flags & HOOKF_ALL
908 let mut node = (*funcs_ptr).first.as_ref(); // c:999 firstnode
909 while let Some(n) = node {
910 // c:999 ; p ;
911 let fn_ptr: Hookfn = std::mem::transmute(n.dat); // c:1000 (Hookfn) getdata(p)
912 let r = fn_ptr(h, d); // c:1000 (...)(h, d)
913 if r != 0 {
914 // c:1000 if ((r = ...))
915 return r; // c:1001
916 }
917 node = n.next.as_ref(); // c:999 incnode
918 }
919 if let Some(def) = (*h).def {
920 // c:1002 if (h->def)
921 return def(h, d); // c:1003 return h->def(h, d)
922 }
923 return 0; // c:1004
924 }
925 // c:1006 — last fn only.
926 let mut tail = (*funcs_ptr).first.as_ref().expect("non-empty");
927 while let Some(next) = tail.next.as_ref() {
928 tail = next;
929 }
930 let fn_ptr: Hookfn = std::mem::transmute(tail.dat);
931 fn_ptr(h, d) // c:1006
932 }
933}
934
935/// Port of `checkaddparam()` from `Src/module.c:1026`. — C decl `checkaddparam(const char *nam, int opt_i)`.
936///
937/// C body:
938/// ```c
939/// checkaddparam(const char *nam, int opt_i)
940/// {
941/// Param pm;
942/// if (!(pm = (Param) gethashnode2(paramtab, nam)))
943/// return 0;
944/// if (pm->level || !(pm->node.flags & PM_AUTOLOAD)) {
945/// if (!opt_i || pm->level) {
946/// zwarn("Can't add module parameter `%s': %s",
947/// nam, pm->level ? "local parameter exists" :
948/// "parameter already exists");
949/// return 1;
950/// }
951/// return 2;
952/// }
953/// unsetparam_pm(pm, 0, 1);
954/// return 0;
955/// }
956/// ```
957///
958/// Returns: 0 = OK to add, 1 = error printed, 2 = blocked but `-i`
959/// suppressed warning. `pm->level != 0` means a local param shadows
960/// the name (always errors). `PM_AUTOLOAD` set means the existing
961/// param is an autoload stub the C source unsets to make room.
962///
963/// Checks for an existing param of the same name; if absent → 0
964/// (free to add). If present and not autoloadable → 1 (warn) or
965/// 2 (skip warning under `-i`). If present + autoload-marked →
966/// unset the existing entry and return 0.
967pub fn checkaddparam(nam: &str, opt_i: i32) -> i32 {
968 // c:1026
969 // c:1030 — if (!(pm = gethashnode2(paramtab, nam))) return 0;
970 let pm_clone = {
971 let tab = paramtab().read().expect("paramtab poisoned");
972 tab.get(nam).cloned()
973 };
974 let mut pm = match pm_clone {
975 Some(p) => p,
976 None => return 0,
977 };
978 // c:1033 — if (pm->level || !(pm->node.flags & PM_AUTOLOAD)) {
979 if pm.level != 0 || (pm.node.flags as u32 & PM_AUTOLOAD) == 0 {
980 // c:1042-1048 — if (!opt_i || pm->level) zwarn(...); return 1;
981 if opt_i == 0 || pm.level != 0 {
982 zwarn(&format!(
983 "Can't add module parameter `{}': {}",
984 nam,
985 if pm.level != 0 {
986 "local parameter exists"
987 } else {
988 "parameter already exists"
989 }
990 ));
991 return 1;
992 }
993 // c:1049 — return 2;
994 return 2;
995 }
996 // c:1052 — unsetparam_pm(pm, 0, 1);
997 unsetparam_pm(&mut pm, 0, 1);
998 // !!! RUST-ONLY STEP — NO DIRECT C COUNTERPART !!!
999 // C's `unsetparam_pm` ends in the `paramtab->removenode` postlude
1000 // (c:Src/params.c:3853-3935) that drops the stub node; the Rust one
1001 // only marks the by-value copy it was handed (see the same step in
1002 // `deleteparamdef`). Without the removal the PM_AUTOLOAD stub stayed
1003 // in paramtab, addparamdef's createparam found it and never
1004 // installed the module's parameter, and loadparamnode
1005 // (c:Src/params.c:553-563) reported "autoloading module zsh/example
1006 // failed to define parameter: exint".
1007 if let Ok(mut tab) = paramtab().write() {
1008 tab.remove(nam); // c:Src/params.c:3900 paramtab->removenode
1009 }
1010 // c:1053 — return 0;
1011 0
1012}
1013
1014/// Port of `addparamdef()` from `Src/module.c:1061`. — C decl `addparamdef(Paramdef d)`.
1015/// Registers a module-supplied parameter definition into the canonical
1016/// `paramtab`, wiring the GSU vtable per `PM_TYPE`. Returns 0 on
1017/// success, 1 on error.
1018///
1019/// ```c
1020/// int
1021/// addparamdef(Paramdef d)
1022/// {
1023/// Param pm;
1024/// if (checkaddparam(d->name, 0)) return 1;
1025/// if (d->getnfn) {
1026/// if (!(pm = createspecialhash(d->name, d->getnfn,
1027/// d->scantfn, d->flags)))
1028/// return 1;
1029/// }
1030/// else if (!(pm = createparam(d->name, d->flags)) &&
1031/// !(pm = (Param) paramtab->getnode(paramtab, d->name)))
1032/// return 1;
1033/// d->pm = pm;
1034/// pm->level = 0;
1035/// if (d->var) pm->u.data = d->var;
1036/// if (d->var || d->gsu) {
1037/// switch (PM_TYPE(pm->node.flags)) {
1038/// case PM_SCALAR:
1039/// if (pm->node.flags & PM_TIED)
1040/// pm->ename = ztrdup(casemodify(pm->node.nam, CASMOD_LOWER));
1041/// /* fall-through */
1042/// case PM_NAMEREF:
1043/// pm->gsu.s = d->gsu ? (GsuScalar)d->gsu : &varscalar_gsu;
1044/// break;
1045/// case PM_INTEGER:
1046/// pm->gsu.i = d->gsu ? (GsuInteger)d->gsu : &varinteger_gsu;
1047/// break;
1048/// case PM_FFLOAT: case PM_EFLOAT:
1049/// pm->gsu.f = d->gsu;
1050/// break;
1051/// case PM_ARRAY:
1052/// if (pm->node.flags & PM_TIED)
1053/// pm->ename = ztrdup(casemodify(pm->node.nam, CASMOD_UPPER));
1054/// pm->gsu.a = d->gsu ? (GsuArray)d->gsu : &vararray_gsu;
1055/// break;
1056/// case PM_HASHED:
1057/// if (d->gsu) pm->gsu.h = (GsuHash)d->gsu;
1058/// break;
1059/// default:
1060/// unsetparam_pm(pm, 0, 1);
1061/// return 1;
1062/// }
1063/// }
1064/// return 0;
1065/// }
1066/// ```
1067pub fn addparamdef(d: &mut paramdef) -> i32 {
1068 // c:1061
1069
1070 // c:1065 — `if (checkaddparam(d->name, 0)) return 1;`
1071 if checkaddparam(&d.name, 0) != 0 {
1072 // c:1065
1073 return 1; // c:1066
1074 }
1075
1076 // c:1068-1075 — either createspecialhash (hash params with getnfn)
1077 // or createparam, falling back to gethashnode on collision.
1078 let pm_opt: Option<Param> = if d.getnfn.is_some() {
1079 // c:1068
1080 // c:1069-1071 — createspecialhash(d->name, d->getnfn, d->scantfn, d->flags)
1081 // The Rust createspecialhash takes (name, flags) only; the
1082 // getnfn/scantfn fields aren't yet wired through the typed
1083 // Rust API. Pass flags and let the param be created.
1084 createspecialhash(&d.name, d.flags) // c:1069
1085 } else {
1086 // c:1072
1087 match createparam(&d.name, d.flags) {
1088 // c:1073
1089 Some(p) => Some(p),
1090 None => {
1091 // c:1074 — fall back to paramtab->getnode(paramtab, d->name)
1092 let tab = paramtab().read().ok();
1093 tab.and_then(|t| {
1094 t.get(&d.name).map(|p| {
1095 // Clone the existing param so we can mutate the
1096 // returned handle without holding the read lock.
1097 let mut clone = p.clone();
1098 clone.level = 0;
1099 Box::new(*clone)
1100 })
1101 })
1102 }
1103 }
1104 };
1105 let mut pm = match pm_opt {
1106 // c:1074-1075
1107 Some(p) => p,
1108 None => return 1,
1109 };
1110
1111 // c:1077-1078 — `d->pm = pm; pm->level = 0;`
1112 pm.level = 0; // c:1078
1113
1114 // c:1079-1080 — `if (d->var) pm->u.data = d->var;`
1115 if d.var != 0 { // c:1079
1116 // pm.u.data is a raw `void *` slot — not yet exposed on the
1117 // Rust param mirror. Carry the assignment as a comment.
1118 // pm.u.data = d->var as *mut _; // c:1080
1119 }
1120
1121 if d.var != 0 || d.gsu != 0 {
1122 // c:1081
1123 let t = PM_TYPE(pm.node.flags as u32); // c:1086
1124 let pmflags = pm.node.flags as u32;
1125 if t == PM_SCALAR || t == PM_NAMEREF {
1126 // c:1087/1091
1127 if t == PM_SCALAR && (pmflags & PM_TIED) != 0 {
1128 // c:1088
1129 let lower = casemodify(&pm.node.nam, CASMOD_LOWER);
1130 pm.ename = Some(ztrdup(&lower)); // c:1089
1131 }
1132 // c:1092 — `pm->gsu.s = d->gsu ? (GsuScalar)d->gsu :
1133 // &varscalar_gsu;`. A module's `gsu` is the address of its static
1134 // `gsu_scalar` (`&sh_name_gsu`, Src/Modules/ksh93.c:100); with
1135 // none the param keeps the plain variable accessors. And c:1080
1136 // `pm->u.data = d->var`: for a PM_NAMEREF row (ksh93.c:121-130)
1137 // `var` is the referent name GETREFNAME reads back, here the
1138 // address of a `&'static str`. `pm` is a copy, so both go onto the
1139 // live node too.
1140 let gsu = (d.gsu != 0).then(|| {
1141 let g = unsafe { &*(d.gsu as *const crate::ported::zsh_h::gsu_scalar) };
1142 (g.getfn, g.setfn, g.unsetfn)
1143 });
1144 let refname = (t == PM_NAMEREF && d.var != 0)
1145 .then(|| unsafe { *(d.var as *const &'static str) }.to_string());
1146 let mk = |g: (
1147 fn(&crate::ported::zsh_h::param) -> String,
1148 fn(&mut crate::ported::zsh_h::param, String),
1149 fn(&mut crate::ported::zsh_h::param, i32),
1150 )| {
1151 Box::new(crate::ported::zsh_h::gsu_scalar { getfn: g.0, setfn: g.1, unsetfn: g.2 })
1152 };
1153 if let Some(g) = gsu {
1154 pm.gsu_s = Some(mk(g)); // c:1092
1155 }
1156 if let Some(r) = &refname {
1157 pm.u_str = Some(r.clone()); // c:1080
1158 }
1159 if let Ok(mut tab) = paramtab().write() {
1160 if let Some(live) = tab.get_mut(&d.name) {
1161 if let Some(g) = gsu {
1162 live.gsu_s = Some(mk(g));
1163 }
1164 if let Some(r) = refname {
1165 live.u_str = Some(r);
1166 }
1167 }
1168 }
1169 } else if t == PM_INTEGER {
1170 // c:1095-1096 — `pm->gsu.i = d->gsu ? (GsuInteger)d->gsu :
1171 // &varinteger_gsu;`. A module's `gsu` is the address of its
1172 // static `gsu_integer` (`&srandom_gsu`, Src/Modules/random.c:219).
1173 // `pm` is a copy, so the vtable goes onto the live node too.
1174 if d.gsu != 0 {
1175 let g = unsafe { &*(d.gsu as *const crate::ported::zsh_h::gsu_integer) };
1176 let mk = || {
1177 Box::new(crate::ported::zsh_h::gsu_integer {
1178 getfn: g.getfn,
1179 setfn: g.setfn,
1180 unsetfn: g.unsetfn,
1181 })
1182 };
1183 pm.gsu_i = Some(mk());
1184 if let Ok(mut tab) = paramtab().write() {
1185 if let Some(live) = tab.get_mut(&d.name) {
1186 live.gsu_i = Some(mk());
1187 }
1188 }
1189 }
1190 } else if t == PM_FFLOAT || t == PM_EFLOAT {
1191 // c:1099-1100
1192 let _ = d.gsu; // c:1101
1193 } else if t == PM_ARRAY {
1194 // c:1104
1195 if (pmflags & PM_TIED) != 0 {
1196 // c:1105
1197 let upper = casemodify(&pm.node.nam, CASMOD_UPPER);
1198 pm.ename = Some(ztrdup(&upper)); // c:1106
1199 }
1200 let _ = d.gsu; // c:1107
1201 } else if t == PM_HASHED {
1202 // c:1110
1203 let _ = d.gsu; // c:1112-1113
1204 } else {
1205 // c:1116
1206 unsetparam_pm(&mut pm, 0, 1); // c:1117
1207 return 1; // c:1118
1208 }
1209 }
1210
1211 d.pm = Some(pm); // c:1077 d->pm = pm
1212 0 // c:1122
1213}
1214
1215/// Port of `deleteparamdef()` from `Src/module.c:1124`. — C decl `deleteparamdef(Paramdef d)`.
1216/// Removes a previously-registered module parameter, unwinding any
1217/// hidden-param shadow chain so the matching `d->pm` instance is the
1218/// one actually unset.
1219///
1220/// ```c
1221/// int
1222/// deleteparamdef(Paramdef d)
1223/// {
1224/// Param pm = (Param) paramtab->getnode(paramtab, d->name);
1225/// if (!pm) return 1;
1226/// if (pm != d->pm) {
1227/// Param prevpm, searchpm;
1228/// for (prevpm = pm, searchpm = pm->old;
1229/// searchpm;
1230/// prevpm = searchpm, searchpm = searchpm->old)
1231/// if (searchpm == d->pm) break;
1232/// if (!searchpm) return 1;
1233/// paramtab->removenode(paramtab, pm->node.nam);
1234/// prevpm->old = searchpm->old;
1235/// searchpm->old = pm;
1236/// paramtab->addnode(paramtab, searchpm->node.nam, searchpm);
1237/// pm = searchpm;
1238/// }
1239/// pm->node.flags = (pm->node.flags & ~PM_READONLY) | PM_REMOVABLE;
1240/// unsetparam_pm(pm, 0, 1);
1241/// d->pm = NULL;
1242/// return 0;
1243/// }
1244/// ```
1245pub fn deleteparamdef(d: &mut paramdef) -> i32 {
1246 // c:1128
1247
1248 // c:1131 — `Param pm = (Param) paramtab->getnode(paramtab, d->name);`
1249 let mut pm: Param = {
1250 let tab = paramtab().read();
1251 match tab {
1252 Ok(t) => match t.get(&d.name) {
1253 Some(p) => p.clone(),
1254 None => return 1, // c:1133-1134
1255 },
1256 Err(_) => return 1,
1257 }
1258 };
1259
1260 // c:1135-1156 — shadow-chain unwind: if the live pm isn't d->pm,
1261 // walk pm->old searching for d->pm; if found, splice it out,
1262 // re-add the matching node under its name, and operate on it.
1263 // The Rust param mirror's `old` chain isn't yet wired through
1264 // paramtab; the typed paramtab dispatches the latest binding
1265 // directly. Mirror the C structure so callers see the same
1266 // semantics when the shadow chain lands.
1267 if let Some(expected) = d.pm.as_ref() {
1268 // c:1130 `if (pm != d->pm)`
1269 // !!! RUST-ONLY IDENTITY TEST !!! C compares POINTERS: `pm` and
1270 // `d->pm` are the same heap object whenever the module's own
1271 // parameter is the live binding. zshrs's `paramtab` stores `Param`
1272 // BY VALUE and hands out clones, so `std::ptr::eq` was false for
1273 // every call — `deleteparamdef` always took the c:1143 `return 1`
1274 // path, and `zmodload -u zsh/datetime` reported "parameter
1275 // `EPOCHSECONDS' already deleted" for a parameter it had just
1276 // added. Two things stand in for the pointer compare:
1277 // * the NAME, which is what identity means for a by-value table;
1278 // * `pm.level`, because `addparamdef` (c:1078) pins the module's
1279 // own binding at `pm->level = 0` while `local NAME=…` records
1280 // the enclosing `locallevel` (params.rs:2762-2766, c:1136).
1281 // A non-zero level therefore means the live node is a LOCAL that
1282 // has hidden the module's parameter — exactly the case c:1131-1133
1283 // describes ("See if the parameter has been hidden").
1284 if pm.node.nam != expected.node.nam || pm.level != 0 {
1285 // c:1136-1140 — walk pm->old looking for d->pm. The whole chain
1286 // carries one name, so the level-0 link is the module's.
1287 // for (prevpm = pm, searchpm = pm->old;
1288 // searchpm;
1289 // prevpm = searchpm, searchpm = searchpm->old)
1290 // if (searchpm == d->pm) break;
1291 // Flatten the by-value chain so the splice below is a Vec edit
1292 // rather than a doubly-borrowed linked-list walk.
1293 let mut chain: Vec<Param> = Vec::new();
1294 let mut link = Some(pm);
1295 while let Some(mut n) = link {
1296 link = n.old.take();
1297 chain.push(n);
1298 }
1299 let searchpm = chain
1300 .iter()
1301 .skip(1)
1302 .position(|p| p.level == 0 && p.node.nam == expected.node.nam)
1303 .map(|i| i + 1);
1304 let Some(hit) = searchpm else {
1305 // c:1142-1143 — `if (!searchpm) return 1;`
1306 return 1;
1307 };
1308 // c:1145-1150:
1309 // paramtab->removenode(paramtab, pm->node.nam);
1310 // prevpm->old = searchpm->old;
1311 // searchpm->old = pm;
1312 // paramtab->addnode(paramtab, searchpm->node.nam, searchpm);
1313 // pm = searchpm;
1314 // C hoists the module's binding to the front so the unset below
1315 // hits it, with the hidden chain hanging off its ->old; then
1316 // unsetparam_pm's removenode postlude (c:Src/params.c:3900)
1317 // restores that chain. The net effect on the table is that the
1318 // module's link, and only it, leaves the chain — so splice it
1319 // out here and rebuild.
1320 let mut modpm = chain.remove(hit);
1321 let mut rebuilt: Option<Param> = None;
1322 for mut link in chain.into_iter().rev() {
1323 link.old = rebuilt.take();
1324 rebuilt = Some(link);
1325 }
1326 // c:1152-1153 on the module's own binding, not on the local.
1327 modpm.node.flags =
1328 (modpm.node.flags & !(PM_READONLY as i32)) | (PM_REMOVABLE as i32);
1329 unsetparam_pm(&mut modpm, 0, 1);
1330 if let Ok(mut tab) = paramtab().write() {
1331 match rebuilt {
1332 // c:1150 — the hidden chain is what stays visible.
1333 Some(front) => {
1334 tab.insert(d.name.clone(), front);
1335 }
1336 None => {
1337 tab.remove(&d.name);
1338 }
1339 }
1340 }
1341 d.pm = None; // c:1154 d->pm = NULL
1342 return 0; // c:1155
1343 }
1344 }
1345
1346 // c:1157 — `pm->node.flags = (pm->node.flags & ~PM_READONLY) | PM_REMOVABLE;`
1347 pm.node.flags = (pm.node.flags & !(PM_READONLY as i32)) | (PM_REMOVABLE as i32); // c:1157
1348 unsetparam_pm(&mut pm, 0, 1); // c:1158
1349 // !!! RUST-ONLY STEP — NO DIRECT C COUNTERPART !!!
1350 // C's `unsetparam_pm` ends in the `paramtab->removenode` postlude
1351 // (c:Src/params.c:3853-3935) that physically drops the node. The Rust
1352 // `unsetparam_pm` stops short of it ("Tied alt-name removal +
1353 // paramtab restore-from-old not yet possible without HashTable
1354 // backend", params.rs:9807-9810) and only stamps PM_UNSET on the
1355 // by-value COPY it was handed, so the paramtab entry survived intact:
1356 // after `zmodload -u zsh/datetime` the name was still there and still
1357 // PM_READONLY, so the next `zmodload zsh/datetime` failed with
1358 // "parameter already exists" and a plain `EPOCHSECONDS=(...)` failed
1359 // with "read-only variable". Do the removenode half here.
1360 //
1361 // KNOWN DIVERGENCE when a local shadows the name: C's c:1141-1153 walk
1362 // splices `d->pm` out of the `pm->old` chain first, so it unsets the
1363 // MODULE's binding and the local survives. zshrs has no `old` chain
1364 // (`params.rs` never assigns `Param::old`) and the local's saved outer
1365 // copy lives in the executor's own scope stack, so the node reached
1366 // here is the local and the module's binding is unreachable. Removing
1367 // it matches C on the three lasting observations — the module's claim
1368 // is dropped, `${+NAME}` is 0 after the scope pops, and a later plain
1369 // assignment works — and diverges only on the local's value for the
1370 // remainder of the enclosing scope (V04features.ztst "Successfully
1371 // unloaded a module despite a parameter being hidden" reads it back).
1372 if let Ok(mut tab) = paramtab().write() {
1373 tab.remove(&d.name); // c:Src/params.c:3900 paramtab->removenode
1374 }
1375 d.pm = None; // c:1159 d->pm = NULL
1376 0 // c:1160
1377}
1378
1379// Moved here from `src/ported/modules/datetime.rs`: this is a
1380// `Src/module.c` function, and every module's `setfeatureenables`
1381// (c:3450-3470) calls it, not just zsh/datetime's.
1382/// Port of `setparamdefs()` from `Src/module.c:1165`. — C decl `setparamdefs(char const *nam, Paramdef d, int size, int *e)`.
1383///
1384/// C body:
1385/// ```c
1386/// while (size--) {
1387/// if (e && *e++) {
1388/// if (d->pm) { d++; continue; }
1389/// if (addparamdef(d)) { zwarnnam(nam, "error when adding parameter `%s'", d->name); ret = 1; }
1390/// } else {
1391/// if (!d->pm) { d++; continue; }
1392/// if (deleteparamdef(d)) { zwarnnam(nam, "parameter `%s' already deleted", d->name); ret = 1; }
1393/// }
1394/// d++;
1395/// }
1396/// ```
1397/// WARNING: param names don't match C — Rust=(nam, d, e) vs C=(nam, d, size, e)
1398pub(crate) fn setparamdefs(nam: &str, d: &mut [crate::ported::zsh_h::paramdef], e: Option<&[i32]>) -> i32 {
1399 // c:1169
1400 let mut ret = 0; // c:1172
1401 for (n, def) in d.iter_mut().enumerate() {
1402 // c:1174 while (size--)
1403 let on = e
1404 .map(|a| a.get(n).copied().unwrap_or(0) != 0)
1405 .unwrap_or(false); // c:1175
1406 if on {
1407 if def.pm.is_some() {
1408 continue; // c:1176-1179
1409 }
1410 if addparamdef(def) != 0 {
1411 // c:1180
1412 zwarnnam(nam, &format!("error when adding parameter `{}'", def.name));
1413 // c:1181
1414 ret = 1; // c:1182
1415 }
1416 } else {
1417 if def.pm.is_none() {
1418 continue; // c:1185-1188
1419 }
1420 if deleteparamdef(def) != 0 {
1421 // c:1189
1422 zwarnnam(nam, &format!("parameter `{}' already deleted", def.name));
1423 // c:1190
1424 ret = 1; // c:1191
1425 }
1426 }
1427 }
1428 ret // c:1196
1429}
1430
1431/// Port of `add_autoparam()` from `Src/module.c:1198`. — C decl `add_autoparam(const char *module, const char *pnam, int flags)`.
1432///
1433/// C body c:1197-1228:
1434/// ```c
1435/// static int
1436/// add_autoparam(const char *module, const char *pnam, int flags)
1437/// {
1438/// Param pm;
1439/// int ret;
1440/// int ne = noerrs;
1441///
1442/// queue_signals();
1443/// if ((ret = checkaddparam(pnam, (flags & FEAT_IGNORE)))) {
1444/// unqueue_signals();
1445/// return ret == 2 ? 0 : -1;
1446/// }
1447/// noerrs = 2;
1448/// if ((pm = setsparam(dupstring(pnam), ztrdup(module)))) {
1449/// pm->node.flags |= PM_AUTOLOAD;
1450/// if (flags & FEAT_AUTOALL)
1451/// pm->node.flags |= PM_AUTOALL;
1452/// ret = 0;
1453/// } else
1454/// ret = -1;
1455/// noerrs = ne;
1456/// unqueue_signals();
1457///
1458/// return ret;
1459/// }
1460/// ```
1461///
1462/// Adds an autoload stub for a module parameter: `setsparam` creates
1463/// the param as a scalar with `value=module name`, then OR-in
1464/// `PM_AUTOLOAD` so first access fires `loadparamnode` (c:546) which
1465/// calls `ensurefeature(module, "p:", nam)` to actually load the
1466/// module. `typeset +` listing checks `PM_AUTOLOAD` and emits
1467/// "undefined NAME" instead of the usual type prefix.
1468pub fn add_autoparam(module: &str, pnam: &str, flags: i32) -> i32 {
1469 // c:1197
1470 use crate::ported::signals::queue_signals;
1471 use std::sync::atomic::Ordering;
1472
1473 // c:1202 — int ne = noerrs;
1474 let ne = *crate::ported::utils::noerrs_lock().lock().unwrap();
1475
1476 // c:1204 — queue_signals();
1477 queue_signals();
1478
1479 // c:1205 — if ((ret = checkaddparam(pnam, (flags & FEAT_IGNORE)))) {
1480 let ret_check = checkaddparam(pnam, flags & FEAT_IGNORE as i32);
1481 if ret_check != 0 {
1482 // c:1206 — unqueue_signals();
1483 unqueue_signals();
1484 // c:1214 — return ret == 2 ? 0 : -1;
1485 return if ret_check == 2 { 0 } else { -1 };
1486 }
1487
1488 // c:1217 — noerrs = 2;
1489 *crate::ported::utils::noerrs_lock().lock().unwrap() = 2;
1490
1491 // c:1218 — if ((pm = setsparam(dupstring(pnam), ztrdup(module))))
1492 let pm_opt = crate::ported::params::setsparam(pnam, module);
1493 let ret = if let Some(mut pm) = pm_opt {
1494 // c:1219 — pm->node.flags |= PM_AUTOLOAD;
1495 pm.node.flags |= PM_AUTOLOAD as i32;
1496 // c:1220-1221 — if (flags & FEAT_AUTOALL) pm->node.flags |= PM_AUTOALL;
1497 if (flags & FEAT_AUTOALL as i32) != 0 {
1498 pm.node.flags |= PM_AUTOALL as i32;
1499 }
1500 // Re-insert the modified clone back into paramtab so the flag
1501 // bits stick (paramtab stores `Param` by value, not by pointer).
1502 {
1503 let mut tab = paramtab().write().expect("paramtab poisoned");
1504 tab.insert(pnam.to_string(), pm);
1505 }
1506 // c:1222 — ret = 0;
1507 0
1508 } else {
1509 // c:1224 — ret = -1;
1510 -1
1511 };
1512
1513 // c:1225 — noerrs = ne;
1514 *crate::ported::utils::noerrs_lock().lock().unwrap() = ne;
1515 // c:1226 — unqueue_signals();
1516 unqueue_signals();
1517 // c:1228 — return ret;
1518 ret
1519}
1520
1521/// Port of `del_autoparam()` from `Src/module.c:1235`. — C decl `del_autoparam(UNUSED(const char *modnam), const char *pnam, int flags)`.
1522///
1523/// C body c:1234-1248:
1524/// ```c
1525/// static int
1526/// del_autoparam(UNUSED(const char *modnam), const char *pnam, int flags)
1527/// {
1528/// Param pm = (Param) gethashnode2(paramtab, pnam);
1529/// if (!pm) {
1530/// if (!(flags & FEAT_IGNORE)) return 2;
1531/// } else if (!(pm->node.flags & PM_AUTOLOAD)) {
1532/// if (!(flags & FEAT_IGNORE)) return 3;
1533/// } else
1534/// unsetparam_pm(pm, 0, 1);
1535/// return 0;
1536/// }
1537/// ```
1538///
1539/// Removes a param previously registered by `add_autoparam`. Returns
1540/// 2 when the param doesn't exist, 3 when it exists but isn't an
1541/// autoload stub, 0 on success — `FEAT_IGNORE` masks both error
1542/// returns to 0.
1543pub fn del_autoparam(_modnam: &str, pnam: &str, flags: i32) -> i32 {
1544 // c:1234
1545 // c:1237 — Param pm = (Param) gethashnode2(paramtab, pnam);
1546 let pm_opt = {
1547 let tab = paramtab().read().expect("paramtab poisoned");
1548 tab.get(pnam).cloned()
1549 };
1550 match pm_opt {
1551 // c:1239 — if (!pm) { if (!(flags & FEAT_IGNORE)) return 2; }
1552 None => {
1553 if (flags & FEAT_IGNORE as i32) == 0 {
1554 return 2; // c:1241
1555 }
1556 }
1557 Some(mut pm) => {
1558 if (pm.node.flags as u32 & PM_AUTOLOAD) == 0 {
1559 // c:1242 — else if (!(pm->node.flags & PM_AUTOLOAD))
1560 if (flags & FEAT_IGNORE as i32) == 0 {
1561 return 3; // c:1244
1562 }
1563 } else {
1564 // c:1246 — else unsetparam_pm(pm, 0, 1);
1565 unsetparam_pm(&mut pm, 0, 1);
1566 }
1567 }
1568 }
1569 0 // c:1248
1570}
1571
1572impl modulestab {
1573 /// `new` — see implementation.
1574 pub fn new() -> Self {
1575 let mut table = Self {
1576 // `Src/init.c:1193` — `modulestab = newmoduletable(17, "modules")`.
1577 modules: crate::ported::hashtable::hashtable_nodes::newhashtable(17),
1578 ..Self::default()
1579 };
1580 table.register_builtin_modules();
1581 table
1582 }
1583
1584 /// Register all statically-compiled modules (replaces dlopen)
1585 fn register_builtin_modules(&mut self) {
1586 let builtin_modules = [
1587 // Module→features MUST match the C BUILTIN() homes or a
1588 // builtin's feature lookup (`b:<name>` against its home module)
1589 // fails with "module `X' has no such feature". These were
1590 // SCRAMBLED: compadd/compset (complete.c:1693-1694) were under
1591 // zsh/computil, while the computil builtins (computil.c:5131-
1592 // 5138) were under zsh/complete, so calling `compset` from a
1593 // shell fn errored `module 'zsh/complete' has no such feature`.
1594 ("zsh/complete", &["compadd", "compset"][..]), // complete.c:1693-1694
1595 ("zsh/complist", &["complist"][..]),
1596 (
1597 "zsh/computil",
1598 &[
1599 "comparguments", // computil.c:5131
1600 "compdescribe", // c:5132
1601 "compfiles", // c:5133
1602 "compgroups", // c:5134
1603 "compquote", // c:5135
1604 "comptags", // c:5136
1605 "comptry", // c:5137
1606 "compvalues", // c:5138
1607 ][..],
1608 ),
1609 ("zsh/datetime", &["output_strftime"][..]),
1610 (
1611 "zsh/files",
1612 &[
1613 "mkdir", "rmdir", "ln", "mv", "cp", "rm", "chmod", "chown", "sync",
1614 ][..],
1615 ),
1616 ("zsh/langinfo", &[][..]),
1617 // c:Src/Modules/hlgroup.mdd — `link=either`, no builtins; its
1618 // features are the `.zle.esc` / `.zle.sgr` special hashes.
1619 ("zsh/hlgroup", &[][..]),
1620 // c:Src/Modules/ksh93.mdd — `link=either`, `moddeps="zsh/zle"`;
1621 // ksh93.c:40-42 bintab is `nameref`.
1622 ("zsh/ksh93", &["nameref"][..]),
1623 ("zsh/mapfile", &[][..]),
1624 ("zsh/mathfunc", &[][..]),
1625 ("zsh/nearcolor", &[][..]),
1626 ("zsh/net/socket", &["zsocket"][..]),
1627 ("zsh/net/tcp", &["ztcp"][..]),
1628 ("zsh/parameter", &[][..]),
1629 (
1630 "zsh/pcre",
1631 &["pcre_compile", "pcre_match", "pcre_study"][..],
1632 ),
1633 // c:Src/Modules/random.mdd — `link=either`, no builtins.
1634 ("zsh/random", &[][..]),
1635 ("zsh/regex", &[][..]),
1636 ("zsh/sched", &["sched"][..]),
1637 ("zsh/stat", &["zstat"][..]),
1638 (
1639 "zsh/system",
1640 &[
1641 "bin_sysread",
1642 "bin_syswrite",
1643 "bin_sysopen",
1644 "bin_sysseek",
1645 "bin_syserror",
1646 "zsystem",
1647 ][..],
1648 ),
1649 ("zsh/termcap", &["echotc"][..]),
1650 ("zsh/terminfo", &["echoti"][..]),
1651 ("zsh/watch", &["log"][..]),
1652 ("zsh/zftp", &["zftp"][..]),
1653 ("zsh/zleparameter", &[][..]),
1654 ("zsh/zprof", &["zprof"][..]),
1655 ("zsh/zpty", &["zpty"][..]),
1656 ("zsh/zselect", &["zselect"][..]),
1657 (
1658 "zsh/zutil",
1659 &["zstyle", "zformat", "zparseopts", "zregexparse"][..],
1660 ),
1661 (
1662 "zsh/attr",
1663 &["zgetattr", "zsetattr", "zdelattr", "zlistattr"][..],
1664 ),
1665 ("zsh/cap", &["cap", "getcap", "setcap"][..]),
1666 ("zsh/clone", &["clone"][..]),
1667 ("zsh/curses", &["zcurses"][..]),
1668 // c:Src/Modules/db_gdbm.mdd — `link='if test "x$enable_gdbm"
1669 // = xyes; then echo dynamic; else echo no; fi'`. Without the
1670 // gdbm backend the module is not built at all, so
1671 // `zmodload zsh/db/gdbm` fails ("failed to load module").
1672 // The backend here is the `gdbm` cfg used by db_gdbm.rs.
1673 #[cfg(feature = "gdbm")]
1674 ("zsh/db/gdbm", &["ztie", "zuntie", "zgdbmpath"][..]),
1675 ("zsh/param/private", &["private"][..]),
1676 // c:Src/Modules/newuser.mdd — `link=dynamic`, no builtins;
1677 // its whole body is boot_ (newuser.c). newuser.rs ports it
1678 // and every lifecycle dispatch arm below names it, but it
1679 // was missing here, so `module_linked` said no and
1680 // `zmodload zsh/newuser` failed where zsh loads it.
1681 ("zsh/newuser", &[][..]),
1682 // c:Src/Modules/compctl.c — statically-linked completion
1683 // module providing the compctl/compcall builtins. zsh
1684 // exposes it as autoloadable; `zmodload zsh/compctl`
1685 // succeeds on the running zsh because the symbol is
1686 // baked in. The zshrs auto-load registry (zsh_default_
1687 // loaded at line 1127) references it but the entry was
1688 // missing from this builtin_modules table, so
1689 // try_load_module returned 0 and zmodload failed with
1690 // "failed to load module `zsh/compctl'".
1691 ("zsh/compctl", &["compctl", "compcall"][..]),
1692 // c:Src/Builtins/rlimits.c — limit/ulimit/unlimit are
1693 // baked in. Same gap as zsh/compctl above.
1694 ("zsh/rlimits", &["limit", "ulimit", "unlimit"][..]),
1695 // c:Src/Zle/zle_main.c — zle/vared/bindkey baked in.
1696 ("zsh/zle", &["zle", "vared", "bindkey"][..]),
1697 // c:Src/Modules/example.c — example module that prints
1698 // "The example module has now been set up." on boot;
1699 // statically linked so `zmodload zsh/example` succeeds.
1700 ("zsh/example", &["example"][..]),
1701 // c:Src/Zle/deltochar.mdd — `link=dynamic`, `moddeps="zsh/zle"`,
1702 // no builtins (its features table is empty, deltochar.c:79-85);
1703 // boot_ adds the delete-to-char / zap-to-char widgets. A stock
1704 // zsh only fails to dlopen it when zsh/zle is not loaded yet
1705 // (the bundle needs zle's `bindk`); V01zmodload declares the
1706 // dependency with `zmodload -d` and loads it like every other
1707 // module in config.modules.
1708 ("zsh/deltochar", &[][..]),
1709 // NOT registered (zsh -fc parity probes confirm these
1710 // FAIL to load on this system because the dynamic
1711 // .bundle file doesn't exist):
1712 // zsh/calendar — pure dynamic module, no static
1713 // linkage in upstream zsh build.
1714 // zsh/db_gdbm — underscore alias for zsh/db/gdbm;
1715 // on the system zsh tested, neither
1716 // form loads (dlopen "no such file").
1717 // zsh/compwid — compwid bundle missing.
1718 // Letting zshrs zmodload succeed on these names would
1719 // diverge from `zsh -fc` which reports the dlopen error.
1720 // The names appear above only via try_load_module's
1721 // negative path (returns 0 → zmodload prints "failed to
1722 // load module").
1723 ];
1724
1725 // c:Src/init.c::init_bltinmods — C zsh's bltinmods.list is
1726 // generated at build time from Config/installmodules + the
1727 // active Modules/*.mdd files. Homebrew's stock zsh 5.9.1
1728 // binary ships these 14 modules in modulestab from the
1729 // start (verified via `${(@k)modules}` under `zsh -fc`):
1730 // zsh/compctl zsh/complete zsh/computil zsh/main
1731 // zsh/param/private zsh/parameter zsh/rlimits zsh/sched
1732 // zsh/termcap zsh/terminfo zsh/watch zsh/zle
1733 // zsh/zleparameter zsh/zutil
1734 // The rest (zsh/system, zsh/stat, zsh/zftp, zsh/zpty,
1735 // zsh/zselect, zsh/files, zsh/mapfile, etc.) require
1736 // explicit `zmodload NAME` before `${modules[NAME]}`
1737 // returns "loaded". All entries are registered into
1738 // modulestab so `zmodload NAME` can resolve them, but
1739 // entries OUTSIDE the default-loaded set carry `MOD_UNLOAD`
1740 // at init so `getpmmodule`'s is_loaded() check
1741 // (MOD_LINKED && !MOD_UNLOAD per zsh_h::is_loaded c:962)
1742 // returns false until `zmodload` clears the MOD_UNLOAD bit.
1743 // Bugs #530/#532/#535 in docs/BUGS.md.
1744 let zsh_default_loaded: &[&str] = &[
1745 "zsh/compctl",
1746 "zsh/complete",
1747 "zsh/computil",
1748 "zsh/main",
1749 "zsh/param/private",
1750 "zsh/parameter",
1751 "zsh/rlimits",
1752 "zsh/sched",
1753 "zsh/termcap",
1754 "zsh/terminfo",
1755 "zsh/watch",
1756 "zsh/zle",
1757 "zsh/zleparameter",
1758 "zsh/zutil",
1759 ];
1760 // c:Src/module.c:359-379 `register_module` — every compiled-in
1761 // module is appended to `linkedmodules` and NOTHING ELSE. C's
1762 // `modulestab` starts EMPTY and is filled lazily; the node set
1763 // it ends up with at the end of `main()` is decided entirely by
1764 // the generated `bltinmods.list` replayed below.
1765 //
1766 // Inserting all ~40 known modules as `modulestab` nodes here
1767 // (the previous shape) put `ct` past `hsize * 2` and quadrupled
1768 // the table from C's 17 buckets to 68, so `${(k)modules}` —
1769 // which is a raw bucket walk (`Src/Modules/parameter.c:1102`)
1770 // — listed loaded modules in an order that could not match zsh
1771 // for any collision: `zmodload zsh/system; zmodload zsh/zle`
1772 // printed `zle, system` where zsh prints `system, zle`
1773 // (bucket 6 vs buckets 23/57).
1774 for (name, _builtins) in &builtin_modules {
1775 // C zsh tracks builtin→module mapping in `builtintab` (the
1776 // canonical hashtable), not on a per-module ledger. We
1777 // just record the module as linked here; the builtins
1778 // themselves come in via the canonical table in `cmd.rs`.
1779 self.linkedmodules.push((*name).to_string()); // c:378
1780 }
1781 // `zsh/main` is the `link=static` module `Src/mkbltnmlst.sh:107`
1782 // emits a `register_module("zsh/main", …)` call for; it is not
1783 // in the autoloadable table above.
1784 self.linkedmodules.push("zsh/main".to_string()); // c:378
1785
1786 // c:Src/init.c:1739 `#include "bltinmods.list"` — the generated
1787 // boot code, replayed in order. `Src/mkbltnmlst.sh` walks
1788 // `config.modules` and emits, for every `load=yes` module:
1789 // * `autofeatures("zsh", MODULE, features, 0, 1)` when the
1790 // `.mdd` has an `autofeatures=` line (mkbltnmlst.sh:44-70);
1791 // `autofeatures` opens with
1792 // `find_module(module, FINDMOD_ALIASP|FINDMOD_CREATE, NULL)`
1793 // (c:3449) — THAT is what creates the module's `modulestab`
1794 // node.
1795 // * one `add_dep(MODULE, DEP)` per `moddeps=` entry
1796 // (mkbltnmlst.sh:71-73); `add_dep` also opens with
1797 // `find_module(name, …|FINDMOD_CREATE, &name)` (c:2390), so
1798 // a module with deps but no autofeatures (`zsh/complist`)
1799 // still gets a node.
1800 // then a second pass (mkbltnmlst.sh:83-105) emits `add_dep` for
1801 // `link=dynamic load=no` modules that declare `moddeps`.
1802 //
1803 // ORDER IS LOAD-BEARING: `addhashnode` front-inserts, so the
1804 // chain order inside a bucket is reverse creation order, and
1805 // `${(k)modules}` shows it. The sequence below is
1806 // `config.modules` order (Src/Builtins, then Src/Modules, then
1807 // Src/Zle, then Src/zsh.mdd), verified against the oracle:
1808 // `zsh -f -c 'zmodload <all 15>; print -rl -- ${(k)modules}'`
1809 // pins terminfo-before-watch, parameter-before-computil,
1810 // zle-before-zleparameter and rlimits-before-param/private.
1811 //
1812 // The resulting node count is 17 (16 here + `zsh/main` below),
1813 // well under the `ct >= 34` re-hash of the 17-bucket table.
1814 //
1815 // `zsh/hlgroup` and `zsh/ksh93` carry `load=yes` in the reference
1816 // tree's config.modules (lines 39-40) and boot here like the rest;
1817 // zsh 5.9.2 has neither, so its `${#modules}` / `${#parameters}`
1818 // rows are two modules short of the fork's.
1819 //
1820 // The `autofeatures` column below is each `.mdd`'s
1821 // `autofeatures=` line VERBATIM — that string is what
1822 // `mkbltnmlst.sh:63-69` bakes into the generated
1823 // `char *features[] = { … }` array, and `autofeatures("zsh",
1824 // MODULE, features, 0, 1)` (defflags 1 = FEAT_IGNORE, c:62) is
1825 // what registers the `b:`/`c:`/`p:`/`f:` autoloads. Replaying
1826 // only the `find_module` half left `zmodload -ac` and
1827 // `zmodload -ap` printing nothing where zsh lists four
1828 // conditions and forty parameters.
1829 let bltinmods_list: &[(&str, &[&str], &[&str])] = &[
1830 // (module, `autofeatures=`, `moddeps=`)
1831 // Src/Builtins/rlimits.mdd:5
1832 (
1833 "zsh/rlimits",
1834 &["b:limit", "b:ulimit", "b:unlimit"][..],
1835 &[][..],
1836 ),
1837 // Src/Builtins/sched.mdd:5
1838 (
1839 "zsh/sched",
1840 &["b:sched", "p:zsh_scheduled_events"][..],
1841 &[][..],
1842 ),
1843 // Src/Modules/hlgroup.mdd:5
1844 ("zsh/hlgroup", &["p:.zle.esc", "p:.zle.sgr"][..], &[][..]),
1845 // Src/Modules/ksh93.mdd:5,9
1846 ("zsh/ksh93", &["b:nameref"][..], &["zsh/zle"][..]),
1847 // Src/Modules/param_private.mdd:5
1848 ("zsh/param/private", &["b:private"][..], &[][..]),
1849 // Src/Modules/parameter.mdd:5
1850 (
1851 "zsh/parameter",
1852 &[
1853 "p:parameters",
1854 "p:commands",
1855 "p:functions",
1856 "p:dis_functions",
1857 "p:functions_source",
1858 "p:dis_functions_source",
1859 "p:funcfiletrace",
1860 "p:funcsourcetrace",
1861 "p:funcstack",
1862 "p:functrace",
1863 "p:builtins",
1864 "p:dis_builtins",
1865 "p:reswords",
1866 "p:dis_reswords",
1867 "p:patchars",
1868 "p:dis_patchars",
1869 "p:options",
1870 "p:modules",
1871 "p:dirstack",
1872 "p:history",
1873 "p:historywords",
1874 "p:jobtexts",
1875 "p:jobdirs",
1876 "p:jobstates",
1877 "p:nameddirs",
1878 "p:userdirs",
1879 "p:usergroups",
1880 "p:aliases",
1881 "p:dis_aliases",
1882 "p:galiases",
1883 "p:dis_galiases",
1884 "p:saliases",
1885 "p:dis_saliases",
1886 ][..],
1887 &[][..],
1888 ),
1889 // Src/Modules/random.mdd:5
1890 (
1891 "zsh/random",
1892 &["p:SRANDOM", "f:zrand_float", "f:zrand_int"][..],
1893 &[][..],
1894 ),
1895 // Src/Modules/termcap.mdd:15
1896 ("zsh/termcap", &["b:echotc", "p:termcap"][..], &[][..]),
1897 // Src/Modules/terminfo.mdd:15
1898 ("zsh/terminfo", &["b:echoti", "p:terminfo"][..], &[][..]),
1899 // Src/Modules/watch.mdd:5
1900 ("zsh/watch", &["b:log", "p:WATCH", "p:watch"][..], &[][..]),
1901 // Src/Modules/zutil.mdd:9
1902 (
1903 "zsh/zutil",
1904 &["b:zformat", "b:zstyle", "b:zregexparse", "b:zparseopts"][..],
1905 &["zsh/complete"][..],
1906 ),
1907 // Src/Zle/compctl.mdd:7
1908 (
1909 "zsh/compctl",
1910 &["b:compctl", "b:compcall"][..],
1911 &["zsh/complete", "zsh/zle"][..],
1912 ),
1913 // Src/Zle/complete.mdd:8 — the only `c:` autofeatures in the
1914 // boot set, and the source of `zmodload -ac`'s four rows.
1915 (
1916 "zsh/complete",
1917 &[
1918 "b:compadd",
1919 "b:compset",
1920 "c:prefix",
1921 "c:suffix",
1922 "c:between",
1923 "c:after",
1924 ][..],
1925 &["zsh/zle"][..],
1926 ),
1927 // No `autofeatures=`; the node comes from `add_dep` alone.
1928 ("zsh/complist", &[][..], &["zsh/complete", "zsh/zle"][..]), // Src/Zle/complist.mdd
1929 // Src/Zle/computil.mdd:9
1930 (
1931 "zsh/computil",
1932 &[
1933 "b:compdescribe",
1934 "b:comparguments",
1935 "b:compvalues",
1936 "b:compquote",
1937 "b:comptags",
1938 "b:comptry",
1939 "b:compfiles",
1940 "b:compgroups",
1941 ][..],
1942 &["zsh/complete", "zsh/zle"][..],
1943 ),
1944 // Src/Zle/zle.mdd:6
1945 ("zsh/zle", &["b:bindkey", "b:vared", "b:zle"][..], &[][..]),
1946 // Src/Zle/zleparameter.mdd:7
1947 (
1948 "zsh/zleparameter",
1949 &["p:widgets", "p:keymaps"][..],
1950 &["zsh/zle"][..],
1951 ),
1952 // mkbltnmlst.sh:83-105 second pass — `link=dynamic
1953 // load=no` with `moddeps`. `zsh/deltochar` is the only
1954 // other module in that shape and does NOT appear: the
1955 // grep is `' link=dynamic .* load=no '` with a TRAILING
1956 // space, and deltochar's `config.modules` line ends at
1957 // `load=no` (it declares no `functions=`), so it never
1958 // matches. Verified on the oracle: `zmodload -d` lists
1959 // seven modules and deltochar is not one of them.
1960 // zftp.mdd DOES carry `autofeatures="b:zftp"`, but it is
1961 // `load=no`, so mkbltnmlst.sh's second pass emits ONLY the
1962 // `add_dep` — no `autofeatures()` call, which is why
1963 // `zmodload -a` on the oracle has no `zftp` row.
1964 ("zsh/zftp", &[][..], &["zsh/net/tcp"][..]), // Src/Modules/zftp.mdd
1965 ];
1966 for (name, autofeature_list, deps) in bltinmods_list {
1967 if !autofeature_list.is_empty() {
1968 // c:3449 — autofeatures() opens with
1969 // find_module(module, FINDMOD_ALIASP|FINDMOD_CREATE, NULL)
1970 // which is what creates the module's modulestab node.
1971 // The feature registration itself is replayed after the
1972 // whole list has its nodes + MOD_LINKED flags (see the
1973 // second pass below) — `autofeatures` reads
1974 // `MOD_INIT_B`/the feature tables off the node it is
1975 // registering against, so the node has to exist first.
1976 find_module(self, name, FINDMOD_CREATE);
1977 }
1978 for dep in *deps {
1979 add_dep(self, name, dep); // c:2390 (via c:3449's sibling)
1980 }
1981 // WARNING: RUST-ONLY — C's `find_module` node is
1982 // `zshcalloc`ed with flags 0 and a NULL union, so
1983 // `module_loaded` reads 0 for every one of these until a
1984 // real `zmodload`. zshrs's static-link mirror instead uses
1985 // `MOD_LINKED` as the "this name has backing code" bit
1986 // (`zsh_h::module::is_loaded`) and `MOD_UNLOAD` as the
1987 // "not live yet" sentinel, and a long tail of gates in
1988 // `subst.rs` / `params.rs` / `fusevm_bridge.rs` read those
1989 // two bits. Keep the flags each of these nodes carried
1990 // before the boot set was trimmed, so this change moves
1991 // ONLY the bucket layout.
1992 if let Some(m) = self.modules.get_mut(name) {
1993 m.node.flags |= MOD_LINKED;
1994 if !zsh_default_loaded.contains(name) {
1995 m.node.flags |= crate::ported::zsh_h::MOD_UNLOAD;
1996 }
1997 }
1998 }
1999
2000 // c:Src/init.c:1708 init_bltinmods — run per-module `boot_`
2001 // for each statically-linked default-loaded module so paramtab
2002 // entries (e.g. `watch`/`WATCH` from zsh/watch, c:734) get
2003 // installed without an explicit zmodload. Without this,
2004 // `${(t)watch}` returns empty instead of `array-special`
2005 // even though zsh treats zsh/watch as effectively part of
2006 // the shell. Bug #270 in docs/BUGS.md. Keep the modules at
2007 // their MOD_LINKED-without-MOD_INIT_B initial state so the
2008 // `zmodload` (no-args) listing — gated on MOD_INIT_B per
2009 // bug #76 — still shows only `zsh/main`.
2010 for name in zsh_default_loaded {
2011 #[allow(clippy::single_match)]
2012 match *name {
2013 "zsh/watch" => {
2014 // `Src/Modules/watch.mdd` is `link=dynamic` with
2015 // `autofeatures="b:log p:WATCH p:watch"`, so real zsh
2016 // registers ONLY those three names up front and defers
2017 // `boot_` until the module is actually loaded. C's
2018 // `boot_` is the sole creator of WATCHFMT/LOGCHECK:
2019 //
2020 // c:Src/Modules/watch.c:756-759
2021 // if (!paramtab->getnode(paramtab, "WATCHFMT"))
2022 // setsparam("WATCHFMT", ztrdup_metafy(default_watchfmt));
2023 // if (!paramtab->getnode(paramtab, "LOGCHECK"))
2024 // setiparam("LOGCHECK", 60);
2025 //
2026 // and it runs only from `load_module`
2027 // (c:Src/module.c:2306). Verified against the oracle:
2028 // `zsh -f -c 'print ${WATCHFMT-UNSET}'` prints UNSET and
2029 // neither name appears in `${(ko)parameters}`, while
2030 // `zsh -f -c 'zmodload zsh/watch; print $WATCHFMT'`
2031 // prints `%n has %a %l from %m.` — the same is true for
2032 // any first touch of `$WATCH`/`$watch`/`log`, which is
2033 // what triggers the autoload.
2034 //
2035 // This call is a zshrs-only shim: the reason it exists
2036 // is the `watch`/`WATCH` paramtab registration that C
2037 // gets from the autofeature list, so `${(t)watch}`
2038 // reads `array-special` from the first prompt (bug #270
2039 // in docs/BUGS.md). Its WATCHFMT/LOGCHECK side effect is
2040 // NOT part of that contract, so undo it for names the
2041 // shim itself invented. The condition is "was zsh/watch
2042 // really loaded", exactly as in C — NOT the shell's
2043 // emulation mode: the previous `--zsh`-only gate inside
2044 // `watch::boot_` left both names in `${(ko)parameters}`
2045 // for the native binary. A pre-existing value (exported
2046 // WATCHFMT, or a real `zmodload zsh/watch` later on)
2047 // is left untouched.
2048 // `boot_` no longer seeds WATCHFMT/LOGCHECK when `m` is
2049 // null (watch.rs's `mid_load` gate), so there is nothing
2050 // left to undo here.
2051 crate::ported::modules::watch::boot_(std::ptr::null());
2052 }
2053 // c:Src/Zle/compctl.c:4016 setup_ — seed the hardwired
2054 // default compctls (cc_compos/cc_default/cc_first) the
2055 // bare shell uses for command/file completion. Runs here
2056 // at module registration (before any rc file), so it
2057 // never clobbers a user's later `compctl` definitions.
2058 // Without it CC_COMPOS is unset and `l<Tab>` in `zsh -f`
2059 // produced no matches.
2060 "zsh/compctl" => {
2061 crate::ported::zle::compctl::setup_();
2062 }
2063 // NOTE: `zsh/zle` deliberately has NO arm here. `setup_` is
2064 // the zle module's boot function, so C reaches it only from
2065 // the LOAD chain (`setup_module`, c:1884), never from static
2066 // registration — this table is the boot-time
2067 // `autofeatures` replay, which registers a node without
2068 // loading anything. Calling it here made
2069 // `$zle_bracketed_paste` (c:Src/Zle/zle_main.c:2276-2280)
2070 // exist in every shell, including one that never enters the
2071 // editor, where zsh leaves it unset:
2072 // zsh -f -c 'print ${+zle_bracketed_paste}' -> 0
2073 // The arm lives in `setup_module` instead, beside the other
2074 // per-module `setup_` dispatch.
2075 _ => {}
2076 }
2077 }
2078
2079 // c:Src/init.c:1739 `#include "bltinmods.list"` — the
2080 // `autofeatures("zsh", MODULE, features, 0, 1)` half, replayed in
2081 // `config.modules` order. `mkbltnmlst.sh:62-70` wraps each call in
2082 // `if (EMULATION(EMULATE_ZSH))`, and every boot module except
2083 // `zsh/rlimits` / `zsh/ksh93` (the two with `autofeatures_emu=`)
2084 // registers NOTHING outside zsh emulation. zshrs's emulation is
2085 // not settled at `modulestab::new()` time — this runs from
2086 // `init_bltinmods`, before `apply_cli_flags`' `emulate("sh")` —
2087 // so the zsh arm is the one replayed; a `--sh`/`--bash` process
2088 // will carry the zsh feature set, matching what zshrs already
2089 // does for the builtin ledger this replaces.
2090 //
2091 // `prefchar = 0` (the features carry their own `b:`/`c:`/`p:`
2092 // type prefix) and `defflags = 1` = FEAT_IGNORE (c:62), which is
2093 // what lets a name the static link already provides — every one
2094 // of the 27 `b:` builtins, and the eagerly-seeded `p:` specials
2095 // (`vm_helper::init_partab_params`) — register its ledger entry
2096 // without an "already defined" diagnostic.
2097 //
2098 // ORDERING (Rust-only): this runs AFTER the `zsh_default_loaded`
2099 // `boot_` shim loop above, not interleaved with node creation as
2100 // `bltinmods.list` has it. That loop is itself a zshrs-only shim
2101 // (see its comment) which installs `zsh/watch`'s `watch`/`WATCH`
2102 // specials eagerly, the way `init_partab_params` installs the
2103 // `partab[]` ones. Running the replay first made `add_autoparam`
2104 // (c:1197) win the race and leave a live PM_AUTOLOAD SCALAR stub
2105 // whose value is the module name, so `${#watch}` read 9
2106 // ("zsh/watch") instead of 0. With the specials already in
2107 // paramtab, `checkaddparam` (c:1026) returns 2 under FEAT_IGNORE
2108 // and `add_autoparam` is the no-op C reaches once a module is
2109 // loaded — while the ledger entry `autofeatures` records is what
2110 // `zmodload -ap` lists.
2111 for (name, autofeature_list, _deps) in bltinmods_list {
2112 if autofeature_list.is_empty() {
2113 continue;
2114 }
2115 // c:mkbltnmlst.sh:62-70 — zsh/hlgroup's autofeatures are the
2116 // dotted `.zle.esc` / `.zle.sgr`, which are not identifiers under
2117 // sh/bash emulation: add_autoparam's setsparam fails `isident`
2118 // and leaves errflag set (c:Src/params.c:3203-3207), so the shell
2119 // ran nothing at all. C never gets there — the call is inside
2120 // `if (EMULATION(EMULATE_ZSH))` — and the emulation is already
2121 // installed when this replay runs, so honour that gate here.
2122 if *name == "zsh/hlgroup"
2123 && !crate::ported::zsh_h::EMULATION(crate::ported::zsh_h::EMULATE_ZSH)
2124 {
2125 continue;
2126 }
2127 let features: Vec<String> = autofeature_list.iter().map(|f| f.to_string()).collect();
2128 // c:3440 `autofeatures(cmdnam, module, features, prefchar,
2129 // defflags)` — cmdnam "zsh" per mkbltnmlst.sh:69.
2130 autofeatures(self, "zsh", Some(name), &features, 0, FEAT_IGNORE);
2131 }
2132
2133 // The auto-load builtin→module bindings `zmodload -a` reports
2134 // used to be a hand-maintained 27-row `autoload_pairs` table
2135 // here. They are now produced by the `b:` half of the
2136 // `autofeatures` replay above, from each `.mdd`'s own
2137 // `autofeatures=` line — one source of truth instead of two.
2138 // The set is unchanged: rlimits 3 + sched 1 + param/private 1 +
2139 // termcap 1 + terminfo 1 + watch 1 + zutil 4 + compctl 2 +
2140 // complete 2 + computil 8 + zle 3 = 27, matching
2141 // `/opt/homebrew/bin/zsh -fc 'zmodload -a'` exactly. `zsh/files`
2142 // builtins (mkdir, rm, …) are statically linked but carry no
2143 // boot autofeatures entry (files.mdd is `load=no`), so they stay
2144 // out of the registry — zsh requires an explicit
2145 // `zmodload zsh/files`. Bug #222.
2146
2147 // c:Src/init.c:1708 — `init_bltinmods` ends with
2148 // `load_module("zsh/main", NULL, 0)`. `zsh/main` is the
2149 // always-loaded master module: every zsh process has it in
2150 // `modulestab` from boot, with `m->u.handle` (or `u.linked`)
2151 // non-NULL so `printmodulenode`'s "loaded" gate (c:218
2152 // `m->u.handle`) fires. Register here with `MOD_INIT_B` set
2153 // so `zmodload` (no args) lists `zsh/main` and ONLY `zsh/main`
2154 // — matching `/opt/homebrew/bin/zsh -fc 'zmodload'` output
2155 // exactly. Bug #76 in docs/BUGS.md.
2156 let mut main = module::new("zsh/main");
2157 main.node.flags |= crate::ported::zsh_h::MOD_INIT_B | MOD_INIT_S; // c:2244
2158 // c:2289 — load_module("zsh/main") assigns `m->u.linked`; the
2159 // union must be non-NULL so `zmodload -e zsh/main` (c:2637
2160 // `!m->u.handle`) reports the master module as loaded.
2161 main.linked = Some(Box::new(linkedmod {
2162 name: "zsh/main".to_string(),
2163 setup: None,
2164 features: None,
2165 enables: None,
2166 boot: None,
2167 cleanup: None,
2168 finish: None,
2169 }));
2170 self.modules.insert("zsh/main".to_string(), main);
2171 }
2172
2173 // Returns 0 success, 1 complete failure, 2 partial-features-fail. // c:2200-2201
2174 /// Port of `int load_module(char const *name, Feature_enables
2175 /// enablesarr, int silent)` from `Src/module.c:2206`. C body:
2176 /// validate name with `modname_ok`, queue_signals(), resolve alias
2177 /// chain via `find_module(FINDMOD_ALIASP)`. If not found, attempt
2178 /// `module_linked` then `do_load_module` (dlopen). Allocate a new
2179 /// `Module`, set `MOD_SETUP` (+ `MOD_LINKED` if statically linked),
2180 /// add to `modulestab`, run `setup_module`/`do_boot_module`. If
2181 /// either fails, cleanup+finish+delete and return 1. Else clear
2182 /// `MOD_SETUP`, set `MOD_INIT_S | MOD_INIT_B`, return `bootret`.
2183 /// If found and already `MOD_SETUP`, return 0. Detect circular
2184 /// deps via `MOD_BUSY`, load dependency list recursively. zshrs:
2185 /// all modules are statically linked, so dlopen path is skipped
2186 /// and we operate on the static registry.
2187 /// `enablesarr` is C's `Feature_enables` argument, threaded through
2188 /// to `do_boot_module` (c:2306) so a `require_module` that asks for ONE
2189 /// feature enables only that feature — the port used to drop the
2190 /// argument and enable every feature the module has.
2191 /// `enablesarr` is C's `Feature_enables` argument, threaded through to
2192 /// `do_boot_module` (c:2306) so a `require_module` that asks for ONE
2193 /// feature enables only that feature. The port used to drop the
2194 /// argument and pass `None` (= enable everything), so the first
2195 /// `[[ -prefix … ]]` installed all four of `zsh/complete`'s conddefs
2196 /// where zsh installs only `prefix`.
2197 /// WARNING: param names don't match C — Rust=(name, enablesarr) vs C=(name, enablesarr, silent)
2198 pub fn load_module(
2199 &mut self,
2200 name: &str,
2201 enablesarr: Option<&[String]>,
2202 // !!! RUST-ONLY PARAM !!! — carries C's per-entry
2203 // `Feature_enables.pat` (see FEAT_PATTERN_ARGS).
2204 feat_pat: bool,
2205 ) -> i32 {
2206 // c:2200
2207 // c:2201-2202 —
2208 // Now returns 0 for success (changed post-4.3.4),
2209 // 1 for complete failure, 2 if some features couldn't be set.
2210 // The port used to return `bool`, which collapsed 2 onto 0: a
2211 // `zmodload zsh/datetime` whose `p:EPOCHSECONDS` could not be added
2212 // (local parameter in the way, or a global of the same name) printed
2213 // C's two diagnostics and then exited 0 instead of 2
2214 // (V04features.ztst "Failed to add parameter if local parameter
2215 // present" / "Loading won't override global parameter").
2216 // Faithful port of the find_module-found branch (c:2249-2320).
2217 // The !find_module branch (c:2219-2247) requires DSO loading and
2218 // never fires in zshrs's static-link path: every linked module
2219 // is pre-registered by register_builtin_modules.
2220 //
2221 // C body c:2249-2320:
2222 // if (m->flags & MOD_SETUP) return 0;
2223 // if (m->flags & MOD_UNLOAD) m->flags &= ~MOD_UNLOAD;
2224 // else if (m->u.linked / m->u.handle) return 0;
2225 // if (m->flags & MOD_BUSY) {
2226 // zerr("circular dependencies for module ;%s", name);
2227 // return 1;
2228 // }
2229 // m->flags |= MOD_BUSY;
2230 // if (m->deps) for each dep: load_module(dep, NULL, silent);
2231 // m->flags &= ~MOD_BUSY;
2232 // if (!m->u.handle) {
2233 // module_linked / do_load_module
2234 // if (handle) m->u.handle = handle, m->flags |= MOD_SETUP
2235 // else m->u.linked = linked, m->flags |= MOD_SETUP|MOD_LINKED
2236 // if (setup_module(m)) { finish, NULL handles, ~SETUP, return 1; }
2237 // m->flags |= MOD_INIT_S;
2238 // }
2239 // m->flags |= MOD_SETUP;
2240 // if ((bootret = do_boot_module(m, enablesarr, silent)) == 1) {
2241 // do_cleanup_module(m); finish_module(m);
2242 // m->u.linked/handle = NULL;
2243 // m->flags &= ~MOD_SETUP;
2244 // return 1;
2245 // }
2246 // m->flags |= MOD_INIT_B;
2247 // m->flags &= ~MOD_SETUP;
2248 // return bootret;
2249
2250 // c:2208 — modname_ok(name)
2251 if modname_ok(name) == 0 {
2252 // c:2210 — zerr if !silent (silent flag not threaded yet)
2253 return 1;
2254 }
2255 crate::ported::signals::queue_signals(); // c:2222
2256 // c:2223 — find_module(name, FINDMOD_ALIASP)
2257 if !self.modules.contains_key(name) {
2258 // c:2223-2251 — the allocate-on-miss branch. C reaches it
2259 // for every module whose `modulestab` node has not been
2260 // created yet, which is the NORMAL state: `bltinmods.list`
2261 // creates nodes for only 16 modules at boot, so the first
2262 // `zmodload zsh/system` (or any other module) lands here.
2263 //
2264 // c:2224-2225 —
2265 // if (!(linked = module_linked(name)) &&
2266 // !(handle = do_load_module(name, silent))) { return 1; }
2267 // zshrs compiles every module in, so `module_linked` is the
2268 // only arm that can succeed; `do_load_module` (dlopen) is
2269 // called for its failure path only. `silent = 1` because
2270 // `require_module` (c:2354's caller) has already emitted the
2271 // canonical `failed to load module` warning through its own
2272 // `try_load_module` gate — matching the pre-existing
2273 // behaviour where this branch returned quietly.
2274 if !self.module_linked(name) {
2275 let _ = do_load_module(self, name, 1); // c:2225
2276 unqueue_signals(); // c:2226
2277 return 1; // c:2227
2278 }
2279 // c:2229 — m = zshcalloc(sizeof(*m));
2280 let mut m = module::new(name);
2281 // c:2234-2235 — m->u.linked = linked;
2282 // m->node.flags |= MOD_SETUP | MOD_LINKED;
2283 m.linked = Some(Box::new(linkedmod {
2284 name: name.to_string(),
2285 setup: None,
2286 features: None,
2287 enables: None,
2288 boot: None,
2289 cleanup: None,
2290 finish: None,
2291 }));
2292 m.node.flags = MOD_SETUP | MOD_LINKED;
2293 // c:2237 — modulestab->addnode(modulestab, ztrdup(name), m);
2294 self.modules.insert(name.to_string(), m);
2295
2296 // Same Rust-only paramtab re-seed the already-noded path
2297 // does below: C gets these SPECIALPMDEF entries from
2298 // `do_boot_module` → `enables_module` → `addparam`, which
2299 // zshrs services out of `PARTAB` instead.
2300 for nm in crate::vm_helper::module_gated_params_for(name) {
2301 crate::vm_helper::seed_partab_param(nm);
2302 }
2303
2304 // c:2239-2240 —
2305 // if ((set = setup_module(m)) ||
2306 // (bootret = do_boot_module(m, enablesarr, silent)) == 1)
2307 let set = setup_module(self, name);
2308 let bootret = if set == 0 {
2309 do_boot_module(self, name, enablesarr, 0, feat_pat)
2310 } else {
2311 1
2312 };
2313 if set != 0 || bootret == 1 {
2314 if set == 0 {
2315 let _ = do_cleanup_module(self, name); // c:2242
2316 }
2317 let _ = finish_module(self, name); // c:2243
2318 delete_module(self, name); // c:2244
2319 unqueue_signals(); // c:2245
2320 return 1; // c:2246
2321 }
2322 // c:2248-2249 — m->node.flags |= MOD_INIT_S | MOD_INIT_B;
2323 // m->node.flags &= ~MOD_SETUP;
2324 if let Some(m) = self.modules.get_mut(name) {
2325 m.node.flags |= MOD_INIT_S | MOD_INIT_B;
2326 m.node.flags &= !MOD_SETUP;
2327 }
2328 unqueue_signals(); // c:2250
2329 return bootret; // c:2251
2330 }
2331
2332 // c:2249 — if (MOD_SETUP) return 0;
2333 let flags = self.modules.get(name).unwrap().node.flags;
2334 if (flags & MOD_SETUP) != 0 {
2335 unqueue_signals(); // c:2250
2336 return 0; // c:2251
2337 }
2338 // c:2253-2257 —
2339 // if (m->node.flags & MOD_UNLOAD)
2340 // m->node.flags &= ~MOD_UNLOAD;
2341 // else if ((m->node.flags & MOD_LINKED) ? m->u.linked
2342 // : m->u.handle) {
2343 // unqueue_signals();
2344 // return 0;
2345 // }
2346 // The union read is real now: load assigns m.linked at c:2289,
2347 // so already-loaded modules early-return here while
2348 // pre-registered-but-unloaded ones (linked: None) fall through.
2349 if (flags & MOD_UNLOAD) != 0 {
2350 self.modules.get_mut(name).unwrap().node.flags &= !MOD_UNLOAD;
2351 } else if {
2352 let m = self.modules.get(name).unwrap();
2353 if (flags & MOD_LINKED) != 0 {
2354 m.linked.is_some() // c:2255 m->u.linked
2355 } else {
2356 m.handle.is_some() // c:2255 m->u.handle
2357 }
2358 } {
2359 unqueue_signals(); // c:2256
2360 return 0; // c:2257
2361 }
2362 // c:2259-2262 — circular-dependency detection.
2363 if (flags & MOD_BUSY) != 0 {
2364 unqueue_signals(); // c:2260
2365 crate::ported::utils::zerr(&format!("circular dependencies for module ;{}", name));
2366 return 1; // c:2262
2367 }
2368 self.modules.get_mut(name).unwrap().node.flags |= MOD_BUSY; // c:2264
2369
2370 // c:2269-2277 — recurse into m->deps.
2371 let deps_snapshot: Vec<String> = self
2372 .modules
2373 .get(name)
2374 .and_then(|m| m.deps.as_ref())
2375 .map(|d| d.iter().cloned().collect())
2376 .unwrap_or_default();
2377 for dep in &deps_snapshot {
2378 // c:2272 — `if (load_module(…, NULL, silent) == 1)`: only a COMPLETE
2379 // dependency failure aborts; a status-2 dep (features partly set)
2380 // carries on.
2381 if self.load_module(dep, None, false) == 1 {
2382 self.modules.get_mut(name).unwrap().node.flags &= !MOD_BUSY; // c:2273
2383 unqueue_signals(); // c:2274
2384 return 1; // c:2275
2385 }
2386 }
2387 self.modules.get_mut(name).unwrap().node.flags &= !MOD_BUSY; // c:2278
2388
2389 // c:2279-2304 — `if (!m->u.handle)` setup branch. The union
2390 // read means "no live binding yet" — neither handle (DSO) nor
2391 // linked (static) assigned. A deferred-unload reload re-enters
2392 // with m.linked still set and skips straight to boot, like C.
2393 let needs_setup = {
2394 let m = self.modules.get(name).unwrap();
2395 m.handle.is_none() && m.linked.is_none() // c:2279 !m->u.handle
2396 };
2397 if needs_setup {
2398 // c:2281 — `linked = module_linked(name)`: every zshrs
2399 // module is statically linked, so the lookup always hits;
2400 // callbacks dispatch by name (839f32249b), the record
2401 // carries the name like C's linkedmod.
2402 // c:2289-2291 — `m->u.linked = linked;
2403 // m->node.flags |= MOD_SETUP | MOD_LINKED;`
2404 if let Some(m) = self.modules.get_mut(name) {
2405 m.linked = Some(Box::new(linkedmod {
2406 name: name.to_string(),
2407 setup: None,
2408 features: None,
2409 enables: None,
2410 boot: None,
2411 cleanup: None,
2412 finish: None,
2413 }));
2414 m.node.flags |= MOD_SETUP | MOD_LINKED;
2415 }
2416 // c:2293 — setup_module(m). Routes through the dispatcher
2417 // (839f32249b) to per-module setup_(m). Most modules return
2418 // 0; some initialise module-private state.
2419 if setup_module(self, name) != 0 {
2420 // c:2294-2301 — failure: finish, clear handles, return 1.
2421 // else m->u.linked = NULL; (c:2298)
2422 let _ = finish_module(self, name);
2423 if let Some(m) = self.modules.get_mut(name) {
2424 m.linked = None; // c:2298
2425 m.node.flags &= !MOD_SETUP;
2426 }
2427 unqueue_signals(); // c:2300
2428 return 1; // c:2301
2429 }
2430 // c:2303 — `m->flags |= MOD_INIT_S;`
2431 self.modules.get_mut(name).unwrap().node.flags |= MOD_INIT_S;
2432 }
2433 // c:2305 — `m->flags |= MOD_SETUP;`
2434 self.modules.get_mut(name).unwrap().node.flags |= MOD_SETUP;
2435
2436 // c:Src/Modules/system.c:902,904 + zsh/mapfile — SPECIALPMDEF
2437 // entries get added to paramtab via the module's feature
2438 // dispatch (enables_ → handlefeatures → addparam). zshrs's
2439 // vm_helper::init_partab_params skips zmodload-gated names to
2440 // avoid them appearing before explicit load (bug #69). Re-seed
2441 // here once boot completes. This is a Rust-only bridge — no
2442 // direct C counterpart since C's handlefeatures runs implicitly
2443 // inside do_boot_module → enables_module.
2444 for nm in crate::vm_helper::module_gated_params_for(name) {
2445 crate::vm_helper::seed_partab_param(nm);
2446 }
2447
2448 // c:2306 — `bootret = do_boot_module(m, enablesarr, silent);`
2449 // The Rust do_boot_module routes through boot_module dispatcher
2450 // (b474b62898) to the per-module boot_(m) — the real partab
2451 // and bintab installations land here.
2452 let bootret = do_boot_module(self, name, enablesarr, 0, feat_pat);
2453 if bootret == 1 {
2454 // c:2306-2315 — boot failure: cleanup + finish + clear, return 1.
2455 // else m->u.linked = NULL; (c:2312)
2456 let _ = cleanup_module(self, name);
2457 let _ = finish_module(self, name);
2458 if let Some(m) = self.modules.get_mut(name) {
2459 m.linked = None; // c:2312
2460 m.node.flags &= !MOD_SETUP;
2461 }
2462 unqueue_signals(); // c:2314
2463 return 1; // c:2315
2464 }
2465 // c:2317-2318 — `m->flags |= MOD_INIT_B; m->flags &= ~MOD_SETUP;`
2466 if let Some(m) = self.modules.get_mut(name) {
2467 m.node.flags |= MOD_INIT_B;
2468 m.node.flags &= !MOD_SETUP;
2469 }
2470 unqueue_signals(); // c:2319
2471 bootret // c:2320 return bootret
2472 }
2473
2474 // Backend handler for zmodload -u // c:2813
2475 /// Port of `int unload_module(Module m)` from `Src/module.c:2817`.
2476 /// C body: resolve `MOD_ALIAS` via `find_module(FINDMOD_ALIASP)`;
2477 /// if `MOD_INIT_S` set and !`MOD_UNLOAD`, call `do_cleanup_module`.
2478 /// Clear `MOD_INIT_B|MOD_INIT_S`. If a `wrapper` is present, set
2479 /// `MOD_UNLOAD` and bail (deferred). Else clear `MOD_UNLOAD` and
2480 /// call `m->u.linked->finish(m)` or `finish_module(m)` depending
2481 /// on `MOD_LINKED`. Finally walk `m->deps` and unload modules
2482 /// that were tagged `MOD_UNLOAD` when the last dependent dies.
2483 /// WARNING: param names don't match C — Rust=(name) vs C=(m)
2484 pub fn unload_module(&mut self, name: &str) -> bool {
2485 // c:2812 — faithful port of the static-link control flow.
2486 // C body c:2818-2913:
2487 // if (m->flags & MOD_ALIAS) { resolve via find_module }
2488 // if (MOD_INIT_S && !MOD_UNLOAD && do_cleanup_module(m))
2489 // return 1;
2490 // m->flags &= ~(MOD_INIT_B | MOD_INIT_S);
2491 // del = m->flags & MOD_UNLOAD;
2492 // if (m->wrapper) { m->flags |= MOD_UNLOAD; return 0; }
2493 // m->flags &= ~MOD_UNLOAD;
2494 // if (MOD_LINKED) { if (u.linked) { u.linked->finish(m);
2495 // u.linked = NULL; } }
2496 // else { if (u.handle) { finish_module(m);
2497 // u.handle = NULL; } }
2498 // if (del && m->deps) { /* unload deps flagged MOD_UNLOAD */ }
2499 // if (m->autoloads && firstnode(m->autoloads))
2500 // autofeatures("zsh", name, hlinklist2array(autoloads),
2501 // 0, FEAT_IGNORE);
2502 // else if (!m->deps) delete_module(m);
2503 // return 0;
2504
2505 // c:2819-2823 — alias resolve. Mutate the lookup name, not
2506 // the live record (alias chases the target).
2507 let mut target_name = name.to_string();
2508 let needs_alias_chase = self
2509 .modules
2510 .get(&target_name)
2511 .map(|m| (m.node.flags & MOD_ALIAS) != 0)
2512 .unwrap_or(false);
2513 if needs_alias_chase {
2514 target_name = match self.modules.get(name).and_then(|m| m.alias.clone()) {
2515 Some(a) => a,
2516 None => return false, // c:2821-2822 — alias target gone.
2517 };
2518 }
2519
2520 // c:2831-2834 — cleanup if booted.
2521 let (init_s, unload_flag) = match self.modules.get(&target_name) {
2522 Some(m) => (
2523 (m.node.flags & MOD_INIT_S) != 0,
2524 (m.node.flags & MOD_UNLOAD) != 0,
2525 ),
2526 None => return false, // c:2820-2822 fall-through to !m
2527 };
2528 if init_s && !unload_flag {
2529 // c:2833 — `do_cleanup_module` is `cleanup_module` for the
2530 // MOD_LINKED branch (static-link path always).
2531 if cleanup_module(self, &target_name) != 0 {
2532 return false; // c:2834 cleanup error
2533 }
2534 }
2535
2536 // c:2835 — `m->flags &= ~(MOD_INIT_B | MOD_INIT_S);`
2537 if let Some(m) = self.modules.get_mut(&target_name) {
2538 m.node.flags &= !(MOD_INIT_B | MOD_INIT_S);
2539 }
2540
2541 // c:2837 — `del = m->flags & MOD_UNLOAD;`
2542 let del = unload_flag;
2543
2544 // c:2839-2842 — wrapper deferred-unload path.
2545 let has_wrapper = self
2546 .modules
2547 .get(&target_name)
2548 .map(|m| m.wrapper != 0)
2549 .unwrap_or(false);
2550 if has_wrapper {
2551 if let Some(m) = self.modules.get_mut(&target_name) {
2552 m.node.flags |= MOD_UNLOAD; // c:2840
2553 }
2554 return true; // c:2841
2555 }
2556
2557 // c:2843 — `m->flags &= ~MOD_UNLOAD;`
2558 if let Some(m) = self.modules.get_mut(&target_name) {
2559 m.node.flags &= !MOD_UNLOAD;
2560 }
2561
2562 // c:2849-2859 — finish hook:
2563 // if (m->node.flags & MOD_LINKED) {
2564 // if (m->u.linked) {
2565 // m->u.linked->finish(m);
2566 // m->u.linked = NULL;
2567 // }
2568 // }
2569 // Static-link branch routes through finish_module which
2570 // dispatches to the per-module finish_(m) (839f32249b), gated
2571 // and cleared exactly like C's u.linked.
2572 let was_linked = self
2573 .modules
2574 .get(&target_name)
2575 .map(|m| m.linked.is_some())
2576 .unwrap_or(false);
2577 if was_linked {
2578 let _ = finish_module(self, &target_name); // c:2851
2579 if let Some(m) = self.modules.get_mut(&target_name) {
2580 m.linked = None; // c:2852
2581 }
2582 }
2583
2584 // c:2861-2902 — dep walk: when del was set, find every
2585 // dep that has MOD_UNLOAD and check no other live module
2586 // depends on it, then recursively unload.
2587 if del {
2588 let deps_snapshot: Vec<String> = self
2589 .modules
2590 .get(&target_name)
2591 .and_then(|m| m.deps.as_ref())
2592 .map(|d| d.iter().cloned().collect())
2593 .unwrap_or_default();
2594 for dep_name in deps_snapshot {
2595 let dm_target = match find_module(self, &dep_name, FINDMOD_ALIASP) {
2596 Some(n) => n,
2597 None => continue, // c:2867 dm == NULL
2598 };
2599 let dm_unloading = self
2600 .modules
2601 .get(&dm_target)
2602 .map(|m| (m.node.flags & MOD_UNLOAD) != 0)
2603 .unwrap_or(false);
2604 if !dm_unloading {
2605 continue; // c:2870-2871
2606 }
2607 // c:2872-2897 — scan every other module's deps for
2608 // dm_target.nam; bail if any live module still
2609 // depends on it.
2610 let still_depended = self.modules.iter().any(|(other_name, other)| {
2611 if other_name == &target_name {
2612 return false; // c:2884 don't scan ourselves
2613 }
2614 let other_deps = match other.deps.as_ref() {
2615 Some(d) => d,
2616 None => return false, // c:2884 no deps to scan
2617 };
2618 // c:2887-2889 — only scan live modules (MOD_LINKED
2619 // set + !MOD_UNLOAD in zshrs's static-link path).
2620 if (other.node.flags & MOD_LINKED) == 0 || (other.node.flags & MOD_UNLOAD) != 0
2621 {
2622 return false;
2623 }
2624 other_deps.iter().any(|d| d == &dm_target)
2625 });
2626 if !still_depended {
2627 // c:2898-2899 — `unload_module(dm)` recursive.
2628 self.unload_module(&dm_target);
2629 }
2630 }
2631 }
2632
2633 // c:2903-2912 — autoload restore OR delete_module.
2634 let (has_autoloads, has_deps, autoloads) = match self.modules.get(&target_name) {
2635 Some(m) => (
2636 m.autoloads.as_ref().map(|a| !a.is_empty()).unwrap_or(false),
2637 m.deps.is_some(),
2638 m.autoloads
2639 .as_ref()
2640 .map(|a| a.iter().cloned().collect::<Vec<_>>())
2641 .unwrap_or_default(),
2642 ),
2643 None => (false, false, Vec::new()),
2644 };
2645 if has_autoloads {
2646 // c:2908-2909 — autofeatures("zsh", m->nam, ..., 0, FEAT_IGNORE)
2647 autofeatures(self, "zsh", Some(&target_name), &autoloads, 0, FEAT_IGNORE);
2648 } else if !has_deps {
2649 // c:2910-2911 — delete_module(m). Inline since the free fn
2650 // also calls remove + drop.
2651 self.modules.remove(&target_name);
2652 }
2653 true // c:2913 return 0
2654 }
2655
2656 /// Check if module is loaded
2657 pub fn is_loaded(&self, name: &str) -> bool {
2658 self.modules
2659 .get(name)
2660 .map(|m| m.is_loaded())
2661 .unwrap_or(false)
2662 }
2663
2664 /// Whether a module is BOUND — its setup_/boot_ has actually run.
2665 /// Mirrors the `zmodload -e` existence test (bin_zmodload_exist,
2666 /// c:Src/module.c:2637): the union slot `m->u.handle` is non-NULL
2667 /// (handle for dlopen, linked for a static module) and the module is
2668 /// not mid-unload. Distinct from `is_loaded()` / `module_loaded()`,
2669 /// which key off `MOD_LINKED` — a flag `register_builtin_modules`
2670 /// pre-seeds for every statically-compiled module at startup, so it
2671 /// is true before `zmodload` ever runs. Use this for "was the module
2672 /// actually loaded by the user" gates.
2673 pub fn is_bound(&self, name: &str) -> bool {
2674 self.modules
2675 .get(name)
2676 .map(|m| (m.handle.is_some() || m.linked.is_some()) && (m.node.flags & MOD_UNLOAD) == 0)
2677 .unwrap_or(false)
2678 }
2679
2680 /// List all loaded modules
2681 pub fn list_loaded(&self) -> Vec<&str> {
2682 self.modules
2683 .iter()
2684 .filter(|(_, m)| m.is_loaded())
2685 .map(|(name, _)| name.as_str())
2686 .collect()
2687 }
2688
2689 /// List all modules (including unloaded). Returns name + raw
2690 /// `MOD_*` flag bits — caller can inspect `MOD_UNLOAD` / `MOD_LINKED`
2691 /// directly (matches C, which exposes `m->node.flags`).
2692 pub fn list_all(&self) -> Vec<(&str, i32)> {
2693 self.modules
2694 .iter()
2695 .map(|(name, m)| (name.as_str(), m.node.flags))
2696 .collect()
2697 }
2698
2699 // ------- Builtin management (from module.c addbuiltin/deletebuiltin) -------
2700
2701 /// Register a builtin (from module.c addbuiltin)
2702 /// Port of `addbuiltin(Builtin b)` from `Src/module.c:409`.
2703 /// WARNING: param names don't match C — Rust=(name, module) vs C=(b)
2704 ///
2705 /// In C, this inserts the builtin into the canonical `builtintab`
2706 /// hashtable (Src/builtin.c). The real builtin registration lives
2707 /// in `cmd.rs::BUILTINTAB` and the routing fn here delegates to
2708 /// the free `addbuiltin` (c:303 port) so the canonical BINF_ADDED
2709 /// clash gate fires.
2710 ///
2711 /// Returns 0 on success, 1 on clash (per C's signature).
2712 pub fn addbuiltin(&mut self, name: &str, module: &str) -> i32 {
2713 // c:409
2714 // Construct a probe builtin matching what add_autobin /
2715 // setbuiltins build; route through the free addbuiltin which
2716 // probes createbuiltintable() for BINF_ADDED.
2717 let mut bn = builtin {
2718 node: hashnode {
2719 next: None,
2720 nam: name.to_string(),
2721 flags: 0,
2722 },
2723 handlerfunc: None,
2724 minargs: 0,
2725 maxargs: 0,
2726 funcid: 0,
2727 optstr: Some(module.to_string()),
2728 defopts: None,
2729 };
2730 if addbuiltin(&mut bn) != 0 {
2731 return 1; // c:417 clash
2732 }
2733 // On success: record in added_builtins ledger so the
2734 // module's setbuiltins delete path can find this entry.
2735 self.added_builtins.insert(name.to_string(), BINF_ADDED);
2736 0 // c:417 OK
2737 }
2738
2739 /// Unregister a builtin (from module.c deletebuiltin)
2740 /// Port of `deletebuiltin(const char *nam)` from `Src/module.c:449`.
2741 /// Returns 0 on success (entry found + removed from ledger), -1
2742 /// on miss — matching the canonical free fn (00e6a9ce7e) and
2743 /// C's deletebuiltin return contract.
2744 /// WARNING: param names don't match C — Rust=(name, module) vs C=(nam)
2745 pub fn deletebuiltin(&mut self, name: &str, _module: &str) -> i32 {
2746 // c:449
2747 // Route through the free deletebuiltin for the canonical
2748 // present/absent probe.
2749 let r = deletebuiltin(name);
2750 if r == 0 {
2751 // Drop the ledger entry so setbuiltins's `already_added`
2752 // probe sees this name as removed.
2753 self.added_builtins.remove(name);
2754 }
2755 r
2756 }
2757
2758 /// Register autoloading builtin.
2759 /// Port of `static int add_autobin(const char *module, const char *bnam,
2760 /// int flags)` from `Src/module.c:426`. C allocates a `Builtin` node
2761 /// with `optstr=module`, sets `BINF_AUTOALL` if `FEAT_AUTOALL` is in
2762 /// `flags`, then calls `addbuiltin(bn)`. On failure, the freshly
2763 /// allocated node is freed; success returns 0, conflict returns 1
2764 /// unless `FEAT_IGNORE` masks it.
2765 /// WARNING: param names don't match C — Rust=(name, module, flags) vs C=(module, bnam, flags)
2766 pub fn add_autobin(&mut self, name: &str, module: &str, flags: i32) -> i32 {
2767 // c:426
2768 // Faithful port of c:426-441:
2769 // Builtin bn = zshcalloc(sizeof(*bn));
2770 // bn->node.nam = ztrdup(bnam);
2771 // bn->optstr = ztrdup(module);
2772 // if (flags & FEAT_AUTOALL)
2773 // bn->node.flags |= BINF_AUTOALL;
2774 // if ((ret = addbuiltin(bn))) {
2775 // builtintab->freenode(&bn->node);
2776 // if (!(flags & FEAT_IGNORE))
2777 // return 1;
2778 // }
2779 // return 0;
2780 //
2781 // Prior port did a ledger-only insert into autoload_builtins
2782 // without ever calling the canonical addbuiltin. Now constructs
2783 // the builtin struct and routes through the free addbuiltin
2784 // (c:303 port) which probes createbuiltintable() for the
2785 // BINF_ADDED clash gate. Ledger entry preserved on success so
2786 // resolve_autoload_builtin() / del_autobin's fast path can
2787 // find it without re-scanning.
2788 let node_flags: i32 = if (flags & FEAT_AUTOALL as i32) != 0 {
2789 BINF_AUTOALL as i32 // c:435
2790 } else {
2791 0
2792 };
2793 let mut bn = builtin {
2794 // c:431-433 — zshcalloc + populate.
2795 node: hashnode {
2796 next: None,
2797 nam: name.to_string(),
2798 flags: node_flags,
2799 },
2800 handlerfunc: None,
2801 minargs: 0,
2802 maxargs: 0,
2803 funcid: 0,
2804 optstr: Some(module.to_string()),
2805 defopts: None,
2806 };
2807 // c:436 — `if ((ret = addbuiltin(bn)))`. Clash gate via the
2808 // canonical builtintab.
2809 if addbuiltin(&mut bn) != 0 {
2810 // c:437 — freenode drops the input via Rust's Drop.
2811 if (flags & FEAT_IGNORE as i32) == 0 {
2812 return 1; // c:439
2813 }
2814 // c:440 — FEAT_IGNORE masks → fall through to ret 0 but
2815 // don't insert into the ledger (the canonical table has
2816 // this name already).
2817 return 0;
2818 }
2819 // c:441 success path: register in the autoload ledger.
2820 self.autoload_builtins
2821 .insert(name.to_string(), module.to_string());
2822 0
2823 }
2824
2825 // Remove an autoloaded added by add_autobin // c:464
2826 /// Port of `static int del_autobin(const char *module, const char *bnam,
2827 /// int flags)` from `Src/module.c:464`.
2828 ///
2829 /// C body (c:464-478):
2830 /// ```c
2831 /// Builtin bn = (Builtin) builtintab->getnode2(builtintab, bnam);
2832 /// if (!bn) { if(!(flags & FEAT_IGNORE)) return 2; }
2833 /// else if (bn->node.flags & BINF_ADDED) {
2834 /// if (!(flags & FEAT_IGNORE)) return 3;
2835 /// } else deletebuiltin(bnam);
2836 /// return 0;
2837 /// ```
2838 ///
2839 /// 2 = "no such builtin", 3 = "real registered builtin (BINF_ADDED) —
2840 /// can't unload", 0 = success (removed autoload entry).
2841 /// FEAT_IGNORE masks both error returns.
2842 ///
2843 /// zshrs architecture: `builtintab` is static-linked at startup
2844 /// (`createbuiltintable()` builds an immutable HashMap), so every
2845 /// entry there is effectively BINF_ADDED. The autoload-only entries
2846 /// live in `self.autoload_builtins`. The faithful mapping:
2847 /// * if name is in `builtintab` → BINF_ADDED → return 3 (or 0 with
2848 /// FEAT_IGNORE).
2849 /// * else if name is in `autoload_builtins` → remove it, return 0.
2850 /// * else → not present → return 2 (or 0 with FEAT_IGNORE).
2851 /// WARNING: param names don't match C — Rust=(name, flags) vs C=(module, bnam, flags)
2852 pub fn del_autobin(&mut self, name: &str, flags: i32) -> i32 {
2853 // c:464
2854 // Faithful port of c:464-478:
2855 // Builtin bn = (Builtin) builtintab->getnode2(builtintab, bnam);
2856 // if (!bn) {
2857 // if(!(flags & FEAT_IGNORE)) return 2;
2858 // } else if (bn->node.flags & BINF_ADDED) {
2859 // if (!(flags & FEAT_IGNORE)) return 3;
2860 // } else
2861 // deletebuiltin(bnam);
2862 // return 0;
2863 //
2864 // Three distinct return codes: 2 = no such builtin (autoload
2865 // ledger absent), 3 = real registered (BINF_ADDED set — can't
2866 // unload via del_auto*), 0 = success.
2867 //
2868 // zshrs's createbuiltintable() entries are all BINF_ADDED by
2869 // construction (the static-linked canonical table), so a hit
2870 // there always means "real registered" → return 3. The
2871 // autoload stubs live in self.autoload_builtins; a hit there
2872 // is the c:475 deletebuiltin path.
2873 //
2874 // Now routes through the free deletebuiltin (00e6a9ce7e) for
2875 // the canonical-table probe so the present/absent contract
2876 // matches C exactly. Prior port did the probe inline.
2877
2878 // c:466 — `builtintab->getnode2(builtintab, bnam)`. zshrs's
2879 // builtintab is split: the immutable createbuiltintable()
2880 // carries the static flags, the autoload_builtins ledger holds
2881 // runtime stubs from add_autobin, and added_builtins is the
2882 // runtime BINF_ADDED bit a module load flips (setbuiltins
2883 // c:508 probes the same ledger).
2884 let static_flags: Option<i32> = createbuiltintable().get(name).map(|b| b.node.flags);
2885 let in_ledger = self.autoload_builtins.contains_key(name);
2886 // BINF_ADDED equivalent: C sets the bit at addbuiltins time —
2887 // startup for core builtins, the module's boot_ for module
2888 // builtins. zshrs's static table folds BOTH in unflagged, so a
2889 // static hit counts as ADDED only when the name is core (no
2890 // owning module advertises `b:NAME`) or its owning module is
2891 // loaded. Runtime loads also land in the added_builtins ledger
2892 // (setbuiltins c:508 probes the same).
2893 let added = self.added_builtins.contains_key(name)
2894 || static_flags
2895 .map(|f| (f & BINF_ADDED as i32) != 0)
2896 .unwrap_or(false)
2897 || (static_flags.is_some() && {
2898 let mod_names: Vec<String> = self.modules.keys().cloned().collect();
2899 let mut owner_loaded: Option<bool> = None; // None = core builtin
2900 'outer: for mn in &mod_names {
2901 let mut feats: Vec<String> = Vec::new();
2902 if features_module(self, mn, &mut feats) != 0 {
2903 continue;
2904 }
2905 for f in &feats {
2906 if f.strip_prefix("b:") == Some(name) {
2907 owner_loaded = Some(self.is_loaded(mn));
2908 break 'outer;
2909 }
2910 }
2911 }
2912 owner_loaded != Some(false)
2913 });
2914 if static_flags.is_none() && !in_ledger {
2915 // c:467-469 — `if (!bn) { if(!(flags & FEAT_IGNORE)) return 2; }`
2916 if (flags & FEAT_IGNORE as i32) == 0 {
2917 return 2; // c:469
2918 }
2919 } else if added {
2920 // c:470-473 — `else if (bn->node.flags & BINF_ADDED)` —
2921 // a real, live builtin can't be un-autoloaded.
2922 if (flags & FEAT_IGNORE as i32) == 0 {
2923 return 3; // c:472
2924 }
2925 } else {
2926 // c:474-475 — `else deletebuiltin(bnam);` — drop the
2927 // autoload stub from the ledger.
2928 self.autoload_builtins.remove(name);
2929 }
2930 0 // c:477
2931 }
2932
2933 /// Set/clear a slice of builtins per `e[]` mask.
2934 /// Port of `static int setbuiltins(char const *nam, Builtin binl,
2935 /// int size, int *e)` from `Src/module.c:501`. For each Builtin in
2936 /// `binl[0..size]`: if `e[n]` is set, add the builtin (skip if
2937 /// already `BINF_ADDED`); else delete the builtin (skip if not
2938 /// `BINF_ADDED`). Warnings on clash/already-deleted; returns 1 if
2939 /// any op failed.
2940 /// WARNING: param names don't match C — Rust=(module, names, e) vs C=(nam, binl, size, e)
2941 pub fn setbuiltins(&mut self, module: &str, names: &[&str], e: Option<&[i32]>) -> i32 {
2942 // c:501
2943 let mut ret: i32 = 0; // c:503
2944 for (n, name) in names.iter().enumerate() {
2945 // c:505 — `for (n = 0; n < size; n++) { Builtin b = &binl[n]; ... }`
2946 let enable = e
2947 .map(|arr| arr.get(n).copied().unwrap_or(0)) // c:507 *e++
2948 .unwrap_or(1);
2949 let already_added = self.added_builtins.contains_key(*name); // c:508 b->flags & BINF_ADDED
2950 if enable != 0 {
2951 // c:507 — `if (e && *e++)` add branch
2952 if already_added {
2953 // c:508-509 — skip already-added.
2954 continue;
2955 }
2956 // c:510 — `if (addbuiltin(b))` — probe the canonical
2957 // table for the clash gate. The free fn returns 1
2958 // when an existing entry already has BINF_ADDED set.
2959 let mut probe = builtin {
2960 node: hashnode {
2961 next: None,
2962 nam: name.to_string(),
2963 flags: 0,
2964 },
2965 handlerfunc: None,
2966 minargs: 0,
2967 maxargs: 0,
2968 funcid: 0,
2969 optstr: Some(module.to_string()),
2970 defopts: None,
2971 };
2972 if addbuiltin(&mut probe) != 0 {
2973 // c:511-513 — `zwarnnam(nam, "name clash...")`
2974 zwarnnam(
2975 module,
2976 &format!("name clash when adding builtin `{}'", name),
2977 );
2978 ret = 1; // c:513
2979 } else {
2980 // c:515 — `b->node.flags |= BINF_ADDED;`. Mirror
2981 // the in-place bit-set with the per-module
2982 // ledger flip.
2983 self.added_builtins.insert(name.to_string(), BINF_ADDED);
2984 }
2985 } else {
2986 // c:517-525 — del branch.
2987 if !already_added {
2988 // c:518-519 — skip already-not-added.
2989 continue;
2990 }
2991 // c:520 — `if (deletebuiltin(b->node.nam))`. Free fn
2992 // returns -1 on miss; treat any non-zero as the
2993 // "already deleted" condition.
2994 if deletebuiltin(name) != 0 {
2995 // c:521-523 — `zwarnnam(nam, "builtin `%s' already
2996 // deleted")`.
2997 zwarnnam(module, &format!("builtin `{}' already deleted", name));
2998 ret = 1; // c:523
2999 } else {
3000 // c:524 — `b->node.flags &= ~BINF_ADDED;`
3001 self.added_builtins.remove(*name);
3002 }
3003 }
3004 }
3005 ret // c:528
3006 }
3007
3008 // ------- Condition management (from module.c addconddef/deleteconddef) -------
3009
3010 /// Register a condition (from module.c addconddef)
3011 /// Port of `addconddef(Conddef c)` from `Src/module.c:703`.
3012 /// WARNING: param names don't match C — Rust=(name, module) vs C=(c)
3013 ///
3014 /// Like `addbuiltin`, the real registration lives in
3015 /// `cond.rs::CONDTAB`; the routing fn here delegates to the free
3016 /// `addconddef` (4304-port) so the canonical name+infix-flag
3017 /// clash gate fires.
3018 ///
3019 /// Returns 0 on success, 1 on clash (matches C's signature).
3020 pub fn addconddef(&mut self, name: &str, module: &str) -> i32 {
3021 // c:703
3022 // Construct a probe conddef matching what add_autocond /
3023 // setconddefs build. handler/min/max/condid stay default
3024 // (filled in by the actual module on load).
3025 let cd = conddef {
3026 next: None,
3027 name: name.to_string(),
3028 flags: 0, // c:703 entries here aren't infix by default
3029 handler: None,
3030 min: 0,
3031 max: 0,
3032 condid: 0,
3033 module: Some(module.to_string()),
3034 };
3035 // c:705-715 — addconddef walks CONDTAB for clash, replaces
3036 // autoload entries, prepends on success. Returns 0/1.
3037 addconddef(cd)
3038 }
3039
3040 /// Unregister a condition (from module.c deleteconddef)
3041 /// Port of `deleteconddef(Conddef c)` from `Src/module.c:724`.
3042 /// Returns 0 on success (entry was found + removed), -1 on miss.
3043 /// Mirrors C's deleteconddef return contract.
3044 /// WARNING: param names don't match C — Rust=(name, module) vs C=(c)
3045 pub fn deleteconddef(&mut self, name: &str, _module: &str) -> i32 {
3046 // c:724
3047 // Build a probe conddef and call the free deleteconddef
3048 // (4304-port) which walks CONDTAB by (name, infix-flag) for
3049 // identity. infix=0 here matches the prefix-style default
3050 // used by the auto* registrations.
3051 let probe = conddef {
3052 next: None,
3053 name: name.to_string(),
3054 flags: 0,
3055 handler: None,
3056 min: 0,
3057 max: 0,
3058 condid: 0,
3059 module: None,
3060 };
3061 deleteconddef(&probe)
3062 }
3063
3064 /// Get condition definition (from module.c getconddef)
3065 /// Port of `getconddef(int inf, const char *name, int autol)` from `Src/module.c:648`.
3066 /// WARNING: param names don't match C — Rust=(name) vs C=(inf, name, autol)
3067 ///
3068 /// Returns the autoload mapping if any. C consults the canonical
3069 /// `condtab` first; the autoload table is the fallback.
3070 pub fn getconddef(&self, name: &str) -> Option<&str> {
3071 self.autoload_conditions.get(name).map(|s| s.as_str())
3072 }
3073
3074 /// Register autoloading condition.
3075 /// Port of `static int add_autocond(const char *module, const char
3076 /// *cnam, int flags)` from `Src/module.c:792`. C body allocates a
3077 /// `Conddef`, copies name/module, sets `CONDF_INFIX` if
3078 /// `FEAT_INFIX` and `CONDF_AUTOALL` if `FEAT_AUTOALL`, then calls
3079 /// `addconddef(c)`. On addconddef failure (already exists) the
3080 /// node is freed; returns 1 unless `FEAT_IGNORE`.
3081 /// WARNING: param names don't match C — Rust=(name, module, flags) vs C=(module, cnam, flags)
3082 pub fn add_autocond(&mut self, name: &str, module: &str, flags: i32) -> i32 {
3083 // c:792
3084 // Faithful port of c:792-810:
3085 // Conddef c = zshcalloc(sizeof(*c));
3086 // c->name = ztrdup(cnam);
3087 // c->module = ztrdup(module);
3088 // c->flags = (flags & FEAT_INFIX) ? CONDF_INFIX : 0;
3089 // if (flags & FEAT_AUTOALL) c->flags |= CONDF_AUTOALL;
3090 // if (addconddef(c)) {
3091 // zsfree(c->name); zsfree(c->module); zfree(c, sizeof(*c));
3092 // if (!(flags & FEAT_IGNORE)) return 1;
3093 // }
3094 // return 0;
3095 //
3096 // Prior port did a ledger-only insert into autoload_conditions
3097 // without ever touching the canonical CONDTAB. Now constructs
3098 // the conddef struct and routes through the free addconddef
3099 // (1395acf3a7 dependency) which walks CONDTAB for clashes,
3100 // replaces autoload entries, and prepends on success.
3101 let mut cflags: i32 = if (flags & FEAT_INFIX) != 0 {
3102 CONDF_INFIX // c:799
3103 } else {
3104 0
3105 };
3106 if (flags & FEAT_AUTOALL) != 0 {
3107 cflags |= CONDF_AUTOALL; // c:801
3108 }
3109 // c:796-803 — populate the conddef record. `handler` is None
3110 // because autoload stubs don't carry the dispatch fn until
3111 // the module loads and addconddef replaces the entry.
3112 let cd = conddef {
3113 next: None,
3114 name: name.to_string(),
3115 flags: cflags,
3116 handler: None,
3117 min: 0,
3118 max: 0,
3119 condid: 0,
3120 module: Some(module.to_string()),
3121 };
3122 // c:804 — addconddef(c).
3123 if addconddef(cd) != 0 {
3124 // c:805-807 — zsfree/zfree happen via Rust drop.
3125 if (flags & FEAT_IGNORE) == 0 {
3126 return 1; // c:810
3127 }
3128 }
3129 // Keep the ledger entry too so resolve_autoload_condition()
3130 // and del_autocond's fast path can find it without re-scanning
3131 // CONDTAB.
3132 self.autoload_conditions
3133 .insert(name.to_string(), module.to_string());
3134 0 // c:812
3135 }
3136
3137 /// Port of `static int del_autocond(const char *modnam, const char *cnam,
3138 /// int flags)` from `Src/module.c:819`.
3139 ///
3140 /// C body (c:819-835):
3141 /// ```c
3142 /// Conddef cd = getconddef((flags & FEAT_INFIX) ? 1 : 0, cnam, 0);
3143 /// if (!cd) { if (!(flags & FEAT_IGNORE)) return 2; }
3144 /// else if (cd->flags & CONDF_ADDED) {
3145 /// if (!(flags & FEAT_IGNORE)) return 3;
3146 /// } else deleteconddef(cd);
3147 /// return 0;
3148 /// ```
3149 ///
3150 /// 2 = "no such condition", 3 = "registered condition (CONDF_ADDED) —
3151 /// can't unload", 0 = success. FEAT_IGNORE masks both error returns.
3152 /// WARNING: param names don't match C — Rust=(name, flags) vs C=(modnam, cnam, flags)
3153 pub fn del_autocond(&mut self, name: &str, flags: i32) -> i32 {
3154 // c:819
3155 // Faithful port of c:819-835:
3156 // Conddef cd = getconddef((flags & FEAT_INFIX) ? 1 : 0,
3157 // cnam, 0);
3158 // if (!cd) {
3159 // if (!(flags & FEAT_IGNORE)) return 2;
3160 // } else if (cd->flags & CONDF_ADDED) {
3161 // if (!(flags & FEAT_IGNORE)) return 3;
3162 // } else
3163 // deleteconddef(cd);
3164 // return 0;
3165 //
3166 // Prior port skipped CONDTAB entirely; checked the ledger
3167 // only. Now routes through getconddef (1395acf3a7) which
3168 // walks CONDTAB filtered by infix-flag, then deleteconddef
3169 // (free fn) which removes the matched entry.
3170 let inf = if (flags & FEAT_INFIX) != 0 { 1 } else { 0 };
3171 // c:821 — `getconddef(inf, cnam, 0)`.
3172 let cd = getconddef(inf, name, 0, self);
3173 match cd {
3174 None => {
3175 // c:823-825 — !cd: return 2 unless FEAT_IGNORE.
3176 if (flags & FEAT_IGNORE) == 0 {
3177 return 2; // c:825
3178 }
3179 0 // c:834
3180 }
3181 Some(ref entry) if (entry.flags & CONDF_ADDED) != 0 => {
3182 // c:826-828 — CONDF_ADDED set: real registered
3183 // condition, can't unload via del_auto*. Return 3
3184 // unless FEAT_IGNORE.
3185 if (flags & FEAT_IGNORE) == 0 {
3186 return 3; // c:828
3187 }
3188 0
3189 }
3190 Some(ref entry) => {
3191 // c:831-832 — deleteconddef(cd); drop ledger too.
3192 let _ = deleteconddef(entry);
3193 self.autoload_conditions.remove(name);
3194 0 // c:834
3195 }
3196 }
3197 }
3198
3199 // ------- Hook management lives in the file-static free ported above ------
3200 //
3201 // C `hooktab` (Src/module.c:843) is a file-static `Hookdef` linked
3202 // list, NOT a member of `ModuleTable` (which is the `modulestab`
3203 // HashTable of Module nodes at c:Modules/zmodload.c:32). Hook ops
3204 // are free ported operating on the file-static `hooktab` (above):
3205 // `gethookdef`, `addhookdef`, `addhookdefs`, `deletehookdef`,
3206 // `deletehookdefs`, `addhookdeffunc`, `addhookfunc`,
3207 // `deletehookdeffunc`, `deletehookfunc`, `runhookdef`.
3208
3209 // ------- Parameter management (from module.c addparamdef/deleteparamdef) -------
3210
3211 /// Add or remove sets of parameters; same shape as `setbuiltins`.
3212 /// Port of `static int setparamdefs(char const *nam, Paramdef d,
3213 /// int size, int *e)` from `Src/module.c:1170`. For each Paramdef
3214 /// in `d[0..size]`: if `e[n]` is set and `d->pm` is null, add the
3215 /// param via `addparamdef(d)`; if `e[n]` is clear and `d->pm` is
3216 /// non-null, remove via `deleteparamdef(d)`. Warnings on
3217 /// error/already-deleted; returns 1 if any op failed.
3218 /// WARNING: param names don't match C — Rust=(module, names, e) vs C=(nam, d, size, e)
3219 pub fn setparamdefs(&mut self, module: &str, names: &[&str], e: Option<&[i32]>) -> i32 {
3220 // c:1165
3221 // Faithful port of c:1165-1192. Prior Rust port skipped the
3222 // diagnostics + ret-tracking C does on failure:
3223 //
3224 // if (e && *e++) { // add branch
3225 // if (d->pm) continue; // already registered
3226 // if (addparamdef(d)) {
3227 // zwarnnam(nam,
3228 // "error when adding parameter `%s'", d->name);
3229 // ret = 1;
3230 // }
3231 // } else { // del branch
3232 // if (!d->pm) continue; // not registered
3233 // if (deleteparamdef(d)) {
3234 // zwarnnam(nam,
3235 // "parameter `%s' already deleted", d->name);
3236 // ret = 1;
3237 // }
3238 // }
3239 //
3240 // Maps to the autoload_params ledger (zshrs-only structural
3241 // equivalent of d->pm presence). Diagnostics now fire when
3242 // the ledger insert/remove conflicts with the C-side
3243 // contract (probe + retry on the canonical paramtab to
3244 // observe the same clash behaviour addparamdef would).
3245 use crate::ported::params::paramtab;
3246 let mut ret: i32 = 0; // c:1167
3247 for (n, name) in names.iter().enumerate() {
3248 // c:1169 — `while (size--)`
3249 let enable = e
3250 .map(|arr| arr.get(n).copied().unwrap_or(0)) // c:1170 *e++
3251 .unwrap_or(1);
3252 // c:1171 / c:1180 — `if (d->pm)` / `if (!d->pm)`.
3253 // Static-link analog: autoload_params contains the name
3254 // when the module has registered it as a paramdef.
3255 let already = self.autoload_params.contains_key(*name);
3256 if enable != 0 {
3257 // c:1170 add branch
3258 if already {
3259 continue; // c:1172
3260 }
3261 // c:1175 — `if (addparamdef(d))`. Faithful path:
3262 // probe the canonical paramtab for a clash; if the
3263 // name already exists with a non-MOD-derived param,
3264 // mirror addparamdef's failure.
3265 let canonical_clash = paramtab()
3266 .read()
3267 .ok()
3268 .map(|t| t.contains_key(*name))
3269 .unwrap_or(false);
3270 if canonical_clash {
3271 // c:1176-1178 — `zwarnnam('error when adding ...')`.
3272 zwarnnam(module, &format!("error when adding parameter `{}'", name));
3273 ret = 1; // c:1177
3274 continue;
3275 }
3276 // c:1181 — register the ledger entry on success.
3277 self.autoload_params
3278 .insert(name.to_string(), module.to_string());
3279 } else {
3280 // c:1179 del branch
3281 if !already {
3282 continue; // c:1181
3283 }
3284 // c:1184 — `if (deleteparamdef(d))`. With the ledger
3285 // hit, the canonical removal succeeds; emit the
3286 // C-equivalent diagnostic only if the paramtab probe
3287 // says the entry got tampered with externally
3288 // (already-deleted from paramtab while still on the
3289 // ledger).
3290 let canonical_present = paramtab()
3291 .read()
3292 .ok()
3293 .map(|t| t.contains_key(*name))
3294 .unwrap_or(false);
3295 self.autoload_params.remove(*name);
3296 if !canonical_present {
3297 // c:1185-1187 — `parameter `%s' already deleted`.
3298 zwarnnam(module, &format!("parameter `{}' already deleted", name));
3299 ret = 1; // c:1186
3300 }
3301 }
3302 }
3303 ret // c:1191
3304 }
3305
3306 /// Register autoloading parameter.
3307 /// Port of `static int add_autoparam(const char *module, const char
3308 /// *pnam, int flags)` from `Src/module.c:1198`. C body:
3309 /// `checkaddparam()` clash check (returns 2 if `-i`'d), then
3310 /// `setsparam(pnam, module)` creating the param with `PM_AUTOLOAD`
3311 /// (+ `PM_AUTOALL` if `FEAT_AUTOALL`). `queue_signals`/`noerrs=2`
3312 /// bracket so the setsparam doesn't echo errors out.
3313 /// WARNING: param names don't match C — Rust=(name, module, flags) vs C=(module, pnam, flags)
3314 pub fn add_autoparam(&mut self, name: &str, module: &str, flags: i32) -> i32 {
3315 // c:1198
3316 // Faithful port of c:1198-1228 routes through the free
3317 // add_autoparam fn (293c041e2f) which already does the full
3318 // checkaddparam + setsparam + PM_AUTOLOAD (+PM_AUTOALL) +
3319 // noerrs/queue_signals bracket.
3320 //
3321 // Prior method was a ledger-only HashMap insert that did the
3322 // queue_signals dance without ever calling setsparam — so
3323 // PM_AUTOLOAD never landed on the canonical paramtab entry,
3324 // and `typeset +` wouldn't see the 'undefined NAME' shape.
3325 //
3326 // The free fn returns 0 / -1 (vs C's 0 / 1 / -1) — preserve
3327 // the existing method-level contract by mapping -1 → 1 below.
3328 let r = add_autoparam(module, name, flags); // c:1198
3329 if r != 0 {
3330 // c:1213-1219 — error: 2 (FEAT_IGNORE soft-fail) maps to
3331 // 0 here per the prior method contract.
3332 if (flags & FEAT_IGNORE) != 0 {
3333 return 0;
3334 }
3335 return r; // -1 → -1
3336 }
3337 // c:1218-1221 success: also keep the ledger entry up so the
3338 // resolve_autoload_param fast path stays consistent.
3339 self.autoload_params
3340 .insert(name.to_string(), module.to_string());
3341 0
3342 }
3343
3344 /// Port of `static int del_autoparam(const char *modnam, const char *pnam,
3345 /// int flags)` from `Src/module.c:1240`.
3346 ///
3347 /// C body (c:1240-1255):
3348 /// ```c
3349 /// Param pm = (Param) gethashnode2(paramtab, pnam);
3350 /// if (!pm) { if (!(flags & FEAT_IGNORE)) return 2; }
3351 /// else if (!(pm->node.flags & PM_AUTOLOAD)) {
3352 /// if (!(flags & FEAT_IGNORE)) return 3;
3353 /// } else unsetparam_pm(pm, 0, 1);
3354 /// return 0;
3355 /// ```
3356 ///
3357 /// 2 = "no such param", 3 = "real param (not autoload) — can't
3358 /// unload", 0 = success. FEAT_IGNORE masks both error returns.
3359 /// WARNING: param names don't match C — Rust=(name, flags) vs C=(modnam, pnam, flags)
3360 pub fn del_autoparam(&mut self, name: &str, flags: i32) -> i32 {
3361 // c:1234
3362 // Faithful port of c:1234-1248 routes through the free
3363 // del_autoparam fn (293c041e2f) which does the canonical
3364 // paramtab probe + PM_AUTOLOAD gate + unsetparam_pm.
3365 //
3366 // Prior method walked paramtab directly and emulated the
3367 // PM_AUTOLOAD gate inline; routing through the free fn keeps
3368 // the logic single-sourced so PM_AUTOLOAD semantics evolve
3369 // in one place. Ledger cleanup preserved on the success path.
3370 let r = del_autoparam("", name, flags); // c:1234 (modnam unused per UNUSED())
3371 if r == 0 {
3372 // c:1246 — `unsetparam_pm` already ran via the free fn;
3373 // also drop the ledger entry so resolve_autoload_param
3374 // stops returning the stale module name.
3375 self.autoload_params.remove(name);
3376 }
3377 r
3378 }
3379
3380 // ------- Feature enable/disable (from module.c features_/enables_) -------
3381
3382 /// Enable a feature (from module.c enables_)
3383 ///
3384 /// Without a per-module feature ledger, enable/disable maps onto
3385 /// the canonical builtin/conddef/paramdef tables. Returns true if
3386 /// the module itself is registered. The actual per-feature
3387 /// enabled-bit lives on the canonical record (e.g. `Builtin.flags`
3388 /// `BINF_DISABLED`).
3389 pub fn enable_feature(&mut self, module: &str, _name: &str) -> bool {
3390 self.modules.contains_key(module)
3391 }
3392
3393 /// Disable a feature
3394 pub fn disable_feature(&mut self, module: &str, _name: &str) -> bool {
3395 self.modules.contains_key(module)
3396 }
3397
3398 /// List feature *names* of a module (from module.c features_).
3399 /// Without a per-module ledger, this returns an empty list — C
3400 /// computes feature names by walking the canonical tables for
3401 /// entries that name the given module. Callers that care use
3402 /// `features_module`/`features_` directly.
3403 pub fn list_features(&self, _module: &str) -> Vec<String> {
3404 Vec::new()
3405 }
3406
3407 /// Check if a module is linked (statically compiled) (from module.c module_linked)
3408 /// Port of `module_linked(char const *name)` from `Src/module.c:385`.
3409 ///
3410 /// C body walks `linkedmodules` comparing `->name`; it never looks
3411 /// at `modulestab`. A module can be linked with no `modulestab`
3412 /// node (the normal state before its first `zmodload`) and can have
3413 /// a node while not linked (an alias, or an `add_dep`/`zmodload -ab`
3414 /// bookkeeping node).
3415 pub fn module_linked(&self, name: &str) -> bool {
3416 // c:389-391 — for (node = firstnode(linkedmodules); …)
3417 self.linkedmodules.iter().any(|n| n == name)
3418 }
3419
3420 /// Resolve autoload — find which module provides a builtin
3421 pub fn resolve_autoload_builtin(&self, name: &str) -> Option<&str> {
3422 self.autoload_builtins.get(name).map(|s| s.as_str())
3423 }
3424
3425 /// Resolve autoload — find which module provides a parameter
3426 pub fn resolve_autoload_param(&self, name: &str) -> Option<&str> {
3427 self.autoload_params.get(name).map(|s| s.as_str())
3428 }
3429
3430 /// Ensure a module's feature is available
3431 /// Port of `ensurefeature(const char *modname, const char *prefix, const char *feature)` from `Src/module.c:3415`.
3432 /// WARNING: param names don't match C — Rust=(module, feature) vs C=(modname, prefix, feature)
3433 pub fn ensurefeature(&mut self, module: &str, feature: &str) -> bool {
3434 if !self.is_loaded(module) {
3435 self.load_module(module, None, false);
3436 }
3437 self.is_loaded(module)
3438 }
3439}
3440
3441/// Module lifecycle callbacks (from module.c setup_/getrandom_buffer/cleanup_/finish_)
3442/// Lifecycle hooks every module must implement.
3443/// Port of the `setup_`/`features_`/`enables_`/`getrandom_buffer`/`cleanup_`
3444/// /`finish_` entry points every C module exposes (Src/module.c
3445/// lines 306-345 illustrate the canonical no-op set). Rust
3446/// modules implement this trait directly.
3447pub trait ModuleLifecycle {
3448 fn setup(&mut self) -> i32 {
3449 0
3450 }
3451 fn boot(&mut self) -> i32 {
3452 0
3453 }
3454 fn cleanup(&mut self) -> i32 {
3455 0
3456 }
3457 fn finish(&mut self) -> i32 {
3458 0
3459 }
3460}
3461
3462/// Port of `getmathfunc()` from `Src/module.c:1283`. — C decl `getmathfunc(const char *name, int autol)`.
3463///
3464/// C body: linear-search `mathfuncs` for `name`; if found and `autol`
3465/// is true and the entry is autoloadable, demand-load via
3466/// `ensurefeature("f:", name)`. Returns the resolved entry or NULL.
3467///
3468/// Rust port returns `Some(module_name)` on hit, `None` on miss.
3469///
3470/// Faithful port of c:1283-1306:
3471/// ```c
3472/// MathFunc
3473/// getmathfunc(const char *name, int autol)
3474/// {
3475/// MathFunc p, q = NULL;
3476/// for (p = mathfuncs; p; q = p, p = p->next)
3477/// if (!strcmp(name, p->name)) {
3478/// if (autol && p->module && !(p->flags & MFF_USERFUNC)) {
3479/// char *n = dupstring(p->module);
3480/// int flags = p->flags;
3481/// removemathfunc(q, p);
3482/// (void)ensurefeature(n, "f:",
3483/// (flags & MFF_AUTOALL) ? NULL : name);
3484/// p = getmathfunc(name, 0);
3485/// if (!p) {
3486/// zerr("autoloading module %s failed to define math function: %s", n, name);
3487/// }
3488/// }
3489/// return p;
3490/// }
3491/// return NULL;
3492/// }
3493/// ```
3494///
3495/// C walks `mathfuncs` (file-static linked list) — Rust walks
3496/// MATHFUNCS (same shape, Vec). The MFF_USERFUNC filter
3497/// (`functions -M` user-defined math) keeps user fns from being
3498/// autoloaded out from under the caller. The autoload arm:
3499/// 1. snapshot module name + flags
3500/// 2. removemathfunc(name) — drop the autoload stub
3501/// 3. ensurefeature(module, "f:", AUTOALL?NULL:name) — load real
3502/// 4. re-query mathfuncs (autol=0) — return the loaded entry
3503/// 5. zerr if still missing (load failed to populate the table)
3504/// WARNING: param names don't match C — Rust=(table, name, autol) vs C=(name, autol)
3505pub fn getmathfunc(table: &mut modulestab, name: &str, autol: i32) -> Option<String> {
3506 // c:1283
3507 // c:1287-1288 — `for (p = mathfuncs; p; q = p, p = p->next)`:
3508 // walk MATHFUNCS for the first name match. Snapshot under the
3509 // lock so we can release before mutating (autoload arm calls
3510 // ensurefeature which can re-enter MATHFUNCS via addmathfunc).
3511 let hit: Option<(String, i32, bool)> = {
3512 let tab = MATHFUNCS.lock().unwrap();
3513 tab.iter().find_map(|p| {
3514 if p.name == name {
3515 Some((
3516 p.module.clone().unwrap_or_default(),
3517 p.flags,
3518 p.module.is_some(),
3519 ))
3520 } else {
3521 None
3522 }
3523 })
3524 };
3525 let (module, flags, has_module) = match hit {
3526 Some(t) => t,
3527 None => return None, // c:1306 `return NULL;`
3528 };
3529 // c:1289 — `if (autol && p->module && !(p->flags & MFF_USERFUNC))`.
3530 if autol != 0 && has_module && (flags & MFF_USERFUNC) == 0 {
3531 // c:1290-1291 — snapshot already done above.
3532 // c:1293 — `removemathfunc(q, p)`: drop the stub before load.
3533 removemathfunc(name);
3534 // c:1295-1296 — `ensurefeature(n, "f:", AUTOALL?NULL:name)`.
3535 let feature_arg = if (flags & crate::ported::zsh_h::MFF_AUTOALL) != 0 {
3536 None
3537 } else {
3538 Some(name)
3539 };
3540 let _ = ensurefeature(table, &module, "f:", feature_arg);
3541 // c:1298 — `p = getmathfunc(name, 0);` recurse w/o autol.
3542 // EXISTENCE check: C zerrs only when the re-lookup returns
3543 // NULL. A freshly registered real entry has module == NULL
3544 // (mftab entries, zsh.h:133 NUMMATHFUNC), so mapping the hit
3545 // through `p.module.clone()` (None for real entries) wrongly
3546 // reported a successful load as "failed to define". Mirror the
3547 // non-autoload tail below: empty string marks a module-less
3548 // hit.
3549 let after = {
3550 let tab = MATHFUNCS.lock().unwrap();
3551 tab.iter().find_map(|p| {
3552 if p.name == name {
3553 Some(p.module.clone().unwrap_or_default())
3554 } else {
3555 None
3556 }
3557 })
3558 };
3559 // c:1299-1301 — `if (!p) zerr(...)`.
3560 if after.is_none() {
3561 crate::ported::utils::zerr(&format!(
3562 "autoloading module {} failed to define math function: {}",
3563 module, name
3564 ));
3565 }
3566 return after; // c:1303 `return p;`
3567 }
3568 // c:1303 — non-autoload (or user-fn) hit: return the entry.
3569 if has_module {
3570 Some(module)
3571 } else {
3572 // User-fn entries (`functions -M`) have no module — return
3573 // an empty string so the caller still observes the hit.
3574 Some(String::new())
3575 }
3576}
3577
3578/// Port of `add_automathfunc()` from `Src/module.c:1410`. — C decl `add_automathfunc(const char *module, const char *fnam, int flags)`.
3579///
3580/// C body:
3581/// ```c
3582/// add_automathfunc(const char *module, const char *fnam, int flags) {
3583/// MathFunc f = zalloc(sizeof(*f));
3584/// f->name = ztrdup(fnam);
3585/// f->module = ztrdup(module);
3586/// f->flags = 0;
3587/// if (addmathfunc(f)) {
3588/// zsfree(f->name); zsfree(f->module); zfree(f, sizeof(*f));
3589/// if (!(flags & FEAT_IGNORE))
3590/// return 1;
3591/// }
3592/// return 0;
3593/// }
3594/// ```
3595///
3596/// Registers `fnam` as an autoloadable math function provided by `module`.
3597/// WARNING: param names don't match C — Rust=(table, module, fnam, flags) vs C=(module, fnam, flags)
3598pub fn add_automathfunc(table: &mut modulestab, module: &str, fnam: &str, flags: i32) -> i32 {
3599 // c:1410
3600 // Faithful port of c:1410-1429:
3601 // MathFunc f = zalloc(sizeof(*f));
3602 // f->name = ztrdup(fnam);
3603 // f->module = ztrdup(module);
3604 // f->flags = 0;
3605 // if (addmathfunc(f)) {
3606 // zsfree(f->name); zsfree(f->module); zfree(f, sizeof(*f));
3607 // if (!(flags & FEAT_IGNORE)) return 1;
3608 // }
3609 // return 0;
3610 //
3611 // Prior port did ledger-only autoload_mathfuncs.insert without
3612 // ever touching the canonical MATHFUNCS table. Now constructs
3613 // the mathfunc struct and routes through the free addmathfunc
3614 // (c:1313) which walks MATHFUNCS for clashes and replaces
3615 // autoloadable entries.
3616 let f = mathfunc {
3617 next: None,
3618 name: fnam.to_string(),
3619 flags: 0, // c:1417 — autoload entries don't carry MFF_ADDED
3620 nfunc: None,
3621 sfunc: None,
3622 module: Some(module.to_string()),
3623 minargs: 0,
3624 maxargs: 0,
3625 funcid: 0,
3626 };
3627 // c:1420 — `if (addmathfunc(f))` clash gate.
3628 if addmathfunc(f) != 0 {
3629 // c:1421-1424 — free happens via Rust drop on the returned-
3630 // by-value f going out of scope.
3631 if (flags & FEAT_IGNORE) == 0 {
3632 return 1; // c:1426
3633 }
3634 // c:1427 — FEAT_IGNORE: fall through to success but skip
3635 // the ledger insert (the canonical table already has this).
3636 return 0;
3637 }
3638 // c:1429 success path: register in the autoload ledger.
3639 table
3640 .autoload_mathfuncs
3641 .insert(fnam.to_string(), module.to_string());
3642 0
3643}
3644
3645/// Port of `del_automathfunc()` from `Src/module.c:1436`. — C decl `del_automathfunc(UNUSED(const char *modnam), const char *fnam, int flags)`.
3646///
3647/// C body:
3648/// ```c
3649/// del_automathfunc(UNUSED(const char *modnam), const char *fnam, int flags) {
3650/// MathFunc f = getmathfunc(fnam, 0);
3651/// if (!f) {
3652/// if (!(flags & FEAT_IGNORE)) return 2;
3653/// } else if (f->flags & MFF_ADDED) {
3654/// if (!(flags & FEAT_IGNORE)) return 3;
3655/// } else
3656/// deletemathfunc(f);
3657/// return 0;
3658/// }
3659/// ```
3660///
3661/// Removes `fnam` from the autoloadable math-function registry.
3662/// WARNING: param names don't match C — Rust=(table, _modnam, fnam, flags) vs C=(modnam, fnam, flags)
3663pub fn del_automathfunc(table: &mut modulestab, _modnam: &str, fnam: &str, flags: i32) -> i32 {
3664 // c:1436
3665 // Faithful port of c:1436-1449:
3666 // MathFunc f = getmathfunc(fnam, 0);
3667 // if (!f) { if (!(flags & FEAT_IGNORE)) return 2; }
3668 // else if (f->flags & MFF_ADDED) {
3669 // if (!(flags & FEAT_IGNORE)) return 3;
3670 // } else deletemathfunc(f);
3671 // return 0;
3672 //
3673 // Prior port skipped the MFF_ADDED gate at c:1444 entirely.
3674 // That meant `zmodload -ufd` on a real (module-registered)
3675 // math function silently succeeded instead of returning 3 like
3676 // C does, dropping the user's actual function out from under
3677 // them. Now uses getmathfunc (fd1ec84bab) to find the entry
3678 // and checks MFF_ADDED before removal.
3679
3680 // c:1440 — `getmathfunc(fnam, 0)`. autol=0 since we don't want
3681 // the autoload trigger to fire during a deletion query.
3682 let entry = getmathfunc(table, fnam, 0);
3683 match entry {
3684 None => {
3685 // c:1441-1442 — `if (!f) { if (!FEAT_IGNORE) return 2; }`
3686 if (flags & FEAT_IGNORE) == 0 {
3687 return 2;
3688 }
3689 0
3690 }
3691 Some(_) => {
3692 // c:1443 — `else if (f->flags & MFF_ADDED)`.
3693 // Look up the entry in MATHFUNCS to read its flags
3694 // (getmathfunc returns the module string, not the
3695 // mathfunc struct).
3696 let added = {
3697 let tab = MATHFUNCS.lock().unwrap();
3698 tab.iter()
3699 .find(|m| m.name == fnam)
3700 .map(|m| (m.flags & MFF_ADDED) != 0)
3701 .unwrap_or(false)
3702 };
3703 if added {
3704 // c:1444-1445 — real registered, can't unload via
3705 // del_auto*. Return 3 unless FEAT_IGNORE.
3706 if (flags & FEAT_IGNORE) == 0 {
3707 return 3;
3708 }
3709 return 0;
3710 }
3711 // c:1447 — deletemathfunc(f). Use removemathfunc
3712 // (which deletemathfunc delegates to in the autoload
3713 // path) + drop the ledger entry.
3714 removemathfunc(fnam);
3715 table.autoload_mathfuncs.remove(fnam);
3716 0
3717 }
3718 }
3719}
3720
3721/// Port of `load_and_bind()` from `Src/module.c:1468`. — C decl `load_and_bind(const char *fn)`.
3722///
3723/// C body: AIX-only `load() + loadbind()` wrapper. Iterates the
3724/// `modulestab` hash table, binding each loaded module's handle to
3725/// the new module's symbols. On loadbind failure, calls `unload()`
3726/// and stores the error in `dlerrstr`.
3727///
3728/// Static-link path: dlopen/dlsym aren't used since modules are
3729/// linked at compile time. Returns 0 (NULL handle).
3730/// WARNING: param names don't match C — Rust=(_fn_path) vs C=(fn)
3731pub fn load_and_bind(_fn_path: &str) -> usize {
3732 // c:1468
3733 0 // c:1492 NULL
3734}
3735
3736/// Port of `hpux_dlsym()` from `Src/module.c:1530`. — C decl `hpux_dlsym(void *handle, char *name)`.
3737///
3738/// C body:
3739/// ```c
3740/// hpux_dlsym(void *handle, char *name)
3741/// {
3742/// void *sym_addr;
3743/// if (!shl_findsym((shl_t *)&handle, name, TYPE_UNDEFINED, &sym_addr))
3744/// return sym_addr;
3745/// return NULL;
3746/// }
3747/// ```
3748///
3749/// HP-UX-specific dlsym wrapper around `shl_findsym(3)`. Static-link
3750/// path: never invoked since zshrs doesn't dlopen modules.
3751#[allow(unused_variables)]
3752pub fn hpux_dlsym(handle: usize, name: &str) -> usize {
3753 // c:1530
3754 0 // c:1530 NULL
3755}
3756
3757/// Port of `try_load_module()` from `Src/module.c:1583`. — C decl `try_load_module(char const *name)`.
3758///
3759/// C body iterates `module_path` looking for a loadable file via
3760/// `dlopen`. Static-link path: a module is "loadable" iff it's in
3761/// our static `ModuleTable.modules` map.
3762/// WARNING: param names don't match C — Rust=(table, name) vs C=(name)
3763pub fn try_load_module(table: &modulestab, name: &str) -> i32 {
3764 // c:1583
3765 // C dlopens the module path (or falls back to module_linked for
3766 // compiled-in modules). Static-link analog: the name must be on
3767 // `linkedmodules` (c:385 `module_linked`) — zshrs has no DSOs, so
3768 // "the file exists and dlopens" is exactly "the module is compiled
3769 // in". Deliberately NOT a `modulestab` membership test: a bare
3770 // FINDMOD_CREATE bookkeeping node (flags=0 per C's zshcalloc at
3771 // c:1676) has no backing code, so `zmodload -ab zsh/bogus x; x`
3772 // must still emit `failed to load module` rather than "boot" the
3773 // phantom; and a real module has no node at all until its first
3774 // load, which must still succeed.
3775 if table.module_linked(name) {
3776 1
3777 } else {
3778 0
3779 }
3780}
3781
3782/// Port of `do_load_module()` from `Src/module.c:1610`. — C decl `do_load_module(char const *name, int silent)`.
3783///
3784/// C body:
3785/// ```c
3786/// do_load_module(char const *name, int silent)
3787/// {
3788/// void *ret;
3789/// ret = try_load_module(name);
3790/// if (!ret && !silent) {
3791/// zwarn("failed to load module `%s': %s", name, ...);
3792/// }
3793/// return ret;
3794/// }
3795/// ```
3796///
3797/// C returns `void *` (the dlopen handle); Rust port returns 0 on
3798/// success / 1 on failure. zshrs's static-link path: `try_load_module`
3799/// always succeeds for built-in modules. Returns 1 + zwarn on miss.
3800/// WARNING: param names don't match C — Rust=(table, name, silent) vs C=(name, silent)
3801pub fn do_load_module(table: &mut modulestab, name: &str, silent: i32) -> i32 {
3802 // c:1610
3803 // c:1610 — ret = try_load_module(name);
3804 let ret = try_load_module(table, name);
3805 if ret == 0 && silent == 0 {
3806 // c:1615
3807 // c:1622 `zwarn("failed to load module `%s': %s", name, dlerror())`.
3808 // The dlerror TAIL is deliberately omitted: zshrs's modules are
3809 // statically linked, so nothing was dlopened and there is no real
3810 // diagnostic to report — synthesising one would be a fabricated
3811 // message, not a port. Prefix + rc are the pinned contract
3812 // (docs/BUGS.md #376, `zmodload_nonexistent_diagnostic` in
3813 // tests/parity/modules_parity.rs). Both emit sites must agree.
3814 zwarn(&format!("failed to load module `{}'", name));
3815 }
3816 ret // c:1624
3817}
3818
3819/// Port of `find_module()` from `Src/module.c:1659`. — C decl `find_module(const char *name, int flags, const char **namep)`.
3820///
3821/// C body:
3822/// ```c
3823/// find_module(const char *name, int flags, const char **namep)
3824/// {
3825/// Module m;
3826/// m = (Module)modulestab->getnode2(modulestab, name);
3827/// if (m) {
3828/// if ((flags & FINDMOD_ALIASP) && (m->node.flags & MOD_ALIAS)) {
3829/// if (namep) *namep = m->u.alias;
3830/// return find_module(m->u.alias, flags, namep);
3831/// }
3832/// if (namep) *namep = m->node.nam;
3833/// return m;
3834/// }
3835/// if (!(flags & FINDMOD_CREATE))
3836/// return NULL;
3837/// m = zshcalloc(sizeof(*m));
3838/// modulestab->addnode(modulestab, ztrdup(name), m);
3839/// return m;
3840/// }
3841/// ```
3842///
3843/// Returns the resolved module name (after alias chasing) and
3844/// whether an entry was created. C's `Module` return becomes
3845/// `Option<String>` of the canonical name.
3846/// WARNING: param names don't match C — Rust=(table, name, flags) vs C=(name, flags, namep)
3847pub fn find_module(table: &mut modulestab, name: &str, flags: i32) -> Option<String> {
3848 // c:1659
3849 // c:1659 — m = modulestab->getnode2(modulestab, name);
3850 let mut cur_name = name.to_string();
3851 let mut depth = 0;
3852 loop {
3853 if depth > 64 {
3854 return None;
3855 } // alias-cycle guard
3856 depth += 1;
3857 match table.modules.get(&cur_name) {
3858 Some(m) => {
3859 // c:1665 — if ((flags & FINDMOD_ALIASP) && (m->node.flags & MOD_ALIAS))
3860 if (flags & FINDMOD_ALIASP) != 0 && (m.node.flags & MOD_ALIAS) != 0 {
3861 // c:1668 — return find_module(m->u.alias, flags, namep);
3862 if let Some(target) = m.alias.clone() {
3863 cur_name = target;
3864 continue;
3865 }
3866 return None;
3867 }
3868 // c:1671 — *namep = m->node.nam; return m;
3869 return Some(cur_name);
3870 }
3871 None => {
3872 // c:1674 — if (!(flags & FINDMOD_CREATE)) return NULL;
3873 if (flags & FINDMOD_CREATE) == 0 {
3874 return None;
3875 }
3876 // c:1676-1677 — m = zshcalloc(...); addnode(name, m);
3877 // zshcalloc zero-fills: the created node has flags=0
3878 // and NULL handle/linked — a bookkeeping entry (alias
3879 // targets, autoload owners), NOT a loadable module.
3880 // module::new() seeds MOD_LINKED (the register_module
3881 // shape, c:359); that bit wrongly made phantom nodes
3882 // (`zmodload -ab zsh/bogus x`) pass try_load_module's
3883 // loadable gate and "boot" successfully.
3884 let mut m = module::new(&cur_name);
3885 m.node.flags = 0; // c:1676 zshcalloc — all-zero node
3886 table.modules.insert(cur_name.clone(), m);
3887 return Some(cur_name);
3888 }
3889 }
3890 }
3891}
3892
3893/// Port of `delete_module()` from `Src/module.c:1687`. — C decl `delete_module(Module m)`.
3894///
3895/// C body:
3896/// ```c
3897/// delete_module(Module m) {
3898/// modulestab->removenode(modulestab, m->node.nam);
3899/// modulestab->freenode(&m->node);
3900/// }
3901/// ```
3902///
3903/// Removes a module from the live `modulestab` and frees its node.
3904/// Rust port operates on the `ModuleTable` `modules` HashMap.
3905/// WARNING: param names don't match C — Rust=(table, name) vs C=(m)
3906pub fn delete_module(table: &mut modulestab, name: &str) -> i32 {
3907 // c:1687
3908 table.modules.remove(name); // c:1687 removenode
3909 // c:1691 — freenode(&m->node) — Rust drops on `remove` return.
3910 0
3911}
3912
3913/// Port of `module_loaded()` from `Src/module.c:1703`. — C decl `module_loaded(const char *name)`.
3914///
3915/// C body:
3916/// ```c
3917/// module_loaded(const char *name)
3918/// {
3919/// Module m;
3920/// return ((m = find_module(name, FINDMOD_ALIASP, NULL)) &&
3921/// m->u.handle &&
3922/// !(m->node.flags & MOD_UNLOAD));
3923/// }
3924/// ```
3925///
3926/// Returns true (non-zero) if the named module is currently loaded.
3927///
3928/// Faithful port of c:1702-1710:
3929/// ```c
3930/// mod_export int
3931/// module_loaded(const char *name)
3932/// {
3933/// Module m;
3934/// return ((m = find_module(name, FINDMOD_ALIASP, NULL)) &&
3935/// m->u.handle &&
3936/// !(m->node.flags & MOD_UNLOAD));
3937/// }
3938/// ```
3939///
3940/// All three gates matter in zshrs's static-link path:
3941/// 1. `find_module(FINDMOD_ALIASP)` — resolves aliases, returns the
3942/// target's modulestab entry (or None on miss).
3943/// 2. `m->u.handle` — non-null when the module's setup has run; in
3944/// the Rust mirror, MOD_INIT_S being set is the structural
3945/// equivalent (no dlopen handle to test).
3946/// 3. `!(m->node.flags & MOD_UNLOAD)` — MOD_UNLOAD is the
3947/// "registered + autoload-only" sentinel set by
3948/// register_builtin_modules for entries OUTSIDE
3949/// zsh_default_loaded (e.g. zsh/files, zsh/system, zsh/zftp).
3950/// Without this gate, `${modules[zsh/files]}` reads as "loaded"
3951/// from initial register even though no `zmodload zsh/files` has
3952/// run — diverging from `zsh -fc` which reports the names as
3953/// autoload-pending.
3954/// WARNING: param names don't match C — Rust=(table, name) vs C=(name)
3955pub fn module_loaded(table: &modulestab, name: &str) -> i32 {
3956 // c:1703
3957 // c:1707 — `find_module(name, FINDMOD_ALIASP, NULL)`: resolve
3958 // alias chains via the existing free-fn port (c:1659).
3959 // The Rust port returns Option<String> — Some(target) on hit.
3960 // Inline the resolution here so we can read the target's flags
3961 // without re-locking.
3962 let target = match table.modules.get(name) {
3963 Some(m) if (m.node.flags & MOD_ALIAS) != 0 => {
3964 // c:FINDMOD_ALIASP — chase alias.
3965 // m->u.alias is the alias target; the Rust mirror stores
3966 // it on `module::aliased` (set by zmodload -A). Probe.
3967 match m.alias.as_ref().and_then(|a| table.modules.get(a)) {
3968 Some(t) => t,
3969 None => return 0, // alias points nowhere → not loaded
3970 }
3971 }
3972 Some(m) => m,
3973 None => return 0, // c:1707-1709 — find_module miss.
3974 };
3975 // c:1708 — `m->u.handle` — non-null on a fully-loaded module.
3976 // Static-link analog: MOD_LINKED set + module entry exists in
3977 // modulestab means register_module fired (= setup ran). The
3978 // dlopen `u.handle` check translates to MOD_LINKED here because
3979 // every modulestab entry is a statically-linked module in
3980 // zshrs's compile-time-only loader.
3981 if (target.node.flags & MOD_LINKED) == 0 {
3982 return 0;
3983 }
3984 // c:1709 — `!(m->node.flags & MOD_UNLOAD)`. MOD_UNLOAD is set by
3985 // register_builtin_modules on the autoload-only subset
3986 // (zsh/files, zsh/system, zsh/zftp, …); cleared by an explicit
3987 // `zmodload NAME` once the user wants the module live.
3988 if (target.node.flags & MOD_UNLOAD) != 0 {
3989 return 0;
3990 }
3991 1
3992}
3993
3994/// Port of `dyn_setup_module()` from `Src/module.c:1726`. — C decl `dyn_setup_module(Module m)`.
3995///
3996/// C body: `return ((int (*)(int,Module,void*)) m->u.handle)(0, m, NULL);`
3997/// Op-code 0 = setup. AIX-only path that multiplexes all six module
3998/// hooks through one symbol; static-link path skips it entirely.
3999#[allow(unused_variables)]
4000pub fn dyn_setup_module(m: *const module) -> i32 {
4001 // c:1726
4002 0 // c:1726
4003}
4004
4005/// Port of `dyn_features_module()` from `Src/module.c:1733`. — C decl `dyn_features_module(Module m, char ***features)`.
4006///
4007/// C body: `return ((int (*)(int,Module,void*)) m->u.handle)(4, m, features);`
4008/// Op-code 4 = features.
4009#[allow(unused_variables)]
4010pub fn dyn_features_module(m: *const module, features: &mut Vec<String>) -> i32 {
4011 // c:1733
4012 0 // c:1733
4013}
4014
4015/// Port of `dyn_enables_module()` from `Src/module.c:1740`. — C decl `dyn_enables_module(Module m, int **enables)`.
4016///
4017/// C body: `return ((int (*)(int,Module,void*)) m->u.handle)(5, m, enables);`
4018/// Op-code 5 = enables.
4019#[allow(unused_variables)]
4020pub fn dyn_enables_module(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
4021 // c:1740
4022 0 // c:1733
4023}
4024
4025/// Port of `dyn_boot_module()` from `Src/module.c:1747`. — C decl `dyn_boot_module(Module m)`.
4026///
4027/// C body: `return ((int (*)(int,Module,void*)) m->u.handle)(1, m, NULL);`
4028/// Calls the dynamic module's exported entry-point with op-code 1
4029/// (boot). Static-link path: opcode dispatch unused, returns 0.
4030#[allow(unused_variables)]
4031pub fn dyn_boot_module(m: *const module) -> i32 {
4032 // c:1747
4033 0 // c:1754
4034}
4035
4036/// Port of `dyn_cleanup_module()` from `Src/module.c:1754`. — C decl `dyn_cleanup_module(Module m)`.
4037///
4038/// C body: `return ((int (*)(int,Module,void*)) m->u.handle)(2, m, NULL);`
4039/// Op-code 2 = cleanup.
4040#[allow(unused_variables)]
4041pub fn dyn_cleanup_module(m: *const module) -> i32 {
4042 // c:1754
4043 0 // c:1740
4044}
4045
4046/// Port of `dyn_finish_module()` from `Src/module.c:1761`. — C decl `dyn_finish_module(Module m)`.
4047/// C body: `return ((int (*)(int,Module,void*))
4048/// m->u.handle)(3, m, NULL);` — invokes the DSO entry point with
4049/// opcode 3 (finish). no-op in Rust: zshrs has no dlopen path, all
4050/// modules are statically linked, `m->u.handle` is always null, and
4051/// finish-time resource release happens at process exit. Static-link
4052/// path defers all DSO entry calls; the no-op return preserves
4053/// caller semantics (0 = success).
4054#[allow(unused_variables)]
4055pub fn dyn_finish_module(m: *const module) -> i32 {
4056 // c:1766
4057 // c:1768 — ((int (*)(int,Module,void*)) m->u.handle)(3, m, NULL).
4058 // Static modules: no handle, opcode 3 (finish) is a no-op.
4059 0 // c:1768 success
4060}
4061
4062/// Port of `module_func()` from `Src/module.c:1770`. — C decl `module_func(Module m, const char *name)`.
4063///
4064/// C body (DYNAMIC_NAME_CLASH_OK off — the typical case):
4065/// ```c
4066/// module_func(Module m, const char *name)
4067/// {
4068/// VARARR(char, buf, strlen(name) + strlen(m->node.nam)*2 + 1);
4069/// char const *p; char *q;
4070/// strcpy(buf, name);
4071/// q = strchr(buf, 0);
4072/// for(p = m->node.nam; *p; p++) {
4073/// if(*p == '/') { *q++ = 'Q'; *q++ = 's'; }
4074/// else if(*p == '_') { *q++ = 'Q'; *q++ = 'u'; }
4075/// else if(*p == 'Q') { *q++ = 'Q'; *q++ = 'q'; }
4076/// else *q++ = *p;
4077/// }
4078/// *q = 0;
4079/// return (Module_generic_func) dlsym(m->u.handle, buf);
4080/// }
4081/// ```
4082///
4083/// Builds a mangled symbol name (`<name><module-name-mangled>`) and
4084/// dlsym's it. The mangling encodes `/` as `Qs`, `_` as `Qu`, `Q` as
4085/// `Qq` so e.g. `setup_zsh_random` becomes `setup_zshQurandom`.
4086///
4087/// Static-link path: dlsym not used; returns 0 (NULL handle).
4088#[allow(unused_variables)]
4089pub fn module_func(m: &module, name: &str) -> usize {
4090 // c:1770
4091 0 // c:1794 NULL
4092}
4093
4094/// Port of `setup_module()` from `Src/module.c:1884`. — C decl `setup_module(Module m)`.
4095///
4096/// C body:
4097/// ```c
4098/// setup_module(Module m) {
4099/// return ((m->node.flags & MOD_LINKED) ?
4100/// (m->u.linked->setup)(m) : dyn_setup_module(m));
4101/// }
4102/// ```
4103/// Symmetric with boot_module/cleanup_module/finish_module: routes
4104/// to the per-module `setup_(m)`. C `setup_` is the first lifecycle
4105/// hook called when load_module loads the module (before
4106/// do_module_features registers features and before boot_ runs).
4107/// Most per-module setup_ bodies are `return 0;` — exceptions
4108/// initialise module-private state (curses screen handle, regex
4109/// compile cache, etc.).
4110/// WARNING: param names don't match C — Rust=(_table, name) vs C=(m)
4111pub fn setup_module(_table: &mut modulestab, name: &str) -> i32 {
4112 // c:1884
4113 match name {
4114 "zsh/attr" => crate::ported::modules::attr::setup_(std::ptr::null()),
4115 "zsh/cap" => crate::ported::modules::cap::setup_(std::ptr::null()),
4116 "zsh/clone" => crate::ported::modules::clone::setup_(std::ptr::null()),
4117 "zsh/curses" => crate::ported::modules::curses::setup_(std::ptr::null()),
4118 "zsh/datetime" => crate::ported::modules::datetime::setup_(std::ptr::null()),
4119 "zsh/db/gdbm" => crate::ported::modules::db_gdbm::setup_(std::ptr::null()),
4120 "zsh/example" => crate::ported::modules::example::setup_(std::ptr::null()),
4121 "zsh/files" => crate::ported::modules::files::setup_(std::ptr::null()),
4122 "zsh/hlgroup" => crate::ported::modules::hlgroup::setup_(std::ptr::null()),
4123 "zsh/ksh93" => crate::ported::modules::ksh93::setup_(std::ptr::null()),
4124 "zsh/langinfo" => crate::ported::modules::langinfo::setup_(std::ptr::null()),
4125 "zsh/mapfile" => crate::ported::modules::mapfile::setup_(std::ptr::null()),
4126 "zsh/mathfunc" => crate::ported::modules::mathfunc::setup_(std::ptr::null()),
4127 "zsh/nearcolor" => crate::ported::modules::nearcolor::setup_(std::ptr::null()),
4128 "zsh/newuser" => crate::ported::modules::newuser::setup_(std::ptr::null()),
4129 "zsh/deltochar" => crate::ported::zle::deltochar::setup_(), // c:Zle/deltochar.c:90
4130 "zsh/parameter" => crate::ported::modules::parameter::setup_(std::ptr::null()),
4131 "zsh/param/private" => crate::ported::modules::param_private::setup_(std::ptr::null()),
4132 "zsh/pcre" => crate::ported::modules::pcre::setup_(std::ptr::null()),
4133 "zsh/random" => crate::ported::modules::random::setup_(std::ptr::null()),
4134 "zsh/regex" => crate::ported::modules::regex::setup_(std::ptr::null()),
4135 "zsh/net/socket" => crate::ported::modules::socket::setup_(std::ptr::null()),
4136 "zsh/stat" => crate::ported::modules::stat::setup_(std::ptr::null()),
4137 "zsh/system" => crate::ported::modules::system::setup_(std::ptr::null()),
4138 "zsh/net/tcp" => crate::ported::modules::tcp::setup_(std::ptr::null()),
4139 "zsh/termcap" => crate::ported::modules::termcap::setup_(std::ptr::null()),
4140 "zsh/terminfo" => crate::ported::modules::terminfo::setup_(std::ptr::null()),
4141 "zsh/watch" => crate::ported::modules::watch::setup_(std::ptr::null()),
4142 "zsh/zftp" => crate::ported::modules::zftp::setup_(std::ptr::null()),
4143 "zsh/zprof" => crate::ported::modules::zprof::setup_(std::ptr::null()),
4144 "zsh/zpty" => crate::ported::modules::zpty::setup_(std::ptr::null()),
4145 "zsh/zselect" => crate::ported::modules::zselect::setup_(std::ptr::null()),
4146 "zsh/zutil" => crate::ported::modules::zutil::setup_(std::ptr::null()),
4147 "zsh/compctl" => crate::ported::zle::compctl::setup_(),
4148 // c:Src/Zle/complete.c:1730 setup_ — clears the comp* string globals
4149 // and raises `hascompmod` (c:1746), the flag `docomplete` reads at
4150 // `zle_tricky.c:712`. Missing from this table, `setup_` never ran on
4151 // any load path, so the flag stayed 0 and `=ls<TAB>` under
4152 // `expand-or-complete` expanded even when the prefix was ambiguous.
4153 "zsh/complete" => crate::ported::zle::complete::setup_(std::ptr::null()),
4154 // c:Src/Zle/zle_main.c:2246-2288 — zle's `setup_` runs `init_thingies`
4155 // and assigns `$zle_bracketed_paste` (c:2276-2280). C reaches it from
4156 // here, the module-LOAD chain, so a shell that loaded `zsh/zle`
4157 // without entering the editor still has the parameter:
4158 // zsh -f -c 'zmodload zsh/zle; print ${+zle_bracketed_paste}' -> 1
4159 // zsh -f -c ' print ${+zle_bracketed_paste}' -> 0
4160 // zshrs links zle statically, so `zleread` (zle_main.rs:1520) shares
4161 // the same one-shot guard for the editor-entry half of the contract.
4162 "zsh/zle" => {
4163 let mut r = 0;
4164 crate::ported::zle::zle_main::ZLE_MODULE_SETUP.call_once(|| {
4165 r = crate::ported::zle::zle_main::setup_(std::ptr::null());
4166 });
4167 r
4168 }
4169 _ => 0,
4170 }
4171}
4172
4173/// Port of `features_module()` from `Src/module.c:1892`. — C decl `features_module(Module m, char ***features)`.
4174///
4175/// C body:
4176/// ```c
4177/// features_module(Module m, char ***features) {
4178/// return ((m->node.flags & MOD_LINKED) ?
4179/// (m->u.linked->features)(m, features) :
4180/// dyn_features_module(m, features));
4181/// }
4182/// ```
4183///
4184/// Per-module features_ populates the out-array with each feature
4185/// the module provides — `b:bn`, `c:cn`, `C:Cn`, `f:fn`, `p:pn`
4186/// per featuresarray (Src/module.c:3279). Used by zmodload -L to
4187/// list feature surfaces.
4188/// WARNING: param names don't match C — Rust=(_table, name, features) vs C=(m, features)
4189pub fn features_module(_table: &mut modulestab, name: &str, features: &mut Vec<String>) -> i32 {
4190 // c:1892
4191 match name {
4192 "zsh/attr" => crate::ported::modules::attr::features_(std::ptr::null(), features),
4193 "zsh/cap" => crate::ported::modules::cap::features_(std::ptr::null(), features),
4194 "zsh/clone" => crate::ported::modules::clone::features_(std::ptr::null(), features),
4195 "zsh/curses" => crate::ported::modules::curses::features_(std::ptr::null(), features),
4196 "zsh/datetime" => crate::ported::modules::datetime::features_(std::ptr::null(), features),
4197 "zsh/db/gdbm" => crate::ported::modules::db_gdbm::features_(std::ptr::null(), features),
4198 "zsh/example" => crate::ported::modules::example::features_(std::ptr::null(), features),
4199 "zsh/files" => crate::ported::modules::files::features_(std::ptr::null(), features),
4200 "zsh/hlgroup" => crate::ported::modules::hlgroup::features_(std::ptr::null(), features),
4201 "zsh/ksh93" => crate::ported::modules::ksh93::features_(std::ptr::null(), features),
4202 "zsh/langinfo" => crate::ported::modules::langinfo::features_(std::ptr::null(), features),
4203 "zsh/mapfile" => crate::ported::modules::mapfile::features_(std::ptr::null(), features),
4204 "zsh/mathfunc" => crate::ported::modules::mathfunc::features_(std::ptr::null(), features),
4205 "zsh/nearcolor" => crate::ported::modules::nearcolor::features_(std::ptr::null(), features),
4206 "zsh/newuser" => crate::ported::modules::newuser::features_(std::ptr::null(), features),
4207 // c:Zle/deltochar.c:97-101 — `featuresarray` over an all-empty
4208 // `module_features` (c:79-85): an empty array, return 0.
4209 "zsh/deltochar" => crate::ported::zle::deltochar::features_(),
4210 "zsh/parameter" => crate::ported::modules::parameter::features_(std::ptr::null(), features),
4211 "zsh/param/private" => {
4212 crate::ported::modules::param_private::features_(std::ptr::null(), features)
4213 }
4214 "zsh/pcre" => crate::ported::modules::pcre::features_(std::ptr::null(), features),
4215 "zsh/random" => crate::ported::modules::random::features_(std::ptr::null(), features),
4216 "zsh/regex" => crate::ported::modules::regex::features_(std::ptr::null(), features),
4217 "zsh/net/socket" => crate::ported::modules::socket::features_(std::ptr::null(), features),
4218 "zsh/stat" => crate::ported::modules::stat::features_(std::ptr::null(), features),
4219 "zsh/system" => crate::ported::modules::system::features_(std::ptr::null(), features),
4220 "zsh/net/tcp" => crate::ported::modules::tcp::features_(std::ptr::null(), features),
4221 "zsh/termcap" => crate::ported::modules::termcap::features_(std::ptr::null(), features),
4222 "zsh/terminfo" => crate::ported::modules::terminfo::features_(std::ptr::null(), features),
4223 "zsh/watch" => crate::ported::modules::watch::features_(std::ptr::null(), features),
4224 // c:Src/Zle/zle_main.c:2286 — zsh/zle's features_() returns
4225 // b:zle/b:bindkey/b:vared + the p:BUFFER-family params via
4226 // featuresarray. Without this arm the dispatch fell to the
4227 // `_ => 0` default with an EMPTY features vec, so once any
4228 // zle/bindkey use marked the module loaded, the NEXT
4229 // autoloaded-builtin dispatch (ensurefeature → autofeatures
4230 // c:3558-3577 loaded-module check) found no `b:bindkey` in
4231 // the table and errored `module has no such feature` —
4232 // breaking every zinit plugin that calls `zle -N` before
4233 // `bindkey` (zsh-autopair, zsh-hist, zconvey, zui, …).
4234 "zsh/zle" => crate::ported::zle::zle_main::features_(std::ptr::null(), features),
4235 "zsh/zftp" => crate::ported::modules::zftp::features_(std::ptr::null(), features),
4236 "zsh/zprof" => crate::ported::modules::zprof::features_(std::ptr::null(), features),
4237 "zsh/zpty" => crate::ported::modules::zpty::features_(std::ptr::null(), features),
4238 "zsh/zselect" => crate::ported::modules::zselect::features_(std::ptr::null(), features),
4239 "zsh/zutil" => crate::ported::modules::zutil::features_(std::ptr::null(), features),
4240 // The three statically-linked completion modules have no per-module
4241 // features_() fn in the port, so they fell to `_ => 0` (EMPTY
4242 // features). That broke `ensurefeature`: calling an autoloadable
4243 // completion builtin (`compset`/`compadd` from a shell completer)
4244 // looked up `b:<name>` against its home module and errored "module
4245 // `zsh/X' has no such feature: `b:<name>'" (same class as the
4246 // zsh/sched `b:sched` error). The feature surface is `b:<builtin>`
4247 // per the C BUILTIN() homes (complete.c:1693-1694 / computil.c:5131-
4248 // 5138 / compctl.c:4006-4007).
4249 // c:Src/Zle/complete.c:1720-1726 — `module_features = { bintab,
4250 // …, cotab, …, NULL, 0, NULL, 0, 0 }`, so `featuresarray`
4251 // (c:3283-3308) emits the two `b:` rows in bintab order
4252 // (c:1693-1694) then the four `c:` rows in cotab order
4253 // (c:1698-1701: after, between, prefix, suffix). The `c:` rows
4254 // were missing, so `zmodload zsh/complete` after the boot
4255 // autofeatures replay hit do_module_features' FEAT_CHECKAUTO arm
4256 // (c:2024-2044) and cancelled all four autoloads with
4257 // "module `zsh/complete' has no such feature".
4258 "zsh/complete" => {
4259 for f in [
4260 "b:compadd",
4261 "b:compset",
4262 "c:after", // c:1698
4263 "c:between", // c:1699
4264 "c:prefix", // c:1700
4265 "c:suffix", // c:1701
4266 ] {
4267 features.push(f.to_string());
4268 }
4269 0
4270 }
4271 // c:Src/Zle/zleparameter.c:137-143 — `module_features` carries
4272 // ONLY `partab` (c:131-135), so the feature surface is the two
4273 // `p:` rows in partab order.
4274 "zsh/zleparameter" => {
4275 for f in ["p:keymaps", "p:widgets"] {
4276 features.push(f.to_string());
4277 }
4278 0
4279 }
4280 "zsh/computil" => {
4281 for f in [
4282 "b:comparguments",
4283 "b:compdescribe",
4284 "b:compfiles",
4285 "b:compgroups",
4286 "b:compquote",
4287 "b:comptags",
4288 "b:comptry",
4289 "b:compvalues",
4290 ] {
4291 features.push(f.to_string());
4292 }
4293 0
4294 }
4295 "zsh/compctl" => {
4296 // c:3295-3296 — `featuresarray` walks `bn_list` IN TABLE ORDER,
4297 // and `bintab` (c:Src/Zle/compctl.c:4005-4008) is
4298 // `compcall` then `compctl`.
4299 for f in ["b:compcall", "b:compctl"] {
4300 features.push(f.to_string());
4301 }
4302 0
4303 }
4304 // The two statically-linked Builtins/ modules with builtins also
4305 // had no features_() fn → `_ => 0`. `zsh/sched` is the one the user
4306 // hit at session start: `zmodload zsh/sched; sched …` errored
4307 // "module `zsh/sched' has no such feature: `b:sched'". Feature
4308 // surface = `b:<builtin>` (sched.c:376 / rlimits.c limit/ulimit/
4309 // unlimit).
4310 // c:Src/Builtins/sched.c:387-393 — `module_features = { bintab,
4311 // …, NULL, 0, NULL, 0, partab, … }`: one `b:` row then the
4312 // `p:zsh_scheduled_events` paramdef (sched.c partab), which
4313 // `featuresarray` emits last (c:3304-3305).
4314 "zsh/sched" => {
4315 for f in ["b:sched", "p:zsh_scheduled_events"] {
4316 features.push(f.to_string());
4317 }
4318 0
4319 }
4320 "zsh/rlimits" => {
4321 for f in ["b:limit", "b:ulimit", "b:unlimit"] {
4322 features.push(f.to_string());
4323 }
4324 0
4325 }
4326 // c:Src/Zle/complist.c:3518-3524 — complist's `module_features` is
4327 // `{ NULL, 0, NULL, 0, NULL, 0, NULL, 0, 0 }`: four empty descriptor
4328 // lists and no abstract features. Its `features_` (c:3535-3538) still
4329 // returns 0, so `featuresarray` hands back an EMPTY (but present)
4330 // array and `zmodload -lF zsh/complist` prints nothing with rc 0.
4331 // complist's user surface is widgets + `$ZLS_COLORS`, neither of which
4332 // travels through the feature interface.
4333 //
4334 // This arm has to be explicit so the `_ =>` default below can carry
4335 // C's real "no features_ entry point" answer. Without it complist
4336 // would inherit that 1 and start claiming "does not support features".
4337 "zsh/complist" => 0, // c:Zle/complist.c:3537
4338 // c:305-320 — zsh/main is the `link=static` pseudo-module holding the
4339 // shell core, and its `features_` is one of the two in the tree that
4340 // returns 1 outright: "There are lots and lots of features, but
4341 // they're not handled here." (c:315-319). The other is
4342 // `Src/Modules/newuser.c:44-47`, whose arm dispatches to
4343 // `newuser::features_` above and gets the same 1 from there.
4344 //
4345 // zshrs answered 0-with-an-empty-vec, so `zmodload -lF zsh/main`
4346 // printed nothing and exited 0 where zsh prints
4347 // `module `zsh/main' does not support features` and exits 1
4348 // (the c:3118-3123 arm of `bin_zmodload_features`).
4349 "zsh/main" => 1, // c:319
4350 // c:1816-1825 `dyn_features_module` — a module with no `features_`
4351 // entry point yields 1, "not a user-visible error if no features
4352 // function" (c:1823). Every module zshrs links in that C gives a
4353 // `features_` to has an explicit arm above, so reaching here means
4354 // the name has no feature interface at all and 1 is the faithful
4355 // answer. Returning 0 instead left the caller with an EMPTY feature
4356 // array that reads as "supports features, has none" — a different
4357 // statement, and the one that silenced the zsh/main diagnostic.
4358 _ => 1, // c:1824
4359 }
4360}
4361
4362/// Port of `enables_module()` from `Src/module.c:1901`. — C decl `enables_module(Module m, int **enables)`.
4363///
4364/// C body:
4365/// ```c
4366/// enables_module(Module m, int **enables) {
4367/// return ((m->node.flags & MOD_LINKED) ?
4368/// (m->u.linked->enables)(m, enables) :
4369/// dyn_enables_module(m, enables));
4370/// }
4371/// ```
4372///
4373/// Per-module enables_ populates the enable-bit array parallel to
4374/// the features_ surface — getfeatureenables (c:3314) emits 1 for
4375/// each currently-active feature (BINF_ADDED / CONDF_ADDED /
4376/// MFF_ADDED / pd->pm non-null), 0 otherwise. Used by zmodload -L
4377/// to report enabled-vs-disabled features per module.
4378/// WARNING: param names don't match C — Rust=(_table, name, enables) vs C=(m, enables)
4379pub fn enables_module(table: &mut modulestab, name: &str, enables: &mut Option<Vec<i32>>) -> i32 {
4380 // c:1901
4381 match name {
4382 "zsh/attr" => crate::ported::modules::attr::enables_(std::ptr::null(), enables),
4383 "zsh/cap" => crate::ported::modules::cap::enables_(std::ptr::null(), enables),
4384 "zsh/clone" => crate::ported::modules::clone::enables_(std::ptr::null(), enables),
4385 "zsh/curses" => crate::ported::modules::curses::enables_(std::ptr::null(), enables),
4386 "zsh/datetime" => crate::ported::modules::datetime::enables_(std::ptr::null(), enables),
4387 "zsh/db/gdbm" => crate::ported::modules::db_gdbm::enables_(std::ptr::null(), enables),
4388 "zsh/example" => crate::ported::modules::example::enables_(std::ptr::null(), enables),
4389 "zsh/files" => crate::ported::modules::files::enables_(std::ptr::null(), enables),
4390 "zsh/hlgroup" => crate::ported::modules::hlgroup::enables_(std::ptr::null(), enables),
4391 "zsh/ksh93" => crate::ported::modules::ksh93::enables_(std::ptr::null(), enables),
4392 "zsh/langinfo" => crate::ported::modules::langinfo::enables_(std::ptr::null(), enables),
4393 "zsh/mapfile" => crate::ported::modules::mapfile::enables_(std::ptr::null(), enables),
4394 "zsh/mathfunc" => crate::ported::modules::mathfunc::enables_(std::ptr::null(), enables),
4395 "zsh/nearcolor" => crate::ported::modules::nearcolor::enables_(std::ptr::null(), enables),
4396 "zsh/newuser" => crate::ported::modules::newuser::enables_(std::ptr::null(), enables),
4397 "zsh/deltochar" => crate::ported::zle::deltochar::enables_(), // c:Zle/deltochar.c:105
4398 "zsh/parameter" => crate::ported::modules::parameter::enables_(std::ptr::null(), enables),
4399 "zsh/param/private" => {
4400 crate::ported::modules::param_private::enables_(std::ptr::null(), enables)
4401 }
4402 "zsh/pcre" => crate::ported::modules::pcre::enables_(std::ptr::null(), enables),
4403 "zsh/random" => crate::ported::modules::random::enables_(std::ptr::null(), enables),
4404 "zsh/regex" => crate::ported::modules::regex::enables_(std::ptr::null(), enables),
4405 "zsh/net/socket" => crate::ported::modules::socket::enables_(std::ptr::null(), enables),
4406 "zsh/stat" => crate::ported::modules::stat::enables_(std::ptr::null(), enables),
4407 "zsh/system" => crate::ported::modules::system::enables_(std::ptr::null(), enables),
4408 "zsh/net/tcp" => crate::ported::modules::tcp::enables_(std::ptr::null(), enables),
4409 "zsh/termcap" => crate::ported::modules::termcap::enables_(std::ptr::null(), enables),
4410 "zsh/terminfo" => crate::ported::modules::terminfo::enables_(std::ptr::null(), enables),
4411 "zsh/watch" => crate::ported::modules::watch::enables_(std::ptr::null(), enables),
4412 "zsh/zftp" => crate::ported::modules::zftp::enables_(std::ptr::null(), enables),
4413 "zsh/zprof" => crate::ported::modules::zprof::enables_(std::ptr::null(), enables),
4414 "zsh/zpty" => crate::ported::modules::zpty::enables_(std::ptr::null(), enables),
4415 "zsh/zselect" => crate::ported::modules::zselect::enables_(std::ptr::null(), enables),
4416 "zsh/zutil" => crate::ported::modules::zutil::enables_(std::ptr::null(), enables),
4417 // c:Src/Zle/complete.c:1751 — `handlefeatures(m, &module_features,
4418 // enables)`. This is the arm that installs the four cotab
4419 // conddefs (`-prefix`/`-suffix`/`-after`/`-between`), replacing the
4420 // `c:` autoload stubs the boot `autofeatures` replay planted.
4421 // Falling to `_ => 0` left them installed forever, so
4422 // `zmodload zsh/complete; zmodload -ac` listed all four where
4423 // `zsh -f` lists none.
4424 "zsh/complete" => crate::ported::zle::complete::enables_(std::ptr::null(), enables),
4425 // c:1901 — `enables_module` is the `m->u.linked->enables` call, and
4426 // EVERY module that answers `features_module` must answer this too:
4427 // `bin_zmodload_features` (c:3163-3167) bails out of the listing when
4428 // `enables_module` fails, and treats a left-alone `enables` as an
4429 // all-zero bitmap. These six fell to `_ => 0` below, so a LOADED
4430 // zsh/zle / zsh/computil listed every builtin as `-` where zsh lists
4431 // `+`.
4432 "zsh/zle" => crate::ported::zle::zle_main::enables_(std::ptr::null(), enables),
4433 "zsh/sched" => crate::ported::builtins::sched::enables_(std::ptr::null(), enables),
4434 "zsh/rlimits" => crate::ported::builtins::rlimits::enables_(std::ptr::null(), enables),
4435 // These three keep their feature list inline in `features_module`
4436 // above rather than in a per-module `features_()`, so the enables
4437 // half reads it back from there and hands it to the name-keyed
4438 // `handlefeatures` (c:3392) that holds the per-feature ADDED bit.
4439 "zsh/computil" | "zsh/zleparameter" | "zsh/compctl" => {
4440 let mut feats: Vec<String> = Vec::new();
4441 if features_module(table, name, &mut feats) != 0 {
4442 return 1; // c:3164
4443 }
4444 handlefeatures(name, &feats, enables) // c:3396
4445 }
4446 _ => 0,
4447 }
4448}
4449
4450/// Port of `boot_module()` from `Src/module.c:1910`. — C decl `boot_module(Module m)`.
4451///
4452/// C body:
4453/// ```c
4454/// boot_module(Module m) {
4455/// return ((m->node.flags & MOD_LINKED) ?
4456/// (m->u.linked->boot)(m) : dyn_boot_module(m));
4457/// }
4458/// ```
4459///
4460/// Static-link path: every modulestab entry is MOD_LINKED, so the
4461/// branch collapses to `(m->u.linked->boot)(m)`. The Rust analog
4462/// of C's `m->u.linked->boot` function-pointer is the per-module
4463/// `boot_(m)` defined in `src/ported/modules/<name>.rs`. Dispatch
4464/// via a static name → fn-pointer table.
4465///
4466/// Modules not in the dispatch table (because their per-module
4467/// boot_ isn't ported yet) fall through to 0 — same observable
4468/// outcome as a no-op boot, matching pre-port behaviour.
4469/// WARNING: param names don't match C — Rust=(_table, name) vs C=(m)
4470pub fn boot_module(_table: &mut modulestab, name: &str) -> i32 {
4471 // c:1910
4472 // c:1912 — `(m->u.linked->boot)(m)` for MOD_LINKED.
4473 // The per-module boot_ entry points carry the partab/bintab
4474 // dispatch that addparamdef/addbuiltins would otherwise run
4475 // for a dlopen'd module (e.g. watch.rs::boot_ seeds
4476 // WATCH/watch in paramtab + installs the checksched preprompt
4477 // hook). NULL module pointer is fine — every per-module boot_
4478 // either ignores the arg or null-checks before deref.
4479 match name {
4480 "zsh/attr" => crate::ported::modules::attr::boot_(std::ptr::null()),
4481 "zsh/cap" => crate::ported::modules::cap::boot_(std::ptr::null()),
4482 "zsh/clone" => crate::ported::modules::clone::boot_(std::ptr::null()),
4483 // c:Src/Zle/complist.c:3566 boot_ — installs the `menuselect` and
4484 // `listscroll` keymaps (via menuselect_bindings). Without this,
4485 // `zmodload zsh/complist` was a no-op and every `bindkey -M
4486 // menuselect …` (zpwr's completion-menu keybindings) errored
4487 // "no such keymap `menuselect'".
4488 // c:Src/Zle/complete.c:1758 boot_ — registers the six completion
4489 // hookfns and (via the cotab half of `handlefeatures`,
4490 // c:3364-3369) installs the four `-prefix`/`-suffix`/`-after`/
4491 // `-between` conddefs that replace the `c:` autoload stubs.
4492 // Missing from this table, `zmodload zsh/complete` left all four
4493 // stubs in condtab where `zsh -f` clears them.
4494 "zsh/complete" => crate::ported::zle::complete::boot_(std::ptr::null()),
4495 "zsh/complist" => crate::ported::zle::complist::boot_(),
4496 "zsh/curses" => crate::ported::modules::curses::boot_(std::ptr::null()),
4497 "zsh/datetime" => crate::ported::modules::datetime::boot_(std::ptr::null()),
4498 "zsh/db/gdbm" => crate::ported::modules::db_gdbm::boot_(std::ptr::null()),
4499 "zsh/example" => crate::ported::modules::example::boot_(std::ptr::null()),
4500 "zsh/files" => crate::ported::modules::files::boot_(std::ptr::null()),
4501 "zsh/hlgroup" => crate::ported::modules::hlgroup::boot_(std::ptr::null()),
4502 "zsh/ksh93" => crate::ported::modules::ksh93::boot_(std::ptr::null()),
4503 "zsh/langinfo" => crate::ported::modules::langinfo::boot_(std::ptr::null()),
4504 "zsh/mapfile" => crate::ported::modules::mapfile::boot_(std::ptr::null()),
4505 "zsh/mathfunc" => crate::ported::modules::mathfunc::boot_(std::ptr::null()),
4506 "zsh/nearcolor" => crate::ported::modules::nearcolor::boot_(std::ptr::null()),
4507 "zsh/newuser" => crate::ported::modules::newuser::boot_(std::ptr::null()),
4508 "zsh/deltochar" => crate::ported::zle::deltochar::boot_(), // c:Zle/deltochar.c:112
4509 "zsh/parameter" => crate::ported::modules::parameter::boot_(std::ptr::null()),
4510 "zsh/param/private" => crate::ported::modules::param_private::boot_(std::ptr::null()),
4511 "zsh/pcre" => crate::ported::modules::pcre::boot_(std::ptr::null()),
4512 "zsh/random" => crate::ported::modules::random::boot_(std::ptr::null()),
4513 "zsh/regex" => crate::ported::modules::regex::boot_(std::ptr::null()),
4514 // c:Src/Builtins/sched.c:421 boot_ — `addprepromptfn(&checksched)`.
4515 // That single line is the ONLY thing that arms the schedule at
4516 // prompt time; `sched.mdd` is `load=yes` with `autofeatures="b:sched
4517 // p:zsh_scheduled_events"`, so C boots the module the first time
4518 // `sched` is run (`$modules[zsh/sched]` goes `autoloaded` ->
4519 // `loaded`). With no arm here `sched::boot_` had zero callers, so a
4520 // due entry was never checked between commands — only the
4521 // `addtimedfn` deadline inside ZLE's read loop ever reached
4522 // `checksched`, and a shell that was NOT idle in the editor (ZLE
4523 // off, or busy running a command when the deadline passed) never
4524 // ran the command at all.
4525 "zsh/sched" => crate::ported::builtins::sched::boot_(std::ptr::null()),
4526 "zsh/net/socket" => crate::ported::modules::socket::boot_(std::ptr::null()),
4527 "zsh/stat" => crate::ported::modules::stat::boot_(std::ptr::null()),
4528 "zsh/system" => crate::ported::modules::system::boot_(std::ptr::null()),
4529 "zsh/net/tcp" => crate::ported::modules::tcp::boot_(std::ptr::null()),
4530 "zsh/termcap" => crate::ported::modules::termcap::boot_(std::ptr::null()),
4531 "zsh/terminfo" => crate::ported::modules::terminfo::boot_(std::ptr::null()),
4532 // c:1912 — C passes the real Module m to boot. watch::boot_
4533 // reads m->node.flags (MOD_SETUP = mid-load_module) to gate
4534 // its WATCHFMT/LOGCHECK seeding under --zsh; passing the
4535 // table entry avoids re-locking MODULESTAB (the caller chain
4536 // load_module -> do_boot_module already holds it).
4537 "zsh/watch" => {
4538 let mptr = _table
4539 .modules
4540 .get(name)
4541 .map(|m| m as *const crate::ported::zsh_h::module)
4542 .unwrap_or(std::ptr::null());
4543 crate::ported::modules::watch::boot_(mptr)
4544 }
4545 "zsh/zftp" => crate::ported::modules::zftp::boot_(std::ptr::null()),
4546 "zsh/zprof" => crate::ported::modules::zprof::boot_(std::ptr::null()),
4547 "zsh/zpty" => crate::ported::modules::zpty::boot_(std::ptr::null()),
4548 "zsh/zselect" => crate::ported::modules::zselect::boot_(std::ptr::null()),
4549 "zsh/zutil" => crate::ported::modules::zutil::boot_(std::ptr::null()),
4550 // Modules without a ported per-module boot_ (e.g. zsh/main —
4551 // purely-static modules with no setup hook):
4552 // 0 == success no-op, matching the pre-port behaviour.
4553 _ => 0,
4554 }
4555}
4556
4557/// Port of `cleanup_module()` from `Src/module.c:1918`. — C decl `cleanup_module(Module m)`.
4558///
4559/// C body:
4560/// ```c
4561/// cleanup_module(Module m) {
4562/// return ((m->node.flags & MOD_LINKED) ?
4563/// (m->u.linked->cleanup)(m) : dyn_cleanup_module(m));
4564/// }
4565/// ```
4566///
4567/// Static-link path: dispatch via name → fn-pointer table to the
4568/// per-module `cleanup_(m)` defined in `src/ported/modules/<name>.rs`.
4569/// Mirrors the symmetric `boot_module` dispatcher (`Src/module.c:1910`).
4570/// Per-module cleanup_ typically calls `delprepromptfn` /
4571/// `setfeatureenables(NULL)` to roll back what boot_ installed.
4572/// WARNING: param names don't match C — Rust=(_table, name) vs C=(m)
4573pub fn cleanup_module(_table: &mut modulestab, name: &str) -> i32 {
4574 // c:1918
4575 match name {
4576 "zsh/attr" => crate::ported::modules::attr::cleanup_(std::ptr::null()),
4577 "zsh/cap" => crate::ported::modules::cap::cleanup_(std::ptr::null()),
4578 "zsh/clone" => crate::ported::modules::clone::cleanup_(std::ptr::null()),
4579 "zsh/curses" => crate::ported::modules::curses::cleanup_(std::ptr::null()),
4580 "zsh/datetime" => crate::ported::modules::datetime::cleanup_(std::ptr::null()),
4581 "zsh/db/gdbm" => crate::ported::modules::db_gdbm::cleanup_(std::ptr::null()),
4582 "zsh/example" => crate::ported::modules::example::cleanup_(std::ptr::null()),
4583 "zsh/files" => crate::ported::modules::files::cleanup_(std::ptr::null()),
4584 "zsh/hlgroup" => crate::ported::modules::hlgroup::cleanup_(std::ptr::null()),
4585 "zsh/ksh93" => crate::ported::modules::ksh93::cleanup_(std::ptr::null()),
4586 "zsh/langinfo" => crate::ported::modules::langinfo::cleanup_(std::ptr::null()),
4587 "zsh/mapfile" => crate::ported::modules::mapfile::cleanup_(std::ptr::null()),
4588 "zsh/mathfunc" => crate::ported::modules::mathfunc::cleanup_(std::ptr::null()),
4589 "zsh/nearcolor" => crate::ported::modules::nearcolor::cleanup_(std::ptr::null()),
4590 "zsh/newuser" => crate::ported::modules::newuser::cleanup_(std::ptr::null()),
4591 "zsh/deltochar" => crate::ported::zle::deltochar::cleanup_(), // c:Zle/deltochar.c:129
4592 "zsh/parameter" => crate::ported::modules::parameter::cleanup_(std::ptr::null()),
4593 "zsh/param/private" => crate::ported::modules::param_private::cleanup_(std::ptr::null()),
4594 "zsh/pcre" => crate::ported::modules::pcre::cleanup_(std::ptr::null()),
4595 "zsh/random" => crate::ported::modules::random::cleanup_(std::ptr::null()),
4596 "zsh/regex" => crate::ported::modules::regex::cleanup_(std::ptr::null()),
4597 // c:Src/Builtins/sched.c:438 cleanup_ — `delprepromptfn(&checksched)`
4598 // plus the free of every pending entry. Symmetric with the boot_ arm
4599 // above: without it a `zmodload -u zsh/sched` would leave the
4600 // preprompt hook installed and re-loading would push a second copy.
4601 "zsh/sched" => crate::ported::builtins::sched::cleanup_(std::ptr::null()),
4602 "zsh/net/socket" => crate::ported::modules::socket::cleanup_(std::ptr::null()),
4603 "zsh/stat" => crate::ported::modules::stat::cleanup_(std::ptr::null()),
4604 "zsh/system" => crate::ported::modules::system::cleanup_(std::ptr::null()),
4605 "zsh/net/tcp" => crate::ported::modules::tcp::cleanup_(std::ptr::null()),
4606 "zsh/termcap" => crate::ported::modules::termcap::cleanup_(std::ptr::null()),
4607 "zsh/terminfo" => crate::ported::modules::terminfo::cleanup_(std::ptr::null()),
4608 "zsh/watch" => crate::ported::modules::watch::cleanup_(std::ptr::null()),
4609 "zsh/zftp" => crate::ported::modules::zftp::cleanup_(std::ptr::null()),
4610 "zsh/zprof" => crate::ported::modules::zprof::cleanup_(std::ptr::null()),
4611 "zsh/zpty" => crate::ported::modules::zpty::cleanup_(std::ptr::null()),
4612 "zsh/zselect" => crate::ported::modules::zselect::cleanup_(std::ptr::null()),
4613 "zsh/zutil" => crate::ported::modules::zutil::cleanup_(std::ptr::null()),
4614 _ => 0,
4615 }
4616}
4617
4618/// Port of `finish_module()` from `Src/module.c:1926`. — C decl `finish_module(Module m)`.
4619///
4620/// C body:
4621/// ```c
4622/// finish_module(Module m) {
4623/// return ((m->node.flags & MOD_LINKED) ?
4624/// (m->u.linked->finish)(m) : dyn_finish_module(m));
4625/// }
4626/// ```
4627///
4628/// Static-link path: dispatch to the per-module `finish_(m)`.
4629/// finish_ is the second teardown step (after cleanup_) — it frees
4630/// process-lifetime state that survives a single `zmodload -u`.
4631/// Most modules have an empty body returning 0; a few (zftp, zpty,
4632/// watch with utmpx descriptors) keep state explicitly.
4633/// WARNING: param names don't match C — Rust=(_table, name) vs C=(m)
4634pub fn finish_module(_table: &mut modulestab, name: &str) -> i32 {
4635 // c:1926
4636 match name {
4637 "zsh/attr" => crate::ported::modules::attr::finish_(std::ptr::null()),
4638 "zsh/cap" => crate::ported::modules::cap::finish_(std::ptr::null()),
4639 "zsh/clone" => crate::ported::modules::clone::finish_(std::ptr::null()),
4640 "zsh/curses" => crate::ported::modules::curses::finish_(std::ptr::null()),
4641 "zsh/datetime" => crate::ported::modules::datetime::finish_(std::ptr::null()),
4642 "zsh/db/gdbm" => crate::ported::modules::db_gdbm::finish_(std::ptr::null()),
4643 "zsh/example" => crate::ported::modules::example::finish_(std::ptr::null()),
4644 "zsh/files" => crate::ported::modules::files::finish_(std::ptr::null()),
4645 "zsh/hlgroup" => crate::ported::modules::hlgroup::finish_(std::ptr::null()),
4646 "zsh/ksh93" => crate::ported::modules::ksh93::finish_(std::ptr::null()),
4647 "zsh/langinfo" => crate::ported::modules::langinfo::finish_(std::ptr::null()),
4648 "zsh/mapfile" => crate::ported::modules::mapfile::finish_(std::ptr::null()),
4649 "zsh/mathfunc" => crate::ported::modules::mathfunc::finish_(std::ptr::null()),
4650 "zsh/nearcolor" => crate::ported::modules::nearcolor::finish_(std::ptr::null()),
4651 "zsh/newuser" => crate::ported::modules::newuser::finish_(std::ptr::null()),
4652 "zsh/deltochar" => crate::ported::zle::deltochar::finish_(), // c:Zle/deltochar.c:138
4653 "zsh/parameter" => crate::ported::modules::parameter::finish_(std::ptr::null()),
4654 "zsh/param/private" => crate::ported::modules::param_private::finish_(std::ptr::null()),
4655 "zsh/pcre" => crate::ported::modules::pcre::finish_(std::ptr::null()),
4656 "zsh/random" => crate::ported::modules::random::finish_(std::ptr::null()),
4657 "zsh/regex" => crate::ported::modules::regex::finish_(std::ptr::null()),
4658 "zsh/net/socket" => crate::ported::modules::socket::finish_(std::ptr::null()),
4659 "zsh/stat" => crate::ported::modules::stat::finish_(std::ptr::null()),
4660 "zsh/system" => crate::ported::modules::system::finish_(std::ptr::null()),
4661 "zsh/net/tcp" => crate::ported::modules::tcp::finish_(std::ptr::null()),
4662 "zsh/termcap" => crate::ported::modules::termcap::finish_(std::ptr::null()),
4663 "zsh/terminfo" => crate::ported::modules::terminfo::finish_(std::ptr::null()),
4664 "zsh/watch" => crate::ported::modules::watch::finish_(std::ptr::null()),
4665 "zsh/zftp" => crate::ported::modules::zftp::finish_(std::ptr::null()),
4666 "zsh/zprof" => crate::ported::modules::zprof::finish_(std::ptr::null()),
4667 "zsh/zpty" => crate::ported::modules::zpty::finish_(std::ptr::null()),
4668 "zsh/zselect" => crate::ported::modules::zselect::finish_(std::ptr::null()),
4669 "zsh/zutil" => crate::ported::modules::zutil::finish_(std::ptr::null()),
4670 _ => 0,
4671 }
4672}
4673
4674/// Port of `do_module_features()` from `Src/module.c:1998`. — C decl `do_module_features(Module m, Feature_enables enablesarr, int flags)`.
4675///
4676/// C body c:1998-2125 (128 lines):
4677/// ```c
4678/// if (features_module(m, &features) == 0) {
4679/// int *enables = NULL;
4680/// if (enables_module(m, &enables)) {
4681/// if (!(flags & FEAT_IGNORE)) zwarn(...);
4682/// return 1;
4683/// }
4684/// if ((flags & FEAT_CHECKAUTO) && m->autoloads) {
4685/// /* validate autoloads against features list */
4686/// /* on mismatch: zwarn + autofeatures(REMOVE|IGNORE) +
4687/// expunge from enablesarr */
4688/// }
4689/// if (enablesarr) {
4690/// /* walk enablesarr, flip enables bits per +/- prefix */
4691/// } else {
4692/// /* enable all features */
4693/// }
4694/// if (enables_module(m, &enables)) return 2;
4695/// } else if (enablesarr) {
4696/// if (!(flags & FEAT_IGNORE)) zwarn("module does not support features");
4697/// return 1;
4698/// }
4699/// return ret;
4700/// ```
4701///
4702/// Prior Rust port confused module-name and enables-string into a
4703/// single `enablesarr: &str` param — the module-lookup and zwarn
4704/// diagnostic both used the feature-list string instead of the
4705/// module's name. Signature now separates them per C semantics:
4706/// modname identifies the module, features is the list of features
4707/// to enable (None = "enable all").
4708/// WARNING: param names don't match C — Rust=(table, modname, features, flags) vs C=(m, enablesarr, flags)
4709pub fn do_module_features(
4710 table: &mut modulestab,
4711 modname: &str,
4712 features: Option<&[String]>,
4713 flags: i32,
4714) -> i32 {
4715 // c:1998
4716 let mut module_features: Vec<String> = Vec::new(); // c:2000
4717 let mut ret: i32 = 0; // c:2001
4718 // C edits the caller's `enablesarr` in place (c:2059-2066) and no
4719 // caller reads it afterwards, so a local copy carries the same effect.
4720 let mut features: Option<Vec<String>> = features.map(|a| a.to_vec());
4721
4722 // c:2003 — `if (features_module(m, &features) == 0)`.
4723 if features_module(table, modname, &mut module_features) == 0 {
4724 // c:2011-2018 — fetch enables. Features supported → enables
4725 // should be too; error here is reported unless FEAT_IGNORE.
4726 let mut enables: Option<Vec<i32>> = None;
4727 if enables_module(table, modname, &mut enables) != 0 {
4728 // c:2012
4729 if (flags & FEAT_IGNORE) == 0 {
4730 // c:2014
4731 zwarn(&format!(
4732 "error getting enabled features for module `{}'", // c:2015
4733 modname
4734 ));
4735 }
4736 return 1; // c:2017
4737 }
4738
4739 // c:2020 — `if ((flags & FEAT_CHECKAUTO) && m->autoloads)`
4740 if (flags & FEAT_CHECKAUTO) != 0 {
4741 let autoloads: Vec<String> = table
4742 .modules
4743 .get(modname)
4744 .and_then(|m| m.autoloads.as_ref())
4745 .map(|al| al.iter().cloned().collect())
4746 .unwrap_or_default();
4747 // c:2027-2074 — walk autoloads, cancel mismatches.
4748 for al in &autoloads {
4749 // c:2028
4750 // c:2032-2034 — match `al` against the features array.
4751 let found = module_features.iter().any(|f| f == al);
4752 if !found {
4753 // c:2035
4754 if (flags & FEAT_IGNORE) == 0 {
4755 // c:2037
4756 zwarn(&format!(
4757 "module `{}' has no such feature: `{}': autoload cancelled", // c:2038-2040
4758 modname, al
4759 ));
4760 }
4761 // c:2045-2047 — autofeatures(NULL, m->node.nam, arg, 0, FEAT_IGNORE|FEAT_REMOVE)
4762 let arg = vec![al.clone()];
4763 autofeatures(table, "", Some(modname), &arg, 0, FEAT_IGNORE | FEAT_REMOVE);
4764 /*
4765 * don't want to try to enable *that*...
4766 * expunge it from the enable string.
4767 */
4768 // c:2053-2072 — without this the enables walk below
4769 // warned a second time ("has no such feature: `b:fail'")
4770 // for the feature the autoload just cancelled.
4771 if let Some(arr) = features.as_mut() {
4772 // c:2055-2071
4773 let pos = arr.iter().position(|fep| {
4774 let s = fep
4775 .strip_prefix('+')
4776 .or_else(|| fep.strip_prefix('-'))
4777 .unwrap_or(fep); // c:2057-2058
4778 if (flags & FEAT_PATTERN_ARGS) != 0 {
4779 let mut pat_src = crate::ported::string::dupstring(s);
4780 crate::ported::glob::tokenize(&mut pat_src);
4781 crate::ported::pattern::patcompile(
4782 &pat_src,
4783 crate::ported::zsh_h::PAT_STATIC,
4784 None,
4785 )
4786 .is_some_and(|p| crate::ported::pattern::pattry(&p, al))
4787 } else {
4788 al == s // c:2059-2060
4789 }
4790 });
4791 if let Some(i) = pos {
4792 /* can't enable it after all, so return 1 */
4793 ret = 1; // c:2062
4794 // c:2063-2067 — shift the rest down; the loop
4795 // leaves `fep` on the terminator, so c:2068
4796 // `if (!fep->pat) break;` always stops here.
4797 arr.remove(i);
4798 }
4799 }
4800 }
4801 }
4802 }
4803
4804 // c:2077-2113 — apply enablesarr (or enable all).
4805 match &features {
4806 Some(arr) => {
4807 // c:2079-2103 — walk enablesarr.
4808 let enables_vec = enables.get_or_insert_with(Vec::new);
4809 if enables_vec.len() < module_features.len() {
4810 enables_vec.resize(module_features.len(), 0);
4811 }
4812 for fep_str in arr {
4813 // c:2079 for (fep = enablesarr; fep->str; fep++)
4814 let (on, esp) = if let Some(rest) = fep_str.strip_prefix('+') {
4815 // c:2082-2083
4816 (1i32, rest)
4817 } else if let Some(rest) = fep_str.strip_prefix('-') {
4818 // c:2084-2086
4819 (0i32, rest)
4820 } else {
4821 (1i32, fep_str.as_str())
4822 };
4823 // c:2088-2094 —
4824 // for (fp = features; *fp; fp++)
4825 // if (fep->pat ? pattry(fep->pat, *fp)
4826 // : !strcmp(*fp, esp)) {
4827 // enables[fp - features] = on;
4828 // found++;
4829 // if (!fep->pat) break;
4830 // }
4831 // `fep->pat` is non-NULL exactly when `zmodload -m`
4832 // was given (c:3258); zshrs carries that as
4833 // FEAT_PATTERN_ARGS (see its doc comment for why).
4834 // A pattern keeps scanning — it may enable several
4835 // features — while an exact name stops at the first
4836 // hit.
4837 let pat = if (flags & FEAT_PATTERN_ARGS) != 0 {
4838 let mut pat_src = crate::ported::string::dupstring(esp);
4839 crate::ported::glob::tokenize(&mut pat_src);
4840 crate::ported::pattern::patcompile(
4841 &pat_src,
4842 crate::ported::zsh_h::PAT_STATIC,
4843 None,
4844 )
4845 } else {
4846 None
4847 };
4848 let mut found = false;
4849 for (i, f) in module_features.iter().enumerate() {
4850 let hit = match pat.as_ref() {
4851 Some(p) => crate::ported::pattern::pattry(p, f), // c:2093
4852 None => f == esp, // c:2093
4853 };
4854 if hit {
4855 enables_vec[i] = on; // c:2090
4856 found = true;
4857 if pat.is_none() {
4858 break; // c:2096-2097 `if (!fep->pat) break;`
4859 }
4860 }
4861 }
4862 if !found {
4863 // c:2099-2106 — the diagnostic differs for the
4864 // pattern form.
4865 if (flags & FEAT_IGNORE) == 0 {
4866 zwarn(&format!(
4867 "module `{}' has no {}: `{}'",
4868 modname,
4869 if pat.is_some() {
4870 "feature matching" // c:2102
4871 } else {
4872 "such feature" // c:2103
4873 },
4874 esp
4875 ));
4876 }
4877 return 1; // c:2105
4878 }
4879 }
4880 }
4881 None => {
4882 // c:2105-2112 — enable all features.
4883 let enables_vec = enables.get_or_insert_with(Vec::new);
4884 enables_vec.clear();
4885 enables_vec.resize(module_features.len(), 1);
4886 // c:2115-2116 — `for (ep = enables; n_features--; ep++) *ep = 1;`
4887 // with the comment "Enable all features. This is used when
4888 // loading without using zmodload -F." The commit below
4889 // (`enables_module`, c:2119) runs the module's `enables_` →
4890 // `handlefeatures` (c:3392) → `setfeatureenables` (c:3354) →
4891 // `setparamdefs` (c:1169-1181), which calls `addparamdef`
4892 // (c:1060) for EVERY `p:` feature whose enable bit is set.
4893 // `addparamdef` installs the real special Param over the
4894 // PM_AUTOLOAD stub `autofeatures` planted, so after a plain
4895 // `zmodload zsh/parameter` NONE of that module's parameters
4896 // is an autoload stub any more — only the single-feature
4897 // form (`ensurefeature(mn, "p:", nam)` from `loadparamnode`,
4898 // c:Src/params.c:568, which lands in the `Some(arr)` arm
4899 // above) leaves the siblings as stubs.
4900 //
4901 // zshrs seeds every magic parameter eagerly
4902 // (`vm_helper::init_partab_params`) and models PM_AUTOLOAD as
4903 // the `MATERIALIZED_MODULE_PARAMS` side set instead of a node
4904 // flag, so the stub-clearing half of `setparamdefs` has to be
4905 // replayed here. Without it a plain `zmodload zsh/parameter`
4906 // (which plugin managers such as zinit run at startup) left
4907 // `commands` reading as a
4908 // stub, so `local -A +h commands` in `_command_names` built a
4909 // PLAIN empty local instead of a special one (c:Src/builtin.c
4910 // :2083-2085 needs PM_SPECIAL on the node to set
4911 // `newspecial`), and `_path_commands`' `compadd -k commands`
4912 // added nothing until the user touched `$commands` by hand.
4913 for f in &module_features {
4914 if let Some(pname) = f.strip_prefix("p:") {
4915 crate::vm_helper::mark_module_param_used(pname);
4916 }
4917 }
4918 }
4919 }
4920
4921 // c:2115 — final `enables_module(m, &enables)` commits the bits.
4922 if enables_module(table, modname, &mut enables) != 0 {
4923 return 2; // c:2116
4924 }
4925 } else if features.is_some() {
4926 // c:2117-2121 — features_module failed AND enablesarr non-NULL:
4927 // module doesn't support features. zwarn unless FEAT_IGNORE.
4928 if (flags & FEAT_IGNORE) == 0 {
4929 zwarn(&format!(
4930 "module `{}' does not support features", // c:2119
4931 modname
4932 ));
4933 }
4934 return 1; // c:2120
4935 }
4936 // c:2122 — `Else it doesn't support features but we don't care.`
4937 let _ = &mut ret;
4938 ret // c:2124
4939}
4940
4941/// Port of `do_boot_module()` from `Src/module.c:2139`. — C decl `do_boot_module(Module m, Feature_enables enablesarr, int silent)`.
4942///
4943/// C body:
4944/// ```c
4945/// do_boot_module(Module m, Feature_enables enablesarr, int silent)
4946/// {
4947/// int ret = do_module_features(m, enablesarr,
4948/// silent ? FEAT_IGNORE|FEAT_CHECKAUTO :
4949/// FEAT_CHECKAUTO);
4950/// if (ret == 1) return 1;
4951/// if (boot_module(m)) return 1;
4952/// return ret;
4953/// }
4954/// ```
4955pub fn do_boot_module(
4956 table: &mut modulestab,
4957 modname: &str,
4958 features: Option<&[String]>,
4959 silent: i32,
4960 // !!! RUST-ONLY PARAM !!! — carries C's per-entry
4961 // `Feature_enables.pat` (see FEAT_PATTERN_ARGS).
4962 feat_pat: bool,
4963) -> i32 {
4964 // c:2139
4965 let mut flags = if silent != 0 {
4966 // c:2142 — silent → IGNORE | CHECKAUTO
4967 FEAT_IGNORE | FEAT_CHECKAUTO
4968 } else {
4969 FEAT_CHECKAUTO // c:2143
4970 };
4971 if feat_pat {
4972 flags |= FEAT_PATTERN_ARGS;
4973 }
4974 let ret = do_module_features(table, modname, features, flags); // c:2141
4975 if ret == 1 {
4976 // c:2145
4977 return 1; // c:2146
4978 }
4979 if boot_module(table, modname) != 0 {
4980 // c:2148
4981 return 1; // c:2149
4982 }
4983 ret // c:2150
4984}
4985
4986/// Port of `do_cleanup_module()` from `Src/module.c:2159`. — C decl `do_cleanup_module(Module m)`.
4987///
4988/// C body:
4989/// ```c
4990/// do_cleanup_module(Module m) {
4991/// return (m->node.flags & MOD_LINKED) ?
4992/// (m->u.linked && m->u.linked->cleanup(m)) :
4993/// (m->u.handle && cleanup_module(m));
4994/// }
4995/// ```
4996/// WARNING: param names don't match C — Rust=(table, name) vs C=(m)
4997pub fn do_cleanup_module(table: &mut modulestab, name: &str) -> i32 {
4998 // c:2159
4999 // Check the module is registered, then dispatch to cleanup_module.
5000 if table.modules.contains_key(name) {
5001 // c:2162 m->u.linked
5002 cleanup_module(table, name) // c:2163 cleanup_module(m)
5003 } else {
5004 0
5005 }
5006}
5007
5008/// Port of `modname_ok()` from `Src/module.c:2173`. — C decl `modname_ok(char const *p)`.
5009///
5010/// Returns 1 iff `p` is a valid module name: one or more
5011/// `/`-separated identifier segments.
5012///
5013/// C body:
5014/// ```c
5015/// modname_ok(char const *p)
5016/// {
5017/// do {
5018/// p = itype_end(p, IIDENT, 0);
5019/// if (!*p)
5020/// return 1;
5021/// } while(*p++ == '/');
5022/// return 0;
5023/// }
5024/// ```
5025pub fn modname_ok(p: &str) -> i32 {
5026 // c:2173
5027 let bytes = p.as_bytes();
5028 let mut i: usize = 0;
5029 loop {
5030 // c:2176 — `p = itype_end(p, IIDENT, 0);`
5031 // IIDENT = identifier-byte (alpha/digit/underscore + extended).
5032 while i < bytes.len() {
5033 let b = bytes[i];
5034 // Inline IIDENT check — alphanumeric or underscore. Mirrors
5035 // utils.c:itype_end stepping for the IIDENT bit.
5036 if b.is_ascii_alphanumeric() || b == b'_' {
5037 i += 1;
5038 } else {
5039 break;
5040 }
5041 }
5042 if i >= bytes.len() {
5043 // c:2177 if (!*p)
5044 return 1; // c:2178
5045 }
5046 if bytes[i] != b'/' {
5047 break;
5048 } // c:2179 while(*p++ == '/')
5049 i += 1;
5050 }
5051 0 // c:2180
5052}
5053
5054/// Port of `require_module()` from `Src/module.c:2344`. — C decl `require_module(const char *module, Feature_enables features, int silent)`.
5055///
5056/// C body c:2342-2360:
5057/// ```c
5058/// mod_export int
5059/// require_module(const char *module, Feature_enables features, int silent)
5060/// {
5061/// Module m = NULL;
5062/// int ret = 0;
5063/// queue_signals();
5064/// m = find_module(module, FINDMOD_ALIASP, &module);
5065/// if (!m || !m->u.handle ||
5066/// (m->node.flags & MOD_UNLOAD))
5067/// ret = load_module(module, features, silent);
5068/// else
5069/// ret = do_module_features(m, features, 0);
5070/// unqueue_signals();
5071/// return ret;
5072/// }
5073/// ```
5074///
5075/// Two branches: when the module isn't loaded yet (or is mid-unload),
5076/// route through `load_module` which runs the full setup → features →
5077/// boot lifecycle. When it's already loaded, skip to
5078/// `do_module_features` to just enable the requested per-feature
5079/// surface — much cheaper.
5080///
5081/// Static-link analog of `m->u.handle` is `MOD_INIT_B` (boot ran):
5082/// once boot_module has fired, the module is "loaded" in zshrs's
5083/// non-dlopen world.
5084/// WARNING: param names don't match C — Rust=(table, modname, features, silent) vs C=(module, features, silent)
5085pub fn require_module(
5086 table: &mut modulestab,
5087 modname: &str,
5088 features: Option<&[String]>,
5089 silent: i32,
5090 // !!! RUST-ONLY PARAM !!! — carries C's per-entry
5091 // `Feature_enables.pat` (see FEAT_PATTERN_ARGS).
5092 feat_pat: bool,
5093) -> i32 {
5094 // c:2344
5095 // c:2350 — queue_signals(): signal-deferral wrapper.
5096 crate::ported::signals::queue_signals();
5097
5098 // c:2351 — `m = find_module(module, FINDMOD_ALIASP, &module);`
5099 // Resolves alias chain; canonical name lives in `mname`.
5100 let mname_opt = find_module(table, modname, FINDMOD_ALIASP);
5101
5102 // c:2352-2353 — `if (!m || !m->u.handle || MOD_UNLOAD)`.
5103 // Static-link analog of `m->u.handle`: MOD_INIT_B (boot ran).
5104 let needs_load = match &mname_opt {
5105 None => true, // c:2352 !m
5106 Some(mname) => match table.modules.get(mname) {
5107 None => true,
5108 Some(m) => {
5109 (m.node.flags & MOD_INIT_B) == 0 // c:2352 !u.handle analog
5110 || (m.node.flags & MOD_UNLOAD) != 0 // c:2353
5111 }
5112 },
5113 };
5114
5115 let mname = mname_opt.unwrap_or_else(|| modname.to_string());
5116
5117 let ret = if needs_load {
5118 // c:2354 — `ret = load_module(module, features, silent);`
5119 // c:2208-2211 — load_module's first step: `if (!modname_ok(name))
5120 // { if (!silent) zerr("invalid module name `%s'", name); return 1; }`.
5121 // It runs before any lookup, so `zmodload 'zsh/da*'` is a fatal
5122 // `invalid module name`, not a failed load.
5123 if modname_ok(&mname) == 0 {
5124 if silent == 0 {
5125 crate::ported::utils::zerr(&format!("invalid module name `{}'", mname)); // c:2210
5126 }
5127 crate::ported::signals::unqueue_signals();
5128 return 1; // c:2211
5129 }
5130 // try_load_module gates the static-link path. On miss, emit
5131 // the canonical zwarn (gated by silent).
5132 if try_load_module(table, &mname) == 0 {
5133 if silent == 0 {
5134 // c:1622 — same message and same reasoning as do_load_module
5135 // (module.rs, `failed to load module `%s'` with the dlerror tail
5136 // omitted). docs/BUGS.md #376 records the backquoted prefix as
5137 // the FINAL intended state and the parity test pins it, so this
5138 // site must not diverge from the other one.
5139 crate::ported::utils::zwarn(&format!("failed to load module `{}'", mname));
5140 }
5141 crate::ported::signals::unqueue_signals();
5142 return 1;
5143 }
5144 // c:2354 — `ret = load_module(module, features, silent);` — the full
5145 // 0/1/2 status, NOT a success flag: 2 means the module loaded but some
5146 // feature could not be set, and `zmodload` reports it.
5147 table.load_module(&mname, features, feat_pat)
5148 } else {
5149 // c:2356 — `ret = do_module_features(m, features, 0);`
5150 // Module already loaded; just enable the requested features.
5151 // features=NULL in C means "enable all features"; the Rust
5152 // do_module_features takes a single enablesstr arg, so flatten
5153 // C: features=NULL means "enable all"; pass through.
5154 do_module_features(
5155 table,
5156 &mname,
5157 features,
5158 if feat_pat { FEAT_PATTERN_ARGS } else { 0 },
5159 )
5160 };
5161
5162 // c:2357 — unqueue_signals();
5163 crate::ported::signals::unqueue_signals();
5164
5165 ret // c:2359
5166}
5167
5168/// Port of `add_dep()` from `Src/module.c:2369`. — C decl `add_dep(const char *name, char *from)`.
5169///
5170/// C body:
5171/// ```c
5172/// add_dep(const char *name, char *from)
5173/// {
5174/// LinkNode node;
5175/// Module m;
5176/// m = find_module(name, FINDMOD_ALIASP|FINDMOD_CREATE, &name);
5177/// if (!m->deps)
5178/// m->deps = znewlinklist();
5179/// for (node = firstnode(m->deps);
5180/// node && strcmp((char *) getdata(node), from);
5181/// incnode(node));
5182/// if (!node)
5183/// zaddlinknode(m->deps, ztrdup(from));
5184/// }
5185/// ```
5186///
5187/// Records that module `name` depends on module `from`. Resolves
5188/// aliases so dependency graphs always point at canonical names.
5189/// WARNING: param names don't match C — Rust=(table, name, from) vs C=(name, from)
5190pub fn add_dep(table: &mut modulestab, name: &str, from: &str) -> i32 {
5191 // c:2369
5192 // c:2369 — m = find_module(name, FINDMOD_ALIASP|FINDMOD_CREATE, &name)
5193 let canon = match find_module(table, name, FINDMOD_ALIASP | FINDMOD_CREATE) {
5194 Some(n) => n,
5195 None => return 0,
5196 };
5197 if let Some(m) = table.modules.get_mut(&canon) {
5198 // c:2389-2391 — walk deps, skip if `from` already present.
5199 let deps = m
5200 .deps
5201 .get_or_insert_with(crate::ported::linklist::LinkList::new);
5202 if !deps.iter().any(|d| d == from) {
5203 // c:2392 if (!node)
5204 deps.push_back(from.to_string()); // c:2393 zaddlinknode
5205 }
5206 }
5207 0
5208}
5209
5210/// Port of `autoloadscan()` from `Src/module.c:2403`. — C decl `autoloadscan(HashNode hn, int printflags)`.
5211///
5212/// C body:
5213/// ```c
5214/// autoloadscan(HashNode hn, int printflags)
5215/// {
5216/// Builtin bn = (Builtin) hn;
5217/// if(bn->node.flags & BINF_ADDED)
5218/// return;
5219/// if(printflags & PRINT_LIST) {
5220/// fputs("zmodload -ab ", stdout);
5221/// if(bn->optstr[0] == '-') fputs("-- ", stdout);
5222/// quotedzputs(bn->optstr, stdout);
5223/// if(strcmp(bn->node.nam, bn->optstr)) {
5224/// putchar(' ');
5225/// quotedzputs(bn->node.nam, stdout);
5226/// }
5227/// } else {
5228/// nicezputs(bn->node.nam, stdout);
5229/// if(strcmp(bn->node.nam, bn->optstr)) {
5230/// fputs(" (", stdout);
5231/// nicezputs(bn->optstr, stdout);
5232/// putchar(')');
5233/// }
5234/// }
5235/// putchar('\n');
5236/// }
5237/// ```
5238///
5239/// Hash-table scan callback for autoloadable-builtin listing.
5240/// `printflags & PRINT_LIST` selects long form (`zmodload -ab MOD NAME`)
5241/// vs short form (`NAME (MOD)`). Skips already-registered builtins
5242/// (BINF_ADDED set).
5243/// WARNING: param names don't match C — Rust=(name, optstr, flags, printflags) vs C=(hn, printflags)
5244pub fn autoloadscan(name: &str, optstr: &str, flags: u32, printflags: i32) {
5245 // c:2403
5246 use crate::ported::utils::{nicezputs, quotedzputs};
5247 let mut stdout = std::io::stdout();
5248 if (flags & BINF_ADDED) != 0 {
5249 // c:2407
5250 return; // c:2408
5251 }
5252 if (printflags & PRINT_LIST) != 0 {
5253 // c:2409
5254 // c:2410-2417 — long form `zmodload -ab MOD NAME`
5255 print!("zmodload -ab ");
5256 if optstr.starts_with('-') {
5257 // c:2411
5258 print!("-- "); // c:2412
5259 }
5260 // c:2413 — `quotedzputs(bn->optstr, stdout);`
5261 print!("{}", quotedzputs(optstr));
5262 if name != optstr {
5263 // c:2414 — `if(strcmp(bn->node.nam, bn->optstr))`
5264 print!(" "); // c:2415 putchar(' ')
5265 // c:2416 — `quotedzputs(bn->node.nam, stdout);`
5266 print!("{}", quotedzputs(name));
5267 }
5268 } else {
5269 // c:2418-2424 — short form `NAME (MOD)`
5270 let _ = nicezputs(name, &mut stdout); // c:2419
5271 if name != optstr {
5272 // c:2420 — `if(strcmp(bn->node.nam, bn->optstr))`
5273 print!(" ("); // c:2421
5274 let _ = nicezputs(optstr, &mut stdout); // c:2422
5275 print!(")"); // c:2423
5276 }
5277 }
5278 println!(); // c:2426 putchar('\n')
5279}
5280
5281/// Port of `bin_zmodload()` from `Src/module.c:2440`. — C decl `bin_zmodload(char *nam, char **args, Options ops, UNUSED(int func))`.
5282/// Top-level dispatcher for the `zmodload` builtin. Validates flag
5283/// combinations then routes to one of the per-mode helpers:
5284/// -F → bin_zmodload_features (c:3003)
5285/// -e → bin_zmodload_exist (c:2623)
5286/// -d → bin_zmodload_dep (c:2649)
5287/// -a/-b/-c/-p/-f → bin_zmodload_auto (c:2726)
5288/// default → bin_zmodload_load (c:2971)
5289/// -A/-R → bin_zmodload_alias (c:2515)
5290/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
5291pub fn bin_zmodload(
5292 nam: &str,
5293 args: &[String], // c:2440
5294 ops: &options,
5295 _func: i32,
5296) -> i32 {
5297 let mut table = MODULESTAB.lock().unwrap();
5298 let table = &mut *table;
5299
5300 let ops_bcpf = OPT_ISSET(ops, b'b') || OPT_ISSET(ops, b'c') // c:2443
5301 || OPT_ISSET(ops, b'p') || OPT_ISSET(ops, b'f');
5302 let ops_au = OPT_ISSET(ops, b'a') || OPT_ISSET(ops, b'u'); // c:2445
5303 let mut ret: i32; // c:2446
5304
5305 if ops_bcpf && !ops_au {
5306 // c:2451
5307 zwarnnam(nam, "-b, -c, -f, and -p must be combined with -a or -u"); // c:2452
5308 return 1; // c:2453
5309 }
5310 if OPT_ISSET(ops, b'F') && (ops_bcpf || OPT_ISSET(ops, b'u')) {
5311 // c:2455
5312 zwarnnam(nam, "-b, -c, -f, -p and -u cannot be combined with -F"); // c:2456
5313 return 1; // c:2457
5314 }
5315 // !!! WARNING: RUST-ONLY EXTENSION — NO C COUNTERPART !!!
5316 // zshrs repurposes `-R` WITHOUT `-A` to load a native (Rust)
5317 // plugin cdylib through the stable C ABI (src/extensions/
5318 // plugin_host.rs): the first JIT-compiled Unix shell hosting
5319 // compiled-language plugins loaded at runtime. In C zsh `-R` means
5320 // "remove module alias" and is only ever meaningful alongside `-A`
5321 // (Src/module.c:2459); that behaviour is preserved for `-A -R name`
5322 // (both flags set falls through to bin_zmodload_alias below), so no
5323 // parity is lost for the near-unused alias-removal case.
5324 // zmodload -R <path>... load each plugin cdylib
5325 // zmodload -R list loaded plugins
5326 // zmodload -uR <name>... unload each plugin by name
5327 // …but only when no operand names a REAL zsh module. `zmodload -R
5328 // zsh/complete` is C's alias-removal form applied to a known module
5329 // and must keep C's diagnostic (`module is not an alias: zsh/complete`,
5330 // c:2561), while a native plugin is a cdylib path or an arbitrary
5331 // plugin name that is never in `linkedmodules`. Discriminating on the
5332 // operand lets BOTH behaviours coexist instead of the extension
5333 // shadowing the ported one: previously every `-R` operand went to the
5334 // plugin host, so a zsh module name came back as a dlopen error.
5335 // No operands (`zmodload -R` = list loaded plugins) still routes here.
5336 // A name that already has a `modulestab` node is C's territory too —
5337 // most importantly a MOD_ALIAS created by `zmodload -A NAME=REAL`,
5338 // whose removal is the whole point of C's `-R` (c:2552-2568). Those
5339 // never appear in `linkedmodules`, so the `module_linked` test alone
5340 // sent `zmodload -R example` to the plugin host and it came back as a
5341 // dlopen error where zsh silently deletes the alias
5342 // (V01zmodload.ztst "Delete the module alias again").
5343 if OPT_ISSET(ops, b'R')
5344 && !OPT_ISSET(ops, b'A')
5345 && !args
5346 .iter()
5347 .any(|a| table.module_linked(a) || table.modules.contains_key(a))
5348 {
5349 // Placed before the c:2490 queue_signals, so nothing to unqueue.
5350 // Handler lives in the extensions tree (not a port) —
5351 // src/extensions/plugin_host.rs.
5352 return crate::plugin_host::zmodload_rust_cmd(nam, args, ops);
5353 }
5354 if OPT_ISSET(ops, b'A') || OPT_ISSET(ops, b'R') {
5355 // c:2459
5356 if ops_bcpf || ops_au || OPT_ISSET(ops, b'd') // c:2460
5357 || (OPT_ISSET(ops, b'R') && OPT_ISSET(ops, b'e'))
5358 {
5359 zwarnnam(nam, "invalid flags combined with -A or -R"); // c:2462
5360 return 1; // c:2463
5361 }
5362 if !OPT_ISSET(ops, b'e') {
5363 // c:2465
5364 return bin_zmodload_alias(table, nam, args, ops); // c:2466
5365 }
5366 }
5367 if OPT_ISSET(ops, b'd') && OPT_ISSET(ops, b'a') {
5368 // c:2468
5369 zwarnnam(nam, "-d cannot be combined with -a"); // c:2469
5370 return 1; // c:2470
5371 }
5372 if OPT_ISSET(ops, b'u') && args.is_empty() {
5373 // c:2472
5374 zwarnnam(nam, "what do you want to unload?"); // c:2473
5375 return 1; // c:2474
5376 }
5377 if OPT_ISSET(ops, b'e')
5378 && (OPT_ISSET(ops, b'I') || OPT_ISSET(ops, b'L') // c:2476
5379 || (OPT_ISSET(ops, b'a') && !OPT_ISSET(ops, b'F'))
5380 || OPT_ISSET(ops, b'd') || OPT_ISSET(ops, b'i')
5381 || OPT_ISSET(ops, b'u'))
5382 {
5383 zwarnnam(nam, "-e cannot be combined with other options"); // c:2480
5384 return 1; // c:2482
5385 }
5386 // c:2484 — `for (fp = fonly; *fp; fp++)` — `l` and `P` only with `-F`.
5387 for fp in [b'l', b'P'] {
5388 // c:2484
5389 if OPT_ISSET(ops, fp) && !OPT_ISSET(ops, b'F') {
5390 // c:2485
5391 zwarnnam(nam, &format!("-{} is only allowed with -F", fp as char)); // c:2486
5392 return 1; // c:2487
5393 }
5394 }
5395 crate::ported::mem::queue_signals(); // c:2490
5396 if OPT_ISSET(ops, b'F') {
5397 // c:2491
5398 ret = bin_zmodload_features(table, nam, args, ops); // c:2492
5399 } else if OPT_ISSET(ops, b'e') {
5400 // c:2493
5401 ret = bin_zmodload_exist(table, nam, args, ops); // c:2494
5402 } else if OPT_ISSET(ops, b'd') {
5403 // c:2495
5404 ret = bin_zmodload_dep(table, nam, args, ops); // c:2496
5405 } else {
5406 let autoopts = (OPT_ISSET(ops, b'b') as i32) // c:2497
5407 + (OPT_ISSET(ops, b'c') as i32)
5408 + (OPT_ISSET(ops, b'p') as i32)
5409 + (OPT_ISSET(ops, b'f') as i32);
5410 if autoopts != 0 || OPT_ISSET(ops, b'a') {
5411 // c:2497-2499
5412 if autoopts > 1 {
5413 // c:2502
5414 zwarnnam(nam, "use only one of -b, -c, or -p"); // c:2503
5415 ret = 1; // c:2504
5416 } else {
5417 ret = bin_zmodload_auto(table, nam, args, ops); // c:2506
5418 }
5419 } else {
5420 ret = bin_zmodload_load(table, nam, args, ops); // c:2508
5421 }
5422 }
5423 unqueue_signals(); // c:2515
5424 ret // c:2515
5425}
5426
5427/// Port of `bin_zmodload_alias()` from `Src/module.c:2515`. — C decl `bin_zmodload_alias(char *nam, char **args, Options ops)`.
5428///
5429/// `zmodload -A [-L|-R] [name=alias ...]`. Three modes:
5430/// - no args: list all module aliases (`-L` = long form).
5431/// - `-R name`: remove alias `name` (must already be MOD_ALIAS).
5432/// - `name=target`: install/replace alias `name` pointing at `target`.
5433/// Detects self-cycles before committing.
5434/// WARNING: param names don't match C — Rust=(table, nam, args, ops) vs C=(nam, args, ops)
5435pub fn bin_zmodload_alias(
5436 table: &mut modulestab,
5437 nam: &str,
5438 args: &[String],
5439 ops: &options,
5440) -> i32 {
5441 // c:2515
5442 /*
5443 * TODO: while it would be too nasty to have aliases, as opposed
5444 * to real loadable modules, with dependencies --- just what would
5445 * we need to load when, exactly? --- there is in principle no objection
5446 * to making it possible to force an alias onto an existing unloaded
5447 * module which has dependencies. This would simply transfer
5448 * the dependencies down the line to the aliased-to module name.
5449 * This is actually useful, since then you can alias zsh/zle=mytestzle
5450 * to load another version of zle. But then what happens when the
5451 * alias is removed? Do you transfer the dependencies back? And
5452 * suppose other names are aliased to the same file? It might be
5453 * kettle of fish best left unwormed.
5454 */ // c:2517-2529
5455
5456 // c:2532-2541 — no args: list aliases.
5457 // C body:
5458 // if (!*args) {
5459 // if (OPT_ISSET(ops,'R')) {
5460 // zwarnnam(nam, "no module alias to remove");
5461 // return 1;
5462 // }
5463 // scanhashtable(modulestab, 1, MOD_ALIAS, 0,
5464 // modulestab->printnode,
5465 // OPT_ISSET(ops,'L') ? PRINTMOD_LIST : 0);
5466 // return 0;
5467 // }
5468 //
5469 // scanhashtable args: sorted=1, INCLUDE=MOD_ALIAS, EXCLUDE=0.
5470 // Only MOD_ALIAS entries pass, walked in name order. Each entry
5471 // dispatched through printmodulenode (the alias-emit arm).
5472 if args.is_empty() {
5473 if OPT_ISSET(ops, b'R') {
5474 // c:2533
5475 zwarnnam(nam, "no module alias to remove"); // c:2534
5476 return 1; // c:2535
5477 }
5478 let listflags = if OPT_ISSET(ops, b'L') {
5479 PRINTMOD_LIST
5480 } else {
5481 0
5482 };
5483 let mut names: Vec<&String> = table
5484 .modules
5485 .iter()
5486 .filter(|(_, m)| (m.node.flags & MOD_ALIAS) != 0) // c:2537 INCLUDE
5487 .map(|(n, _)| n)
5488 .collect();
5489 names.sort(); // c:2537 sorted=1
5490 for name in names {
5491 let m = &table.modules[name];
5492 let line = printmodulenode(name, m, listflags);
5493 if !line.is_empty() {
5494 println!("{}", line);
5495 }
5496 }
5497 return 0; // c:2540
5498 }
5499
5500 // c:2543 — for each arg, parse name=alias and dispatch.
5501 for arg in args {
5502 // c:2544-2547 — split at '='
5503 let (lhs, aliasname): (&str, Option<&str>) = match arg.find('=') {
5504 Some(eq) => (&arg[..eq], Some(&arg[eq + 1..])),
5505 None => (arg.as_str(), None),
5506 };
5507 // c:2548 — modname_ok check on the LHS
5508 if modname_ok(lhs) == 0 {
5509 // c:2548
5510 zwarnnam(nam, &format!("invalid module name `{}'", lhs)); // c:2549
5511 return 1; // c:2550
5512 }
5513 if OPT_ISSET(ops, b'R') {
5514 // c:2552
5515 // -R: remove alias path.
5516 if aliasname.is_some() {
5517 // c:2553
5518 zwarnnam(
5519 nam,
5520 &format!("bad syntax for removing module alias: {}", lhs),
5521 ); // c:2554
5522 return 1; // c:2556
5523 }
5524 // c:2558 — find_module(lhs, 0, NULL)
5525 match table.modules.get(lhs) {
5526 Some(m) => {
5527 if (m.node.flags & MOD_ALIAS) == 0 {
5528 // c:2560
5529 zwarnnam(nam, &format!("module is not an alias: {}", lhs)); // c:2561
5530 return 1; // c:2562
5531 }
5532 table.modules.remove(lhs); // c:2564 delete_module
5533 }
5534 None => {
5535 zwarnnam(nam, &format!("no such module alias: {}", lhs)); // c:2566
5536 return 1; // c:2567
5537 }
5538 }
5539 } else {
5540 // No -R: install/replace alias OR list one.
5541 if let Some(target) = aliasname {
5542 // c:2570
5543 if modname_ok(target) == 0 {
5544 // c:2572
5545 zwarnnam(nam, &format!("invalid module name `{}'", target)); // c:2573
5546 return 1; // c:2574
5547 }
5548 // c:2576-2584 — cycle detection: walk alias chain
5549 let mut mname = target;
5550 let mut depth = 0;
5551 loop {
5552 if depth > 256 {
5553 break;
5554 }
5555 depth += 1;
5556 if mname == lhs {
5557 // c:2577
5558 zwarnnam(nam, &format!("module alias would refer to itself: {}", lhs)); // c:2578
5559 return 1; // c:2580
5560 }
5561 match table.modules.get(mname) {
5562 Some(m) if (m.node.flags & MOD_ALIAS) != 0 => {
5563 mname = m.alias.as_deref().unwrap_or("");
5564 }
5565 _ => break,
5566 }
5567 }
5568 // c:2585-2596 — install or replace
5569 if let Some(m) = table.modules.get_mut(lhs) {
5570 if (m.node.flags & MOD_ALIAS) == 0 {
5571 // c:2587
5572 zwarnnam(nam, &format!("module is not an alias: {}", lhs)); // c:2588
5573 return 1; // c:2589
5574 }
5575 m.alias = Some(target.to_string()); // c:2591/2597
5576 } else {
5577 let mut m = module::new(lhs); // c:2593 zshcalloc
5578 m.node.flags = MOD_ALIAS; // c:2594
5579 m.alias = Some(target.to_string()); // c:2597
5580 table.modules.insert(lhs.to_string(), m); // c:2595
5581 }
5582 } else {
5583 // c:2599-2611 — list one alias.
5584 // C body:
5585 // if ((m = find_module(*args, 0, NULL))) {
5586 // if (m->node.flags & MOD_ALIAS)
5587 // modulestab->printnode(&m->node,
5588 // OPT_ISSET(ops,'L')
5589 // ? PRINTMOD_LIST : 0);
5590 // else { zwarnnam(...); return 1; }
5591 // } else { zwarnnam(...); return 1; }
5592 match table.modules.get(lhs) {
5593 Some(m) if (m.node.flags & MOD_ALIAS) != 0 => {
5594 // c:2601-2603 — printnode dispatch
5595 let listflags = if OPT_ISSET(ops, b'L') {
5596 PRINTMOD_LIST
5597 } else {
5598 0
5599 };
5600 let line = printmodulenode(lhs, m, listflags);
5601 if !line.is_empty() {
5602 println!("{}", line);
5603 }
5604 }
5605 Some(_) => {
5606 zwarnnam(nam, &format!("module is not an alias: {}", lhs)); // c:2605
5607 return 1; // c:2606
5608 }
5609 None => {
5610 zwarnnam(nam, &format!("no such module alias: {}", lhs)); // c:2609
5611 return 1; // c:2610
5612 }
5613 }
5614 }
5615 }
5616 }
5617 0 // c:2616
5618}
5619
5620/// Port of `bin_zmodload_exist()` from `Src/module.c:2623`. — C decl `bin_zmodload_exist(UNUSED(char *nam), char **args, Options ops)`.
5621///
5622/// C body:
5623/// ```c
5624/// bin_zmodload_exist(UNUSED(char *nam), char **args, Options ops)
5625/// {
5626/// Module m;
5627/// if (!*args) {
5628/// scanhashtable(modulestab, 1, 0, 0, modulestab->printnode,
5629/// OPT_ISSET(ops,'A') ? PRINTMOD_EXIST|PRINTMOD_ALIAS :
5630/// PRINTMOD_EXIST);
5631/// return 0;
5632/// } else {
5633/// int ret = 0;
5634/// for (; !ret && *args; args++) {
5635/// if (!(m = find_module(*args, FINDMOD_ALIASP, NULL))
5636/// || !m->u.handle
5637/// || (m->node.flags & MOD_UNLOAD))
5638/// ret = 1;
5639/// }
5640/// return ret;
5641/// }
5642/// }
5643/// ```
5644///
5645/// `zmodload [-A]` lists or tests module presence. Returns 0 if all
5646/// named modules exist (or if no args, after listing); 1 if any
5647/// named module is missing/unloading.
5648/// WARNING: param names don't match C — Rust=(table, _nam, args, _ops) vs C=(nam, args, ops)
5649pub fn bin_zmodload_exist(
5650 table: &mut modulestab,
5651 _nam: &str,
5652 args: &[String],
5653 ops: &options,
5654) -> i32 {
5655 // c:2623
5656 if args.is_empty() {
5657 // c:2627
5658 // c:2628-2630 — scanhashtable(modulestab, 1, 0, 0,
5659 // modulestab->printnode,
5660 // OPT_ISSET(ops,'A')
5661 // ? PRINTMOD_EXIST|PRINTMOD_ALIAS
5662 // : PRINTMOD_EXIST);
5663 // Sorted walk (the `1` 2nd arg) over the full table (no
5664 // INCLUDE/EXCLUDE filters), dispatching every entry through
5665 // printmodulenode. -A toggles PRINTMOD_ALIAS so alias entries
5666 // emit their alias target alongside the existence line.
5667 let printflags = if OPT_ISSET(ops, b'A') {
5668 PRINTMOD_EXIST | PRINTMOD_ALIAS
5669 } else {
5670 PRINTMOD_EXIST
5671 };
5672 let mut names: Vec<&String> = table.modules.keys().collect();
5673 names.sort(); // c:2628 sorted=1
5674 for name in names {
5675 let m = &table.modules[name];
5676 let line = printmodulenode(name, m, printflags);
5677 if !line.is_empty() {
5678 println!("{}", line);
5679 }
5680 }
5681 return 0; // c:2631
5682 }
5683 // c:2633-2640 — for each arg, test existence.
5684 // C:
5685 // for (; !ret && *args; args++) {
5686 // if (!(m = find_module(*args, FINDMOD_ALIASP, NULL))
5687 // || !m->u.handle
5688 // || (m->node.flags & MOD_UNLOAD))
5689 // ret = 1;
5690 // }
5691 // The `!m->u.handle` clause is union-typed in C: for static-link
5692 // modules it reads `m->u.linked` (non-NULL when linked, NULL when
5693 // pre-registered but not yet bound); for dynamic modules it's the
5694 // dlopen handle. Translate to: handle.is_none() && linked.is_none()
5695 // — both representations of "no live binding."
5696 let mut ret: i32 = 0;
5697 for arg in args {
5698 // c:2635
5699 if ret != 0 {
5700 break;
5701 }
5702 let canon = match find_module(table, arg, FINDMOD_ALIASP) {
5703 // c:2636
5704 Some(n) => n,
5705 None => {
5706 ret = 1; // c:2639
5707 continue;
5708 }
5709 };
5710 let live = match table.modules.get(&canon) {
5711 Some(m) => {
5712 // c:2637 — `!m->u.handle` (union-semantics).
5713 let bound = m.handle.is_some() || m.linked.is_some();
5714 // c:2638 — `(m->node.flags & MOD_UNLOAD)`.
5715 let unloading = (m.node.flags & MOD_UNLOAD) != 0;
5716 bound && !unloading
5717 }
5718 None => false,
5719 };
5720 if !live {
5721 ret = 1; // c:2639
5722 }
5723 }
5724 ret // c:2641
5725}
5726
5727/// Port of `bin_zmodload_dep()` from `Src/module.c:2649`. — C decl `bin_zmodload_dep(UNUSED(char *nam), char **args, Options ops)`.
5728///
5729/// `zmodload -d [-u] [target [dep ...]]`. Three modes:
5730/// - `-u target` removes all deps from target; `-u target d1 d2` removes
5731/// only those.
5732/// - no args lists all dependencies.
5733/// - `target dep1 ...` adds each dep to target's dependency list.
5734/// WARNING: param names don't match C — Rust=(table, _nam, args, ops) vs C=(nam, args, ops)
5735pub fn bin_zmodload_dep(table: &mut modulestab, _nam: &str, args: &[String], ops: &options) -> i32 {
5736 // c:2649
5737 if OPT_ISSET(ops, b'u') {
5738 // c:2652
5739 // c:2654 — `const char *tnam = *args++;`
5740 if args.is_empty() {
5741 return 0;
5742 }
5743 let tnam = &args[0];
5744 let rest = &args[1..];
5745 // c:2655 — `find_module(tnam, FINDMOD_ALIASP, &tnam)`
5746 let canon = match find_module(table, tnam, FINDMOD_ALIASP) {
5747 Some(n) => n,
5748 None => return 0, // c:2656-2657
5749 };
5750 if let Some(m) = table.modules.get_mut(&canon) {
5751 if !rest.is_empty() && m.deps.is_some() {
5752 // c:2658-2671 — remove specific deps.
5753 // C body c:2659-2667 walks args, finds each in deps,
5754 // removes the matched node; the inner break ends the
5755 // current arg's search but the outer while continues
5756 // to the next arg. Mirrors with a for-loop.
5757 let deps = m.deps.as_mut().unwrap();
5758 for to_remove in rest {
5759 // c:2659 do { ... } while(*++args)
5760 if let Some(pos) = deps.iter().position(|d| d == to_remove) {
5761 deps.delete_node(pos); // c:2664 remnode
5762 }
5763 }
5764 // c:2668-2671 — if deps now empty, free the list.
5765 if deps.is_empty() {
5766 m.deps = None;
5767 }
5768 } else if m.deps.is_some() {
5769 // c:2672-2676 — no specific deps given: clear all.
5770 m.deps = None;
5771 }
5772 // c:2678-2679 — `if (!m->deps && !m->u.handle) delete_module(m);`
5773 // Static-link analog of `!u.handle` is `!MOD_INIT_B` (boot
5774 // hasn't run = no loaded handle). Without the MOD_INIT_B
5775 // gate, the prior port deleted any module whose deps got
5776 // cleared, dropping the modulestab entries for already-
5777 // loaded modules (zsh/main, zsh/watch, etc.) the moment
5778 // their dep list went empty.
5779 let no_deps = m.deps.is_none();
5780 let no_handle = (m.node.flags & MOD_INIT_B) == 0;
5781 if no_deps && no_handle {
5782 table.modules.remove(&canon); // c:2679 delete_module
5783 }
5784 }
5785 return 0; // c:2680
5786 }
5787 // c:2681 — `else if (!args[0] || !args[1])`: list-mode (one or all).
5788 if args.is_empty() || args.len() == 1 {
5789 // c:2682-2691 — `int depflags = OPT_ISSET(ops,'L')
5790 // ? PRINTMOD_DEPS|PRINTMOD_LIST : PRINTMOD_DEPS;`
5791 let depflags = if OPT_ISSET(ops, b'L') {
5792 PRINTMOD_DEPS | PRINTMOD_LIST
5793 } else {
5794 PRINTMOD_DEPS
5795 };
5796 if !args.is_empty() {
5797 // c:2685-2687 — single-name list:
5798 // if ((m = modulestab->getnode2(modulestab, args[0])))
5799 // modulestab->printnode(&m->node, depflags);
5800 if let Some(m) = table.modules.get(&args[0]) {
5801 let line = printmodulenode(&args[0], m, depflags);
5802 if !line.is_empty() {
5803 println!("{}", line);
5804 }
5805 }
5806 } else {
5807 // c:2688-2691 — full sorted scan.
5808 // scanhashtable(modulestab, 1, 0, 0, printnode, depflags);
5809 let mut names: Vec<&String> = table.modules.keys().collect();
5810 names.sort(); // c:2689 sorted=1
5811 for name in names {
5812 let m = &table.modules[name];
5813 let line = printmodulenode(name, m, depflags);
5814 if !line.is_empty() {
5815 println!("{}", line);
5816 }
5817 }
5818 }
5819 return 0; // c:2692
5820 }
5821 // c:2693-2701 — add deps: args[0] target, args[1..] deps to add.
5822 let target = &args[0];
5823 for dep in &args[1..] {
5824 add_dep(table, target, dep); // c:2699
5825 }
5826 0 // c:2700 (ret stays 0)
5827}
5828
5829/// Port of `printautoparams()` from `Src/module.c:2710`. — C decl `printautoparams(HashNode hn, int lon)`.
5830///
5831/// C body:
5832/// ```c
5833/// printautoparams(HashNode hn, int lon)
5834/// {
5835/// Param pm = (Param) hn;
5836/// if (pm->node.flags & PM_AUTOLOAD) {
5837/// if (lon)
5838/// printf("zmodload -ap %s %s\n", pm->u.str, pm->node.nam);
5839/// else
5840/// printf("%s (%s)\n", pm->node.nam, pm->u.str);
5841/// }
5842/// }
5843/// ```
5844///
5845/// Hash-table scan callback for `zmodload -ap` listing. Rust port
5846/// takes a `(name, module, flags)` triple instead of a HashNode ptr
5847/// since zshrs's autoload-params live in `ModuleTable.autoload_params`.
5848/// WARNING: param names don't match C — Rust=(name, module, flags, lon) vs C=(hn, lon)
5849pub fn printautoparams(name: &str, module: &str, flags: u32, lon: i32) {
5850 // c:2710
5851 if (flags & PM_AUTOLOAD) != 0 {
5852 // c:2710
5853 if lon != 0 {
5854 // c:2715
5855 // c:2716 — printf("zmodload -ap %s %s\n", pm->u.str, pm->node.nam);
5856 println!("zmodload -ap {} {}", module, name);
5857 } else {
5858 // c:2718 — printf("%s (%s)\n", pm->node.nam, pm->u.str);
5859 println!("{} ({})", name, module);
5860 }
5861 }
5862}
5863
5864/// Port of `bin_zmodload_auto()` from `Src/module.c:2726`. — C decl `bin_zmodload_auto(char *nam, char **args, Options ops)`.
5865///
5866/// `zmodload [-c] [-p] [-f] [-a] module name [name ...]` —
5867/// register-autoload of builtins/conditions/params/mathfns. C body
5868/// (80 lines) walks the appropriate dispatch table per opt flag.
5869///
5870/// `-c` lists/registers conditions, `-p` parameters, `-f` math ported,
5871/// default is builtins. `-L` toggles long-form listing.
5872///
5873/// Static-link path: registers via `add_autoaliasbuiltin` /
5874/// `add_autoparam` / `add_automathfunc` already ported. Without a
5875/// module name (just `-a`), runs the listing scan via `autoloadscan`
5876/// or its conddef/param/mathfn equivalents.
5877/// WARNING: param names don't match C — Rust=(table, _nam, args, ops) vs C=(nam, args, ops)
5878pub fn bin_zmodload_auto(
5879 table: &mut modulestab,
5880 _nam: &str,
5881 args: &[String],
5882 ops: &options,
5883) -> i32 {
5884 // c:2726
5885 let fchar: char; // c:2726
5886 let _flags: i32 = if OPT_ISSET(ops, b'i') { FEAT_IGNORE } else { 0 }; // c:2728
5887
5888 // c:2731-2753 — conditions branch (-c).
5889 // C body:
5890 // if (!*args) {
5891 // Conddef p;
5892 // for (p = condtab; p; p = p->next) {
5893 // if (p->module) {
5894 // if (OPT_ISSET(ops,'L')) {
5895 // fputs("zmodload -ac", stdout);
5896 // if (p->flags & CONDF_INFIX) putchar('I');
5897 // printf(" %s %s\n", p->module, p->name);
5898 // } else {
5899 // if (p->flags & CONDF_INFIX) fputs("infix ", stdout);
5900 // else fputs("post ", stdout);
5901 // printf("%s (%s)\n", p->name, p->module);
5902 // }
5903 // }
5904 // }
5905 // return 0;
5906 // }
5907 // C walks condtab in registration order (linked-list head→tail).
5908 // The Rust port walks CONDTAB (same shape, Vec instead of list).
5909 if OPT_ISSET(ops, b'c') {
5910 fchar = if OPT_ISSET(ops, b'I') { 'C' } else { 'c' };
5911 let _ = fchar;
5912 if args.is_empty() {
5913 let snap: Vec<(String, i32, String)> = {
5914 let tab = CONDTAB.lock().unwrap();
5915 tab.iter()
5916 // c:2737 — `if (p->module)` skips entries without
5917 // an owning module (built-in cond defs use NULL).
5918 .filter_map(|p| {
5919 p.module
5920 .as_ref()
5921 .map(|m| (p.name.clone(), p.flags, m.clone()))
5922 })
5923 .collect()
5924 };
5925 let l_flag = OPT_ISSET(ops, b'L');
5926 for (name, flags, module) in snap {
5927 if l_flag {
5928 // c:2738-2742 — `zmodload -ac[I] MODULE NAME`.
5929 if (flags & CONDF_INFIX) != 0 {
5930 // c:2740
5931 println!("zmodload -acI {} {}", module, name);
5932 } else {
5933 println!("zmodload -ac {} {}", module, name);
5934 }
5935 } else {
5936 // c:2743-2748 — `infix NAME (MODULE)` /
5937 // `post NAME (MODULE)`.
5938 let kind = if (flags & CONDF_INFIX) != 0 {
5939 "infix"
5940 } else {
5941 "post"
5942 };
5943 println!("{} {} ({})", kind, name, module);
5944 }
5945 }
5946 return 0;
5947 }
5948 } else if OPT_ISSET(ops, b'p') {
5949 // c:2755-2761 — params branch.
5950 // C body:
5951 // if (!*args) {
5952 // /* list autoloaded parameters */
5953 // scanhashtable(paramtab, 1, 0, 0, printautoparams,
5954 // OPT_ISSET(ops,'L'));
5955 // return 0;
5956 // }
5957 // The `sorted=1` (2nd arg) walks the table in name order.
5958 // `printautoparams` (c:2710) checks `pm->flags & PM_AUTOLOAD`
5959 // and emits either `zmodload -ap MODULE NAME` (under `-L`)
5960 // or `NAME (MODULE)` form. Module name is read from
5961 // `pm->u.str`, which `add_autoparam` (c:1218) sets to the
5962 // module that registered the autoload stub.
5963 if args.is_empty() {
5964 let lon = if OPT_ISSET(ops, b'L') { 1 } else { 0 };
5965 let entries: Vec<(String, u32, String)> = {
5966 let tab = crate::ported::params::paramtab()
5967 .read()
5968 .expect("paramtab poisoned");
5969 let mut v: Vec<(String, u32, String)> = tab
5970 .iter()
5971 .filter(|(name, p)| {
5972 (p.node.flags as u32 & crate::ported::zsh_h::PM_AUTOLOAD) != 0
5973 // !!! RUST-ONLY CONDITION !!!
5974 // C has ONE storage for a module parameter: the
5975 // PM_AUTOLOAD stub `add_autoparam` (c:1218) plants
5976 // is the paramtab node, and `addparamdef`
5977 // (c:1065) only replaces it with the real special
5978 // when the module actually loads. zshrs seeds every
5979 // `partab[]` special EAGERLY at startup
5980 // (`vm_helper::init_partab_params`), which wipes
5981 // the stub's PM_AUTOLOAD bit, so it models
5982 // "still an unresolved stub" as a side-set instead
5983 // (`vm_helper::MATERIALIZED_MODULE_PARAMS`, whose
5984 // doc comment carries the same warning). That set
5985 // is per-NAME, exactly like C's flag: after
5986 // `: ${keymaps}`, zsh drops `keymaps` from
5987 // `zmodload -ap` but keeps its sibling `widgets`,
5988 // which a per-MODULE "is it loaded" test cannot
5989 // express. Read it here so `-ap`/`-apL` see the
5990 // same stubs C's PM_AUTOLOAD scan does.
5991 || crate::vm_helper::module_param_is_autoload_stub(name)
5992 })
5993 .map(|(name, p)| {
5994 (
5995 name.clone(),
5996 p.node.flags as u32 | crate::ported::zsh_h::PM_AUTOLOAD,
5997 // c:2716/2718 read the module off `pm->u.str`.
5998 // An eagerly-seeded special has no `u_str`; its
5999 // owning module is the one the boot
6000 // `autofeatures` replay recorded.
6001 p.u_str.clone().unwrap_or_else(|| {
6002 table.autoload_params.get(name).cloned().unwrap_or_default()
6003 }),
6004 )
6005 })
6006 .collect();
6007 v.sort_by(|a, b| a.0.cmp(&b.0)); // c:2758 sorted=1
6008 v
6009 };
6010 for (name, flags, module) in entries {
6011 printautoparams(&name, &module, flags, lon); // c:2758
6012 }
6013 return 0;
6014 }
6015 } else if OPT_ISSET(ops, b'f') {
6016 // c:2763-2778 — math-function branch (-f).
6017 // C body:
6018 // if (!*args) {
6019 // MathFunc p;
6020 // for (p = mathfuncs; p; p = p->next) {
6021 // if (!(p->flags & MFF_USERFUNC) && p->module) {
6022 // if (OPT_ISSET(ops,'L')) {
6023 // fputs("zmodload -af", stdout);
6024 // printf(" %s %s\n", p->module, p->name);
6025 // } else
6026 // printf("%s (%s)\n", p->name, p->module);
6027 // }
6028 // }
6029 // return 0;
6030 // }
6031 // C walks `mathfuncs` (file-static linked list). Rust port
6032 // walks MATHFUNCS (same shape, Vec). The MFF_USERFUNC filter
6033 // excludes `functions -M` user math from the autoload list.
6034 if args.is_empty() {
6035 let snap: Vec<(String, String)> = {
6036 let tab = MATHFUNCS.lock().unwrap();
6037 tab.iter()
6038 // c:2769 — `!(p->flags & MFF_USERFUNC) && p->module`.
6039 .filter_map(|p| {
6040 if (p.flags & MFF_USERFUNC) != 0 {
6041 return None;
6042 }
6043 p.module.as_ref().map(|m| (p.name.clone(), m.clone()))
6044 })
6045 .collect()
6046 };
6047 let l_flag = OPT_ISSET(ops, b'L');
6048 for (name, module) in snap {
6049 if l_flag {
6050 // c:2770-2772 — `zmodload -af MODULE NAME`.
6051 println!("zmodload -af {} {}", module, name);
6052 } else {
6053 // c:2774 — `NAME (MODULE)`.
6054 println!("{} ({})", name, module);
6055 }
6056 }
6057 return 0;
6058 }
6059 } else {
6060 // Default: builtins branch
6061 if args.is_empty() {
6062 // c:Src/module.c:2756 — `scanhashtable(autoloadtab, 1, 0,
6063 // 0, ...)`. The sorted=1 arg sorts entries by name before
6064 // dispatch. zshrs's HashMap iteration is unordered, so the
6065 // 27 auto-loaded entries came out in random order. Bug
6066 // #222 in docs/BUGS.md. Sort by name to match zsh.
6067 let mut entries: Vec<(&String, &String)> = table.autoload_builtins.iter().collect();
6068 entries.sort_by(|a, b| a.0.cmp(b.0));
6069 for (name, module) in entries {
6070 autoloadscan(
6071 name,
6072 module,
6073 0,
6074 if OPT_ISSET(ops, b'L') { PRINT_LIST } else { 0 },
6075 );
6076 }
6077 return 0;
6078 }
6079 }
6080
6081 // c:2791-2805 — register/unregister via autofeatures with
6082 // FEAT_AUTOALL. (Earlier zshrs revision inserted into the
6083 // autoload_* maps directly, bypassing autofeatures — so
6084 // `m->autoloads` bookkeeping and the add_autobin/add_autoparam/…
6085 // canonical-table dispatch never ran.)
6086 let fchar: u8 = if OPT_ISSET(ops, b'c') {
6087 if OPT_ISSET(ops, b'I') {
6088 b'C' // c:2754 fchar = OPT_ISSET(ops,'I') ? 'C' : 'c'
6089 } else {
6090 b'c'
6091 }
6092 } else if OPT_ISSET(ops, b'p') {
6093 b'p' // c:2762
6094 } else if OPT_ISSET(ops, b'f') {
6095 b'f' // c:2779
6096 } else {
6097 b'b' // c:2789
6098 };
6099 let mut flags = FEAT_AUTOALL; // c:2791
6100 if OPT_ISSET(ops, b'i') {
6101 flags |= FEAT_IGNORE; // c:2792-2793
6102 }
6103 if OPT_ISSET(ops, b'u') {
6104 /* remove autoload */
6105 // c:2795
6106 flags |= FEAT_REMOVE; // c:2796
6107 // c:2797 — `modnam = NULL;` — every arg is a feature name.
6108 return autofeatures(table, _nam, None, args, fchar, flags); // c:2805
6109 }
6110 /* add autoload */
6111 // c:2799
6112 let modnam = &args[0]; // c:2800
6113 // c:2802-2803 — `if (args[1]) args++;` — with a single arg the
6114 // module name doubles as the feature arg (C quirk; autofeatures
6115 // then rejects the `/` in it).
6116 let feat_args: &[String] = if args.len() > 1 { &args[1..] } else { args };
6117 autofeatures(table, _nam, Some(modnam), feat_args, fchar, flags) // c:2805
6118}
6119
6120/// Port of `unload_named_module()` from `Src/module.c:2924`. — C decl `unload_named_module(char *modname, char *nam, int silent)`.
6121///
6122/// C body:
6123/// ```c
6124/// int
6125/// unload_named_module(char *modname, char *nam, int silent)
6126/// {
6127/// const char *mname;
6128/// Module m;
6129/// int ret = 0;
6130/// m = find_module(modname, FINDMOD_ALIASP, &mname);
6131/// if (m) {
6132/// int i, del = 0;
6133/// Module dm;
6134/// for (i = 0; i < modulestab->hsize; i++) {
6135/// for (dm = (Module)modulestab->nodes[i]; dm;
6136/// dm = (Module)dm->node.next) {
6137/// LinkNode dn;
6138/// if (!dm->deps || !dm->u.handle)
6139/// continue;
6140/// for (dn = firstnode(dm->deps); dn; incnode(dn)) {
6141/// if (!strcmp((char *) getdata(dn), mname)) {
6142/// if (dm->node.flags & MOD_UNLOAD)
6143/// del = 1;
6144/// else {
6145/// zwarnnam(nam, "module %s is in use ...");
6146/// return 1;
6147/// }
6148/// }
6149/// }
6150/// }
6151/// }
6152/// if (del) m->wrapper++;
6153/// if (unload_module(m)) ret = 1;
6154/// if (del) m->wrapper--;
6155/// } else if (!silent) {
6156/// zwarnnam(nam, "no such module %s", modname);
6157/// ret = 1;
6158/// }
6159/// return ret;
6160/// }
6161/// ```
6162///
6163/// Now that unload_module is faithfully ported (2b986c9e8b), this
6164/// wraps it with the C dep-walk that gates the unload on whether
6165/// any other loaded module depends on the target. If a dependent
6166/// is already tagged MOD_UNLOAD, set del=1 to bracket the unload
6167/// with `m->wrapper++` / `m->wrapper--` so unload_module's wrapper
6168/// branch (c:2839-2842) defers correctly.
6169/// WARNING: param names don't match C — Rust=(table, name, nam, silent) vs C=(modname, nam, silent)
6170pub fn unload_named_module(table: &mut modulestab, name: &str, nam: &str, silent: i32) -> i32 {
6171 // c:2924
6172 // c:2930 — `m = find_module(modname, FINDMOD_ALIASP, &mname);`
6173 // Returns the canonical (alias-resolved) name; we drive
6174 // unload_module against that, matching C's `m` pointer.
6175 let mname = match find_module(table, name, FINDMOD_ALIASP) {
6176 Some(n) => n,
6177 None => {
6178 // c:2959-2961 — !m branch: silent gate.
6179 if silent == 0 {
6180 crate::ported::utils::zwarnnam(nam, &format!("no such module {}", name));
6181 return 1;
6182 }
6183 return 0;
6184 }
6185 };
6186
6187 // c:2932 — `int del = 0;`
6188 let mut del = 0;
6189
6190 // c:2935-2952 — scan every module's deps for `mname`.
6191 // Snapshot first so we don't double-borrow the modules HashMap
6192 // when we mutate via unload_module below.
6193 let candidates: Vec<(String, i32, bool, Vec<String>)> = table
6194 .modules
6195 .iter()
6196 .filter_map(|(other_name, other)| {
6197 // c:2939 — `if (!dm->deps || !dm->u.handle) continue;`
6198 // Static-link analog of `u.handle` is MOD_INIT_B ("boot_
6199 // ran"), the same analog `require_module` (c:2352) and
6200 // `getpmmodule` (c:1069) use. It is NOT MOD_LINKED, which
6201 // only means "this name has compiled-in code" and is set on
6202 // every boot node: with that gate, the never-loaded
6203 // `zsh/compctl` / `zsh/complete` / `zsh/computil` /
6204 // `zsh/zleparameter` boot nodes counted as live dependents,
6205 // so a bare `zmodload -u zsh/zle` (nothing loaded) failed
6206 // with "in use by another module" where zsh silently
6207 // returns 0.
6208 // The MOD_UNLOAD check stays INSIDE the inner loop: a
6209 // dependent already flagged for deferred unload must still
6210 // be seen there so it can set `del = 1`.
6211 let deps = other.deps.as_ref()?;
6212 if (other.node.flags & MOD_INIT_B) == 0 {
6213 return None;
6214 }
6215 // Note: C checks `u.handle` (loaded handle) not
6216 // `!MOD_UNLOAD` here — MOD_UNLOAD-flagged modules still
6217 // get scanned because the inner branch needs to see them
6218 // to set `del = 1`.
6219 Some((
6220 other_name.clone(),
6221 other.node.flags,
6222 (other.node.flags & MOD_UNLOAD) != 0,
6223 deps.iter().cloned().collect(),
6224 ))
6225 })
6226 .collect();
6227
6228 for (_other_name, _other_flags, other_unloading, other_deps) in candidates {
6229 for dep in &other_deps {
6230 if dep != &mname {
6231 continue; // c:2942
6232 }
6233 if other_unloading {
6234 // c:2943-2944 — dependent already marked MOD_UNLOAD →
6235 // we'll be cascade-unloading it after the target.
6236 del = 1;
6237 } else {
6238 // c:2945-2948 — live dependent: refuse the unload.
6239 crate::ported::utils::zwarnnam(
6240 nam,
6241 &format!(
6242 "module {} is in use by another module and cannot be unloaded",
6243 mname
6244 ),
6245 );
6246 return 1;
6247 }
6248 }
6249 }
6250
6251 // c:2953-2954 — `if (del) m->wrapper++;`. The wrapper++/--
6252 // bracket gates unload_module's wrapper branch (c:2839-2842):
6253 // with wrapper > 0, unload_module sets MOD_UNLOAD and returns
6254 // rather than running finish. The actual cascade fires via the
6255 // recursive deferred-dep walk inside unload_module.
6256 if del != 0 {
6257 if let Some(m) = table.modules.get_mut(&mname) {
6258 m.wrapper += 1;
6259 }
6260 }
6261
6262 // c:2955-2956 — `if (unload_module(m)) ret = 1;`. Rust's
6263 // unload_module returns bool (true=success). Map to C ret:
6264 // false → 1, true → 0.
6265 let mut ret = if !table.unload_module(&mname) { 1 } else { 0 };
6266
6267 // c:2957-2958 — `if (del) m->wrapper--;`
6268 if del != 0 {
6269 if let Some(m) = table.modules.get_mut(&mname) {
6270 m.wrapper -= 1;
6271 }
6272 }
6273 let _ = silent;
6274 let _ = &mut ret;
6275 ret // c:2964
6276}
6277
6278/// Port of `bin_zmodload_load()` from `Src/module.c:2971`. — C decl `bin_zmodload_load(char *nam, char **args, Options ops)`.
6279///
6280/// C body:
6281/// ```c
6282/// bin_zmodload_load(char *nam, char **args, Options ops)
6283/// {
6284/// int ret = 0;
6285/// if(OPT_ISSET(ops,'u')) {
6286/// for(; *args; args++) {
6287/// if (unload_named_module(*args, nam, OPT_ISSET(ops,'i')))
6288/// ret = 1;
6289/// }
6290/// return ret;
6291/// } else if(!*args) {
6292/// scanhashtable(modulestab, ..., PRINTMOD_LIST);
6293/// return 0;
6294/// } else {
6295/// for (; *args; args++) {
6296/// int tmpret = require_module(*args, NULL, OPT_ISSET(ops,'s'));
6297/// if (tmpret && ret != 1) ret = tmpret;
6298/// }
6299/// return ret;
6300/// }
6301/// }
6302/// ```
6303///
6304/// `zmodload [-u] [args]`: load, unload, or list modules.
6305/// WARNING: param names don't match C — Rust=(table, nam, args, ops) vs C=(nam, args, ops)
6306pub fn bin_zmodload_load(table: &mut modulestab, nam: &str, args: &[String], ops: &options) -> i32 {
6307 // c:2971
6308 let mut ret: i32 = 0;
6309 if OPT_ISSET(ops, b'u') {
6310 // c:2974
6311 // c:2976-2979 — unload loop
6312 for arg in args {
6313 if unload_named_module(table, arg, nam, OPT_ISSET(ops, b'i') as i32) != 0 {
6314 ret = 1;
6315 }
6316 }
6317 return ret; // c:2980
6318 } else if args.is_empty() {
6319 // c:2981
6320 // c:2983-2985 — list modules:
6321 // `scanhashtable(modulestab, 1, 0, MOD_UNLOAD|MOD_ALIAS,
6322 // modulestab->printnode,
6323 // OPT_ISSET(ops,'L') ? PRINTMOD_LIST : 0);`
6324 // The 4th arg to scanhashtable is the EXCLUDE mask — entries
6325 // with `MOD_UNLOAD` or `MOD_ALIAS` set are skipped. The
6326 // surviving names are routed through `printmodulenode`,
6327 // which itself gates the visible-line emission on
6328 // `m->u.handle` (=> `MOD_INIT_B` in zshrs) so registered-
6329 // but-unloaded modules drop out. Plain `zmodload` (no `-L`)
6330 // passes `flags=0`; `zmodload -L` passes `PRINTMOD_LIST`.
6331 // Previous Rust impl printed every key in `table.modules`
6332 // unconditionally, which leaked the 32 statically-registered
6333 // builtin entries (#76 in docs/BUGS.md).
6334 let listflags = if OPT_ISSET(ops, b'L') {
6335 PRINTMOD_LIST
6336 } else {
6337 0
6338 };
6339 let mut names: Vec<&String> = table.modules.keys().collect();
6340 names.sort(); // c:scanhashtable sorted=1 arg
6341 for name in names {
6342 let m = &table.modules[name]; // c:154 printnode call
6343 if (m.node.flags & (MOD_UNLOAD | MOD_ALIAS)) != 0 {
6344 continue; // c:2983 EXCLUDE mask
6345 }
6346 let line = printmodulenode(name, m, listflags);
6347 if !line.is_empty() {
6348 println!("{}", line);
6349 }
6350 }
6351 return 0; // c:2986
6352 } else {
6353 // c:2989-2992 — load loop
6354 for arg in args {
6355 let tmpret = require_module(table, arg, None, OPT_ISSET(ops, b's') as i32, false); // c:2990
6356 if tmpret != 0 && ret != 1 {
6357 // c:2991
6358 ret = tmpret;
6359 }
6360 // PFA-SMR: one event per `zmodload MODULE` load form
6361 // (only the bare-load path; listing/-u/-d/-A handled
6362 // upstream and never reaches here). Pass empty flags
6363 // since the per-feature -F path lives in
6364 // bin_zmodload_features. Record even on failure so
6365 // replay sees the user's intent.
6366 #[cfg(feature = "recorder")]
6367 if crate::recorder::is_enabled() {
6368 let ctx = crate::recorder::recorder_ctx_global();
6369 crate::recorder::emit_zmodload(arg, "", ctx);
6370 }
6371 }
6372 ret
6373 }
6374}
6375
6376/// Port of `bin_zmodload_features()` from `Src/module.c:3003`. — C decl `bin_zmodload_features(const char *nam, char **args, Options ops)`.
6377///
6378/// `zmodload -F [-L|-l|-P|-a|-m|-i] module [+/-feature ...]` —
6379/// per-feature enable/disable for an already-loaded module.
6380///
6381/// C body (~135 lines) handles:
6382/// - no module: list all modules with their features (`-L` long form,
6383/// `-l` show all enables, `-a` show autoloads).
6384/// - `-P` requires a module name; lists patterns.
6385/// - `-m` interprets each feature as a glob pattern.
6386/// - default: `+feature` enables, `-feature` disables, then calls
6387/// `do_module_features` to apply.
6388/// WARNING: param names don't match C — Rust=(table, nam, args, ops) vs C=(nam, args, ops)
6389pub fn bin_zmodload_features(
6390 table: &mut modulestab,
6391 nam: &str,
6392 args: &[String],
6393 ops: &options,
6394) -> i32 {
6395 // c:3003
6396 let modname = args.first(); // c:3003
6397 let rest_args = if args.is_empty() {
6398 &args[..]
6399 } else {
6400 &args[1..]
6401 };
6402
6403 // c:3010-3030 — no-module-name listing branch.
6404 // C body:
6405 // if (modname)
6406 // args++;
6407 // else if (OPT_ISSET(ops,'L')) {
6408 // int printflags = PRINTMOD_LIST|PRINTMOD_FEATURES;
6409 // if (OPT_ISSET(ops,'P')) {
6410 // zwarnnam(nam, "-P is only allowed with a module name");
6411 // return 1;
6412 // }
6413 // if (OPT_ISSET(ops,'l')) printflags |= PRINTMOD_LISTALL;
6414 // if (OPT_ISSET(ops,'a')) printflags |= PRINTMOD_AUTO;
6415 // scanhashtable(modulestab, 1, 0, MOD_ALIAS,
6416 // modulestab->printnode, printflags);
6417 // return 0;
6418 // }
6419 // if (!modname) {
6420 // zwarnnam(nam, "-F requires a module name");
6421 // return 1;
6422 // }
6423 if modname.is_none() {
6424 if OPT_ISSET(ops, b'L') {
6425 // c:3012
6426 // c:3014-3016 — `-P` check must fire BEFORE the listing
6427 // dispatch. Without modname, -P is illegal.
6428 if OPT_ISSET(ops, b'P') {
6429 zwarnnam(nam, "-P is only allowed with a module name"); // c:3015
6430 return 1; // c:3016
6431 }
6432 // c:3013 / c:3018-3021 — assemble PRINTMOD_LIST|PRINTMOD_FEATURES
6433 // [|PRINTMOD_LISTALL][|PRINTMOD_AUTO]
6434 let mut printflags = PRINTMOD_LIST | PRINTMOD_FEATURES;
6435 if OPT_ISSET(ops, b'l') {
6436 printflags |= PRINTMOD_LISTALL; // c:3019
6437 }
6438 if OPT_ISSET(ops, b'a') {
6439 printflags |= PRINTMOD_AUTO; // c:3021
6440 }
6441 // c:3022-3023 — `scanhashtable(modulestab, 1, 0, MOD_ALIAS,
6442 // printnode, printflags);`
6443 // sorted=1, INCLUDE=0 (all), EXCLUDE=MOD_ALIAS (skip aliases).
6444 let mut names: Vec<String> = table
6445 .modules
6446 .iter()
6447 .filter(|(_, m)| (m.node.flags & MOD_ALIAS) == 0) // c:3022 EXCLUDE
6448 .map(|(n, _)| n.clone())
6449 .collect();
6450 names.sort(); // c:3022 sorted=1
6451 for name in &names {
6452 if printflags & PRINTMOD_AUTO != 0 {
6453 // c:229-231 / c:238-251 — autoload form; printmodulenode
6454 // covers this branch fully.
6455 let m = &table.modules[name];
6456 let line = printmodulenode(name, m, printflags);
6457 if !line.is_empty() {
6458 println!("{}", line);
6459 }
6460 continue;
6461 }
6462 // c:218 / c:232-235 — loaded-module + features gate:
6463 // `if (features_module(m, &features) ||
6464 // enables_module(m, &enables) || !*features) return;`
6465 // printmodulenode has no &table handle, so the FEATURES
6466 // dispatch happens here (see its c:252-263 comment).
6467 let loaded = {
6468 let m = &table.modules[name];
6469 (m.node.flags & MOD_INIT_B) != 0 && (m.node.flags & MOD_UNLOAD) == 0
6470 };
6471 if !loaded {
6472 continue;
6473 }
6474 let mut features: Vec<String> = Vec::new();
6475 if features_module(table, name, &mut features) != 0 || features.is_empty() {
6476 continue; // c:233-235
6477 }
6478 let mut enables_opt: Option<Vec<i32>> = None;
6479 if enables_module(table, name, &mut enables_opt) != 0 {
6480 continue; // c:233-235
6481 }
6482 let enables = enables_opt.unwrap_or_else(|| vec![0; features.len()]);
6483 // c:237-245 — `printf("zmodload "); fputs("-F ", stdout);`
6484 let mut line = String::from("zmodload -F ");
6485 if name.starts_with('-') {
6486 line.push_str("-- "); // c:244-245
6487 }
6488 line.push_str(&crate::ported::utils::quotedzputs(name)); // c:246
6489 // c:252-262 — per-feature tail: LISTALL emits ` +f`/` -f`,
6490 // plain -L skips disabled and emits ` f`.
6491 for (f, on) in features.iter().zip(enables.iter()) {
6492 if printflags & PRINTMOD_LISTALL != 0 {
6493 line.push_str(if *on != 0 { " +" } else { " -" }); // c:256
6494 } else if *on == 0 {
6495 continue; // c:258
6496 } else {
6497 line.push(' '); // c:260
6498 }
6499 line.push_str(&crate::ported::utils::quotedzputs(f)); // c:261
6500 }
6501 println!("{}", line);
6502 }
6503 return 0; // c:3024
6504 }
6505 zwarnnam(nam, "-F requires a module name"); // c:3028
6506 return 1; // c:3029
6507 }
6508
6509 let modname = modname.unwrap();
6510
6511 // c:3032-3047 — `-m` glob-pattern branch.
6512 // C body:
6513 // if (OPT_ISSET(ops,'m')) {
6514 // patprogs = zhalloc(arrlen(args)*sizeof(Patprog));
6515 // for (argp = args; *argp; argp++, patprogp++) {
6516 // if (*arg == '+' || *arg == '-') arg++;
6517 // tokenize(arg);
6518 // *patprogp = patcompile(arg, 0, 0);
6519 // }
6520 // } else patprogs = NULL;
6521 let patprogs = if OPT_ISSET(ops, b'm') {
6522 Some(
6523 rest_args
6524 .iter()
6525 .map(|raw| {
6526 let arg = raw
6527 .strip_prefix('+')
6528 .or_else(|| raw.strip_prefix('-'))
6529 .unwrap_or(raw); // c:3044-3045
6530 let mut pat_src = arg.to_string();
6531 crate::ported::glob::tokenize(&mut pat_src); // c:3046
6532 crate::ported::pattern::patcompile(&pat_src, 0, None) // c:3047
6533 })
6534 .collect::<Vec<_>>(),
6535 )
6536 } else {
6537 None
6538 };
6539 // `-m` also reaches autofeatures / do_module_features through
6540 // FEAT_PATTERN_ARGS (see its doc comment).
6541
6542 // c:3049-3226 — `-l/-L/-e` arm: list features one per line with
6543 // +/- (-l), as a `zmodload -F` statement (-L), or test existence
6544 // (-e).
6545 if OPT_ISSET(ops, b'l') || OPT_ISSET(ops, b'L') || OPT_ISSET(ops, b'e') {
6546 let param: Option<String> = OPT_ARG_SAFE(ops, b'P').map(|s| s.to_string()); // c:3060
6547 // c:3062 — `m = find_module(modname, FINDMOD_ALIASP, NULL);`
6548 let resolved = find_module(table, modname, FINDMOD_ALIASP);
6549
6550 // c:3063-3107 — `-a` sub-arm: autoload listing/testing.
6551 if OPT_ISSET(ops, b'a') {
6552 // c:3067-3068 — `if (!m || !m->autoloads) return 1;`
6553 let autoloads: Vec<String> = match resolved
6554 .as_ref()
6555 .and_then(|r| table.modules.get(r))
6556 .and_then(|m| m.autoloads.as_ref())
6557 {
6558 Some(al) => al.iter().cloned().collect(),
6559 None => return 1,
6560 };
6561 if OPT_ISSET(ops, b'e') {
6562 // c:3070-3085 — each arg must (mis)match per its +/- sense.
6563 for fstr in rest_args {
6564 let (sense, name) = match fstr.strip_prefix('+') {
6565 Some(rest) => (true, rest), // c:3074
6566 None => match fstr.strip_prefix('-') {
6567 Some(rest) => (false, rest), // c:3076-3078
6568 None => (true, fstr.as_str()),
6569 },
6570 };
6571 // c:3080-3082 — `(linknodebystring(...) != NULL) != sense`
6572 if autoloads.iter().any(|a| a == name) != sense {
6573 return 1; // c:3082
6574 }
6575 }
6576 return 0; // c:3084
6577 }
6578 if let Some(p) = param {
6579 // c:3086-3088 / c:3098-3100 — collect into the array,
6580 // then `setaparam(param, arrset)`.
6581 // c:3103-3106
6582 if crate::ported::params::setaparam(&p, autoloads).is_none() {
6583 return 1; // c:3105
6584 }
6585 return 0; // c:3106
6586 }
6587 if OPT_ISSET(ops, b'L') {
6588 // c:3089-3091 — `printf("zmodload -aF %s%c", ...)`
6589 let rname = resolved.as_deref().unwrap_or(modname);
6590 print!(
6591 "zmodload -aF {}{}",
6592 crate::ported::utils::quotedzputs(rname),
6593 if autoloads.is_empty() { '\n' } else { ' ' }
6594 );
6595 // c:3092-3098 — space-separated, final '\n'.
6596 for (i, al) in autoloads.iter().enumerate() {
6597 print!("{}{}", al, if i + 1 < autoloads.len() { ' ' } else { '\n' });
6598 }
6599 } else {
6600 // c:3093-3097 — one per line.
6601 for al in &autoloads {
6602 println!("{}", al);
6603 }
6604 }
6605 return 0; // c:3107
6606 }
6607
6608 // c:3108-3112 — `if (!m || !m->u.handle || (m->node.flags &
6609 // MOD_UNLOAD))`. zshrs maps "u.handle installed" to MOD_INIT_B
6610 // (see the printmodulenode comment at c:218-241 above).
6611 let loaded = resolved
6612 .as_ref()
6613 .and_then(|r| table.modules.get(r))
6614 .map(|m| (m.node.flags & MOD_INIT_B) != 0 && (m.node.flags & MOD_UNLOAD) == 0)
6615 .unwrap_or(false);
6616 if !loaded {
6617 if !OPT_ISSET(ops, b'e') {
6618 zwarnnam(nam, &format!("module `{}' is not yet loaded", modname));
6619 // c:3110
6620 }
6621 return 1; // c:3111
6622 }
6623 let rname = resolved.unwrap();
6624
6625 // c:3113-3118 — `features_module(m, &features)`.
6626 let mut features: Vec<String> = Vec::new();
6627 if features_module(table, &rname, &mut features) != 0 {
6628 if !OPT_ISSET(ops, b'e') {
6629 zwarnnam(
6630 nam,
6631 &format!("module `{}' does not support features", rname), // c:3115
6632 );
6633 }
6634 return 1; // c:3117
6635 }
6636 // c:3119-3124 — `enables_module(m, &enables)`.
6637 let mut enables_opt: Option<Vec<i32>> = None;
6638 if enables_module(table, &rname, &mut enables_opt) != 0 {
6639 /* this shouldn't ever happen, so don't silence this error */
6640 // c:3120
6641 zwarnnam(
6642 nam,
6643 &format!("error getting enabled features for module `{}'", rname), // c:3121
6644 );
6645 return 1; // c:3123
6646 }
6647 let enables: Vec<i32> = enables_opt.unwrap_or_else(|| vec![0; features.len()]);
6648
6649 // c:3125-3155 — validate every feature argument.
6650 for (iarg, raw) in rest_args.iter().enumerate() {
6651 // c:3127-3135 — strip +/- into `on`.
6652 let (on, arg): (i32, &str) = match raw.strip_prefix('-') {
6653 Some(rest) => (0, rest),
6654 None => match raw.strip_prefix('+') {
6655 Some(rest) => (1, rest),
6656 None => (-1, raw.as_str()),
6657 },
6658 };
6659 let mut found = 0;
6660 for (fp, ep) in features.iter().zip(enables.iter()) {
6661 // c:3142-3143 — `patprogs ? pattry(patprogs[iarg], *fp) : !strcmp(arg, *fp)`
6662 let hit = match patprogs.as_ref() {
6663 Some(pp) => pp[iarg]
6664 .as_ref()
6665 .is_some_and(|p| crate::ported::pattern::pattry(p, fp)),
6666 None => arg == fp,
6667 };
6668 if hit {
6669 // c:3140-3142 — for -e, check given state, if any.
6670 if OPT_ISSET(ops, b'e') && on != -1 && on != (ep & 1) {
6671 return 1; // c:3142
6672 }
6673 found += 1;
6674 if patprogs.is_none() {
6675 break; // c:3149-3150
6676 }
6677 }
6678 }
6679 if found == 0 {
6680 // c:3148-3154
6681 if !OPT_ISSET(ops, b'e') {
6682 zwarnnam(
6683 nam,
6684 &if patprogs.is_some() {
6685 format!("module `{}' has no feature matching: `{}'", modname, raw)
6686 } else {
6687 format!("module `{}' has no such feature: `{}'", modname, raw)
6688 },
6689 );
6690 }
6691 return 1; // c:3153
6692 }
6693 }
6694 if OPT_ISSET(ops, b'e') {
6695 /* yep, everything we want exists */
6696 // c:3156
6697 return 0; // c:3157
6698 }
6699
6700 let opt_big_l = OPT_ISSET(ops, b'L');
6701 let opt_small_l = OPT_ISSET(ops, b'l');
6702 // c:3186-3194 / c:3164-3172 — arg filter helpers. The C print
6703 // loop compares the UNSTRIPPED arg (`!strcmp(*fp, *argp)`,
6704 // c:3193) while the param-count loop compares stripped
6705 // (`!strcmp(*fp, arg)`, c:3170). Ported as written.
6706 let matches_stripped = |f: &str| -> bool {
6707 rest_args.is_empty()
6708 || rest_args.iter().enumerate().any(|(iarg, raw)| {
6709 let arg = raw
6710 .strip_prefix('+')
6711 .or_else(|| raw.strip_prefix('-'))
6712 .unwrap_or(raw);
6713 match patprogs.as_ref() {
6714 Some(pp) => pp[iarg]
6715 .as_ref()
6716 .is_some_and(|p| crate::ported::pattern::pattry(p, f)),
6717 None => f == arg,
6718 }
6719 })
6720 };
6721 let matches_unstripped = |f: &str| -> bool {
6722 rest_args.is_empty()
6723 || rest_args.iter().enumerate().any(|(iarg, raw)| match patprogs.as_ref() {
6724 Some(pp) => pp[iarg]
6725 .as_ref()
6726 .is_some_and(|p| crate::ported::pattern::pattry(p, f)),
6727 None => f == raw,
6728 })
6729 };
6730
6731 let mut arrset: Option<Vec<String>> = None;
6732 if param.is_some() {
6733 // c:3158-3183 — size pass folded away (Vec grows); keep the
6734 // same membership filter.
6735 arrset = Some(Vec::new());
6736 } else if opt_big_l {
6737 // c:3184-3185 — `printf("zmodload -F %s ", m->node.nam);`
6738 print!("zmodload -F {} ", crate::ported::utils::quotedzputs(&rname));
6739 }
6740 // c:3186-3219 — main feature emit loop.
6741 for (i, (f, ep)) in features.iter().zip(enables.iter()).enumerate() {
6742 if param.is_some() {
6743 if !matches_stripped(f) {
6744 continue; // c:3170-3173 stripped compare
6745 }
6746 } else if !matches_unstripped(f) {
6747 continue; // c:3193-3196 unstripped compare
6748 }
6749 let onoff: &str = if opt_big_l && !opt_small_l {
6750 // c:3198-3200
6751 if *ep == 0 {
6752 continue; // c:3199
6753 }
6754 ""
6755 } else if *ep != 0 {
6756 "+" // c:3203
6757 } else {
6758 "-" // c:3205
6759 };
6760 if let Some(ref mut arr) = arrset {
6761 arr.push(format!("{}{}", onoff, f)); // c:3208 bicat
6762 } else {
6763 // c:3210-3216 — term ' ' while a next feature EXISTS in
6764 // the full array (fp[1]), even if it won't be printed.
6765 let term = if opt_big_l && i + 1 < features.len() {
6766 ' '
6767 } else {
6768 '\n'
6769 };
6770 print!("{}{}{}", onoff, crate::ported::utils::quotedzputs(f), term);
6771 }
6772 }
6773 if let (Some(p), Some(arr)) = (param, arrset) {
6774 // c:3220-3224 — `setaparam(param, arrset)`.
6775 if crate::ported::params::setaparam(&p, arr).is_none() {
6776 return 1; // c:3223
6777 }
6778 }
6779 return 0; // c:3225
6780 }
6781
6782 // c:3227-3229 — `-P` is illegal without -l/-L/-e.
6783 if OPT_ISSET(ops, b'P')
6784 && !(OPT_ISSET(ops, b'l') || OPT_ISSET(ops, b'L') || OPT_ISSET(ops, b'e'))
6785 {
6786 zwarnnam(nam, "-P can only be used with -l or -L"); // c:3228
6787 return 1; // c:3229
6788 }
6789
6790 // c:3230-3247 — `-a` arm: route through autofeatures with
6791 // FEAT_IGNORE (the autoload-feature registration path).
6792 if OPT_ISSET(ops, b'a') {
6793 // c:3231-3234 — `-m` incompatible with `-a`.
6794 if OPT_ISSET(ops, b'm') {
6795 zwarnnam(nam, "-m cannot be used with -a"); // c:3232
6796 return 1; // c:3233
6797 }
6798 // c:3246 — `return autofeatures(nam, modname, args, 0, FEAT_IGNORE);`
6799 // FEAT_IGNORE is hard-coded here because marking-for-autoload
6800 // is separate from enable/disable (per the c:3236-3244 comment).
6801 return autofeatures(table, nam, Some(modname), rest_args, 0, FEAT_IGNORE);
6802 }
6803
6804 // c:3249-3260 — default arm: build Feature_enables array from
6805 // `+name`/`-name` args, then `require_module(modname, features,
6806 // OPT_ISSET(ops,'s'))`.
6807 //
6808 // C builds a fep[] array with str + (optional) patprog pairs.
6809 // The Rust port flattens to a `Vec<String>`; the `-m` decision rides
6810 // along as the `pat` argument of require_module (FEAT_PATTERN_ARGS).
6811 let feats: Vec<String> = rest_args.to_vec();
6812 // c:3252-3260 —
6813 // fep = features = (Feature_enables)zhalloc((arrlen(args)+1)*sizeof(*fep));
6814 // while (*args) { fep->str = *args++; fep->pat = …; fep++; }
6815 // fep->str = NULL; fep->pat = NULL;
6816 // return require_module(modname, features, OPT_ISSET(ops,'s'));
6817 // The array is ALWAYS allocated in this arm, so `enablesarr` reaching
6818 // do_module_features (c:2077) is never NULL even when `zmodload -F
6819 // MODULE` is given no feature words — and NULL is what selects C's
6820 // "enable all features" arm (c:2105-2112). An empty non-NULL array
6821 // therefore means "enable NOTHING", which is exactly what
6822 // `zmodload -F zsh/datetime; zmodload -lF zsh/datetime` must report
6823 // (`-b:strftime -p:EPOCHSECONDS …`, V04features.ztst:1). Mapping the
6824 // empty case to `None` collapsed the two and enabled everything.
6825 let features_arg = Some(feats.as_slice());
6826 require_module(
6827 table,
6828 modname,
6829 features_arg,
6830 OPT_ISSET(ops, b's') as i32,
6831 OPT_ISSET(ops, b'm'), // c:3258 `fep->pat = patprogs ? *patprogs++ : NULL`
6832 ) // c:3260
6833}
6834
6835/// Port of `ensurefeature()` from `Src/module.c:3415`. — C decl `ensurefeature(const char *modname, const char *prefix, const char *feature)`.
6836///
6837/// C body:
6838/// ```c
6839/// ensurefeature(const char *modname, const char *prefix, const char *feature)
6840/// {
6841/// char *f;
6842/// struct feature_enables features[2];
6843/// if (!feature)
6844/// return require_module(modname, NULL, 0);
6845/// f = dyncat(prefix, feature);
6846/// features[0].str = f;
6847/// features[0].pat = NULL;
6848/// features[1].str = NULL;
6849/// features[1].pat = NULL;
6850/// return require_module(modname, features, 0);
6851/// }
6852/// ```
6853/// WARNING: param names don't match C — Rust=(table, modname, prefix, feature) vs C=(modname, prefix, feature)
6854pub fn ensurefeature(
6855 table: &mut modulestab,
6856 modname: &str,
6857 prefix: &str,
6858 feature: Option<&str>,
6859) -> i32 {
6860 // c:3415
6861 match feature {
6862 // c:3420-3421 — `if (!feature) return require_module(modname, NULL, 0);`
6863 None => require_module(table, modname, None, 0, false),
6864 Some(f) => {
6865 // c:3422-3428 — build single-element features[2] array.
6866 let combined = crate::ported::string::dyncat(prefix, f); // c:3422
6867 let arr = vec![combined];
6868 require_module(table, modname, Some(&arr), 0, false) // c:3428
6869 }
6870 }
6871}
6872
6873/// Port of `autofeatures()` from `Src/module.c:3437`. — C decl `autofeatures(const char *cmdnam, const char *module, char **features, int prefchar, int defflags)`.
6874///
6875/// C body is ~140 lines. Top-level structure:
6876/// ```c
6877/// autofeatures(const char *cmdnam, const char *module, char **features,
6878/// int prefchar, int defflags)
6879/// {
6880/// // Resolve module, fetch its features+enables tables.
6881/// // For each feature in `features`:
6882/// // parse `+`/`-` prefix → add/remove
6883/// // parse type prefix (b/c/C/p/f) → fchar
6884/// // dispatch to add_aliasbuiltin / add_autocondition /
6885/// // add_autoparam / add_automathfunc / del_* matching
6886/// }
6887/// ```
6888///
6889/// Static-link path: registers each `module:feature` pair into the
6890/// matching `table.autoload_*` map. Honors `+`/`-` prefix for
6891/// add/remove, and the type prefix or `prefchar` arg for routing.
6892/// WARNING: param names don't match C — Rust=(table, _cmdnam, module, features, prefchar, defflags) vs C=(cmdnam, module, features, prefchar, defflags)
6893pub fn autofeatures(
6894 table: &mut modulestab,
6895 cmdnam: &str,
6896 module: Option<&str>,
6897 features: &[String],
6898 prefchar: u8,
6899 defflags: i32,
6900) -> i32 {
6901 // c:3437
6902 let mut ret: i32 = 0;
6903
6904 // c:3445-3453 — resolve `defm` up front (FINDMOD_ALIASP|
6905 // FINDMOD_CREATE); if its union slot is populated (loaded —
6906 // MOD_INIT_B in zshrs, see the printmodulenode c:218-241 note)
6907 // fetch the feature + enable tables for the c:3558-3577 checks.
6908 let mut modfeatures: Option<Vec<String>> = None;
6909 let mut modenables: Vec<i32> = Vec::new();
6910 let defm_name: Option<String> = match module {
6911 Some(modn) => {
6912 let resolved = find_module(table, modn, FINDMOD_ALIASP | FINDMOD_CREATE);
6913 if let Some(ref r) = resolved {
6914 let booted = table
6915 .modules
6916 .get(r)
6917 .map(|m| (m.node.flags & MOD_INIT_B) != 0)
6918 .unwrap_or(false);
6919 if booted {
6920 // c:3449-3451
6921 let mut f: Vec<String> = Vec::new();
6922 if features_module(table, r, &mut f) == 0 {
6923 let mut e: Option<Vec<i32>> = None;
6924 let _ = enables_module(table, r, &mut e);
6925 modenables = e.unwrap_or_else(|| vec![0; f.len()]);
6926 modfeatures = Some(f);
6927 }
6928 }
6929 }
6930 resolved
6931 }
6932 None => None, // c:3454-3455 `defm = NULL`
6933 };
6934
6935 for feature in features {
6936 let s = feature.as_str();
6937 let mut add: bool = true; // c:3466 / c:3477 default `add = 1`
6938 let mut flags = defflags; // c:3458 `flags = defflags`
6939
6940 // `feature_full` is the string C keeps in `m->autoloads`
6941 // (c:3584/3597 `ztrdup(feature)`): the arg after the +/-
6942 // strip, type prefix included — prefchar mode synthesizes it
6943 // via `sprintf(feature, "%c:%s", fchar, fnam)` (c:3468-3469).
6944 let prefixed: String;
6945 let (fchar, fnam, feature_full): (u8, &str, &str) = if prefchar != 0 {
6946 // c:3461-3470 — `prefchar` mode: feature is bare name with
6947 // no `+`/`-` / `b:` prefix; fchar comes from the arg.
6948 prefixed = format!("{}:{}", prefchar as char, s); // c:3468-3469
6949 (prefchar, s, prefixed.as_str()) // c:3467-3468
6950 } else {
6951 // c:3471-3490 — parse `+`/`-` then the `b:`/`c:`/`C:`/`p:`/`f:`
6952 // type prefix.
6953 let mut t = s;
6954 if let Some(rest) = t.strip_prefix('-') {
6955 // c:3473
6956 add = false;
6957 t = rest;
6958 } else if let Some(rest) = t.strip_prefix('+') {
6959 // c:3478
6960 t = rest;
6961 }
6962 // c:3482-3487 — bad format check: `!*feature || feature[1] != ':'`
6963 let bytes = t.as_bytes();
6964 if bytes.is_empty() || bytes.len() < 2 || bytes[1] != b':' {
6965 // c:3483-3486 — zwarnnam + ret=1 + continue.
6966 crate::ported::utils::zwarnnam(
6967 cmdnam,
6968 &format!("bad format for autoloadable feature: `{}'", t),
6969 );
6970 ret = 1; // c:3485
6971 continue; // c:3486
6972 }
6973 // c:3488-3489 — `fnam = feature + 2; fchar = feature[0];`
6974 (bytes[0], &t[2..], t)
6975 };
6976
6977 // c:3491-3492 — `if (flags & FEAT_REMOVE) add = 0;`
6978 if (flags & FEAT_REMOVE) != 0 {
6979 add = false;
6980 }
6981
6982 let typnam: &str; // c:3457
6983 let _ = typnam; // referenced below for fnam validation
6984 let typnam = match fchar {
6985 // c:3494-3522 — switch (fchar): map each type-letter to
6986 // the typnam used in the diagnostic + add/del fn dispatch.
6987 b'b' => "builtin", // c:3495-3498
6988 b'c' | b'C' => {
6989 if fchar == b'C' {
6990 flags |= FEAT_INFIX; // c:3501
6991 }
6992 "condition" // c:3505
6993 }
6994 b'f' => "math function", // c:3508-3511
6995 b'p' => "parameter", // c:3513-3516
6996 _ => {
6997 // c:3518-3522 — `zwarnnam(cmdnam, "bad autoloadable
6998 // feature type: `%c'", fchar); ret = 1; continue;`
6999 crate::ported::utils::zwarnnam(
7000 cmdnam,
7001 &format!("bad autoloadable feature type: `{}'", fchar as char),
7002 );
7003 ret = 1; // c:3521
7004 continue; // c:3522
7005 }
7006 };
7007
7008 // c:3525-3529 — reject `/` in the feature name.
7009 if fnam.contains('/') {
7010 crate::ported::utils::zwarnnam(
7011 cmdnam,
7012 &format!("{}: `/' is invalid in a {}", fnam, typnam),
7013 );
7014 ret = 1;
7015 continue;
7016 }
7017
7018 // c:3531-3553 — resolve module: if `module` arg is None,
7019 // walk every module's `m->autoloads` list looking for the
7020 // feature; if found, that's the owning module. C's
7021 // `m->autoloads` per-module list isn't modelled in the Rust
7022 // port — the autoload_* HashMaps store `feature → module`
7023 // directly, so we can derive the owning module from those.
7024 // When `module` arg IS set, use it (c:3553 `m = defm;`).
7025 let modname_owned: String = match module {
7026 Some(m) => m.to_string(), // c:3553
7027 None => {
7028 // c:3537-3551 — search for the owning module across all
7029 // autoload maps; fall back to error if not found.
7030 let map = match fchar {
7031 b'b' => &table.autoload_builtins,
7032 b'c' | b'C' => &table.autoload_conditions,
7033 b'p' => &table.autoload_params,
7034 b'f' => &table.autoload_mathfuncs,
7035 _ => unreachable!(),
7036 };
7037 match map.get(fnam).cloned() {
7038 Some(m) => m,
7039 None => {
7040 if (flags & FEAT_IGNORE) == 0 {
7041 // c:3546-3549
7042 ret = 1;
7043 crate::ported::utils::zwarnnam(
7044 cmdnam,
7045 &format!("{}: no such {}", fnam, typnam),
7046 );
7047 }
7048 continue; // c:3550
7049 }
7050 }
7051 }
7052 };
7053 let modname = modname_owned.as_str();
7054
7055 // c:3554 `subret = 0;` — the m->autoloads maintenance below can
7056 // set it to ±2 on the remove-missing path (c:3614).
7057 let mut autoload_subret: i32 = 0;
7058 // Owning module node: the alias-resolved defm when a module arg
7059 // was given (C `m = defm`, c:3553), else the searched-up name.
7060 let owner: &str = match (module.is_some(), defm_name.as_deref()) {
7061 (true, Some(r)) => r,
7062 _ => modname,
7063 };
7064 if add {
7065 // c:3558-3577 — if the module is already loaded, the feature
7066 // must exist in its table; if it's already enabled there is
7067 // nothing to mark.
7068 if module.is_some() {
7069 if let Some(ref mf) = modfeatures {
7070 match mf.iter().position(|f| f == feature_full) {
7071 None => {
7072 // c:3566-3570
7073 crate::ported::utils::zwarnnam(
7074 cmdnam,
7075 &format!(
7076 "module `{}' has no such feature: `{}'",
7077 owner, feature_full
7078 ),
7079 );
7080 ret = 1;
7081 continue;
7082 }
7083 Some(idx) => {
7084 if modenables.get(idx).copied().unwrap_or(0) != 0 {
7085 continue; // c:3572-3577 already provided
7086 }
7087 }
7088 }
7089 }
7090 }
7091 // c:3583-3603 — insert into m->autoloads in lexical order
7092 // (dup is "never an error", c:3590-3593).
7093 if let Some(m) = table.modules.get_mut(owner) {
7094 let list = m
7095 .autoloads
7096 .get_or_insert_with(crate::ported::linklist::znewlinklist);
7097 let mut insert_at: Option<usize> = Some(list.len()); // c:3602 append default
7098 for (i, existing) in list.iter().enumerate() {
7099 match feature_full.cmp(existing.as_str()) {
7100 std::cmp::Ordering::Equal => {
7101 insert_at = None; // c:3591-3593 already there
7102 break;
7103 }
7104 std::cmp::Ordering::Less => {
7105 insert_at = Some(i); // c:3595-3598
7106 break;
7107 }
7108 std::cmp::Ordering::Greater => {}
7109 }
7110 }
7111 if let Some(i) = insert_at {
7112 list.insert_at(i, feature_full.to_string());
7113 }
7114 }
7115 } else {
7116 // c:3605-3615 — `else if (m->autoloads) { remnode or
7117 // subret = FEAT_IGNORE ? -2 : 2; }`
7118 let removed = table
7119 .modules
7120 .get_mut(owner)
7121 .and_then(|m| m.autoloads.as_mut())
7122 .map(|list| {
7123 match list
7124 .iter()
7125 .position(|existing| existing.as_str() == feature_full)
7126 {
7127 Some(i) => {
7128 list.delete_node(i);
7129 true
7130 }
7131 None => false,
7132 }
7133 })
7134 .unwrap_or(false);
7135 if !removed {
7136 autoload_subret = if (flags & FEAT_IGNORE) != 0 { -2 } else { 2 };
7137 // c:3614
7138 }
7139 }
7140
7141 // c:3618-3619 — `if (subret == 0) subret = fn(module, fnam, flags);`
7142 // The fn does NOT run when the m->autoloads remove already
7143 // produced ±2. Dispatch through the per-type add/del fn so the
7144 // canonical tables (paramtab for `p:`, condtab for `c:`, etc.)
7145 // carry the PM_AUTOLOAD / CONDF flag bits expected by
7146 // downstream code (e.g. paramtypestr's `undefined NAME`).
7147 let subret = if autoload_subret != 0 {
7148 autoload_subret // c:3618 subret already set
7149 } else if add {
7150 match fchar {
7151 b'p' => add_autoparam(modname, fnam, flags),
7152 b'f' => add_automathfunc(table, modname, fnam, flags),
7153 b'b' => table.add_autobin(fnam, modname, flags),
7154 b'c' | b'C' => table.add_autocond(fnam, modname, flags),
7155 _ => unreachable!(),
7156 }
7157 } else {
7158 match fchar {
7159 b'p' => del_autoparam(modname, fnam, flags),
7160 b'f' => del_automathfunc(table, modname, fnam, flags),
7161 b'b' => table.del_autobin(fnam, flags),
7162 b'c' | b'C' => table.del_autocond(fnam, flags),
7163 _ => unreachable!(),
7164 }
7165 };
7166
7167 // !!! RUST-ONLY BOOKKEEPING — NO DIRECT C COUNTERPART !!!
7168 // C keeps ONE store per feature kind: the autoload stub IS the
7169 // `builtintab`/`condtab`/`paramtab`/`mathfuncs` entry, so
7170 // `add_autobin`/`del_autobin` (c:426/464) et al. are the only
7171 // writers. zshrs splits that into the canonical table PLUS the
7172 // `autoload_*` feature→module reverse maps the static-link
7173 // dispatch needs (`resolve_autoload_builtin`, printautoparams'
7174 // module fallback, autofeatures' own c:3535-3555 owner search).
7175 //
7176 // These MUST run AFTER the c:3618-3619 fn dispatch, never
7177 // before: `del_autobin` probes `autoload_builtins` as its
7178 // stand-in for C's `builtintab->getnode2(builtintab, bnam)`
7179 // (c:466), so removing the entry first made every
7180 // `zmodload -ub NAME` report `NAME: no such builtin` (subret 2)
7181 // for a builtin it had just autoloaded — V01zmodload.ztst
7182 // "Add/remove autoloaded builtin".
7183 //
7184 // Mirror the canonical table's outcome: insert only on a clean
7185 // add (subret 0), drop only when the delete actually happened
7186 // (subret 0, or -2 = "not there, FEAT_IGNORE" per c:3614).
7187 if add {
7188 if subret == 0 {
7189 match fchar {
7190 b'b' => {
7191 table
7192 .autoload_builtins
7193 .insert(fnam.to_string(), modname.to_string());
7194 }
7195 b'c' | b'C' => {
7196 table
7197 .autoload_conditions
7198 .insert(fnam.to_string(), modname.to_string());
7199 }
7200 b'p' => {
7201 table
7202 .autoload_params
7203 .insert(fnam.to_string(), modname.to_string());
7204 }
7205 b'f' => {
7206 table
7207 .autoload_mathfuncs
7208 .insert(fnam.to_string(), modname.to_string());
7209 }
7210 _ => unreachable!(),
7211 }
7212 }
7213 } else if subret == 0 || subret == -2 {
7214 match fchar {
7215 b'b' => {
7216 table.autoload_builtins.remove(fnam);
7217 }
7218 b'c' | b'C' => {
7219 table.autoload_conditions.remove(fnam);
7220 }
7221 b'p' => {
7222 table.autoload_params.remove(fnam);
7223 }
7224 b'f' => {
7225 table.autoload_mathfuncs.remove(fnam);
7226 }
7227 _ => unreachable!(),
7228 }
7229 }
7230
7231 // c:3621-3642 — per-error-code diagnostic.
7232 if subret != 0 && subret != -2 {
7233 ret = 1; // c:3624
7234 match subret {
7235 1 => {
7236 // c:3627
7237 crate::ported::utils::zwarnnam(
7238 cmdnam,
7239 &format!("failed to add {} `{}'", typnam, fnam),
7240 );
7241 }
7242 2 => {
7243 // c:3631
7244 crate::ported::utils::zwarnnam(
7245 cmdnam,
7246 &format!("{}: no such {}", fnam, typnam),
7247 );
7248 }
7249 3 => {
7250 // c:3635
7251 crate::ported::utils::zwarnnam(
7252 cmdnam,
7253 &format!("{}: {} is already defined", fnam, typnam),
7254 );
7255 }
7256 _ => { /* c:3638 no further message */ }
7257 }
7258 }
7259 }
7260 ret
7261}
7262
7263/// Port of `MathFunc mathfuncs;` from `Src/module.c:1258` — the
7264/// global head of the linked list of math functions. Both
7265/// autoloadable math ported (added by modules) and user math ported
7266/// (added by `functions -M`) live here.
7267///
7268/// C is a singly linked list with `mathfunc.next` chaining. The
7269/// Rust port stores entries in a `Vec` — the call sites only ever
7270/// walk linearly and erase by name, so the linked-list shape buys
7271/// nothing in safe Rust.
7272pub static MATHFUNCS: Lazy<Mutex<Vec<mathfunc>>> = // c:1258
7273 Lazy::new(|| Mutex::new(Vec::new()));
7274
7275/// Port of `setconddefs()` from `Src/module.c:754`. — C decl `setconddefs(char const *nam, Conddef c, int size, int *e)`.
7276/// Bulk add/delete of condition definitions:
7277/// the parallel `e[]` array selects per-entry add (`e[i] != 0`) vs delete
7278/// (`e[i] == 0`). Returns 1 if any individual op clashed, 0 if all clean.
7279pub fn setconddefs(
7280 nam: &str, // c:754
7281 c: &mut [conddef],
7282 e: Option<&[i32]>,
7283) -> i32 {
7284 let mut ret = 0; // c:758
7285 for (i, entry) in c.iter_mut().enumerate() {
7286 // c:760 while (size--)
7287 let want_add = e.map(|es| es[i] != 0).unwrap_or(false); // c:761 if (e && *e++) — NULL e deletes
7288 if want_add {
7289 if (entry.flags & CONDF_ADDED) != 0 {
7290 continue;
7291 } // c:763 already added
7292 let dup = conddef {
7293 next: None,
7294 name: entry.name.clone(),
7295 flags: entry.flags,
7296 handler: entry.handler,
7297 min: entry.min,
7298 max: entry.max,
7299 condid: entry.condid,
7300 module: entry.module.clone(),
7301 };
7302 if addconddef(dup) != 0 {
7303 // c:768 addconddef
7304 zwarnnam(
7305 nam, // c:769 zwarnnam
7306 &format!("name clash when adding condition `{}'", entry.name),
7307 );
7308 ret = 1;
7309 } else {
7310 entry.flags |= CONDF_ADDED; // c:773
7311 }
7312 } else {
7313 if (entry.flags & CONDF_ADDED) == 0 {
7314 continue;
7315 } // c:776
7316 if deleteconddef(entry) != 0 {
7317 // c:780 deleteconddef
7318 zwarnnam(
7319 nam, // c:781
7320 &format!("condition `{}' already deleted", entry.name),
7321 );
7322 ret = 1;
7323 } else {
7324 entry.flags &= !CONDF_ADDED; // c:785
7325 }
7326 }
7327 }
7328 ret // c:790
7329}
7330
7331/// Port of `setmathfuncs()` from `Src/module.c:1374`. — C decl `setmathfuncs(char const *nam, MathFunc f, int size, int *e)`.
7332/// Bulk add/delete of math-function definitions
7333/// via the parallel `e[]` selector array (same shape as setconddefs).
7334pub fn setmathfuncs(
7335 nam: &str, // c:1374
7336 f: &mut [mathfunc],
7337 e: Option<&[i32]>,
7338) -> i32 {
7339 let mut ret = 0; // c:1378
7340 for (i, entry) in f.iter_mut().enumerate() {
7341 // c:1380 while (size--)
7342 // c:1381 — `if (e && *e++)`: e == NULL means REMOVE all
7343 // (same contract as setbuiltins, c:497-503 doc: "e is either
7344 // NULL, in which case all builtins in the table are
7345 // removed"). The previous `.unwrap_or(true)` inverted the
7346 // None case into add-all.
7347 let want_add = e.map(|es| es[i] != 0).unwrap_or(false); // c:1381
7348 if want_add {
7349 if (entry.flags & MFF_ADDED) != 0 {
7350 continue;
7351 } // c:1383
7352 let dup = mathfunc {
7353 next: None,
7354 name: entry.name.clone(),
7355 flags: entry.flags,
7356 nfunc: entry.nfunc,
7357 sfunc: entry.sfunc,
7358 module: entry.module.clone(),
7359 minargs: entry.minargs,
7360 maxargs: entry.maxargs,
7361 funcid: entry.funcid,
7362 };
7363 if addmathfunc(dup) != 0 {
7364 // c:1388 addmathfunc
7365 zwarnnam(
7366 nam, // c:1389
7367 &format!("name clash when adding math function `{}'", entry.name),
7368 );
7369 ret = 1;
7370 } else {
7371 entry.flags |= MFF_ADDED; // c:1393
7372 // c:1388+1393 — C links `f` ITSELF into mathfuncs and
7373 // the flag-set above mutates that same (aliased) node.
7374 // The Rust insert is a by-value dup, so mirror the flag
7375 // onto the global-list entry; otherwise
7376 // del_automathfunc / getfeatureenables reading
7377 // MATHFUNCS never observe MFF_ADDED.
7378 if let Ok(mut gtab) = MATHFUNCS.lock() {
7379 if let Some(p) = gtab.iter_mut().find(|p| p.name == entry.name) {
7380 p.flags |= MFF_ADDED;
7381 }
7382 }
7383 }
7384 } else {
7385 if (entry.flags & MFF_ADDED) == 0 {
7386 continue;
7387 } // c:1396
7388 if deletemathfunc(entry) != 0 {
7389 // c:1400 deletemathfunc
7390 zwarnnam(
7391 nam, // c:1401
7392 &format!("math function `{}' already deleted", entry.name),
7393 );
7394 ret = 1;
7395 } else {
7396 // c:1356-1357 — C's deletemathfunc clears MFF_ADDED on
7397 // the (aliased) static mftab struct; Rust's global-list
7398 // entry is a dup, so clear it here on the caller's
7399 // record. Without this, a disable→re-enable cycle hits
7400 // the c:1383 `continue` and never re-registers.
7401 entry.flags &= !MFF_ADDED;
7402 }
7403 }
7404 }
7405 ret // c:1407
7406}
7407
7408/// Port of file-static `Conddef condtab;` from `Src/cond.c:21` — the
7409/// global condition-definition linked-list head consulted by `[[ ... ]]`
7410/// dispatch. Modules register custom conditions via `addconddef`; the
7411/// runtime walks `condtab` looking for the matching name+infix flag at
7412/// each `[[` evaluation. Rust port stores entries in a `Vec` (linear
7413/// add/remove + walk; same observable behaviour as C linked list).
7414pub static CONDTAB: Lazy<Mutex<Vec<conddef>>> = // c:cond.c:21
7415 Lazy::new(|| Mutex::new(Vec::new()));
7416
7417/// Port of `getconddef()` from `Src/module.c:648`. — C decl `getconddef(int inf, const char *name, int autol)`.
7418///
7419/// C body c:648-689:
7420/// ```c
7421/// Conddef
7422/// getconddef(int inf, const char *name, int autol)
7423/// {
7424/// Conddef p;
7425/// int f = 1;
7426/// char *lookup, *s;
7427/// lookup = dupstring(name);
7428/// if (!lookup) return NULL;
7429/// for (s = lookup; *s != '\0'; s++) {
7430/// if (*s == Dash) *s = '-';
7431/// }
7432/// do {
7433/// for (p = condtab; p; p = p->next) {
7434/// if ((!!inf == !!(p->flags & CONDF_INFIX)) &&
7435/// !strcmp(lookup, p->name))
7436/// break;
7437/// }
7438/// if (autol && p && p->module) {
7439/// if (f) {
7440/// (void)ensurefeature(p->module,
7441/// (p->flags & CONDF_INFIX) ? "C:" : "c:",
7442/// (p->flags & CONDF_AUTOALL) ? NULL : lookup);
7443/// f = 0;
7444/// p = NULL;
7445/// } else {
7446/// deleteconddef(p);
7447/// return NULL;
7448/// }
7449/// } else
7450/// break;
7451/// } while (!p);
7452/// return p;
7453/// }
7454/// ```
7455///
7456/// Returns a clone of the matched `conddef` (or `None` if absent).
7457/// `inf` selects between infix-style (`[[ A op B ]]`) and prefix-style
7458/// (`[[ -X arg ]]`) conditions — `CONDF_INFIX` on the entry must match.
7459/// `autol` triggers `ensurefeature` on autoload-stubs; the autoload
7460/// loop runs at most once (gated by `f`) — if the second iteration
7461/// still finds the entry, the stub is treated as a failed load and
7462/// removed via `deleteconddef`.
7463pub fn getconddef(inf: i32, name: &str, autol: i32, table: &mut modulestab) -> Option<conddef> {
7464 // c:648
7465 // c:655 — `lookup = dupstring(name)` then Dash → '-' substitution.
7466 let lookup: String = crate::ported::string::dupstring(name)
7467 .chars()
7468 .map(|c| {
7469 if c == crate::ported::zsh_h::Dash {
7470 '-'
7471 } else {
7472 c
7473 }
7474 })
7475 .collect();
7476 let mut f = 1; // c:651 `int f = 1;`
7477 loop {
7478 // c:663 do { ... } while (!p);
7479 // c:664-668 — walk condtab matching (!!inf == !!CONDF_INFIX) && name.
7480 let want_infix = inf != 0;
7481 let hit: Option<conddef> = {
7482 let tab = CONDTAB.lock().unwrap();
7483 tab.iter().find_map(|p| {
7484 let p_infix = (p.flags & CONDF_INFIX) != 0;
7485 if p_infix == want_infix && p.name == lookup {
7486 // Manual field-by-field clone: conddef doesn't
7487 // derive Clone (function-pointer + Option<Conddef>
7488 // mix with no PartialEq).
7489 Some(conddef {
7490 next: None,
7491 name: p.name.clone(),
7492 flags: p.flags,
7493 handler: p.handler,
7494 min: p.min,
7495 max: p.max,
7496 condid: p.condid,
7497 module: p.module.clone(),
7498 })
7499 } else {
7500 None
7501 }
7502 })
7503 };
7504 // c:669-685 — autoload trigger + failure-retry loop.
7505 let has_autoload_module = hit.as_ref().map(|p| p.module.is_some()).unwrap_or(false);
7506 if autol != 0 && hit.is_some() && has_autoload_module {
7507 let p = hit.as_ref().unwrap();
7508 if f != 0 {
7509 // c:674-678 — first miss: load the module + retry.
7510 let module = p.module.as_ref().unwrap().clone();
7511 let prefix = if (p.flags & CONDF_INFIX) != 0 {
7512 "C:"
7513 } else {
7514 "c:"
7515 };
7516 let feature_arg = if (p.flags & CONDF_AUTOALL) != 0 {
7517 // c:677 — NULL → autoload-all branch.
7518 None
7519 } else {
7520 Some(lookup.as_str())
7521 };
7522 let _ = ensurefeature(table, &module, prefix, feature_arg);
7523 f = 0;
7524 continue; // c:680 `p = NULL;` + outer do-while re-tries.
7525 } else {
7526 // c:681-683 — second pass still hit autoload entry →
7527 // load failed; remove the stub and return None.
7528 let _ = deleteconddef(p);
7529 return None;
7530 }
7531 }
7532 return hit; // c:684-685 `else break;` then return p.
7533 }
7534}
7535
7536/// Port of `deleteconddef()` from `Src/module.c:724`. — C decl `deleteconddef(Conddef c)`.
7537/// Removes condition definition `c` from `condtab`. Returns 0 on
7538/// success, -1 on miss. C also frees the autoloaded entry's name +
7539/// module; Rust drop subsumes that.
7540pub fn deleteconddef(c: &conddef) -> i32 {
7541 // c:724
7542 let mut tab = CONDTAB.lock().unwrap();
7543 // c:728 — `for (p = condtab, q = NULL; p && p != c; ...)`. C uses
7544 // pointer identity; the Rust analog is name+infix-flag equality
7545 // (the natural key — `[[ -z STR ]]` and `STR == VAL` share neither).
7546 let infix = c.flags & CONDF_INFIX;
7547 match tab
7548 .iter()
7549 .position(|p| p.name == c.name && (p.flags & CONDF_INFIX) == infix)
7550 {
7551 Some(i) => {
7552 tab.remove(i);
7553 0
7554 } // c:733-738 unlink + free
7555 None => -1, // c:743 not found
7556 }
7557}
7558
7559/// Port of `addconddef()` from `Src/module.c:703`. — C decl `addconddef(Conddef c)`.
7560/// Walks
7561/// CONDTAB for a clash on (name, infix-flag); replaces autoloadable
7562/// entries via deleteconddef; otherwise prepends. Returns 0 on add,
7563/// 1 on clash (existing entry already added).
7564pub fn addconddef(c: conddef) -> i32 {
7565 // c:703
7566 let infix = c.flags & CONDF_INFIX;
7567 let clash_idx = {
7568 let tab = CONDTAB.lock().unwrap();
7569 tab.iter()
7570 .position(|p| p.name == c.name && (p.flags & CONDF_INFIX) == infix) // c:705 getconddef
7571 };
7572 if let Some(i) = clash_idx {
7573 let (autoload, added) = {
7574 let tab = CONDTAB.lock().unwrap();
7575 (tab[i].module.is_some(), (tab[i].flags & CONDF_ADDED) != 0)
7576 };
7577 if !autoload || added {
7578 return 1;
7579 } // c:708 already added
7580 CONDTAB.lock().unwrap().remove(i); // c:711 deleteconddef
7581 }
7582 CONDTAB.lock().unwrap().insert(0, c); // c:713-714 c->next = condtab; condtab = c
7583 0
7584}
7585
7586/// Port of file-static `FuncWrap wrappers;` from `Src/module.c:567`
7587/// — the global wrapper-function linked-list head. Modules register
7588/// wrapper callbacks via `addwrapper(FuncWrap)` and the runtime fires
7589/// them around `runshfunc()`. The Rust port stores entries in a `Vec`
7590/// (linear add/remove + iterate; same observable behaviour).
7591pub static WRAPPERS: Lazy<Mutex<Vec<funcwrap>>> = // c:567
7592 Lazy::new(|| Mutex::new(Vec::new()));
7593
7594// !!! WARNING: RUST-ONLY HELPER !!!
7595//
7596// No C counterpart. C's `runshfunc` (`Src/exec.c:6177`) walks the
7597// `wrappers` linked list with plain pointer chasing — no lock, because
7598// the C shell is single-threaded. zshrs keeps the same list behind
7599// [`WRAPPERS`]'s `Mutex`, and `runshfunc` sits on the shell-function
7600// call path, the hottest path in the shell: taking that mutex on every
7601// call would cost far more than the whole wrapper mechanism.
7602//
7603// So `addwrapper` / `deletewrapper` mirror list MEMBERSHIP into this
7604// relaxed atomic bitmask, one bit per module that can install a
7605// wrapper. `runshfunc` tests a chain node with a single relaxed load
7606// and, when no module has registered, does not touch the mutex at all.
7607// The mask is derived state — [`WRAPPERS`] stays the source of truth.
7608/// Relaxed-atomic membership mirror of [`WRAPPERS`], one bit per
7609/// wrapper-installing module (see [`WRAPPER_BIT_ZPROF`]). Written by
7610/// `addwrapper` / `deletewrapper`, read by `exec::runshfunc`. See the
7611/// WARNING block above.
7612pub static WRAPPERS_ADDED: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
7613
7614/// [`WRAPPERS_ADDED`] bit for `zsh/zprof`'s
7615/// `WRAPDEF(zprof_wrapper)` (`Src/Modules/zprof.c:318-320`).
7616pub const WRAPPER_BIT_ZPROF: u32 = 1 << 0;
7617
7618/// [`WRAPPERS_ADDED`] bit for `zsh/ksh93`'s
7619/// `WRAPDEF(ksh93_wrapper)` (`Src/Modules/ksh93.c:230-232`).
7620pub const WRAPPER_BIT_KSH93: u32 = 1 << 1;
7621
7622/// Port of `addwrapper()` from `Src/module.c:577`. — C decl `addwrapper(Module m, FuncWrap w)`.
7623/// Tail-appends a module's function wrapper onto the
7624/// global [`WRAPPERS`] list. Returns 1 on error, 0 on success.
7625///
7626/// C body (c:578-600):
7627/// ```c
7628/// FuncWrap p, q;
7629/// if (m->node.flags & MOD_ALIAS)
7630/// return 1;
7631/// if (w->flags & WRAPF_ADDED)
7632/// return 1;
7633/// for (p = wrappers, q = NULL; p; q = p, p = p->next);
7634/// if (q)
7635/// q->next = w;
7636/// else
7637/// wrappers = w;
7638/// w->next = NULL;
7639/// w->flags |= WRAPF_ADDED;
7640/// w->module = m;
7641/// return 0;
7642/// ```
7643///
7644/// WARNING: param types don't match C — Rust=(m: &str, w) vs
7645/// C=(Module m, FuncWrap w). Every zshrs module entry point is reached
7646/// by NAME through the `module.rs` dispatcher (`setup_module` /
7647/// `boot_module` / `cleanup_module` all take `name: &str` and hand the
7648/// per-module `*_` fn a null `*const module`), so a `Module` pointer is
7649/// not available at the call site. The name is looked up in
7650/// [`MODULESTAB`] for the c:586 `MOD_ALIAS` test and stored back into
7651/// `w.module` — C keeps the pointer, this port keeps a name-carrying
7652/// [`module`] node, which is what `deletewrapper` matches on.
7653pub fn addwrapper(m: &str, mut w: funcwrap) -> i32 {
7654 // c:576
7655 // c:586-587 — `if (m->node.flags & MOD_ALIAS) return 1;`
7656 // We can't add a wrapper to an alias, since it's supposed to behave
7657 // identically to the resolved module. This shouldn't happen since
7658 // we usually add wrappers when a real module is loaded. (c:580-585)
7659 //
7660 // `try_lock`, not `lock`: every caller of this function is a
7661 // module `boot_`, which `bin_zmodload` reaches with the
7662 // [`MODULESTAB`] mutex already held (module.rs:4877 →
7663 // `load_module` → `do_boot_module`). `std::sync::Mutex` is not
7664 // reentrant, so a blocking `lock()` here self-deadlocks. A failed
7665 // `try_lock` means WE are the holder, i.e. the module is mid-load
7666 // and therefore not an alias — exactly the c:580-585 "shouldn't
7667 // happen" case — so the test is skipped.
7668 if let Ok(tab) = MODULESTAB.try_lock() {
7669 if let Some(md) = tab.modules.get(m) {
7670 if (md.node.flags & MOD_ALIAS) != 0 {
7671 return 1; // c:587
7672 }
7673 }
7674 }
7675
7676 // c:589-590 — `if (w->flags & WRAPF_ADDED) return 1;`
7677 if (w.flags & crate::ported::zsh_h::WRAPF_ADDED) != 0 {
7678 return 1; // c:590
7679 }
7680
7681 let mut wrappers = WRAPPERS.lock().unwrap_or_else(|e| e.into_inner());
7682 // c:591-595 — walk to the tail and link on (`q->next = w`), or
7683 // become the head (`wrappers = w`). A `Vec` push IS that walk.
7684 // c:596 — `w->next = NULL;`
7685 w.next = None;
7686 // c:597 — `w->flags |= WRAPF_ADDED;`
7687 w.flags |= crate::ported::zsh_h::WRAPF_ADDED;
7688 // c:598 — `w->module = m;`
7689 w.module = Some(Box::new(module::new(m)));
7690 wrappers.push(w); // c:593/595
7691
7692 // Rust-only — see [`WRAPPERS_ADDED`]. Modules with no bit (the
7693 // statically-linked wrappers that never reach `addwrapper`) map to
7694 // 0, which leaves the mask untouched.
7695 let bit = match m {
7696 "zsh/zprof" => WRAPPER_BIT_ZPROF,
7697 "zsh/ksh93" => WRAPPER_BIT_KSH93,
7698 _ => 0,
7699 };
7700 WRAPPERS_ADDED.fetch_or(bit, std::sync::atomic::Ordering::Relaxed);
7701
7702 0 // c:600
7703}
7704
7705/// Port of `deletewrapper()` from `Src/module.c:609`. — C decl `deletewrapper(Module m, FuncWrap w)`.
7706/// Unlinks a module's wrapper from [`WRAPPERS`].
7707/// Returns 0 when the node was found and removed, 1 otherwise.
7708///
7709/// C body (c:610-628):
7710/// ```c
7711/// FuncWrap p, q;
7712/// if (m->node.flags & MOD_ALIAS)
7713/// return 1;
7714/// if (w->flags & WRAPF_ADDED) {
7715/// for (p = wrappers, q = NULL; p && p != w; q = p, p = p->next);
7716/// if (p) {
7717/// if (q) q->next = p->next; else wrappers = p->next;
7718/// p->flags &= ~WRAPF_ADDED;
7719/// return 0;
7720/// }
7721/// }
7722/// return 1;
7723/// ```
7724///
7725/// WARNING: param types don't match C — Rust=(m: &str) vs C=(Module m,
7726/// FuncWrap w), and the node is identified by owning-module name rather
7727/// than by the `p != w` pointer compare at c:616. C's `w` is always the
7728/// module's own file-static `wrapper[]` array (`Src/Modules/zprof.c:318`,
7729/// `Src/Zle/complete.c:1694`, `Src/Modules/param_private.c:541`), so
7730/// "the node whose `module` is `m`" selects exactly the same entry;
7731/// zshrs has no stable address to compare against because the port
7732/// stores the node by value in a `Vec`.
7733pub fn deletewrapper(m: &str) -> i32 {
7734 // c:608
7735 // c:612-613 — `if (m->node.flags & MOD_ALIAS) return 1;`
7736 // `try_lock` for the same reentrancy reason as `addwrapper`: the
7737 // caller is a module `cleanup_`, reached with [`MODULESTAB`] held.
7738 if let Ok(tab) = MODULESTAB.try_lock() {
7739 if let Some(md) = tab.modules.get(m) {
7740 if (md.node.flags & MOD_ALIAS) != 0 {
7741 return 1; // c:613
7742 }
7743 }
7744 }
7745
7746 let mut wrappers = WRAPPERS.lock().unwrap_or_else(|e| e.into_inner());
7747 // c:615-616 — `if (w->flags & WRAPF_ADDED)` then walk for the node.
7748 let found = wrappers.iter().position(|p| {
7749 (p.flags & crate::ported::zsh_h::WRAPF_ADDED) != 0
7750 && p.module.as_ref().map(|md| md.node.nam.as_str()) == Some(m)
7751 });
7752 if let Some(i) = found {
7753 // c:618-623 — unlink and clear WRAPF_ADDED. Removing from the
7754 // `Vec` both unlinks (c:619-622) and drops the flag with the
7755 // node (c:623).
7756 wrappers.remove(i);
7757 // Rust-only — see [`WRAPPERS_ADDED`].
7758 let bit = match m {
7759 "zsh/zprof" => WRAPPER_BIT_ZPROF,
7760 "zsh/ksh93" => WRAPPER_BIT_KSH93,
7761 _ => 0,
7762 };
7763 WRAPPERS_ADDED.fetch_and(!bit, std::sync::atomic::Ordering::Relaxed);
7764 return 0; // c:625
7765 }
7766 1 // c:628
7767}
7768
7769/// Port of `addmathfunc()` from `Src/module.c:1313`. — C decl `addmathfunc(MathFunc f)`.
7770/// Returns 0 on add, 1 on clash (existing entry not autoloadable).
7771/// Replaces autoloadable entries via `removemathfunc`.
7772pub fn addmathfunc(f: mathfunc) -> i32 {
7773 // c:1313
7774 if (f.flags & MFF_ADDED) != 0 {
7775 return 1;
7776 } // c:1318
7777 let mut tab = MATHFUNCS.lock().unwrap();
7778 let mut found_idx: Option<usize> = None;
7779 for (i, p) in tab.iter().enumerate() {
7780 // c:1321
7781 if p.name == f.name {
7782 // c:1322
7783 if p.module.is_some() && (p.flags & MFF_USERFUNC) == 0 {
7784 // c:1323
7785 found_idx = Some(i); // c:1327 removemathfunc + replace
7786 break;
7787 }
7788 return 1; // c:1330
7789 }
7790 }
7791 if let Some(i) = found_idx {
7792 tab.remove(i);
7793 } // c:1327
7794 tab.insert(0, f); // c:1334-1335 f->next = mathfuncs; mathfuncs = f
7795 0
7796}
7797
7798/// Port of `removemathfunc()` from `Src/module.c:1267`. — C decl `removemathfunc(MathFunc previous, MathFunc current)`.
7799/// Removes the named entry from MATHFUNCS and
7800/// drops it (Rust drop subsumes C's zsfree/zfree ladder).
7801/// WARNING: param names don't match C — Rust=(name) vs C=(previous, current)
7802pub fn removemathfunc(name: &str) {
7803 // c:1267
7804 let mut tab = MATHFUNCS.lock().unwrap();
7805 if let Some(i) = tab.iter().position(|m| m.name == name) {
7806 // c:1270 walk
7807 tab.remove(i); // c:1273-1274 unlink + zfree
7808 }
7809}
7810
7811/// Port of `deletemathfunc()` from `Src/module.c:1342`. — C decl `deletemathfunc(MathFunc f)`.
7812/// Removes f from MATHFUNCS; for unloaded/user-defined entries clears
7813/// the MFF_ADDED flag instead of dropping the node (C: `f->flags &=
7814/// ~MFF_ADDED` when f->module is null).
7815pub fn deletemathfunc(f: &mathfunc) -> i32 {
7816 // c:1342
7817 // c:1348-1352 — the node is unlinked from the global list in BOTH
7818 // arms (`q->next = f->next` / `mathfuncs = f->next`); f->module
7819 // only decides free-vs-keep of the struct itself. The struct for
7820 // module=NULL entries lives in the module's static mftab (the C
7821 // list links the static structs by pointer), so "keep" means the
7822 // mftab record survives with MFF_ADDED cleared — in Rust the
7823 // global-list entry is a by-value dup, so the unlink is a plain
7824 // remove either way and the MFF_ADDED clear on the static record
7825 // happens at the setmathfuncs caller (which holds `&mut` to the
7826 // mftab entry). The previous port left module-less entries IN the
7827 // global list, so a feature-disable never actually deregistered.
7828 let mut tab = MATHFUNCS.lock().unwrap();
7829 match tab.iter().position(|m| m.name == f.name) {
7830 // c:1346
7831 Some(i) => {
7832 tab.remove(i); // c:1349-1352 unlink (+ Rust Drop ≙ c:1355-1357 free)
7833 0 // c:1361
7834 }
7835 None => -1, // c:1363
7836 }
7837}
7838
7839/// Port of `featuresarray()` from `Src/module.c:3279`. — C decl `featuresarray(UNUSED(Module m), Features f)`.
7840/// Construct the feature-name array for a
7841/// module: builtins get `b:NAME`, conditions `c:NAME` or `C:NAME` if
7842/// `CONDF_INFIX`, math funcs `f:NAME`, params `p:NAME`. Trailing
7843/// abstract slots (`n_abstract`) are pre-allocated but left empty so
7844/// the module's own setup can fill them in. C uses zhalloc heap
7845/// allocation — Box goes out of scope here as Rust's `Vec<String>`
7846/// owns the entries (Drop happens automatically). Per-module Rust
7847/// files in `src/ported/modules/*.rs` and `src/ported/builtins/*.rs`
7848/// each carry a `featuresarray` shim that delegates to this
7849/// canonical free fn once the modules table is wired through.
7850/// WARNING: param names don't match C — Rust=(_m, bn, cd, mf, pd, n_abstract) vs C=(m, f)
7851pub fn featuresarray(
7852 // c:3284
7853 _m: *const module,
7854 bn: &[builtin], // c:3289 f->bn_list
7855 cd: &[conddef], // c:3290 f->cd_list
7856 mf: &[mathfunc], // c:3291 f->mf_list
7857 pd: &[paramdef], // c:3292 f->pd_list
7858 n_abstract: i32, // c:3288 f->n_abstract
7859) -> Vec<String> {
7860 let features_size = bn.len() + cd.len() + mf.len() + pd.len() // c:3288
7861 + n_abstract.max(0) as usize;
7862 let mut features: Vec<String> = Vec::with_capacity(features_size + 1); // c:3293
7863 for b in bn {
7864 // c:3296
7865 features.push(format!("b:{}", b.node.nam)); // c:3297
7866 }
7867 for c in cd {
7868 // c:3298
7869 let prefix = if (c.flags & CONDF_INFIX) != 0 {
7870 "C:"
7871 } else {
7872 "c:"
7873 }; // c:3299
7874 features.push(format!("{}{}", prefix, c.name)); // c:3299-3300
7875 }
7876 for m in mf {
7877 // c:3303
7878 features.push(format!("f:{}", m.name)); // c:3304
7879 }
7880 for p in pd {
7881 // c:3305
7882 features.push(format!("p:{}", p.name)); // c:3306
7883 }
7884 // c:3308 — features[features_size] = NULL; Rust analog: trailing
7885 // abstract slots remain unset (Vec is one-shot allocated).
7886 features
7887}
7888
7889/// Port of `getfeatureenables()` from `Src/module.c:3314`. — C decl `getfeatureenables(UNUSED(Module m), Features f)`.
7890/// Returns the per-feature
7891/// enable bitmap for a module: builtins use `BINF_ADDED`, conditions
7892/// `CONDF_ADDED`, math funcs `MFF_ADDED`, params the `pm` non-null
7893/// check. Trailing abstract slots are left at 0 (filled by the
7894/// module's own enables_). C uses zhalloc heap allocation; Rust's
7895/// `Vec<i32>` owns the entries (Drop happens automatically). Per-
7896/// module shims in `src/ported/modules/*.rs` delegate to this
7897/// canonical free fn once the modules table is wired through.
7898/// WARNING: param names don't match C — Rust=(_m, bn, cd, mf, pd, n_abstract) vs C=(m, f)
7899pub fn getfeatureenables(
7900 // c:3319
7901 _m: *const module,
7902 bn: &[builtin], // c:3324
7903 cd: &[conddef], // c:3325
7904 mf: &[mathfunc], // c:3326
7905 pd: &[paramdef], // c:3327
7906 n_abstract: i32, // c:3323
7907) -> Vec<i32> {
7908 let features_size = bn.len() + cd.len() + mf.len() + pd.len() // c:3323
7909 + n_abstract.max(0) as usize;
7910 let mut enables: Vec<i32> = Vec::with_capacity(features_size); // c:3328
7911 for b in bn {
7912 // c:3331
7913 enables.push(if (b.node.flags & BINF_ADDED as i32) != 0 {
7914 1
7915 } else {
7916 0
7917 });
7918 }
7919 for c in cd {
7920 // c:3333
7921 enables.push(if (c.flags & CONDF_ADDED) != 0 { 1 } else { 0 });
7922 }
7923 for m in mf {
7924 // c:3335
7925 enables.push(if (m.flags & MFF_ADDED) != 0 { 1 } else { 0 });
7926 }
7927 for p in pd {
7928 // c:3337
7929 enables.push(if p.pm.is_some() { 1 } else { 0 });
7930 }
7931 for _ in 0..n_abstract.max(0) {
7932 // c:3323 n_abstract slots
7933 enables.push(0);
7934 }
7935 enables // c:3340
7936}
7937
7938/// !!! RUST-ONLY REGISTRY — NO C COUNTERPART !!!
7939///
7940/// The per-feature "is this feature currently ADDED" bit, keyed by module
7941/// name and by the feature string `featuresarray` emits (`b:zstyle`,
7942/// `p:functions`, `c:prefix`, `f:sin`, …).
7943///
7944/// C keeps that bit ON THE DESCRIPTOR the module ships. `getfeatureenables`
7945/// (c:3330-3337) reads `b->node.flags & BINF_ADDED`, `cd->flags &
7946/// CONDF_ADDED`, `mf->flags & MFF_ADDED` and `pd->pm != NULL` straight out of
7947/// the module's `static struct features module_features` tables, and
7948/// `setfeatureenables` (c:3358-3381) writes them back through `setbuiltins` /
7949/// `setconddefs` / `setmathfuncs` / `setparamdefs`. The bit is therefore
7950/// per-module storage that outlives a single `zmodload` call.
7951///
7952/// Most zshrs module ports carry no descriptor table — their `features_()`
7953/// returns a hardcoded name list, so there is no `Builtin`/`Paramdef` struct
7954/// whose flags could hold the bit. This map is that storage. Modules that DO
7955/// ship real `Vec<builtin>` / `Vec<paramdef>` statics (`zsh/datetime`) keep
7956/// using the descriptor path and never appear here.
7957static MODULE_FEATURE_ENABLES: Lazy<Mutex<HashMap<String, std::collections::HashSet<String>>>> =
7958 Lazy::new(|| Mutex::new(HashMap::new()));
7959
7960/// Port of `setfeatureenables()` from `Src/module.c:3354`. — C decl
7961/// `setfeatureenables(Module m, Features f, int *e)`.
7962///
7963/// C body:
7964/// ```c
7965/// if (f->bn_size) { if (setbuiltins(m->node.nam, f->bn_list, f->bn_size, e)) ret = 1;
7966/// if (e) e += f->bn_size; }
7967/// if (f->cd_size) { if (setconddefs(m->node.nam, f->cd_list, f->cd_size, e)) ret = 1;
7968/// if (e) e += f->cd_size; }
7969/// if (f->mf_size) { if (setmathfuncs(m->node.nam, f->mf_list, f->mf_size, e)) ret = 1;
7970/// if (e) e += f->mf_size; }
7971/// if (f->pd_size) { if (setparamdefs(m->node.nam, f->pd_list, f->pd_size, e)) ret = 1; }
7972/// return ret;
7973/// ```
7974///
7975/// WARNING: param names don't match C — Rust=(modname, features, e) vs
7976/// C=(m, f, e). The four per-kind blocks C walks in the fixed order
7977/// `bn`/`cd`/`mf`/`pd` are already flattened into that same order by
7978/// `featuresarray` (c:3288-3305), so this variant walks the ONE name list
7979/// and keys the enable bit off the `b:`/`c:`/`f:`/`p:` prefix instead of the
7980/// block it fell in. It is the entry point for the module ports that have no
7981/// `Features` descriptor tables — see `MODULE_FEATURE_ENABLES`.
7982///
7983/// C's contract "If e is NULL, disable everything" (c:3345) is preserved:
7984/// `None` clears every bit.
7985pub fn setfeatureenables(modname: &str, features: &[String], e: Option<&[i32]>) -> i32 {
7986 // c:3354
7987 let mut ret = 0; // c:3356
7988 // Collect the p:-feature transitions while the ledger lock is held, and
7989 // apply them after it is dropped: `mark_module_param_used` re-enters
7990 // this fn through `ensurefeature` -> `require_module` ->
7991 // `do_module_features` -> `enables_module`, and `Mutex` is not
7992 // reentrant.
7993 let mut param_on: Vec<String> = Vec::new();
7994 let mut param_off: Vec<String> = Vec::new();
7995 {
7996 let mut tab = MODULE_FEATURE_ENABLES.lock().unwrap();
7997 let set = tab.entry(modname.to_string()).or_default();
7998 for (n, f) in features.iter().enumerate() {
7999 // c:3345 — `If e is NULL, disable everything.`
8000 let on = e
8001 .map(|a| a.get(n).copied().unwrap_or(0) != 0)
8002 .unwrap_or(false);
8003 if on {
8004 set.insert(f.clone()); // c:515 `b->node.flags |= BINF_ADDED`
8005 } else {
8006 set.remove(f); // c:524 `b->node.flags &= ~BINF_ADDED`
8007 }
8008 // c:3377 `setparamdefs` -> c:1060 `addparamdef` / c:1128
8009 // `deleteparamdef`: the `p:` block installs the real special Param
8010 // over the PM_AUTOLOAD stub, or removes it again. zshrs seeds every
8011 // magic parameter eagerly and models PM_AUTOLOAD as vm_helper's
8012 // MATERIALIZED_MODULE_PARAMS side set, so `pd->pm` is that set's
8013 // membership and this is where it has to move.
8014 if let Some(pname) = f.strip_prefix("p:") {
8015 if on {
8016 param_on.push(pname.to_string());
8017 } else {
8018 param_off.push(pname.to_string());
8019 }
8020 }
8021 }
8022 }
8023 // c:3377 setparamdefs (c:1169-1196) for the zmodload-gated specials
8024 // (`errnos`/`sysparams`, `mapfile`, `langinfo`): unlike the
8025 // `zsh/parameter` rows, which zshrs seeds eagerly and toggles through
8026 // the side set above, these enter paramtab only when their module turns
8027 // them on, so `d->pm` is "the live node is the module's special".
8028 let gated = crate::vm_helper::module_gated_params_for(modname);
8029 let live_special = |name: &str| -> Option<Option<Param>> {
8030 let tab = paramtab().read().ok()?;
8031 Some(tab.get(name).cloned())
8032 };
8033 for p in ¶m_on {
8034 crate::vm_helper::mark_module_param_used(p); // c:1069-1073 createspecialhash/createparam
8035 if gated.contains(&p.as_str()) {
8036 match live_special(p) {
8037 // c:1176-1179 — `if (d->pm) { d++; continue; }`
8038 Some(Some(pm)) if (pm.node.flags as u32 & PM_SPECIAL) != 0 => {}
8039 // c:1180-1183 — addparamdef fails when a non-special of the
8040 // same name is in the way (checkaddparam, c:1062).
8041 Some(Some(_)) => {
8042 zwarnnam(modname, &format!("error when adding parameter `{}'", p));
8043 ret = 1;
8044 }
8045 // c:1180 — `addparamdef(d)`.
8046 _ => crate::vm_helper::seed_partab_param(p),
8047 }
8048 }
8049 }
8050 for p in ¶m_off {
8051 crate::vm_helper::unmark_module_param_used(p); // c:1177 unsetparam_pm
8052 if gated.contains(&p.as_str()) {
8053 // c:1186-1189 — `if (!d->pm) { d++; continue; }`: only the
8054 // module's own special node is deleted.
8055 if let Some(Some(pm)) = live_special(p) {
8056 if (pm.node.flags as u32 & PM_SPECIAL) != 0 {
8057 let mut d = paramdef {
8058 name: p.clone(),
8059 flags: pm.node.flags,
8060 var: 0,
8061 gsu: 0,
8062 getnfn: None,
8063 scantfn: None,
8064 pm: Some(pm),
8065 };
8066 // c:1190 — `if (deleteparamdef(d))`
8067 if deleteparamdef(&mut d) != 0 {
8068 zwarnnam(modname, &format!("parameter `{}' already deleted", p)); // c:1191
8069 ret = 1; // c:1192
8070 }
8071 }
8072 }
8073 }
8074 }
8075 ret // c:3382
8076}
8077
8078/// Port of `handlefeatures()` from `Src/module.c:3392`. — C decl
8079/// `handlefeatures(Module m, Features f, int **enables)`.
8080///
8081/// C body:
8082/// ```c
8083/// if (!enables || *enables)
8084/// return setfeatureenables(m, f, enables ? *enables : NULL);
8085/// *enables = getfeatureenables(m, f);
8086/// return 0;
8087/// ```
8088///
8089/// WARNING: param names don't match C — Rust=(modname, features, enables) vs
8090/// C=(m, f, enables). Rust's `&mut Option<Vec<i32>>` is never NULL, so C's
8091/// `*enables` non-NULL is `Some` (SET the given bitmap) and NULL is `None`
8092/// (GET the live one). The name-keyed variant for module ports with no
8093/// `Features` descriptor tables — see `MODULE_FEATURE_ENABLES`.
8094pub fn handlefeatures(
8095 modname: &str,
8096 features: &[String],
8097 enables: &mut Option<Vec<i32>>,
8098) -> i32 {
8099 // c:3392
8100 if let Some(e) = enables.as_ref() {
8101 // c:3394-3395
8102 let e = e.clone();
8103 return setfeatureenables(modname, features, Some(&e));
8104 }
8105 // c:3396 — `*enables = getfeatureenables(m, f);`
8106 let tab = MODULE_FEATURE_ENABLES.lock().unwrap();
8107 let set = tab.get(modname);
8108 *enables = Some(
8109 features
8110 .iter()
8111 .map(|f| match f.strip_prefix("p:") {
8112 // c:3337 — `*enablep++ = (pdp++)->pm ? 1 : 0;`. For a module
8113 // whose `.mdd` declares the parameter autoloadable, `pd->pm`
8114 // is set by `addparamdef` from EITHER the module load or
8115 // `loadparamnode`'s single-feature `ensurefeature` (c:Src/
8116 // params.c:568) — the second is what leaves a plain
8117 // `compinit` with `+p:commands` and every sibling `-`. zshrs
8118 // routes both through `mark_module_param_used`, so the
8119 // materialized set is the authority here, not the ledger.
8120 Some(pname)
8121 if crate::vm_helper::AUTOLOAD_PARAMS
8122 .iter()
8123 .any(|(n, owner)| *n == pname && *owner == modname) =>
8124 {
8125 i32::from(!crate::vm_helper::module_param_is_autoload_stub(pname))
8126 }
8127 // c:3331-3335 — the BINF_ADDED / CONDF_ADDED / MFF_ADDED
8128 // reads, plus any `p:` row whose module declares no autoload.
8129 _ => i32::from(set.is_some_and(|s| s.contains(f))),
8130 })
8131 .collect(),
8132 );
8133 0 // c:3397
8134}
8135
8136/// Port of `Hookdef hooktab;` from `Src/module.c:843` — the file-static
8137/// linked-list head pointer to the chain of registered `hookdef`
8138/// nodes. Walked by `gethookdef`; mutated by `addhookdef` /
8139/// `deletehookdef`. Each node is a `Box::leak`'d hookdef (so the raw
8140/// pointer has program-lifetime, matching C's static-storage
8141/// `zshhooks[]` and module-side hookdef arrays).
8142pub static hooktab: std::sync::atomic::AtomicPtr<hookdef> = // c:843
8143 std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());
8144
8145/// Port of `mod_export ModuleTable modulestab` from
8146/// `Src/Modules/zmodload.c:32`. The C source keeps the module
8147/// hashtable as a process-global accessed by every module-mgmt
8148/// path (zmodload, addbuiltin, deletebuiltin, etc.). This Rust
8149/// global mirrors that — bin_zmodload_handler reaches for it so
8150/// the canonical `bin_zmodload` can be wired into BUILTINS via
8151/// HandlerFunc without an extra table-arg.
8152pub static MODULESTAB: Lazy<Mutex<modulestab>> = // c:zmodload.c:32
8153 Lazy::new(|| Mutex::new(modulestab::new()));
8154
8155/// !!! RUST-ONLY REGISTRY — NO C COUNTERPART !!!
8156///
8157/// Names a module's `setbuiltins` (`Src/module.c:501`) has turned OFF
8158/// through the feature interface (`zmodload -F MODULE -b:NAME`).
8159///
8160/// C has no such table: `setbuiltins` calls `deletebuiltin` (c:521), which
8161/// physically removes the node from the live `builtintab`, so "is this
8162/// builtin currently visible" is answered by the table itself. zshrs's
8163/// `builtintab` (`createbuiltintable()`) is an immutable, statically-linked
8164/// map that already carries every module builtin, and per-MODULE visibility
8165/// is decided by `module_builtin_available`
8166/// (`src/extensions/ext_builtins.rs`) from the modulestab load flags. That
8167/// is one granularity too coarse for features: `zmodload -F zsh/datetime
8168/// -b:strftime` leaves the module LOADED while removing just `strftime`
8169/// (V04features.ztst "Enabling and disabling of builtins as features"), so
8170/// the per-NAME half needs its own store.
8171///
8172/// Per-NAME, exactly like C's node removal — a module can disable one of
8173/// its builtins and keep the rest.
8174pub static DISABLED_MODULE_BUILTINS: Lazy<Mutex<std::collections::HashSet<String>>> =
8175 Lazy::new(|| Mutex::new(std::collections::HashSet::new()));
8176
8177// C zsh classifies module exports by bare integer `type` index 0..4
8178// (no named constants — just position-in-table) in `features_()`
8179// (`Src/module.c:313+`). Module load state is the `MOD_*` bitmask in
8180// `module.node.flags` (`Src/zsh.h:1516-1532`, mirrored at
8181// `zsh_h.rs:2249-2255`). C does not record which features a module
8182// added on the module struct — feature registration flows into the
8183// canonical per-feature-kind tables (`builtintab`, `condtab`,
8184// `paramtab`, `mathfuncs`, `hooktab`).
8185
8186/// Feature-type index passed to `features_()` (`Src/module.c:313+`).
8187/// C ships bare ints; Rust adds names for readability.
8188pub const FEATURE_TYPE_BUILTIN: i32 = 0;
8189/// `FEATURE_TYPE_CONDITION` constant.
8190pub const FEATURE_TYPE_CONDITION: i32 = 1;
8191/// `FEATURE_TYPE_PARAMETER` constant.
8192pub const FEATURE_TYPE_PARAMETER: i32 = 2;
8193/// `FEATURE_TYPE_MATHFUNC` constant.
8194pub const FEATURE_TYPE_MATHFUNC: i32 = 3;
8195/// `FEATURE_TYPE_HOOK` constant.
8196pub const FEATURE_TYPE_HOOK: i32 = 4;
8197/// Module table (from module.c module hash table)
8198#[derive(Debug, Default)]
8199/// Table of registered modules.
8200/// Port of the `modulestab` HashTable Src/module.c keeps —
8201/// `newmoduletable()` (line 274) creates it, `register_module()`
8202/// (line 359) inserts entries, `printmodulenode()` (line 154)
8203/// renders for `zmodload`.
8204pub struct modulestab {
8205 /// C's `HashTable modulestab` node storage (`Src/module.c:274`
8206 /// `newmoduletable`, created as `newmoduletable(17, "modules")` at
8207 /// `Src/init.c:1193`). The bucket walk is user-visible: the
8208 /// `modules` special parameter (`Src/Modules/parameter.c`) emits
8209 /// `${(k)modules}` straight out of `scanhashtable(modulestab, …)`,
8210 /// so a Rust `HashMap` here produced an order that matched zsh on
8211 /// no line at all.
8212 pub modules: crate::ported::hashtable::hashtable_nodes<module>,
8213 /// C's `LinkList linkedmodules` (`Src/module.c:39`, created at
8214 /// `Src/init.c:1194` `linkedmodules = znewlinklist()`), the list
8215 /// `register_module` appends to (`c:378 zaddlinknode(linkedmodules,
8216 /// m)`) and `module_linked` (`c:385`) searches. It is a SEPARATE
8217 /// store from `modulestab`: a statically-linked module is on this
8218 /// list from boot but gets a `modulestab` node only when something
8219 /// creates one — `autofeatures`/`add_dep` at boot (`c:3449`,
8220 /// `c:2390`) or `load_module`'s allocate-on-miss branch
8221 /// (`c:2229-2237`).
8222 ///
8223 /// zshrs compiles every module in, so this list holds every name
8224 /// `zmodload` can resolve; C only reaches it for `link=static`
8225 /// modules and dlopens the rest. Keeping it separate is what makes
8226 /// `modulestab->hsize` stay at C's boot value of 17 — inserting all
8227 /// ~40 known modules as nodes tripped `ct >= hsize * 2`
8228 /// (`Src/hashtable.c` addhashnode) and quadrupled the table to 68
8229 /// buckets, so every `${(k)modules}` bucket index differed from C.
8230 ///
8231 /// C stores `Linkedmod` records (name + six entry points); zshrs
8232 /// dispatches the entry points by name (`setup_module` etc.), so
8233 /// only the name is kept.
8234 pub linkedmodules: Vec<String>,
8235 /// Builtin name → module name mapping for autoload
8236 pub autoload_builtins: HashMap<String, String>,
8237 /// Condition name → module name mapping for autoload
8238 pub autoload_conditions: HashMap<String, String>,
8239 /// Parameter name → module name mapping for autoload
8240 pub autoload_params: HashMap<String, String>,
8241 /// Math function name → module name mapping for autoload
8242 pub autoload_mathfuncs: HashMap<String, String>,
8243 /// BINF_ADDED ledger — tracks which builtins have been added via
8244 /// `setbuiltins` (C: `b->node.flags & BINF_ADDED`, c:508).
8245 pub added_builtins: HashMap<String, u32>,
8246}
8247
8248// =====================================================================
8249// Builtin / Conddef / MathFunc / Paramdef descriptors and the
8250// `struct features` aggregator from `Src/zsh.h:1440-1571` and
8251// `Src/module.c:3279+`.
8252//
8253// In zsh C these are linked into modules via `dlsym()`; in zshrs
8254// modules are compiled in (no dlopen), so each module ships a
8255// `static` `Features` describing its `bintab[]` / etc. that the
8256// `features_` / `enables_` / `cleanup_` entry points hand to the
8257// helpers below.
8258// =====================================================================
8259
8260// `BINF_ADDED` / `CONDF_INFIX` / `CONDF_ADDED` / `MFF_ADDED` are
8261// re-exported from zsh_h.rs (single source of truth, i32 matching
8262// C `int`).
8263
8264// ===========================================================
8265// Methods moved verbatim from src/ported/vm_helper because their
8266// C counterpart's source file maps 1:1 to this Rust module.
8267// Rust permits multiple inherent impl blocks for the same
8268// type within a crate, so call sites in vm_helper are unchanged.
8269// ===========================================================
8270
8271// BEGIN moved-from-exec-rs
8272// (impl ShellExecutor block moved to src/exec_shims.rs — see file marker)
8273
8274// END moved-from-exec-rs
8275
8276// ===========================================================
8277// Direct ports of module-loader / dlsym / feature-array /
8278// math-func registration entries from Src/module.c. The Rust
8279// rewrite uses statically-linked module impls (each module
8280// compiled into the binary, registered through a static
8281// dispatch table — see `crate::ported::modules::mod`), so the
8282// dynamic-loader plumbing collapses to no-ops. These free-fn
8283// entries satisfy ABI/name parity for the drift gate.
8284// ===========================================================
8285
8286/// `FEAT_IGNORE` — bit in the `flags` arg to add_/del_-automathfunc
8287/// and friends. Port of `enum { FEAT_IGNORE = 0x0001 }` from
8288/// `Src/module.c:62`. /* `-i` option: ignore redefinition errors. */
8289pub const FEAT_IGNORE: i32 = 0x0001; // c:62
8290
8291/// `FEAT_INFIX` — bit indicating a condition is infix-style. Port of
8292/// `enum { FEAT_INFIX = 0x0002 }` from `Src/module.c:64`.
8293pub const FEAT_INFIX: i32 = 0x0002; // c:64
8294
8295/// `FEAT_AUTOALL` — `zmodload -a` enable-all-features. Port of
8296/// `enum { FEAT_AUTOALL = 0x0004 }` from `Src/module.c:69`.
8297pub const FEAT_AUTOALL: i32 = 0x0004; // c:69
8298
8299/// `FEAT_REMOVE` — bit indicating feature removal pass. Port of
8300/// `enum { FEAT_REMOVE = 0x0008 }` from `Src/module.c:76`.
8301pub const FEAT_REMOVE: i32 = 0x0008; // c:76
8302
8303/// `FEAT_CHECKAUTO` — verify autoloads are actually provided. Port of
8304/// `enum { FEAT_CHECKAUTO = 0x0010 }` from `Src/module.c:81`.
8305pub const FEAT_CHECKAUTO: i32 = 0x0010; // c:81
8306
8307/// !!! WARNING: RUST-ONLY FLAG BIT !!!
8308/// C has no such bit. It carries the `zmodload -m` decision on the
8309/// `Patprog pat` field of each `struct feature_enables` entry
8310/// (`Src/module.c:3253-3262` fills `fep->pat = patprogs ? *patprogs++ :
8311/// NULL`, and `do_module_features` at c:2093 branches
8312/// `fep->pat ? pattry(fep->pat, *fp) : !strcmp(*fp, esp)`). zshrs's
8313/// feature list is a plain `&[String]` with no per-entry payload, so
8314/// there is nowhere to hang the compiled pattern. `-m` sets the pat for
8315/// EVERY entry of one invocation uniformly, so the decision is carried
8316/// as this one flag bit alongside `FEAT_IGNORE`/`FEAT_CHECKAUTO` and the
8317/// pattern is compiled at the match site. Deliberately outside C's
8318/// 0x0001-0x0010 range so it cannot collide with a future upstream bit.
8319pub const FEAT_PATTERN_ARGS: i32 = 0x1000;
8320
8321/// `FINDMOD_ALIASP` — bit in `find_module()`'s `flags` arg.
8322/// Port of `enum { FINDMOD_ALIASP = 0x0001 }` from `Src/module.c:110`.
8323/// /* Resolve any aliases to the underlying module. */
8324pub const FINDMOD_ALIASP: i32 = 0x0001; // c:110
8325
8326/// `FINDMOD_CREATE` — bit in `find_module()`'s `flags` arg.
8327/// Port of `enum { FINDMOD_CREATE = 0x0002 }` from `Src/module.c:115`.
8328/// /* Create an element for the module in the list if not found. */
8329pub const FINDMOD_CREATE: i32 = 0x0002; // c:115
8330
8331#[cfg(test)]
8332mod tests {
8333 use super::*;
8334 use crate::ported::zsh_h::hashnode;
8335
8336 #[test]
8337 fn test_module_table_new() {
8338 let _g = crate::test_util::global_state_lock();
8339 let table = modulestab::new();
8340 // zsh/complete is in the default-loaded set (module.rs:1128).
8341 // zsh/datetime and zsh/system are REGISTERED but carry
8342 // MOD_UNLOAD at init — they require explicit `zmodload NAME`
8343 // to become is_loaded(). Bugs #530/#532/#535.
8344 assert!(table.is_loaded("zsh/complete"));
8345 assert!(!table.is_loaded("zsh/datetime"));
8346 assert!(!table.is_loaded("zsh/system"));
8347 assert!(!table.is_loaded("nonexistent"));
8348 }
8349
8350 /// c:2812-2913 — `unload_module` clears the bound state (finish hook
8351 /// runs, `u.linked` goes NULL, MOD_INIT_B/MOD_INIT_S drop) but only
8352 /// *deletes* the record when the module has no deps (c:2910
8353 /// `else if (!m->deps) delete_module(m)`). `zsh/complete` declares
8354 /// `zsh/zle` as a dep (Src/Zle/complete.mdd, module.rs:1513), so its
8355 /// record survives the unload with MOD_LINKED still set — which is
8356 /// what `is_loaded()` reports. `is_bound()` is the predicate that
8357 /// tracks C's "actually loaded" test (`m->u.handle && !MOD_UNLOAD`,
8358 /// c:2637), so that is what flips here.
8359 #[test]
8360 fn test_load_unload() {
8361 let _g = crate::test_util::global_state_lock();
8362 let mut table = modulestab::new();
8363 assert!(table.is_loaded("zsh/complete"));
8364 table.load_module("zsh/complete", None, false);
8365 assert!(table.is_bound("zsh/complete"));
8366
8367 table.unload_module("zsh/complete");
8368 assert!(!table.is_bound("zsh/complete"));
8369 assert!(
8370 table.is_loaded("zsh/complete"),
8371 "record must survive: zsh/complete has deps (c:2910)"
8372 );
8373
8374 table.load_module("zsh/complete", None, false);
8375 assert!(table.is_loaded("zsh/complete"));
8376 assert!(table.is_bound("zsh/complete"));
8377 }
8378
8379 #[test]
8380 fn test_list_loaded() {
8381 let _g = crate::test_util::global_state_lock();
8382 let table = modulestab::new();
8383 let loaded = table.list_loaded();
8384 // C zsh ships exactly 14 default-loaded modules (verified
8385 // against Homebrew zsh 5.9.1; full list at module.rs:1126).
8386 // The other ~17 registered modules carry MOD_UNLOAD until
8387 // `zmodload NAME` clears it (see module.rs:1148-1152).
8388 // Default-loaded count intersects (a) the registered set in
8389 // `builtin_modules` (module.rs:1034-1105, ~30 entries) with
8390 // (b) the `zsh_default_loaded` whitelist (module.rs:1126,
8391 // 14 entries). Items in (b) but NOT (a) — `zsh/compctl`,
8392 // `zsh/main`, `zsh/rlimits`, `zsh/zle` — currently land in the
8393 // autoload registry only, so the actual count is 14 - 4 = 10
8394 // (a couple of overlaps land it at ~11 in practice).
8395 assert!(
8396 loaded.len() >= 10,
8397 "expected >= 10 default-loaded modules, got {}",
8398 loaded.len()
8399 );
8400 assert!(loaded.contains(&"zsh/complete"));
8401 }
8402
8403 #[test]
8404 fn test_autoload() {
8405 let _g = crate::test_util::global_state_lock();
8406 let mut table = modulestab::new();
8407 table.add_autobin("my_cmd", "zsh/mymodule", 0);
8408 assert_eq!(
8409 table.resolve_autoload_builtin("my_cmd"),
8410 Some("zsh/mymodule")
8411 );
8412 assert_eq!(table.resolve_autoload_builtin("nonexistent"), None);
8413 }
8414
8415 #[test]
8416 fn test_features() {
8417 let _g = crate::test_util::global_state_lock();
8418 // The per-module feature ledger has been deleted (canonical
8419 // C-tables track features in `BUILTINTAB`/`CONDTAB`/`PARAMTAB`).
8420 // `list_features` now returns an empty vec — `module_linked`
8421 // is the right test for "is this module registered".
8422 let table = modulestab::new();
8423 let features = table.list_features("zsh/complete");
8424 assert!(features.is_empty());
8425 assert!(table.module_linked("zsh/complete"));
8426 }
8427
8428 #[test]
8429 fn test_module_linked() {
8430 let _g = crate::test_util::global_state_lock();
8431 let table = modulestab::new();
8432 assert!(table.module_linked("zsh/complete"));
8433 assert!(table.module_linked("zsh/stat"));
8434 assert!(!table.module_linked("zsh/nonexistent"));
8435 }
8436
8437 #[test]
8438 fn test_printmodulenode() {
8439 let _g = crate::test_util::global_state_lock();
8440 // C-source-true print gate: `m->u.handle || (flags &
8441 // PRINTMOD_AUTO)` at Src/module.c:218 — only fires once
8442 // `load_module` has wired up the union slot AND set
8443 // MOD_INIT_B (c:2244). Fresh `module::new` returns a
8444 // registered-but-not-loaded entry, so the gate misses.
8445 // Mark MOD_INIT_B to simulate the post-boot state.
8446 let mut module = module::new("zsh/test");
8447 module.node.flags |= MOD_INIT_B;
8448 // Loaded module, no flags → emit just the module name
8449 // (c:240 nicezputs(modname)).
8450 let output = printmodulenode("zsh/test", &module, 0);
8451 assert_eq!(output, "zsh/test");
8452 // Under PRINTMOD_LIST the loaded branch emits `zmodload MOD`.
8453 let listed = printmodulenode("zsh/test", &module, PRINTMOD_LIST);
8454 assert_eq!(listed, "zmodload zsh/test");
8455 // Registered-but-not-loaded: no MOD_INIT_B → empty output
8456 // (matches C's `m->u.handle` being NULL).
8457 let unloaded = module::new("zsh/unloaded");
8458 let nope = printmodulenode("zsh/unloaded", &unloaded, 0);
8459 assert_eq!(nope, "");
8460 }
8461
8462 // ===== Tests for the `addmathfunc` / `removemathfunc` /
8463 // `deletemathfunc` family ported in this session against the
8464 // MATHFUNCS Lazy<Mutex<Vec<mathfunc>>> global. Each test isolates
8465 // its names with a unique prefix so they don't collide if the
8466 // suite runs in parallel.
8467
8468 fn mk_mf(name: &str, autoload: bool) -> mathfunc {
8469 mathfunc {
8470 next: None,
8471 name: name.to_string(),
8472 flags: 0,
8473 nfunc: None,
8474 sfunc: None,
8475 module: if autoload {
8476 Some("zsh/test".to_string())
8477 } else {
8478 None
8479 },
8480 minargs: 0,
8481 maxargs: 0,
8482 funcid: 0,
8483 }
8484 }
8485
8486 #[test]
8487 fn addmathfunc_clash_returns_one_when_already_added() {
8488 let _g = crate::test_util::global_state_lock();
8489 // C addmathfunc returns 1 when an existing entry has the same
8490 // name AND is non-autoloadable (no module set). Verifies the
8491 // "already in table" branch at module.c:1322-1330.
8492 let f1 = mk_mf("zshrs_test_clash_a", false);
8493 let f2 = mk_mf("zshrs_test_clash_a", false);
8494 assert_eq!(addmathfunc(f1), 0);
8495 assert_eq!(addmathfunc(f2), 1, "second add should clash");
8496 removemathfunc("zshrs_test_clash_a");
8497 }
8498
8499 #[test]
8500 fn addmathfunc_autoload_replace_succeeds() {
8501 let _g = crate::test_util::global_state_lock();
8502 // When existing entry IS autoloadable (module.is_some, no
8503 // MFF_USERFUNC), C removes-then-replaces. The new entry should
8504 // land at index 0 (prepend) per c:1334-1335.
8505 let auto = mk_mf("zshrs_test_replace", true);
8506 let real = mk_mf("zshrs_test_replace", false);
8507 assert_eq!(addmathfunc(auto), 0);
8508 assert_eq!(
8509 addmathfunc(real),
8510 0,
8511 "autoloadable entry must be replaceable"
8512 );
8513 let tab = MATHFUNCS.lock().unwrap();
8514 let entry = tab.iter().find(|m| m.name == "zshrs_test_replace").unwrap();
8515 assert!(
8516 entry.module.is_none(),
8517 "after replace, module should be None"
8518 );
8519 drop(tab);
8520 removemathfunc("zshrs_test_replace");
8521 }
8522
8523 #[test]
8524 fn removemathfunc_returns_unit_and_drops() {
8525 let _g = crate::test_util::global_state_lock();
8526 let f = mk_mf("zshrs_test_remove", false);
8527 assert_eq!(addmathfunc(f), 0);
8528 assert!(MATHFUNCS
8529 .lock()
8530 .unwrap()
8531 .iter()
8532 .any(|m| m.name == "zshrs_test_remove"));
8533 removemathfunc("zshrs_test_remove");
8534 assert!(!MATHFUNCS
8535 .lock()
8536 .unwrap()
8537 .iter()
8538 .any(|m| m.name == "zshrs_test_remove"));
8539 }
8540
8541 #[test]
8542 fn deletemathfunc_returns_minus_one_on_miss() {
8543 let _g = crate::test_util::global_state_lock();
8544 // C: returns -1 when no matching entry; verifies the c:1361 branch.
8545 let probe = mk_mf("zshrs_test_never_added_xyz", false);
8546 assert_eq!(deletemathfunc(&probe), -1);
8547 }
8548
8549 #[test]
8550 fn deletemathfunc_clears_added_flag_for_userfunc() {
8551 let _g = crate::test_util::global_state_lock();
8552 // For non-module entries (`!f->module`), C clears the MFF_ADDED
8553 // flag instead of dropping the node (c:1357). Tests by adding
8554 // a user-defined mathfunc, flipping MFF_ADDED on, then deleting.
8555 let mut f = mk_mf("zshrs_test_clear_flag", false);
8556 f.flags = MFF_ADDED;
8557 assert_eq!(
8558 addmathfunc(f),
8559 1,
8560 "MFF_ADDED set → addmathfunc clashes at c:1318"
8561 );
8562 // Now seed it manually with module=None and MFF_ADDED so deletemathfunc
8563 // exercises the clear-flag branch.
8564 MATHFUNCS
8565 .lock()
8566 .unwrap()
8567 .insert(0, mk_mf("zshrs_test_clear_flag2", false));
8568 let mut f2 = mk_mf("zshrs_test_clear_flag2", false);
8569 f2.flags = MFF_ADDED;
8570 // f2 is the lookup probe; by name it matches the seeded entry.
8571 assert_eq!(deletemathfunc(&f2), 0);
8572 let tab = MATHFUNCS.lock().unwrap();
8573 let entry = tab.iter().find(|m| m.name == "zshrs_test_clear_flag2");
8574 // Entry stays in the table (module was None) but MFF_ADDED cleared.
8575 if let Some(e) = entry {
8576 assert_eq!(e.flags & MFF_ADDED, 0);
8577 }
8578 drop(tab);
8579 removemathfunc("zshrs_test_clear_flag2");
8580 }
8581
8582 // ===== Tests for `addconddef` / `deleteconddef` against CONDTAB.
8583
8584 fn mk_cd(name: &str, infix: bool, autoload: bool) -> conddef {
8585 conddef {
8586 next: None,
8587 name: name.to_string(),
8588 flags: if infix { CONDF_INFIX } else { 0 },
8589 handler: None,
8590 min: 0,
8591 max: 0,
8592 condid: 0,
8593 module: if autoload {
8594 Some("zsh/cond".to_string())
8595 } else {
8596 None
8597 },
8598 }
8599 }
8600
8601 #[test]
8602 fn addconddef_clash_returns_one() {
8603 let _g = crate::test_util::global_state_lock();
8604 // C addconddef: clash when existing has same name+infix AND is
8605 // not autoloadable, OR is already added (CONDF_ADDED flag).
8606 let c1 = mk_cd("zshrs_test_cond_clash", false, false);
8607 let c2 = mk_cd("zshrs_test_cond_clash", false, false);
8608 assert_eq!(addconddef(c1), 0);
8609 assert_eq!(addconddef(c2), 1);
8610 let probe = mk_cd("zshrs_test_cond_clash", false, false);
8611 assert_eq!(deleteconddef(&probe), 0);
8612 }
8613
8614 #[test]
8615 fn deleteconddef_returns_minus_one_on_miss() {
8616 let _g = crate::test_util::global_state_lock();
8617 let probe = mk_cd("zshrs_test_cond_never_added", false, false);
8618 assert_eq!(deleteconddef(&probe), -1);
8619 }
8620
8621 #[test]
8622 fn addconddef_distinguishes_infix_from_prefix() {
8623 let _g = crate::test_util::global_state_lock();
8624 // CONDF_INFIX is part of the clash key — a prefix-form `-z` and
8625 // an infix-form `==` share neither name nor flag, so adding both
8626 // names with different infix bits should both succeed.
8627 let prefix = mk_cd("zshrs_test_cond_dual", false, false);
8628 let infix = mk_cd("zshrs_test_cond_dual", true, false);
8629 assert_eq!(addconddef(prefix), 0);
8630 assert_eq!(
8631 addconddef(infix),
8632 0,
8633 "infix variant must not clash with prefix variant"
8634 );
8635 // Cleanup both
8636 let _ = deleteconddef(&mk_cd("zshrs_test_cond_dual", false, false));
8637 let _ = deleteconddef(&mk_cd("zshrs_test_cond_dual", true, false));
8638 }
8639
8640 // ===== Tests for `setconddefs` / `setmathfuncs` bulk dispatch.
8641
8642 #[test]
8643 fn setconddefs_bulk_add_then_bulk_delete_via_e_array() {
8644 let _g = crate::test_util::global_state_lock();
8645 // C setconddefs: walks (c, e) pairs; e[i]!=0 → addconddef path,
8646 // e[i]==0 → deleteconddef path. Tests the round trip.
8647 let mut entries = vec![
8648 mk_cd("zshrs_test_bulk_a", false, false),
8649 mk_cd("zshrs_test_bulk_b", false, false),
8650 ];
8651 let add_selectors = [1, 1];
8652 assert_eq!(setconddefs("test", &mut entries, Some(&add_selectors)), 0);
8653 // Both should now have CONDF_ADDED set per c:773.
8654 assert_ne!(entries[0].flags & CONDF_ADDED, 0);
8655 assert_ne!(entries[1].flags & CONDF_ADDED, 0);
8656 // Now delete both via e=[0,0].
8657 let del_selectors = [0, 0];
8658 assert_eq!(setconddefs("test", &mut entries, Some(&del_selectors)), 0);
8659 assert_eq!(entries[0].flags & CONDF_ADDED, 0);
8660 assert_eq!(entries[1].flags & CONDF_ADDED, 0);
8661 }
8662
8663 // ===== Tests for `addbuiltin` / `addbuiltins` against canonical builtintab.
8664
8665 fn mk_b(nam: &str) -> builtin {
8666 builtin {
8667 node: hashnode {
8668 next: None,
8669 nam: nam.to_string(),
8670 flags: 0,
8671 },
8672 handlerfunc: None,
8673 minargs: 0,
8674 maxargs: 0,
8675 funcid: 0,
8676 optstr: None,
8677 defopts: None,
8678 }
8679 }
8680
8681 #[test]
8682 fn addbuiltin_clash_against_existing_builtintab_entry() {
8683 let _g = crate::test_util::global_state_lock();
8684 // C addbuiltin: returns 1 when builtintab already has an entry
8685 // for the same name with BINF_ADDED set. The canonical Rust
8686 // builtintab is populated at startup via createbuiltintable;
8687 // probing a real builtin like "echo" should clash if BINF_ADDED.
8688 let _ = createbuiltintable();
8689 let mut b = mk_b("echo");
8690 let r = addbuiltin(&mut b);
8691 // BINF_ADDED gets set on b when no clash. If echo was BINF_ADDED in
8692 // the static table, r==1; otherwise r==0 and b.flags now has BINF_ADDED.
8693 assert!(r == 0 || r == 1);
8694 if r == 0 {
8695 assert_ne!(b.node.flags & BINF_ADDED as i32, 0);
8696 }
8697 }
8698
8699 #[test]
8700 fn addbuiltins_skips_already_added_entries() {
8701 let _g = crate::test_util::global_state_lock();
8702 // C addbuiltins (c:553): `if (b->node.flags & BINF_ADDED) continue`.
8703 // Pre-marking BINF_ADDED should skip both entries; ret stays 0.
8704 let mut b1 = mk_b("zshrs_test_already_added_1");
8705 b1.node.flags = BINF_ADDED as i32;
8706 let mut b2 = mk_b("zshrs_test_already_added_2");
8707 b2.node.flags = BINF_ADDED as i32;
8708 let mut binl = vec![b1, b2];
8709 assert_eq!(addbuiltins("test", &mut binl), 0);
8710 }
8711
8712 // ===== Tests for `addwrapper` / `deletewrapper` against WRAPPERS.
8713
8714 fn mk_w() -> funcwrap {
8715 funcwrap {
8716 next: None,
8717 flags: 0,
8718 handler: Some(|_prog, _w, _name| 0),
8719 module: None,
8720 }
8721 }
8722
8723 #[test]
8724 fn addwrapper_then_deletewrapper_round_trip() {
8725 let _g = crate::test_util::global_state_lock();
8726 // Register a non-alias module in the live table so the c:586
8727 // MOD_ALIAS gate is exercised and clears.
8728 MODULESTAB
8729 .lock()
8730 .unwrap()
8731 .modules
8732 .insert("zsh/test".to_string(), module::new("zsh/test"));
8733 let w = mk_w();
8734 assert_eq!(addwrapper("zsh/test", w), 0);
8735 // c:597-598 — the node lands in `wrappers` carrying WRAPF_ADDED
8736 // and a back-link to its module, which is what `deletewrapper`
8737 // matches on.
8738 assert!(WRAPPERS.lock().unwrap().iter().any(|p| {
8739 p.flags & crate::ported::zsh_h::WRAPF_ADDED != 0
8740 && p.module.as_ref().map(|m| m.node.nam.as_str()) == Some("zsh/test")
8741 }));
8742 // c:618-625 — found: unlink and return 0.
8743 assert_eq!(deletewrapper("zsh/test"), 0);
8744 // c:628 — second removal misses.
8745 assert_eq!(deletewrapper("zsh/test"), 1);
8746 MODULESTAB.lock().unwrap().modules.remove("zsh/test");
8747 }
8748
8749 /// c:586-587 — `addwrapper` refuses an alias module, so nothing
8750 /// lands in `wrappers` and `deletewrapper` reports the same refusal.
8751 #[test]
8752 fn addwrapper_refuses_alias_module() {
8753 let _g = crate::test_util::global_state_lock();
8754 {
8755 let mut tab = MODULESTAB.lock().unwrap();
8756 let mut m = module::new("zsh/testalias");
8757 m.node.flags |= MOD_ALIAS;
8758 tab.modules.insert("zsh/testalias".to_string(), m);
8759 }
8760 assert_eq!(addwrapper("zsh/testalias", mk_w()), 1); // c:587
8761 assert_eq!(deletewrapper("zsh/testalias"), 1); // c:613
8762 MODULESTAB.lock().unwrap().modules.remove("zsh/testalias");
8763 }
8764
8765 /// c:589-590 — a node that already carries `WRAPF_ADDED` is
8766 /// refused rather than double-linked.
8767 #[test]
8768 fn addwrapper_refuses_already_added_node() {
8769 let _g = crate::test_util::global_state_lock();
8770 let mut w = mk_w();
8771 w.flags |= crate::ported::zsh_h::WRAPF_ADDED;
8772 assert_eq!(addwrapper("zsh/test", w), 1); // c:590
8773 }
8774
8775 #[test]
8776 fn deletewrapper_returns_one_when_not_found() {
8777 let _g = crate::test_util::global_state_lock();
8778 // Empty WRAPPERS means any probe misses. Take a snapshot of the
8779 // current state, drain WRAPPERS, run the test, restore.
8780 let snapshot: Vec<_> = WRAPPERS.lock().unwrap().drain(..).collect();
8781 assert_eq!(deletewrapper("zsh/test"), 1);
8782 WRAPPERS.lock().unwrap().extend(snapshot);
8783 }
8784
8785 // C-faithful hookdef tests. The system under test is:
8786 // - `hooktab` (file-static linked-list head, port of c:843)
8787 // - `gethookdef` / `addhookdef` / `addhookdefs` / `deletehookdef` /
8788 // `deletehookdefs` / `addhookdeffunc` / `addhookfunc` /
8789 // `deletehookdeffunc` / `deletehookfunc` / `runhookdef` —
8790 // C-identical signatures over real `Hookfn` fn pointers.
8791 //
8792 // Each test holds `global_state_lock` and snapshots the chain at
8793 // start so any incidental registrations from other state leak don't
8794 // affect outcomes.
8795
8796 // Real Rust ported matching the `Hookfn` shape. Used as test handlers
8797 // that bump a per-test atomic counter when invoked, proving
8798 // `runhookdef` actually dispatches the registered fn pointers.
8799 static H1_CALLS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
8800 static H2_CALLS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
8801 static H1_RETVAL: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
8802
8803 fn h1(_h: *mut hookdef, _d: *mut std::ffi::c_void) -> i32 {
8804 H1_CALLS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
8805 H1_RETVAL.load(std::sync::atomic::Ordering::SeqCst)
8806 }
8807 fn h2(_h: *mut hookdef, _d: *mut std::ffi::c_void) -> i32 {
8808 H2_CALLS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
8809 0
8810 }
8811
8812 // Allocate a fresh hookdef on the heap and leak its Box so that the
8813 // returned `*mut hookdef` has program-lifetime — matching C's
8814 // static-storage hookdef nodes (zshhooks[] etc.). The test then
8815 // takes responsibility for splicing it out via deletehookdef.
8816 fn leak_hookdef(name: &str, flags: i32) -> *mut hookdef {
8817 Box::into_raw(Box::new(hookdef {
8818 next: std::ptr::null_mut(),
8819 name: name.to_string(),
8820 def: None,
8821 flags,
8822 funcs: std::ptr::null_mut(),
8823 }))
8824 }
8825
8826 /// `gethookdef` returns null when no hookdef with that name is in
8827 /// `hooktab`, and the registered pointer once `addhookdef` runs.
8828 #[test]
8829 fn gethookdef_returns_null_then_registered_ptr() {
8830 let _g = crate::test_util::global_state_lock();
8831 // Unique name to avoid colliding with other tests' registrations.
8832 let h = leak_hookdef("zshrs_test_get_hook", HOOKF_ALL);
8833 unsafe {
8834 assert!(gethookdef(&(*h).name).is_null());
8835 }
8836 assert_eq!(addhookdef(h), 0);
8837 unsafe {
8838 assert_eq!(gethookdef(&(*h).name), h);
8839 }
8840 // Cleanup so the chain doesn't leak into other tests.
8841 assert_eq!(deletehookdef(h), 0);
8842 unsafe {
8843 drop(Box::from_raw(h));
8844 }
8845 }
8846
8847 /// `addhookdef` rejects a name already present in `hooktab`
8848 /// (port of c:866-867: `if (gethookdef(h->name)) return 1`).
8849 #[test]
8850 fn addhookdef_rejects_duplicate_name() {
8851 let _g = crate::test_util::global_state_lock();
8852 let h1 = leak_hookdef("zshrs_test_dup_hook", HOOKF_ALL);
8853 let h2 = leak_hookdef("zshrs_test_dup_hook", HOOKF_ALL);
8854 assert_eq!(addhookdef(h1), 0);
8855 assert_eq!(addhookdef(h2), 1, "duplicate name must return 1");
8856 // Cleanup.
8857 assert_eq!(deletehookdef(h1), 0);
8858 unsafe {
8859 drop(Box::from_raw(h1));
8860 drop(Box::from_raw(h2));
8861 }
8862 }
8863
8864 /// `deletehookdef` returns 1 when the hookdef isn't in the chain
8865 /// (port of c:909-910: `if (!p) return 1`) and 0 after add.
8866 #[test]
8867 fn deletehookdef_returns_one_on_miss_zero_on_hit() {
8868 let _g = crate::test_util::global_state_lock();
8869 let h = leak_hookdef("zshrs_test_del_hook", HOOKF_ALL);
8870 assert_eq!(deletehookdef(h), 1, "not in chain → 1");
8871 assert_eq!(addhookdef(h), 0);
8872 assert_eq!(deletehookdef(h), 0, "spliced out → 0");
8873 assert_eq!(deletehookdef(h), 1, "second delete misses → 1");
8874 unsafe {
8875 drop(Box::from_raw(h));
8876 }
8877 }
8878
8879 /// `runhookdef` dispatches every registered Hookfn under `HOOKF_ALL`
8880 /// (port of c:996-1004) — the test handlers' counters bump on every
8881 /// fire, and the first non-zero return short-circuits.
8882 #[test]
8883 fn runhookdef_dispatches_all_under_hookf_all_short_circuits_on_nonzero() {
8884 let _g = crate::test_util::global_state_lock();
8885 H1_CALLS.store(0, std::sync::atomic::Ordering::SeqCst);
8886 H2_CALLS.store(0, std::sync::atomic::Ordering::SeqCst);
8887 H1_RETVAL.store(0, std::sync::atomic::Ordering::SeqCst);
8888
8889 let h = leak_hookdef("zshrs_test_runall", HOOKF_ALL);
8890 assert_eq!(addhookdef(h), 0);
8891 assert_eq!(addhookdeffunc(h, h1), 0);
8892 assert_eq!(addhookdeffunc(h, h2), 0);
8893
8894 // Both fire → r1=0, r2=0 → final return 0.
8895 assert_eq!(runhookdef(h, std::ptr::null_mut()), 0);
8896 assert_eq!(H1_CALLS.load(std::sync::atomic::Ordering::SeqCst), 1);
8897 assert_eq!(H2_CALLS.load(std::sync::atomic::Ordering::SeqCst), 1);
8898
8899 // Make h1 return 7 → runhookdef should return 7 without calling h2.
8900 H1_CALLS.store(0, std::sync::atomic::Ordering::SeqCst);
8901 H2_CALLS.store(0, std::sync::atomic::Ordering::SeqCst);
8902 H1_RETVAL.store(7, std::sync::atomic::Ordering::SeqCst);
8903 assert_eq!(runhookdef(h, std::ptr::null_mut()), 7);
8904 assert_eq!(H1_CALLS.load(std::sync::atomic::Ordering::SeqCst), 1);
8905 assert_eq!(
8906 H2_CALLS.load(std::sync::atomic::Ordering::SeqCst),
8907 0,
8908 "h2 must not fire after h1 returns nonzero"
8909 );
8910
8911 // Cleanup.
8912 assert_eq!(deletehookdef(h), 0);
8913 unsafe {
8914 drop(Box::from_raw(h));
8915 }
8916 }
8917
8918 /// `runhookdef` calls only the LAST registered Hookfn when
8919 /// `HOOKF_ALL` is clear (port of c:1006).
8920 #[test]
8921 fn runhookdef_calls_only_last_when_hookf_all_clear() {
8922 let _g = crate::test_util::global_state_lock();
8923 H1_CALLS.store(0, std::sync::atomic::Ordering::SeqCst);
8924 H2_CALLS.store(0, std::sync::atomic::Ordering::SeqCst);
8925 H1_RETVAL.store(0, std::sync::atomic::Ordering::SeqCst);
8926
8927 let h = leak_hookdef("zshrs_test_runlast", 0); // HOOKF_ALL clear
8928 assert_eq!(addhookdef(h), 0);
8929 assert_eq!(addhookdeffunc(h, h1), 0);
8930 assert_eq!(addhookdeffunc(h, h2), 0);
8931
8932 runhookdef(h, std::ptr::null_mut());
8933 assert_eq!(
8934 H1_CALLS.load(std::sync::atomic::Ordering::SeqCst),
8935 0,
8936 "h1 must not fire when HOOKF_ALL is clear"
8937 );
8938 assert_eq!(
8939 H2_CALLS.load(std::sync::atomic::Ordering::SeqCst),
8940 1,
8941 "only the last-registered handler fires"
8942 );
8943
8944 assert_eq!(deletehookdef(h), 0);
8945 unsafe {
8946 drop(Box::from_raw(h));
8947 }
8948 }
8949
8950 /// `deletehookdeffunc` removes a single Hookfn from the chain
8951 /// (port of c:961-973). Pins the closure-shadow regression at the
8952 /// new free-fn surface.
8953 #[test]
8954 fn deletehookdeffunc_removes_only_target_hookfn() {
8955 let _g = crate::test_util::global_state_lock();
8956 H1_CALLS.store(0, std::sync::atomic::Ordering::SeqCst);
8957 H2_CALLS.store(0, std::sync::atomic::Ordering::SeqCst);
8958 H1_RETVAL.store(0, std::sync::atomic::Ordering::SeqCst);
8959
8960 let h = leak_hookdef("zshrs_test_del_fn", HOOKF_ALL);
8961 assert_eq!(addhookdef(h), 0);
8962 addhookdeffunc(h, h1);
8963 addhookdeffunc(h, h2);
8964 // Remove only h1 — h2 must remain and fire.
8965 assert_eq!(deletehookdeffunc(h, h1), 0);
8966 // Second remove of h1 returns 1 (miss).
8967 assert_eq!(deletehookdeffunc(h, h1), 1);
8968 runhookdef(h, std::ptr::null_mut());
8969 assert_eq!(H1_CALLS.load(std::sync::atomic::Ordering::SeqCst), 0);
8970 assert_eq!(H2_CALLS.load(std::sync::atomic::Ordering::SeqCst), 1);
8971
8972 assert_eq!(deletehookdef(h), 0);
8973 unsafe {
8974 drop(Box::from_raw(h));
8975 }
8976 }
8977
8978 /// `addhookfunc` / `deletehookfunc` return 1 when no hookdef with
8979 /// that name is registered (port of c:953, c:982).
8980 #[test]
8981 fn addhookfunc_and_deletehookfunc_return_one_when_no_hookdef() {
8982 let _g = crate::test_util::global_state_lock();
8983 assert_eq!(addhookfunc("zshrs_test_no_such_hook", h1), 1);
8984 assert_eq!(deletehookfunc("zshrs_test_no_such_hook", h1), 1);
8985 }
8986
8987 /// `addhookdefs(NULL, h_array, size)` registers contiguous hookdef
8988 /// nodes from an array (port of c:883-895). Each entry must be
8989 /// findable via `gethookdef` after the call.
8990 #[test]
8991 fn addhookdefs_registers_contiguous_array() {
8992 let _g = crate::test_util::global_state_lock();
8993 let arr: Box<[hookdef; 2]> = Box::new([
8994 hookdef {
8995 next: std::ptr::null_mut(),
8996 name: "zshrs_test_arr_0".into(),
8997 def: None,
8998 flags: HOOKF_ALL,
8999 funcs: std::ptr::null_mut(),
9000 },
9001 hookdef {
9002 next: std::ptr::null_mut(),
9003 name: "zshrs_test_arr_1".into(),
9004 def: None,
9005 flags: HOOKF_ALL,
9006 funcs: std::ptr::null_mut(),
9007 },
9008 ]);
9009 let base: *mut hookdef = Box::into_raw(arr) as *mut hookdef;
9010 assert_eq!(addhookdefs(std::ptr::null(), base, 2), 0);
9011 assert_eq!(gethookdef("zshrs_test_arr_0"), base);
9012 unsafe {
9013 assert_eq!(gethookdef("zshrs_test_arr_1"), base.add(1));
9014 }
9015 // Cleanup. Splice out then re-take ownership of the boxed array.
9016 unsafe {
9017 assert_eq!(deletehookdef(base), 0);
9018 assert_eq!(deletehookdef(base.add(1)), 0);
9019 drop(Box::from_raw(base as *mut [hookdef; 2]));
9020 }
9021 }
9022}
9023
9024#[cfg(test)]
9025mod modname_tests {
9026 use super::*;
9027
9028 /// c:2173 — `modname_ok` accepts shell identifier names possibly
9029 /// joined by `/` (zsh modules are namespaced like `zsh/datetime`).
9030 /// Plain alphanumeric names with `/` separators MUST pass.
9031 /// A regression rejecting valid module names would break every
9032 /// `zmodload zsh/datetime` invocation.
9033 #[test]
9034 fn modname_ok_accepts_canonical_zsh_module_paths() {
9035 let _g = crate::test_util::global_state_lock();
9036 assert_eq!(modname_ok("zsh"), 1);
9037 assert_eq!(modname_ok("zsh/datetime"), 1);
9038 assert_eq!(modname_ok("zsh/zle"), 1);
9039 assert_eq!(modname_ok("foo_bar"), 1);
9040 assert_eq!(modname_ok("foo123"), 1);
9041 }
9042
9043 /// c:2179 — non-identifier chars (excluding `/`) MUST cause
9044 /// rejection. A regression accepting them would let modules
9045 /// install with names that no later `zmodload -u` could remove.
9046 #[test]
9047 fn modname_ok_rejects_special_chars() {
9048 let _g = crate::test_util::global_state_lock();
9049 assert_eq!(modname_ok("zsh space"), 0);
9050 assert_eq!(modname_ok("zsh-bad"), 0, "hyphen is not IIDENT");
9051 assert_eq!(modname_ok("zsh.foo"), 0, "dot is not IIDENT");
9052 assert_eq!(modname_ok("$foo"), 0);
9053 }
9054
9055 /// c:2177 — `if (!*p) return 1` runs at the START of the loop;
9056 /// empty input therefore returns 1. Pin this behaviour so callers
9057 /// know the empty-string case maps to "trivially OK" not "error".
9058 #[test]
9059 fn modname_ok_treats_empty_as_trivially_ok() {
9060 let _g = crate::test_util::global_state_lock();
9061 assert_eq!(modname_ok(""), 1);
9062 }
9063
9064 /// `Src/module.c:464-478` — `del_autobin` returns 2 for "no such
9065 /// builtin" (unless FEAT_IGNORE), 3 for "registered builtin —
9066 /// can't unload" (unless FEAT_IGNORE), 0 for success.
9067 #[test]
9068 fn del_autobin_unknown_name_returns_2_or_zero_per_feat_ignore() {
9069 let _g = crate::test_util::global_state_lock();
9070 let mut t = modulestab::new();
9071 // unknown name + no FEAT_IGNORE → 2
9072 assert_eq!(t.del_autobin("definitely_not_a_builtin", 0), 2);
9073 // unknown name + FEAT_IGNORE → 0
9074 assert_eq!(t.del_autobin("definitely_not_a_builtin", FEAT_IGNORE), 0);
9075 }
9076
9077 /// `Src/module.c:464-478` — known static-linked builtin (e.g. "echo")
9078 /// is in createbuiltintable(), counts as BINF_ADDED → return 3
9079 /// unless FEAT_IGNORE.
9080 #[test]
9081 fn del_autobin_registered_builtin_returns_3_or_zero_per_feat_ignore() {
9082 let _g = crate::test_util::global_state_lock();
9083 let mut t = modulestab::new();
9084 // "echo" is a static-linked builtin → can't unload → 3
9085 assert_eq!(t.del_autobin("echo", 0), 3);
9086 // With FEAT_IGNORE → 0
9087 assert_eq!(t.del_autobin("echo", FEAT_IGNORE), 0);
9088 }
9089
9090 /// `Src/module.c:464-478` — name that was added via `add_autobin`
9091 /// (i.e. in autoload_builtins) but NOT in builtintab → success
9092 /// path → return 0 + removes from autoload ledger.
9093 #[test]
9094 fn del_autobin_autoload_only_entry_removed() {
9095 let _g = crate::test_util::global_state_lock();
9096 let mut t = modulestab::new();
9097 // Seed an autoload entry not in the static builtintab.
9098 t.autoload_builtins
9099 .insert("zshrs_test_autobin_x".to_string(), "mymod".to_string());
9100 assert_eq!(t.del_autobin("zshrs_test_autobin_x", 0), 0);
9101 assert!(
9102 !t.autoload_builtins.contains_key("zshrs_test_autobin_x"),
9103 "successful del must remove ledger entry"
9104 );
9105 // Second call → now "no such" → 2.
9106 assert_eq!(t.del_autobin("zshrs_test_autobin_x", 0), 2);
9107 assert_eq!(t.del_autobin("zshrs_test_autobin_x", FEAT_IGNORE), 0);
9108 }
9109
9110 /// `Src/module.c:819-835` — `del_autocond` parallel contract: 2
9111 /// for "no such", 0 for autoload-entry-removed.
9112 #[test]
9113 fn del_autocond_autoload_entry_removed_or_not_found() {
9114 let _g = crate::test_util::global_state_lock();
9115 let mut t = modulestab::new();
9116 // Not present → 2 / 0 per FEAT_IGNORE.
9117 assert_eq!(t.del_autocond("zshrs_test_cond_x", 0), 2);
9118 assert_eq!(t.del_autocond("zshrs_test_cond_x", FEAT_IGNORE), 0);
9119 // Seed through the C counterpart `add_autocond` (c:791) — it is
9120 // what puts the conddef in CONDTAB. A bare `autoload_conditions`
9121 // insert is NOT enough: `del_autocond` looks the name up with
9122 // `getconddef` (c:820), which only ever walks CONDTAB, so a
9123 // ledger-only entry still reports "no such" (2).
9124 assert_eq!(t.add_autocond("zshrs_test_cond_x", "mymod", 0), 0);
9125 assert_eq!(t.del_autocond("zshrs_test_cond_x", 0), 0);
9126 assert!(!t.autoload_conditions.contains_key("zshrs_test_cond_x"));
9127 // Removed from CONDTAB too → back to "no such".
9128 assert_eq!(t.del_autocond("zshrs_test_cond_x", 0), 2);
9129 }
9130
9131 /// `Src/module.c:1240-1255` — `del_autoparam` parallel contract.
9132 /// 2 for "no such", 3 for "param exists without PM_AUTOLOAD —
9133 /// can't unload", 0 for success.
9134 #[test]
9135 fn del_autoparam_unknown_name_returns_2() {
9136 let _g = crate::test_util::global_state_lock();
9137 let mut t = modulestab::new();
9138 assert_eq!(t.del_autoparam("zshrs_test_param_x_unknown", 0), 2);
9139 assert_eq!(
9140 t.del_autoparam("zshrs_test_param_x_unknown", FEAT_IGNORE),
9141 0
9142 );
9143 }
9144
9145 // ─── zsh-corpus pins for module table ───────────────────────────
9146
9147 /// `register_module` returns true on first registration.
9148 #[test]
9149 fn module_corpus_register_new_returns_true() {
9150 let _g = crate::test_util::global_state_lock();
9151 let mut t = newmoduletable();
9152 assert!(register_module(&mut t, "myfresh.module"));
9153 }
9154
9155 /// `register_module` returns false on duplicate registration.
9156 #[test]
9157 fn module_corpus_register_duplicate_returns_false() {
9158 let _g = crate::test_util::global_state_lock();
9159 let mut t = newmoduletable();
9160 assert!(register_module(&mut t, "dup.module"));
9161 assert!(
9162 !register_module(&mut t, "dup.module"),
9163 "second registration of same name = false"
9164 );
9165 }
9166
9167 /// New modulestab is pre-seeded with built-in modules.
9168 /// Pin: pre-seed count is positive and stable.
9169 #[test]
9170 fn module_corpus_new_table_has_builtin_modules() {
9171 let _g = crate::test_util::global_state_lock();
9172 let t = newmoduletable();
9173 assert!(
9174 t.modules.len() >= 1,
9175 "fresh modulestab has built-ins, got {}",
9176 t.modules.len()
9177 );
9178 }
9179
9180 /// `register_module` with empty name does not panic.
9181 #[test]
9182 fn module_corpus_register_empty_name_does_not_panic() {
9183 let _g = crate::test_util::global_state_lock();
9184 let mut t = newmoduletable();
9185 let _ = register_module(&mut t, "");
9186 }
9187
9188 /// Default callback `setup_` returns 0.
9189 #[test]
9190 fn module_corpus_setup_callback_default_returns_zero() {
9191 let _g = crate::test_util::global_state_lock();
9192 assert_eq!(setup_(std::ptr::null()), 0);
9193 }
9194
9195 /// Default `boot_` returns 0.
9196 #[test]
9197 fn module_corpus_boot_callback_default_returns_zero() {
9198 let _g = crate::test_util::global_state_lock();
9199 assert_eq!(boot_(std::ptr::null()), 0);
9200 }
9201
9202 /// Default `cleanup_` returns 0.
9203 #[test]
9204 fn module_corpus_cleanup_callback_default_returns_zero() {
9205 let _g = crate::test_util::global_state_lock();
9206 assert_eq!(cleanup_(std::ptr::null()), 0);
9207 }
9208
9209 /// Default `finish_` returns 0.
9210 #[test]
9211 fn module_corpus_finish_callback_default_returns_zero() {
9212 let _g = crate::test_util::global_state_lock();
9213 assert_eq!(finish_(std::ptr::null()), 0);
9214 }
9215
9216 // ═══════════════════════════════════════════════════════════════════
9217 // C-parity tests pinning Src/module.c. Tests that capture KNOWN
9218 // ZSHRS BUGS use #[ignore = "ZSHRS BUG: …"].
9219 // ═══════════════════════════════════════════════════════════════════
9220
9221 /// `newmoduletable()` returns an empty modules table per C.
9222 /// C `Src/module.c:newmoduletable` allocates a fresh HashTable
9223 /// with NO entries — modules are added later via addbuiltin /
9224 /// load_module dispatch.
9225 /// ZSHRS BUG: Rust port at module.rs:1013 pre-registers all the
9226 /// statically-compiled module names (zsh/complete, zsh/datetime,
9227 /// zsh/files etc.) at construction time — an architectural
9228 /// C's `newmoduletable` (Src/module.c) creates an empty hashtable —
9229 /// entries appear only via `register_module` from dlopen-driven
9230 /// loads. The Rust port pre-registers all statically-compiled
9231 /// builtin modules at table construction (no-dlopen model), so
9232 /// `newmoduletable().modules.is_empty()` is FALSE. The matching
9233 /// pin is `newmoduletable_pre_registers_builtin_modules` below.
9234 #[test]
9235 fn newmoduletable_returns_empty_table() {
9236 let _g = crate::test_util::global_state_lock();
9237 let t = newmoduletable();
9238 // Rust-port divergence from C — pre-registration is intentional
9239 // (see register_builtin_modules at module.rs:1033). Pin the
9240 // actual behavior: non-empty after construction.
9241 assert!(
9242 !t.modules.is_empty(),
9243 "Rust port pre-registers builtins (no-dlopen model)"
9244 );
9245 }
9246
9247 /// `newmoduletable()` pre-registers the known statically-compiled
9248 /// builtin modules — pins the divergent Rust behavior.
9249 #[test]
9250 fn newmoduletable_pre_registers_builtin_modules() {
9251 let _g = crate::test_util::global_state_lock();
9252 let t = newmoduletable();
9253 // Rust pre-registers known builtins; pin that some are present.
9254 assert!(
9255 !t.modules.is_empty(),
9256 "Rust port pre-registers builtin modules at construction"
9257 );
9258 assert!(
9259 t.modules.contains_key("zsh/complete"),
9260 "zsh/complete should be pre-registered"
9261 );
9262 }
9263
9264 /// `gethookdef("zshrs_definitely_not_a_hook")` returns null ptr.
9265 /// C `Src/module.c:gethookdef` — walks hooktab, missing → NULL.
9266 #[test]
9267 fn gethookdef_unknown_name_returns_null() {
9268 let _g = crate::test_util::global_state_lock();
9269 let p = gethookdef("zshrs_definitely_not_a_hook_xyz");
9270 assert!(p.is_null(), "missing hook name → NULL pointer");
9271 }
9272
9273 /// `gethookdef("")` on empty name returns null.
9274 #[test]
9275 fn gethookdef_empty_name_returns_null() {
9276 let _g = crate::test_util::global_state_lock();
9277 let p = gethookdef("");
9278 assert!(p.is_null(), "empty name → NULL (no empty-named hooks)");
9279 }
9280
9281 /// `register_module` of a fresh name. C analog uses int return
9282 /// (0=success), Rust uses bool — sig divergence.
9283 #[test]
9284 fn register_module_fresh_name_returns_true() {
9285 let _g = crate::test_util::global_state_lock();
9286 let mut tab = newmoduletable();
9287 let r = register_module(&mut tab, "zshrs_test_fresh_module");
9288 assert!(r, "fresh module name should register successfully");
9289 }
9290
9291 // ═══════════════════════════════════════════════════════════════════
9292 // C-parity tests for Src/module.c modname_ok validator.
9293 // ═══════════════════════════════════════════════════════════════════
9294
9295 /// c:2173 — `modname_ok("zsh/complete")` returns 1 (valid: alphanum
9296 /// + slash separators between identifier components).
9297 #[test]
9298 fn modname_ok_canonical_slash_name() {
9299 let _g = crate::test_util::global_state_lock();
9300 assert_eq!(modname_ok("zsh/complete"), 1);
9301 assert_eq!(modname_ok("zsh/zftp"), 1);
9302 assert_eq!(modname_ok("zsh/zle"), 1);
9303 }
9304
9305 /// c:2173 — `modname_ok("simple")` (no slash) is also valid.
9306 #[test]
9307 fn modname_ok_single_component() {
9308 let _g = crate::test_util::global_state_lock();
9309 assert_eq!(modname_ok("simple"), 1);
9310 assert_eq!(modname_ok("a"), 1);
9311 assert_eq!(modname_ok("module123"), 1);
9312 }
9313
9314 /// c:2173 — `modname_ok("")` returns 1 (empty traverses zero
9315 /// components and succeeds — C's loop exits immediately).
9316 #[test]
9317 fn modname_ok_empty_returns_one() {
9318 let _g = crate::test_util::global_state_lock();
9319 assert_eq!(
9320 modname_ok(""),
9321 1,
9322 "empty name passes loop without finding bad char"
9323 );
9324 }
9325
9326 /// c:2173 — `modname_ok("foo-bar")` returns 0 (hyphen NOT in
9327 /// IIDENT — alphanumeric + underscore only).
9328 #[test]
9329 fn modname_ok_hyphen_returns_zero() {
9330 let _g = crate::test_util::global_state_lock();
9331 assert_eq!(modname_ok("foo-bar"), 0, "hyphen not in IIDENT");
9332 assert_eq!(modname_ok("zsh/foo-bar"), 0);
9333 }
9334
9335 /// c:2173 — underscore IS allowed in identifier component.
9336 #[test]
9337 fn modname_ok_underscore_allowed() {
9338 let _g = crate::test_util::global_state_lock();
9339 assert_eq!(modname_ok("foo_bar"), 1);
9340 assert_eq!(modname_ok("zsh/foo_bar"), 1);
9341 }
9342
9343 /// c:2173 — leading digit allowed in identifier component
9344 /// (C uses IIDENT which doesn't restrict first char to alpha).
9345 #[test]
9346 fn modname_ok_digit_allowed() {
9347 let _g = crate::test_util::global_state_lock();
9348 assert_eq!(modname_ok("123abc"), 1);
9349 assert_eq!(modname_ok("zsh/2023"), 1);
9350 }
9351
9352 /// c:2173 — special chars like `.`, `*`, ` `, `\` all rejected.
9353 #[test]
9354 fn modname_ok_special_chars_rejected() {
9355 let _g = crate::test_util::global_state_lock();
9356 assert_eq!(modname_ok("foo.bar"), 0, "dot rejected");
9357 assert_eq!(modname_ok("foo*"), 0, "glob rejected");
9358 assert_eq!(modname_ok("foo bar"), 0, "space rejected");
9359 assert_eq!(modname_ok("foo\\bar"), 0, "backslash rejected");
9360 assert_eq!(modname_ok("foo@bar"), 0, "@ rejected");
9361 }
9362
9363 /// c:2173 — trailing slash without component is rejected by C
9364 /// (the inner `if (!*p)` check happens BEFORE the slash skip).
9365 /// `foo/` walks `foo`, hits `/`, consumes it, then loops back and
9366 /// hits empty → returns 1. Pin the bare-`foo/` case.
9367 #[test]
9368 fn modname_ok_trailing_slash() {
9369 let _g = crate::test_util::global_state_lock();
9370 // Per C body: while loop consumes identifier, then if (!*p)
9371 // returns 1, else if *p != '/' returns 0, else skip and loop.
9372 // "foo/" → walks foo, *p='/', skip, loop, *p='\0' → 1.
9373 assert_eq!(modname_ok("foo/"), 1);
9374 }
9375
9376 /// c:2173 — leading slash rejected (no identifier component first).
9377 #[test]
9378 fn modname_ok_leading_slash_returns_zero() {
9379 let _g = crate::test_util::global_state_lock();
9380 // C: identifier loop consumes 0 chars, *p='/' but then i=0
9381 // and bytes[0]='/' ≠ alphanum → break → check *p == '/' → skip.
9382 // Walk continues: at byte 1, identifier loop consumes "foo",
9383 // then i=4 and *p='\0' → return 1. Wait, that's 1?
9384 // Actually C: bytes start with '/', identifier loop breaks
9385 // immediately. !*p? No (still '/' at index 0). *p++ != '/'?
9386 // It IS '/', so increment past. Now at byte 1 = 'f'.
9387 // Identifier loop consumes "foo", then loop again at next
9388 // iter, hits '\0' → return 1. Per C, "/foo" returns 1.
9389 assert_eq!(
9390 modname_ok("/foo"),
9391 1,
9392 "C allows leading slash (loop just skips)"
9393 );
9394 }
9395
9396 /// c:2173 — double slash also handled (zero-length component
9397 /// between is consumed by identifier loop = empty match).
9398 #[test]
9399 fn modname_ok_double_slash_allowed() {
9400 let _g = crate::test_util::global_state_lock();
9401 assert_eq!(modname_ok("foo//bar"), 1);
9402 }
9403
9404 // ═══════════════════════════════════════════════════════════════════
9405 // Additional C-parity tests for Src/module.c
9406 // c:1726-1766 dyn_* / c:1703 module_loaded / c:167 newmoduletable
9407 // ═══════════════════════════════════════════════════════════════════
9408
9409 /// c:1703 — `module_loaded` returns 1 only when name is registered.
9410 #[test]
9411 fn module_loaded_returns_one_for_registered_pin() {
9412 let _g = crate::test_util::global_state_lock();
9413 let mut t = newmoduletable();
9414 register_module(&mut t, "zsh_test_loaded_check");
9415 assert_eq!(module_loaded(&t, "zsh_test_loaded_check"), 1);
9416 }
9417
9418 /// c:1703 — `module_loaded` returns 0 for unregistered names.
9419 #[test]
9420 fn module_loaded_returns_zero_for_unregistered_pin() {
9421 let _g = crate::test_util::global_state_lock();
9422 let t = newmoduletable();
9423 assert_eq!(module_loaded(&t, "definitely_not_a_module_xyz"), 0);
9424 }
9425
9426 /// c:1703 — `module_loaded("")` returns 0.
9427 #[test]
9428 fn module_loaded_empty_name_returns_zero() {
9429 let _g = crate::test_util::global_state_lock();
9430 let t = newmoduletable();
9431 assert_eq!(module_loaded(&t, ""), 0);
9432 }
9433
9434 /// c:1726 — `dyn_setup_module(null)` returns 0 (static-link no-op).
9435 #[test]
9436 fn dyn_setup_module_null_returns_zero() {
9437 let _g = crate::test_util::global_state_lock();
9438 assert_eq!(dyn_setup_module(std::ptr::null()), 0);
9439 }
9440
9441 /// c:1747 — `dyn_boot_module(null)` returns 0.
9442 #[test]
9443 fn dyn_boot_module_null_returns_zero() {
9444 let _g = crate::test_util::global_state_lock();
9445 assert_eq!(dyn_boot_module(std::ptr::null()), 0);
9446 }
9447
9448 /// c:1754 — `dyn_cleanup_module(null)` returns 0.
9449 #[test]
9450 fn dyn_cleanup_module_null_returns_zero() {
9451 let _g = crate::test_util::global_state_lock();
9452 assert_eq!(dyn_cleanup_module(std::ptr::null()), 0);
9453 }
9454
9455 /// c:1766 — `dyn_finish_module(null)` returns 0.
9456 #[test]
9457 fn dyn_finish_module_null_returns_zero() {
9458 let _g = crate::test_util::global_state_lock();
9459 assert_eq!(dyn_finish_module(std::ptr::null()), 0);
9460 }
9461
9462 /// c:1733 — `dyn_features_module(null, &mut vec)` returns 0
9463 /// without panicking; features unchanged.
9464 #[test]
9465 fn dyn_features_module_null_returns_zero_no_mutation() {
9466 let _g = crate::test_util::global_state_lock();
9467 let mut features: Vec<String> = vec!["preserved".into()];
9468 assert_eq!(dyn_features_module(std::ptr::null(), &mut features), 0);
9469 assert_eq!(
9470 features,
9471 vec!["preserved".to_string()],
9472 "static-link path must NOT mutate features"
9473 );
9474 }
9475
9476 /// c:1740 — `dyn_enables_module(null, &mut None)` returns 0
9477 /// without mutating enables.
9478 #[test]
9479 fn dyn_enables_module_null_returns_zero_no_mutation() {
9480 let _g = crate::test_util::global_state_lock();
9481 let mut enables: Option<Vec<i32>> = None;
9482 assert_eq!(dyn_enables_module(std::ptr::null(), &mut enables), 0);
9483 assert!(
9484 enables.is_none(),
9485 "static-link path must NOT mutate enables"
9486 );
9487 }
9488
9489 /// c:167 — `newmoduletable` produces a table where `register_module`
9490 /// of fresh name succeeds.
9491 #[test]
9492 fn newmoduletable_accepts_fresh_register() {
9493 let _g = crate::test_util::global_state_lock();
9494 let mut t = newmoduletable();
9495 assert!(register_module(&mut t, "zshrs_fresh_test_module_xyz"));
9496 }
9497
9498 /// c:245 — `register_module` is idempotent on duplicate (returns false
9499 /// per existing pin) and doesn't mutate state.
9500 #[test]
9501 fn register_module_duplicate_does_not_grow_table() {
9502 let _g = crate::test_util::global_state_lock();
9503 let mut t = newmoduletable();
9504 let name = "zshrs_dup_register_test";
9505 assert!(register_module(&mut t, name), "first call succeeds");
9506 let count_after_first = t.modules.len();
9507 let r = register_module(&mut t, name);
9508 assert!(!r, "duplicate must return false");
9509 assert_eq!(
9510 t.modules.len(),
9511 count_after_first,
9512 "table size unchanged on dup"
9513 );
9514 }
9515
9516 // ═══════════════════════════════════════════════════════════════════
9517 // Additional C-parity tests for Src/module.c
9518 // c:339 gethookdef / c:367 addhookdef / c:451 deletehookdef /
9519 // c:740 checkaddparam / c:1730 getmathfunc / c:1825 load_and_bind /
9520 // c:1857 try_load_module / c:1885 do_load_module / c:1925 find_module /
9521 // c:1977 delete_module / c:2002 module_loaded
9522 // ═══════════════════════════════════════════════════════════════════
9523
9524 /// c:339 — `gethookdef` returns *mut hookdef (compile-time type pin).
9525 #[test]
9526 fn gethookdef_returns_raw_ptr_type() {
9527 let _g = crate::test_util::global_state_lock();
9528 let _: *mut hookdef = gethookdef("anything");
9529 }
9530
9531 /// c:339 — `gethookdef("")` empty returns null pointer.
9532 #[test]
9533 fn gethookdef_empty_returns_null() {
9534 let _g = crate::test_util::global_state_lock();
9535 assert!(gethookdef("").is_null(), "empty → null");
9536 }
9537
9538 /// c:740 — `checkaddparam("", 0)` empty returns i32 type.
9539 #[test]
9540 fn checkaddparam_empty_returns_i32_type() {
9541 let _g = crate::test_util::global_state_lock();
9542 let _: i32 = checkaddparam("", 0);
9543 }
9544
9545 /// c:1730 — `getmathfunc` returns Option<String> (compile-time type pin).
9546 #[test]
9547 fn getmathfunc_returns_option_string_type() {
9548 let _g = crate::test_util::global_state_lock();
9549 let mut t = newmoduletable();
9550 let _: Option<String> = getmathfunc(&mut t, "anything", 0);
9551 }
9552
9553 /// c:1730 — `getmathfunc(empty, "")` returns None on empty table.
9554 #[test]
9555 fn getmathfunc_empty_table_unknown_returns_none() {
9556 let _g = crate::test_util::global_state_lock();
9557 let mut t = newmoduletable();
9558 let r = getmathfunc(&mut t, "__never_a_real_math_fn_xyz__", 0);
9559 assert!(r.is_none(), "unknown math fn → None");
9560 }
9561
9562 /// c:1925 — `find_module` returns Option<String> (compile-time type pin).
9563 #[test]
9564 fn find_module_returns_option_string_type() {
9565 let _g = crate::test_util::global_state_lock();
9566 let mut t = newmoduletable();
9567 let _: Option<String> = find_module(&mut t, "anything", 0);
9568 }
9569
9570 /// c:1977 — `delete_module(empty_table, _)` returns i32 (type pin).
9571 #[test]
9572 fn delete_module_returns_i32_type() {
9573 let _g = crate::test_util::global_state_lock();
9574 let mut t = newmoduletable();
9575 let _: i32 = delete_module(&mut t, "__never_loaded__");
9576 }
9577
9578 /// c:1857 — `try_load_module` returns i32 (compile-time type pin).
9579 #[test]
9580 fn try_load_module_returns_i32_type() {
9581 let _g = crate::test_util::global_state_lock();
9582 let t = newmoduletable();
9583 let _: i32 = try_load_module(&t, "__never_real_module__");
9584 }
9585
9586 /// c:1885 — `do_load_module(empty, unknown, 1)` silent failure
9587 /// returns i32 type.
9588 #[test]
9589 fn do_load_module_returns_i32_type() {
9590 let _g = crate::test_util::global_state_lock();
9591 let mut t = newmoduletable();
9592 let _: i32 = do_load_module(&mut t, "__never_real_module_xyz__", 1);
9593 }
9594
9595 /// c:1825 — `load_and_bind("")` empty returns usize (compile-time pin).
9596 #[test]
9597 fn load_and_bind_returns_usize_type() {
9598 let _g = crate::test_util::global_state_lock();
9599 let _: usize = load_and_bind("");
9600 }
9601
9602 /// c:1846 — `hpux_dlsym(0, "")` empty inputs returns usize (type pin).
9603 #[test]
9604 fn hpux_dlsym_returns_usize_type() {
9605 let _g = crate::test_util::global_state_lock();
9606 let _: usize = hpux_dlsym(0, "");
9607 }
9608}