Skip to main content

zsh/ported/modules/
ksh93.rs

1//! `zsh/ksh93` module — port of `Src/Modules/ksh93.c`.
2//!
3//! Implementing "namespace" requires creating a new keyword.               // c:34
4//! Dummy to treat as NULL                                                  // c:105
5//!
6//! Top-level declaration order matches C source line-by-line:
7//!   - `static struct builtin bintab[]`             c:40
8//!   - `edcharsetfn(pm, x)`                          c:46
9//!   - `matchgetfn(pm)`                              c:60
10//!   - 6× `static const struct gsu_*`                c:91-103
11//!   - `static char sh_unsetval[2]`                  c:105
12//!   - `static char *sh_name = sh_unsetval;`         c:106
13//!   - `static char *sh_subscript = sh_unsetval;`    c:107
14//!   - `static char *sh_edchar = sh_unsetval;`       c:108
15//!   - `static char sh_edmode[2]`                    c:109
16//!   - `static struct paramdef partab[]`             c:116
17//!   - `static struct features module_features`     c:133
18//!   - `ksh93_wrapper(prog, w, name)`                c:142
19//!   - `static struct funcwrap wrapper[]`            c:230
20//!   - `setup_(m)` / `features_(m, features)` /
21//!     `enables_(m, enables)` / `boot_(m)` /
22//!     `cleanup_(m)` / `finish_(m)`                  c:235-287
23
24#![allow(non_camel_case_types)]
25#![allow(non_upper_case_globals)]
26#![allow(non_snake_case)]
27
28use crate::ported::builtins::sched::zleactive;
29pub use crate::ported::options::emulation;
30pub use crate::ported::params::locallevel;
31use crate::ported::params::{createparam, paramtab, setiparam, setloopvar, setsparam};
32use crate::ported::signals_h::{queue_signals, unqueue_signals};
33use crate::ported::string::{dupstring, ztrdup};
34use crate::ported::zsh_h::{
35    eprog, features, funcstack, funcwrap, isset, module, param, paramdef, EMULATE_KSH, EMULATION,
36    KSHARRAYS, PARAMDEF, PM_LOCAL, PM_NAMEREF, PM_READONLY, PM_UNSET, VIMODE,
37};
38use crate::ported::ztype_h::INAMESPC;
39use crate::zsh_h::{PM_ARRAY, PM_SCALAR, PM_SPECIAL};
40use std::sync::atomic::{AtomicI64, Ordering};
41use std::sync::{Mutex, OnceLock};
42
43// =====================================================================
44// /* Implementing "namespace" requires creating a new keword.  Hrm. */ c:34
45// /* Standard module configuration/linkage */                          c:36-38
46// static struct builtin bintab[] = {
47//     BUILTIN("nameref", BINF_ASSIGN, ..., 0, -1, 0, "gpru", "n")
48// };                                                                  c:40
49//
50// Static dispatch table consumed by C module loader. Static-link path:
51// the `nameref` builtin is dispatched directly via `bin_typeset` in
52// the typeset port. Table omitted from Rust port pending module-loader.
53// =====================================================================
54
55// =====================================================================
56// edcharsetfn(Param pm, char *x)                                     c:46
57// =====================================================================
58
59/// Port of `edcharsetfn(Param pm, char *x)` from `Src/Modules/ksh93.c:47`.
60#[allow(unused_variables)]
61pub fn edcharsetfn(pm: *mut param, x: *mut libc::c_char) { // c:47
62                                                           /*
63                                                            * To make this work like ksh, we must intercept $KEYS before the widget
64                                                            * is looked up, so that changing the key sequence causes a different
65                                                            * widget to be substituted.  Somewhat similar to "bindkey -s".
66                                                            *
67                                                            * Ksh93 adds SIGKEYBD to the trap list for this purpose.
68                                                            */                                                                 // c:49-55
69                                                           /* ; */                                                             // c:56
70}
71
72// =====================================================================
73// matchgetfn(Param pm)                                               c:60
74// =====================================================================
75
76/// Port of `matchgetfn(Param pm)` from `Src/Modules/ksh93.c:60`.
77///
78/// C signature mirrored verbatim:
79/// ```c
80/// static char **
81/// matchgetfn(Param pm)
82/// ```
83pub fn matchgetfn(pm: *mut param) -> Vec<String> {
84    // c:60
85    // c:Src/Modules/ksh93.c:60 — C's matchgetfn dereferences
86    // `pm->u.arr` directly with no null guard (the function is wired
87    // into a gsu_array vtable and never invoked with a NULL `pm`).
88    // The Rust port is callable from tests that pass `null_mut` to
89    // pin the safety boundary; preserve the convention by short-
90    // circuiting to an empty Vec when `pm` is null, BEFORE looking
91    // at the `match` paramtab entry. Without this, leftover `match`
92    // array values from a prior test (the global paramtab is shared
93    // across tests via the `global_state_lock`) leak into the null-pm
94    // call's return value, breaking the "null pm → empty vec" pin.
95    if pm.is_null() {
96        return Vec::new();
97    }
98    // c:60 — `char **zsh_match = getaparam("match");`
99    let zsh_match: Vec<String> = paramtab()
100        .read()
101        .ok()
102        .and_then(|t| t.get("match").and_then(|p| p.u_arr.clone()))
103        .unwrap_or_default();
104    /*
105     * For this to work accurately, ksh emulation should always imply
106     * that the (#m) and (#b) extendedglob operators are enabled.
107     *
108     * When we have a 0th element (ksharrays), it is $MATCH.  Elements
109     * 1st and larger mirror the $match array.
110     */
111    // c:64-69
112    // c:71-72 — `if (pm->u.arr) freearray(pm->u.arr);`
113    if !pm.is_null() {
114        unsafe {
115            (*pm).u_arr = None;
116        } // c:71-72 freearray
117    }
118    if !zsh_match.is_empty() {
119        // c:73
120        if isset(KSHARRAYS) {
121            // c:74
122            // c:75-80 — char **ap = zalloc(...); pm->u.arr = ap;
123            //           *ap++ = ztrdup(getsparam("MATCH"));
124            //           while (*zsh_match) *ap = ztrdup(*zsh_match++);
125            let match_str: String = paramtab()
126                .read()
127                .ok()
128                .and_then(|t| t.get("MATCH").and_then(|p| p.u_str.clone()))
129                .unwrap_or_default();
130            let mut ap: Vec<String> = Vec::with_capacity(zsh_match.len() + 1);
131            ap.push(ztrdup(&match_str)); // c:78
132            for s in &zsh_match {
133                // c:79-80
134                ap.push(ztrdup(s));
135            }
136            if !pm.is_null() {
137                unsafe {
138                    (*pm).u_arr = Some(ap.clone());
139                }
140            }
141            // c:88 — `return arrgetfn(pm);` — arrgetfn (params.c) reads
142            // pm->u.arr we just set, so return the same vec.
143            ap
144        } else {
145            // c:81-82 — pm->u.arr = zarrdup(zsh_match);
146            let dup: Vec<String> = zsh_match.iter().map(|s| ztrdup(s)).collect();
147            if !pm.is_null() {
148                unsafe {
149                    (*pm).u_arr = Some(dup.clone());
150                }
151            }
152            dup // c:88 arrgetfn(pm)
153        }
154    } else if isset(KSHARRAYS) {
155        // c:83
156        // c:84 — pm->u.arr = mkarray(ztrdup(getsparam("MATCH")));
157        let match_str: String = paramtab()
158            .read()
159            .ok()
160            .and_then(|t| t.get("MATCH").and_then(|p| p.u_str.clone()))
161            .unwrap_or_default();
162        let one = vec![ztrdup(&match_str)];
163        if !pm.is_null() {
164            unsafe {
165                (*pm).u_arr = Some(one.clone());
166            }
167        }
168        one // c:88 arrgetfn(pm)
169    } else {
170        // c:86 — pm->u.arr = NULL;
171        if !pm.is_null() {
172            unsafe {
173                (*pm).u_arr = None;
174            }
175        }
176        Vec::new() // c:88 arrgetfn(pm) → NULL
177    }
178}
179
180// =====================================================================
181// static const struct gsu_scalar constant_gsu                        c:91
182// static const struct gsu_scalar sh_edchar_gsu                       c:94
183// static const struct gsu_scalar sh_edmode_gsu                       c:96
184// static const struct gsu_array sh_match_gsu                         c:98
185// static const struct gsu_scalar sh_name_gsu                         c:100
186// static const struct gsu_scalar sh_subscript_gsu                    c:102
187//
188// GSU vtables for the `.sh.*` parameters. Wired into `partab[]` below.
189// Static-link path: dispatcher invokes the getfn/setfn directly.
190// Tables omitted pending param-table port.
191// =====================================================================
192
193// =====================================================================
194// static char sh_unsetval[2];	/* Dummy to treat as NULL */          c:105
195// static char *sh_name = sh_unsetval;                                c:106
196// static char *sh_subscript = sh_unsetval;                           c:107
197// static char *sh_edchar = sh_unsetval;                              c:108
198// static char sh_edmode[2];                                          c:109
199// =====================================================================
200
201/// Port of `static char sh_unsetval[2];` from `ksh93.c:105`.
202/// /* Dummy to treat as NULL */
203pub static sh_unsetval: [u8; 2] = [0, 0]; // c:105
204
205/// Port of `static char *sh_name = sh_unsetval;` from `ksh93.c:106`.
206pub static sh_name: Mutex<String> = Mutex::new(String::new()); // c:106
207
208/// Port of `static char *sh_subscript = sh_unsetval;` from `ksh93.c:107`.
209pub static sh_subscript: Mutex<String> = Mutex::new(String::new()); // c:107
210
211/// Port of `static char *sh_edchar = sh_unsetval;` from `ksh93.c:108`.
212pub static sh_edchar: Mutex<String> = Mutex::new(String::new()); // c:108
213
214/// Port of `static char sh_edmode[2];` from `ksh93.c:109`.
215pub static sh_edmode: Mutex<[u8; 2]> = Mutex::new([0, 0]); // c:109
216
217/// Port of `ksh93_wrapper(Eprog prog, FuncWrap w, char *name)` from `Src/Modules/ksh93.c:143`.
218///
219/// C signature mirrored verbatim:
220/// ```c
221/// static int
222/// ksh93_wrapper(Eprog prog, FuncWrap w, char *name)
223/// ```
224#[allow(unused_variables)]
225pub fn ksh93_wrapper(prog: *const eprog, w: *const funcwrap, name: *mut libc::c_char) -> i32 {
226    // c:143
227    // c:143 — `Funcstack f;`
228    let mut f: *const funcstack;
229    // c:146 — `Param pm;`
230    let mut pm: *mut param;
231    // c:147 — `zlong num = funcstack->prev ? getiparam(".sh.level") : 0;`
232    // funcstack is the global from Src/exec.c:340; stub holds NULL so
233    // funcstack->prev is always NULL → branch picks 0.
234    let mut num: i64 = if (*funcstack.lock().unwrap()) != 0 {
235        paramtab()
236            .read()
237            .ok()
238            .and_then(|t| {
239                t.get(".sh.level")
240                    .and_then(|p| p.u_str.as_ref().and_then(|s| s.parse::<i64>().ok()))
241            })
242            .unwrap_or(0)
243    } else {
244        0
245    };
246
247    if !EMULATION(EMULATE_KSH) {
248        // c:149
249        return 1; // c:150
250    }
251
252    if num == 0 {
253        // c:152
254        // c:153 — `for (f = funcstack; f; f = f->prev, num++);` — count
255        // function-call-stack depth. Route through the canonical
256        // FUNCSTACK Vec<funcstack> in modules::parameter (each push is
257        // a nested call). `len()` gives the depth directly.
258        let _ = f; // C iterates a linked list; Rust uses Vec depth.
259        if let Ok(stack) = crate::ported::modules::parameter::FUNCSTACK.lock() {
260            num = stack.len() as i64;
261        }
262    } else {
263        // c:154
264        num += 1; // c:155
265    }
266
267    queue_signals(); // c:157
268    locallevel.fetch_add(1, Ordering::SeqCst); // c:158 ++locallevel;
269                                               /* Make these local */                                              // c:158 trailing comment
270
271    // c:160-165 — .sh.command setup
272    pm = createparam(".sh.command", LOCAL_NAMEREF as i32)
273        .map(Box::into_raw)
274        .unwrap_or(std::ptr::null_mut());
275    if !pm.is_null() {
276        unsafe {
277            (*pm).level = locallevel.load(Ordering::Relaxed);
278        } // c:161 pm->level = locallevel;
279          //       /* Why is this necessary? */
280          /* Force scoping by assignent hack */                           // c:162
281        setloopvar(".sh.command", "ZSH_DEBUG_CMD"); // c:163
282        unsafe {
283            (*pm).node.flags |= PM_READONLY as i32;
284        } // c:164
285    }
286    /* .sh.edchar is in partab and below */
287    // c:166
288    if zleactive.load(Ordering::Relaxed) != 0 {
289        pm = createparam(".sh.edcol", LOCAL_NAMEREF as i32) // c:167
290            .map(Box::into_raw)
291            .unwrap_or(std::ptr::null_mut());
292        if !pm.is_null() {
293            unsafe {
294                (*pm).level = locallevel.load(Ordering::Relaxed);
295            } // c:168
296            setloopvar(".sh.edcol", "CURSOR"); // c:169
297            unsafe {
298                (*pm).node.flags |= (PM_NAMEREF | PM_READONLY) as i32;
299            } // c:170
300        }
301    }
302    /* .sh.edmode is in partab and below */
303    // c:172
304    if zleactive.load(Ordering::Relaxed) != 0 {
305        pm = createparam(".sh.edtext", LOCAL_NAMEREF as i32) // c:173
306            .map(Box::into_raw)
307            .unwrap_or(std::ptr::null_mut());
308        if !pm.is_null() {
309            unsafe {
310                (*pm).level = locallevel.load(Ordering::Relaxed);
311            } // c:174
312            setloopvar(".sh.edtext", "BUFFER"); // c:175
313            unsafe {
314                (*pm).node.flags |= PM_READONLY as i32;
315            } // c:176
316        }
317    }
318
319    pm = createparam(
320        ".sh.fun", // c:179
321        (PM_LOCAL | PM_UNSET) as i32,
322    )
323    .map(Box::into_raw)
324    .unwrap_or(std::ptr::null_mut());
325    if !pm.is_null() {
326        unsafe {
327            (*pm).level = locallevel.load(Ordering::Relaxed);
328        } // c:180
329        let name_str: String = if name.is_null() {
330            String::new()
331        } else {
332            unsafe {
333                std::ffi::CStr::from_ptr(name)
334                    .to_string_lossy()
335                    .into_owned()
336            }
337        };
338        setsparam(
339            ".sh.fun", // c:181
340            &ztrdup(&name_str),
341        );
342        unsafe {
343            (*pm).node.flags |= PM_READONLY as i32;
344        } // c:182
345    }
346    pm = createparam(
347        ".sh.level", // c:184
348        (PM_LOCAL | PM_UNSET) as i32,
349    )
350    .map(Box::into_raw)
351    .unwrap_or(std::ptr::null_mut());
352    if !pm.is_null() {
353        unsafe {
354            (*pm).level = locallevel.load(Ordering::Relaxed);
355        } // c:185
356        setiparam(".sh.level", num); // c:186
357    }
358    if zleactive.load(Ordering::Relaxed) != 0 {
359        // c:188
360        // c:189-190 — extern mod_import_variable char *curkeymapname / *varedarg;
361        // (extern declarations are at the locals-block level in C; ours
362        // are file-level statics below.)
363        /* bindkey -v forces VIMODE so this test is as good as any */   // c:191
364        let curkmap = curkeymapname.lock().unwrap().clone();
365        if !curkmap.is_empty() && isset(VIMODE) && curkmap == "main" {
366            // c:192-193
367            // c:194 — strcpy(sh_edmode, "\033");
368            let mut em = sh_edmode.lock().unwrap();
369            em[0] = 0o33;
370            em[1] = 0;
371        } else {
372            // c:196 — strcpy(sh_edmode, "");
373            let mut em = sh_edmode.lock().unwrap();
374            em[0] = 0;
375            em[1] = 0;
376        }
377        // c:197-198 — if (sh_edchar == sh_unsetval) sh_edchar = dupstring(getsparam("KEYS"));
378        let edch_unset = sh_edchar.lock().unwrap().is_empty();
379        if edch_unset {
380            let keys: String = paramtab()
381                .read()
382                .ok()
383                .and_then(|t| t.get("KEYS").and_then(|p| p.u_str.clone()))
384                .unwrap_or_default();
385            *sh_edchar.lock().unwrap() = dupstring(&keys);
386        }
387        let varedarg_val = varedarg.lock().unwrap().clone();
388        if !varedarg_val.is_empty() {
389            // c:199
390            // c:200 — char *ie = itype_end((sh_name = dupstring(varedarg)), INAMESPC, 0);
391            *sh_name.lock().unwrap() = dupstring(&varedarg_val);
392            let nm = sh_name.lock().unwrap().clone();
393            // c:200 — `char *ie = itype_end((sh_name=...), INAMESPC, 0);`
394            // itype_end returns a pointer past the run of chars matching
395            // the type-bits; INAMESPC = identifier-namespace chars.
396            // Rust port: utils::itype_end takes (s, allow_digits_start)
397            // — wrong signature for INAMESPC. Inline the byte-walk to
398            // mirror the C `for (;;) test bit; advance;` loop.
399            let _ = INAMESPC;
400            let ie_off = {
401                let mut k = 0;
402                for &b in nm.as_bytes() {
403                    match b {
404                        b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' => k += 1,
405                        _ => break,
406                    }
407                }
408                k
409            };
410            if ie_off < nm.len() {
411                // c:201 if (ie && *ie)
412                // c:202 — *ie++ = '\0';
413                let head = &nm[..ie_off];
414                let tail = &nm[ie_off + 1..]; // skip the bracket char
415                *sh_name.lock().unwrap() = head.to_string();
416                /* Assume bin_vared has validated subscript */
417                // c:203
418                // c:204 — sh_subscript = dupstring(ie);
419                *sh_subscript.lock().unwrap() = dupstring(tail);
420                // c:205-206 — ie = sh_subscript + strlen(sh_subscript); *--ie = '\0';
421                let mut sub = sh_subscript.lock().unwrap();
422                if sub.ends_with(']') {
423                    sub.pop();
424                }
425            } else {
426                // c:208 — sh_subscript = sh_unsetval;
427                sh_subscript.lock().unwrap().clear();
428            }
429            pm = createparam(
430                ".sh.value", // c:209
431                LOCAL_NAMEREF as i32,
432            )
433            .map(Box::into_raw)
434            .unwrap_or(std::ptr::null_mut());
435            if !pm.is_null() {
436                unsafe {
437                    (*pm).level = locallevel.load(Ordering::Relaxed);
438                } // c:210
439                setloopvar(".sh.value", "BUFFER"); /* Hack */
440                // c:211
441                unsafe {
442                    (*pm).node.flags |= PM_READONLY as i32;
443                } // c:212
444            }
445        } else {
446            // c:215 — sh_name = sh_subscript = sh_unsetval;
447            sh_name.lock().unwrap().clear();
448            sh_subscript.lock().unwrap().clear();
449        }
450    } else {
451        // c:217 — sh_edchar = sh_name = sh_subscript = sh_unsetval;
452        sh_edchar.lock().unwrap().clear();
453        sh_name.lock().unwrap().clear();
454        sh_subscript.lock().unwrap().clear();
455        // c:218 — strcpy(sh_edmode, "");
456        let mut em = sh_edmode.lock().unwrap();
457        em[0] = 0;
458        em[1] = 0;
459        /* TODO:                                                       // c:219-222
460         * - disciplines
461         * - special handling of .sh.value in math
462         */
463    }
464    locallevel.fetch_sub(1, Ordering::SeqCst); // c:224 --locallevel;
465    unqueue_signals(); // c:225
466
467    1 // c:227
468}
469
470// =====================================================================
471/*
472 * Some parameters listed here do not appear in ksh93.mdd autofeatures
473 * because they are only instantiated by ksh93_wrapper() below.  This
474 * obviously includes those commented out here.
475 */                                                                    // c:111-115
476// static struct paramdef partab[]                                    c:116
477// static struct features module_features                             c:133
478//
479// Param/feature dispatch tables. Omitted pending module-loader port.
480// =====================================================================
481
482// =====================================================================
483// ksh93_wrapper(Eprog prog, FuncWrap w, char *name)                  c:142
484// =====================================================================
485
486/// `LOCAL_NAMEREF` — `#define LOCAL_NAMEREF (PM_LOCAL|PM_UNSET|PM_NAMEREF)`
487/// from `Src/Modules/ksh93.c:143`.
488#[allow(dead_code)]
489const LOCAL_NAMEREF: u32 = PM_LOCAL | PM_UNSET | PM_NAMEREF; // c:143
490
491// =====================================================================
492// static struct funcwrap wrapper[]                                   c:230
493//
494// Per-function wrapper table consumed by `addwrapper(m, wrapper)` at
495// boot_. Omitted pending module-loader port.
496// =====================================================================
497
498// =====================================================================
499// setup_(UNUSED(Module m))                                           c:235
500// =====================================================================
501
502/// Port of `setup_(UNUSED(Module m))` from `Src/Modules/ksh93.c:236`.
503#[allow(unused_variables)]
504pub fn setup_(m: *const module) -> i32 {
505    // c:236
506    // C body c:238-239 — `return 0`. Faithful empty-body port.
507    0
508}
509
510/// Port of `features_(UNUSED(Module m), UNUSED(char ***features))` from `Src/Modules/ksh93.c:243`.
511/// C body c:245-247 — `*features = featuresarray(m, &module_features); return 0`.
512pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
513    // c:243
514    *features = featuresarray(m, module_features());
515    0 // c:258
516}
517
518/// Port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from `Src/Modules/ksh93.c:251`.
519/// C body c:253-254 — `return handlefeatures(m, &module_features, enables)`.
520pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
521    // c:251
522    handlefeatures(m, module_features(), enables) // c:258
523}
524
525/// Port of `boot_(UNUSED(Module m))` from `Src/Modules/ksh93.c:258`.
526/// C body: `return addwrapper(m, wrapper);`
527pub fn boot_(m: *const module) -> i32 {
528    // c:258 — addwrapper(m, wrapper); zshrs's fusevm doesn't run
529    // through C's wrapper-dispatch chain, no-op until wrapper
530    // machinery gets a Rust equivalent.
531    let _ = m;
532    0
533}
534
535/// Port of `cleanup_(UNUSED(Module m))` from `Src/Modules/ksh93.c:265`.
536/// C body (c:267-278):
537/// ```c
538/// struct paramdef *p;
539/// deletewrapper(m, wrapper);
540/// for (p = partab; p < partab + sizeof(partab)/sizeof(*partab); ++p) {
541///     if (p->flags & PM_NAMEREF) {
542///         HashNode hn = gethashnode2(paramtab, p->name);
543///         if (hn)
544///             ((Param)hn)->node.flags &= ~PM_NAMEREF;
545///     }
546/// }
547/// return setfeatureenables(m, &module_features, NULL);
548/// ```
549pub fn cleanup_(m: *const module) -> i32 {
550    // c:267 — `struct paramdef *p;`
551    let mut p: usize; // c:267 (index over partab)
552                      // c:269 — deletewrapper(m, wrapper); zshrs's fusevm wrapper
553                      // machinery is a no-op (see boot_ note).
554
555    // c:116-131 — `static struct paramdef partab[]` inlined here
556    // because Rust statics can't hold String-typed paramdef.name.
557    let partab: [paramdef; 9] = [
558        PARAMDEF(".sh.edchar", (PM_SCALAR | PM_SPECIAL) as i32, 0, 0), // c:117
559        PARAMDEF(
560            ".sh.edmode",
561            (PM_SCALAR | PM_READONLY | PM_SPECIAL) as i32,
562            0,
563            0,
564        ), // c:119
565        PARAMDEF(".sh.file", (PM_NAMEREF | PM_READONLY) as i32, 0, 0), // c:121
566        PARAMDEF(".sh.lineno", (PM_NAMEREF | PM_READONLY) as i32, 0, 0), // c:122
567        PARAMDEF(".sh.match", (PM_ARRAY | PM_READONLY) as i32, 0, 0),  // c:123
568        PARAMDEF(
569            ".sh.name",
570            (PM_SCALAR | PM_READONLY | PM_SPECIAL) as i32,
571            0,
572            0,
573        ), // c:124
574        PARAMDEF(
575            ".sh.subscript",
576            (PM_SCALAR | PM_READONLY | PM_SPECIAL) as i32,
577            0,
578            0,
579        ), // c:126
580        PARAMDEF(".sh.subshell", (PM_NAMEREF | PM_READONLY) as i32, 0, 0), // c:128
581        PARAMDEF(".sh.version", (PM_NAMEREF | PM_READONLY) as i32, 0, 0), // c:130
582    ];
583
584    /* Clean up namerefs, otherwise deleteparamdef() is confused */
585    // c:271
586    // c:272-277 — `for (p = partab; p < partab + ARRSZ; ++p) { ... }`
587    p = 0;
588    while p < partab.len() {
589        // c:272
590        let entry = &partab[p];
591        if (entry.flags as u32 & PM_NAMEREF) != 0 {
592            // c:273
593            // c:274-276 — `HashNode hn = gethashnode2(paramtab, p->name);`
594            // `if (hn) hn->flags &= ~PM_NAMEREF;`
595            if let Ok(mut t) = paramtab().write() {
596                if let Some(pm) = t.get_mut(&entry.name) {
597                    pm.node.flags &= !(PM_NAMEREF as i32); // c:276
598                }
599            }
600        }
601        p += 1;
602    }
603    setfeatureenables(m, module_features(), None) // c:279
604}
605
606/// Port of `finish_(UNUSED(Module m))` from `Src/Modules/ksh93.c:284`.
607#[allow(unused_variables)]
608pub fn finish_(m: *const module) -> i32 {
609    // c:284
610    // C body c:286-287 — `return 0`. Faithful empty-body port; the
611    //                    ksh93 wrapper unregisters in cleanup_ via
612    //                    deletewrapper.
613    0
614}
615
616static MODULE_FEATURES: OnceLock<Mutex<features>> = OnceLock::new();
617
618// Local descriptor stub mirroring the C bintab + partab.
619// WARNING: NOT IN KSH93.C — Rust-only module-framework shim.
620// C uses generic featuresarray/handlefeatures/setfeatureenables from
621// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
622// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
623fn featuresarray(_m: *const module, _f: &Mutex<features>) -> Vec<String> {
624    vec![
625        "b:nameref".to_string(),
626        "p:.sh.edchar".to_string(),
627        "p:.sh.edmode".to_string(),
628        "p:.sh.file".to_string(),
629        "p:.sh.lineno".to_string(),
630        "p:.sh.match".to_string(),
631        "p:.sh.name".to_string(),
632        "p:.sh.subscript".to_string(),
633        "p:.sh.subshell".to_string(),
634        "p:.sh.version".to_string(),
635    ]
636}
637
638// WARNING: NOT IN KSH93.C — Rust-only module-framework shim.
639// C uses generic featuresarray/handlefeatures/setfeatureenables from
640// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
641// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
642fn handlefeatures(_m: *const module, _f: &Mutex<features>, enables: &mut Option<Vec<i32>>) -> i32 {
643    if enables.is_none() {
644        *enables = Some(vec![1; 10]);
645    }
646    0
647}
648
649// WARNING: NOT IN KSH93.C — Rust-only module-framework shim.
650// C uses generic featuresarray/handlefeatures/setfeatureenables from
651// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
652// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
653fn setfeatureenables(_m: *const module, _f: &Mutex<features>, _e: Option<&[i32]>) -> i32 {
654    0
655}
656
657// =====================================================================
658// External ported / globals from other Src/*.c files. Stubbed locally
659// pending the proper ports of their home files.
660// =====================================================================
661
662// `emulation` lives in `crate::ported::options::emulation` per Rule C
663// (its C definition is `Src/options.c:36`, not `ksh93.c`). The
664// `EMULATION(bits)` macro at zsh.h:2347 tests bits against it.
665
666// `locallevel` lives in `crate::ported::params::locallevel` per Rule C
667// (its C definition is `Src/params.c:54`, not `ksh93.c`). Bumped by
668// `startparamscope` on function entry, decremented by `endparamscope`
669// on return. `ksh93_wrapper` increments before `createparam()` and
670// decrements after.
671
672/// `curkeymapname` — `char *` global from `Src/Zle/zle_keymap.c`,
673/// declared `extern` at c:189. Holds the active keymap name.
674pub static curkeymapname: Mutex<String> = Mutex::new(String::new());
675
676/// `varedarg` — `char *` global from `Src/Zle/zle_misc.c`, declared
677/// `extern` at c:190. Holds the parameter name being edited by `vared`.
678pub static varedarg: Mutex<String> = Mutex::new(String::new());
679
680// `funcstack` — `Funcstack` global from `Src/exec.c:340`. Stubbed as
681// a Mutex-wrapped raw-pointer holder since pointers aren't `Sync`
682// without explicit handling. NULL by default — exec.c port wires the
683// real walk.
684static funcstack: Mutex<usize> = Mutex::new(0);
685
686// `param.u.arr` field — the C `union u` has `char **arr` at c:1835.
687// The Rust `param` struct exposes it as `pub u_arr: Option<Vec<String>>`
688// (zsh_h.rs:732), already accessible via `(*pm).u_arr`.
689
690// Suppress "unused" for the AtomicI64 import; we don't use it directly
691// (locallevel is AtomicI32 to match C `int` for that field).
692#[allow(dead_code)]
693const _: AtomicI64 = AtomicI64::new(0);
694
695// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
696// ─── RUST-ONLY ACCESSORS ───
697//
698// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
699// RwLock<T>>` globals declared above. C zsh uses direct global
700// access; Rust needs these wrappers because `OnceLock::get_or_init`
701// is the only way to lazily construct shared state. These ported sit
702// here so the body of this file reads in C source order without
703// the accessor wrappers interleaved between real port ported.
704// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
705
706// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
707// ─── RUST-ONLY ACCESSORS ───
708//
709// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
710// RwLock<T>>` globals declared above. C zsh uses direct global
711// access; Rust needs these wrappers because `OnceLock::get_or_init`
712// is the only way to lazily construct shared state. These ported sit
713// here so the body of this file reads in C source order without
714// the accessor wrappers interleaved between real port ported.
715// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
716
717/// Port of `ksh93_wrapper(Eprog prog, FuncWrap w, char *name)` from `Src/Modules/ksh93.c:143`.
718fn module_features() -> &'static Mutex<features> {
719    MODULE_FEATURES.get_or_init(|| {
720        Mutex::new(features {
721            bn_list: None,
722            bn_size: 1, // bintab: nameref (ksh93.c)
723            cd_list: None,
724            cd_size: 0,
725            mf_list: None,
726            mf_size: 0,
727            pd_list: None,
728            pd_size: 9, // partab: .sh.edchar/edmode/file/lineno/match/name/subscript/subshell/version
729            n_abstract: 0,
730        })
731    })
732}
733
734#[cfg(test)]
735mod tests {
736    use super::*;
737    use crate::zsh_h::hashnode;
738
739    /// Verifies `ksh93_wrapper` returns 1 in the !EMULATE_KSH branch
740    /// (c:149-150) when `emulation` global is 0 (default).
741    #[test]
742    fn ksh93_wrapper_returns_one_when_not_emulate_ksh() {
743        let _g = crate::test_util::global_state_lock();
744        emulation.store(0, Ordering::SeqCst);
745        let rc = ksh93_wrapper(std::ptr::null(), std::ptr::null(), std::ptr::null_mut());
746        assert_eq!(rc, 1);
747    }
748
749    /// Verifies `ksh93_wrapper` runs the full body (and still returns 1
750    /// per c:227) when EMULATE_KSH is set on the `emulation` global.
751    /// Body relies on stubbed externals so it can't validate the
752    /// param-creation side-effects yet, but it MUST not panic and MUST
753    /// terminate.
754    #[test]
755    fn ksh93_wrapper_runs_full_body_under_emulate_ksh() {
756        let _g = crate::test_util::global_state_lock();
757        let saved = emulation.load(Ordering::SeqCst);
758        emulation.store(EMULATE_KSH, Ordering::SeqCst);
759        let rc = ksh93_wrapper(std::ptr::null(), std::ptr::null(), std::ptr::null_mut());
760        assert_eq!(rc, 1);
761        // c:158 ++locallevel + c:224 --locallevel must net to 0.
762        assert_eq!(locallevel.load(Ordering::SeqCst), 0);
763        emulation.store(saved, Ordering::SeqCst);
764    }
765
766    /// Verifies `matchgetfn` returns empty Vec when `match` array is
767    /// unset and KSHARRAYS is off (c:86 NULL branch).
768    #[test]
769    fn matchgetfn_empty_returns_empty() {
770        let _g = crate::test_util::global_state_lock();
771        let v = matchgetfn(std::ptr::null_mut());
772        assert!(v.is_empty());
773    }
774
775    /// Verifies `edcharsetfn` is a no-op (c:56 `;`).
776    #[test]
777    fn edcharsetfn_noop() {
778        let _g = crate::test_util::global_state_lock();
779        edcharsetfn(std::ptr::null_mut(), std::ptr::null_mut());
780    }
781
782    /// Verifies all module loaders return 0.
783    #[test]
784    fn module_loaders_return_zero() {
785        let _g = crate::test_util::global_state_lock();
786        let m: *const module = std::ptr::null();
787        assert_eq!(setup_(m), 0);
788        let mut features = Vec::new();
789        assert_eq!(features_(m, &mut features), 0);
790        let mut enables: Option<Vec<i32>> = None;
791        assert_eq!(enables_(m, &mut enables), 0);
792        assert_eq!(boot_(m), 0);
793        assert_eq!(cleanup_(m), 0);
794        assert_eq!(finish_(m), 0);
795    }
796
797    /// Port of `ksh93_wrapper(Eprog prog, FuncWrap w, char *name)` from `Src/Modules/ksh93.c:143`.
798    /// Verifies the C-faithful static globals are initialized empty
799    /// (sh_unsetval-equivalent) at module-load.
800    #[test]
801    fn statics_default_to_unsetval() {
802        let _g = crate::test_util::global_state_lock();
803        sh_name.lock().unwrap().clear();
804        sh_subscript.lock().unwrap().clear();
805        sh_edchar.lock().unwrap().clear();
806        *sh_edmode.lock().unwrap() = [0, 0];
807        assert!(sh_name.lock().unwrap().is_empty());
808        assert!(sh_subscript.lock().unwrap().is_empty());
809        assert!(sh_edchar.lock().unwrap().is_empty());
810        assert_eq!(*sh_edmode.lock().unwrap(), [0u8, 0u8]);
811        assert_eq!(sh_unsetval, [0u8, 0u8]);
812    }
813
814    /// Port of `ksh93_wrapper(Eprog prog, FuncWrap w, char *name)` from `Src/Modules/ksh93.c:143`.
815    /// Verifies `LOCAL_NAMEREF` matches the C `#define` at c:158.
816    #[test]
817    fn local_nameref_matches_c_define() {
818        let _g = crate::test_util::global_state_lock();
819        assert_eq!(LOCAL_NAMEREF, PM_LOCAL | PM_UNSET | PM_NAMEREF);
820    }
821
822    /// c:60 — `matchgetfn` on a non-null `Param` that has no array
823    /// data must still return an empty Vec. Builds a valid `param`
824    /// with `u_arr = None` to exercise the "has Param but no array"
825    /// branch — a regression that adds `.unwrap()` would SIGSEGV.
826    #[test]
827    fn matchgetfn_with_param_no_array_returns_empty() {
828        let _g = crate::test_util::global_state_lock();
829        // matchgetfn reads `match` from paramtab (ksh93.rs:60 — c:60).
830        // A prior subst-related test may have written to it; clear so
831        // the "no array data" branch isn't masked by leftover values.
832        crate::ported::params::unsetparam("match");
833        let mut pm = param {
834            node: hashnode {
835                next: None,
836                nam: "match".to_string(),
837                flags: 0,
838            },
839            u_data: 0,
840            u_tied: None,
841            u_arr: None,
842            u_str: None,
843            u_val: 0,
844            u_dval: 0.0,
845            u_hash: None,
846            gsu_s: None,
847            gsu_i: None,
848            gsu_f: None,
849            gsu_a: None,
850            gsu_h: None,
851            base: 0,
852            width: 0,
853            env: None,
854            ename: None,
855            old: None,
856            level: 0,
857        };
858        let v = matchgetfn(&mut pm as *mut _);
859        assert!(
860            v.is_empty(),
861            "param without array data must produce empty result"
862        );
863    }
864
865    /// c:158 — `LOCAL_NAMEREF` must include the three component flags
866    /// AND no others. Pin the exact bit set so a regen that adds
867    /// PM_READONLY silently changes the unset semantics.
868    #[test]
869    fn local_nameref_has_exactly_three_component_flags() {
870        let _g = crate::test_util::global_state_lock();
871        let expected = PM_LOCAL | PM_UNSET | PM_NAMEREF;
872        assert_eq!(LOCAL_NAMEREF, expected);
873        // Subtracting each in turn yields the other two
874        assert_eq!(LOCAL_NAMEREF & !PM_LOCAL, PM_UNSET | PM_NAMEREF);
875        assert_eq!(LOCAL_NAMEREF & !PM_UNSET, PM_LOCAL | PM_NAMEREF);
876        assert_eq!(LOCAL_NAMEREF & !PM_NAMEREF, PM_LOCAL | PM_UNSET);
877    }
878
879    /// c:50 — `sh_unsetval` is the byte sentinel for "unset" string
880    /// params (`{ 0, 0 }`). Pin the exact value so a regen flipping
881    /// it to `[b'\0']` or `[1, 0]` silently breaks unset detection
882    /// at every shXgetfn call site.
883    #[test]
884    fn sh_unsetval_is_two_zero_bytes() {
885        let _g = crate::test_util::global_state_lock();
886        assert_eq!(
887            sh_unsetval,
888            [0u8, 0u8],
889            "sh_unsetval must be the C-canonical [0,0] sentinel"
890        );
891    }
892
893    /// c:60 — `matchgetfn` with a null pointer must NOT dereference.
894    /// Pinning null-safety so a regen that adds `*pm` outside a
895    /// guard SIGSEGVs the test instead of shipping.
896    #[test]
897    fn matchgetfn_null_pointer_is_safe() {
898        let _g = crate::test_util::global_state_lock();
899        let _ = matchgetfn(std::ptr::null_mut());
900    }
901
902    /// c:47 — `edcharsetfn` accepts null param + null arg pointer
903    /// without dereferencing. The C body is `;` (empty) so the Rust
904    /// stub must mirror that no-op + re-entry safety.
905    #[test]
906    fn edcharsetfn_double_null_is_safe() {
907        let _g = crate::test_util::global_state_lock();
908        edcharsetfn(std::ptr::null_mut(), std::ptr::null_mut());
909        edcharsetfn(std::ptr::null_mut(), std::ptr::null_mut());
910    }
911
912    // ─── zsh-corpus pins for ksh93 ─────────────────────────────────
913
914    /// `matchgetfn(null)` returns empty vec without panic.
915    #[test]
916    fn ksh93_corpus_matchgetfn_null_returns_empty() {
917        let _g = crate::test_util::global_state_lock();
918        let r = matchgetfn(std::ptr::null_mut());
919        assert!(r.is_empty(), "null pm → empty vec, got {r:?}");
920    }
921
922    /// All four lifecycle shims return 0.
923    #[test]
924    fn ksh93_corpus_lifecycle_returns_zero() {
925        let _g = crate::test_util::global_state_lock();
926        let m: *const module = std::ptr::null();
927        assert_eq!(setup_(m), 0);
928        assert_eq!(boot_(m), 0);
929        assert_eq!(cleanup_(m), 0);
930        assert_eq!(finish_(m), 0);
931    }
932
933    /// `edcharsetfn` cycling null calls remains stable.
934    #[test]
935    fn ksh93_corpus_edcharsetfn_repeated_null_no_panic() {
936        let _g = crate::test_util::global_state_lock();
937        for _ in 0..100 {
938            edcharsetfn(std::ptr::null_mut(), std::ptr::null_mut());
939        }
940    }
941
942    // ═══════════════════════════════════════════════════════════════════
943    // Additional C-parity tests for Src/Modules/ksh93.c.
944    // ═══════════════════════════════════════════════════════════════════
945
946    /// c:47 — `edcharsetfn(null, null)` is no-op (placeholder for the
947    /// SIGKEYBD trap interception; the body is just a comment in C).
948    /// Pin: single null call doesn't panic.
949    #[test]
950    fn edcharsetfn_single_null_call_no_panic() {
951        let _g = crate::test_util::global_state_lock();
952        edcharsetfn(std::ptr::null_mut(), std::ptr::null_mut());
953    }
954
955    /// c:60 — `matchgetfn(NULL)` returns empty Vec when `match` array
956    /// is unset (no $match parameter present).
957    #[test]
958    fn matchgetfn_null_pm_unset_match_returns_empty() {
959        let _g = crate::test_util::global_state_lock();
960        // Without setting the "match" param, getaparam returns None →
961        // empty result.
962        crate::ported::params::unsetparam("match");
963        let r = matchgetfn(std::ptr::null_mut());
964        assert!(r.is_empty(), "no $match → empty vec");
965    }
966
967    /// c:212 — `ksh93_wrapper(null, null, null)` no panic on all-null.
968    #[test]
969    fn ksh93_wrapper_all_null_no_panic() {
970        let _g = crate::test_util::global_state_lock();
971        let _ = ksh93_wrapper(std::ptr::null(), std::ptr::null(), std::ptr::null_mut());
972    }
973
974    /// c:491 — `setup_(NULL)` returns 0 (split per-hook).
975    #[test]
976    fn ksh93_setup_returns_zero_pin() {
977        let _g = crate::test_util::global_state_lock();
978        assert_eq!(setup_(std::ptr::null()), 0);
979    }
980
981    /// c:514 — `boot_(NULL)` returns 0.
982    #[test]
983    fn ksh93_boot_returns_zero_pin() {
984        let _g = crate::test_util::global_state_lock();
985        assert_eq!(boot_(std::ptr::null()), 0);
986    }
987
988    /// c:536 — `cleanup_(NULL)` returns 0.
989    #[test]
990    fn ksh93_cleanup_returns_zero_pin() {
991        let _g = crate::test_util::global_state_lock();
992        assert_eq!(cleanup_(std::ptr::null()), 0);
993    }
994
995    /// c:595 — `finish_(NULL)` returns 0.
996    #[test]
997    fn ksh93_finish_returns_zero_pin() {
998        let _g = crate::test_util::global_state_lock();
999        assert_eq!(finish_(std::ptr::null()), 0);
1000    }
1001
1002    /// c:499 — `features_(NULL, _)` returns 0 with features populated.
1003    #[test]
1004    fn ksh93_features_returns_zero_pin() {
1005        let _g = crate::test_util::global_state_lock();
1006        let mut f = Vec::new();
1007        assert_eq!(features_(std::ptr::null(), &mut f), 0);
1008    }
1009
1010    /// c:507 — `enables_(NULL, _)` no panic.
1011    #[test]
1012    fn ksh93_enables_no_panic() {
1013        let _g = crate::test_util::global_state_lock();
1014        let mut e: Option<Vec<i32>> = None;
1015        let _ = enables_(std::ptr::null(), &mut e);
1016    }
1017
1018    // ═══════════════════════════════════════════════════════════════════
1019    // Additional C-parity tests for Src/Modules/ksh93.c
1020    // c:47 edcharsetfn / c:83 matchgetfn / c:212 ksh93_wrapper / lifecycle
1021    // ═══════════════════════════════════════════════════════════════════
1022
1023    /// c:47 — `edcharsetfn(null, null)` is idempotent (multiple calls safe).
1024    #[test]
1025    fn edcharsetfn_double_null_idempotent() {
1026        let _g = crate::test_util::global_state_lock();
1027        for _ in 0..10 {
1028            edcharsetfn(std::ptr::null_mut(), std::ptr::null_mut());
1029        }
1030    }
1031
1032    /// c:83 — `matchgetfn(null)` returns Vec (not panic) — type pinning.
1033    #[test]
1034    fn matchgetfn_null_returns_vec_type() {
1035        let _g = crate::test_util::global_state_lock();
1036        let _: Vec<String> = matchgetfn(std::ptr::null_mut());
1037    }
1038
1039    /// c:83 — `matchgetfn(null)` is deterministic across repeated calls.
1040    #[test]
1041    fn matchgetfn_null_is_deterministic() {
1042        let _g = crate::test_util::global_state_lock();
1043        let first = matchgetfn(std::ptr::null_mut());
1044        for _ in 0..5 {
1045            assert_eq!(
1046                matchgetfn(std::ptr::null_mut()),
1047                first,
1048                "matchgetfn(null) must be deterministic"
1049            );
1050        }
1051    }
1052
1053    /// c:212 — `ksh93_wrapper(null, null, null)` is safe.
1054    #[test]
1055    fn ksh93_wrapper_all_null_no_panic_pin() {
1056        let _g = crate::test_util::global_state_lock();
1057        let _ = ksh93_wrapper(std::ptr::null(), std::ptr::null(), std::ptr::null_mut());
1058    }
1059
1060    /// c:212 — `ksh93_wrapper` returns i32 (type pinning).
1061    #[test]
1062    fn ksh93_wrapper_returns_i32_type() {
1063        let _g = crate::test_util::global_state_lock();
1064        let _: i32 = ksh93_wrapper(std::ptr::null(), std::ptr::null(), std::ptr::null_mut());
1065    }
1066
1067    /// c:491-595 — full lifecycle setup→features→enables→boot→cleanup→finish
1068    /// all return 0.
1069    #[test]
1070    fn ksh93_full_lifecycle_returns_zero_for_all() {
1071        let _g = crate::test_util::global_state_lock();
1072        let null = std::ptr::null();
1073        assert_eq!(setup_(null), 0);
1074        let mut feats = Vec::new();
1075        let _ = features_(null, &mut feats);
1076        let mut enables: Option<Vec<i32>> = None;
1077        let _ = enables_(null, &mut enables);
1078        assert_eq!(boot_(null), 0);
1079        assert_eq!(cleanup_(null), 0);
1080        assert_eq!(finish_(null), 0);
1081    }
1082
1083    /// c:491 — setup_ idempotent.
1084    #[test]
1085    fn ksh93_setup_idempotent() {
1086        let _g = crate::test_util::global_state_lock();
1087        for _ in 0..10 {
1088            assert_eq!(setup_(std::ptr::null()), 0);
1089        }
1090    }
1091
1092    /// c:595 — finish_ idempotent.
1093    #[test]
1094    fn ksh93_finish_idempotent() {
1095        let _g = crate::test_util::global_state_lock();
1096        for _ in 0..10 {
1097            assert_eq!(finish_(std::ptr::null()), 0);
1098        }
1099    }
1100
1101    /// c:536 — cleanup_ idempotent.
1102    #[test]
1103    fn ksh93_cleanup_idempotent() {
1104        let _g = crate::test_util::global_state_lock();
1105        for _ in 0..10 {
1106            assert_eq!(cleanup_(std::ptr::null()), 0);
1107        }
1108    }
1109
1110    /// c:514 — boot_ idempotent.
1111    #[test]
1112    fn ksh93_boot_idempotent() {
1113        let _g = crate::test_util::global_state_lock();
1114        for _ in 0..10 {
1115            assert_eq!(boot_(std::ptr::null()), 0);
1116        }
1117    }
1118
1119    /// c:212 — ksh93_wrapper return value in canonical exit-code range
1120    /// (signed-byte 0..256).
1121    #[test]
1122    fn ksh93_wrapper_return_in_exit_code_range() {
1123        let _g = crate::test_util::global_state_lock();
1124        let r = ksh93_wrapper(std::ptr::null(), std::ptr::null(), std::ptr::null_mut());
1125        assert!(
1126            (0..256).contains(&r),
1127            "exit code must fit in u8 range, got {}",
1128            r
1129        );
1130    }
1131
1132    // ═══════════════════════════════════════════════════════════════════
1133    // Additional C-parity tests for Src/Modules/ksh93.c
1134    // c:47 edcharsetfn / c:83 matchgetfn / c:212 ksh93_wrapper
1135    // ═══════════════════════════════════════════════════════════════════
1136
1137    /// c:83 — `matchgetfn` returns Vec<String> (compile-time type pin).
1138    #[test]
1139    fn matchgetfn_returns_vec_string_type() {
1140        let _g = crate::test_util::global_state_lock();
1141        let _: Vec<String> = matchgetfn(std::ptr::null_mut());
1142    }
1143
1144    /// c:47 — `edcharsetfn` returns void (compile-time pin).
1145    #[test]
1146    fn edcharsetfn_returns_void() {
1147        let _g = crate::test_util::global_state_lock();
1148        let _: () = edcharsetfn(std::ptr::null_mut(), std::ptr::null_mut());
1149    }
1150
1151    /// c:212 — `ksh93_wrapper` returns i32 (compile-time pin).
1152    #[test]
1153    fn ksh93_wrapper_returns_i32_type_pin() {
1154        let _g = crate::test_util::global_state_lock();
1155        let _: i32 = ksh93_wrapper(std::ptr::null(), std::ptr::null(), std::ptr::null_mut());
1156    }
1157
1158    /// c:212 — `ksh93_wrapper` is deterministic for all-null args.
1159    #[test]
1160    fn ksh93_wrapper_all_null_is_deterministic() {
1161        let _g = crate::test_util::global_state_lock();
1162        let first = ksh93_wrapper(std::ptr::null(), std::ptr::null(), std::ptr::null_mut());
1163        for _ in 0..5 {
1164            assert_eq!(
1165                ksh93_wrapper(std::ptr::null(), std::ptr::null(), std::ptr::null_mut()),
1166                first,
1167                "ksh93_wrapper(all null) must be deterministic"
1168            );
1169        }
1170    }
1171
1172    /// c:83 — `matchgetfn(null)` returns empty Vec (no match available).
1173    #[test]
1174    fn matchgetfn_null_returns_empty_pin() {
1175        let _g = crate::test_util::global_state_lock();
1176        let v = matchgetfn(std::ptr::null_mut());
1177        assert!(v.is_empty(), "null pm → empty vec");
1178    }
1179
1180    /// c:491 — `setup_` is idempotent + returns void/i32.
1181    #[test]
1182    fn ksh93_setup_returns_i32_type() {
1183        let _g = crate::test_util::global_state_lock();
1184        let _: i32 = setup_(std::ptr::null());
1185    }
1186
1187    /// c:514 — `boot_` returns i32.
1188    #[test]
1189    fn ksh93_boot_returns_i32_type() {
1190        let _g = crate::test_util::global_state_lock();
1191        let _: i32 = boot_(std::ptr::null());
1192    }
1193
1194    /// c:536 — `cleanup_` returns i32.
1195    #[test]
1196    fn ksh93_cleanup_returns_i32_type() {
1197        let _g = crate::test_util::global_state_lock();
1198        let _: i32 = cleanup_(std::ptr::null());
1199    }
1200
1201    /// c:595 — `finish_` returns i32.
1202    #[test]
1203    fn ksh93_finish_returns_i32_type() {
1204        let _g = crate::test_util::global_state_lock();
1205        let _: i32 = finish_(std::ptr::null());
1206    }
1207
1208    /// c:47 — `edcharsetfn` idempotent for repeated null calls.
1209    #[test]
1210    fn edcharsetfn_repeated_null_calls_idempotent() {
1211        let _g = crate::test_util::global_state_lock();
1212        for _ in 0..20 {
1213            edcharsetfn(std::ptr::null_mut(), std::ptr::null_mut());
1214        }
1215    }
1216
1217    // ═══════════════════════════════════════════════════════════════════
1218    // Additional C-parity tests for Src/Modules/ksh93.c
1219    // c:47 edcharsetfn / c:83 matchgetfn / c:212 ksh93_wrapper + lifecycle
1220    // ═══════════════════════════════════════════════════════════════════
1221
1222    /// c:83 — `matchgetfn(null)` is deterministic across calls (alt).
1223    #[test]
1224    fn matchgetfn_null_deterministic_alt() {
1225        let _g = crate::test_util::global_state_lock();
1226        let first = matchgetfn(std::ptr::null_mut());
1227        for _ in 0..5 {
1228            assert_eq!(
1229                matchgetfn(std::ptr::null_mut()),
1230                first,
1231                "matchgetfn(null) must be pure"
1232            );
1233        }
1234    }
1235
1236    /// c:83 — `matchgetfn(null)` length is exactly 0 (empty Vec).
1237    #[test]
1238    fn matchgetfn_null_length_is_zero() {
1239        let _g = crate::test_util::global_state_lock();
1240        let v = matchgetfn(std::ptr::null_mut());
1241        assert_eq!(v.len(), 0, "matchgetfn(null) must have len=0");
1242    }
1243
1244    /// c:212 — `ksh93_wrapper` exit code is non-negative.
1245    #[test]
1246    fn ksh93_wrapper_exit_code_non_negative() {
1247        let _g = crate::test_util::global_state_lock();
1248        let r = ksh93_wrapper(std::ptr::null(), std::ptr::null(), std::ptr::null_mut());
1249        assert!(r >= 0, "exit code must be non-negative, got {}", r);
1250    }
1251
1252    /// c:212 — `ksh93_wrapper` exit code fits in u8 range (canonical
1253    /// shell exit code 0..256).
1254    #[test]
1255    fn ksh93_wrapper_exit_code_fits_u8() {
1256        let _g = crate::test_util::global_state_lock();
1257        let r = ksh93_wrapper(std::ptr::null(), std::ptr::null(), std::ptr::null_mut());
1258        assert!(
1259            (0..256).contains(&r),
1260            "exit code {} must fit in u8 range",
1261            r
1262        );
1263    }
1264
1265    /// c:499 — `features_` returns i32 (compile-time pin).
1266    #[test]
1267    fn ksh93_features_returns_i32_type() {
1268        let _g = crate::test_util::global_state_lock();
1269        let mut v: Vec<String> = Vec::new();
1270        let _: i32 = features_(std::ptr::null(), &mut v);
1271    }
1272
1273    /// c:507 — `enables_` returns i32 (compile-time pin).
1274    #[test]
1275    fn ksh93_enables_with_none_returns_i32() {
1276        let _g = crate::test_util::global_state_lock();
1277        let mut e: Option<Vec<i32>> = None;
1278        let _: i32 = enables_(std::ptr::null(), &mut e);
1279    }
1280
1281    /// c:491 — `setup_` is idempotent across many calls (alt).
1282    #[test]
1283    fn ksh93_setup_idempotent_many_call_alt() {
1284        let _g = crate::test_util::global_state_lock();
1285        for _ in 0..20 {
1286            assert_eq!(setup_(std::ptr::null()), 0);
1287        }
1288    }
1289
1290    /// c:514 — `boot_` is idempotent across many calls (alt).
1291    #[test]
1292    fn ksh93_boot_idempotent_many_call_alt() {
1293        let _g = crate::test_util::global_state_lock();
1294        for _ in 0..20 {
1295            assert_eq!(boot_(std::ptr::null()), 0);
1296        }
1297    }
1298
1299    /// c:536 — `cleanup_` is idempotent across many calls (alt).
1300    #[test]
1301    fn ksh93_cleanup_idempotent_many_call_alt() {
1302        let _g = crate::test_util::global_state_lock();
1303        for _ in 0..20 {
1304            assert_eq!(cleanup_(std::ptr::null()), 0);
1305        }
1306    }
1307
1308    /// c:595 — `finish_` is idempotent across many calls (alt).
1309    #[test]
1310    fn ksh93_finish_idempotent_many_call_alt() {
1311        let _g = crate::test_util::global_state_lock();
1312        for _ in 0..20 {
1313            assert_eq!(finish_(std::ptr::null()), 0);
1314        }
1315    }
1316
1317    /// c:491/499/507/514/536/595 — each lifecycle hook returns 0
1318    /// individually (tighter failure resolution).
1319    #[test]
1320    fn ksh93_each_lifecycle_hook_returns_zero_individually() {
1321        let _g = crate::test_util::global_state_lock();
1322        let null = std::ptr::null();
1323        let mut v: Vec<String> = Vec::new();
1324        let mut e: Option<Vec<i32>> = None;
1325        assert_eq!(setup_(null), 0, "c:491 setup_");
1326        assert_eq!(features_(null, &mut v), 0, "c:499 features_");
1327        assert_eq!(enables_(null, &mut e), 0, "c:507 enables_");
1328        assert_eq!(boot_(null), 0, "c:514 boot_");
1329        assert_eq!(cleanup_(null), 0, "c:536 cleanup_");
1330        assert_eq!(finish_(null), 0, "c:595 finish_");
1331    }
1332
1333    // ═══════════════════════════════════════════════════════════════════
1334    // Additional C-parity pins for Src/Modules/ksh93.c
1335    // c:47 edcharsetfn / c:83 matchgetfn / c:212 ksh93_wrapper /
1336    // c:491-595 lifecycle hooks
1337    // ═══════════════════════════════════════════════════════════════════
1338
1339    /// c:491 — `setup_` return type i32 (compile-time pin, alt).
1340    #[test]
1341    fn ksh93_setup_returns_i32_type_alt_pin() {
1342        let _g = crate::test_util::global_state_lock();
1343        let _: i32 = setup_(std::ptr::null());
1344    }
1345
1346    /// c:536 — `cleanup_` return type i32 (compile-time pin).
1347    #[test]
1348    fn ksh93_cleanup_returns_i32_type_alt_pin() {
1349        let _g = crate::test_util::global_state_lock();
1350        let _: i32 = cleanup_(std::ptr::null());
1351    }
1352
1353    /// c:595 — `finish_` return type i32 (compile-time pin).
1354    #[test]
1355    fn ksh93_finish_returns_i32_type_alt_pin() {
1356        let _g = crate::test_util::global_state_lock();
1357        let _: i32 = finish_(std::ptr::null());
1358    }
1359
1360    /// c:514 — `boot_` return type i32 (compile-time pin).
1361    #[test]
1362    fn ksh93_boot_returns_i32_type_alt_pin() {
1363        let _g = crate::test_util::global_state_lock();
1364        let _: i32 = boot_(std::ptr::null());
1365    }
1366
1367    /// c:83 — `matchgetfn(null)` returns Vec<String> safely.
1368    #[test]
1369    fn matchgetfn_null_pm_returns_vec_string() {
1370        let _g = crate::test_util::global_state_lock();
1371        let r = matchgetfn(std::ptr::null_mut());
1372        let _: Vec<String> = r;
1373    }
1374
1375    /// c:83 — `matchgetfn` is deterministic for null pm.
1376    #[test]
1377    fn matchgetfn_null_pm_deterministic() {
1378        let _g = crate::test_util::global_state_lock();
1379        let a = matchgetfn(std::ptr::null_mut());
1380        let b = matchgetfn(std::ptr::null_mut());
1381        assert_eq!(a, b, "matchgetfn(null) must be deterministic");
1382    }
1383
1384    /// c:47 — `edcharsetfn(null, null)` doesn't panic.
1385    #[test]
1386    fn edcharsetfn_null_inputs_no_panic() {
1387        let _g = crate::test_util::global_state_lock();
1388        edcharsetfn(std::ptr::null_mut(), std::ptr::null_mut());
1389    }
1390
1391    /// c:212 — `ksh93_wrapper` with all null inputs doesn't panic.
1392    #[test]
1393    fn ksh93_wrapper_all_null_inputs_no_panic() {
1394        let _g = crate::test_util::global_state_lock();
1395        let name = std::ffi::CString::new("test").unwrap();
1396        let _ = ksh93_wrapper(
1397            std::ptr::null(),
1398            std::ptr::null(),
1399            name.as_ptr() as *mut libc::c_char,
1400        );
1401    }
1402
1403    /// c:212 — `ksh93_wrapper` return type i32 (compile-time pin, alt).
1404    #[test]
1405    fn ksh93_wrapper_returns_i32_type_alt() {
1406        let _g = crate::test_util::global_state_lock();
1407        let name = std::ffi::CString::new("test").unwrap();
1408        let _: i32 = ksh93_wrapper(
1409            std::ptr::null(),
1410            std::ptr::null(),
1411            name.as_ptr() as *mut libc::c_char,
1412        );
1413    }
1414
1415    /// c:499 — `features_` deterministic on null module.
1416    #[test]
1417    fn ksh93_features_deterministic_alt_pin() {
1418        let _g = crate::test_util::global_state_lock();
1419        let mut v1: Vec<String> = Vec::new();
1420        let mut v2: Vec<String> = Vec::new();
1421        let _ = features_(std::ptr::null(), &mut v1);
1422        let _ = features_(std::ptr::null(), &mut v2);
1423        assert_eq!(v1, v2, "features_ must be deterministic");
1424    }
1425
1426    /// c:507 — `enables_` with Some(non-empty) doesn't panic.
1427    #[test]
1428    fn ksh93_enables_with_some_non_empty_no_panic() {
1429        let _g = crate::test_util::global_state_lock();
1430        let mut e: Option<Vec<i32>> = Some(vec![1, 2, 3]);
1431        let _ = enables_(std::ptr::null(), &mut e);
1432    }
1433
1434    /// c:491/514/595 — setup→boot→finish chain returns 0 each.
1435    #[test]
1436    fn ksh93_setup_boot_finish_chain_returns_zero_each() {
1437        let _g = crate::test_util::global_state_lock();
1438        let null = std::ptr::null();
1439        assert_eq!(setup_(null), 0);
1440        assert_eq!(boot_(null), 0);
1441        assert_eq!(finish_(null), 0);
1442    }
1443}